@heyditto/cli 1.4.0 → 1.6.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/dist/cli.js CHANGED
@@ -2,14 +2,26 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { parseArgs } from "node:util";
6
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
6
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
+ import { Command, Option } from "commander";
8
8
  import { agentSignupURL, apiBaseURL, authFilePath, mcpServerURL, newKeyURL, packageName, packageVersion, resolveApiKey, } from "./config.js";
9
9
  import { clearStoredKey, readStoredAuth, writeStoredAuth, writeStoredKey } from "./store.js";
10
10
  const OUTPUT_FORMATS = ["json", "text", "markdown", "raw"];
11
- const outputOption = { type: "string" };
12
11
  const MEMORY_FORMATS = ["full", "outline", "blocks"];
12
+ function outputOption() {
13
+ return new Option("--output <format>", "output format")
14
+ .choices([...OUTPUT_FORMATS])
15
+ .default("text");
16
+ }
17
+ function hiddenOutputOption() {
18
+ return outputOption().hideHelp();
19
+ }
20
+ function memoryFormatOption() {
21
+ return new Option("--memory-format <format>", "memory body format")
22
+ .choices([...MEMORY_FORMATS])
23
+ .default("full");
24
+ }
13
25
  function parseOutputFormat(value) {
14
26
  if (!value)
15
27
  return "text";
@@ -32,6 +44,14 @@ function parseIntegerOption(value, name) {
32
44
  throw new Error(`${name} must be an integer`);
33
45
  return n;
34
46
  }
47
+ function parseNumberOption(value, name) {
48
+ if (!value)
49
+ return undefined;
50
+ const n = Number.parseFloat(value);
51
+ if (!Number.isFinite(n))
52
+ throw new Error(`${name} must be a number`);
53
+ return n;
54
+ }
35
55
  function parseJSONOption(value, name) {
36
56
  try {
37
57
  return JSON.parse(value);
@@ -72,60 +92,9 @@ function formatToolResult(result, format) {
72
92
  return JSON.stringify({ text }, null, 2);
73
93
  }
74
94
  }
75
- // text / markdown — pass the text block through; fall back to raw envelope.
95
+ // text / markdown: pass the text block through; fall back to raw envelope.
76
96
  return text ?? JSON.stringify(result, null, 2);
77
97
  }
78
- function usage() {
79
- return `${packageName} ${packageVersion}
80
-
81
- Usage:
82
- heyditto save <content> [--source <s>] [--source-context <c>]
83
- heyditto search <query>... [--include-public] [--filter-username <u>]
84
- heyditto fetch <id>... [--memory-format full|outline|blocks]
85
- heyditto list [--username <u>] [--limit <n>] [--offset <n>] [--source <s>]
86
- heyditto update <id> [--content <text>|--content-file <path>] [--title <t>]
87
- [--source-context <c>] [--edits-json <json>|--edits-file <path>]
88
- [--base-revision <n>]
89
- heyditto publish <id> [--title <t>] [--privacy-mode scan_and_block|scan_and_warn|scan_and_redact]
90
- heyditto unpublish (--memory-id <id>|--share-id <id>|<id>)
91
- heyditto subjects <query> [--top-k <n>]
92
- heyditto memories <subject-id>... [--query <q>]
93
- heyditto network <pair-id> [--limit <n>]
94
- heyditto graphs create <name> Create a dedicated graph you own
95
- heyditto graphs list Public graphs you're subscribed to
96
- heyditto graphs add <@username> Subscribe to a public graph
97
- heyditto graphs remove <@username> Unsubscribe from a public graph
98
- heyditto graphs subscribers Who is subscribed to your graph
99
- heyditto init --agent [--agent-caller <name>] [--json]
100
-
101
- All data commands (and 'status') accept --output <format>, where <format>
102
- is one of: json, text, markdown, raw. Default is 'text' (passthrough of
103
- the server's text block, which is JSON for data commands). Use --output json
104
- to guarantee structured JSON output suitable for piping into 'jq'.
105
-
106
- Auth:
107
- heyditto init --agent [--json] Create a free, claimable agent account
108
- heyditto login [<key>] [--paste] [--stdin] Save an API key to ${authFilePath()}
109
- heyditto logout Delete the saved key
110
- heyditto status [--output <format>] Show endpoint, key source, live tools
111
- heyditto config Print MCP client config snippet
112
-
113
- Other:
114
- heyditto help Show this message
115
-
116
- Note: on macOS, Apple ships /usr/bin/ditto (a file-copy utility). If 'ditto'
117
- runs the wrong tool, install with 'npm i -g @heyditto/cli' and invoke as
118
- 'heyditto' (alias bin), or check 'type -a ditto' to disambiguate.
119
-
120
- Environment:
121
- DITTO_API_KEY Optional override (takes precedence over the saved key).
122
- Run 'heyditto init --agent --json' for no-human setup, or get
123
- a human-owned key at ${newKeyURL()}.
124
- DITTO_API_BASE Optional. Defaults to https://api.heyditto.ai.
125
- DITTO_CONFIG_DIR Optional. Defaults to $XDG_CONFIG_HOME/heyditto/cli or
126
- ~/.config/heyditto/cli.
127
- `;
128
- }
129
98
  async function getClient() {
130
99
  const { key, source } = await resolveApiKey();
131
100
  if (!key) {
@@ -161,11 +130,6 @@ async function callAndPrint(name, args, format) {
161
130
  await client.close();
162
131
  }
163
132
  }
164
- function requirePositionals(positionals, minimum, label) {
165
- if (positionals.length < minimum) {
166
- throw new Error(`${label}: expected at least ${minimum} argument(s), got ${positionals.length}`);
167
- }
168
- }
169
133
  async function readKeyFromStdin() {
170
134
  return new Promise((resolve, reject) => {
171
135
  let buf = "";
@@ -189,28 +153,19 @@ function openInBrowser(url) {
189
153
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
190
154
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
191
155
  child.on("error", () => {
192
- /* swallow — best-effort */
156
+ /* swallow: best-effort */
193
157
  });
194
158
  child.unref();
195
159
  }
196
- async function cmdLogin(rest) {
197
- const { values, positionals } = parseArgs({
198
- args: rest,
199
- options: {
200
- paste: { type: "boolean", default: false },
201
- stdin: { type: "boolean", default: false },
202
- output: outputOption,
203
- },
204
- allowPositionals: true,
205
- });
206
- parseOutputFormat(values.output); // validate but ignored — login is interactive
207
- let key = positionals[0]?.trim();
208
- if (!key && values.stdin) {
160
+ async function cmdLogin(keyArg, options) {
161
+ parseOutputFormat(options.output); // validate but ignored: login is interactive
162
+ let key = keyArg?.trim();
163
+ if (!key && options.stdin) {
209
164
  key = (await readKeyFromStdin()).trim();
210
165
  }
211
166
  else if (!key) {
212
- if (values.paste) {
213
- process.stderr.write(`Opening ${newKeyURL()} in your browser…\n`);
167
+ if (options.paste) {
168
+ process.stderr.write(`Opening ${newKeyURL()} in your browser...\n`);
214
169
  openInBrowser(newKeyURL());
215
170
  }
216
171
  if (!process.stdin.isTTY) {
@@ -221,7 +176,7 @@ async function cmdLogin(rest) {
221
176
  if (!key)
222
177
  throw new Error("no key provided");
223
178
  if (!key.startsWith("ditto_mcp_")) {
224
- process.stderr.write(`warning: key does not start with "ditto_mcp_" — proceeding anyway\n`);
179
+ process.stderr.write(`warning: key does not start with "ditto_mcp_" - proceeding anyway\n`);
225
180
  }
226
181
  await writeStoredKey(key);
227
182
  process.stdout.write(`Saved key to ${authFilePath()}\n`);
@@ -233,21 +188,19 @@ async function cmdLogin(rest) {
233
188
  function defaultAgentCaller() {
234
189
  return process.env.DITTO_AGENT_CALLER?.trim() || process.env.CURSOR_AGENT?.trim() || "agent";
235
190
  }
236
- async function cmdInit(rest) {
237
- const { values } = parseArgs({
238
- args: rest,
239
- options: {
240
- agent: { type: "boolean", default: false },
241
- "agent-caller": { type: "string" },
242
- json: { type: "boolean", default: false },
243
- output: outputOption,
244
- },
245
- allowPositionals: true,
246
- });
247
- const output = values.json ? "json" : parseOutputFormat(values.output);
248
- if (!values.agent) {
191
+ async function cmdInit(graphs, options) {
192
+ const output = options.json ? "json" : parseOutputFormat(options.output);
193
+ if (!options.agent) {
249
194
  throw new Error("init currently supports only --agent");
250
195
  }
196
+ // --subscribe pre-subscribes the new agent to public foundation knowledge
197
+ // graphs (e.g. the @minos mentor KG). Accepts repeats and comma-separated
198
+ // lists, plus bare positional graph names: --subscribe @minos @a,@b.
199
+ // De-duped; '@' optional.
200
+ const subscribeGraphs = Array.from(new Set([...(options.subscribe ?? []), ...graphs]
201
+ .flatMap((v) => v.split(","))
202
+ .map((g) => g.trim().replace(/^@/, ""))
203
+ .filter((g) => g.length > 0)));
251
204
  const stored = await readStoredAuth();
252
205
  if (stored?.apiKey && stored.agentMode) {
253
206
  const existing = {
@@ -273,7 +226,7 @@ async function cmdInit(rest) {
273
226
  if (stored?.apiKey) {
274
227
  throw new Error(`a Ditto API key is already saved at ${authFilePath()}; run 'heyditto logout' before creating an agent account`);
275
228
  }
276
- const agentCaller = values["agent-caller"]?.trim() || defaultAgentCaller();
229
+ const agentCaller = options.agentCaller?.trim() || defaultAgentCaller();
277
230
  const response = await fetch(agentSignupURL(), {
278
231
  method: "POST",
279
232
  headers: {
@@ -282,6 +235,7 @@ async function cmdInit(rest) {
282
235
  },
283
236
  body: JSON.stringify({
284
237
  agentCaller,
238
+ ...(subscribeGraphs.length > 0 ? { subscribeGraphs } : {}),
285
239
  metadata: {
286
240
  package: packageName,
287
241
  version: packageVersion,
@@ -324,6 +278,12 @@ async function cmdInit(rest) {
324
278
  claimURL: signup.claimURL,
325
279
  status: signup.status,
326
280
  configPath: authFilePath(),
281
+ ...(subscribeGraphs.length > 0
282
+ ? {
283
+ subscribedGraphs: signup.subscribedGraphs ?? [],
284
+ failedGraphs: signup.failedGraphs ?? [],
285
+ }
286
+ : {}),
327
287
  };
328
288
  if (output === "json" || output === "raw") {
329
289
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -331,15 +291,22 @@ async function cmdInit(rest) {
331
291
  }
332
292
  process.stdout.write(`Created agent account ${signup.userID}\n`);
333
293
  process.stdout.write(`Saved key to ${authFilePath()}\n`);
294
+ if (subscribeGraphs.length > 0) {
295
+ const subscribed = signup.subscribedGraphs ?? [];
296
+ const failed = signup.failedGraphs ?? [];
297
+ if (subscribed.length > 0) {
298
+ process.stdout.write(`Subscribed to ${subscribed.map((g) => `@${g}`).join(", ")}\n`);
299
+ }
300
+ if (failed.length > 0) {
301
+ process.stdout.write(`Could not subscribe (not found or not public): ${failed
302
+ .map((g) => `@${g}`)
303
+ .join(", ")}\n`);
304
+ }
305
+ }
334
306
  process.stdout.write(`Claim later: ${signup.claimURL}\n`);
335
307
  }
336
- async function cmdLogout(rest) {
337
- const { values } = parseArgs({
338
- args: rest,
339
- options: { output: outputOption },
340
- allowPositionals: true,
341
- });
342
- parseOutputFormat(values.output);
308
+ async function cmdLogout(options) {
309
+ parseOutputFormat(options.output);
343
310
  const removed = await clearStoredKey();
344
311
  if (removed) {
345
312
  process.stdout.write(`Removed ${authFilePath()}\n`);
@@ -351,225 +318,183 @@ async function cmdLogout(rest) {
351
318
  process.stderr.write(`note: DITTO_API_KEY is still set in your environment and will continue to be used.\n`);
352
319
  }
353
320
  }
354
- async function cmdSave(rest) {
355
- const { values, positionals } = parseArgs({
356
- args: rest,
357
- options: {
358
- source: { type: "string", default: "cli" },
359
- "source-context": { type: "string" },
360
- output: outputOption,
361
- },
362
- allowPositionals: true,
363
- });
364
- const format = parseOutputFormat(values.output);
365
- requirePositionals(positionals, 1, "save");
321
+ async function cmdSave(content, options) {
322
+ const format = parseOutputFormat(options.output);
366
323
  await callAndPrint("save_memory", {
367
- content: positionals.join(" "),
368
- source: values.source,
369
- sourceContext: values["source-context"],
324
+ content: content.join(" "),
325
+ source: options.source ?? "cli",
326
+ sourceContext: options.sourceContext,
370
327
  }, format);
371
328
  }
372
- async function cmdSearch(rest) {
373
- const { values, positionals } = parseArgs({
374
- args: rest,
375
- options: {
376
- "include-public": { type: "boolean", default: false },
377
- "filter-username": { type: "string" },
378
- output: outputOption,
379
- },
380
- allowPositionals: true,
381
- });
382
- const format = parseOutputFormat(values.output);
383
- requirePositionals(positionals, 1, "search");
384
- const args = { queries: positionals };
385
- if (values["include-public"])
329
+ async function cmdSearch(queries, options) {
330
+ const format = parseOutputFormat(options.output);
331
+ const args = { queries };
332
+ if (options.includePublic)
386
333
  args.includePublic = true;
387
- if (values["filter-username"])
388
- args.filterUsername = values["filter-username"];
334
+ if (options.filterUsername)
335
+ args.filterUsername = options.filterUsername;
389
336
  await callAndPrint("search_memories", args, format);
390
337
  }
391
- async function cmdFetch(rest) {
392
- const { values, positionals } = parseArgs({
393
- args: rest,
394
- options: { "memory-format": { type: "string" }, output: outputOption },
395
- allowPositionals: true,
396
- });
397
- const format = parseOutputFormat(values.output);
398
- const memoryFormat = parseMemoryFormat(values["memory-format"]);
399
- requirePositionals(positionals, 1, "fetch");
400
- await callAndPrint("fetch_memories", { ids: positionals, format: memoryFormat }, format);
401
- }
402
- async function cmdList(rest) {
403
- const { values } = parseArgs({
404
- args: rest,
405
- options: {
406
- username: { type: "string" },
407
- limit: { type: "string" },
408
- offset: { type: "string" },
409
- source: { type: "string" },
410
- output: outputOption,
411
- },
412
- allowPositionals: true,
413
- });
414
- const format = parseOutputFormat(values.output);
338
+ async function cmdFetch(ids, options) {
339
+ const format = parseOutputFormat(options.output);
340
+ const memoryFormat = parseMemoryFormat(options.memoryFormat);
341
+ await callAndPrint("fetch_memories", { ids, format: memoryFormat }, format);
342
+ }
343
+ async function cmdList(options) {
344
+ const format = parseOutputFormat(options.output);
415
345
  const args = {};
416
- if (values.username)
417
- args.username = values.username;
418
- const limit = parseIntegerOption(values.limit, "--limit");
346
+ if (options.username)
347
+ args.username = options.username;
348
+ const limit = parseIntegerOption(options.limit, "--limit");
419
349
  if (limit !== undefined)
420
350
  args.limit = limit;
421
- const offset = parseIntegerOption(values.offset, "--offset");
351
+ const offset = parseIntegerOption(options.offset, "--offset");
422
352
  if (offset !== undefined)
423
353
  args.offset = offset;
424
- if (values.source)
425
- args.source = values.source;
354
+ if (options.source)
355
+ args.source = options.source;
426
356
  await callAndPrint("list_memories", args, format);
427
357
  }
428
- async function cmdUpdate(rest) {
429
- const { values, positionals } = parseArgs({
430
- args: rest,
431
- options: {
432
- content: { type: "string" },
433
- "content-file": { type: "string" },
434
- title: { type: "string" },
435
- "source-context": { type: "string" },
436
- "edits-json": { type: "string" },
437
- "edits-file": { type: "string" },
438
- "base-revision": { type: "string" },
439
- output: outputOption,
440
- },
441
- allowPositionals: true,
442
- });
443
- const format = parseOutputFormat(values.output);
444
- requirePositionals(positionals, 1, "update");
445
- if (values.content && values["content-file"]) {
358
+ async function cmdUpdate(id, options) {
359
+ const format = parseOutputFormat(options.output);
360
+ if (options.content && options.contentFile) {
446
361
  throw new Error("update: use either --content or --content-file, not both");
447
362
  }
448
- if (values["edits-json"] && values["edits-file"]) {
363
+ if (options.editsJson && options.editsFile) {
449
364
  throw new Error("update: use either --edits-json or --edits-file, not both");
450
365
  }
451
- if ((values.content || values["content-file"]) && (values["edits-json"] || values["edits-file"])) {
366
+ if ((options.content || options.contentFile) && (options.editsJson || options.editsFile)) {
452
367
  throw new Error("update: content replacement and block edits are mutually exclusive");
453
368
  }
454
- const args = { memoryId: positionals[0] };
455
- if (values.content)
456
- args.content = values.content;
457
- if (values["content-file"])
458
- args.content = await readTextFile(values["content-file"], "--content-file");
459
- if (values.title !== undefined)
460
- args.title = values.title;
461
- if (values["source-context"] !== undefined)
462
- args.sourceContext = values["source-context"];
463
- if (values["edits-json"] || values["edits-file"]) {
464
- const raw = values["edits-json"] ?? (await readTextFile(values["edits-file"], "--edits-file"));
465
- args.edits = parseJSONOption(raw, values["edits-json"] ? "--edits-json" : "--edits-file");
466
- const baseRevision = parseIntegerOption(values["base-revision"], "--base-revision");
369
+ const args = { memoryId: id };
370
+ if (options.content)
371
+ args.content = options.content;
372
+ if (options.contentFile)
373
+ args.content = await readTextFile(options.contentFile, "--content-file");
374
+ if (options.title !== undefined)
375
+ args.title = options.title;
376
+ if (options.sourceContext !== undefined)
377
+ args.sourceContext = options.sourceContext;
378
+ if (options.editsJson || options.editsFile) {
379
+ const raw = options.editsJson ?? (await readTextFile(options.editsFile, "--edits-file"));
380
+ args.edits = parseJSONOption(raw, options.editsJson ? "--edits-json" : "--edits-file");
381
+ const baseRevision = parseIntegerOption(options.baseRevision, "--base-revision");
467
382
  if (baseRevision === undefined) {
468
383
  throw new Error("update: --base-revision is required with block edits");
469
384
  }
470
385
  args.baseRevision = baseRevision;
471
386
  }
472
387
  else {
473
- const baseRevision = parseIntegerOption(values["base-revision"], "--base-revision");
388
+ const baseRevision = parseIntegerOption(options.baseRevision, "--base-revision");
474
389
  if (baseRevision !== undefined)
475
390
  args.baseRevision = baseRevision;
476
391
  }
477
392
  await callAndPrint("update_memory", args, format);
478
393
  }
479
- async function cmdPublish(rest) {
480
- const { values, positionals } = parseArgs({
481
- args: rest,
482
- options: {
483
- title: { type: "string" },
484
- "privacy-mode": { type: "string" },
485
- output: outputOption,
486
- },
487
- allowPositionals: true,
488
- });
489
- const format = parseOutputFormat(values.output);
490
- requirePositionals(positionals, 1, "publish");
491
- const args = { memoryId: positionals[0] };
492
- if (values.title !== undefined)
493
- args.title = values.title;
494
- if (values["privacy-mode"] !== undefined)
495
- args.privacyMode = values["privacy-mode"];
394
+ async function cmdPublish(id, options) {
395
+ const format = parseOutputFormat(options.output);
396
+ const args = { memoryId: id };
397
+ if (options.title !== undefined)
398
+ args.title = options.title;
399
+ if (options.privacyMode !== undefined)
400
+ args.privacyMode = options.privacyMode;
496
401
  await callAndPrint("publish_memory", args, format);
497
402
  }
498
- async function cmdUnpublish(rest) {
499
- const { values, positionals } = parseArgs({
500
- args: rest,
501
- options: {
502
- "memory-id": { type: "string" },
503
- "share-id": { type: "string" },
504
- output: outputOption,
505
- },
506
- allowPositionals: true,
507
- });
508
- const format = parseOutputFormat(values.output);
509
- const provided = [values["memory-id"], values["share-id"], positionals[0]].filter(Boolean);
403
+ async function cmdUnpublish(id, options) {
404
+ const format = parseOutputFormat(options.output);
405
+ const provided = [options.memoryId, options.shareId, id].filter(Boolean);
510
406
  if (provided.length !== 1) {
511
407
  throw new Error("unpublish: provide exactly one of --memory-id, --share-id, or positional id");
512
408
  }
513
409
  const args = {};
514
- if (values["memory-id"]) {
515
- args.memoryId = values["memory-id"];
410
+ if (options.memoryId) {
411
+ args.memoryId = options.memoryId;
516
412
  }
517
- else if (values["share-id"]) {
518
- args.shareId = values["share-id"];
413
+ else if (options.shareId) {
414
+ args.shareId = options.shareId;
519
415
  }
520
416
  else {
521
- args.memoryId = positionals[0];
417
+ args.memoryId = id;
522
418
  }
523
419
  await callAndPrint("unpublish_memory", args, format);
524
420
  }
525
- async function cmdSubjects(rest) {
526
- const { values, positionals } = parseArgs({
527
- args: rest,
528
- options: { "top-k": { type: "string" }, output: outputOption },
529
- allowPositionals: true,
530
- });
531
- const format = parseOutputFormat(values.output);
532
- requirePositionals(positionals, 1, "subjects");
533
- const args = { query: positionals.join(" ") };
534
- const topK = parseIntegerOption(values["top-k"], "--top-k");
421
+ async function cmdSubjects(query, options) {
422
+ const format = parseOutputFormat(options.output);
423
+ const args = { query: query.join(" ") };
424
+ const topK = parseIntegerOption(options.topK, "--top-k");
535
425
  if (topK !== undefined)
536
426
  args.topK = topK;
537
427
  await callAndPrint("search_subjects", args, format);
538
428
  }
539
- async function cmdMemories(rest) {
540
- const { values, positionals } = parseArgs({
541
- args: rest,
542
- options: { query: { type: "string" }, output: outputOption },
543
- allowPositionals: true,
544
- });
545
- const format = parseOutputFormat(values.output);
546
- requirePositionals(positionals, 1, "memories");
547
- const args = { subjectIds: positionals };
548
- if (values.query)
549
- args.query = values.query;
429
+ async function cmdMemories(subjectIds, options) {
430
+ const format = parseOutputFormat(options.output);
431
+ const args = { subjectIds };
432
+ if (options.query)
433
+ args.query = options.query;
550
434
  await callAndPrint("search_memories_in_subjects", args, format);
551
435
  }
552
- async function cmdNetwork(rest) {
553
- const { values, positionals } = parseArgs({
554
- args: rest,
555
- options: { limit: { type: "string" }, output: outputOption },
556
- allowPositionals: true,
557
- });
558
- const format = parseOutputFormat(values.output);
559
- requirePositionals(positionals, 1, "network");
560
- const args = { pairId: positionals[0] };
561
- const limit = parseIntegerOption(values.limit, "--limit");
436
+ async function cmdNetwork(pairId, options) {
437
+ const format = parseOutputFormat(options.output);
438
+ const args = { pairId };
439
+ const limit = parseIntegerOption(options.limit, "--limit");
562
440
  if (limit !== undefined)
563
441
  args.limit = limit;
564
442
  await callAndPrint("get_memory_network", args, format);
565
443
  }
566
- async function cmdStatus(rest) {
567
- const { values } = parseArgs({
568
- args: rest,
569
- options: { output: outputOption },
570
- allowPositionals: true,
571
- });
572
- const format = parseOutputFormat(values.output);
444
+ async function cmdFriends(options) {
445
+ await callAndPrint("list_friends", {}, parseOutputFormat(options.output));
446
+ }
447
+ async function cmdKnowledgeGraphs(options) {
448
+ await callAndPrint("list_knowledge_graphs", {}, parseOutputFormat(options.output));
449
+ }
450
+ async function cmdMyMemories(options) {
451
+ const args = {};
452
+ const limit = parseIntegerOption(options.limit, "--limit");
453
+ if (limit !== undefined)
454
+ args.limit = limit;
455
+ const offset = parseIntegerOption(options.offset, "--offset");
456
+ if (offset !== undefined)
457
+ args.offset = offset;
458
+ if (options.source)
459
+ args.source = options.source;
460
+ await callAndPrint("list_my_memories", args, parseOutputFormat(options.output));
461
+ }
462
+ async function cmdSubjectEdges(subjectId, options) {
463
+ const args = { subjectId };
464
+ if (options.kg)
465
+ args.kg = options.kg;
466
+ const limit = parseIntegerOption(options.limit, "--limit");
467
+ if (limit !== undefined)
468
+ args.limit = limit;
469
+ const minWeight = parseNumberOption(options.minWeight, "--min-weight");
470
+ if (minWeight !== undefined)
471
+ args.minWeight = minWeight;
472
+ await callAndPrint("get_subject_edges", args, parseOutputFormat(options.output));
473
+ }
474
+ async function cmdGraphSharing(options) {
475
+ if (!!options.enable === !!options.disable) {
476
+ throw new Error("sharing: provide exactly one of --enable or --disable");
477
+ }
478
+ const args = {
479
+ publicSubscriptionsEnabled: !!options.enable,
480
+ };
481
+ if (options.title !== undefined)
482
+ args.title = options.title;
483
+ if (options.description !== undefined)
484
+ args.description = options.description;
485
+ await callAndPrint("set_knowledge_graph_sharing", args, parseOutputFormat(options.output));
486
+ }
487
+ async function cmdDelete(memoryId, options) {
488
+ if (!options.confirm) {
489
+ throw new Error("delete: pass --confirm to permanently delete this memory");
490
+ }
491
+ const args = { memoryId, confirm: true };
492
+ if (options.kg)
493
+ args.kg = options.kg;
494
+ await callAndPrint("delete_memory", args, parseOutputFormat(options.output));
495
+ }
496
+ async function cmdStatus(options) {
497
+ const format = parseOutputFormat(options.output);
573
498
  const [{ key, source }, stored] = await Promise.all([resolveApiKey(), readStoredAuth()]);
574
499
  const report = {
575
500
  package: packageName,
@@ -633,20 +558,15 @@ async function cmdStatus(rest) {
633
558
  lines.push(`connect: ok`, `tools: unavailable (tools/list failed: ${report.toolsError})`);
634
559
  }
635
560
  else if (report.connect && !report.connect.ok) {
636
- lines.push(`connect: FAILED — ${report.connect.error}`);
561
+ lines.push(`connect: FAILED - ${report.connect.error}`);
637
562
  }
638
563
  if (report.agent?.claimURL) {
639
564
  lines.push(`agent: unclaimed (${report.agent.caller || "agent"})`, `claim: ${report.agent.claimURL}`);
640
565
  }
641
566
  process.stdout.write(`${lines.join("\n")}\n`);
642
567
  }
643
- function cmdConfig(rest) {
644
- const { values } = parseArgs({
645
- args: rest,
646
- options: { output: outputOption },
647
- allowPositionals: true,
648
- });
649
- parseOutputFormat(values.output); // accepted; output is always JSON
568
+ function cmdConfig(options) {
569
+ parseOutputFormat(options.output); // accepted; output is always JSON
650
570
  const config = {
651
571
  mcpServers: {
652
572
  ditto: {
@@ -658,141 +578,296 @@ function cmdConfig(rest) {
658
578
  };
659
579
  process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
660
580
  }
661
- // cmdGraphs manages the public knowledge graphs this account is subscribed to,
662
- // mirroring the MCP subscription tools. Subscriptions only ever cover OTHER
663
- // users' public graphs (by @username); this command cannot touch the account's
664
- // own KG or its app KG, since those are not subscriptions.
665
- async function cmdGraphs(rest) {
666
- const sub = rest[0];
667
- const subRest = rest.slice(1);
668
- switch (sub) {
669
- case "create": {
670
- // Provision a NEW dedicated graph you own + get a key scoped to only it.
671
- const { values, positionals } = parseArgs({
672
- args: subRest,
673
- options: { output: outputOption },
674
- allowPositionals: true,
675
- });
676
- requirePositionals(positionals, 1, "graphs create");
677
- await callAndPrint("create_dedicated_graph", { name: positionals.join(" ") }, parseOutputFormat(values.output));
678
- return;
679
- }
680
- case undefined:
681
- case "list": {
682
- const { values } = parseArgs({
683
- args: subRest,
684
- options: { output: outputOption },
685
- allowPositionals: true,
686
- });
687
- await callAndPrint("list_knowledge_graph_subscriptions", {}, parseOutputFormat(values.output));
688
- return;
689
- }
690
- case "subscribers": {
691
- const { values } = parseArgs({
692
- args: subRest,
693
- options: { output: outputOption },
694
- allowPositionals: true,
695
- });
696
- await callAndPrint("list_knowledge_graph_subscribers", {}, parseOutputFormat(values.output));
697
- return;
698
- }
699
- case "add": {
700
- const { values, positionals } = parseArgs({
701
- args: subRest,
702
- options: { output: outputOption },
703
- allowPositionals: true,
704
- });
705
- requirePositionals(positionals, 1, "graphs add");
706
- if (positionals.length > 1) {
707
- throw new Error(`graphs add: expected exactly 1 username, got ${positionals.length}`);
708
- }
709
- await callAndPrint("subscribe_knowledge_graph", { username: positionals[0] }, parseOutputFormat(values.output));
710
- return;
711
- }
712
- case "remove": {
713
- const { values, positionals } = parseArgs({
714
- args: subRest,
715
- options: { output: outputOption },
716
- allowPositionals: true,
717
- });
718
- requirePositionals(positionals, 1, "graphs remove");
719
- if (positionals.length > 1) {
720
- throw new Error(`graphs remove: expected exactly 1 username, got ${positionals.length}`);
721
- }
722
- await callAndPrint("unsubscribe_knowledge_graph", { username: positionals[0] }, parseOutputFormat(values.output));
723
- return;
724
- }
725
- default:
726
- throw new Error(`graphs: unknown subcommand "${sub}" (expected: create <name>, list, add <@user>, remove <@user>, subscribers)`);
727
- }
581
+ // These commands mirror the MCP subscription tools. Subscriptions only ever
582
+ // cover other users' public graphs by @username; this cannot touch the account's
583
+ // own graph or app graph, since those are not subscriptions.
584
+ async function cmdGraphsCreate(nameParts, options) {
585
+ await callAndPrint("create_dedicated_graph", { name: nameParts.join(" ") }, parseOutputFormat(options.output));
586
+ }
587
+ async function cmdGraphsList(options) {
588
+ await callAndPrint("list_knowledge_graph_subscriptions", {}, parseOutputFormat(options.output));
589
+ }
590
+ async function cmdGraphsSubscribers(options) {
591
+ await callAndPrint("list_knowledge_graph_subscribers", {}, parseOutputFormat(options.output));
592
+ }
593
+ async function cmdGraphsAdd(username, options) {
594
+ await callAndPrint("subscribe_knowledge_graph", { username }, parseOutputFormat(options.output));
595
+ }
596
+ async function cmdGraphsRemove(username, options) {
597
+ await callAndPrint("unsubscribe_knowledge_graph", { username }, parseOutputFormat(options.output));
598
+ }
599
+ function addExamples(command, examples) {
600
+ return command.addHelpText("after", `\nExamples:\n${examples}`);
601
+ }
602
+ function buildProgram() {
603
+ const program = new Command();
604
+ program
605
+ .name("heyditto")
606
+ .description("Save, search, fetch, and traverse Ditto memories from the shell.")
607
+ .version(packageVersion, "-v, --version", "print the CLI version")
608
+ .helpCommand("help [command]", "show help for a command")
609
+ .showHelpAfterError()
610
+ .addHelpText("after", `
611
+ Notes:
612
+ On macOS, Apple ships /usr/bin/ditto (a file-copy utility). If 'ditto'
613
+ runs the wrong tool, install with 'npm i -g @heyditto/cli' and invoke as
614
+ 'heyditto' (alias bin), or check 'type -a ditto' to disambiguate.
615
+
616
+ Environment:
617
+ DITTO_API_KEY Optional override, taking precedence over the saved key.
618
+ DITTO_API_BASE Optional API base URL. Defaults to https://api.heyditto.ai.
619
+ DITTO_CONFIG_DIR Optional config directory. Defaults to $XDG_CONFIG_HOME/heyditto/cli
620
+ or ~/.config/heyditto/cli.
621
+ `);
622
+ addExamples(program
623
+ .command("save")
624
+ .description("save a memory")
625
+ .summary("save a memory")
626
+ .argument("<content...>", "memory content")
627
+ .option("--source <source>", "memory source", "cli")
628
+ .option("--source-context <context>", "source context, such as a filename")
629
+ .addOption(outputOption())
630
+ .action(cmdSave), ` heyditto save "Project X uses Bun + SolidJS"
631
+ heyditto save "$(cat note.md)" --source document --source-context note.md`);
632
+ addExamples(program
633
+ .command("search")
634
+ .description("search private memories, optionally public graphs")
635
+ .summary("search private memories, optionally public graphs")
636
+ .argument("<query...>", "one or more search queries")
637
+ .option("--include-public", "include public DittoHub memories")
638
+ .option("--filter-username <username>", "scope public results to a username")
639
+ .addOption(outputOption())
640
+ .action(cmdSearch), ` heyditto search "typescript preferences"
641
+ heyditto search "launch notes" --include-public --filter-username peyton`);
642
+ program
643
+ .command("fetch")
644
+ .description("fetch memories by id")
645
+ .summary("fetch memories by id")
646
+ .argument("<id...>", "memory ids or public share ids")
647
+ .addOption(memoryFormatOption())
648
+ .addOption(outputOption())
649
+ .action(cmdFetch);
650
+ program
651
+ .command("list")
652
+ .description("list memories or public publishes")
653
+ .summary("list memories or public publishes")
654
+ .option("--username <username>", "list public DittoHub publishes for a username")
655
+ .option("--limit <number>", "maximum number of results")
656
+ .option("--offset <number>", "result offset")
657
+ .option("--source <source>", "filter by memory source")
658
+ .addOption(outputOption())
659
+ .action(cmdList);
660
+ program
661
+ .command("my-memories")
662
+ .alias("list_my_memories")
663
+ .description("list only your saved memories")
664
+ .summary("list only your saved memories")
665
+ .option("--limit <number>", "maximum number of results")
666
+ .option("--offset <number>", "result offset")
667
+ .option("--source <source>", "filter by memory source")
668
+ .addOption(outputOption())
669
+ .action(cmdMyMemories);
670
+ program
671
+ .command("update")
672
+ .description("update a saved memory")
673
+ .summary("update a saved memory")
674
+ .argument("<id>", "memory id")
675
+ .option("--content <text>", "replacement memory content")
676
+ .option("--content-file <path>", "path to replacement memory content")
677
+ .option("--title <title>", "memory title")
678
+ .option("--source-context <context>", "source context")
679
+ .option("--edits-json <json>", "structured block edits as JSON")
680
+ .option("--edits-file <path>", "path to structured block edits JSON")
681
+ .option("--base-revision <number>", "base memory revision")
682
+ .addOption(outputOption())
683
+ .addHelpText("after", `
684
+ Examples:
685
+ heyditto update <memory-id> --content-file revised.md --output json
686
+ heyditto update <memory-id> --edits-file edits.json --base-revision 3 --output json`)
687
+ .action(cmdUpdate);
688
+ program
689
+ .command("publish")
690
+ .description("publish a memory to DittoHub")
691
+ .summary("publish a memory to DittoHub")
692
+ .argument("<id>", "memory id")
693
+ .option("--title <title>", "public title")
694
+ .option("--privacy-mode <mode>", "privacy mode: scan_and_block, scan_and_warn, or scan_and_redact")
695
+ .addOption(outputOption())
696
+ .action(cmdPublish);
697
+ program
698
+ .command("unpublish")
699
+ .description("remove an existing public share")
700
+ .summary("remove an existing public share")
701
+ .argument("[id]", "memory id")
702
+ .option("--memory-id <id>", "memory id")
703
+ .option("--share-id <id>", "share id")
704
+ .addOption(outputOption())
705
+ .action(cmdUnpublish);
706
+ program
707
+ .command("delete")
708
+ .alias("delete_memory")
709
+ .description("permanently delete a saved memory")
710
+ .summary("permanently delete a saved memory")
711
+ .argument("<memory-id>", "memory id")
712
+ .requiredOption("--confirm", "confirm permanent deletion")
713
+ .option("--kg <alias>", "knowledge graph alias")
714
+ .addOption(outputOption())
715
+ .action(cmdDelete);
716
+ program
717
+ .command("subjects")
718
+ .description("search the subject graph")
719
+ .summary("search the subject graph")
720
+ .argument("<query...>", "subject search query")
721
+ .option("--top-k <number>", "maximum number of subjects")
722
+ .addOption(outputOption())
723
+ .action(cmdSubjects);
724
+ program
725
+ .command("subject-edges")
726
+ .alias("get_subject_edges")
727
+ .description("list related subjects for a subject")
728
+ .summary("list related subjects for a subject")
729
+ .argument("<subject-id>", "subject id")
730
+ .option("--kg <alias>", "knowledge graph alias")
731
+ .option("--limit <number>", "maximum number of related subjects")
732
+ .option("--min-weight <number>", "minimum edge weight from 0 to 1")
733
+ .addOption(outputOption())
734
+ .action(cmdSubjectEdges);
735
+ program
736
+ .command("memories")
737
+ .description("fetch memory previews for subjects")
738
+ .summary("fetch memory previews for subjects")
739
+ .argument("<subject-id...>", "subject ids")
740
+ .option("--query <query>", "optional search query")
741
+ .addOption(outputOption())
742
+ .action(cmdMemories);
743
+ program
744
+ .command("network")
745
+ .description("traverse related memories")
746
+ .summary("traverse related memories")
747
+ .argument("<pair-id>", "memory pair id")
748
+ .option("--limit <number>", "maximum number of related memories")
749
+ .addOption(outputOption())
750
+ .action(cmdNetwork);
751
+ program
752
+ .command("friends")
753
+ .alias("list_friends")
754
+ .description("list Ditto friends")
755
+ .summary("list Ditto friends")
756
+ .addOption(outputOption())
757
+ .action(cmdFriends);
758
+ program
759
+ .command("knowledge-graphs")
760
+ .alias("list_knowledge_graphs")
761
+ .description("list readable knowledge graphs")
762
+ .summary("list readable knowledge graphs")
763
+ .addOption(outputOption())
764
+ .action(cmdKnowledgeGraphs);
765
+ program
766
+ .command("graph-sharing")
767
+ .alias("set_knowledge_graph_sharing")
768
+ .description("configure whether others can subscribe to your graph")
769
+ .summary("configure graph sharing")
770
+ .option("--enable", "allow public subscriptions to your graph")
771
+ .option("--disable", "disable public subscriptions to your graph")
772
+ .option("--title <title>", "subscribable graph title")
773
+ .option("--description <description>", "subscribable graph description")
774
+ .addOption(outputOption())
775
+ .action(cmdGraphSharing);
776
+ const graphs = program
777
+ .command("graphs")
778
+ .description("manage knowledge graph subscriptions")
779
+ .summary("manage knowledge graph subscriptions")
780
+ .showHelpAfterError()
781
+ .addHelpText("after", `
782
+ Subscriptions cover other users' public graphs by @username. They do not modify
783
+ your own graph or an app graph.`);
784
+ graphs
785
+ .command("create")
786
+ .description("create a dedicated graph you own")
787
+ .argument("<name...>", "graph name")
788
+ .addOption(outputOption())
789
+ .action(cmdGraphsCreate);
790
+ graphs
791
+ .command("list")
792
+ .description("list public graphs you're subscribed to")
793
+ .addOption(outputOption())
794
+ .action(cmdGraphsList);
795
+ graphs
796
+ .command("available")
797
+ .alias("list_knowledge_graphs")
798
+ .description("list readable knowledge graphs")
799
+ .addOption(outputOption())
800
+ .action(cmdKnowledgeGraphs);
801
+ graphs
802
+ .command("add")
803
+ .description("subscribe to a public graph")
804
+ .argument("<username>", "public graph username, with or without @")
805
+ .addOption(outputOption())
806
+ .action(cmdGraphsAdd);
807
+ graphs
808
+ .command("remove")
809
+ .description("unsubscribe from a public graph")
810
+ .argument("<username>", "public graph username, with or without @")
811
+ .addOption(outputOption())
812
+ .action(cmdGraphsRemove);
813
+ graphs
814
+ .command("subscribers")
815
+ .description("list who is subscribed to your graph")
816
+ .addOption(outputOption())
817
+ .action(cmdGraphsSubscribers);
818
+ graphs
819
+ .command("sharing")
820
+ .alias("set_knowledge_graph_sharing")
821
+ .description("configure whether others can subscribe to your graph")
822
+ .option("--enable", "allow public subscriptions to your graph")
823
+ .option("--disable", "disable public subscriptions to your graph")
824
+ .option("--title <title>", "subscribable graph title")
825
+ .option("--description <description>", "subscribable graph description")
826
+ .addOption(outputOption())
827
+ .action(cmdGraphSharing);
828
+ program
829
+ .command("init")
830
+ .description("initialize a claimable agent account")
831
+ .argument("[graph...]", "public graphs to subscribe to")
832
+ .option("--agent", "create a free, claimable agent account")
833
+ .option("--agent-caller <name>", "agent name")
834
+ .option("--subscribe <graph>", "public graph to subscribe to", (value, previous) => [...previous, value], [])
835
+ .option("--json", "print machine-readable output")
836
+ .addOption(hiddenOutputOption())
837
+ .action(cmdInit);
838
+ program
839
+ .command("login")
840
+ .description("save an API key")
841
+ .argument("[key]", "Ditto API key")
842
+ .option("--paste", "open the key creation page before prompting")
843
+ .option("--stdin", "read the API key from stdin")
844
+ .addOption(hiddenOutputOption())
845
+ .action(cmdLogin);
846
+ program
847
+ .command("logout")
848
+ .description("delete the saved API key")
849
+ .addOption(hiddenOutputOption())
850
+ .action(cmdLogout);
851
+ program
852
+ .command("status")
853
+ .description("show CLI auth and endpoint status")
854
+ .addOption(outputOption())
855
+ .action(cmdStatus);
856
+ program
857
+ .command("config")
858
+ .description("print MCP client configuration")
859
+ .addOption(hiddenOutputOption())
860
+ .action(cmdConfig);
861
+ return program;
728
862
  }
729
863
  async function main() {
730
- const argv = process.argv.slice(2);
731
- const command = argv[0];
732
- const rest = argv.slice(1);
733
- switch (command) {
734
- case "init":
735
- await cmdInit(rest);
736
- return;
737
- case "save":
738
- await cmdSave(rest);
739
- return;
740
- case "search":
741
- await cmdSearch(rest);
742
- return;
743
- case "fetch":
744
- await cmdFetch(rest);
745
- return;
746
- case "list":
747
- await cmdList(rest);
748
- return;
749
- case "update":
750
- await cmdUpdate(rest);
751
- return;
752
- case "publish":
753
- await cmdPublish(rest);
754
- return;
755
- case "unpublish":
756
- await cmdUnpublish(rest);
757
- return;
758
- case "subjects":
759
- await cmdSubjects(rest);
760
- return;
761
- case "memories":
762
- await cmdMemories(rest);
763
- return;
764
- case "network":
765
- await cmdNetwork(rest);
766
- return;
767
- case "graphs":
768
- await cmdGraphs(rest);
769
- return;
770
- case "login":
771
- await cmdLogin(rest);
772
- return;
773
- case "logout":
774
- await cmdLogout(rest);
775
- return;
776
- case "status":
777
- await cmdStatus(rest);
778
- return;
779
- case "config":
780
- cmdConfig(rest);
781
- return;
782
- case undefined:
783
- case "help":
784
- case "--help":
785
- case "-h":
786
- process.stdout.write(usage());
787
- return;
788
- case "--version":
789
- case "-v":
790
- process.stdout.write(`${packageVersion}\n`);
791
- return;
792
- default:
793
- process.stderr.write(`Unknown command: ${command}\n\n${usage()}`);
794
- process.exitCode = 2;
864
+ const argv = [...process.argv];
865
+ const args = argv.slice(2);
866
+ if (args[0] === "graphs" &&
867
+ (args.length === 1 || (args[1].startsWith("-") && args[1] !== "-h" && args[1] !== "--help"))) {
868
+ argv.splice(3, 0, "list");
795
869
  }
870
+ await buildProgram().parseAsync(argv);
796
871
  }
797
872
  main().catch((error) => {
798
873
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);