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.
- package/CHANGELOG.md +109 -1
- package/README.md +20 -7
- package/bin/evda.mjs +277 -7
- package/create-evda.mjs +17 -4
- package/dist/pairing.mjs +2932 -1684
- package/hooks/evda-day-writer-guard.mjs +7 -0
- package/lib/acknowledge.mjs +187 -0
- package/lib/adopt.mjs +488 -0
- package/lib/agents.mjs +1 -1
- package/lib/attested-host.mjs +269 -0
- package/lib/browse.mjs +354 -0
- package/lib/circles.mjs +5 -1
- package/lib/clients.mjs +29 -2
- package/lib/connect.mjs +9 -2
- package/lib/connecting-apps.mjs +56 -0
- package/lib/connector-tools.mjs +85 -0
- package/lib/corrections.mjs +34 -0
- package/lib/credentials-migrate.mjs +120 -0
- package/lib/day-file.mjs +173 -15
- package/lib/day-writer.mjs +110 -0
- package/lib/doctor.mjs +45 -3
- package/lib/every-chat.mjs +128 -0
- package/lib/filechanges.mjs +203 -68
- package/lib/gather-apply.mjs +176 -26
- package/lib/gather.mjs +119 -17
- package/lib/html-notes.mjs +124 -0
- package/lib/keep-hook.mjs +61 -8
- package/lib/keychain.mjs +181 -0
- package/lib/manifest.mjs +5 -1
- package/lib/mcp-auth.mjs +41 -1
- package/lib/mcp-http.mjs +42 -4
- package/lib/mcp.mjs +1198 -76
- package/lib/measurement-log.mjs +249 -0
- package/lib/messages.mjs +5 -2
- package/lib/migrate-people.mjs +54 -20
- package/lib/pairing.mjs +243 -5
- package/lib/parity.mjs +47 -13
- package/lib/person-folders.mjs +89 -0
- package/lib/plugin-hook.mjs +109 -0
- package/lib/plugin.mjs +208 -0
- package/lib/presets.mjs +26 -5
- package/lib/publish.mjs +55 -10
- package/lib/qr.mjs +317 -0
- package/lib/record.mjs +74 -0
- package/lib/routines.mjs +54 -4
- package/lib/sigstore-trust.json +20 -0
- package/lib/skills.mjs +1 -1
- package/lib/sync-lock.mjs +143 -0
- package/lib/sync-rules.mjs +154 -1
- package/lib/takeout.mjs +169 -0
- package/lib/tdx-attestation.mjs +412 -0
- package/lib/tdx-trust.json +10 -0
- package/lib/tools.mjs +1040 -31
- package/lib/touches.mjs +427 -0
- package/lib/update-check.mjs +30 -0
- package/lib/watch.mjs +160 -0
- package/lib/yaml.mjs +8 -4
- package/package.json +2 -1
- package/skills/evda/SKILL.md +14 -0
- package/templates/AGENTS.md +81 -10
- 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
|
|
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(`
|
|
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("");
|