create-evda 0.1.5 → 0.1.6
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 +100 -0
- package/README.md +74 -7
- package/bin/evda.mjs +391 -6
- package/create-evda.mjs +54 -10
- package/docs/local-model.md +34 -0
- package/hooks/evda-read-guard.mjs +7 -0
- package/lib/chat-export.mjs +198 -0
- package/lib/circle-plan.mjs +277 -0
- package/lib/circles.mjs +479 -28
- package/lib/connect.mjs +46 -0
- package/lib/day-file.mjs +93 -0
- package/lib/declarations.mjs +235 -0
- package/lib/devices.mjs +202 -0
- package/lib/doctor.mjs +346 -13
- package/lib/filechanges.mjs +622 -0
- package/lib/gather-apply.mjs +635 -0
- package/lib/gather.mjs +808 -0
- package/lib/grants.mjs +7 -1
- package/lib/guard.mjs +132 -0
- package/lib/hooks.mjs +124 -0
- package/lib/keep-hook.mjs +185 -0
- package/lib/kinds.mjs +23 -6
- package/lib/kinds.schema.json +2 -1
- package/lib/manifest.mjs +14 -1
- package/lib/mcp-auth.mjs +370 -0
- package/lib/mcp-http.mjs +336 -0
- package/lib/mcp.mjs +425 -36
- package/lib/migrate-people.mjs +231 -0
- package/lib/organize.mjs +52 -12
- package/lib/parity.mjs +111 -0
- package/lib/presets.mjs +58 -7
- package/lib/prompt-hook.mjs +88 -0
- package/lib/publish.mjs +264 -0
- package/lib/readers.mjs +319 -0
- package/lib/records.mjs +52 -0
- package/lib/references.mjs +5 -51
- package/lib/render.mjs +158 -0
- package/lib/roles.mjs +102 -1
- package/lib/secret-shape.mjs +58 -0
- package/lib/setup.mjs +118 -0
- package/lib/skills.mjs +266 -0
- package/lib/subrecord.mjs +121 -0
- package/lib/sync-rules.mjs +87 -0
- package/lib/tools.mjs +118 -27
- package/lib/update-check.mjs +146 -0
- package/lib/work.mjs +180 -0
- package/lib/yaml.mjs +117 -42
- package/package.json +24 -15
- package/presets/journal/preset.json +9 -1
- package/presets/projects-and-areas/preset.json +16 -9
- package/skills/evda/SKILL.md +58 -0
- package/templates/AGENTS.md +118 -14
- package/templates/README.md +15 -1
- package/templates/circles-README.md +3 -3
- package/templates/evda-digest.md +30 -6
- package/LICENSE +0 -21
- package/presets/student/preset.json +0 -13
package/bin/evda.mjs
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// ~/.evda/current. Nothing assumes it was launched inside one.
|
|
7
7
|
|
|
8
8
|
import { resolve, join, relative } from "node:path";
|
|
9
|
-
import { readFileSync, writeFileSync, statSync } from "node:fs";
|
|
9
|
+
import { readFileSync, writeFileSync, statSync, existsSync } from "node:fs";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { openEvda, readManifest, readVersion, writeVersion, findRoot, SCHEMA_VERSION } from "../lib/manifest.mjs";
|
|
11
|
+
import { openEvda, readManifest, readVersion, writeVersion, findRoot, SCHEMA_VERSION, manifestPath } from "../lib/manifest.mjs";
|
|
12
12
|
import { locate, setPointer, pointerPath } from "../lib/locate.mjs";
|
|
13
13
|
import { resolveAll, agentsFile } from "../lib/roles.mjs";
|
|
14
14
|
import { doctor } from "../lib/doctor.mjs";
|
|
@@ -24,6 +24,17 @@ import { resolveReference } from "../lib/resolvers.mjs";
|
|
|
24
24
|
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
|
+
import { publishCommand } from "../lib/publish.mjs";
|
|
28
|
+
import { checkForUpdate, summarize, stampInstalledVersion } from "../lib/update-check.mjs";
|
|
29
|
+
import { planCircles, coverage, declare, deviceCircles, planBeside } from "../lib/circle-plan.mjs";
|
|
30
|
+
import { circleIndex, grantView } from "../lib/filechanges.mjs";
|
|
31
|
+
import { mountRoot } from "../lib/declarations.mjs";
|
|
32
|
+
import { excluder } from "../lib/sync-rules.mjs";
|
|
33
|
+
import { parity, formatParity } from "../lib/parity.mjs";
|
|
34
|
+
import { note, releaseNote, ownerSearch, readContext, readersFor, describeReaders, parseReaders, READ_BY } from "../lib/readers.mjs";
|
|
35
|
+
import { runHook } from "../lib/guard.mjs";
|
|
36
|
+
import { markdownFiles } from "../lib/circles.mjs";
|
|
37
|
+
import { refuseSecrets } from "../lib/tools.mjs";
|
|
27
38
|
|
|
28
39
|
const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
29
40
|
|
|
@@ -36,6 +47,12 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
36
47
|
evda upgrade [dir] replace the installer-owned block of the operating layer; touch nothing else
|
|
37
48
|
evda digest [dir] print the instruction-only digest of the operating layer
|
|
38
49
|
evda migrate [dir] forward migrations, each saying what it cannot reverse
|
|
50
|
+
evda check-update [dir] [--force]
|
|
51
|
+
ask npm for the latest create-evda, throttled to once an hour;
|
|
52
|
+
never silent, a failure prints CHECK_FAILED rather than looking current
|
|
53
|
+
evda update [dir] check-update, then upgrade and migrate if a newer version exists.
|
|
54
|
+
Run this through npx so the CLI itself is current first:
|
|
55
|
+
npx -y -p create-evda@latest evda update
|
|
39
56
|
evda vector <vdir> [--check] the conformance projection of <vdir>/ledger.md (Spec 04 vectors)
|
|
40
57
|
evda references [dir] [--resolve] [--timeout ms]
|
|
41
58
|
every reference in the record, its folder's connector, and with
|
|
@@ -50,12 +67,39 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
50
67
|
may that record read the circle? exit 0 yes, 2 refused, with why
|
|
51
68
|
evda mcp [dir] serve the record over MCP on stdio, for Claude Code, Codex or the
|
|
52
69
|
Claude desktop app. No key and no model call: the AI is the owner's
|
|
70
|
+
evda mcp --http --public-url <https url> [--port 8378] [--host 127.0.0.1] [--log file] [dir]
|
|
71
|
+
the same server over streamable HTTP, behind OAuth, for the Claude
|
|
72
|
+
apps through a tunnel. Binds to localhost; the tunnel is the TLS
|
|
73
|
+
evda mcp token --new [--ttl 24h] | --revoke-all | --status [dir]
|
|
74
|
+
a one-time owner code for the authorization page, or end every
|
|
75
|
+
connection, or say what is connected
|
|
53
76
|
evda connect [dir] how to add that server to each client, with this record's path
|
|
54
77
|
evda skill install [--project <dir>]
|
|
55
78
|
install the evda skill for Claude Code (~/.claude/skills, or a project)
|
|
79
|
+
evda hook install [dir] add the keep-record Stop hook and the read guard to the record's .claude/settings.json,
|
|
80
|
+
and the prompt hook that files each message when evda.json says "filing": "everything"
|
|
81
|
+
evda hook status [dir] whether each is installed and current
|
|
56
82
|
evda organize [dir] [--dry-run] [--with-ai] [--model M]
|
|
57
83
|
file captures into their day and write a report of what belongs where.
|
|
58
84
|
Moves nothing. --with-ai hands the judgment to the owner's claude -p
|
|
85
|
+
evda gather scan <source>... [--into <record>] [--out <file>] [--with-ai] [--model M]
|
|
86
|
+
walk a pile (folders, export files), type each file, and write a
|
|
87
|
+
proposal: a preset, folders, a placement per item. Writes nothing
|
|
88
|
+
into a source or a record; the proposal goes to --out, or
|
|
89
|
+
$EVDA_GATHER_DIR, or ~/.evda/gather/. --into names the record it
|
|
90
|
+
would grow: what it already holds is left out of the proposal
|
|
91
|
+
evda gather questions <proposal>
|
|
92
|
+
what the owner has to decide, only where it changes a placement
|
|
93
|
+
evda gather answer <proposal> <question> <value>
|
|
94
|
+
record one answer on the proposal and say what it re-placed
|
|
95
|
+
evda gather show <proposal> [--all]
|
|
96
|
+
the proposal in plain words
|
|
97
|
+
evda gather apply <proposal> --into <record> [--copy|--move] [--dry] [--manifest <file>]
|
|
98
|
+
after the owner's yes: write the placements into a record (a new
|
|
99
|
+
one is made with the proposal's preset), copying by default, then
|
|
100
|
+
run doctor. Writes a reversal manifest beside the proposal
|
|
101
|
+
evda gather revert <manifest> [--force]
|
|
102
|
+
undo an apply byte for byte; refused when a placed file was edited
|
|
59
103
|
evda look-back [dir] [--model M]
|
|
60
104
|
the week, read back by the owner's claude -p, appended to a report
|
|
61
105
|
evda routine <morning|weekly> [dir] [--launchd] [--install]
|
|
@@ -68,6 +112,32 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
68
112
|
manifest that differs is a CONFLICT and is left as it is
|
|
69
113
|
evda preset diff <id> [dir] what the preset has that this record does not
|
|
70
114
|
evda status [dir] one screen summary
|
|
115
|
+
evda publish public [dir] --out <dir> --key <seed file>
|
|
116
|
+
the public circle as a signed plaintext export; refuses (exit 2) on a
|
|
117
|
+
path another circle covers or a private name or path in the bytes
|
|
118
|
+
evda circles plan --plan <file> [dir]
|
|
119
|
+
which folders each circle of the plan gets on this disk. Writes nothing
|
|
120
|
+
evda circles declare --plan <file> [--record-id <id>] [dir]
|
|
121
|
+
write the plan's circles and device grants into the record. Adds the
|
|
122
|
+
circles and grants roles and their folders; moves and changes no file.
|
|
123
|
+
A circle rooted at "" is written as the root itself, and a circle
|
|
124
|
+
already declared keeps its id, release, rules, mount and policy
|
|
125
|
+
evda circles check [--plan <file>] [dir]
|
|
126
|
+
every syncable file in exactly one circle (CHECKED n files); fails on a
|
|
127
|
+
file no circle covers, and on a symlink, iCloud stub or oversize file.
|
|
128
|
+
Without --plan, reads <circles role>/plan.json for the excludes
|
|
129
|
+
evda parity --plan <file> --device <name> --shadow <dir> [dir]
|
|
130
|
+
the device's shadow equals the record for the circles it holds, byte for
|
|
131
|
+
byte, and holds nothing of a circle it may never hold (Spec 08 S1)
|
|
132
|
+
evda work [dir] [--env-file F] [--json]
|
|
133
|
+
the record's efforts from its work role: THE HARLEQUIN by vault_path
|
|
134
|
+
or project, or a folder of effort files. CHECKED n, or SKIPPED and why
|
|
135
|
+
evda note "<words>" [--in dir] just note it: a capture no AI reads until you release it (words from stdin with -)
|
|
136
|
+
evda release <path> [dir] you can read that now: clear the just-noted flag on one entry
|
|
137
|
+
evda search <words> [dir] your own search, on this device, no model; just-noted and sealed entries included and marked
|
|
138
|
+
evda readers [dir] which AI reads each part of the record, from read_by
|
|
139
|
+
evda local-model how to run a model on your own computer and point a circle at it
|
|
140
|
+
evda guard the Claude Code read guard (a PreToolUse hook; reads the tool call on stdin)
|
|
71
141
|
evda where which EVDA the locator resolves to, and by which rule
|
|
72
142
|
evda use <dir> point ~/.evda/current at a record
|
|
73
143
|
evda init <dir> [--yes] [--agent-name N] [--owner O] [--purpose P] [--voice V]
|
|
@@ -95,7 +165,7 @@ function isDirArg(a) {
|
|
|
95
165
|
}
|
|
96
166
|
|
|
97
167
|
// `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
|
|
98
|
-
const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday"];
|
|
168
|
+
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"];
|
|
99
169
|
function flagValue(args, name) {
|
|
100
170
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
101
171
|
if (eq) return eq.slice(name.length + 3);
|
|
@@ -113,6 +183,63 @@ function pickDir(args) {
|
|
|
113
183
|
return { dir: found.dir, via: found.via, flags, rest };
|
|
114
184
|
}
|
|
115
185
|
|
|
186
|
+
// `release`'s own positional is a printed entry path, which always contains a "/" (Spec: entries
|
|
187
|
+
// live under record/), so pickDir's "any positional with a slash is the record" rule always ate it.
|
|
188
|
+
// The record here comes from the working directory (or the pointer) by default; a positional is
|
|
189
|
+
// only ever the record when it is a SECOND positional that is itself an EVDA on disk, never the
|
|
190
|
+
// entry path itself.
|
|
191
|
+
function pickReleaseArgs(args) {
|
|
192
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
193
|
+
let explicit = null;
|
|
194
|
+
let entryParts = positional;
|
|
195
|
+
if (positional.length > 1) {
|
|
196
|
+
const last = positional[positional.length - 1];
|
|
197
|
+
try {
|
|
198
|
+
if (statSync(last).isDirectory() && existsSync(manifestPath(resolve(last)))) {
|
|
199
|
+
explicit = last;
|
|
200
|
+
entryParts = positional.slice(0, -1);
|
|
201
|
+
}
|
|
202
|
+
} catch { /* not a directory on disk -> it's part of the entry path */ }
|
|
203
|
+
}
|
|
204
|
+
const found = locate({ explicit });
|
|
205
|
+
return { dir: found.dir, entry: entryParts.join(" ") };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// `evda mcp token`: the owner's side of the remote server's authorization. Printed here, on the
|
|
209
|
+
// machine that holds the record, and typed into the authorization page: never in a URL or a log.
|
|
210
|
+
async function mcpToken(args) {
|
|
211
|
+
const { dir, flags } = pickDir(args);
|
|
212
|
+
const auth = await import("../lib/mcp-auth.mjs");
|
|
213
|
+
const store = auth.openStore(auth.authDir(openEvda(dir)));
|
|
214
|
+
if (flags.includes("--new")) {
|
|
215
|
+
const ttl = parseDuration(flagValue(args, "ttl") ?? "24h");
|
|
216
|
+
const { code, expires } = auth.newOwnerCode(store, { ttlMs: ttl });
|
|
217
|
+
console.log(code);
|
|
218
|
+
console.error(`One owner code, good once, until ${new Date(expires).toISOString()}. Type it into the page Claude opens when you connect.`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
if (flags.includes("--revoke-all")) {
|
|
222
|
+
const n = auth.revokeAll(store);
|
|
223
|
+
console.log(`Revoked ${n.clients} client(s), ${n.access} access token(s), ${n.refresh} refresh token(s) and ${n.owner} owner code(s). A connector stops at its next call and has to connect again.`);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (flags.includes("--status")) {
|
|
227
|
+
const s = auth.status(store);
|
|
228
|
+
console.log(`${s.clients.length} client(s), ${s.access} live access token(s), ${s.refresh} live refresh token(s), ${s.ownerCodes.length} open owner code(s). State: ${store.file}`);
|
|
229
|
+
for (const c of s.clients) console.log(` ${c.client_id} ${c.client_name ?? "(no name)"} registered ${new Date(c.created).toISOString()} -> ${c.redirects.join(", ")}`);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
throw new EvdaError("REFUSED: evda mcp token needs --new [--ttl 24h], --revoke-all or --status.", { code: "USAGE", exit: 2 });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseDuration(s) {
|
|
236
|
+
const m = /^(\d+)(m|h|d)$/.exec(String(s));
|
|
237
|
+
if (!m) throw new EvdaError(`REFUSED: --ttl is a number and m, h or d (got ${JSON.stringify(s)}).`, { code: "BAD_TTL", exit: 2 });
|
|
238
|
+
const ms = Number(m[1]) * { m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
|
|
239
|
+
if (ms < 60_000 || ms > 7 * 86_400_000) throw new EvdaError("REFUSED: --ttl is between 1m and 7d.", { code: "BAD_TTL", exit: 2 });
|
|
240
|
+
return ms;
|
|
241
|
+
}
|
|
242
|
+
|
|
116
243
|
async function main(argv) {
|
|
117
244
|
const [cmd, ...args] = argv;
|
|
118
245
|
switch (cmd) {
|
|
@@ -134,8 +261,19 @@ async function main(argv) {
|
|
|
134
261
|
}
|
|
135
262
|
|
|
136
263
|
case "mcp": {
|
|
264
|
+
if (args[0] === "token") return mcpToken(args.slice(1));
|
|
265
|
+
const { dir, flags } = pickDir(args);
|
|
266
|
+
if (flags.includes("--http")) {
|
|
267
|
+
// The record over streamable HTTP with OAuth in front (HQ-1378), for a tunnel to publish.
|
|
268
|
+
const port = Number(flagValue(args, "port") ?? 8378);
|
|
269
|
+
const publicUrl = flagValue(args, "public-url");
|
|
270
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new EvdaError("REFUSED: --port must be a port number.", { code: "BAD_PORT", exit: 2 });
|
|
271
|
+
if (!publicUrl) throw new EvdaError("REFUSED: --http needs --public-url, the https address the tunnel serves this on (every URL in the OAuth metadata is built from it).", { code: "NO_PUBLIC_URL", exit: 2 });
|
|
272
|
+
const { serveHttp } = await import("../lib/mcp-http.mjs");
|
|
273
|
+
await serveHttp({ dir, version: packageVersion(), port, host: flagValue(args, "host") ?? "127.0.0.1", publicUrl, logFile: flagValue(args, "log"), lifeline: flagValue(args, "lifeline") });
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
137
276
|
// The record as an MCP server on stdio. stdout is the protocol; everything else is stderr.
|
|
138
|
-
const { dir } = pickDir(args);
|
|
139
277
|
const { serve } = await import("../lib/mcp.mjs");
|
|
140
278
|
await serve({ dir, version: packageVersion() });
|
|
141
279
|
return 0;
|
|
@@ -149,6 +287,68 @@ async function main(argv) {
|
|
|
149
287
|
return r.exit;
|
|
150
288
|
}
|
|
151
289
|
|
|
290
|
+
case "gather": {
|
|
291
|
+
const g = await import("../lib/gather.mjs");
|
|
292
|
+
const [sub, ...rest] = args;
|
|
293
|
+
const values = new Set(["out", "model", "into", "manifest", "owner"].map((n) => rest.indexOf(`--${n}`)).filter((i) => i >= 0).map((i) => i + 1));
|
|
294
|
+
const positional = rest.filter((a, i) => !a.startsWith("--") && !values.has(i));
|
|
295
|
+
if (sub === "scan") {
|
|
296
|
+
if (!positional.length) throw new EvdaError("usage: evda gather scan <source>... [--into <record>] [--out <file>] [--with-ai] [--model M]", { exit: 2 });
|
|
297
|
+
let p = g.gather(positional, { into: flagValue(rest, "into") ?? null });
|
|
298
|
+
let exit = 0;
|
|
299
|
+
if (rest.includes("--with-ai")) {
|
|
300
|
+
const r = g.withOwnersAi(p, { model: flagValue(rest, "model") ?? null });
|
|
301
|
+
p = r.p;
|
|
302
|
+
if (!r.ok) exit = 1;
|
|
303
|
+
}
|
|
304
|
+
const out = g.saveProposal(p, flagValue(rest, "out") ?? join(g.gatherDir(), `${p.id}.json`));
|
|
305
|
+
console.log(g.summaryText(p));
|
|
306
|
+
if (p.ai) console.log(`\nYour AI: ${p.ai.status}${p.ai.why ? `, ${p.ai.why}` : `, ${p.ai.applied} of ${p.ai.asked} typed, ${p.ai.ignored} line(s) not read`}`);
|
|
307
|
+
console.log(`\nProposal written to ${out}`);
|
|
308
|
+
for (const l of g.checkedLines(p)) console.log(l);
|
|
309
|
+
return p.counts.items === 0 && !p.counts.chats ? 1 : exit;
|
|
310
|
+
}
|
|
311
|
+
if (sub === "questions" || sub === "show") {
|
|
312
|
+
if (!positional[0]) throw new EvdaError(`usage: evda gather ${sub} <proposal>`, { exit: 2 });
|
|
313
|
+
const { p, abs } = g.loadProposal(positional[0]);
|
|
314
|
+
if (sub === "show") { console.log(g.summaryText(p, { detail: rest.includes("--all") })); console.log(`\n${abs}`); for (const l of g.checkedLines(p)) console.log(l); return 0; }
|
|
315
|
+
const open = p.questions.filter((q) => q.answer === undefined);
|
|
316
|
+
for (const q of open) console.log(`[${q.id}] ${q.text} (${q.options.join(" / ")}) moves ${q.affects.length} item(s)`);
|
|
317
|
+
for (const q of p.questions.filter((x) => x.answer !== undefined)) console.log(`answered [${q.id}] ${q.answer}`);
|
|
318
|
+
console.log(open.length ? `CHECKED ${p.questions.length} question(s) ${open.length} open` : `SKIPPED questions: none open on ${abs}`);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
if (sub === "answer") {
|
|
322
|
+
const [ref, question, value] = positional;
|
|
323
|
+
if (!value) throw new EvdaError("usage: evda gather answer <proposal> <question> <value>", { exit: 2 });
|
|
324
|
+
const { p, abs } = g.loadProposal(ref);
|
|
325
|
+
const r = g.answer(p, question, value);
|
|
326
|
+
g.saveProposal(r.proposal, abs);
|
|
327
|
+
console.log(`Recorded ${question} = ${value} on ${abs}`);
|
|
328
|
+
for (const l of g.answerLines(r)) console.log(l);
|
|
329
|
+
console.log(`CHECKED ${r.proposal.items.length} items ${r.moved.length} re-placed`);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
if (sub === "apply") {
|
|
333
|
+
const into = flagValue(rest, "into");
|
|
334
|
+
if (!positional[0] || !into) throw new EvdaError("usage: evda gather apply <proposal> --into <record> [--copy|--move] [--dry] [--manifest <file>] [--owner NAME]", { exit: 2 });
|
|
335
|
+
if (rest.includes("--copy") && rest.includes("--move")) throw new EvdaError("REFUSED: --copy and --move together. Copy is the default; pass --move to take the files out of the sources.", { exit: 2 });
|
|
336
|
+
const a = await import("../lib/gather-apply.mjs");
|
|
337
|
+
const { p, abs } = g.loadProposal(positional[0]);
|
|
338
|
+
const r = await a.applyGather(p, into, { mode: rest.includes("--move") ? "move" : "copy", dry: rest.includes("--dry"), manifestOut: flagValue(rest, "manifest") ?? null, owner: flagValue(rest, "owner") ?? "", proposalPath: abs });
|
|
339
|
+
for (const l of r.lines) console.log(l);
|
|
340
|
+
return r.dry ? 0 : r.doctor.exit === 0 ? 0 : 1;
|
|
341
|
+
}
|
|
342
|
+
if (sub === "revert") {
|
|
343
|
+
if (!positional[0]) throw new EvdaError("usage: evda gather revert <manifest> [--force]", { exit: 2 });
|
|
344
|
+
const a = await import("../lib/gather-apply.mjs");
|
|
345
|
+
const r = a.revertGather(positional[0], { force: rest.includes("--force") });
|
|
346
|
+
for (const l of a.revertLines(r)) console.log(l);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
throw new EvdaError("usage: evda gather scan <source>... | questions <proposal> | answer <proposal> <question> <value> | show <proposal> | apply <proposal> --into <record> | revert <manifest>", { exit: 2 });
|
|
350
|
+
}
|
|
351
|
+
|
|
152
352
|
case "look-back": {
|
|
153
353
|
const { dir } = pickDir(args);
|
|
154
354
|
const { lookBack } = await import("../lib/organize.mjs");
|
|
@@ -167,6 +367,40 @@ async function main(argv) {
|
|
|
167
367
|
return 0;
|
|
168
368
|
}
|
|
169
369
|
|
|
370
|
+
case "hook": {
|
|
371
|
+
// evda hook install [dir] | status [dir] the keep-record Stop hook for Claude Code (HQ-1376)
|
|
372
|
+
const [sub, ...rest] = args;
|
|
373
|
+
if (!["install", "status"].includes(sub)) throw new EvdaError("usage: evda hook install [dir] | status [dir]", { exit: 2 });
|
|
374
|
+
const { dir } = pickDir(rest);
|
|
375
|
+
// Every hook create-evda installs for a Claude owner (HQ-1377): the keep-record Stop hook and
|
|
376
|
+
// the read guard, both in the record's own settings and nowhere else.
|
|
377
|
+
const hooks = await import("../lib/hooks.mjs");
|
|
378
|
+
const { installGuard, guardStatus } = await import("../lib/connect.mjs");
|
|
379
|
+
if (sub === "install") {
|
|
380
|
+
const r = hooks.installHook(dir);
|
|
381
|
+
console.log(`${r.action}: ${r.settings} runs ${r.hook} in this record's Claude Code sessions, and nowhere else`);
|
|
382
|
+
const g = installGuard(dir);
|
|
383
|
+
console.log(`read guard ${g.action} ${g.path}`);
|
|
384
|
+
// The prompt hook (HQ-1385) files every message, so only the owner's "everything" installs it.
|
|
385
|
+
const { filingOf } = await import("../lib/day-file.mjs");
|
|
386
|
+
const { readManifest } = await import("../lib/manifest.mjs");
|
|
387
|
+
const filing = filingOf(readManifest(dir));
|
|
388
|
+
if (filing === "everything") {
|
|
389
|
+
const p = hooks.installPromptHook(dir);
|
|
390
|
+
console.log(`prompt hook ${p.action}: ${p.settings} runs ${p.hook}, which files each message the owner sends`);
|
|
391
|
+
} else console.log(`prompt hook not installed: evda.json says ${filing ? `"filing": "${filing}"` : "nothing about filing yet"}, and it is installed only for "everything"`);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
const st = hooks.hookStatus(dir);
|
|
395
|
+
if (st.problem) { console.log(st.problem); return 1; }
|
|
396
|
+
console.log(st.registered ? (st.file ? (st.current ? `installed and current: ${hooks.SETTINGS_REL} runs ${hooks.HOOK_REL}` : `installed, older than the shipped hook: run \`evda hook install\``) : `registered, but ${hooks.HOOK_REL} is missing: run \`evda hook install\``) : "not installed: run `evda hook install`");
|
|
397
|
+
const ph = hooks.promptHookStatus(dir);
|
|
398
|
+
console.log(ph.registered ? (ph.file ? (ph.current ? `prompt hook installed and current: ${hooks.SETTINGS_REL} runs ${hooks.PROMPT_HOOK_REL}` : "prompt hook installed, older than the shipped hook: run `evda hook install`") : `prompt hook registered, but ${hooks.PROMPT_HOOK_REL} is missing: run \`evda hook install\``) : "prompt hook not installed (it is only for an owner who chose to file everything)");
|
|
399
|
+
const g = guardStatus(dir);
|
|
400
|
+
console.log(g.registered ? (g.file ? `read guard installed: ${hooks.SETTINGS_REL} runs ${g.target ?? "evda guard through npx"}` : `read guard registered, but ${g.target} is missing: run \`evda hook install\``) : "read guard not installed: run `evda hook install`");
|
|
401
|
+
return st.registered && st.file && st.current && g.registered && g.file && (!ph.registered || (ph.file && ph.current)) ? 0 : 1;
|
|
402
|
+
}
|
|
403
|
+
|
|
170
404
|
case "connect": {
|
|
171
405
|
// How to add this record's MCP server to each client, with the record's path filled in.
|
|
172
406
|
const { dir } = pickDir(args);
|
|
@@ -198,6 +432,7 @@ async function main(argv) {
|
|
|
198
432
|
console.log(`${p.title} (${p.id}): ${p.for}`);
|
|
199
433
|
for (const f of p.folders) console.log(` ${`${f.path}/`.padEnd(32)} ${(f.type ?? "untyped").padEnd(8)}${f.connectors?.length ? ` connectors ${f.connectors.join(", ")}` : ""}${f.note ? ` ${f.note}` : ""}`);
|
|
200
434
|
for (const c of p.circles ?? []) console.log(` circle ${c.name} (${c.public ? "public" : "private"}, proposed only): ${c.description}`);
|
|
435
|
+
for (const [k, v] of Object.entries(p.mapping ?? {})) console.log(` role ${k.padEnd(24)} ${v}/`);
|
|
201
436
|
for (const r of p.routines ?? []) console.log(` routine ${r} (offered, never scheduled without a yes)`);
|
|
202
437
|
return 0;
|
|
203
438
|
}
|
|
@@ -214,13 +449,28 @@ async function main(argv) {
|
|
|
214
449
|
const plan = presets.applyPreset(dir, id, { dry });
|
|
215
450
|
console.log(`evda preset apply ${id}${dry ? " --dry" : ""} ${plan.root}`);
|
|
216
451
|
for (const l of presets.planLines(plan)) console.log(l);
|
|
217
|
-
const wrote = plan.create.length + plan.write.length + plan.views.length + plan.proposed.length;
|
|
452
|
+
const wrote = plan.create.length + plan.write.length + plan.views.length + plan.proposed.length + plan.roles.length;
|
|
218
453
|
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)`);
|
|
219
454
|
if (!dry && wrote === 0 && !plan.conflicts.length) console.log("Nothing was written: the record already has this preset.");
|
|
220
455
|
if (dry) console.log("Dry run: nothing was written.");
|
|
221
456
|
return plan.conflicts.length ? 1 : 0;
|
|
222
457
|
}
|
|
223
458
|
|
|
459
|
+
case "work": {
|
|
460
|
+
// evda work [dir] [--env-file <file>] [--json]: the record's efforts, from THE HARLEQUIN or a
|
|
461
|
+
// folder of effort files (HQ-1371). Read only.
|
|
462
|
+
const { readWork } = await import("../lib/work.mjs");
|
|
463
|
+
const { dir } = pickDir(args);
|
|
464
|
+
const envFile = flagValue(args, "env-file");
|
|
465
|
+
const r = await readWork(openEvda(dir), { envFile: envFile ? resolve(envFile) : null });
|
|
466
|
+
if (args.includes("--json")) console.log(JSON.stringify({ efforts: r.efforts, lines: r.lines }, null, 2));
|
|
467
|
+
else {
|
|
468
|
+
for (const e of r.efforts) console.log(` ${e.id.padEnd(12)} ${String(e.status).padEnd(16)} ${(e.planned_for ?? "").padEnd(10)} ${e.title}`);
|
|
469
|
+
for (const l of r.lines) console.log(l);
|
|
470
|
+
}
|
|
471
|
+
return r.exit;
|
|
472
|
+
}
|
|
473
|
+
|
|
224
474
|
case "where": {
|
|
225
475
|
const found = locate({});
|
|
226
476
|
console.log(`${found.dir} (via ${found.via})`);
|
|
@@ -240,6 +490,62 @@ async function main(argv) {
|
|
|
240
490
|
return doctor(dir, { via }).exit;
|
|
241
491
|
}
|
|
242
492
|
|
|
493
|
+
case "guard":
|
|
494
|
+
return runHook();
|
|
495
|
+
|
|
496
|
+
case "note": {
|
|
497
|
+
// The words may hold a slash ("50/50"), so the record is never guessed from them: --in <dir>,
|
|
498
|
+
// or the locator.
|
|
499
|
+
const i = args.indexOf("--in");
|
|
500
|
+
const dir = locate({ explicit: i >= 0 ? args[i + 1] : undefined }).dir;
|
|
501
|
+
let words = args.filter((_, j) => i < 0 || (j !== i && j !== i + 1)).join(" ");
|
|
502
|
+
if (words === "-" || words === "") words = readFileSync(0, "utf8");
|
|
503
|
+
if (!words.trim()) throw new EvdaError('REFUSED: note needs the words, as an argument or on stdin. Nothing was written.', { exit: 2 });
|
|
504
|
+
refuseSecrets(words, "evda note");
|
|
505
|
+
const r = note(openEvda(dir), words);
|
|
506
|
+
console.log(`Noted at ${r.path}. No AI reads it until you say it can: evda release ${JSON.stringify(r.path)}`);
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case "release": {
|
|
511
|
+
const { dir, entry } = pickReleaseArgs(args);
|
|
512
|
+
const r = releaseNote(openEvda(dir), entry);
|
|
513
|
+
console.log(`Released ${r.path}. It reads like any other entry now.`);
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
case "search": {
|
|
518
|
+
const { dir, rest } = pickDir(args);
|
|
519
|
+
const query = rest.join(" ").trim();
|
|
520
|
+
if (!query) throw new EvdaError("search needs words to find", { exit: 2 });
|
|
521
|
+
const evda = openEvda(dir);
|
|
522
|
+
const files = markdownFiles(evda.dir);
|
|
523
|
+
const hits = ownerSearch(evda, query, { files });
|
|
524
|
+
for (const h of hits) console.log(`${h.sealed ? "[sealed] " : ""}${h.noted ? "[noted] " : ""}${h.path}:${h.line}: ${h.text.length > 300 ? `${h.text.slice(0, 300)}...` : h.text}`);
|
|
525
|
+
console.log(`CHECKED ${files.length} files ${hits.length} match(es), ${hits.filter((h) => h.noted).length} in just-noted entries, ${hits.filter((h) => h.sealed).length} in sealed entries; on this device, no model`);
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
case "readers": {
|
|
530
|
+
const { dir } = pickDir(args);
|
|
531
|
+
const evda = openEvda(dir);
|
|
532
|
+
const ctx = readContext(evda);
|
|
533
|
+
const rec = parseReaders(evda.manifest[READ_BY], "evda.json read_by");
|
|
534
|
+
console.log(`the record: Read by: ${describeReaders(rec.problems.length ? null : rec.readers)}${ctx.circles.length ? " (every part without its own)" : ""}`);
|
|
535
|
+
for (const c of ctx.circles) {
|
|
536
|
+
const any = Object.values(c.members).find((m) => m.kind === "path");
|
|
537
|
+
const probe = any ? (any.isDir ? join(any.abs, ".probe.md") : any.abs) : null;
|
|
538
|
+
const r = probe ? readersFor(ctx, probe) : { readers: null, isPublic: c.manifest?.public === true, source: null };
|
|
539
|
+
console.log(`${c.name}: Read by: ${describeReaders(r.readers, { isPublic: r.isPublic })}${r.source === "evda.json" ? " (from evda.json)" : ""}`);
|
|
540
|
+
}
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
case "local-model": {
|
|
545
|
+
console.log(readFileSync(fileURLToPath(new URL("../docs/local-model.md", import.meta.url)), "utf8"));
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
243
549
|
case "reindex": {
|
|
244
550
|
const { dir, flags } = pickDir(args);
|
|
245
551
|
if (flags.includes("--adopt")) {
|
|
@@ -314,13 +620,36 @@ async function main(argv) {
|
|
|
314
620
|
const parts = splitFence(text);
|
|
315
621
|
if (!parts) throw new EvdaError(`${a.rel} has no evda:generated fence. The owner has taken the whole file; there is nothing the installer owns to replace, and nothing was changed.`);
|
|
316
622
|
const shipped = shippedGenerated();
|
|
317
|
-
if (parts.generated === shipped) {
|
|
623
|
+
if (parts.generated === shipped) {
|
|
624
|
+
stampInstalledVersion(dir, packageVersion());
|
|
625
|
+
console.log("upgrade: the generated block already matches what this tool ships. Nothing outside it was read.");
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
318
628
|
writeFileSync(a.abs, replaceFence(text, shipped));
|
|
319
629
|
reindex(evda);
|
|
630
|
+
stampInstalledVersion(dir, packageVersion());
|
|
320
631
|
console.log(`upgrade: replaced the generated block of ${a.rel} (${Buffer.byteLength(parts.generated)} -> ${Buffer.byteLength(shipped)} bytes). Everything outside the fence is untouched. Digest rebuilt.`);
|
|
321
632
|
return 0;
|
|
322
633
|
}
|
|
323
634
|
|
|
635
|
+
case "check-update": {
|
|
636
|
+
const { dir } = pickDir(args);
|
|
637
|
+
const result = await checkForUpdate(dir, { force: args.includes("--force") });
|
|
638
|
+
console.log(summarize(result));
|
|
639
|
+
return result.status === "CHECK_FAILED" ? 1 : 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
case "update": {
|
|
643
|
+
const { dir } = pickDir(args);
|
|
644
|
+
const result = await checkForUpdate(dir, { force: true });
|
|
645
|
+
console.log(summarize(result));
|
|
646
|
+
if (result.status === "CHECK_FAILED") return 1;
|
|
647
|
+
if (result.status !== "UPDATE_AVAILABLE" && result.status !== "UNSTAMPED") return 0;
|
|
648
|
+
const upgradeRc = await main(["upgrade", dir]);
|
|
649
|
+
if (upgradeRc !== 0) return upgradeRc;
|
|
650
|
+
return main(["migrate", dir]);
|
|
651
|
+
}
|
|
652
|
+
|
|
324
653
|
case "digest": {
|
|
325
654
|
const { dir } = pickDir(args);
|
|
326
655
|
const evda = openEvda(dir);
|
|
@@ -455,6 +784,62 @@ async function main(argv) {
|
|
|
455
784
|
return 0;
|
|
456
785
|
}
|
|
457
786
|
|
|
787
|
+
case "publish": {
|
|
788
|
+
const { dir, rest } = pickDir(args);
|
|
789
|
+
return publishCommand(dir, rest[0], { out: flagValue(args, "out"), key: flagValue(args, "key") });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
case "circles": {
|
|
793
|
+
const sub = args[0];
|
|
794
|
+
const { dir } = pickDir(args.slice(1));
|
|
795
|
+
const planFile = flagValue(args, "plan");
|
|
796
|
+
const plan = planFile ? JSON.parse(readFileSync(resolve(planFile), "utf8")) : null;
|
|
797
|
+
if (sub === "plan" || sub === "declare") {
|
|
798
|
+
if (!plan) throw new EvdaError(`circles ${sub} needs --plan <file>`, { exit: 2 });
|
|
799
|
+
if (sub === "plan") {
|
|
800
|
+
const { circles, missing } = planCircles(dir, plan);
|
|
801
|
+
for (const c of circles) console.log(`${c.name}${c.public ? " (public)" : ""}: ${c.members.length} member(s)\n${c.members.map((m) => ` ${m.path === "" ? "(the record root: everything no other circle claims)" : `${m.path}${m.isDir ? "/" : ""}`}`).join("\n")}`);
|
|
802
|
+
for (const m of missing) console.log(`MISSING root ${m}: nothing is there yet; declared so the path has a circle when it appears`);
|
|
803
|
+
return 0;
|
|
804
|
+
}
|
|
805
|
+
const { written } = declare(dir, plan, { recordId: flagValue(args, "record-id") });
|
|
806
|
+
console.log(`wrote ${written.length} files: ${written.map((p) => relative(dir, p)).join(", ")}`);
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
if (sub === "check") {
|
|
810
|
+
const evda = openEvda(dir);
|
|
811
|
+
// Without --plan, the plan kept beside the circles (`<circles role>/plan.json`) says what is
|
|
812
|
+
// excluded, so this check and doctor exclude what the scan excludes.
|
|
813
|
+
const beside = plan ? null : planBeside(evda);
|
|
814
|
+
const excludes = plan?.exclude ?? beside?.plan?.exclude ?? [];
|
|
815
|
+
// A bounded record's circles stop at its sub-records (HQ-1371); each is checked as its own.
|
|
816
|
+
const { boundaries } = await import("../lib/records.mjs");
|
|
817
|
+
const stops = boundaries(evda);
|
|
818
|
+
if (stops.length) console.log(`STOPS at ${stops.length} sub-record(s): ${stops.join(", ")}`);
|
|
819
|
+
const r = coverage(dir, circleIndex(evda), { exclude: excluder([...excludes, ...stops]), mountRoot: mountRoot(evda) });
|
|
820
|
+
for (const p of r.problems) console.log(`FAIL ${p.code} ${p.rel}: ${p.why}`);
|
|
821
|
+
console.log(`CHECKED ${r.checked} files in ${Object.keys(r.perCircle).length} circles: ${Object.entries(r.perCircle).map(([n, k]) => `${n} ${k}`).join(", ")}; EXCLUDED ${r.excluded} (plan: ${planFile ? relative(dir, resolve(planFile)) : beside ? beside.path : "none, built-in excludes only"})`);
|
|
822
|
+
if (r.checked === 0) { console.log("FAIL CHECKED 0: nothing was examined"); return 1; }
|
|
823
|
+
return r.problems.length ? 1 : 0;
|
|
824
|
+
}
|
|
825
|
+
throw new EvdaError("circles needs plan, declare or check", { exit: 2 });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
case "parity": {
|
|
829
|
+
const { dir } = pickDir(args);
|
|
830
|
+
const planFile = flagValue(args, "plan");
|
|
831
|
+
const device = flagValue(args, "device");
|
|
832
|
+
const shadow = flagValue(args, "shadow");
|
|
833
|
+
if (!planFile || !device || !shadow) throw new EvdaError("parity needs --plan <file> --device <name> --shadow <dir>", { exit: 2 });
|
|
834
|
+
const plan = JSON.parse(readFileSync(resolve(planFile), "utf8"));
|
|
835
|
+
const evda = openEvda(dir);
|
|
836
|
+
const { holds, never, drift } = deviceCircles(plan, device, readGrants(evda) === null ? null : grantView(evda));
|
|
837
|
+
for (const d of drift) console.log(`NOTE plan drift ${d}; the grant files decide`);
|
|
838
|
+
const r = parity({ source: dir, shadow: resolve(shadow), index: circleIndex(evda), holds, never, exclude: excluder(plan.exclude) });
|
|
839
|
+
for (const line of formatParity(r)) console.log(line);
|
|
840
|
+
return r.ok ? 0 : 1;
|
|
841
|
+
}
|
|
842
|
+
|
|
458
843
|
case "status": {
|
|
459
844
|
const { dir, via } = pickDir(args);
|
|
460
845
|
const evda = openEvda(dir);
|