@thothica/docs-cli 1.0.0 → 1.1.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/README.md CHANGED
@@ -43,15 +43,20 @@ thd whoami Show the signed-in account
43
43
 
44
44
  thd doc ls [-q QUERY] [--shared] [--json] List / search documents
45
45
  thd doc new "Title" [--md FILE|-] Create (markdown body, or --template ID)
46
+ thd doc upload FILE.docx [--folder id|name] Upload an existing .docx (optional --title)
46
47
  thd doc cat ID [--text] Print content (markdown by default)
47
48
  thd doc get ID [-o FILE] Download the real .docx
48
- thd doc set ID --md FILE|- Replace content from markdown
49
+ thd doc set ID --md FILE|- [--force] Replace content from markdown
50
+ thd doc mv ID (--folder id|name | --root) Move a document into a folder
49
51
  thd doc rm ID [--permanent] Trash (or delete) a document
50
52
 
53
+ thd folder ls List your folders
54
+ thd folder new "Name" [--parent id|name] Create a folder
51
55
  thd comment ls ID List comments inside the document
52
56
  thd comment add ID "text" [--anchor "..."] Add a comment (optional --reply-to ID)
53
57
 
54
58
  thd templates List document templates
59
+ thd mcp-config [--client NAME] Print connector config for MCP clients
55
60
  thd open [ID] Open the app (or a document) in the browser
56
61
  thd mcp Run the MCP server (for AI agents) over stdio
