create-evda 0.1.8 → 0.1.10

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +20 -7
  3. package/bin/evda.mjs +277 -7
  4. package/create-evda.mjs +17 -4
  5. package/dist/pairing.mjs +2932 -1684
  6. package/hooks/evda-day-writer-guard.mjs +7 -0
  7. package/lib/acknowledge.mjs +187 -0
  8. package/lib/adopt.mjs +488 -0
  9. package/lib/agents.mjs +1 -1
  10. package/lib/attested-host.mjs +269 -0
  11. package/lib/browse.mjs +354 -0
  12. package/lib/circles.mjs +5 -1
  13. package/lib/clients.mjs +29 -2
  14. package/lib/connect.mjs +9 -2
  15. package/lib/connecting-apps.mjs +56 -0
  16. package/lib/connector-tools.mjs +85 -0
  17. package/lib/corrections.mjs +34 -0
  18. package/lib/credentials-migrate.mjs +120 -0
  19. package/lib/day-file.mjs +173 -15
  20. package/lib/day-writer.mjs +110 -0
  21. package/lib/doctor.mjs +45 -3
  22. package/lib/every-chat.mjs +128 -0
  23. package/lib/filechanges.mjs +203 -68
  24. package/lib/gather-apply.mjs +176 -26
  25. package/lib/gather.mjs +119 -17
  26. package/lib/html-notes.mjs +124 -0
  27. package/lib/keep-hook.mjs +61 -8
  28. package/lib/keychain.mjs +181 -0
  29. package/lib/manifest.mjs +5 -1
  30. package/lib/mcp-auth.mjs +41 -1
  31. package/lib/mcp-http.mjs +42 -4
  32. package/lib/mcp.mjs +1198 -76
  33. package/lib/measurement-log.mjs +249 -0
  34. package/lib/messages.mjs +5 -2
  35. package/lib/migrate-people.mjs +54 -20
  36. package/lib/pairing.mjs +243 -5
  37. package/lib/parity.mjs +47 -13
  38. package/lib/person-folders.mjs +89 -0
  39. package/lib/plugin-hook.mjs +109 -0
  40. package/lib/plugin.mjs +208 -0
  41. package/lib/presets.mjs +26 -5
  42. package/lib/publish.mjs +55 -10
  43. package/lib/qr.mjs +317 -0
  44. package/lib/record.mjs +74 -0
  45. package/lib/routines.mjs +54 -4
  46. package/lib/sigstore-trust.json +20 -0
  47. package/lib/skills.mjs +1 -1
  48. package/lib/sync-lock.mjs +143 -0
  49. package/lib/sync-rules.mjs +154 -1
  50. package/lib/takeout.mjs +169 -0
  51. package/lib/tdx-attestation.mjs +412 -0
  52. package/lib/tdx-trust.json +10 -0
  53. package/lib/tools.mjs +1040 -31
  54. package/lib/touches.mjs +427 -0
  55. package/lib/update-check.mjs +30 -0
  56. package/lib/watch.mjs +160 -0
  57. package/lib/yaml.mjs +8 -4
  58. package/package.json +2 -1
  59. package/skills/evda/SKILL.md +14 -0
  60. package/templates/AGENTS.md +81 -10
  61. package/templates/evda-digest.md +5 -1
package/bin/evda.mjs CHANGED
@@ -25,7 +25,7 @@ import { grant, revoke, readGrants, access } from "../lib/grants.mjs";
25
25
  import { holderOf, HOLDER_KINDS } from "../lib/manifest.mjs";
26
26
  import { EvdaError, isEntry } from "../lib/errors.mjs";
27
27
  import { publishCommand } from "../lib/publish.mjs";
28
- import { checkForUpdate, summarize, stampInstalledVersion } from "../lib/update-check.mjs";
28
+ import { checkForUpdate, summarize, stampInstalledVersion, isNewer } from "../lib/update-check.mjs";
29
29
  import { planCircles, coverage, declare, deviceCircles, planBeside, defaultParts } from "../lib/circle-plan.mjs";
30
30
  import { circleIndex, grantView } from "../lib/filechanges.mjs";
31
31
  import { mountRoot } from "../lib/declarations.mjs";
@@ -34,8 +34,9 @@ import { parity, formatParity } from "../lib/parity.mjs";
34
34
  import { note, releaseNote, ownerSearch, readContext, readersFor, describeReaders, parseReaders, READ_BY } from "../lib/readers.mjs";
35
35
  import { runHook } from "../lib/guard.mjs";
36
36
  import { markdownFiles } from "../lib/circles.mjs";
