create-evda 0.1.7 → 0.1.9
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 +45 -0
- package/README.md +11 -2
- package/bin/evda.mjs +217 -6
- package/create-evda.mjs +22 -6
- package/dist/pairing.mjs +5494 -0
- package/hooks/evda-day-writer-guard.mjs +7 -0
- package/lib/acknowledge.mjs +187 -0
- package/lib/browse.mjs +354 -0
- package/lib/circle-plan.mjs +24 -1
- package/lib/circles.mjs +5 -1
- package/lib/clients.mjs +18 -2
- package/lib/connect.mjs +8 -1
- package/lib/day-file.mjs +58 -9
- package/lib/day-writer.mjs +103 -0
- package/lib/doctor.mjs +13 -0
- package/lib/every-chat.mjs +132 -0
- package/lib/filechanges.mjs +203 -68
- package/lib/gather-apply.mjs +44 -3
- package/lib/gather.mjs +3 -1
- package/lib/keychain.mjs +144 -0
- package/lib/manifest.mjs +3 -1
- package/lib/mcp-auth.mjs +41 -1
- package/lib/mcp-http.mjs +24 -4
- package/lib/mcp.mjs +931 -67
- package/lib/measurement-log.mjs +246 -0
- package/lib/messages.mjs +5 -2
- package/lib/pairing.mjs +403 -0
- package/lib/parity.mjs +47 -13
- package/lib/qr.mjs +317 -0
- package/lib/record.mjs +20 -0
- package/lib/routines.mjs +42 -3
- package/lib/setup.mjs +41 -0
- 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/tools.mjs +423 -24
- package/lib/watch.mjs +160 -0
- package/package.json +4 -2
- package/skills/evda/SKILL.md +15 -2
- package/templates/AGENTS.md +25 -4
- package/templates/evda-digest.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ Plain-English notes on how Iris behaves differently after each version. Not a co
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
When two of your machines share one record through iCloud, one of them can be the only one that
|
|
8
|
+
writes the daily note. Set EVDA_DAY_WRITER on every other machine and add hooks/evda-day-writer-guard.mjs
|
|
9
|
+
to that machine's own Claude Code settings: an agent there that tries to write the note directly is
|
|
10
|
+
stopped and told to file through evda_write on the writing machine, because two machines writing the
|
|
11
|
+
same note within seconds lose one of the writes with no conflict copy.
|
|
12
|
+
|
|
13
|
+
The first time you talk to your EVÐA from a chat app (Claude, ChatGPT, or any other), Iris gives
|
|
14
|
+
you one line to add to that app's every-chat instructions, and says where that setting is. With it
|
|
15
|
+
there, every chat starts as Iris, even one that never opens your record. Tell Iris it is set, or
|
|
16
|
+
that you don't want it, and that app never asks again; a second app asks once for itself. If you
|
|
17
|
+
file only when you ask, the line loads Iris and asks for nothing more. `create-evda` and `evda
|
|
18
|
+
connect` print the same line.
|
|
19
|
+
|
|
20
|
+
The EVÐA app is one sidebar and the page you are reading. Today's Journal is first, there even
|
|
21
|
+
before you have written today, and what you type at the end of it is added to today's note,
|
|
22
|
+
verbatim, the way your AI's evda_write does. Below it, Journal, Captures, People and your project,
|
|
23
|
+
area and knowledge folders are pinned from your mapping, then Instructions (CLAUDE.md, AGENTS.md,
|
|
24
|
+
evda.json and any file the agents block is written into), then every folder of the record. Devices,
|
|
25
|
+
Connect your AI and Switch EVÐA sit quietly at the foot. A file's circle shows only when it is not
|
|
26
|
+
your record's usual one, and markdown reads formatted. Entries you noted or sealed, and parts you
|
|
27
|
+
set so no AI reads, stay closed. Tooling (dot-files, lockfiles, scripts, licenses, backups, empty
|
|
28
|
+
files) is left out, with one line per folder counting it. The window drags by its top edge. Large
|
|
29
|
+
files open whole: `evda browse` and `evda capture` wait for their answer to reach the app before
|
|
30
|
+
exiting, where before a file past 64 KB arrived cut off.
|
|
31
|
+
|
|
32
|
+
Your phone can join your EVÐA. `evda pair` on the computer your record is on shows a one-time code
|
|
33
|
+
and a QR link; it works once, for 10 minutes, and the phone gets only the parts you name (the
|
|
34
|
+
journal by default). `evda sync` adds the phone once it has used the code, takes in what it wrote,
|
|
35
|
+
and sends what changed; a note written on the phone lands under the day, in your words. `evda
|
|
36
|
+
devices` lists what is paired and `evda remove <device>` removes one: it adds nothing more and
|
|
37
|
+
receives nothing new. You can also ask your AI to do all of it (`evda_pair`, `evda_devices`, `evda_sync_status`,
|
|
38
|
+
`evda_remove_device`): it shows a QR code to scan, adds the phone on its own once it scans, says
|
|
39
|
+
when each device was last heard from and what is still on its way to it, and removes a device only
|
|
40
|
+
after you say yes to what removing does. A phone paired this way pairs to the computer the AI's
|
|
41
|
+
EVÐA server runs on, and the answer names that computer. A second computer of yours (Hestia) speaks
|
|
42
|
+
as its own device and never as the laptop: until a grant makes it owner of a part it refuses to
|
|
43
|
+
pair, and once it owns one it pairs a phone to itself. Pairing goes
|
|
44
|
+
through a relay that cannot read what it carries, and needs Node 22.13 or newer; everything else
|
|
45
|
+
still runs on Node 20.
|
|
46
|
+
|
|
47
|
+
One sync of a record runs at a time. The Mac app syncing on its own, a launchd job and `evda sync`
|
|
48
|
+
at a terminal wait for each other; if a sync is still running after twenty seconds, the second says
|
|
49
|
+
"A sync of this EVÐA is already running. Try again in a moment." and changes nothing. A sync that
|
|
50
|
+
crashed leaves nothing in the way: the next one sees the process is gone and goes ahead.
|
|
51
|
+
|
|
7
52
|
Organizing is on from the start. Setup installs the morning organize on a Mac (a launchd job that
|
|
8
53
|
runs your own Claude each morning at 8:00, or when the computer wakes), says so in one line, and
|
|
9
54
|
offers off: answer no during setup, pass `--no-routines`, or later run `evda routine morning
|
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ apply places only what is new, in the record's own folders.
|
|
|
76
76
|
evda_operating_layer AGENTS.md, read first every session
|
|
77
77
|
evda_recent the last few days of the record
|
|
78
78
|
evda_write your words, verbatim, appended to today's daily file, with one line of context labeled as not yours. It takes no path and never edits
|
|
79
|
+
evda_record_correction one dated line appended to AGENTS.md's "What I have been taught", outside the installer's block. Never edits an earlier entry
|
|
79
80
|
evda_import what another AI said about you, filed under record/imports with its source and date
|
|
80
81
|
evda_search plain text search inside the record, on this machine
|
|
81
82
|
evda_circles your circles and their policies
|
|
@@ -91,15 +92,23 @@ apply places only what is new, in the record's own folders.
|
|
|
91
92
|
evda_write_skill writes or revises a skill at .claude/skills/<name>/SKILL.md. Never outside that folder, never a rule about your life
|
|
92
93
|
evda_create_subrecord an EVÐA inside this one, with the four roles and its own AGENTS.md, then doctor. Your evda.json changes only on bounded with confirmed: true
|
|
93
94
|
evda_doctor evda doctor, as a report
|
|
94
|
-
|
|
95
|
+
evda_pair a one-time code and a QR code to scan for your phone, paired to the computer this server runs on; the phone gets only the parts named, and is added on its own once it uses the code
|
|
96
|
+
evda_finish_pairing waits, at most two minutes, for the phone to use its code, and says when it is in
|
|
97
|
+
evda_sync_devices adds a phone that used its code, takes in what your devices wrote, sends what changed
|
|
98
|
+
evda_devices the devices paired with your EVÐA on this computer, what each reads, and when it was last heard from
|
|
99
|
+
evda_sync_status per device what is waiting, sent and confirmed, and whether a sync is running. Reads only
|
|
100
|
+
evda_remove_device removes a device; without confirm: true it only says what removing would do, for your yes
|
|
101
|
+
evda_check_update whether newer EVDA tooling exists
|
|
95
102
|
|
|
96
|
-
Every write appends, refuses anything shaped like a password, key or token, and says the path it wrote.
|
|
103
|
+
Every write appends, refuses anything shaped like a password, key or token, and says the path it wrote. Two kinds of call leave this machine. `evda_check_update` asks the npm registry for a version number; it carries nothing from the record and installs nothing, and the update itself is a command you run in a terminal. The device tools talk to the relay that carries your EVÐA between your own devices, and everything they send is sealed here first: the relay cannot read it. `evda connect` prints the setup for your record, with its path filled in:
|
|
97
104
|
|
|
98
105
|
```bash
|
|
99
106
|
claude mcp add --scope user evda -- /path/to/node /path/to/evda.mjs mcp ~/my-evda # Claude Code
|
|
100
107
|
codex mcp add evda -- /path/to/node /path/to/evda.mjs mcp ~/my-evda # Codex
|
|
101
108
|
```
|
|
102
109
|
|
|
110
|
+
**One layer, sized per app.** `evda_operating_layer` serves the core and AGENTS.md to every app, but a section that only makes sense with a shell (scripts to run, hosts to ssh to, local MCP servers) is noise in a chat app. Put `<!-- evda:surface terminal -->` on the line right after a `## ` heading and that section, with its subsections, goes only to coding agents (Claude Code, Codex). Every other app, including one the server has never heard of, gets the rest, in as few parts as its tool result limit allows, and is told which sections were left out. A record with no marker reads the same everywhere. The server knows the app by `initialize`'s `clientInfo` name, and the remote server logs that name and version, nothing else of it.
|
|
111
|
+
|
|
103
112
|
For the Claude desktop app, add the same command under `mcpServers` in `claude_desktop_config.json` (Settings, then Developer, then Edit Config) and restart the app. `create-evda` prints all three at the end of setup.
|
|
104
113
|
|
|
105
114
|
**The skill.** `skills/evda/SKILL.md` (Agent Skills format) tells Claude Code to act as Iris through the tools: load the operating layer first, file verbatim, never quiz, circles only on your yes. `evda skill install` copies it to `~/.claude/skills/evda/`; setup offers it.
|
package/bin/evda.mjs
CHANGED
|
@@ -26,7 +26,7 @@ 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
28
|
import { checkForUpdate, summarize, stampInstalledVersion } from "../lib/update-check.mjs";
|
|
29
|
-
import { planCircles, coverage, declare, deviceCircles, planBeside } from "../lib/circle-plan.mjs";
|
|
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";
|
|
32
32
|
import { excluder } from "../lib/sync-rules.mjs";
|
|
@@ -34,7 +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
|
+
import * as pairing from "../lib/pairing.mjs";
|
|
39
|
+
import { watchRecord, watchRelay } from "../lib/watch.mjs";
|
|
38
40
|
|
|
39
41
|
const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
40
42
|
|
|
@@ -100,6 +102,10 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
100
102
|
run doctor. Writes a reversal manifest beside the proposal
|
|
101
103
|
evda gather revert <manifest> [--force]
|
|
102
104
|
undo an apply byte for byte; refused when a placed file was edited
|
|
105
|
+
evda secret <file> [dir] [--keychain <path>]
|
|
106
|
+
a credential note (in Credentials/, or --into a path of its own) with
|
|
107
|
+
every "in your Keychain" pointer read back and filled in, to this
|
|
108
|
+
terminal only (HQ-1916). No MCP tool reads a value this way
|
|
103
109
|
evda look-back [dir] [--model M]
|
|
104
110
|
the week, read back by the owner's claude -p, appended to a report
|
|
105
111
|
evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
|
|
@@ -112,6 +118,27 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
112
118
|
add the preset's folders and manifests. Only adds: an existing
|
|
113
119
|
manifest that differs is a CONFLICT and is left as it is
|
|
114
120
|
evda preset diff <id> [dir] what the preset has that this record does not
|
|
121
|
+
evda pair [dir] [--name phone] [--parts journal[,people]] [--json]
|
|
122
|
+
a one-time code (and a QR link) for a phone to join this EVÐA. It
|
|
123
|
+
works once, for 10 minutes, and gives the device only the parts named
|
|
124
|
+
(with no --parts, the part that holds the day folder)
|
|
125
|
+
evda pair --finish [dir] [--json]
|
|
126
|
+
add a device that used its code, without a full sync. It never waits:
|
|
127
|
+
while a sync runs it says so, and that sync adds the device itself
|
|
128
|
+
evda devices [dir] [--json] the devices paired with this EVÐA, and what each reads
|
|
129
|
+
evda watch [dir] one line (CHANGED <n>) per burst of saves in the record, and INCOMING
|
|
130
|
+
when a device sent something, for the Mac app's sync; runs until stopped
|
|
131
|
+
evda sync-status [dir] [--json]
|
|
132
|
+
what is syncing on this computer now, since when, and what is still owed
|
|
133
|
+
evda remove <device> [dir] remove a device: it adds nothing more and receives nothing new
|
|
134
|
+
evda sync [dir] add devices that used their code, take in what they sent, send what
|
|
135
|
+
changed. Pairing needs Node 22.13 or newer; --relay <url> for another relay
|
|
136
|
+
One sync of a record runs at a time (the Mac app, launchd, a terminal); a
|
|
137
|
+
second waits for it, then says so in one line if it is still running
|
|
138
|
+
--json on pair, devices, remove and sync prints data for an app, no ids
|
|
139
|
+
evda compact [dir] [--yes] give back the space this computer's sync state keeps for sealed copies
|
|
140
|
+
nothing needs: media (which travels by name only) and copies no device is
|
|
141
|
+
still owed. Shows what it would remove; --yes removes them
|
|
115
142
|
evda status [dir] one screen summary
|
|
116
143
|
evda publish public [dir] --out <dir> --key <seed file>
|
|
117
144
|
the public circle as a signed plaintext export; refuses (exit 2) on a
|
|
@@ -123,6 +150,10 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
123
150
|
circles and grants roles and their folders; moves and changes no file.
|
|
124
151
|
A circle rooted at "" is written as the root itself, and a circle
|
|
125
152
|
already declared keeps its id, release, rules, mount and policy
|
|
153
|
+
evda circles default [--dry-run] [--json] [dir]
|
|
154
|
+
split a record that has no parts into the default two: journal (the
|
|
155
|
+
day files and captures) and private (everything else). A record that
|
|
156
|
+
has parts is left as it is. --dry-run says what it would make
|
|
126
157
|
evda circles check [--plan <file>] [dir]
|
|
127
158
|
every syncable file in exactly one circle (CHECKED n files); fails on a
|
|
128
159
|
file no circle covers, and on a symlink, iCloud stub or oversize file.
|
|
@@ -141,8 +172,14 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
|
|
|
141
172
|
evda replies [--peek] [--in dir] [--json]
|
|
142
173
|
the owner's replies not seen yet, marked seen unless --peek; read
|
|
143
174
|
under read_by as the AI $EVDA_READER names (claude, local, ...)
|
|
175
|
+
evda acknowledge <path> [--section "## HH:MM"] [--note "<one line>"] [--by NAME] [--in dir] [--json]
|
|
176
|
+
you said a weighty entry back to the owner: one mark beside the
|
|
177
|
+
record, so no other reader says it again. Already marked prints who
|
|
178
|
+
did it and writes nothing. No path lists every mark
|
|
144
179
|
evda search <words> [dir] your own search, on this device, no model; just-noted and sealed entries included and marked
|
|
145
180
|
evda readers [dir] which AI reads each part of the record, from read_by
|
|
181
|
+
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)
|
|
182
|
+
evda capture [dir] < words append the words on stdin to today's note, verbatim, under a new timestamped section (the app)
|
|
146
183
|
evda local-model how to run a model on your own computer and point a circle at it
|
|
147
184
|
evda guard the Claude Code read guard (a PreToolUse hook; reads the tool call on stdin)
|
|
148
185
|
evda where which EVDA the locator resolves to, and by which rule
|
|
@@ -172,7 +209,7 @@ function isDirArg(a) {
|
|
|
172
209
|
}
|
|
173
210
|
|
|
174
211
|
// `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
|
|
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"];
|
|
212
|
+
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"];
|
|
176
213
|
function flagValue(args, name) {
|
|
177
214
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
178
215
|
if (eq) return eq.slice(name.length + 3);
|
|
@@ -259,7 +296,7 @@ async function main(argv) {
|
|
|
259
296
|
|
|
260
297
|
case "version":
|
|
261
298
|
case "--version":
|
|
262
|
-
console.log(`evda tool schema ${SCHEMA_VERSION}`);
|
|
299
|
+
console.log(`evda ${packageVersion()} (tool schema ${SCHEMA_VERSION})`);
|
|
263
300
|
return 0;
|
|
264
301
|
|
|
265
302
|
case "init": {
|
|
@@ -356,6 +393,25 @@ async function main(argv) {
|
|
|
356
393
|
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
394
|
}
|
|
358
395
|
|
|
396
|
+
// HQ-1916: a credential note's Keychain pointers, read back and printed to this terminal only.
|
|
397
|
+
// No MCP tool calls into this, on purpose -- the whole point of the Keychain move is that no AI
|
|
398
|
+
// reads the value; only the owner does, on their own Mac.
|
|
399
|
+
case "secret": {
|
|
400
|
+
const { dir, rest } = pickDir(args);
|
|
401
|
+
const file = rest[0];
|
|
402
|
+
if (!file) throw new EvdaError("usage: evda secret <file> [dir] [--keychain <path>]", { exit: 2 });
|
|
403
|
+
const g2 = await import("../lib/gather.mjs");
|
|
404
|
+
const k = await import("../lib/keychain.mjs");
|
|
405
|
+
const abs = file.includes("/") ? resolve(dir, file) : join(dir, g2.CREDENTIALS, file);
|
|
406
|
+
if (!existsSync(abs)) throw new EvdaError(`REFUSED: no file at ${abs}.`, { code: "NOT_FOUND", exit: 2 });
|
|
407
|
+
const text = readFileSync(abs, "utf8");
|
|
408
|
+
const r = k.resolvePointers(text, { keychainPath: flagValue(args, "keychain") ?? process.env.EVDA_KEYCHAIN_PATH ?? null });
|
|
409
|
+
console.log(r.text);
|
|
410
|
+
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)`);
|
|
411
|
+
else console.log(`\nCHECKED ${r.resolved} Keychain pointer(s) resolved, ${r.failed} could not be read`);
|
|
412
|
+
return r.failed ? 1 : 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
359
415
|
case "look-back": {
|
|
360
416
|
const { dir } = pickDir(args);
|
|
361
417
|
const { lookBack } = await import("../lib/organize.mjs");
|
|
@@ -492,6 +548,75 @@ async function main(argv) {
|
|
|
492
548
|
return 0;
|
|
493
549
|
}
|
|
494
550
|
|
|
551
|
+
case "pair": {
|
|
552
|
+
const { dir } = pickDir(args);
|
|
553
|
+
// --finish: add a device that used its code, without a full sync (HQ-2017).
|
|
554
|
+
if (args.includes("--finish")) {
|
|
555
|
+
if (args.includes("--json")) console.log(JSON.stringify(await pairing.finishData(dir, { relay: flagValue(args, "relay") })));
|
|
556
|
+
else for (const line of await pairing.finish(dir, { relay: flagValue(args, "relay") })) console.log(line);
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
const r = await pairing.pair(dir, { name: flagValue(args, "name"), parts: flagValue(args, "parts"), relay: flagValue(args, "relay") });
|
|
560
|
+
// --json is for an app (the EVÐA Mac app, HQ-1925): the code and its QR link, and nothing with an id.
|
|
561
|
+
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 }));
|
|
562
|
+
else for (const line of r.lines) console.log(line);
|
|
563
|
+
return 0;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "sync-status": {
|
|
567
|
+
// What is syncing on this computer now, for the Mac app (HQ-2041): who holds the state and
|
|
568
|
+
// since when, and how much is still owed. Never waits.
|
|
569
|
+
const { dir } = pickDir(args);
|
|
570
|
+
const r = await pairing.statusData(dir, { relay: flagValue(args, "relay") });
|
|
571
|
+
if (args.includes("--json")) console.log(JSON.stringify(r));
|
|
572
|
+
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.` : "");
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
case "devices":
|
|
577
|
+
case "sync": {
|
|
578
|
+
const { dir } = pickDir(args);
|
|
579
|
+
const relay = flagValue(args, "relay");
|
|
580
|
+
if (args.includes("--json")) {
|
|
581
|
+
console.log(JSON.stringify(cmd === "devices" ? await pairing.deviceData(dir, { relay }) : await pairing.syncData(dir, { relay })));
|
|
582
|
+
return 0;
|
|
583
|
+
}
|
|
584
|
+
const lines = await pairing[cmd](dir, { relay });
|
|
585
|
+
for (const line of lines) console.log(line);
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
case "watch": {
|
|
590
|
+
// For the Mac app (HQ-2068): one line a burst of saves in the record, so it syncs in seconds.
|
|
591
|
+
// Runs until it is stopped; the app runs the sync, this only says when.
|
|
592
|
+
const { dir } = pickDir(args);
|
|
593
|
+
watchRecord(dir, (paths) => console.log(`CHANGED ${paths.length}`));
|
|
594
|
+
// And the relay: a device's note says INCOMING within seconds, so the app syncs it in then.
|
|
595
|
+
const relay = flagValue(args, "relay");
|
|
596
|
+
if (!pairing.pairingUnsupported()) watchRelay(() => pairing.inboundMailboxes(dir, { relay }), () => console.log("INCOMING"));
|
|
597
|
+
console.log("WATCHING");
|
|
598
|
+
return await new Promise(() => undefined);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
case "compact": {
|
|
602
|
+
const { dir } = pickDir(args);
|
|
603
|
+
for (const line of await pairing.compact(dir, { yes: args.includes("--yes") })) console.log(line);
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
case "remove": {
|
|
608
|
+
// The device's name comes first and is never read as the record, even when a folder by
|
|
609
|
+
// that name sits in the working directory.
|
|
610
|
+
const values = new Set(VALUE_FLAGS.map((n) => args.indexOf(`--${n}`)).filter((i) => i >= 0).map((i) => i + 1));
|
|
611
|
+
const at = args.findIndex((a, i) => !a.startsWith("--") && !values.has(i));
|
|
612
|
+
const name = at >= 0 ? args[at] : flagValue(args, "name");
|
|
613
|
+
const { dir } = pickDir(at >= 0 ? args.filter((_, i) => i !== at) : args);
|
|
614
|
+
const lines = await pairing.remove(dir, name, { relay: flagValue(args, "relay") });
|
|
615
|
+
if (args.includes("--json")) console.log(JSON.stringify({ removed: String(name).trim(), lines }));
|
|
616
|
+
else for (const line of lines) console.log(line);
|
|
617
|
+
return 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
495
620
|
case "doctor": {
|
|
496
621
|
const { dir, via } = pickDir(args);
|
|
497
622
|
return doctor(dir, { via }).exit;
|
|
@@ -524,7 +649,7 @@ async function main(argv) {
|
|
|
524
649
|
let body = args.join(" ");
|
|
525
650
|
if (body === "-" || body === "") body = readFileSync(0, "utf8");
|
|
526
651
|
const r = messageTool(openEvda(dir), { title, body, ...(from === undefined ? {} : { from }) });
|
|
527
|
-
console.log(`
|
|
652
|
+
console.log(`Filed the message ${r.id} at ${r.path}. This does not confirm it reached the owner's phone.`);
|
|
528
653
|
return 0;
|
|
529
654
|
}
|
|
530
655
|
|
|
@@ -540,6 +665,33 @@ async function main(argv) {
|
|
|
540
665
|
return 0;
|
|
541
666
|
}
|
|
542
667
|
|
|
668
|
+
case "acknowledge": {
|
|
669
|
+
// HQ-2060. A note may hold a slash, so the record is --in <dir> or the locator, as with
|
|
670
|
+
// note and message. No path lists what every reader has already acknowledged.
|
|
671
|
+
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; };
|
|
672
|
+
const json = args.includes("--json") && (args.splice(args.indexOf("--json"), 1), true);
|
|
673
|
+
const dirArg = take("--in");
|
|
674
|
+
const section = take("--section");
|
|
675
|
+
const note = take("--note");
|
|
676
|
+
const by = take("--by");
|
|
677
|
+
const dir = locate({ explicit: dirArg }).dir;
|
|
678
|
+
const evda = openEvda(dir);
|
|
679
|
+
const path = args.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
680
|
+
if (!path) {
|
|
681
|
+
const r = acknowledgementsTool(evda);
|
|
682
|
+
if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
|
|
683
|
+
for (const m of r.marks) console.log(`- ${m.key} (${m.by ?? "unknown"}, ${m.at}): ${m.note}`);
|
|
684
|
+
console.log(`CHECKED ${r.count} acknowledged entr${r.count === 1 ? "y" : "ies"}`);
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
const r = acknowledgeTool(evda, { path, ...(section === undefined ? {} : { section }), ...(note === undefined ? {} : { note }), ...(by === undefined ? {} : { by }) });
|
|
688
|
+
if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
|
|
689
|
+
const what = r.section ? `${r.path} "${r.section}"` : r.path;
|
|
690
|
+
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.`);
|
|
691
|
+
else console.log(`Marked ${what} acknowledged, as ${r.by}. You reached it first, so every later reader sees this mark and stays quiet.`);
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
543
695
|
case "release": {
|
|
544
696
|
const { dir, entry } = pickReleaseArgs(args);
|
|
545
697
|
const r = releaseNote(openEvda(dir), entry);
|
|
@@ -559,6 +711,37 @@ async function main(argv) {
|
|
|
559
711
|
return 0;
|
|
560
712
|
}
|
|
561
713
|
|
|
714
|
+
case "browse": {
|
|
715
|
+
// The owner's app reads the record through this (HQ-2038): the pinned folders, one folder's
|
|
716
|
+
// children, or one file opened, each as one JSON line. The rules are lib/browse.mjs.
|
|
717
|
+
const { dir } = pickDir(args);
|
|
718
|
+
const evda = openEvda(dir);
|
|
719
|
+
const b = await import("../lib/browse.mjs");
|
|
720
|
+
const bctx = b.browseContext(evda);
|
|
721
|
+
const open = flagValue(args, "open");
|
|
722
|
+
const folder = flagValue(args, "folder");
|
|
723
|
+
const attach = flagValue(args, "attach");
|
|
724
|
+
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 });
|
|
725
|
+
// Written to the end before returning: on macOS a pipe write is asynchronous, and the exit
|
|
726
|
+
// that follows cut anything past the pipe's 64 KB buffer (his 71 KB CLAUDE.md, HQ-2038).
|
|
727
|
+
await new Promise((done) => process.stdout.write(`${JSON.stringify(answer)}\n`, done));
|
|
728
|
+
return 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
case "capture": {
|
|
732
|
+
// The app's capture box (HQ-2038): the words come on stdin, verbatim, and are appended to
|
|
733
|
+
// today's daily note under a new timestamped section, by the same write evda_write makes.
|
|
734
|
+
const { dir } = pickDir(args);
|
|
735
|
+
const evda = openEvda(dir);
|
|
736
|
+
const chunks = [];
|
|
737
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
738
|
+
const words = Buffer.concat(chunks).toString("utf8");
|
|
739
|
+
const { capture } = await import("../lib/browse.mjs");
|
|
740
|
+
const r = capture(evda, words);
|
|
741
|
+
await new Promise((done) => process.stdout.write(`${JSON.stringify(r)}\n`, done));
|
|
742
|
+
return 0;
|
|
743
|
+
}
|
|
744
|
+
|
|
562
745
|
case "readers": {
|
|
563
746
|
const { dir } = pickDir(args);
|
|
564
747
|
const evda = openEvda(dir);
|
|
@@ -839,6 +1022,16 @@ async function main(argv) {
|
|
|
839
1022
|
console.log(`wrote ${written.length} files: ${written.map((p) => relative(dir, p)).join(", ")}`);
|
|
840
1023
|
return 0;
|
|
841
1024
|
}
|
|
1025
|
+
if (sub === "default") {
|
|
1026
|
+
// The default parts (HQ-1925), for a record that has none: the app's one button, on the
|
|
1027
|
+
// owner's yes. --dry-run is the question and writes nothing.
|
|
1028
|
+
const r = defaultParts(dir, { dryRun: args.includes("--dry-run") });
|
|
1029
|
+
if (args.includes("--json")) { console.log(JSON.stringify(r)); return 0; }
|
|
1030
|
+
if (r.declared) console.log(`Split into parts: ${r.parts.join(", ")}. Nothing in your files moved.`);
|
|
1031
|
+
else if (r.needed) console.log(`Not split into parts yet. Saying yes makes: ${r.parts.join(", ")}. Nothing was written.`);
|
|
1032
|
+
else console.log(`This EVÐA is already split into parts: ${r.parts.join(", ")}. Nothing was written.`);
|
|
1033
|
+
return 0;
|
|
1034
|
+
}
|
|
842
1035
|
if (sub === "check") {
|
|
843
1036
|
const evda = openEvda(dir);
|
|
844
1037
|
// Without --plan, the plan kept beside the circles (`<circles role>/plan.json`) says what is
|
|
@@ -855,7 +1048,7 @@ async function main(argv) {
|
|
|
855
1048
|
if (r.checked === 0) { console.log("FAIL CHECKED 0: nothing was examined"); return 1; }
|
|
856
1049
|
return r.problems.length ? 1 : 0;
|
|
857
1050
|
}
|
|
858
|
-
throw new EvdaError("circles needs plan, declare or check", { exit: 2 });
|
|
1051
|
+
throw new EvdaError("circles needs plan, declare, default or check", { exit: 2 });
|
|
859
1052
|
}
|
|
860
1053
|
|
|
861
1054
|
case "parity": {
|
|
@@ -905,7 +1098,25 @@ async function main(argv) {
|
|
|
905
1098
|
|
|
906
1099
|
// Through realpath: an installed bin is a symlink and resolve() does not follow it (see the same
|
|
907
1100
|
// guard in create-evda.mjs, and test/bins.test.mjs).
|
|
1101
|
+
/**
|
|
1102
|
+
* A command the EVÐA app runs ends when the app does (HQ-2041). The app names itself in
|
|
1103
|
+
* EVDA_PARENT_PID; once this process's parent is no longer that pid (the app quit or crashed and
|
|
1104
|
+
* this process was handed to launchd), it stops. A lock it held is then stale and taken over.
|
|
1105
|
+
*/
|
|
1106
|
+
export function watchParent(env = process.env, { ppid = () => process.ppid, exit = (c) => process.exit(c), everyMs = 1000 } = {}) {
|
|
1107
|
+
const want = Number(env.EVDA_PARENT_PID);
|
|
1108
|
+
if (!Number.isInteger(want) || want <= 0) return null;
|
|
1109
|
+
const check = () => {
|
|
1110
|
+
if (ppid() !== want) exit(1);
|
|
1111
|
+
};
|
|
1112
|
+
check();
|
|
1113
|
+
const t = setInterval(check, everyMs);
|
|
1114
|
+
t.unref();
|
|
1115
|
+
return t;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
908
1118
|
if (process.argv[1] && isEntry(process.argv[1], import.meta.url)) {
|
|
1119
|
+
watchParent();
|
|
909
1120
|
main(process.argv.slice(2)).then(
|
|
910
1121
|
(code) => process.exit(code),
|
|
911
1122
|
(e) => {
|
package/create-evda.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT]
|
|
5
5
|
// [--platform claude|chatgpt|gemini|cursor|other] [--training-off] [--yes]
|
|
6
|
-
// [--filing everything|asked] [--no-prompt-hook]
|
|
6
|
+
// [--filing everything|asked] [--no-prompt-hook] [--no-parts]
|
|
7
7
|
// [--name NAME] [--layers record,ledger[,circles]] [--preset ID|none] [--force]
|
|
8
8
|
//
|
|
9
9
|
// On a terminal, any setup answer not given as a flag is asked, one question at a time, and setup
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Writes the manifest, the operating layer with the owner's answers in its owner section, the
|
|
15
15
|
// loaders for the tools that do not read AGENTS.md on their own, the corpus, the ledger, and
|
|
16
|
-
// .evda/.
|
|
16
|
+
// .evda/. Splits the record into its default parts (HQ-1925): journal, the day files and captures,
|
|
17
|
+
// and private, everything else, both held by this computer, so a phone can be added with no
|
|
18
|
+
// terminal step (--no-parts leaves it unsplit, for parts declared by hand). Then runs `evda doctor`
|
|
19
|
+
// on what it wrote. It creates no example notes: an empty record
|
|
17
20
|
// that fills with a real life is the product. The one file it writes under record/ is today's
|
|
18
21
|
// daily file holding a true event, the record's own creation, so the first session has something
|
|
19
22
|
// dated to read and the owner sees where their words go.
|
|
@@ -30,12 +33,14 @@ import { readPointer, setPointer, pointerPath } from "./lib/locate.mjs";
|
|
|
30
33
|
import { FOLDER_MANIFEST } from "./lib/folders.mjs";
|
|
31
34
|
import { EvdaError } from "./lib/errors.mjs";
|
|
32
35
|
import { clients, installSkill, installGuard } from "./lib/connect.mjs";
|
|
36
|
+
import { everyChatSetupLines } from "./lib/every-chat.mjs";
|
|
33
37
|
import { defaultRoutines, installAtSetup } from "./lib/routines.mjs";
|
|
34
38
|
import { installHook } from "./lib/hooks.mjs";
|
|
35
39
|
import { applyPreset, listPresets, planLines, presetIds, NONE } from "./lib/presets.mjs";
|
|
36
40
|
import { FILING_MODES } from "./lib/day-file.mjs";
|
|
37
41
|
import { installPromptHook } from "./lib/hooks.mjs";
|
|
38
|
-
import {
|
|
42
|
+
import { defaultParts } from "./lib/circle-plan.mjs";
|
|
43
|
+
import { PLATFORMS, PLATFORM_KEYS, PARTS_LINE, LOADERS, DESKTOP_FILESYSTEM, DICTATION, DEFAULT_AGENT_NAME, platformKey, fillOwnerSection, aiLine, interview, trainingSection, termsSection, readerFor, INTRO } from "./lib/setup.mjs";
|
|
39
44
|
import { describeReaders, parseReaders } from "./lib/readers.mjs";
|
|
40
45
|
|
|
41
46
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -65,13 +70,13 @@ const FOLDER_TYPES = {
|
|
|
65
70
|
messages: "message",
|
|
66
71
|
};
|
|
67
72
|
|
|
68
|
-
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] [--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] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]`;
|
|
69
74
|
|
|
70
75
|
export function parseArgs(argv) {
|
|
71
76
|
const opts = {
|
|
72
77
|
layers: ["record", "ledger"], force: false, name: null, owner: "", kind: "person", id: null, pointer: true, use: false,
|
|
73
78
|
yes: false, trainingOff: false, agentName: null, purpose: "", voice: "", platform: null, platformName: null,
|
|
74
|
-
skill: null, routines: null, preset: null, guard: true, filing: null, promptHook: true,
|
|
79
|
+
skill: null, routines: null, preset: null, guard: true, filing: null, promptHook: true, parts: true,
|
|
75
80
|
};
|
|
76
81
|
const args = argv.flatMap((a) => {
|
|
77
82
|
const eq = /^(--[a-z-]+)=([\s\S]*)$/.exec(a);
|
|
@@ -92,6 +97,7 @@ export function parseArgs(argv) {
|
|
|
92
97
|
else if (a === "--skill") opts.skill = true;
|
|
93
98
|
else if (a === "--no-guard") opts.guard = false;
|
|
94
99
|
else if (a === "--no-prompt-hook") opts.promptHook = false;
|
|
100
|
+
else if (a === "--no-parts") opts.parts = false;
|
|
95
101
|
else if (a === "--filing") {
|
|
96
102
|
opts.filing = value(++i, a).trim().toLowerCase();
|
|
97
103
|
if (!FILING_MODES.includes(opts.filing)) throw new EvdaError(`--filing takes ${FILING_MODES.join(" or ")}, got ${args[i]}`, { exit: 2 });
|
|
@@ -302,6 +308,12 @@ export async function main(argv, io = null) {
|
|
|
302
308
|
for (const l of planLines(plan)) log(l);
|
|
303
309
|
log("");
|
|
304
310
|
}
|
|
311
|
+
// The default parts, after the preset so its folders exist, and before doctor so doctor reads them.
|
|
312
|
+
if (opts.parts !== false) {
|
|
313
|
+
const parts = defaultParts(dir);
|
|
314
|
+
log(parts.parts.includes("journal") ? PARTS_LINE : `parts: ${parts.parts.join(", ")}`);
|
|
315
|
+
log("");
|
|
316
|
+
}
|
|
305
317
|
const report = doctor(dir, { out: log, via: "create-evda" });
|
|
306
318
|
if (report.exit !== 0) {
|
|
307
319
|
console.error("\ncreate-evda wrote the layout but doctor does not pass on it. That is a bug in create-evda; the output above says where.");
|
|
@@ -321,7 +333,11 @@ export async function main(argv, io = null) {
|
|
|
321
333
|
}
|
|
322
334
|
log("");
|
|
323
335
|
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:");
|
|
324
|
-
for (const c of clients(dir)) if (c.key !== "project") { log(` ${c.label}:`); for (const l of c.text.split("\n")) log(` ${l}`); }
|
|
336
|
+
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}`); }
|
|
337
|
+
// The every-chat line (HQ-2205), from the one source: a chat that never calls an evda tool is
|
|
338
|
+
// reached only by the app's own every-chat setting.
|
|
339
|
+
log("");
|
|
340
|
+
for (const l of everyChatSetupLines(manifest.filing ?? null)) log(l);
|
|
325
341
|
if (opts.skill) {
|
|
326
342
|
const r = installSkill({ env: io?.env ?? process.env });
|
|
327
343
|
log("");
|