@heyditto/cli 1.5.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,63 +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>] [--subscribe <@graph>] [<@graph>...] [--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 init --agent --subscribe @minos ...pre-subscribed to public graph(s)
109
- heyditto init --agent @minos @a,@b (positional form; repeatable /
110
- comma-separated; '@' optional)
111
- heyditto login [<key>] [--paste] [--stdin] Save an API key to ${authFilePath()}
112
- heyditto logout Delete the saved key
113
- heyditto status [--output <format>] Show endpoint, key source, live tools
114
- heyditto config Print MCP client config snippet
115
-
116
- Other:
117
- heyditto help Show this message
118
-
119
- Note: on macOS, Apple ships /usr/bin/ditto (a file-copy utility). If 'ditto'
120
- runs the wrong tool, install with 'npm i -g @heyditto/cli' and invoke as
121
- 'heyditto' (alias bin), or check 'type -a ditto' to disambiguate.
122
-
123
- Environment:
124
- DITTO_API_KEY Optional override (takes precedence over the saved key).
125
- Run 'heyditto init --agent --json' for no-human setup, or get
126
- a human-owned key at ${newKeyURL()}.
127
- DITTO_API_BASE Optional. Defaults to https://api.heyditto.ai.
128
- DITTO_CONFIG_DIR Optional. Defaults to $XDG_CONFIG_HOME/heyditto/cli or
129
- ~/.config/heyditto/cli.
130
- `;
131
- }
132
98
  async function getClient() {
133
99
  const { key, source } = await resolveApiKey();
134
100
  if (!key) {
@@ -164,11 +130,6 @@ async function callAndPrint(name, args, format) {
164
130
  await client.close();
165
131
  }
166
132
  }
167
- function requirePositionals(positionals, minimum, label) {
168
- if (positionals.length < minimum) {
169
- throw new Error(`${label}: expected at least ${minimum} argument(s), got ${positionals.length}`);
170
- }
171
- }
172
133
  async function readKeyFromStdin() {
173
134
  return new Promise((resolve, reject) => {
174
135
  let buf = "";
@@ -192,28 +153,19 @@ function openInBrowser(url) {
192
153
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
193
154
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
194
155
  child.on("error", () => {
195
- /* swallow — best-effort */
156
+ /* swallow: best-effort */
196
157
  });
197
158
  child.unref();
198
159
  }
199
- async function cmdLogin(rest) {
200
- const { values, positionals } = parseArgs({
201
- args: rest,
202
- options: {
203
- paste: { type: "boolean", default: false },
204
- stdin: { type: "boolean", default: false },
205
- output: outputOption,
206
- },
207
- allowPositionals: true,
208
- });
209
- parseOutputFormat(values.output); // validate but ignored — login is interactive
210
- let key = positionals[0]?.trim();
211
- 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) {
212
164
  key = (await readKeyFromStdin()).trim();
213
165
  }
214
166
  else if (!key) {
215
- if (values.paste) {
216
- process.stderr.write(`Opening ${newKeyURL()} in your browser…\n`);
167
+ if (options.paste) {
168
+ process.stderr.write(`Opening ${newKeyURL()} in your browser...\n`);
217
169
  openInBrowser(newKeyURL());
218
170
  }
219
171
  if (!process.stdin.isTTY) {
@@ -224,7 +176,7 @@ async function cmdLogin(rest) {
224
176
  if (!key)
225
177
  throw new Error("no key provided");
226
178
  if (!key.startsWith("ditto_mcp_")) {
227
- 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`);
228
180
  }
229
181
  await writeStoredKey(key);
230
182
  process.stdout.write(`Saved key to ${authFilePath()}\n`);
