@heyditto/cli 1.5.0 → 1.7.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,68 +92,14 @@ 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) {
135
101
  process.stderr.write(`error: no Ditto API key configured.\n\n` +
136
- ` Run: heyditto init --agent --json\n` +
102
+ ` Run: heyditto init --json\n` +
137
103
  ` Or save an existing key with: heyditto login <key>\n` +
138
104
  ` Human-owned keys are available at ${newKeyURL()}.\n`);
139
105
  process.exit(1);
@@ -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,17 @@ 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) {
253
- throw new Error("init currently supports only --agent");
191
+ async function cmdInit(graphs, options) {
192
+ const output = options.json ? "json" : parseOutputFormat(options.output);
193
+ // --agent is accepted for backward compatibility. Agent init is the default.
194
+ if (options.agentCaller && options.name && options.agentCaller !== options.name) {
195
+ throw new Error("init: use either --name or --agent-caller, not both");
254
196
  }
255
197
  // --subscribe pre-subscribes the new agent to public foundation knowledge
256
198
  // graphs (e.g. the @minos mentor KG). Accepts repeats and comma-separated
257
199
  // lists, plus bare positional graph names: --subscribe @minos @a,@b.
258
200
  // De-duped; '@' optional.
259
- const subscribeGraphs = Array.from(new Set([...(values.subscribe ?? []), ...positionals]
201
+ const subscribeGraphs = Array.from(new Set([...(options.subscribe ?? []), ...graphs]
260
202
  .flatMap((v) => v.split(","))
261
203
  .map((g) => g.trim().replace(/^@/, ""))
262
204
  .filter((g) => g.length > 0)));
@@ -285,7 +227,7 @@ async function cmdInit(rest) {
285
227
  if (stored?.apiKey) {
286
228
  throw new Error(`a Ditto API key is already saved at ${authFilePath()}; run 'heyditto logout' before creating an agent account`);
287
229
  }
288
- const agentCaller = values["agent-caller"]?.trim() || defaultAgentCaller();
230
+ const agentCaller = options.name?.trim() || options.agentCaller?.trim() || defaultAgentCaller();
289
231
  const response = await fetch(agentSignupURL(), {
290
232
  method: "POST",
291
233
  headers: {
@@ -364,13 +306,8 @@ async function cmdInit(rest) {
364
306
  }
365
307
  process.stdout.write(`Claim later: ${signup.claimURL}\n`);
366
308
  }
367
- async function cmdLogout(rest) {
368
- const { values } = parseArgs({
369
- args: rest,
370
- options: { output: outputOption },
371
- allowPositionals: true,
372
- });
373
- parseOutputFormat(values.output);
309
+ async function cmdLogout(options) {
310
+ parseOutputFormat(options.output);
374
311
  const removed = await clearStoredKey();
375
312
  if (removed) {
376
313
  process.stdout.write(`Removed ${authFilePath()}\n`);
@@ -382,225 +319,183 @@ async function cmdLogout(rest) {
382
319
  process.stderr.write(`note: DITTO_API_KEY is still set in your environment and will continue to be used.\n`);
383
320
  }
384
321
  }
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");
322
+ async function cmdSave(content, options) {
323
+ const format = parseOutputFormat(options.output);
397
324
  await callAndPrint("save_memory", {
398
- content: positionals.join(" "),
399
- source: values.source,
400
- sourceContext: values["source-context"],
325
+ content: content.join(" "),
326
+ source: options.source ?? "cli",
327
+ sourceContext: options.sourceContext,
401
328
  }, format);
402
329
  }
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"])
330
+ async function cmdSearch(queries, options) {
331
+ const format = parseOutputFormat(options.output);
332
+ const args = { queries };
333
+ if (options.includePublic)
417
334
  args.includePublic = true;
418
- if (values["filter-username"])
419
- args.filterUsername = values["filter-username"];
335
+ if (options.filterUsername)
336
+ args.filterUsername = options.filterUsername;
420
337
  await callAndPrint("search_memories", args, format);
421
338
  }
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);
339
+ async function cmdFetch(ids, options) {
340
+ const format = parseOutputFormat(options.output);
341
+ const memoryFormat = parseMemoryFormat(options.memoryFormat);
342
+ await callAndPrint("fetch_memories", { ids, format: memoryFormat }, format);
343
+ }
344
+ async function cmdList(options) {
345
+ const format = parseOutputFormat(options.output);
446
346
  const args = {};
447
- if (values.username)
448
- args.username = values.username;
449
- const limit = parseIntegerOption(values.limit, "--limit");
347
+ if (options.username)
348
+ args.username = options.username;
349
+ const limit = parseIntegerOption(options.limit, "--limit");
450
350
  if (limit !== undefined)
451
351
  args.limit = limit;
452
- const offset = parseIntegerOption(values.offset, "--offset");
352
+ const offset = parseIntegerOption(options.offset, "--offset");
453
353
  if (offset !== undefined)
454
354
  args.offset = offset;
455
- if (values.source)
456
- args.source = values.source;
355
+ if (options.source)
356
+ args.source = options.source;
457
357
  await callAndPrint("list_memories", args, format);
458
358
  }
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"]) {
359
+ async function cmdUpdate(id, options) {
360
+ const format = parseOutputFormat(options.output);
361
+ if (options.content && options.contentFile) {
477
362
  throw new Error("update: use either --content or --content-file, not both");
478
363
  }
479
- if (values["edits-json"] && values["edits-file"]) {
364
+ if (options.editsJson && options.editsFile) {
480
365
  throw new Error("update: use either --edits-json or --edits-file, not both");
481
366
  }
482
- if ((values.content || values["content-file"]) && (values["edits-json"] || values["edits-file"])) {
367
+ if ((options.content || options.contentFile) && (options.editsJson || options.editsFile)) {
483
368
  throw new Error("update: content replacement and block edits are mutually exclusive");
484
369
  }
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");
370
+ const args = { memoryId: id };
371
+ if (options.content)
372
+ args.content = options.content;
373
+ if (options.contentFile)
374
+ args.content = await readTextFile(options.contentFile, "--content-file");
375
+ if (options.title !== undefined)
376
+ args.title = options.title;
377
+ if (options.sourceContext !== undefined)
378
+ args.sourceContext = options.sourceContext;
379
+ if (options.editsJson || options.editsFile) {
380
+ const raw = options.editsJson ?? (await readTextFile(options.editsFile, "--edits-file"));
381
+ args.edits = parseJSONOption(raw, options.editsJson ? "--edits-json" : "--edits-file");
382
+ const baseRevision = parseIntegerOption(options.baseRevision, "--base-revision");
498
383
  if (baseRevision === undefined) {
499
384
  throw new Error("update: --base-revision is required with block edits");
500
385
  }
501
386
  args.baseRevision = baseRevision;
502
387
  }
503
388
  else {
504
- const baseRevision = parseIntegerOption(values["base-revision"], "--base-revision");
389
+ const baseRevision = parseIntegerOption(options.baseRevision, "--base-revision");
505
390
  if (baseRevision !== undefined)
506
391
  args.baseRevision = baseRevision;
507
392
  }
508
393
  await callAndPrint("update_memory", args, format);
509
394
  }
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"];
395
+ async function cmdPublish(id, options) {
396
+ const format = parseOutputFormat(options.output);
397
+ const args = { memoryId: id };
398
+ if (options.title !== undefined)
399
+ args.title = options.title;
400
+ if (options.privacyMode !== undefined)
401
+ args.privacyMode = options.privacyMode;
527
402
  await callAndPrint("publish_memory", args, format);
528
403
  }
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);
404
+ async function cmdUnpublish(id, options) {
405
+ const format = parseOutputFormat(options.output);
406
+ const provided = [options.memoryId, options.shareId, id].filter(Boolean);
541
407
  if (provided.length !== 1) {
542
408
  throw new Error("unpublish: provide exactly one of --memory-id, --share-id, or positional id");
543
409
  }
544
410
  const args = {};
545
- if (values["memory-id"]) {
546
- args.memoryId = values["memory-id"];
411
+ if (options.memoryId) {
412
+ args.memoryId = options.memoryId;
547
413
  }
548
- else if (values["share-id"]) {
549
- args.shareId = values["share-id"];
414
+ else if (options.shareId) {
415
+ args.shareId = options.shareId;
550
416
  }
551
417
  else {
552
- args.memoryId = positionals[0];
418
+ args.memoryId = id;
553
419
  }
554
420
  await callAndPrint("unpublish_memory", args, format);
555
421
  }
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");
422
+ async function cmdSubjects(query, options) {
423
+ const format = parseOutputFormat(options.output);
424
+ const args = { query: query.join(" ") };
425
+ const topK = parseIntegerOption(options.topK, "--top-k");
566
426
  if (topK !== undefined)
567
427
  args.topK = topK;
568
428
  await callAndPrint("search_subjects", args, format);
569
429
  }
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;
430
+ async function cmdMemories(subjectIds, options) {
431
+ const format = parseOutputFormat(options.output);
432
+ const args = { subjectIds };
433
+ if (options.query)
434
+ args.query = options.query;
581
435
  await callAndPrint("search_memories_in_subjects", args, format);
582
436
  }
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");
437
+ async function cmdNetwork(pairId, options) {
438
+ const format = parseOutputFormat(options.output);
439
+ const args = { pairId };
440
+ const limit = parseIntegerOption(options.limit, "--limit");
593
441
  if (limit !== undefined)
594
442
  args.limit = limit;
595
443
  await callAndPrint("get_memory_network", args, format);
596
444
  }
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);
445
+ async function cmdFriends(options) {
446
+ await callAndPrint("list_friends", {}, parseOutputFormat(options.output));
447
+ }
448
+ async function cmdKnowledgeGraphs(options) {
449
+ await callAndPrint("list_knowledge_graphs", {}, parseOutputFormat(options.output));
450
+ }
451
+ async function cmdMyMemories(options) {
452
+ const args = {};
453
+ const limit = parseIntegerOption(options.limit, "--limit");
454
+ if (limit !== undefined)
455
+ args.limit = limit;
456
+ const offset = parseIntegerOption(options.offset, "--offset");
457
+ if (offset !== undefined)
458
+ args.offset = offset;
459
+ if (options.source)
460
+ args.source = options.source;
461
+ await callAndPrint("list_my_memories", args, parseOutputFormat(options.output));
462
+ }
463
+ async function cmdSubjectEdges(subjectId, options) {
464
+ const args = { subjectId };
465
+ if (options.kg)
466
+ args.kg = options.kg;
467
+ const limit = parseIntegerOption(options.limit, "--limit");
468
+ if (limit !== undefined)
469
+ args.limit = limit;
470
+ const minWeight = parseNumberOption(options.minWeight, "--min-weight");
471
+ if (minWeight !== undefined)
472
+ args.minWeight = minWeight;
473
+ await callAndPrint("get_subject_edges", args, parseOutputFormat(options.output));
474
+ }
475
+ async function cmdGraphSharing(options) {
476
+ if (!!options.enable === !!options.disable) {
477
+ throw new Error("sharing: provide exactly one of --enable or --disable");
478
+ }
479
+ const args = {
480
+ publicSubscriptionsEnabled: !!options.enable,
481
+ };
482
+ if (options.title !== undefined)
483
+ args.title = options.title;
484
+ if (options.description !== undefined)
485
+ args.description = options.description;
486
+ await callAndPrint("set_knowledge_graph_sharing", args, parseOutputFormat(options.output));
487
+ }
488
+ async function cmdDelete(memoryId, options) {
489
+ if (!options.confirm) {
490
+ throw new Error("delete: pass --confirm to permanently delete this memory");
491
+ }
492
+ const args = { memoryId, confirm: true };
493
+ if (options.kg)
494
+ args.kg = options.kg;
495
+ await callAndPrint("delete_memory", args, parseOutputFormat(options.output));
496
+ }
497
+ async function cmdStatus(options) {
498
+ const format = parseOutputFormat(options.output);
604
499
  const [{ key, source }, stored] = await Promise.all([resolveApiKey(), readStoredAuth()]);
605
500
  const report = {
606
501
  package: packageName,
@@ -655,7 +550,7 @@ async function cmdStatus(rest) {
655
550
  `api key: ${report.apiKey.present ? "set" : "MISSING"} (source: ${report.apiKey.source})`,
656
551
  ];
657
552
  if (!report.apiKey.present) {
658
- lines.push(``, `Run 'heyditto init --agent --json' for no-human setup, or get a key at ${newKeyURL()} and run 'heyditto login <key>'.`);
553
+ lines.push(``, `Run 'heyditto init --json' for no-human setup, or get a key at ${newKeyURL()} and run 'heyditto login <key>'.`);
659
554
  }
660
555
  else if (report.tools) {
661
556
  lines.push(`tools: ${report.tools.join(", ")}`);
@@ -664,20 +559,15 @@ async function cmdStatus(rest) {
664
559
  lines.push(`connect: ok`, `tools: unavailable (tools/list failed: ${report.toolsError})`);
665
560
  }
666
561
  else if (report.connect && !report.connect.ok) {
667
- lines.push(`connect: FAILED — ${report.connect.error}`);
562
+ lines.push(`connect: FAILED - ${report.connect.error}`);
668
563
  }
669
564
  if (report.agent?.claimURL) {
670
565
  lines.push(`agent: unclaimed (${report.agent.caller || "agent"})`, `claim: ${report.agent.claimURL}`);
671
566
  }
672
567
  process.stdout.write(`${lines.join("\n")}\n`);
673
568
  }
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
569
+ function cmdConfig(options) {
570
+ parseOutputFormat(options.output); // accepted; output is always JSON
681
571
  const config = {
682
572
  mcpServers: {
683
573
  ditto: {
@@ -689,141 +579,298 @@ function cmdConfig(rest) {
689
579
  };
690
580
  process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
691
581
  }
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
- }
582
+ // These commands mirror the MCP subscription tools. Subscriptions only ever
583
+ // cover other users' public graphs by @username; this cannot touch the account's
584
+ // own graph or app graph, since those are not subscriptions.
585
+ async function cmdGraphsCreate(nameParts, options) {
586
+ await callAndPrint("create_dedicated_graph", { name: nameParts.join(" ") }, parseOutputFormat(options.output));
587
+ }
588
+ async function cmdGraphsList(options) {
589
+ await callAndPrint("list_knowledge_graph_subscriptions", {}, parseOutputFormat(options.output));
590
+ }
591
+ async function cmdGraphsSubscribers(options) {
592
+ await callAndPrint("list_knowledge_graph_subscribers", {}, parseOutputFormat(options.output));
593
+ }
594
+ async function cmdGraphsAdd(username, options) {
595
+ await callAndPrint("subscribe_knowledge_graph", { username }, parseOutputFormat(options.output));
596
+ }
597
+ async function cmdGraphsRemove(username, options) {
598
+ await callAndPrint("unsubscribe_knowledge_graph", { username }, parseOutputFormat(options.output));
599
+ }
600
+ function addExamples(command, examples) {
601
+ return command.addHelpText("after", `\nExamples:\n${examples}`);
602
+ }
603
+ function buildProgram() {
604
+ const program = new Command();
605
+ program
606
+ .name("heyditto")
607
+ .description("Save, search, fetch, and traverse Ditto memories from the shell.")
608
+ .version(packageVersion, "-v, --version", "print the CLI version")
609
+ .helpCommand("help [command]", "show help for a command")
610
+ .showHelpAfterError()
611
+ .addHelpText("after", `
612
+ Notes:
613
+ On macOS, Apple ships /usr/bin/ditto (a file-copy utility). If 'ditto'
614
+ runs the wrong tool, install with 'npm i -g @heyditto/cli' and invoke as
615
+ 'heyditto' (alias bin), or check 'type -a ditto' to disambiguate.
616
+
617
+ Environment:
618
+ DITTO_API_KEY Optional override, taking precedence over the saved key.
619
+ DITTO_API_BASE Optional API base URL. Defaults to https://api.heyditto.ai.
620
+ DITTO_CONFIG_DIR Optional config directory. Defaults to $XDG_CONFIG_HOME/heyditto/cli
621
+ or ~/.config/heyditto/cli.
622
+ `);
623
+ addExamples(program
624
+ .command("save")
625
+ .description("save a memory")
626
+ .summary("save a memory")
627
+ .argument("<content...>", "memory content")
628
+ .option("--source <source>", "memory source", "cli")
629
+ .option("--source-context <context>", "source context, such as a filename")
630
+ .addOption(outputOption())
631
+ .action(cmdSave), ` heyditto save "Project X uses Bun + SolidJS"
632
+ heyditto save "$(cat note.md)" --source document --source-context note.md`);
633
+ addExamples(program
634
+ .command("search")
635
+ .description("search private memories, optionally public graphs")
636
+ .summary("search private memories, optionally public graphs")
637
+ .argument("<query...>", "one or more search queries")
638
+ .option("--include-public", "include public DittoHub memories")
639
+ .option("--filter-username <username>", "scope public results to a username")
640
+ .addOption(outputOption())
641
+ .action(cmdSearch), ` heyditto search "typescript preferences"
642
+ heyditto search "launch notes" --include-public --filter-username peyton`);
643
+ program
644
+ .command("fetch")
645
+ .description("fetch memories by id")
646
+ .summary("fetch memories by id")
647
+ .argument("<id...>", "memory ids or public share ids")
648
+ .addOption(memoryFormatOption())
649
+ .addOption(outputOption())
650
+ .action(cmdFetch);
651
+ program
652
+ .command("list")
653
+ .description("list memories or public publishes")
654
+ .summary("list memories or public publishes")
655
+ .option("--username <username>", "list public DittoHub publishes for a username")
656
+ .option("--limit <number>", "maximum number of results")
657
+ .option("--offset <number>", "result offset")
658
+ .option("--source <source>", "filter by memory source")
659
+ .addOption(outputOption())
660
+ .action(cmdList);
661
+ program
662
+ .command("my-memories")
663
+ .alias("list_my_memories")
664
+ .description("list only your saved memories")
665
+ .summary("list only your saved memories")
666
+ .option("--limit <number>", "maximum number of results")
667
+ .option("--offset <number>", "result offset")
668
+ .option("--source <source>", "filter by memory source")
669
+ .addOption(outputOption())
670
+ .action(cmdMyMemories);
671
+ program
672
+ .command("update")
673
+ .description("update a saved memory")
674
+ .summary("update a saved memory")
675
+ .argument("<id>", "memory id")
676
+ .option("--content <text>", "replacement memory content")
677
+ .option("--content-file <path>", "path to replacement memory content")
678
+ .option("--title <title>", "memory title")
679
+ .option("--source-context <context>", "source context")
680
+ .option("--edits-json <json>", "structured block edits as JSON")
681
+ .option("--edits-file <path>", "path to structured block edits JSON")
682
+ .option("--base-revision <number>", "base memory revision")
683
+ .addOption(outputOption())
684
+ .addHelpText("after", `
685
+ Examples:
686
+ heyditto update <memory-id> --content-file revised.md --output json
687
+ heyditto update <memory-id> --edits-file edits.json --base-revision 3 --output json`)
688
+ .action(cmdUpdate);
689
+ program
690
+ .command("publish")
691
+ .description("publish a memory to DittoHub")
692
+ .summary("publish a memory to DittoHub")
693
+ .argument("<id>", "memory id")
694
+ .option("--title <title>", "public title")
695
+ .option("--privacy-mode <mode>", "privacy mode: scan_and_block, scan_and_warn, or scan_and_redact")
696
+ .addOption(outputOption())
697
+ .action(cmdPublish);
698
+ program
699
+ .command("unpublish")
700
+ .description("remove an existing public share")
701
+ .summary("remove an existing public share")
702
+ .argument("[id]", "memory id")
703
+ .option("--memory-id <id>", "memory id")
704
+ .option("--share-id <id>", "share id")
705
+ .addOption(outputOption())
706
+ .action(cmdUnpublish);
707
+ program
708
+ .command("delete")
709
+ .alias("delete_memory")
710
+ .description("permanently delete a saved memory")
711
+ .summary("permanently delete a saved memory")
712
+ .argument("<memory-id>", "memory id")
713
+ .requiredOption("--confirm", "confirm permanent deletion")
714
+ .option("--kg <alias>", "knowledge graph alias")
715
+ .addOption(outputOption())
716
+ .action(cmdDelete);
717
+ program
718
+ .command("subjects")
719
+ .description("search the subject graph")
720
+ .summary("search the subject graph")
721
+ .argument("<query...>", "subject search query")
722
+ .option("--top-k <number>", "maximum number of subjects")
723
+ .addOption(outputOption())
724
+ .action(cmdSubjects);
725
+ program
726
+ .command("subject-edges")
727
+ .alias("get_subject_edges")
728
+ .description("list related subjects for a subject")
729
+ .summary("list related subjects for a subject")
730
+ .argument("<subject-id>", "subject id")
731
+ .option("--kg <alias>", "knowledge graph alias")
732
+ .option("--limit <number>", "maximum number of related subjects")
733
+ .option("--min-weight <number>", "minimum edge weight from 0 to 1")
734
+ .addOption(outputOption())
735
+ .action(cmdSubjectEdges);
736
+ program
737
+ .command("memories")
738
+ .description("fetch memory previews for subjects")
739
+ .summary("fetch memory previews for subjects")
740
+ .argument("<subject-id...>", "subject ids")
741
+ .option("--query <query>", "optional search query")
742
+ .addOption(outputOption())
743
+ .action(cmdMemories);
744
+ program
745
+ .command("network")
746
+ .description("traverse related memories")
747
+ .summary("traverse related memories")
748
+ .argument("<pair-id>", "memory pair id")
749
+ .option("--limit <number>", "maximum number of related memories")
750
+ .addOption(outputOption())
751
+ .action(cmdNetwork);
752
+ program
753
+ .command("friends")
754
+ .alias("list_friends")
755
+ .description("list Ditto friends")
756
+ .summary("list Ditto friends")
757
+ .addOption(outputOption())
758
+ .action(cmdFriends);
759
+ program
760
+ .command("knowledge-graphs")
761
+ .alias("list_knowledge_graphs")
762
+ .description("list readable knowledge graphs")
763
+ .summary("list readable knowledge graphs")
764
+ .addOption(outputOption())
765
+ .action(cmdKnowledgeGraphs);
766
+ program
767
+ .command("graph-sharing")
768
+ .alias("set_knowledge_graph_sharing")
769
+ .description("configure whether others can subscribe to your graph")
770
+ .summary("configure graph sharing")
771
+ .option("--enable", "allow public subscriptions to your graph")
772
+ .option("--disable", "disable public subscriptions to your graph")
773
+ .option("--title <title>", "subscribable graph title")
774
+ .option("--description <description>", "subscribable graph description")
775
+ .addOption(outputOption())
776
+ .action(cmdGraphSharing);
777
+ const graphs = program
778
+ .command("graphs")
779
+ .description("manage knowledge graph subscriptions")
780
+ .summary("manage knowledge graph subscriptions")
781
+ .showHelpAfterError()
782
+ .addHelpText("after", `
783
+ Subscriptions cover other users' public graphs by @username. They do not modify
784
+ your own graph or an app graph.`);
785
+ graphs
786
+ .command("create")
787
+ .description("create a dedicated graph you own")
788
+ .argument("<name...>", "graph name")
789
+ .addOption(outputOption())
790
+ .action(cmdGraphsCreate);
791
+ graphs
792
+ .command("list")
793
+ .description("list public graphs you're subscribed to")
794
+ .addOption(outputOption())
795
+ .action(cmdGraphsList);
796
+ graphs
797
+ .command("available")
798
+ .alias("list_knowledge_graphs")
799
+ .description("list readable knowledge graphs")
800
+ .addOption(outputOption())
801
+ .action(cmdKnowledgeGraphs);
802
+ graphs
803
+ .command("add")
804
+ .description("subscribe to a public graph")
805
+ .argument("<username>", "public graph username, with or without @")
806
+ .addOption(outputOption())
807
+ .action(cmdGraphsAdd);
808
+ graphs
809
+ .command("remove")
810
+ .description("unsubscribe from a public graph")
811
+ .argument("<username>", "public graph username, with or without @")
812
+ .addOption(outputOption())
813
+ .action(cmdGraphsRemove);
814
+ graphs
815
+ .command("subscribers")
816
+ .description("list who is subscribed to your graph")
817
+ .addOption(outputOption())
818
+ .action(cmdGraphsSubscribers);
819
+ graphs
820
+ .command("sharing")
821
+ .alias("set_knowledge_graph_sharing")
822
+ .description("configure whether others can subscribe to your graph")
823
+ .option("--enable", "allow public subscriptions to your graph")
824
+ .option("--disable", "disable public subscriptions to your graph")
825
+ .option("--title <title>", "subscribable graph title")
826
+ .option("--description <description>", "subscribable graph description")
827
+ .addOption(outputOption())
828
+ .action(cmdGraphSharing);
829
+ program
830
+ .command("init")
831
+ .description("initialize a claimable agent account")
832
+ .argument("[graph...]", "public graphs to subscribe to")
833
+ .addOption(new Option("--agent", "create a free, claimable agent account").hideHelp())
834
+ .option("--name <name>", "agent name")
835
+ .option("--agent-caller <name>", "agent name")
836
+ .addOption(new Option("--subscribe <graph>", "public graphs to subscribe to")
837
+ .argParser((value, previous) => [...(previous ?? []), value]))
838
+ .option("--json", "print machine-readable output")
839
+ .addOption(hiddenOutputOption())
840
+ .action(cmdInit);
841
+ program
842
+ .command("login")
843
+ .description("save an API key")
844
+ .argument("[key]", "Ditto API key")
845
+ .option("--paste", "open the key creation page before prompting")
846
+ .option("--stdin", "read the API key from stdin")
847
+ .addOption(hiddenOutputOption())
848
+ .action(cmdLogin);
849
+ program
850
+ .command("logout")
851
+ .description("delete the saved API key")
852
+ .addOption(hiddenOutputOption())
853
+ .action(cmdLogout);
854
+ program
855
+ .command("status")
856
+ .description("show CLI auth and endpoint status")
857
+ .addOption(outputOption())
858
+ .action(cmdStatus);
859
+ program
860
+ .command("config")
861
+ .description("print MCP client configuration")
862
+ .addOption(hiddenOutputOption())
863
+ .action(cmdConfig);
864
+ return program;
759
865
  }
760
866
  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;
867
+ const argv = [...process.argv];
868
+ const args = argv.slice(2);
869
+ if (args[0] === "graphs" &&
870
+ (args.length === 1 || (args[1].startsWith("-") && args[1] !== "-h" && args[1] !== "--help"))) {
871
+ argv.splice(3, 0, "list");
826
872
  }
873
+ await buildProgram().parseAsync(argv);
827
874
  }
828
875
  main().catch((error) => {
829
876
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);