37
- import { refuseSecrets, message as messageTool, replies as repliesTool } from "../lib/tools.mjs";
37
+ import { refuseSecrets, message as messageTool, replies as repliesTool, acknowledgeEntry as acknowledgeTool, acknowledgements as acknowledgementsTool } from "../lib/tools.mjs";
38
38
  import * as pairing from "../lib/pairing.mjs";
39
+ import { watchRecord, watchRelay } from "../lib/watch.mjs";
39
40
 
40
41
  const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
41
42
 
@@ -75,6 +76,9 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
75
76
  a one-time owner code for the authorization page, or end every
76
77
  connection, or say what is connected
77
78
  evda connect [dir] how to add that server to each client, with this record's path
79
+ evda plugin [dir] [--out <zip>]
80
+ build a Claude plugin for this record (its MCP server, hooks and skill,
81
+ the path baked in), for Claude Cowork: upload it in Customize, Plugins
78
82
  evda skill install [--project <dir>]
79
83
  install the evda skill for Claude Code (~/.claude/skills, or a project)
80
84
  evda hook install [dir] add the keep-record Stop hook and the read guard to the record's .claude/settings.json,
@@ -101,13 +105,31 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
101
105
  run doctor. Writes a reversal manifest beside the proposal
102
106
  evda gather revert <manifest> [--force]
103
107
  undo an apply byte for byte; refused when a placed file was edited
108
+ evda adopt <dir> [--dry] [--map <folder>=<role>]... [--filing everything|asked] [--owner NAME]
109
+ [--agent-name NAME] [--manifest <file>] [--no-hooks]
110
+ make an existing folder of notes an EVDA where it is: an Iris Vault
111
+ maps without asking, a folder it cannot place is named and answered
112
+ with --map (journal, collections, knowledge, work, or none). Writes
113
+ evda.json, AGENTS.md, one @AGENTS.md line at the end of CLAUDE.md,
114
+ .evda/ and the hooks; every other file stays byte for byte. Writes a
115
+ reversal manifest outside the folder
116
+ evda adopt --revert <manifest> [--force]
117
+ undo an adopt byte for byte; refused when a file it wrote was edited
118
+ evda secret <file> [dir] [--keychain <path>]
119
+ a credential note (in Credentials/, or --into a path of its own) with
120
+ every "in your Keychain" pointer read back and filled in, to this
121
+ terminal only (HQ-1916). No MCP tool reads a value this way
122
+ evda secret migrate [dir] [--dry] [--keychain <path>]
123
+ once, for Credentials/ files sealed before the Keychain move: each
124
+ value goes into the Keychain and the file keeps a pointer. All or
125
+ nothing per file; a second run changes nothing (HQ-2622)
104
126
  evda look-back [dir] [--model M]
105
127
  the week, read back by the owner's claude -p, appended to a report
106
128
  evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
107
129
  print a scheduled routine for Claude, or a launchd fallback; --install
108
130
  writes the launchd job, --uninstall removes it (setup installs the
109
131
  morning organize unless told no)
110
- evda preset list the organizations this package ships, and who each suits
132
+ evda preset list [--json] the organizations this package ships, and who each suits
111
133
  evda preset show <id> one preset's folders
112
134
  evda preset apply <id> [--dry] [dir]
113
135
  add the preset's folders and manifests. Only adds: an existing
@@ -116,11 +138,24 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
116
138
  evda pair [dir] [--name phone] [--parts journal[,people]] [--json]
117
139
  a one-time code (and a QR link) for a phone to join this EVÐA. It
118
140
  works once, for 10 minutes, and gives the device only the parts named
141
+ (with no --parts, the part that holds the day folder)
142
+ evda pair --finish [dir] [--json]
143
+ add a device that used its code, without a full sync. It never waits:
144
+ while a sync runs it says so, and that sync adds the device itself
119
145
  evda devices [dir] [--json] the devices paired with this EVÐA, and what each reads
146
+ evda watch [dir] one line (CHANGED <n>) per burst of saves in the record, and INCOMING
147
+ when a device sent something, for the Mac app's sync; runs until stopped
148
+ evda sync-status [dir] [--json]
149
+ what is syncing on this computer now, since when, and what is still owed
120
150
  evda remove <device> [dir] remove a device: it adds nothing more and receives nothing new
121
151
  evda sync [dir] add devices that used their code, take in what they sent, send what
122
152
  changed. Pairing needs Node 22.13 or newer; --relay <url> for another relay
153
+ One sync of a record runs at a time (the Mac app, launchd, a terminal); a
154
+ second waits for it, then says so in one line if it is still running
123
155
  --json on pair, devices, remove and sync prints data for an app, no ids
156
+ evda compact [dir] [--yes] give back the space this computer's sync state keeps for sealed copies
157
+ nothing needs: media (which travels by name only) and copies no device is
158
+ still owed. Shows what it would remove; --yes removes them
124
159
  evda status [dir] one screen summary
