create-evda 0.1.5 → 0.1.7
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 +114 -0
- package/README.md +74 -7
- package/bin/evda.mjs +427 -9
- package/create-evda.mjs +64 -16
- 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/folders.mjs +1 -1
- package/lib/gather-apply.mjs +640 -0
- package/lib/gather.mjs +858 -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 +456 -36
- package/lib/messages.mjs +205 -0
- package/lib/migrate-people.mjs +231 -0
- package/lib/organize.mjs +176 -32
- package/lib/parity.mjs +184 -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 +106 -1
- package/lib/routines.mjs +142 -18
- package/lib/secret-shape.mjs +58 -0
- package/lib/setup.mjs +124 -4
- package/lib/skills.mjs +266 -0
- package/lib/subrecord.mjs +121 -0
- package/lib/sync-rules.mjs +93 -0
- package/lib/tools.mjs +133 -27
- package/lib/update-check.mjs +146 -0
- package/lib/work.mjs +180 -0
- package/lib/yaml.mjs +117 -42
- package/package.json +25 -15
- package/presets/journal/preset.json +9 -1
- package/presets/projects-and-areas/preset.json +16 -9
- package/skills/evda/SKILL.md +67 -8
- package/templates/AGENTS.md +122 -14
- package/templates/README.md +15 -1
- package/templates/circles-README.md +3 -3
- package/templates/evda-digest.md +31 -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, message as messageTool, replies as repliesTool } 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,17 +67,45 @@ 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
|
-
evda routine <morning|weekly> [dir] [--launchd] [--install]
|
|
105
|
+
evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
|
|
62
106
|
print a scheduled routine for Claude, or a launchd fallback; --install
|
|
63
|
-
writes the launchd job,
|
|
107
|
+
writes the launchd job, --uninstall removes it (setup installs the
|
|
108
|
+
morning organize unless told no)
|
|
64
109
|
evda preset list the organizations this package ships, and who each suits
|
|
65
110
|
evda preset show <id> one preset's folders
|
|
66
111
|
evda preset apply <id> [--dry] [dir]
|
|
@@ -68,6 +113,38 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
68
113
|
manifest that differs is a CONFLICT and is left as it is
|
|
69
114
|
evda preset diff <id> [dir] what the preset has that this record does not
|
|
70
115
|
evda status [dir] one screen summary
|
|
116
|
+
evda publish public [dir] --out <dir> --key <seed file>
|
|
117
|
+
the public circle as a signed plaintext export; refuses (exit 2) on a
|
|
118
|
+
path another circle covers or a private name or path in the bytes
|
|
119
|
+
evda circles plan --plan <file> [dir]
|
|
120
|
+
which folders each circle of the plan gets on this disk. Writes nothing
|
|
121
|
+
evda circles declare --plan <file> [--record-id <id>] [dir]
|
|
122
|
+
write the plan's circles and device grants into the record. Adds the
|
|
123
|
+
circles and grants roles and their folders; moves and changes no file.
|
|
124
|
+
A circle rooted at "" is written as the root itself, and a circle
|
|
125
|
+
already declared keeps its id, release, rules, mount and policy
|
|
126
|
+
evda circles check [--plan <file>] [dir]
|
|
127
|
+
every syncable file in exactly one circle (CHECKED n files); fails on a
|
|
128
|
+
file no circle covers, and on a symlink, iCloud stub or oversize file.
|
|
129
|
+
Without --plan, reads <circles role>/plan.json for the excludes
|
|
130
|
+
evda parity --plan <file> --device <name> --shadow <dir> [dir]
|
|
131
|
+
the device's shadow equals the record for the circles it holds, byte for
|
|
132
|
+
byte, and holds nothing of a circle it may never hold (Spec 08 S1)
|
|
133
|
+
evda work [dir] [--env-file F] [--json]
|
|
134
|
+
the record's efforts from its work role: THE HARLEQUIN by vault_path
|
|
135
|
+
or project, or a folder of effort files. CHECKED n, or SKIPPED and why
|
|
136
|
+
evda note "<words>" [--in dir] just note it: a capture no AI reads until you release it (words from stdin with -)
|
|
137
|
+
evda release <path> [dir] you can read that now: clear the just-noted flag on one entry
|
|
138
|
+
evda message --title "<title>" [--from NAME] [--in dir] "<body>"
|
|
139
|
+
tell the owner something: one new file in the messages role, a card
|
|
140
|
+
in their phone app (body from stdin with -)
|
|
141
|
+
evda replies [--peek] [--in dir] [--json]
|
|
142
|
+
the owner's replies not seen yet, marked seen unless --peek; read
|
|
143
|
+
under read_by as the AI $EVDA_READER names (claude, local, ...)
|
|
144
|
+
evda search <words> [dir] your own search, on this device, no model; just-noted and sealed entries included and marked
|
|
145
|
+
evda readers [dir] which AI reads each part of the record, from read_by
|
|
146
|
+
evda local-model how to run a model on your own computer and point a circle at it
|
|
147
|
+
evda guard the Claude Code read guard (a PreToolUse hook; reads the tool call on stdin)
|
|
71
148
|
evda where which EVDA the locator resolves to, and by which rule
|
|
72
149
|
evda use <dir> point ~/.evda/current at a record
|
|
73
150
|
evda init <dir> [--yes] [--agent-name N] [--owner O] [--purpose P] [--voice V]
|
|
@@ -95,7 +172,7 @@ function isDirArg(a) {
|
|
|
95
172
|
}
|
|
96
173
|
|
|
97
174
|
// `--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"];
|
|
175
|
+
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
176
|
function flagValue(args, name) {
|
|
100
177
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
101
178
|
if (eq) return eq.slice(name.length + 3);
|
|
@@ -113,6 +190,63 @@ function pickDir(args) {
|
|
|
113
190
|
return { dir: found.dir, via: found.via, flags, rest };
|
|
114
191
|
}
|
|
115
192
|
|
|
193
|
+
// `release`'s own positional is a printed entry path, which always contains a "/" (Spec: entries
|
|
194
|
+
// live under record/), so pickDir's "any positional with a slash is the record" rule always ate it.
|
|
195
|
+
// The record here comes from the working directory (or the pointer) by default; a positional is
|
|
196
|
+
// only ever the record when it is a SECOND positional that is itself an EVDA on disk, never the
|
|
197
|
+
// entry path itself.
|
|
198
|
+
function pickReleaseArgs(args) {
|
|
199
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
200
|
+
let explicit = null;
|
|
201
|
+
let entryParts = positional;
|
|
202
|
+
if (positional.length > 1) {
|
|
203
|
+
const last = positional[positional.length - 1];
|
|
204
|
+
try {
|
|
205
|
+
if (statSync(last).isDirectory() && existsSync(manifestPath(resolve(last)))) {
|
|
206
|
+
explicit = last;
|
|
207
|
+
entryParts = positional.slice(0, -1);
|
|
208
|
+
}
|
|
209
|
+
} catch { /* not a directory on disk -> it's part of the entry path */ }
|
|
210
|
+
}
|
|
211
|
+
const found = locate({ explicit });
|
|
212
|
+
return { dir: found.dir, entry: entryParts.join(" ") };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// `evda mcp token`: the owner's side of the remote server's authorization. Printed here, on the
|
|
216
|
+
// machine that holds the record, and typed into the authorization page: never in a URL or a log.
|
|
217
|
+
async function mcpToken(args) {
|
|
218
|
+
const { dir, flags } = pickDir(args);
|
|
219
|
+
const auth = await import("../lib/mcp-auth.mjs");
|
|
220
|
+
const store = auth.openStore(auth.authDir(openEvda(dir)));
|
|
221
|
+
if (flags.includes("--new")) {
|
|
222
|
+
const ttl = parseDuration(flagValue(args, "ttl") ?? "24h");
|
|
223
|
+
const { code, expires } = auth.newOwnerCode(store, { ttlMs: ttl });
|
|
224
|
+
console.log(code);
|
|
225
|
+
console.error(`One owner code, good once, until ${new Date(expires).toISOString()}. Type it into the page Claude opens when you connect.`);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
if (flags.includes("--revoke-all")) {
|
|
229
|
+
const n = auth.revokeAll(store);
|
|
230
|
+
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.`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
if (flags.includes("--status")) {
|
|
234
|
+
const s = auth.status(store);
|
|
235
|
+
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}`);
|
|
236
|
+
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(", ")}`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
throw new EvdaError("REFUSED: evda mcp token needs --new [--ttl 24h], --revoke-all or --status.", { code: "USAGE", exit: 2 });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseDuration(s) {
|
|
243
|
+
const m = /^(\d+)(m|h|d)$/.exec(String(s));
|
|
244
|
+
if (!m) throw new EvdaError(`REFUSED: --ttl is a number and m, h or d (got ${JSON.stringify(s)}).`, { code: "BAD_TTL", exit: 2 });
|
|
245
|
+
const ms = Number(m[1]) * { m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
|
|
246
|
+
if (ms < 60_000 || ms > 7 * 86_400_000) throw new EvdaError("REFUSED: --ttl is between 1m and 7d.", { code: "BAD_TTL", exit: 2 });
|
|
247
|
+
return ms;
|
|
248
|
+
}
|
|
249
|
+
|
|
116
250
|
async function main(argv) {
|
|
117
251
|
const [cmd, ...args] = argv;
|
|
118
252
|
switch (cmd) {
|
|
@@ -134,8 +268,19 @@ async function main(argv) {
|
|
|
134
268
|
}
|
|
135
269
|
|
|
136
270
|
case "mcp": {
|
|
271
|
+
if (args[0] === "token") return mcpToken(args.slice(1));
|
|
272
|
+
const { dir, flags } = pickDir(args);
|
|
273
|
+
if (flags.includes("--http")) {
|
|
274
|
+
// The record over streamable HTTP with OAuth in front (HQ-1378), for a tunnel to publish.
|
|
275
|
+
const port = Number(flagValue(args, "port") ?? 8378);
|
|
276
|
+
const publicUrl = flagValue(args, "public-url");
|
|
277
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new EvdaError("REFUSED: --port must be a port number.", { code: "BAD_PORT", exit: 2 });
|
|
278
|
+
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 });
|
|
279
|
+
const { serveHttp } = await import("../lib/mcp-http.mjs");
|
|
280
|
+
await serveHttp({ dir, version: packageVersion(), port, host: flagValue(args, "host") ?? "127.0.0.1", publicUrl, logFile: flagValue(args, "log"), lifeline: flagValue(args, "lifeline") });
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
137
283
|
// The record as an MCP server on stdio. stdout is the protocol; everything else is stderr.
|
|
138
|
-
const { dir } = pickDir(args);
|
|
139
284
|
const { serve } = await import("../lib/mcp.mjs");
|
|
140
285
|
await serve({ dir, version: packageVersion() });
|
|
141
286
|
return 0;
|
|
@@ -149,6 +294,68 @@ async function main(argv) {
|
|
|
149
294
|
return r.exit;
|
|
150
295
|
}
|
|
151
296
|
|
|
297
|
+
case "gather": {
|
|
298
|
+
const g = await import("../lib/gather.mjs");
|
|
299
|
+
const [sub, ...rest] = args;
|
|
300
|
+
const values = new Set(["out", "model", "into", "manifest", "owner"].map((n) => rest.indexOf(`--${n}`)).filter((i) => i >= 0).map((i) => i + 1));
|
|
301
|
+
const positional = rest.filter((a, i) => !a.startsWith("--") && !values.has(i));
|
|
302
|
+
if (sub === "scan") {
|
|
303
|
+
if (!positional.length) throw new EvdaError("usage: evda gather scan <source>... [--into <record>] [--out <file>] [--with-ai] [--model M]", { exit: 2 });
|
|
304
|
+
let p = g.gather(positional, { into: flagValue(rest, "into") ?? null });
|
|
305
|
+
let exit = 0;
|
|
306
|
+
if (rest.includes("--with-ai")) {
|
|
307
|
+
const r = g.withOwnersAi(p, { model: flagValue(rest, "model") ?? null });
|
|
308
|
+
p = r.p;
|
|
309
|
+
if (!r.ok) exit = 1;
|
|
310
|
+
}
|
|
311
|
+
const out = g.saveProposal(p, flagValue(rest, "out") ?? join(g.gatherDir(), `${p.id}.json`));
|
|
312
|
+
console.log(g.summaryText(p));
|
|
313
|
+
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`}`);
|
|
314
|
+
console.log(`\nProposal written to ${out}`);
|
|
315
|
+
for (const l of g.checkedLines(p)) console.log(l);
|
|
316
|
+
return p.counts.items === 0 && !p.counts.chats ? 1 : exit;
|
|
317
|
+
}
|
|
318
|
+
if (sub === "questions" || sub === "show") {
|
|
319
|
+
if (!positional[0]) throw new EvdaError(`usage: evda gather ${sub} <proposal>`, { exit: 2 });
|
|
320
|
+
const { p, abs } = g.loadProposal(positional[0]);
|
|
321
|
+
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; }
|
|
322
|
+
const open = p.questions.filter((q) => q.answer === undefined);
|
|
323
|
+
for (const q of open) console.log(`[${q.id}] ${q.text} (${q.options.join(" / ")}) moves ${q.affects.length} item(s)`);
|
|
324
|
+
for (const q of p.questions.filter((x) => x.answer !== undefined)) console.log(`answered [${q.id}] ${q.answer}`);
|
|
325
|
+
console.log(open.length ? `CHECKED ${p.questions.length} question(s) ${open.length} open` : `SKIPPED questions: none open on ${abs}`);
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
if (sub === "answer") {
|
|
329
|
+
const [ref, question, value] = positional;
|
|
330
|
+
if (!value) throw new EvdaError("usage: evda gather answer <proposal> <question> <value>", { exit: 2 });
|
|
331
|
+
const { p, abs } = g.loadProposal(ref);
|
|
332
|
+
const r = g.answer(p, question, value);
|
|
333
|
+
g.saveProposal(r.proposal, abs);
|
|
334
|
+
console.log(`Recorded ${question} = ${value} on ${abs}`);
|
|
335
|
+
for (const l of g.answerLines(r)) console.log(l);
|
|
336
|
+
console.log(`CHECKED ${r.proposal.items.length} items ${r.moved.length} re-placed`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
if (sub === "apply") {
|
|
340
|
+
const into = flagValue(rest, "into");
|
|
341
|
+
if (!positional[0] || !into) throw new EvdaError("usage: evda gather apply <proposal> --into <record> [--copy|--move] [--dry] [--manifest <file>] [--owner NAME]", { exit: 2 });
|
|
342
|
+
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 });
|
|
343
|
+
const a = await import("../lib/gather-apply.mjs");
|
|
344
|
+
const { p, abs } = g.loadProposal(positional[0]);
|
|
345
|
+
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 });
|
|
346
|
+
for (const l of r.lines) console.log(l);
|
|
347
|
+
return r.dry ? 0 : r.doctor.exit === 0 ? 0 : 1;
|
|
348
|
+
}
|
|
349
|
+
if (sub === "revert") {
|
|
350
|
+
if (!positional[0]) throw new EvdaError("usage: evda gather revert <manifest> [--force]", { exit: 2 });
|
|
351
|
+
const a = await import("../lib/gather-apply.mjs");
|
|
352
|
+
const r = a.revertGather(positional[0], { force: rest.includes("--force") });
|
|
353
|
+
for (const l of a.revertLines(r)) console.log(l);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
throw new EvdaError("usage: evda gather scan <source>... | questions <proposal> | answer <proposal> <question> <value> | show <proposal> | apply <proposal> --into <record> | revert <manifest>", { exit: 2 });
|
|
357
|
+
}
|
|
358
|
+
|
|
152
359
|
case "look-back": {
|
|
153
360
|
const { dir } = pickDir(args);
|
|
154
361
|
const { lookBack } = await import("../lib/organize.mjs");
|
|
@@ -167,6 +374,40 @@ async function main(argv) {
|
|
|
167
374
|
return 0;
|
|
168
375
|
}
|
|
169
376
|
|
|
377
|
+
case "hook": {
|
|
378
|
+
// evda hook install [dir] | status [dir] the keep-record Stop hook for Claude Code (HQ-1376)
|
|
379
|
+
const [sub, ...rest] = args;
|
|
380
|
+
if (!["install", "status"].includes(sub)) throw new EvdaError("usage: evda hook install [dir] | status [dir]", { exit: 2 });
|
|
381
|
+
const { dir } = pickDir(rest);
|
|
382
|
+
// Every hook create-evda installs for a Claude owner (HQ-1377): the keep-record Stop hook and
|
|
383
|
+
// the read guard, both in the record's own settings and nowhere else.
|
|
384
|
+
const hooks = await import("../lib/hooks.mjs");
|
|
385
|
+
const { installGuard, guardStatus } = await import("../lib/connect.mjs");
|
|
386
|
+
if (sub === "install") {
|
|
387
|
+
const r = hooks.installHook(dir);
|
|
388
|
+
console.log(`${r.action}: ${r.settings} runs ${r.hook} in this record's Claude Code sessions, and nowhere else`);
|
|
389
|
+
const g = installGuard(dir);
|
|
390
|
+
console.log(`read guard ${g.action} ${g.path}`);
|
|
391
|
+
// The prompt hook (HQ-1385) files every message, so only the owner's "everything" installs it.
|
|
392
|
+
const { filingOf } = await import("../lib/day-file.mjs");
|
|
393
|
+
const { readManifest } = await import("../lib/manifest.mjs");
|
|
394
|
+
const filing = filingOf(readManifest(dir));
|
|
395
|
+
if (filing === "everything") {
|
|
396
|
+
const p = hooks.installPromptHook(dir);
|
|
397
|
+
console.log(`prompt hook ${p.action}: ${p.settings} runs ${p.hook}, which files each message the owner sends`);
|
|
398
|
+
} else console.log(`prompt hook not installed: evda.json says ${filing ? `"filing": "${filing}"` : "nothing about filing yet"}, and it is installed only for "everything"`);
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
const st = hooks.hookStatus(dir);
|
|
402
|
+
if (st.problem) { console.log(st.problem); return 1; }
|
|
403
|
+
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`");
|
|
404
|
+
const ph = hooks.promptHookStatus(dir);
|
|
405
|
+
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)");
|
|
406
|
+
const g = guardStatus(dir);
|
|
407
|
+
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`");
|
|
408
|
+
return st.registered && st.file && st.current && g.registered && g.file && (!ph.registered || (ph.file && ph.current)) ? 0 : 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
170
411
|
case "connect": {
|
|
171
412
|
// How to add this record's MCP server to each client, with the record's path filled in.
|
|
172
413
|
const { dir } = pickDir(args);
|
|
@@ -176,7 +417,7 @@ async function main(argv) {
|
|
|
176
417
|
}
|
|
177
418
|
|
|
178
419
|
case "routine": {
|
|
179
|
-
// evda routine <morning|weekly> [dir] [--launchd] [--install]
|
|
420
|
+
// evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
|
|
180
421
|
const { routineMain } = await import("../lib/routines.mjs");
|
|
181
422
|
return routineMain(args, { pickDir, flagValue });
|
|
182
423
|
}
|
|
@@ -198,6 +439,7 @@ async function main(argv) {
|
|
|
198
439
|
console.log(`${p.title} (${p.id}): ${p.for}`);
|
|
199
440
|
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
441
|
for (const c of p.circles ?? []) console.log(` circle ${c.name} (${c.public ? "public" : "private"}, proposed only): ${c.description}`);
|
|
442
|
+
for (const [k, v] of Object.entries(p.mapping ?? {})) console.log(` role ${k.padEnd(24)} ${v}/`);
|
|
201
443
|
for (const r of p.routines ?? []) console.log(` routine ${r} (offered, never scheduled without a yes)`);
|
|
202
444
|
return 0;
|
|
203
445
|
}
|
|
@@ -214,13 +456,28 @@ async function main(argv) {
|
|
|
214
456
|
const plan = presets.applyPreset(dir, id, { dry });
|
|
215
457
|
console.log(`evda preset apply ${id}${dry ? " --dry" : ""} ${plan.root}`);
|
|
216
458
|
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;
|
|
459
|
+
const wrote = plan.create.length + plan.write.length + plan.views.length + plan.proposed.length + plan.roles.length;
|
|
218
460
|
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
461
|
if (!dry && wrote === 0 && !plan.conflicts.length) console.log("Nothing was written: the record already has this preset.");
|
|
220
462
|
if (dry) console.log("Dry run: nothing was written.");
|
|
221
463
|
return plan.conflicts.length ? 1 : 0;
|
|
222
464
|
}
|
|
223
465
|
|
|
466
|
+
case "work": {
|
|
467
|
+
// evda work [dir] [--env-file <file>] [--json]: the record's efforts, from THE HARLEQUIN or a
|
|
468
|
+
// folder of effort files (HQ-1371). Read only.
|
|
469
|
+
const { readWork } = await import("../lib/work.mjs");
|
|
470
|
+
const { dir } = pickDir(args);
|
|
471
|
+
const envFile = flagValue(args, "env-file");
|
|
472
|
+
const r = await readWork(openEvda(dir), { envFile: envFile ? resolve(envFile) : null });
|
|
473
|
+
if (args.includes("--json")) console.log(JSON.stringify({ efforts: r.efforts, lines: r.lines }, null, 2));
|
|
474
|
+
else {
|
|
475
|
+
for (const e of r.efforts) console.log(` ${e.id.padEnd(12)} ${String(e.status).padEnd(16)} ${(e.planned_for ?? "").padEnd(10)} ${e.title}`);
|
|
476
|
+
for (const l of r.lines) console.log(l);
|
|
477
|
+
}
|
|
478
|
+
return r.exit;
|
|
479
|
+
}
|
|
480
|
+
|
|
224
481
|
case "where": {
|
|
225
482
|
const found = locate({});
|
|
226
483
|
console.log(`${found.dir} (via ${found.via})`);
|
|
@@ -240,6 +497,88 @@ async function main(argv) {
|
|
|
240
497
|
return doctor(dir, { via }).exit;
|
|
241
498
|
}
|
|
242
499
|
|
|
500
|
+
case "guard":
|
|
501
|
+
return runHook();
|
|
502
|
+
|
|
503
|
+
case "note": {
|
|
504
|
+
// The words may hold a slash ("50/50"), so the record is never guessed from them: --in <dir>,
|
|
505
|
+
// or the locator.
|
|
506
|
+
const i = args.indexOf("--in");
|
|
507
|
+
const dir = locate({ explicit: i >= 0 ? args[i + 1] : undefined }).dir;
|
|
508
|
+
let words = args.filter((_, j) => i < 0 || (j !== i && j !== i + 1)).join(" ");
|
|
509
|
+
if (words === "-" || words === "") words = readFileSync(0, "utf8");
|
|
510
|
+
if (!words.trim()) throw new EvdaError('REFUSED: note needs the words, as an argument or on stdin. Nothing was written.', { exit: 2 });
|
|
511
|
+
refuseSecrets(words, "evda note");
|
|
512
|
+
const r = note(openEvda(dir), words);
|
|
513
|
+
console.log(`Noted at ${r.path}. No AI reads it until you say it can: evda release ${JSON.stringify(r.path)}`);
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
case "message": {
|
|
518
|
+
// Like note, the body may hold a slash, so the record is --in <dir> or the locator.
|
|
519
|
+
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; };
|
|
520
|
+
const dirArg = take("--in");
|
|
521
|
+
const title = take("--title");
|
|
522
|
+
const from = take("--from");
|
|
523
|
+
const dir = locate({ explicit: dirArg }).dir;
|
|
524
|
+
let body = args.join(" ");
|
|
525
|
+
if (body === "-" || body === "") body = readFileSync(0, "utf8");
|
|
526
|
+
const r = messageTool(openEvda(dir), { title, body, ...(from === undefined ? {} : { from }) });
|
|
527
|
+
console.log(`Sent the message ${r.id}, filed at ${r.path}.`);
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
case "replies": {
|
|
532
|
+
const i = args.indexOf("--in");
|
|
533
|
+
const dir = locate({ explicit: i >= 0 ? args[i + 1] : undefined }).dir;
|
|
534
|
+
const peek = args.includes("--peek");
|
|
535
|
+
const r = repliesTool(openEvda(dir), peek ? { peek: true } : {});
|
|
536
|
+
if (args.includes("--json")) { console.log(JSON.stringify({ replies: r.replies, marked: r.marked }, null, 2)); return 0; }
|
|
537
|
+
for (const x of r.replies) console.log(`## ${x.title} (${x.id}, ${x.date})\n\n${x.reply}\n`);
|
|
538
|
+
if (r.heldBack) console.log(r.heldBack);
|
|
539
|
+
console.log(`CHECKED ${r.dir}/ ${r.replies.length} new repl${r.replies.length === 1 ? "y" : "ies"}, ${r.marked} marked seen${peek ? " (peek)" : ""}`);
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case "release": {
|
|
544
|
+
const { dir, entry } = pickReleaseArgs(args);
|
|
545
|
+
const r = releaseNote(openEvda(dir), entry);
|
|
546
|
+
console.log(`Released ${r.path}. It reads like any other entry now.`);
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case "search": {
|
|
551
|
+
const { dir, rest } = pickDir(args);
|
|
552
|
+
const query = rest.join(" ").trim();
|
|
553
|
+
if (!query) throw new EvdaError("search needs words to find", { exit: 2 });
|
|
554
|
+
const evda = openEvda(dir);
|
|
555
|
+
const files = markdownFiles(evda.dir);
|
|
556
|
+
const hits = ownerSearch(evda, query, { files });
|
|
557
|
+
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}`);
|
|
558
|
+
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`);
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
case "readers": {
|
|
563
|
+
const { dir } = pickDir(args);
|
|
564
|
+
const evda = openEvda(dir);
|
|
565
|
+
const ctx = readContext(evda);
|
|
566
|
+
const rec = parseReaders(evda.manifest[READ_BY], "evda.json read_by");
|
|
567
|
+
console.log(`the record: Read by: ${describeReaders(rec.problems.length ? null : rec.readers)}${ctx.circles.length ? " (every part without its own)" : ""}`);
|
|
568
|
+
for (const c of ctx.circles) {
|
|
569
|
+
const any = Object.values(c.members).find((m) => m.kind === "path");
|
|
570
|
+
const probe = any ? (any.isDir ? join(any.abs, ".probe.md") : any.abs) : null;
|
|
571
|
+
const r = probe ? readersFor(ctx, probe) : { readers: null, isPublic: c.manifest?.public === true, source: null };
|
|
572
|
+
console.log(`${c.name}: Read by: ${describeReaders(r.readers, { isPublic: r.isPublic })}${r.source === "evda.json" ? " (from evda.json)" : ""}`);
|
|
573
|
+
}
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case "local-model": {
|
|
578
|
+
console.log(readFileSync(fileURLToPath(new URL("../docs/local-model.md", import.meta.url)), "utf8"));
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
243
582
|
case "reindex": {
|
|
244
583
|
const { dir, flags } = pickDir(args);
|
|
245
584
|
if (flags.includes("--adopt")) {
|
|
@@ -314,13 +653,36 @@ async function main(argv) {
|
|
|
314
653
|
const parts = splitFence(text);
|
|
315
654
|
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
655
|
const shipped = shippedGenerated();
|
|
317
|
-
if (parts.generated === shipped) {
|
|
656
|
+
if (parts.generated === shipped) {
|
|
657
|
+
stampInstalledVersion(dir, packageVersion());
|
|
658
|
+
console.log("upgrade: the generated block already matches what this tool ships. Nothing outside it was read.");
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
318
661
|
writeFileSync(a.abs, replaceFence(text, shipped));
|
|
319
662
|
reindex(evda);
|
|
663
|
+
stampInstalledVersion(dir, packageVersion());
|
|
320
664
|
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
665
|
return 0;
|
|
322
666
|
}
|
|
323
667
|
|
|
668
|
+
case "check-update": {
|
|
669
|
+
const { dir } = pickDir(args);
|
|
670
|
+
const result = await checkForUpdate(dir, { force: args.includes("--force") });
|
|
671
|
+
console.log(summarize(result));
|
|
672
|
+
return result.status === "CHECK_FAILED" ? 1 : 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
case "update": {
|
|
676
|
+
const { dir } = pickDir(args);
|
|
677
|
+
const result = await checkForUpdate(dir, { force: true });
|
|
678
|
+
console.log(summarize(result));
|
|
679
|
+
if (result.status === "CHECK_FAILED") return 1;
|
|
680
|
+
if (result.status !== "UPDATE_AVAILABLE" && result.status !== "UNSTAMPED") return 0;
|
|
681
|
+
const upgradeRc = await main(["upgrade", dir]);
|
|
682
|
+
if (upgradeRc !== 0) return upgradeRc;
|
|
683
|
+
return main(["migrate", dir]);
|
|
684
|
+
}
|
|
685
|
+
|
|
324
686
|
case "digest": {
|
|
325
687
|
const { dir } = pickDir(args);
|
|
326
688
|
const evda = openEvda(dir);
|
|
@@ -455,6 +817,62 @@ async function main(argv) {
|
|
|
455
817
|
return 0;
|
|
456
818
|
}
|
|
457
819
|
|
|
820
|
+
case "publish": {
|
|
821
|
+
const { dir, rest } = pickDir(args);
|
|
822
|
+
return publishCommand(dir, rest[0], { out: flagValue(args, "out"), key: flagValue(args, "key") });
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
case "circles": {
|
|
826
|
+
const sub = args[0];
|
|
827
|
+
const { dir } = pickDir(args.slice(1));
|
|
828
|
+
const planFile = flagValue(args, "plan");
|
|
829
|
+
const plan = planFile ? JSON.parse(readFileSync(resolve(planFile), "utf8")) : null;
|
|
830
|
+
if (sub === "plan" || sub === "declare") {
|
|
831
|
+
if (!plan) throw new EvdaError(`circles ${sub} needs --plan <file>`, { exit: 2 });
|
|
832
|
+
if (sub === "plan") {
|
|
833
|
+
const { circles, missing } = planCircles(dir, plan);
|
|
834
|
+
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")}`);
|
|
835
|
+
for (const m of missing) console.log(`MISSING root ${m}: nothing is there yet; declared so the path has a circle when it appears`);
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
const { written } = declare(dir, plan, { recordId: flagValue(args, "record-id") });
|
|
839
|
+
console.log(`wrote ${written.length} files: ${written.map((p) => relative(dir, p)).join(", ")}`);
|
|
840
|
+
return 0;
|
|
841
|
+
}
|
|
842
|
+
if (sub === "check") {
|
|
843
|
+
const evda = openEvda(dir);
|
|
844
|
+
// Without --plan, the plan kept beside the circles (`<circles role>/plan.json`) says what is
|
|
845
|
+
// excluded, so this check and doctor exclude what the scan excludes.
|
|
846
|
+
const beside = plan ? null : planBeside(evda);
|
|
847
|
+
const excludes = plan?.exclude ?? beside?.plan?.exclude ?? [];
|
|
848
|
+
// A bounded record's circles stop at its sub-records (HQ-1371); each is checked as its own.
|
|
849
|
+
const { boundaries } = await import("../lib/records.mjs");
|
|
850
|
+
const stops = boundaries(evda);
|
|
851
|
+
if (stops.length) console.log(`STOPS at ${stops.length} sub-record(s): ${stops.join(", ")}`);
|
|
852
|
+
const r = coverage(dir, circleIndex(evda), { exclude: excluder([...excludes, ...stops]), mountRoot: mountRoot(evda) });
|
|
853
|
+
for (const p of r.problems) console.log(`FAIL ${p.code} ${p.rel}: ${p.why}`);
|
|
854
|
+
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"})`);
|
|
855
|
+
if (r.checked === 0) { console.log("FAIL CHECKED 0: nothing was examined"); return 1; }
|
|
856
|
+
return r.problems.length ? 1 : 0;
|
|
857
|
+
}
|
|
858
|
+
throw new EvdaError("circles needs plan, declare or check", { exit: 2 });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
case "parity": {
|
|
862
|
+
const { dir } = pickDir(args);
|
|
863
|
+
const planFile = flagValue(args, "plan");
|
|
864
|
+
const device = flagValue(args, "device");
|
|
865
|
+
const shadow = flagValue(args, "shadow");
|
|
866
|
+
if (!planFile || !device || !shadow) throw new EvdaError("parity needs --plan <file> --device <name> --shadow <dir>", { exit: 2 });
|
|
867
|
+
const plan = JSON.parse(readFileSync(resolve(planFile), "utf8"));
|
|
868
|
+
const evda = openEvda(dir);
|
|
869
|
+
const { holds, never, drift } = deviceCircles(plan, device, readGrants(evda) === null ? null : grantView(evda));
|
|
870
|
+
for (const d of drift) console.log(`NOTE plan drift ${d}; the grant files decide`);
|
|
871
|
+
const r = parity({ source: dir, shadow: resolve(shadow), index: circleIndex(evda), holds, never, exclude: excluder(plan.exclude) });
|
|
872
|
+
for (const line of formatParity(r)) console.log(line);
|
|
873
|
+
return r.ok ? 0 : 1;
|
|
874
|
+
}
|
|
875
|
+
|
|
458
876
|
case "status": {
|
|
459
877
|
const { dir, via } = pickDir(args);
|
|
460
878
|
const evda = openEvda(dir);
|