aboutsummaryrefslogtreecommitdiffstats
path: root/apps/cli
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--apps/cli/package.json6
-rw-r--r--apps/cli/src/commands/admin.ts89
-rw-r--r--apps/cli/src/commands/bookmarks.ts89
-rw-r--r--apps/cli/src/commands/lists.ts11
-rw-r--r--apps/cli/src/commands/migrate.ts1
-rw-r--r--apps/cli/src/index.ts2
6 files changed, 183 insertions, 15 deletions
diff --git a/apps/cli/package.json b/apps/cli/package.json
index e0e9b188..04c72f81 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@karakeep/cli",
- "version": "0.29.0",
+ "version": "0.30.0",
"description": "Command Line Interface (CLI) for Karakeep",
"license": "GNU Affero General Public License version 3",
"type": "module",
@@ -20,8 +20,8 @@
"@karakeep/shared": "workspace:^0.1.0",
"@karakeep/trpc": "workspace:^0.1.0",
"@karakeep/tsconfig": "workspace:^0.1.0",
- "@trpc/client": "^11.4.3",
- "@trpc/server": "^11.4.3",
+ "@trpc/client": "^11.9.0",
+ "@trpc/server": "^11.9.0",
"@tsconfig/node22": "^22.0.0",
"chalk": "^5.3.0",
"commander": "^12.0.0",
diff --git a/apps/cli/src/commands/admin.ts b/apps/cli/src/commands/admin.ts
new file mode 100644
index 00000000..181126f0
--- /dev/null
+++ b/apps/cli/src/commands/admin.ts
@@ -0,0 +1,89 @@
+import { getGlobalOptions } from "@/lib/globals";
+import { printErrorMessageWithReason, printObject } from "@/lib/output";
+import { getAPIClient } from "@/lib/trpc";
+import { Command } from "@commander-js/extra-typings";
+import { getBorderCharacters, table } from "table";
+
+export const adminCmd = new Command()
+ .name("admin")
+ .description("admin commands");
+
+function toHumanReadableSize(size: number): string {
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
+ if (size === 0) return "0 Bytes";
+ const i = Math.floor(Math.log(size) / Math.log(1024));
+ return (size / Math.pow(1024, i)).toFixed(2) + " " + sizes[i];
+}
+
+const usersCmd = new Command()
+ .name("users")
+ .description("user management commands");
+
+usersCmd
+ .command("list")
+ .description("list all users")
+ .action(async () => {
+ const api = getAPIClient();
+
+ try {
+ const [usersResp, userStats] = await Promise.all([
+ api.users.list.query(),
+ api.admin.userStats.query(),
+ ]);
+
+ if (getGlobalOptions().json) {
+ printObject({
+ users: usersResp.users.map((u) => ({
+ ...u,
+ numBookmarks: userStats[u.id]?.numBookmarks ?? 0,
+ assetSizes: userStats[u.id]?.assetSizes ?? 0,
+ })),
+ });
+ } else {
+ const data: string[][] = [
+ [
+ "Name",
+ "Email",
+ "Num Bookmarks",
+ "Asset Sizes",
+ "Role",
+ "Local User",
+ ],
+ ];
+
+ usersResp.users.forEach((user) => {
+ const stats = userStats[user.id] ?? {
+ numBookmarks: 0,
+ assetSizes: 0,
+ };
+
+ const numBookmarksDisplay = `${stats.numBookmarks} / ${user.bookmarkQuota?.toString() ?? "Unlimited"}`;
+ const assetSizesDisplay = `${toHumanReadableSize(stats.assetSizes)} / ${user.storageQuota ? toHumanReadableSize(user.storageQuota) : "Unlimited"}`;
+
+ data.push([
+ user.name,
+ user.email,
+ numBookmarksDisplay,
+ assetSizesDisplay,
+ user.role ?? "",
+ user.localUser ? "✓" : "✗",
+ ]);
+ });
+
+ console.log(
+ table(data, {
+ border: getBorderCharacters("ramac"),
+ drawHorizontalLine: (lineIndex, rowCount) => {
+ return (
+ lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount
+ );
+ },
+ }),
+ );
+ }
+ } catch (error) {
+ printErrorMessageWithReason("Failed to list all users", error as object);
+ }
+ });
+
+adminCmd.addCommand(usersCmd);
diff --git a/apps/cli/src/commands/bookmarks.ts b/apps/cli/src/commands/bookmarks.ts
index 021e344f..e2e8efb6 100644
--- a/apps/cli/src/commands/bookmarks.ts
+++ b/apps/cli/src/commands/bookmarks.ts
@@ -29,18 +29,10 @@ type Bookmark = Omit<ZBookmark, "tags"> & {
};
function normalizeBookmark(bookmark: ZBookmark): Bookmark {
- const ret = {
+ return {
...bookmark,
tags: bookmark.tags.map((t) => t.name),
};
-
- if (ret.content.type == BookmarkTypes.LINK && ret.content.htmlContent) {
- if (ret.content.htmlContent.length > 10) {
- ret.content.htmlContent =
- ret.content.htmlContent.substring(0, 10) + "... <CROPPED>";
- }
- }
- return ret;
}
function printBookmark(bookmark: ZBookmark) {
@@ -151,10 +143,15 @@ bookmarkCmd
.command("get")
.description("fetch information about a bookmark")
.argument("<id>", "The id of the bookmark to get")
- .action(async (id) => {
+ .option(
+ "--include-content",
+ "include full bookmark content in results",
+ false,
+ )
+ .action(async (id, opts) => {
const api = getAPIClient();
await api.bookmarks.getBookmark
- .query({ bookmarkId: id })
+ .query({ bookmarkId: id, includeContent: opts.includeContent })
.then(printBookmark)
.catch(printError(`Failed to get the bookmark with id "${id}"`));
});
@@ -254,6 +251,11 @@ bookmarkCmd
false,
)
.option("--list-id <id>", "if set, only items from that list will be fetched")
+ .option(
+ "--include-content",
+ "include full bookmark content in results",
+ false,
+ )
.action(async (opts) => {
const api = getAPIClient();
@@ -262,6 +264,7 @@ bookmarkCmd
listId: opts.listId,
limit: MAX_NUM_BOOKMARKS_PER_PAGE,
useCursorV2: true,
+ includeContent: opts.includeContent,
};
try {
@@ -282,6 +285,70 @@ bookmarkCmd
});
bookmarkCmd
+ .command("search")
+ .description("search bookmarks using query matchers")
+ .argument(
+ "<query>",
+ "the search query (supports matchers like tag:name, is:fav, etc.)",
+ )
+ .option(
+ "--limit <limit>",
+ "number of results per page",
+ (val) => parseInt(val, 10),
+ 50,
+ )
+ .option(
+ "--sort-order <order>",
+ "sort order for results",
+ (val) => {
+ if (val !== "relevance" && val !== "asc" && val !== "desc") {
+ throw new Error("sort-order must be one of: relevance, asc, desc");
+ }
+ return val;
+ },
+ "relevance",
+ )
+ .option(
+ "--include-content",
+ "include full bookmark content in results",
+ false,
+ )
+ .option("--all", "fetch all results (paginate through all pages)", false)
+ .action(async (query, opts) => {
+ const api = getAPIClient();
+
+ const request = {
+ text: query,
+ limit: opts.limit,
+ sortOrder: opts.sortOrder as "relevance" | "asc" | "desc",
+ includeContent: opts.includeContent,
+ };
+
+ try {
+ let resp = await api.bookmarks.searchBookmarks.query(request);
+ let results: ZBookmark[] = resp.bookmarks;
+
+ // If --all flag is set, fetch all pages
+ if (opts.all) {
+ while (resp.nextCursor) {
+ resp = await api.bookmarks.searchBookmarks.query({
+ ...request,
+ cursor: resp.nextCursor,
+ });
+ results = [...results, ...resp.bookmarks];
+ }
+ }
+
+ printObject(results.map(normalizeBookmark), { maxArrayLength: null });
+ } catch (error) {
+ printStatusMessage(false, "Failed to search bookmarks");
+ if (error instanceof Error) {
+ printStatusMessage(false, error.message);
+ }
+ }
+ });
+
+bookmarkCmd
.command("delete")
.description("delete a bookmark")
.argument("<id>", "the id of the bookmark to delete")
diff --git a/apps/cli/src/commands/lists.ts b/apps/cli/src/commands/lists.ts
index 864fa790..1d9341d7 100644
--- a/apps/cli/src/commands/lists.ts
+++ b/apps/cli/src/commands/lists.ts
@@ -86,15 +86,24 @@ listsCmd
.command("get")
.description("gets all the ids of the bookmarks assigned to the list")
.requiredOption("--list <id>", "the id of the list")
+ .option(
+ "--include-content",
+ "include full bookmark content in results",
+ false,
+ )
.action(async (opts) => {
const api = getAPIClient();
try {
- let resp = await api.bookmarks.getBookmarks.query({ listId: opts.list });
+ let resp = await api.bookmarks.getBookmarks.query({
+ listId: opts.list,
+ includeContent: opts.includeContent,
+ });
let results: string[] = resp.bookmarks.map((b) => b.id);
while (resp.nextCursor) {
resp = await api.bookmarks.getBookmarks.query({
listId: opts.list,
cursor: resp.nextCursor,
+ includeContent: opts.includeContent,
});
results = [...results, ...resp.bookmarks.map((b) => b.id)];
}
diff --git a/apps/cli/src/commands/migrate.ts b/apps/cli/src/commands/migrate.ts
index ee0d85c8..6527be23 100644
--- a/apps/cli/src/commands/migrate.ts
+++ b/apps/cli/src/commands/migrate.ts
@@ -695,6 +695,7 @@ async function migrateBookmarks(
summary: b.summary ?? undefined,
createdAt: b.createdAt,
crawlPriority: "low" as const,
+ source: b.source === null ? undefined : b.source,
};
let createdId: string | null = null;
switch (b.content.type) {
diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts
index df7d9512..8158c0b8 100644
--- a/apps/cli/src/index.ts
+++ b/apps/cli/src/index.ts
@@ -1,3 +1,4 @@
+import { adminCmd } from "@/commands/admin";
import { bookmarkCmd } from "@/commands/bookmarks";
import { dumpCmd } from "@/commands/dump";
import { listsCmd } from "@/commands/lists";
@@ -31,6 +32,7 @@ const program = new Command()
: "0.0.0",
);
+program.addCommand(adminCmd);
program.addCommand(bookmarkCmd);
program.addCommand(listsCmd);
program.addCommand(tagsCmd);