125
160
  evda publish public [dir] --out <dir> --key <seed file>
126
161
  the public circle as a signed plaintext export; refuses (exit 2) on a
@@ -154,8 +189,14 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
154
189
  evda replies [--peek] [--in dir] [--json]
155
190
  the owner's replies not seen yet, marked seen unless --peek; read
156
191
  under read_by as the AI $EVDA_READER names (claude, local, ...)
192
+ evda acknowledge <path> [--section "## HH:MM"] [--note "<one line>"] [--by NAME] [--in dir] [--json]
193
+ you said a weighty entry back to the owner: one mark beside the
194
+ record, so no other reader says it again. Already marked prints who
195
+ did it and writes nothing. No path lists every mark
157
196
  evda search <words> [dir] your own search, on this device, no model; just-noted and sealed entries included and marked
158
197
  evda readers [dir] which AI reads each part of the record, from read_by
198
+ evda browse [dir] [--folder F | --pins | --operating | --today | --index | --open P | --attachments | --attach P] one folder of the record, the pinned folders, the files on how it works, today's note, every text file's path, one file, every PDF, image and other attachment's path, or one attachment, as JSON (the app)
199
+ evda capture [dir] < words append the words on stdin to today's note, verbatim, under a new timestamped section (the app)
159
200
  evda local-model how to run a model on your own computer and point a circle at it
160
201
  evda guard the Claude Code read guard (a PreToolUse hook; reads the tool call on stdin)
161
202
  evda where which EVDA the locator resolves to, and by which rule
@@ -176,6 +217,19 @@ function packageVersion() {
176
217
  }
177
218
  }
178
219
 
220
+ // One command's lines of HELP, cut from it so the two cannot say different things: every entry that
221
+ // starts with `evda <cmd>`, with the indented lines under it.
222
+ function commandHelp(cmd) {
223
+ const out = [];
224
+ let inside = false;
225
+ for (const line of HELP.split("\n")) {
226
+ if (/^ evda /.test(line)) inside = line.startsWith(` evda ${cmd} `) || line === ` evda ${cmd}`;
227
+ else if (!/^ {4,}\S/.test(line)) inside = false;
228
+ if (inside) out.push(line);
229
+ }
230
+ return [`usage: evda ${cmd}`, "", ...out].join("\n");
231
+ }
232
+
179
233
  // A positional argument is the record when it looks like a path or IS a directory on disk. A bare
180
234
  // name that exists (`evda doctor my-evda`) must not fall through to the pointer: on 2026-09-16 it
181
235
  // did, and `evda doctor scratch` read the record ~/.evda/current pointed at instead of `scratch/`.
@@ -185,7 +239,7 @@ function isDirArg(a) {
185
239
  }
186
240
 
187
241
  // `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