57
62
  ```
package/bin/thd.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  // edit, and comment on documents from the terminal or an AI agent. Ships an MCP
4
4
  // server (`thd mcp`). Zero dependencies; Node >= 18.
5
5
  import { readFileSync, writeFileSync } from "node:fs";
6
+ import { basename } from "node:path";
6
7
  import {
7
8
  DEFAULT_ORIGIN,
8
9
  CLI_VERSION,
@@ -42,6 +43,7 @@ const BOOLEANS = new Set([
42
43
  "markdown",
43
44
  "text",
44
45
  "force",
46
+ "root",
45
47
  "no-open",
46
48
  "help",
47
49
  "version",
@@ -120,6 +122,22 @@ function readInput(spec) {
120
122
  return readFileSync(spec, "utf8");
121
123
  }
122
124
 
125
+ const DOCX_MIME =
126
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
127
+
128
+ // Resolve a --folder value that may be a folder id or a folder name.
129
+ async function resolveFolder(origin, token, value) {
130
+ const res = await call(origin, token, "GET", "/folders");
131
+ const folders = res.folders || [];
132
+ const byId = folders.find((f) => f.id === value);
133
+ if (byId) return byId.id;
134
+ const byName = folders.find(
135
+ (f) => f.name.toLowerCase() === String(value).toLowerCase()
136
+ );
137
+ if (byName) return byName.id;
138
+ die(`folder not found: "${value}". See 'thd folder ls', or 'thd folder new "${value}"'.`);
139
+ }
140
+
123
141
  // ---------------------------------------------------------------------------
124
142
  // Commands
125
143
  // ---------------------------------------------------------------------------
@@ -237,6 +255,35 @@ async function cmdDoc(flags, sub, rest) {
237
255
  out(green("✓ Saved ") + dim(`version ${res.version}`));
238
256
  return;
239
257
  }
258
+ case "upload": {
259
+ const file = rest[0];
260
+ if (!file) die("usage: thd doc upload FILE.docx [--folder id|name] [--title T]");
261
+ const buf = readFileSync(file);
262
+ const fd = new FormData();
263
+ fd.set("file", new Blob([buf], { type: DOCX_MIME }), basename(file));
264
+ if (flags.title) fd.set("title", flags.title);
265
+ if (flags.folder) fd.set("folderId", await resolveFolder(origin, token, flags.folder));
266
+ const doc = await call(origin, token, "POST", "/documents", { body: fd });
267
+ if (flags.json) return out(JSON.stringify(doc, null, 2));
268
+ out(green("✓ Uploaded ") + bold(doc.title));
269
+ out(` id: ${doc.id}`);
270
+ out(` url: ${cyan(`${origin}/doc/${doc.id}`)}`);
271
+ return;
272
+ }
273
+ case "mv":
274
+ case "move": {
275
+ const id = rest[0];
276
+ if (!id) die("usage: thd doc mv ID (--folder id|name | --root)");
277
+ let folderId;
278
+ if (flags.root) folderId = null;
279
+ else if (flags.folder) folderId = await resolveFolder(origin, token, flags.folder);
280
+ else die("specify a destination: --folder id|name, or --root");
281
+ await call(origin, token, "PATCH", `/documents/${encodeURIComponent(id)}`, {
282
+ body: { folderId },
283
+ });
284
+ out(green("✓ Moved ") + dim(folderId ? `to folder ${folderId}` : "to root"));
285
+ return;
286
+ }
240
287
  case "rm":
241
288
  case "delete": {
242
289
  const id = rest[0];
@@ -292,6 +339,61 @@ async function cmdTemplates(flags) {
292
339
  }
293
340
  }
294
341
 
342
+ async function cmdFolder(flags, sub, rest) {
343
+ const { origin, token } = requireToken(flags);
344
+ if (!sub || sub === "ls" || sub === "list") {
345
+ const res = await call(origin, token, "GET", "/folders");
346
+ if (flags.json) return out(JSON.stringify(res.folders, null, 2));
347
+ const folders = res.folders || [];
348
+ if (!folders.length) return out(dim("No folders."));
349
+ for (const f of folders)
350
+ out(`${bold(f.id)} ${f.name}${f.parentId ? dim(` (in ${f.parentId})`) : ""}`);
351
+ return;
352
+ }
353
+ if (sub === "new" || sub === "create") {
354
+ const name = rest[0];
355
+ if (!name) die('usage: thd folder new "Name" [--parent id|name]');
356
+ const body = { name };
357
+ if (flags.parent) body.parentId = await resolveFolder(origin, token, flags.parent);
358
+ const f = await call(origin, token, "POST", "/folders", { body });
359
+ out(green("✓ Created folder ") + bold(f.name) + dim(` (${f.id})`));
360
+ return;
361
+ }
362
+ die('usage: thd folder ls | new "Name"');
363
+ }
364
+
365
+ function cmdMcpConfig(flags) {
366
+ const local = { command: "thd", args: ["mcp"] };
367
+ const blocks = {
368
+ "claude-code": "claude mcp add thothica-docs -- thd mcp",
369
+ "claude-desktop": JSON.stringify({ mcpServers: { "thothica-docs": local } }, null, 2),
370
+ cursor: JSON.stringify({ mcpServers: { "thothica-docs": local } }, null, 2),
371
+ codex: '[mcp_servers.thothica-docs]\ncommand = "thd"\nargs = ["mcp"]',
372
+ opencode: JSON.stringify(
373
+ { mcp: { "thothica-docs": { type: "local", command: ["thd", "mcp"], enabled: true } } },
374
+ null,
375
+ 2
376
+ ),
377
+ };
378
+ const client = (flags.client || "").toLowerCase().replace(/\s+/g, "-");
379
+ if (client && blocks[client]) return out(blocks[client]);
380
+ const show = (label, hint, block) => {
381
+ out(bold(label) + (hint ? dim(" " + hint) : ""));
382
+ out(block);
383
+ out("");
384
+ };
385
+ show("Claude Code", "", blocks["claude-code"]);
386
+ show(
387
+ "Claude Desktop",
388
+ "~/Library/Application Support/Claude/claude_desktop_config.json (macOS)",
389
+ blocks["claude-desktop"]
390
+ );
391
+ show("Cursor", ".cursor/mcp.json", blocks.cursor);
392
+ show("Codex", "~/.codex/config.toml", blocks.codex);
393
+ show("opencode", "opencode.json", blocks.opencode);
394
+ out(dim("Run `thd login` once so the server has a credential. For CI, set THOTHICA_DOCS_TOKEN."));
395
+ }
396
+
295
397
  function cmdOpen(flags, rest) {
296
398
  const origin = resolveOrigin(flags.origin);
297
399
  const url = rest[0] ? `${origin}/doc/${rest[0]}` : origin;
@@ -310,17 +412,22 @@ ${bold("Access")}
310
412
  ${bold("Documents")}
311
413
  thd doc ls [-q QUERY] [--shared] [--json] List / search documents
312
414
  thd doc new "Title" [--md FILE|-] Create (markdown body, or --template ID)
415
+ thd doc upload FILE.docx [--folder id|name] Upload an existing .docx (optional --title)
313
416
  thd doc cat ID [--text] Print content (markdown by default)
314
417
  thd doc get ID [-o FILE] Download the real .docx
315
418
  thd doc set ID --md FILE|- [--force] Replace content from markdown
419
+ thd doc mv ID (--folder id|name | --root) Move a document into a folder
316
420
  thd doc rm ID [--permanent] Trash (or delete) a document
317
421
 
318
- ${bold("Comments")}
422
+ ${bold("Folders & comments")}
423
+ thd folder ls List your folders
424
+ thd folder new "Name" [--parent id|name] Create a folder
319
425
  thd comment ls ID List comments inside the document
320
426
  thd comment add ID "text" [--anchor "..."] Add a comment (optional --reply-to ID)
321
427
 
322
428
  ${bold("More")}
323
429
  thd templates List document templates
430
+ thd mcp-config [--client NAME] Print connector config (Claude Desktop, Cursor, ...)
324
431
  thd open [ID] Open the app (or a document) in the browser
325
432
  thd mcp Run the MCP server (for AI agents) over stdio
326
433
 
@@ -357,8 +464,13 @@ async function main() {
357
464
  case "comment":
358
465
  case "comments":
359
466
  return await cmdComment(flags, sub, rest);
467
+ case "folder":
468
+ case "folders":
469
+ return await cmdFolder(flags, sub, rest);
360
470
  case "templates":
361
471
  return await cmdTemplates(flags);
472
+ case "mcp-config":
473
+ return cmdMcpConfig(flags);
362
474
  case "open":
363
475
  return cmdOpen(flags, parsed._.slice(1));
364
476
  case "mcp":
package/lib/core.mjs CHANGED
@@ -17,7 +17,7 @@ import { join } from "node:path";
17
17
  import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
18
18
 
19
19
  export const DEFAULT_ORIGIN = "https://docs.thothica.com";
20
- export const CLI_VERSION = "1.0.0";
20
+ export const CLI_VERSION = "1.1.0";
21
21
  const SERVICE = "thothica-docs";
22
22
 
23
23
  // ---------------------------------------------------------------------------
@@ -334,10 +334,18 @@ export async function api(origin, token, method, path, opts = {}) {
334
334
  const headers = { ...(opts.headers || {}) };
335
335
  if (token) headers["Authorization"] = `Bearer ${token}`;
336
336
  let body = opts.body;
337
- if (body != null && typeof body === "object" && !(body instanceof Uint8Array) && !Buffer.isBuffer(body)) {
337
+ const isForm = typeof FormData !== "undefined" && body instanceof FormData;
338
+ if (
339
+ body != null &&
340
+ typeof body === "object" &&
341
+ !isForm &&
342
+ !(body instanceof Uint8Array) &&
343
+ !Buffer.isBuffer(body)
344
+ ) {
338
345
  headers["Content-Type"] = headers["Content-Type"] || "application/json";
339
346
  body = JSON.stringify(body);
340
347
  }
348
+ // FormData sets its own multipart Content-Type (with boundary); leave it alone.
341
349
  const res = await fetch(url, { method, headers, body });
342
350
  if (opts.raw) return res;
343
351
  const text = await res.text();
package/lib/mcp.mjs CHANGED
@@ -245,6 +245,28 @@ const TOOLS = [
245
245
  );
246
246
  },
247
247
  },
248
+ {
249
+ name: "move_document",
250
+ description:
251
+ "Move a document into a folder, or to the root. Pass folderId (from list_folders/create_folder), or set toRoot:true.",
252
+ inputSchema: {
253
+ type: "object",
254
+ required: ["id"],
255
+ properties: {
256
+ id: { type: "string" },
257
+ folderId: { type: "string" },
258
+ toRoot: { type: "boolean" },
259
+ },
260
+ },
261
+ async handler(args, ctx) {
262
+ const folderId = args.toRoot ? null : args.folderId;
263
+ if (folderId === undefined) return fail("Provide folderId, or set toRoot:true.");
264
+ const doc = await ctx.call("PATCH", `/documents/${encodeURIComponent(args.id)}`, {
265
+ folderId,
266
+ });
267
+ return ok(`Moved "${doc.title}" ${folderId ? `into folder ${folderId}` : "to root"}.`);
268
+ },
269
+ },
248
270
  {
249
271
  name: "list_folders",
250
272
  description: "List the user's folders.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thothica/docs-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Command-line tool and MCP server for Thothica Docs: let any AI agent read, create, edit, and comment on native .docx documents.",
5
5
  "type": "module",
6
6
  "bin": {