@@ -236,27 +188,16 @@ async function cmdLogin(rest) {
236
188
  function defaultAgentCaller() {
237
189
  return process.env.DITTO_AGENT_CALLER?.trim() || process.env.CURSOR_AGENT?.trim() || "agent";
238
190
  }
239
- async function cmdInit(rest) {
240
- const { values, positionals } = parseArgs({
241
- args: rest,
242
- options: {
243
- agent: { type: "boolean", default: false },
244
- "agent-caller": { type: "string" },
245
- subscribe: { type: "string", multiple: true },
246
- json: { type: "boolean", default: false },
247
- output: outputOption,
248
- },
249
- allowPositionals: true,
250
- });
251
- const output = values.json ? "json" : parseOutputFormat(values.output);
252
- if (!values.agent) {
191
+ async function cmdInit(graphs, options) {
192
+ const output = options.json ? "json" : parseOutputFormat(options.output);
193
+ if (!options.agent) {
253
194
  throw new Error("init currently supports only --agent");
254
195
  }
255
196
  // --subscribe pre-subscribes the new agent to public foundation knowledge
256
197
  // graphs (e.g. the @minos mentor KG). Accepts repeats and comma-separated
257
198
  // lists, plus bare positional graph names: --subscribe @minos @a,@b.
258
199
  // De-duped; '@' optional.
259
- const subscribeGraphs = Array.from(new Set([...(values.subscribe ?? []), ...positionals]
200
+ const subscribeGraphs = Array.from(new Set([...(options.subscribe ?? []), ...graphs]
260
201
  .flatMap((v) => v.split(","))
261
202
  .map((g) => g.trim().replace(/^@/, ""))
262
203
  .filter((g) => g.length > 0)));
@@ -285,7 +226,7 @@ async function cmdInit(rest) {
285
226
  if (stored?.apiKey) {
286
227
  throw new Error(`a Ditto API key is already saved at ${authFilePath()}; run 'heyditto logout' before creating an agent account`);
287
228
  }
288
- const agentCaller = values["agent-caller"]?.trim() || defaultAgentCaller();
229
+ const agentCaller = options.agentCaller?.trim() || defaultAgentCaller();
289
230
  const response = await fetch(agentSignupURL(), {
290
231
  method: "POST",
291
232
  headers: {
@@ -364,13 +305,8 @@ async function cmdInit(rest) {
364
305
  }
365
306
  process.stdout.write(`Claim later: ${signup.claimURL}\n`);
366
307
  }
367
- async function cmdLogout(rest) {
368
- const { values } = parseArgs({
369
- args: rest,
370
- options: { output: outputOption },
371
- allowPositionals: true,
372
- });
373
- parseOutputFormat(values.output);
308
+ async function cmdLogout(options) {
309
+ parseOutputFormat(options.output);
374
310
  const removed = await clearStoredKey();
375
311
  if (removed) {
376
312
  process.stdout.write(`Removed ${authFilePath()}\n`);
@@ -382,225 +318,183 @@ async function cmdLogout(rest) {
382
318
  process.stderr.write(`note: DITTO_API_KEY is still set in your environment and will continue to be used.\n`);
383
319
  }
384
320
  }
385
- async function cmdSave(rest) {
386
- const { values, positionals } = parseArgs({
387
- args: rest,
388
- options: {
389
- source: { type: "string", default: "cli" },
390
- "source-context": { type: "string" },
391
- output: outputOption,
392
- },
393
- allowPositionals: true,
394
- });
395
- const format = parseOutputFormat(values.output);
396
- requirePositionals(positionals, 1, "save");
321
+ async function cmdSave(content, options) {
322
+ const format = parseOutputFormat(options.output);
397
323
  await callAndPrint("save_memory", {
398
- content: positionals.join(" "),
399
- source: values.source,
400
- sourceContext: values["source-context"],
324
+ content: content.join(" "),
325
+ source: options.source ?? "cli",
326
+ sourceContext: options.sourceContext,
401
327
  }, format);
402
328
  }
403
- async function cmdSearch(rest) {
404
- const { values, positionals } = parseArgs({
405
- args: rest,
406
- options: {
407
- "include-public": { type: "boolean", default: false },
408
- "filter-username": { type: "string" },
409
- output: outputOption,
410
- },
411
- allowPositionals: true,
412
- });
413
- const format = parseOutputFormat(values.output);
414
- requirePositionals(positionals, 1, "search");
415
- const args = { queries: positionals };
416
- if (values["include-public"])
329
+ async function cmdSearch(queries, options) {
330
+ const format = parseOutputFormat(options.output);
331
+ const args = { queries };
332
+ if (options.includePublic)
417
333
  args.includePublic = true;
418
- if (values["filter-username"])
419
- args.filterUsername = values["filter-username"];
334
+ if (options.filterUsername)
335
+ args.filterUsername = options.filterUsername;
420
336
  await callAndPrint("search_memories", args, format);
421
337
  }
422
- async function cmdFetch(rest) {
423
- const { values, positionals } = parseArgs({
424
- args: rest,
425
- options: { "memory-format": { type: "string" }, output: outputOption },
426
- allowPositionals: true,
427
- });
428
- const format = parseOutputFormat(values.output);
429
- const memoryFormat = parseMemoryFormat(values["memory-format"]);
430
- requirePositionals(positionals, 1, "fetch");
431
- await callAndPrint("fetch_memories", { ids: positionals, format: memoryFormat }, format);
432
- }
433
- async function cmdList(rest) {
434
- const { values } = parseArgs({
435
- args: rest,
436
- options: {
437
- username: { type: "string" },
438
- limit: { type: "string" },
439
- offset: { type: "string" },
440
- source: { type: "string" },
441
- output: outputOption,
442
- },
443
- allowPositionals: true,
444
- });
445
- 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);
446
345
  const args = {};
447
- if (values.username)
448
- args.username = values.username;
449
- const limit = parseIntegerOption(values.limit, "--limit");
346
+ if (options.username)
347
+ args.username = options.username;
348
+ const limit = parseIntegerOption(options.limit, "--limit");
450
349
  if (limit !== undefined)
451
350
  args.limit = limit;
452
- const offset = parseIntegerOption(values.offset, "--offset");
351
+ const offset = parseIntegerOption(options.offset, "--offset");
453
352
  if (offset !== undefined)
454
353
  args.offset = offset;
455
- if (values.source)
456
- args.source = values.source;
354
+ if (options.source)
355
+ args.source = options.source;
457
356
  await callAndPrint("list_memories", args, format);
458
357
  }
459
- async function cmdUpdate(rest) {
460
- const { values, positionals } = parseArgs({
461
- args: rest,
462
- options: {
463
- content: { type: "string" },
464
- "content-file": { type: "string" },
465
- title: { type: "string" },
466
- "source-context": { type: "string" },
467
- "edits-json": { type: "string" },
468
- "edits-file": { type: "string" },
469
- "base-revision": { type: "string" },
470
- output: outputOption,
471
- },
472
- allowPositionals: true,
473
- });
474
- const format = parseOutputFormat(values.output);
475
- requirePositionals(positionals, 1, "update");
476
- if (values.content && values["content-file"]) {
358
+ async function cmdUpdate(id, options) {
359
+ const format = parseOutputFormat(options.output);
360
+ if (options.content && options.contentFile) {
477
361
  throw new Error("update: use either --content or --content-file, not both");
478
362
  }
479
- if (values["edits-json"] && values["edits-file"]) {
363
+ if (options.editsJson && options.editsFile) {
480
364
  throw new Error("update: use either --edits-json or --edits-file, not both");
481
365
  }
482
- if ((values.content || values["content-file"]) && (values["edits-json"] || values["edits-file"])) {
366
+ if ((options.content || options.contentFile) && (options.editsJson || options.editsFile)) {
483
367
  throw new Error("update: content replacement and block edits are mutually exclusive");
484
368
  }
485
- const args = { memoryId: positionals[0] };
486
- if (values.content)
487
- args.content = values.content;
488
- if (values["content-file"])
489
- args.content = await readTextFile(values["content-file"], "--content-file");
490
- if (values.title !== undefined)
491
- args.title = values.title;
492
- if (values["source-context"] !== undefined)
493
- args.sourceContext = values["source-context"];
494
- if (values["edits-json"] || values["edits-file"]) {
495
- const raw = values["edits-json"] ?? (await readTextFile(values["edits-file"], "--edits-file"));
496
- args.edits = parseJSONOption(raw, values["edits-json"] ? "--edits-json" : "--edits-file");
497
- 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");
498
382
  if (baseRevision === undefined) {
499
383
  throw new Error("update: --base-revision is required with block edits");
500
384
  }
501
385
  args.baseRevision = baseRevision;
502
386
  }
503
387
  else {
504
- const baseRevision = parseIntegerOption(values["base-revision"], "--base-revision");
388
+ const baseRevision = parseIntegerOption(options.baseRevision, "--base-revision");
505
389
  if (baseRevision !== undefined)
506
390
  args.baseRevision = baseRevision;
507
391
  }
508
392
  await callAndPrint("update_memory", args, format);
509
393
  }
510
- async function cmdPublish(rest) {
511
- const { values, positionals } = parseArgs({
512
- args: rest,
513
- options: {
514
- title: { type: "string" },
515
- "privacy-mode": { type: "string" },
516
- output: outputOption,
517
- },
518
- allowPositionals: true,
519
- });
520
- const format = parseOutputFormat(values.output);
521
- requirePositionals(positionals, 1, "publish");
522
- const args = { memoryId: positionals[0] };
523
- if (values.title !== undefined)
524
- args.title = values.title;
525
- if (values["privacy-mode"] !== undefined)
526
- 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;
527
401
  await callAndPrint("publish_memory", args, format);
528
402
  }
529
- async function cmdUnpublish(rest) {
530
- const { values, positionals } = parseArgs({
531
- args: rest,
532
- options: {
533
- "memory-id": { type: "string" },
534
- "share-id": { type: "string" },
535
- output: outputOption,
536
- },
537
- allowPositionals: true,
538
- });
539
- const format = parseOutputFormat(values.output);
540
- 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);
541
406
  if (provided.length !== 1) {
542
407
  throw new Error("unpublish: provide exactly one of --memory-id, --share-id, or positional id");
543
408
  }
544
409
  const args = {};
545
- if (values["memory-id"]) {
546
- args.memoryId = values["memory-id"];
410
+ if (options.memoryId) {
411
+ args.memoryId = options.memoryId;
547
412
  }
548
- else if (values["share-id"]) {
549
- args.shareId = values["share-id"];
413
+ else if (options.shareId) {
414
+ args.shareId = options.shareId;
550
415
  }
551
416
  else {
552
- args.memoryId = positionals[0];
417
+ args.memoryId = id;
553
418
  }
554
419
  await callAndPrint("unpublish_memory", args, format);
555
420
  }
556
- async function cmdSubjects(rest) {
557
- const { values, positionals } = parseArgs({
558
- args: rest,
559
- options: { "top-k": { type: "string" }, output: outputOption },
560
- allowPositionals: true,
561
- });
562
- const format = parseOutputFormat(values.output);
563
- requirePositionals(positionals, 1, "subjects");
564
- const args = { query: positionals.join(" ") };
565
- 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");
566
425
  if (topK !== undefined)
567
426
  args.topK = topK;
568
427
  await callAndPrint("search_subjects", args, format);
569
428
  }
570
- async function cmdMemories(rest) {
571
- const { values, positionals } = parseArgs({
572
- args: rest,
573
- options: { query: { type: "string" }, output: outputOption },
574
- allowPositionals: true,
575
- });
576
- const format = parseOutputFormat(values.output);
577
- requirePositionals(positionals, 1, "memories");
578
- const args = { subjectIds: positionals };
579
- if (values.query)
580
- 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;
581
434
  await callAndPrint("search_memories_in_subjects", args, format);
582
435
  }
583
- async function cmdNetwork(rest) {
584
- const { values, positionals } = parseArgs({
585
- args: rest,
586
- options: { limit: { type: "string" }, output: outputOption },
587
- allowPositionals: true,
588
- });
589
- const format = parseOutputFormat(values.output);
590
- requirePositionals(positionals, 1, "network");
591
- const args = { pairId: positionals[0] };
592
- 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");
593
440
  if (limit !== undefined)
594
441
  args.limit = limit;
595
442
  await callAndPrint("get_memory_network", args, format);
596
443
  }
597
- async function cmdStatus(rest) {
598
- const { values } = parseArgs({
599
- args: rest,
600
- options: { output: outputOption },
601
- allowPositionals: true,
602
- });
603
- 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);
604
498
  const [{ key, source }, stored] = await Promise.all([resolveApiKey(), readStoredAuth()]);
605
499
  const report = {
606
500
  package: packageName,
@@ -664,20 +558,15 @@ async function cmdStatus(rest) {
664
558
  lines.push(`connect: ok`, `tools: unavailable (tools/list failed: ${report.toolsError})`);
665
559
  }
666
560
  else if (report.connect && !report.connect.ok) {
667
- lines.push(`connect: FAILED — ${report.connect.error}`);
561
+ lines.push(`connect: FAILED - ${report.connect.error}`);
668
562
  }
669
563
  if (report.agent?.claimURL) {
670
564
  lines.push(`agent: unclaimed (${report.agent.caller || "agent"})`, `claim: ${report.agent.claimURL}`);
671
565
  }
672
566
  process.stdout.write(`${lines.join("\n")}\n`);
673
567
  }
674
- function cmdConfig(rest) {
675
- const { values } = parseArgs({
676
- args: rest,
677
- options: { output: outputOption },
678
- allowPositionals: true,
679
- });
680
- parseOutputFormat(values.output); // accepted; output is always JSON
568
+ function cmdConfig(options) {
569
+ parseOutputFormat(options.output); // accepted; output is always JSON
681
570
  const config = {
682
571
  mcpServers: {
683
572
  ditto: {
@@ -689,141 +578,296 @@ function cmdConfig(rest) {
689
578
  };
690
579
  process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
691
580
  }
692
- // cmdGraphs manages the public knowledge graphs this account is subscribed to,
693
- // mirroring the MCP subscription tools. Subscriptions only ever cover OTHER
694
- // users' public graphs (by @username); this command cannot touch the account's
695
- // own KG or its app KG, since those are not subscriptions.
696
- async function cmdGraphs(rest) {
697
- const sub = rest[0];
698
- const subRest = rest.slice(1);
699
- switch (sub) {
700
- case "create": {
701
- // Provision a NEW dedicated graph you own + get a key scoped to only it.
702
- const { values, positionals } = parseArgs({
703
- args: subRest,
704
- options: { output: outputOption },
705
- allowPositionals: true,
706
- });
707
- requirePositionals(positionals, 1, "graphs create");
708
- await callAndPrint("create_dedicated_graph", { name: positionals.join(" ") }, parseOutputFormat(values.output));
709
- return;
710
- }
711
- case undefined:
712
- case "list": {
713
- const { values } = parseArgs({
714
- args: subRest,
715
- options: { output: outputOption },
716
- allowPositionals: true,
717
- });
718
- await callAndPrint("list_knowledge_graph_subscriptions", {}, parseOutputFormat(values.output));
719
- return;
720
- }
721
- case "subscribers": {
722
- const { values } = parseArgs({
723
- args: subRest,
724
- options: { output: outputOption },
725
- allowPositionals: true,
726
- });
727
- await callAndPrint("list_knowledge_graph_subscribers", {}, parseOutputFormat(values.output));
728
- return;
729
- }
730
- case "add": {
731
- const { values, positionals } = parseArgs({
732
- args: subRest,
733
- options: { output: outputOption },
734
- allowPositionals: true,
735
- });
736
- requirePositionals(positionals, 1, "graphs add");
737
- if (positionals.length > 1) {
738
- throw new Error(`graphs add: expected exactly 1 username, got ${positionals.length}`);
739
- }
740
- await callAndPrint("subscribe_knowledge_graph", { username: positionals[0] }, parseOutputFormat(values.output));
741
- return;
742
- }
743
- case "remove": {
744
- const { values, positionals } = parseArgs({
745
- args: subRest,
746
- options: { output: outputOption },
747
- allowPositionals: true,
748
- });
749
- requirePositionals(positionals, 1, "graphs remove");
750
- if (positionals.length > 1) {
751
- throw new Error(`graphs remove: expected exactly 1 username, got ${positionals.length}`);
752
- }
753
- await callAndPrint("unsubscribe_knowledge_graph", { username: positionals[0] }, parseOutputFormat(values.output));
754
- return;
755
- }
756
- default:
757
- throw new Error(`graphs: unknown subcommand "${sub}" (expected: create <name>, list, add <@user>, remove <@user>, subscribers)`);
758
- }
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;
759
862
  }
760
863
  async function main() {
761
- const argv = process.argv.slice(2);
762
- const command = argv[0];
763
- const rest = argv.slice(1);
764
- switch (command) {
765
- case "init":
766
- await cmdInit(rest);
767
- return;
768
- case "save":
769
- await cmdSave(rest);
770
- return;
771
- case "search":
772
- await cmdSearch(rest);
773
- return;
774
- case "fetch":
775
- await cmdFetch(rest);
776
- return;
777
- case "list":
778
- await cmdList(rest);
779
- return;
780
- case "update":
781
- await cmdUpdate(rest);
782
- return;
783
- case "publish":
784
- await cmdPublish(rest);
785
- return;
786
- case "unpublish":
787
- await cmdUnpublish(rest);
788
- return;
789
- case "subjects":
790
- await cmdSubjects(rest);
791
- return;
792
- case "memories":
793
- await cmdMemories(rest);
794
- return;
795
- case "network":
796
- await cmdNetwork(rest);
797
- return;
798
- case "graphs":
799
- await cmdGraphs(rest);
800
- return;
801
- case "login":
802
- await cmdLogin(rest);
803
- return;
804
- case "logout":
805
- await cmdLogout(rest);
806
- return;
807
- case "status":
808
- await cmdStatus(rest);
809
- return;
810
- case "config":
811
- cmdConfig(rest);
812
- return;
813
- case undefined:
814
- case "help":
815
- case "--help":
816
- case "-h":
817
- process.stdout.write(usage());
818
- return;
819
- case "--version":
820
- case "-v":
821
- process.stdout.write(`${packageVersion}\n`);
822
- return;
823
- default:
824
- process.stderr.write(`Unknown command: ${command}\n\n${usage()}`);
825
- 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");
826
869
  }
870
+ await buildProgram().parseAsync(argv);
827
871
  }
828
872
  main().catch((error) => {
829
873
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);