188
- const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday", "out", "key", "plan", "device", "shadow", "record-id", "env-file", "port", "host", "public-url", "log", "lifeline", "ttl", "parts", "relay"];
242
+ const VALUE_FLAGS = ["open", "folder", "to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday", "out", "key", "plan", "device", "shadow", "record-id", "env-file", "port", "host", "public-url", "log", "lifeline", "ttl", "parts", "relay", "keychain"];
189
243
  function flagValue(args, name) {
190
244
  const eq = args.find((a) => a.startsWith(`--${name}=`));
191
245
  if (eq) return eq.slice(name.length + 3);
@@ -272,7 +326,7 @@ async function main(argv) {
272
326
 
273
327
  case "version":
274
328
  case "--version":
275
- console.log(`evda tool schema ${SCHEMA_VERSION}`);
329
+ console.log(`evda ${packageVersion()} (tool schema ${SCHEMA_VERSION})`);
276
330
  return 0;
277
331
 
278
332
  case "init": {
@@ -281,6 +335,9 @@ async function main(argv) {
281
335
  }
282
336
 
283
337
  case "mcp": {
338
+ // `--help` or `-h` anywhere prints the mcp lines of HELP and starts nothing (HQ-1368): before,
339
+ // `evda mcp --help` took the flag as nothing and served the default record on stdio.
340
+ if (args.includes("--help") || args.includes("-h")) { console.log(commandHelp("mcp")); return 0; }
284
341
  if (args[0] === "token") return mcpToken(args.slice(1));
285
342
  const { dir, flags } = pickDir(args);
286
343
  if (flags.includes("--http")) {
@@ -369,6 +426,86 @@ async function main(argv) {
369
426
  throw new EvdaError("usage: evda gather scan <source>... | questions <proposal> | answer <proposal> <question> <value> | show <proposal> | apply <proposal> --into <record> | revert <manifest>", { exit: 2 });
370
427
  }
371
428
 
429
+ // HQ-2797: an existing folder of notes becomes an EVDA where it is.
430
+ case "adopt": {
431
+ if (args.includes("--help") || args.includes("-h")) { console.log(commandHelp("adopt")); return 0; }
432
+ const ad = await import("../lib/adopt.mjs");
433
+ const revert = flagValue(args, "revert");
434
+ if (revert !== undefined || args.includes("--revert")) {
435
+ if (!revert || revert.startsWith("--")) throw new EvdaError("usage: evda adopt --revert <manifest> [--force]", { exit: 2 });
436
+ const r = ad.revertAdopt(revert, { force: args.includes("--force") });
437
+ for (const l of ad.revertLines(r)) console.log(l);
438
+ return 0;
439
+ }
440
+ const takes = ["map", "filing", "owner", "agent-name", "manifest"];
441
+ const maps = [];
442
+ const positional = [];
443
+ const opts = { dry: false, hooks: true };
444
+ for (let i = 0; i < args.length; i++) {
445
+ const a = args[i];
446
+ const eq = /^--([a-z-]+)=([\s\S]*)$/.exec(a);
447
+ const [flag, inline] = eq ? [eq[1], eq[2]] : a.startsWith("--") ? [a.slice(2), undefined] : [null, undefined];
448
+ if (flag === null) { positional.push(a); continue; }
449
+ if (flag === "dry") { opts.dry = true; continue; }
450
+ if (flag === "no-hooks") { opts.hooks = false; continue; }
451
+ if (!takes.includes(flag)) throw new EvdaError(`unknown option --${flag}\n\n${commandHelp("adopt")}`, { exit: 2 });
452
+ const v = inline ?? args[++i];
453
+ if (v === undefined) throw new EvdaError(`--${flag} needs a value`, { exit: 2 });
454
+ if (flag === "map") maps.push(v);
455
+ else if (flag === "filing") { opts.filing = ad.filingKey(v); if (!opts.filing || !v.trim()) throw new EvdaError(`--filing takes everything or asked, got ${v}`, { exit: 2 }); }
456
+ else if (flag === "owner") opts.owner = v;
457
+ else if (flag === "agent-name") opts.agentName = v;
458
+ else if (flag === "manifest") opts.manifestOut = v;
459
+ }
460
+ if (positional.length !== 1) throw new EvdaError(commandHelp("adopt"), { exit: 2 });
461
+ const target = resolve(positional[0]);
462
+ opts.map = ad.parseMapArgs(maps);
463
+ opts.version = packageVersion();
464
+ // The filing question, asked the way setup asks it, when there is a terminal and no answer.
465
+ if (!opts.dry && opts.filing === undefined && process.stdin.isTTY && process.stdout.isTTY) {
466
+ const { createInterface } = await import("node:readline/promises");
467
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
468
+ try { while (!opts.filing) opts.filing = ad.filingKey(await rl.question(`${ad.FILING_QUESTION(opts.agentName)} `)); } finally { rl.close(); }
469
+ }
470
+ opts.filing ??= null;
471
+ const r = ad.adopt(target, opts);
472
+ for (const l of r.lines) console.log(l);
473
+ if (r.dry) {
474
+ if (r.questions.length) { console.log("\nBefore it can be adopted:"); for (const q of r.questions) console.log(` ${q}`); }
475
+ else console.log(`\nNothing was written. Run it without --dry to adopt ${target}.`);
476
+ return 0;
477
+ }
478
+ console.log("");
479
+ for (const l of ad.connectLines(target, opts.filing)) console.log(l);
480
+ return r.exit;
481
+ }
482
+
483
+ // HQ-1916: a credential note's Keychain pointers, read back and printed to this terminal only.
484
+ // No MCP tool calls into this, on purpose -- the whole point of the Keychain move is that no AI
485
+ // reads the value; only the owner does, on their own Mac.
486
+ case "secret": {
487
+ const { dir, rest } = pickDir(args);
488
+ const file = rest[0];
489
+ if (!file) throw new EvdaError("usage: evda secret <file> [dir] [--keychain <path>] | evda secret migrate [dir] [--dry] [--keychain <path>]", { exit: 2 });
490
+ // HQ-2622: the one-time move of Credentials/ files sealed before HQ-1916 into the Keychain.
491
+ if (file === "migrate") {
492
+ const cm = await import("../lib/credentials-migrate.mjs");
493
+ const r = cm.migrateCredentials(dir, { keychainPath: flagValue(args, "keychain") ?? process.env.EVDA_KEYCHAIN_PATH ?? null, dry: args.includes("--dry") });
494
+ for (const l of cm.migrateLines(r)) console.log(l);
495
+ return r.kept ? 1 : 0;
496
+ }
497
+ const g2 = await import("../lib/gather.mjs");
498
+ const k = await import("../lib/keychain.mjs");
499
+ const abs = file.includes("/") ? resolve(dir, file) : join(dir, g2.CREDENTIALS, file);
500
+ if (!existsSync(abs)) throw new EvdaError(`REFUSED: no file at ${abs}.`, { code: "NOT_FOUND", exit: 2 });
501
+ const text = readFileSync(abs, "utf8");
502
+ const r = k.resolvePointers(text, { keychainPath: flagValue(args, "keychain") ?? process.env.EVDA_KEYCHAIN_PATH ?? null });
503
+ console.log(r.text);
504
+ if (!r.resolved && !r.failed) console.log(`\nSKIPPED Keychain: ${abs} holds no "in your Keychain" pointer -- its value(s) never left the file (not macOS at apply, or the write failed then)`);
505
+ else console.log(`\nCHECKED ${r.resolved} Keychain pointer(s) resolved, ${r.failed} could not be read`);
506
+ return r.failed ? 1 : 0;
507
+ }
508
+
372
509
  case "look-back": {
373
510
  const { dir } = pickDir(args);
374
511
  const { lookBack } = await import("../lib/organize.mjs");
@@ -421,6 +558,16 @@ async function main(argv) {
421
558
  return st.registered && st.file && st.current && g.registered && g.file && (!ph.registered || (ph.file && ph.current)) ? 0 : 1;
422
559
  }
423
560
 
561
+ case "plugin": {
562
+ // evda plugin [dir] [--out <zip>] the one way Cowork reaches a record (HQ-2802)
563
+ const out = flagValue(args, "out");
564
+ const { dir } = pickDir(args);
565
+ const { buildPlugin, uploadLines } = await import("../lib/plugin.mjs");
566
+ const r = buildPlugin(dir, { out });
567
+ for (const l of uploadLines(r)) console.log(l);
568
+ return 0;
569
+ }
570
+
424
571
  case "connect": {
425
572
  // How to add this record's MCP server to each client, with the record's path filled in.
426
573
  const { dir } = pickDir(args);
@@ -441,6 +588,12 @@ async function main(argv) {
441
588
  const [sub, ...rest] = args;
442
589
  if (sub === "list") {
443
590
  const all = presets.listPresets();
591
+ // --json: one line the EVÐA app's setup reads its choices from, so it offers exactly what
592
+ // create-evda --preset accepts (HQ-1217).
593
+ if (rest.includes("--json")) {
594
+ console.log(JSON.stringify(all.map((p) => ({ id: p.id, title: p.title, for: p.for }))));
595
+ return all.length ? 0 : 1;
596
+ }
444
597
  for (const p of all) console.log(`${p.id.padEnd(20)} ${p.title}: ${p.for}`);
445
598
  console.log(`CHECKED ${all.length} presets`);
446
599
  return all.length ? 0 : 1;
@@ -469,7 +622,7 @@ async function main(argv) {
469
622
  const plan = presets.applyPreset(dir, id, { dry });
470
623
  console.log(`evda preset apply ${id}${dry ? " --dry" : ""} ${plan.root}`);
471
624
  for (const l of presets.planLines(plan)) console.log(l);
472
- const wrote = plan.create.length + plan.write.length + plan.views.length + plan.proposed.length + plan.roles.length;
625
+ const wrote = plan.create.length + plan.write.length + plan.files.length + plan.views.length + plan.proposed.length + plan.roles.length;
473
626
  console.log(`CHECKED ${plan.folders} preset folders ${plan.create.length} ${dry ? "to create" : "created"}, ${plan.write.length} manifest(s) ${dry ? "to write" : "written"}, ${plan.matched.length} skipped, ${plan.conflicts.length} conflict(s)`);
474
627
  if (!dry && wrote === 0 && !plan.conflicts.length) console.log("Nothing was written: the record already has this preset.");
475
628
  if (dry) console.log("Dry run: nothing was written.");
@@ -507,6 +660,12 @@ async function main(argv) {
507
660
 
508
661
  case "pair": {
509
662
  const { dir } = pickDir(args);
663
+ // --finish: add a device that used its code, without a full sync (HQ-2017).
664
+ if (args.includes("--finish")) {
665
+ if (args.includes("--json")) console.log(JSON.stringify(await pairing.finishData(dir, { relay: flagValue(args, "relay") })));
666
+ else for (const line of await pairing.finish(dir, { relay: flagValue(args, "relay") })) console.log(line);
667
+ return 0;
668
+ }
510
669
  const r = await pairing.pair(dir, { name: flagValue(args, "name"), parts: flagValue(args, "parts"), relay: flagValue(args, "relay") });
511
670
  // --json is for an app (the EVÐA Mac app, HQ-1925): the code and its QR link, and nothing with an id.
512
671
  if (args.includes("--json")) console.log(JSON.stringify({ name: r.name, code: r.code, qr: r.qr, computer: r.computer, expiresAt: r.expiresAt, parts: r.parts }));
@@ -514,6 +673,16 @@ async function main(argv) {
514
673
  return 0;
515
674
  }
516
675
 
676
+ case "sync-status": {
677
+ // What is syncing on this computer now, for the Mac app (HQ-2041): who holds the state and
678
+ // since when, and how much is still owed. Never waits.
679
+ const { dir } = pickDir(args);
680
+ const r = await pairing.statusData(dir, { relay: flagValue(args, "relay") });
681
+ if (args.includes("--json")) console.log(JSON.stringify(r));
682
+ else console.log(r.holder ? `Syncing: ${r.holder.by === "tick" ? "the background sync" : r.holder.by === "app" ? "the EVÐA app" : "a command"} (${r.holder.op}) since ${new Date(r.holder.since).toLocaleTimeString()}.` : "Nothing is syncing.", r.owed ? `${r.owed} change(s) still owed to a device.` : "");
683
+ return 0;
684
+ }
685
+
517
686
  case "devices":
518
687
  case "sync": {
519
688
  const { dir } = pickDir(args);
@@ -527,6 +696,24 @@ async function main(argv) {
527
696
  return 0;
528
697
  }
529
698
 
699
+ case "watch": {
700
+ // For the Mac app (HQ-2068): one line a burst of saves in the record, so it syncs in seconds.
701
+ // Runs until it is stopped; the app runs the sync, this only says when.
702
+ const { dir } = pickDir(args);
703
+ watchRecord(dir, (paths) => console.log(`CHANGED ${paths.length}`));
704
+ // And the relay: a device's note says INCOMING within seconds, so the app syncs it in then.
705
+ const relay = flagValue(args, "relay");
706
+ if (!pairing.pairingUnsupported()) watchRelay(() => pairing.inboundMailboxes(dir, { relay }), () => console.log("INCOMING"));
707
+ console.log("WATCHING");
708
+ return await new Promise(() => undefined);
709
+ }
710
+
711
+ case "compact": {
712
+ const { dir } = pickDir(args);
713
+ for (const line of await pairing.compact(dir, { yes: args.includes("--yes") })) console.log(line);
714
+ return 0;
715
+ }
716
+
530
717
  case "remove": {
531
718
  // The device's name comes first and is never read as the record, even when a folder by
532
719
  // that name sits in the working directory.
@@ -572,7 +759,7 @@ async function main(argv) {
572
759
  let body = args.join(" ");
573
760
  if (body === "-" || body === "") body = readFileSync(0, "utf8");
574
761
  const r = messageTool(openEvda(dir), { title, body, ...(from === undefined ? {} : { from }) });
575
- console.log(`Sent the message ${r.id}, filed at ${r.path}.`);
762
+ console.log(`Filed the message ${r.id} at ${r.path}. This does not confirm it reached the owner's phone.`);
576
763
  return 0;
577
764
  }
578
765
 
@@ -588,6 +775,33 @@ async function main(argv) {
588
775
  return 0;
589
776
  }
590
777
 
778
+ case "acknowledge": {
779
+ // HQ-2060. A note may hold a slash, so the record is --in <dir> or the locator, as with
780
+ // note and message. No path lists what every reader has already acknowledged.
781
+ const take = (flag) => { const i = args.indexOf(flag); if (i < 0) return undefined; const v = args[i + 1]; if (v === undefined) throw new EvdaError(`${flag} needs a value`, { exit: 2 }); args.splice(i, 2); return v; };
782
+ const json = args.includes("--json") && (args.splice(args.indexOf("--json"), 1), true);
783
+ const dirArg = take("--in");
784
+ const section = take("--section");
785
+ const note = take("--note");
786
+ const by = take("--by");
787
+ const dir = locate({ explicit: dirArg }).dir;
788
+ const evda = openEvda(dir);
789
+ const path = args.filter((a) => !a.startsWith("--")).join(" ").trim();
790
+ if (!path) {
791
+ const r = acknowledgementsTool(evda);
792
+ if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
793
+ for (const m of r.marks) console.log(`- ${m.key} (${m.by ?? "unknown"}, ${m.at}): ${m.note}`);
794
+ console.log(`CHECKED ${r.count} acknowledged entr${r.count === 1 ? "y" : "ies"}`);
795
+ return 0;
796
+ }
797
+ const r = acknowledgeTool(evda, { path, ...(section === undefined ? {} : { section }), ...(note === undefined ? {} : { note }), ...(by === undefined ? {} : { by }) });
798
+ if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
799
+ const what = r.section ? `${r.path} "${r.section}"` : r.path;
800
+ if (!r.first) console.log(`Already acknowledged by ${r.by ?? "an earlier reader"} at ${r.at}: ${r.note}\nNothing was written. Say nothing about ${what} unless the owner brings it up.`);
801
+ else console.log(`Marked ${what} acknowledged, as ${r.by}. You reached it first, so every later reader sees this mark and stays quiet.`);
802
+ return 0;
803
+ }
804
+
591
805
  case "release": {
592
806
  const { dir, entry } = pickReleaseArgs(args);
593
807
  const r = releaseNote(openEvda(dir), entry);
@@ -607,6 +821,37 @@ async function main(argv) {
607
821
  return 0;
608
822
  }
609
823
 
824
+ case "browse": {
825
+ // The owner's app reads the record through this (HQ-2038): the pinned folders, one folder's
826
+ // children, or one file opened, each as one JSON line. The rules are lib/browse.mjs.
827
+ const { dir } = pickDir(args);
828
+ const evda = openEvda(dir);
829
+ const b = await import("../lib/browse.mjs");
830
+ const bctx = b.browseContext(evda);
831
+ const open = flagValue(args, "open");
832
+ const folder = flagValue(args, "folder");
833
+ const attach = flagValue(args, "attach");
834
+ const answer = attach !== undefined ? b.attachEntry(evda, attach, { bctx }) : args.includes("--attachments") ? b.attachments(evda, { bctx }) : open !== undefined ? b.openEntry(evda, open, { bctx }) : args.includes("--pins") ? b.pins(evda, { bctx }) : args.includes("--operating") ? b.operating(evda, { bctx }) : args.includes("--today") ? b.today(evda, { bctx }) : args.includes("--index") ? b.index(evda, { bctx }) : b.browse(evda, folder ?? "", { bctx });
835
+ // Written to the end before returning: on macOS a pipe write is asynchronous, and the exit
836
+ // that follows cut anything past the pipe's 64 KB buffer (his 71 KB CLAUDE.md, HQ-2038).
837
+ await new Promise((done) => process.stdout.write(`${JSON.stringify(answer)}\n`, done));
838
+ return 0;
839
+ }
840
+
841
+ case "capture": {
842
+ // The app's capture box (HQ-2038): the words come on stdin, verbatim, and are appended to
843
+ // today's daily note under a new timestamped section, by the same write evda_write makes.
844
+ const { dir } = pickDir(args);
845
+ const evda = openEvda(dir);
846
+ const chunks = [];
847
+ for await (const c of process.stdin) chunks.push(c);
848
+ const words = Buffer.concat(chunks).toString("utf8");
849
+ const { capture } = await import("../lib/browse.mjs");
850
+ const r = capture(evda, words);
851
+ await new Promise((done) => process.stdout.write(`${JSON.stringify(r)}\n`, done));
852
+ return 0;
853
+ }
854
+
610
855
  case "readers": {
611
856
  const { dir } = pickDir(args);
612
857
  const evda = openEvda(dir);
@@ -726,6 +971,13 @@ async function main(argv) {
726
971
  console.log(summarize(result));
727
972
  if (result.status === "CHECK_FAILED") return 1;
728
973
  if (result.status !== "UPDATE_AVAILABLE" && result.status !== "UNSTAMPED") return 0;
974
+ // HQ-2718: upgrade writes the block THIS tool ships and stamps THIS tool's version. Run by an
975
+ // installed evda older than the registry, it used to do that and exit 0, leaving the record
976
+ // behind while the line above told the owner to run the command they had just run.
977
+ if (isNewer(result.latest, packageVersion())) {
978
+ console.log(`update: this evda is ${packageVersion()}, older than ${result.latest}, so it cannot install ${result.latest}. Nothing was changed. Run "npx -y -p create-evda@latest evda update" to take it.`);
979
+ return 1;
980
+ }
729
981
  const upgradeRc = await main(["upgrade", dir]);
730
982
  if (upgradeRc !== 0) return upgradeRc;
731
983
  return main(["migrate", dir]);
@@ -963,7 +1215,25 @@ async function main(argv) {
963
1215
 
964
1216
  // Through realpath: an installed bin is a symlink and resolve() does not follow it (see the same
965
1217
  // guard in create-evda.mjs, and test/bins.test.mjs).
1218
+ /**
1219
+ * A command the EVÐA app runs ends when the app does (HQ-2041). The app names itself in
1220
+ * EVDA_PARENT_PID; once this process's parent is no longer that pid (the app quit or crashed and
1221
+ * this process was handed to launchd), it stops. A lock it held is then stale and taken over.
1222
+ */
1223
+ export function watchParent(env = process.env, { ppid = () => process.ppid, exit = (c) => process.exit(c), everyMs = 1000 } = {}) {
1224
+ const want = Number(env.EVDA_PARENT_PID);
1225
+ if (!Number.isInteger(want) || want <= 0) return null;
1226
+ const check = () => {
1227
+ if (ppid() !== want) exit(1);
1228
+ };
1229
+ check();
1230
+ const t = setInterval(check, everyMs);
1231
+ t.unref();
1232
+ return t;
1233
+ }
1234
+
966
1235
  if (process.argv[1] && isEntry(process.argv[1], import.meta.url)) {
1236
+ watchParent();
967
1237
  main(process.argv.slice(2)).then(
968
1238
  (code) => process.exit(code),
969
1239
  (e) => {
package/create-evda.mjs CHANGED
@@ -33,6 +33,7 @@ import { readPointer, setPointer, pointerPath } from "./lib/locate.mjs";
33
33
  import { FOLDER_MANIFEST } from "./lib/folders.mjs";
34
34
  import { EvdaError } from "./lib/errors.mjs";
35
35
  import { clients, installSkill, installGuard } from "./lib/connect.mjs";
36
+ import { everyChatSetupLines } from "./lib/every-chat.mjs";
36
37
  import { defaultRoutines, installAtSetup } from "./lib/routines.mjs";
37
38
  import { installHook } from "./lib/hooks.mjs";
38
39
  import { applyPreset, listPresets, planLines, presetIds, NONE } from "./lib/presets.mjs";
@@ -69,7 +70,14 @@ const FOLDER_TYPES = {
69
70
  messages: "message",
70
71
  };
71
72
 
72
- const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--filing everything|asked] [--no-prompt-hook] [--no-parts] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]`;
73
+ const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--filing everything|asked] [--no-prompt-hook] [--no-parts] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly|--no-routines] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]
74
+
75
+ On a Mac, setup installs the morning organize as a launchd job (daily at 8:00, your own Claude) unless --no-routines, --yes included, and says so before it does.`;
76
+
77
+ // `--help` and `-h` anywhere print this and nothing else (HQ-1368): before, `--help` was "unknown
78
+ // option" and `-h` was taken as the folder to create.
79
+ export const HELP_FLAGS = ["--help", "-h"];
80
+ export const wantsHelp = (argv) => argv.some((a) => HELP_FLAGS.includes(a));
73
81
 
74
82
  export function parseArgs(argv) {
75
83
  const opts = {
@@ -276,8 +284,9 @@ export function needsAsking(opts) {
276
284
  // only when both stdin and stdout are a terminal. A piped or redirected run is a script, and a
277
285
  // script is never asked anything.
278
286
  export async function main(argv, io = null) {
279
- let opts = parseArgs(argv);
280
287
  const out = io?.out ?? console.log;
288
+ if (wantsHelp(argv)) { out(USAGE); return 0; }
289
+ let opts = parseArgs(argv);
281
290
  const log = opts.quiet ? () => {} : out;
282
291
  const tty = io ? Boolean(io.interactive) : Boolean(process.stdin.isTTY && process.stdout.isTTY);
283
292
  const asked = tty && !opts.yes && needsAsking(opts);
@@ -332,7 +341,11 @@ export async function main(argv, io = null) {
332
341
  }
333
342
  log("");
334
343
  log("Or connect your AI to this record from any folder, through the evda MCP server. No key and no model call in EVÐA:");
335
- for (const c of clients(dir)) if (c.key !== "project") { log(` ${c.label}:`); for (const l of c.text.split("\n")) log(` ${l}`); }
344
+ for (const c of clients(dir)) if (c.key !== "project" && c.key !== "every-chat") { log(` ${c.label}:`); for (const l of c.text.split("\n")) log(` ${l}`); }
345
+ // The every-chat line (HQ-2205), from the one source: a chat that never calls an evda tool is
346
+ // reached only by the app's own every-chat setting.
347
+ log("");
348
+ for (const l of everyChatSetupLines(manifest.filing ?? null)) log(l);
336
349
  if (opts.skill) {
337
350
  const r = installSkill({ env: io?.env ?? process.env });
338
351
  log("");
@@ -341,7 +354,7 @@ export async function main(argv, io = null) {
341
354
  // Organizing is on unless the owner said otherwise (HQ-1246): no flag and no answer means the
342
355
  // morning organize is installed.
343
356
  const env = io?.env ?? process.env;
344
- for (const l of installAtSetup(opts.routines ?? defaultRoutines(env), dir, { env, platform: io?.platform ?? process.platform })) log(l);
357
+ for (const l of installAtSetup(opts.routines ?? defaultRoutines(env), dir, { env, platform: io?.platform ?? process.platform, say: out, launchctl: io?.launchctl ?? null })) log(l);
345
358
  log("");
346
359
  for (const l of INTRO.split("\n")) log(l);
347
360
  log("");