@hiai-gg/docsmint 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +171 -0
- package/README.md +348 -0
- package/backend/src/lib/logger.ts +18 -0
- package/backend/src/lib/redis-factory.ts +40 -0
- package/backend/src/lib/storage-factory.ts +56 -0
- package/frontend/src/lib/components/editor/shared-document.ts +237 -0
- package/frontend/src/lib/extensions/context.ts +60 -0
- package/frontend/src/lib/extensions/doc-tabs.ts +18 -0
- package/frontend/src/lib/extensions/resolve.ts +48 -0
- package/frontend/src/lib/extensions/types.ts +202 -0
- package/frontend/src/lib/hosts/DocsmintSharedDocumentHost.svelte +65 -0
- package/frontend/src/lib/hosts/HiaiDocsDashboardHost.svelte +1007 -0
- package/frontend/src/lib/hosts/HiaiDocsExtensionProvider.svelte +20 -0
- package/frontend/src/lib/hosts/HiaiDocsSearchHost.svelte +996 -0
- package/frontend/src/lib/hosts/index.ts +25 -0
- package/frontend/src/lib/index.ts +65 -0
- package/frontend/src/lib/stores/doc-tab-registry.svelte.ts +68 -0
- package/package.json +178 -0
- package/packages/cli/src/client.ts +271 -0
- package/packages/cli/src/commands/config.ts +47 -0
- package/packages/cli/src/commands/create.ts +35 -0
- package/packages/cli/src/commands/delete.ts +37 -0
- package/packages/cli/src/commands/export.ts +36 -0
- package/packages/cli/src/commands/folders.ts +88 -0
- package/packages/cli/src/commands/history.ts +55 -0
- package/packages/cli/src/commands/list.ts +61 -0
- package/packages/cli/src/commands/read.ts +38 -0
- package/packages/cli/src/commands/restore.ts +30 -0
- package/packages/cli/src/commands/search.ts +56 -0
- package/packages/cli/src/commands/snapshot.ts +35 -0
- package/packages/cli/src/commands/update.ts +54 -0
- package/packages/cli/src/config.ts +83 -0
- package/packages/cli/src/format.ts +153 -0
- package/packages/cli/src/index.ts +73 -0
- package/packages/db/src/client.ts +20 -0
- package/packages/db/src/index.ts +5 -0
- package/packages/db/src/schema.ts +692 -0
- package/packages/db/src/with-tenant.ts +75 -0
- package/packages/mcp-server/src/client.ts +172 -0
- package/packages/mcp-server/src/index.ts +109 -0
- package/packages/mcp-server/src/tools/create-document.ts +32 -0
- package/packages/mcp-server/src/tools/create-folder.ts +24 -0
- package/packages/mcp-server/src/tools/create-snapshot.ts +30 -0
- package/packages/mcp-server/src/tools/export-document.ts +22 -0
- package/packages/mcp-server/src/tools/get-document.ts +20 -0
- package/packages/mcp-server/src/tools/list-documents.ts +42 -0
- package/packages/mcp-server/src/tools/list-folders.ts +25 -0
- package/packages/mcp-server/src/tools/search.ts +42 -0
- package/packages/mcp-server/src/tools/update-document.ts +30 -0
- package/packages/mcp-server/src/tools/version-history.ts +32 -0
- package/packages/mcp-server/src/types.ts +126 -0
- package/packages/sdk/dist/client.d.ts +187 -0
- package/packages/sdk/dist/client.js +568 -0
- package/packages/sdk/dist/index.d.ts +3 -0
- package/packages/sdk/dist/index.js +1 -0
- package/packages/sdk/dist/types.d.ts +391 -0
- package/packages/sdk/dist/types.js +8 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs delete <id>` — soft confirmation before issuing DELETE.
|
|
3
|
+
*
|
|
4
|
+
* The backend has no soft-delete; this is irreversible, so we
|
|
5
|
+
* prompt unless the user passes `--yes` (or `-y`). The prompt is
|
|
6
|
+
* skipped automatically when stdin isn't a TTY (e.g. piping from
|
|
7
|
+
* CI) so the command remains scriptable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Command } from "commander";
|
|
11
|
+
import { isatty } from "node:tty";
|
|
12
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
13
|
+
import { confirm, formatError, green } from "../format.js";
|
|
14
|
+
|
|
15
|
+
export function registerDelete(program: Command, _getClient: () => HiaiDocsClient) {
|
|
16
|
+
program
|
|
17
|
+
.command("delete <id>")
|
|
18
|
+
.description("Delete a document (irreversible)")
|
|
19
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
20
|
+
.action(async (id: string, opts: { yes?: boolean }) => {
|
|
21
|
+
try {
|
|
22
|
+
const interactive = isatty(0) && isatty(1);
|
|
23
|
+
if (interactive && !opts.yes) {
|
|
24
|
+
const ok = await confirm(`Delete document ${id}? This cannot be undone.`);
|
|
25
|
+
if (!ok) {
|
|
26
|
+
process.stdout.write("Cancelled.\n");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
await client.deleteDocument(id);
|
|
31
|
+
process.stdout.write(`${green("✓")} Deleted ${id}\n`);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs export <id>` — write a document's markdown body.
|
|
3
|
+
*
|
|
4
|
+
* Defaults to stdout for piping. `--output <file>` writes to a
|
|
5
|
+
* file; the existing file is overwritten silently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
9
|
+
import type { Command } from "commander";
|
|
10
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
11
|
+
import { formatError, green } from "../format.js";
|
|
12
|
+
|
|
13
|
+
export function registerExport(program: Command, _getClient: () => HiaiDocsClient) {
|
|
14
|
+
program
|
|
15
|
+
.command("export <id>")
|
|
16
|
+
.description("Export a document's markdown")
|
|
17
|
+
.option("-o, --output <file>", "Write to file (default: stdout)")
|
|
18
|
+
.action(async (id: string, opts: { output?: string }) => {
|
|
19
|
+
try {
|
|
20
|
+
const md = await client.exportDocument(id);
|
|
21
|
+
if (opts.output) {
|
|
22
|
+
if (existsSync(opts.output)) {
|
|
23
|
+
// Confirm before clobbering — but stay scriptable.
|
|
24
|
+
process.stderr.write(`Overwriting ${opts.output}\n`);
|
|
25
|
+
}
|
|
26
|
+
writeFileSync(opts.output, md, "utf-8");
|
|
27
|
+
process.stdout.write(`${green("✓")} Wrote ${opts.output}\n`);
|
|
28
|
+
} else {
|
|
29
|
+
process.stdout.write(md);
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs folders` — render a folder tree.
|
|
3
|
+
*
|
|
4
|
+
* The backend exposes a flat "children of parent" listing; to render
|
|
5
|
+
* the full tree we walk recursively from each root and recurse into
|
|
6
|
+
* children. Output uses ASCII glyphs so it stays readable when piped
|
|
7
|
+
* to files/logs without a TTY.
|
|
8
|
+
*
|
|
9
|
+
* Also registers `folder-create` — a separate subcommand per the
|
|
10
|
+
* spec, since `folders` is a read-only listing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Command } from "commander";
|
|
14
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
15
|
+
import { formatError, green, renderFolderTree } from "../format.js";
|
|
16
|
+
|
|
17
|
+
export function registerFolders(program: Command, _getClient: () => HiaiDocsClient) {
|
|
18
|
+
program
|
|
19
|
+
.command("folders")
|
|
20
|
+
.description("List folders (tree)")
|
|
21
|
+
.option("-p, --parent <uuid>", "Show children of a specific folder")
|
|
22
|
+
.action(async (opts: { parent?: string }) => {
|
|
23
|
+
try {
|
|
24
|
+
// The root listing returns folders with parentId IS NULL.
|
|
25
|
+
// To render a tree we fetch the full set once and walk in-memory.
|
|
26
|
+
// Backend doesn't have a "list all folders" endpoint — `folders`
|
|
27
|
+
// is keyed by parentId — so we fetch root + recurse.
|
|
28
|
+
const root = await client.listFolders({
|
|
29
|
+
parentId: opts.parent === undefined ? undefined : opts.parent,
|
|
30
|
+
});
|
|
31
|
+
if (root.length === 0) {
|
|
32
|
+
process.stdout.write("No folders.\n");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Gather everything for tree rendering when listing from root.
|
|
36
|
+
let all = root;
|
|
37
|
+
if (opts.parent === undefined) {
|
|
38
|
+
all = await collectAllFolders(root);
|
|
39
|
+
}
|
|
40
|
+
const tree = renderFolderTree(all, {
|
|
41
|
+
parentId: opts.parent ?? null,
|
|
42
|
+
});
|
|
43
|
+
process.stdout.write(`${tree}\n`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command("folder-create")
|
|
52
|
+
.description("Create a new folder")
|
|
53
|
+
.requiredOption("-n, --name <name>", "Folder name (1-255 chars)")
|
|
54
|
+
.option("-p, --parent <uuid>", "Parent folder id")
|
|
55
|
+
.action(async (opts: { name: string; parent?: string }) => {
|
|
56
|
+
try {
|
|
57
|
+
const folder = await client.createFolder({
|
|
58
|
+
name: opts.name,
|
|
59
|
+
parentId: opts.parent,
|
|
60
|
+
});
|
|
61
|
+
process.stdout.write(`${folder.id}\n`);
|
|
62
|
+
process.stdout.write(`${green("✓")} Created folder "${folder.name}"\n`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function collectAllFolders(
|
|
71
|
+
initial: Awaited<ReturnType<typeof client.listFolders>>,
|
|
72
|
+
): Promise<Awaited<ReturnType<typeof client.listFolders>>> {
|
|
73
|
+
const seen = new Map<string, (typeof initial)[number]>();
|
|
74
|
+
for (const f of initial) seen.set(f.id, f);
|
|
75
|
+
const queue: string[] = initial.map((f) => f.id);
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
const id = queue.shift();
|
|
78
|
+
if (!id) break;
|
|
79
|
+
const children = await client.listFolders({ parentId: id });
|
|
80
|
+
for (const child of children) {
|
|
81
|
+
if (!seen.has(child.id)) {
|
|
82
|
+
seen.set(child.id, child);
|
|
83
|
+
queue.push(child.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return Array.from(seen.values());
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs history <id>` — list versions/snapshots for a document.
|
|
3
|
+
*
|
|
4
|
+
* Default behavior is to show everything (snapshots + auto-saved
|
|
5
|
+
* versions). `--snapshots-only` narrows to named snapshots, which
|
|
6
|
+
* is what most users care about.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Command } from "commander";
|
|
10
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
11
|
+
import { formatError, renderTable } from "../format.js";
|
|
12
|
+
|
|
13
|
+
export function registerHistory(program: Command, _getClient: () => HiaiDocsClient) {
|
|
14
|
+
program
|
|
15
|
+
.command("history <id>")
|
|
16
|
+
.description("List a document's version history")
|
|
17
|
+
.option("-s, --snapshots-only", "Only show named snapshots")
|
|
18
|
+
.action(async (id: string, opts: { snapshotsOnly?: boolean }) => {
|
|
19
|
+
try {
|
|
20
|
+
const rows = await client.listVersions(id, opts.snapshotsOnly);
|
|
21
|
+
if (rows.length === 0) {
|
|
22
|
+
process.stdout.write("No history.\n");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const table = renderTable(rows, [
|
|
26
|
+
{ header: "ID", width: 36, get: (r) => r.id },
|
|
27
|
+
{
|
|
28
|
+
header: "TYPE",
|
|
29
|
+
width: 12,
|
|
30
|
+
get: (r) => (r.isSnapshot ? "snapshot" : "auto"),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
header: "LABEL",
|
|
34
|
+
width: 30,
|
|
35
|
+
get: (r) => r.label ?? "-",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
header: "CREATED",
|
|
39
|
+
width: 22,
|
|
40
|
+
get: (r) => r.createdAt,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
header: "RESTORED FROM",
|
|
44
|
+
width: 36,
|
|
45
|
+
get: (r) => r.restoredFrom ?? "-",
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
process.stdout.write(`${table}\n`);
|
|
49
|
+
process.stdout.write(`\n${rows.length} version(s)\n`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs list` — paginated document listing.
|
|
3
|
+
*
|
|
4
|
+
* Calls GET /api/documents with optional folder/tag filters and prints
|
|
5
|
+
* a table with title, updated timestamp, and folder id.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Command } from "commander";
|
|
9
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
10
|
+
import { formatError, renderTable } from "../format.js";
|
|
11
|
+
|
|
12
|
+
export function registerList(program: Command, _getClient: () => HiaiDocsClient) {
|
|
13
|
+
program
|
|
14
|
+
.command("list")
|
|
15
|
+
.description("List documents (paginated)")
|
|
16
|
+
.option("-f, --folder <uuid>", "Filter by folder id")
|
|
17
|
+
.option("-t, --tag <uuid>", "Filter by tag id")
|
|
18
|
+
.option("-p, --page <n>", "Page number", (v) => Number.parseInt(v, 10))
|
|
19
|
+
.option("-l, --limit <n>", "Items per page (1-100)", (v) => Number.parseInt(v, 10))
|
|
20
|
+
.action(
|
|
21
|
+
async (opts: { folder?: string; tag?: string; page?: number; limit?: number }) => {
|
|
22
|
+
try {
|
|
23
|
+
const res = await client.listDocuments({
|
|
24
|
+
folderId: opts.folder,
|
|
25
|
+
tag: opts.tag,
|
|
26
|
+
page: opts.page,
|
|
27
|
+
limit: opts.limit,
|
|
28
|
+
});
|
|
29
|
+
if (res.items.length === 0) {
|
|
30
|
+
process.stdout.write("No documents.\n");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const table = renderTable(res.items, [
|
|
34
|
+
{ header: "ID", width: 36, get: (r) => r.id },
|
|
35
|
+
{ header: "TITLE", width: 50, get: (r) => r.title },
|
|
36
|
+
{ header: "UPDATED", width: 22, get: (r) => formatDate(r.updatedAt) },
|
|
37
|
+
{ header: "FOLDER", width: 36, get: (r) => r.folderId ?? "-" },
|
|
38
|
+
]);
|
|
39
|
+
process.stdout.write(`${table}\n`);
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
`\nPage ${res.page} — ${res.items.length} of ${res.total} document(s)\n`,
|
|
42
|
+
);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatDate(iso: string): string {
|
|
52
|
+
const d = new Date(iso);
|
|
53
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
54
|
+
// YYYY-MM-DD HH:MM — short, sortable, no locale surprises.
|
|
55
|
+
const yyyy = d.getUTCFullYear();
|
|
56
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
57
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
58
|
+
const hh = String(d.getUTCHours()).padStart(2, "0");
|
|
59
|
+
const mi = String(d.getUTCMinutes()).padStart(2, "0");
|
|
60
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs read <id>` — fetch a single document and print its body.
|
|
3
|
+
*
|
|
4
|
+
* Title + metadata block first, then the raw markdown body to stdout.
|
|
5
|
+
* Pipe-friendly: emitting markdown only would force callers to either
|
|
6
|
+
* parse the metadata out or accept the whole blob.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Command } from "commander";
|
|
10
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
11
|
+
import { dim, formatError } from "../format.js";
|
|
12
|
+
|
|
13
|
+
export function registerRead(program: Command, _getClient: () => HiaiDocsClient) {
|
|
14
|
+
program
|
|
15
|
+
.command("read <id>")
|
|
16
|
+
.description("Read a document (title + markdown body)")
|
|
17
|
+
.action(async (id: string) => {
|
|
18
|
+
try {
|
|
19
|
+
const doc = await client.getDocument(id);
|
|
20
|
+
const tagList = (doc.tags ?? [])
|
|
21
|
+
.map((t) => t.name)
|
|
22
|
+
.join(", ");
|
|
23
|
+
const meta = [
|
|
24
|
+
`id: ${doc.id}`,
|
|
25
|
+
`updated: ${doc.updatedAt}`,
|
|
26
|
+
doc.folderId ? `folder: ${doc.folderId}` : null,
|
|
27
|
+
tagList ? `tags: ${tagList}` : null,
|
|
28
|
+
]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join("\n");
|
|
31
|
+
process.stdout.write(`# ${doc.title}\n\n${dim(meta)}\n\n`);
|
|
32
|
+
process.stdout.write(`${doc.content ?? ""}\n`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs restore <id> --version <vid>` — restore a prior version.
|
|
3
|
+
*
|
|
4
|
+
* The backend saves an auto-backup of the current content before
|
|
5
|
+
* overwriting, so this command is always reversible. The CLI doesn't
|
|
6
|
+
* add a confirmation prompt: the server-side auto-backup is the
|
|
7
|
+
* safety net.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Command } from "commander";
|
|
11
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
12
|
+
import { formatError, green } from "../format.js";
|
|
13
|
+
|
|
14
|
+
export function registerRestore(program: Command, _getClient: () => HiaiDocsClient) {
|
|
15
|
+
program
|
|
16
|
+
.command("restore <id>")
|
|
17
|
+
.description("Restore a prior version (auto-backup is taken first)")
|
|
18
|
+
.requiredOption("-v, --version <vid>", "Version id to restore")
|
|
19
|
+
.action(async (id: string, opts: { version: string }) => {
|
|
20
|
+
try {
|
|
21
|
+
await client.restoreVersion(id, opts.version);
|
|
22
|
+
process.stdout.write(
|
|
23
|
+
`${green("✓")} Restored ${id} to version ${opts.version}\n`,
|
|
24
|
+
);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs search <query>` — hybrid full-text + semantic search.
|
|
3
|
+
*
|
|
4
|
+
* Calls GET /api/search with optional folder/tags/limit filters and
|
|
5
|
+
* prints a score-ranked table.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Command } from "commander";
|
|
9
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
10
|
+
import { formatError, renderTable } from "../format.js";
|
|
11
|
+
|
|
12
|
+
export function registerSearch(program: Command, _getClient: () => HiaiDocsClient) {
|
|
13
|
+
program
|
|
14
|
+
.command("search <query>")
|
|
15
|
+
.description("Search documents (hybrid full-text + semantic)")
|
|
16
|
+
.option("-l, --limit <n>", "Max results (1-100)", (v) => Number.parseInt(v, 10))
|
|
17
|
+
.option("-f, --folder <uuid>", "Restrict to a folder")
|
|
18
|
+
.option("-t, --tags <list>", "Comma-separated tag names")
|
|
19
|
+
.action(async (query: string, opts: { limit?: number; folder?: string; tags?: string }) => {
|
|
20
|
+
try {
|
|
21
|
+
const tags = opts.tags
|
|
22
|
+
? opts.tags
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((t) => t.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
: undefined;
|
|
27
|
+
const res = await client.search({
|
|
28
|
+
query,
|
|
29
|
+
folder: opts.folder,
|
|
30
|
+
tags,
|
|
31
|
+
limit: opts.limit,
|
|
32
|
+
});
|
|
33
|
+
if (res.items.length === 0) {
|
|
34
|
+
process.stdout.write("No matches.\n");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const table = renderTable(res.items, [
|
|
38
|
+
{ header: "ID", width: 36, get: (r) => r.id },
|
|
39
|
+
{ header: "TITLE", width: 40, get: (r) => r.title },
|
|
40
|
+
{ header: "SCORE", width: 7, get: (r) => r.score.toFixed(3), align: "right" },
|
|
41
|
+
{ header: "SNIPPET", width: 60, get: (r) => oneLineForCli(r.snippet) },
|
|
42
|
+
]);
|
|
43
|
+
process.stdout.write(`${table}\n`);
|
|
44
|
+
process.stdout.write(`\n${res.items.length} of ${res.total} result(s)\n`);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Local helper to avoid pulling in format.ts just for oneLine.
|
|
53
|
+
function oneLineForCli(value: string): string {
|
|
54
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
55
|
+
return flat.length > 60 ? `${flat.slice(0, 59)}…` : flat;
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs snapshot <id>` — create a named, immutable snapshot of
|
|
3
|
+
* the document's current state.
|
|
4
|
+
*
|
|
5
|
+
* Snapshots are stored as `versions` rows with `isSnapshot=true`
|
|
6
|
+
* (see backend/src/api/routes/versions.ts). They survive the
|
|
7
|
+
* auto-prune that keeps ordinary auto-saved versions bounded.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Command } from "commander";
|
|
11
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
12
|
+
import { formatError, green } from "../format.js";
|
|
13
|
+
|
|
14
|
+
export function registerSnapshot(program: Command, _getClient: () => HiaiDocsClient) {
|
|
15
|
+
program
|
|
16
|
+
.command("snapshot <id>")
|
|
17
|
+
.description("Create a named snapshot of a document")
|
|
18
|
+
.requiredOption("-n, --name <label>", "Snapshot label (1-200 chars)")
|
|
19
|
+
.option("-d, --description <text>", "Snapshot description")
|
|
20
|
+
.action(async (id: string, opts: { name: string; description?: string }) => {
|
|
21
|
+
try {
|
|
22
|
+
const snap = await client.createSnapshot(id, {
|
|
23
|
+
label: opts.name,
|
|
24
|
+
description: opts.description,
|
|
25
|
+
});
|
|
26
|
+
process.stdout.write(`${snap.id} (${snap.label})\n`);
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
`${green("✓")} Snapshot created for ${id}\n`,
|
|
29
|
+
);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hiai-docs update <id>` — patch a document's title and/or content.
|
|
3
|
+
*
|
|
4
|
+
* Empty body is rejected by the backend; the CLI mirrors that
|
|
5
|
+
* constraint early to avoid a wasted round trip.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Command } from "commander";
|
|
9
|
+
import { client, type HiaiDocsClient } from "../client.js";
|
|
10
|
+
import { formatError, green } from "../format.js";
|
|
11
|
+
|
|
12
|
+
export function registerUpdate(program: Command, _getClient: () => HiaiDocsClient) {
|
|
13
|
+
program
|
|
14
|
+
.command("update <id>")
|
|
15
|
+
.description("Update a document's title and/or content")
|
|
16
|
+
.option("--title <title>", "New title")
|
|
17
|
+
.option("-c, --content <markdown>", "New content")
|
|
18
|
+
.option("-f, --folder <uuid>", "Move to folder (use '' to clear)")
|
|
19
|
+
.action(
|
|
20
|
+
async (
|
|
21
|
+
id: string,
|
|
22
|
+
opts: { title?: string; content?: string; folder?: string },
|
|
23
|
+
) => {
|
|
24
|
+
try {
|
|
25
|
+
if (
|
|
26
|
+
opts.title === undefined &&
|
|
27
|
+
opts.content === undefined &&
|
|
28
|
+
opts.folder === undefined
|
|
29
|
+
) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
"At least one of --title, --content, or --folder is required.\n",
|
|
32
|
+
);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const folder =
|
|
37
|
+
opts.folder === undefined
|
|
38
|
+
? undefined
|
|
39
|
+
: opts.folder === ""
|
|
40
|
+
? null
|
|
41
|
+
: opts.folder;
|
|
42
|
+
await client.updateDocument(id, {
|
|
43
|
+
title: opts.title,
|
|
44
|
+
content: opts.content,
|
|
45
|
+
folderId: folder,
|
|
46
|
+
});
|
|
47
|
+
process.stdout.write(`${green("✓")} Updated ${id}\n`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader for the hiai-docs CLI.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order (highest priority first):
|
|
5
|
+
* 1. Environment variables (HIAI_DOCS_URL, HIAI_DOCS_API_KEY)
|
|
6
|
+
* 2. JSON file at ~/.hiai-docs/config.json
|
|
7
|
+
* 3. Built-in defaults
|
|
8
|
+
*
|
|
9
|
+
* The file config persists across invocations, so users can run
|
|
10
|
+
* `hiai-docs config --url <url> --key <key>` once and reuse the
|
|
11
|
+
* values. Env vars are intended for CI/ephemeral contexts where
|
|
12
|
+
* writing to disk is undesirable.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
chmodSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
export interface Config {
|
|
26
|
+
url: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CONFIG_DIR = join(homedir(), ".hiai-docs");
|
|
31
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
32
|
+
const DEFAULT_URL = "http://localhost:50700";
|
|
33
|
+
|
|
34
|
+
/** Enforce owner-only access for a config directory and its optional file. */
|
|
35
|
+
export function enforceConfigPermissions(
|
|
36
|
+
configDir: string,
|
|
37
|
+
configFile: string,
|
|
38
|
+
): void {
|
|
39
|
+
if (process.platform === "win32") return;
|
|
40
|
+
if (existsSync(configDir)) chmodSync(configDir, 0o700);
|
|
41
|
+
if (existsSync(configFile)) chmodSync(configFile, 0o600);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadConfig(): Config {
|
|
45
|
+
enforceConfigPermissions(CONFIG_DIR, CONFIG_FILE);
|
|
46
|
+
const envUrl = process.env.HIAI_DOCS_URL;
|
|
47
|
+
const envKey = process.env.HIAI_DOCS_API_KEY;
|
|
48
|
+
|
|
49
|
+
let file: Partial<Config> = {};
|
|
50
|
+
if (existsSync(CONFIG_FILE)) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
53
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
54
|
+
if (parsed && typeof parsed === "object") {
|
|
55
|
+
file = parsed as Partial<Config>;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Corrupted config — fall back to defaults/env. Don't throw:
|
|
59
|
+
// the user should still be able to run `config` to repair it.
|
|
60
|
+
file = {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
url: envUrl ?? file.url ?? DEFAULT_URL,
|
|
66
|
+
apiKey: envKey ?? file.apiKey ?? "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveConfig(cfg: Config): void {
|
|
71
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
72
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
73
|
+
}
|
|
74
|
+
enforceConfigPermissions(CONFIG_DIR, CONFIG_FILE);
|
|
75
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
76
|
+
// writeFileSync preserves the mode of an existing file, so correct it after
|
|
77
|
+
// every write as well as before reading it.
|
|
78
|
+
enforceConfigPermissions(CONFIG_DIR, CONFIG_FILE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function configFilePath(): string {
|
|
82
|
+
return CONFIG_FILE;
|
|
83
|
+
}
|