create-evda 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +12 -5
  3. package/bin/evda.mjs +129 -6
  4. package/create-evda.mjs +14 -11
  5. package/lib/adopt.mjs +516 -0
  6. package/lib/agents.mjs +1 -1
  7. package/lib/attested-host.mjs +287 -0
  8. package/lib/attested-wrap.mjs +147 -0
  9. package/lib/checkout-freshness.mjs +88 -0
  10. package/lib/circles.mjs +4 -2
  11. package/lib/clients.mjs +11 -0
  12. package/lib/connect.mjs +1 -1
  13. package/lib/connecting-apps.mjs +56 -0
  14. package/lib/connector-tools.mjs +85 -0
  15. package/lib/corrections.mjs +34 -0
  16. package/lib/credentials-migrate.mjs +120 -0
  17. package/lib/day-file.mjs +121 -12
  18. package/lib/day-writer.mjs +39 -5
  19. package/lib/doctor.mjs +60 -5
  20. package/lib/every-chat.mjs +7 -11
  21. package/lib/folders.mjs +2 -1
  22. package/lib/gather-apply.mjs +133 -24
  23. package/lib/gather.mjs +120 -19
  24. package/lib/html-notes.mjs +124 -0
  25. package/lib/keep-hook.mjs +84 -12
  26. package/lib/keychain.mjs +38 -1
  27. package/lib/manifest.mjs +3 -1
  28. package/lib/mcp-http.mjs +19 -1
  29. package/lib/mcp.mjs +358 -56
  30. package/lib/measurement-log.mjs +3 -0
  31. package/lib/migrate-people.mjs +54 -20
  32. package/lib/person-folders.mjs +90 -0
  33. package/lib/plugin-hook.mjs +109 -0
  34. package/lib/plugin.mjs +208 -0
  35. package/lib/presets.mjs +28 -6
  36. package/lib/prompt-hook.mjs +115 -4
  37. package/lib/publish.mjs +55 -10
  38. package/lib/readme.mjs +145 -0
  39. package/lib/record-walk.mjs +9 -0
  40. package/lib/record.mjs +54 -0
  41. package/lib/routines.mjs +13 -2
  42. package/lib/takeout.mjs +169 -0
  43. package/lib/tdx-attestation.mjs +432 -0
  44. package/lib/tdx-trust.json +10 -0
  45. package/lib/tools.mjs +669 -59
  46. package/lib/touches.mjs +427 -0
  47. package/lib/update-check.mjs +30 -0
  48. package/lib/yaml.mjs +8 -4
  49. package/package.json +2 -1
  50. package/templates/AGENTS.md +56 -6
  51. package/templates/evda-digest.md +2 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,101 @@ Plain-English notes on how Iris behaves differently after each version. Not a co
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ On a machine that does not write the day (`EVDA_DAY_WRITER` set), the Claude Code prompt hook no
8
+ longer appends to the local day file. It sends each message to the writer through the command in
9
+ `EVDA_DAY_WRITER_SEND` (evda_write's arguments as JSON on stdin), and a message the writer cannot take
10
+ waits in this machine's own outbox (`EVDA_PROMPT_OUTBOX`, default `~/.evda/outbox`) and goes with the
11
+ next one, filed once. The keep-record hook's reminder there names evda_write. The day-writer guard
12
+ now sees a day file named through a variable the same command set, `$HOME` or `~`. `evda doctor`
13
+ warns when evda.json says `"filing": "everything"` and no prompt hook is installed (HQ-2817).
14
+
15
+ An EVÐA made with `evda adopt` now has the steps for turning training off. Iris gives those steps
16
+ only from the "Turning training off" section of `README.md`, and an adopted folder kept its own
17
+ README, which never had that section, or the ones on what each AI company says and running a model
18
+ on your own computer. The adopt now adds those three sections after your README as one block marked
19
+ as EVÐA's, leaving your text exactly as it was, or writes the whole README when you had none.
20
+ `evda adopt --revert` takes the block back out, and `evda upgrade` keeps it current. `evda doctor`
21
+ fails a record whose `AGENTS.md` sends Iris to a `README.md` section that is not there, and says to
22
+ run `evda upgrade`, which adds it.
23
+
24
+ `evda doctor` run from a git checkout now says, just before its verdict, when that checkout is behind
25
+ origin/main or is not main, so a pass from an old branch no longer reads as main's pass. It fetches
26
+ main first with a five-second limit; offline, with no origin, or from an installed copy it still runs
27
+ every check and says it could not compare. On current main it says nothing extra, and the exit code is
28
+ unchanged. `EVDA_DOCTOR_FETCH=0` skips the fetch.
29
+
30
+ When a filing names pages your EVÐA already has (a person, a project, a topic) or open tickets on your
31
+ board, and the conversation adds to none of them, Iris is now asked once, naming them, whether what you said adds to one; adding
32
+ nothing is a fine answer. In Claude Code the record's Stop hook asks before the session stops, and in
33
+ the chat apps the next tool result asks. A record with the Stop hook installed shows it as older in
34
+ `evda doctor` until `evda hook install` refreshes it.
35
+
36
+ Claude Cowork can reach your EVÐA now. Cowork does not start the servers in the desktop app's
37
+ config and does not read your folder's hooks, so `evda plugin` builds a plugin for your record (its
38
+ evda tools, the hooks that keep it, and a skill that has Iris read the operating layer first), and
39
+ you upload the zip in Customize, then Plugins, then Upload plugin. `create-evda` and `evda connect`
40
+ print the command. The plugin's hooks act only in Cowork, so the same plugin syncing into Claude Code
41
+ files nothing twice.
42
+
43
+ `evda secret migrate` moves the passwords in Credentials/ files sealed before they went to the
44
+ Keychain: each value goes into the Keychain, and the file keeps a pointer in its place, the same as a
45
+ new gather does. A file whose Keychain write fails stays exactly as it was, an item already in the
46
+ Keychain is never overwritten, and running it again changes nothing. `--dry` says what it would move.
47
+ Two lines with the same label in one file (two "Password:" lines) now get separate Keychain items,
48
+ where before the second overwrote the first.
49
+
50
+ The EVÐA app's setup now asks how to organize your EVÐA right after what you want it for, offering
51
+ the same presets as `create-evda`, or starting plain. `evda preset list --json` prints the list it
52
+ reads.
53
+
54
+ `create-evda --help`, `create-evda -h`, `evda init --help` and `evda mcp --help` now print how to use
55
+ them and stop. Before, `create-evda -h` made a folder named `-h` and `evda mcp --help` started serving
56
+ your record. On a Mac, `create-evda` now says it is installing the daily morning organize as a
57
+ launchd job before it does, with `--yes` too, and names `--no-routines` to skip it. Gathering a pile
58
+ now skips what apps leave behind (Office lock files, empty files, a Recently Deleted folder, Google
59
+ Takeout's `archive_browser.html`) and lists each one, with why, in the proposal.
60
+
61
+ `evda publish public` now leaves a markdown file's `sources:` frontmatter out of what it exports. That
62
+ field lists the private files a public page was written from, so it never belonged in public, and it
63
+ was making the publish refuse every run once a page carried one. A private path in a page's body
64
+ still refuses the publish exactly as before.
65
+
66
+ When Iris files something, it is now told which pages you already have that the words name (a person,
67
+ a topic, a project) and which shared circles that reaches, such as a public entry derived from the
68
+ folder it just wrote to. It is information for Iris to judge, so it still decides what to write, and
69
+ nothing noted, sealed, or set for a different AI is ever named.
70
+
71
+ When your work lives on a board (`"work": "harlequin:"`), filing also names the open tickets the
72
+ words reference, by `HQ-` number or by a distinctive run of a ticket's title, at most five, so a
73
+ conversation about a ticket's subject points Iris at the ticket. A done or obsolete ticket is never
74
+ named. The board is read in the background with the same credentials as `evda work` (or the env file
75
+ `EVDA_WORK_ENV_FILE` names), so a slow or unreachable board never slows a filing.
76
+
77
+ A people folder now keeps one file per person, and `evda doctor` says so when it does not: a folder
78
+ inside it, a name with a parenthetical when nobody else shares the name, and a stub that only
79
+ points at another page each fail, and so does a page with no person-profile frontmatter, which
80
+ doctor cannot read. Gathering a pile writes every page it puts in a people folder as a profile, and
81
+ migrating people now types a page with no dated section (its one observation is the day it was
82
+ typed) and a page that already has frontmatter, keeping what that frontmatter says. Applying a preset that makes a people folder also makes `Processing/` beside it
83
+ for the docs where you work something through, and a README in the folder saying the rule.
84
+
85
+ In the Claude app, Iris starts a chat knowing what your last days were about, not only when you
86
+ wrote: each recent entry comes with its one line of context and the opening of your words, and the
87
+ people whose pages gained a dated section this week are listed with those sections. Searching your
88
+ record shows the most recent matches first. Iris answers you in the reply itself and says what it
89
+ filed in one line after, instead of writing its answer into a page. And the Claude app now reads
90
+ your whole instructions file at the start of a chat, up to about 100,000 characters, where it used
91
+ to get some sections as a heading to fetch. Noted and sealed entries still never appear.
92
+
93
+ In a chat app, Iris now writes where it writes from a terminal. A correction you give it lands in
94
+ the file you keep corrections in, when `evda.json` names it as `"corrections"`, as a dated section
95
+ in your words; before, a record without a "What I have been taught" heading lost every one. Iris can
96
+ add a row to a log kept as a table (a food log, a spike log, a weekends board) with
97
+ `evda_append_row`, and put a new section inside the part of a file it belongs in with
98
+ `evda_append`'s `under`, so a queued idea no longer lands under "Completed". When Iris offers to
99
+ start a page, a person's page or a folder and you say yes, `evda_create` makes it, and never where
100
+ something already is.
101
+
7
102
  When two of your machines share one record through iCloud, one of them can be the only one that
8
103
  writes the daily note. Set EVDA_DAY_WRITER on every other machine and add hooks/evda-day-writer-guard.mjs
9
104
  to that machine's own Claude Code settings: an agent there that tries to write the note directly is
package/README.md CHANGED
@@ -109,11 +109,16 @@ codex mcp add evda -- /path/to/node /path/to/evda.mjs mcp ~/my-evda
109
109
 
110
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
111
 
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.
112
+ **Each surface reaches a record its own way.** `create-evda` and `evda connect` print every one of these with your record's path filled in.
113
+
114
+ - **Claude Code** (terminal, IDE, the desktop app's Code tab): `claude mcp add` above for every folder, and in sessions opened in the record its own `.claude/settings.json` hooks and `.claude/skills/` load on their own.
115
+ - **The Claude desktop app's chat**: add the same command under `mcpServers` in `claude_desktop_config.json` (Settings, then Developer, then Edit Config) and restart the app. That file reaches only the chat: Cowork does not start its servers.
116
+ - **Claude Cowork**: `evda plugin <dir>` builds a plugin for this one record, a folder and a zip under `.evda/plugin/` (or `--out <zip>`), and the upload is the one step left to you: in the Claude app, Customize, then Plugins, then Add, then Upload plugin, and choose the zip. Cowork reads no folder's `CLAUDE.md`, `AGENTS.md` or hooks, so the plugin carries all three ways in: the record's MCP server (`evda mcp <dir>`, the path baked in, because Cowork does not ask for plugin settings), a skill that loads the operating layer before the first reply, and hooks that run the record's own keep-record hook, prompt-filing hook (with "everything") and read guard, the files `evda hook install` keeps current. It works in Cowork tasks that run on your computer. The plugin also loads in Claude Code sessions signed in to the same account; its hooks stay quiet there (they act only when Claude Code runs a Cowork task), and `claude plugin disable <name>@synced` takes it out of Claude Code. Move the folder and you build and upload it again.
117
+ - **claude.ai** (web and phone): no local server reaches it. `evda mcp --http` serves the record over HTTPS behind OAuth, through a tunnel, as a connector you add in Settings, then Connectors.
113
118
 
114
119
  **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.
115
120
 
116
- **The record keeps itself.** You never have to say "save this". In Claude Code, setup (when you pick Claude) installs a Stop hook in the record's own `.claude/settings.json`: a session that read the record or heard you out and filed nothing to the day's record is asked once, before it stops, to file your words. It runs only in sessions opened in the record; your global settings are not touched. `evda hook install` adds it to a record made earlier, and `evda doctor` says whether it is installed and current. In apps with no hooks (the Claude desktop app, ChatGPT), the MCP server itself adds one line to its tool results once a conversation has read the record and filed nothing, and goes quiet after the first write.
121
+ **The record keeps itself.** You never have to say "save this". In Claude Code, setup (when you pick Claude) installs a Stop hook in the record's own `.claude/settings.json`: a session that read the record or heard you out and filed nothing to the day's record is asked once, before it stops, to file your words. It runs only in sessions opened in the record; your global settings are not touched. `evda hook install` adds it to a record made earlier, and `evda doctor` says whether it is installed and current. In Cowork the plugin from `evda plugin` runs the same hooks. In apps with no hooks (the Claude desktop app's chat, ChatGPT), the MCP server itself adds one line to its tool results once a conversation has read the record and filed nothing, and goes quiet after the first write. A second check rides on both: a write's answer names the existing entries its words bear on, and a conversation that was named some and added to none of them is asked once, naming them (and any open board tickets it named), with adding nothing as a fine answer. It goes quiet after a write to one of them or once asked. The Stop hook reads those names back out of the Claude Code transcript and the MCP server keeps them per chat, so nothing about it is stored in your files.
117
122
 
118
123
  **Filing is the owner's answer.** Setup asks once whether to file everything they say automatically (the default) or only when they ask, and writes it to `evda.json` as `"filing": "everything"` or `"asked"`; a record with no answer is asked at first connect, and `evda_filing` records the reply. `evda_write`'s description leads with when to call it, because a deferred tool in the Claude app shows only that first line. One conversation is one `## HH:MM` section of the day. With "everything", Claude Code also gets a UserPromptSubmit hook, `.claude/hooks/evda-file-prompt.mjs`, that files each message verbatim as it is sent. `evda gather scan <conversations.json> --into <record>` reads a Claude.ai or ChatGPT export and, on apply, adds the owner's own messages the record lacks under each conversation's date and time.
119
124
 
@@ -127,7 +132,7 @@ For the Claude desktop app, add the same command under `mcpServers` in `claude_d
127
132
 
128
133
  How a record is organized comes from a library, so an agent proposes a folder that already exists in it instead of inventing one. Each preset is `presets/<id>/preset.json`: an `id`, a `title`, `for` (who it suits), `folders` (each a path, and for a typed folder its `type`, `connectors` and `views`, validated by the same code as `folder.evda.json`), optional `circles` (proposed as proposal files, never created), optional `routines` (offered, never scheduled), and `mapping`, the folders that fill the four roles. Every folder a preset makes fills a role, and a preset that has one filling none fails its test. Two ship: `projects-and-areas` (numbered folders for projects, areas with courses under Academics, resources, work, captures, the daily journal and attachments) and `journal`. A course is a collection, so there is no student preset, and there is no archive folder: finished is a status. Setup offers them after purpose, with "none, start plain". Applying one adds the role paths the record lacks to `evda.json`; a role key the record already uses keeps its path, and the preset's folder fills the same role under a facet key named for it.
129
134
 
130
- evda preset list
135
+ evda preset list [--json] --json: one line, [{id, title, for}], what the EVÐA app's setup offers
131
136
  evda preset show <id>
132
137
  evda preset apply <id> [--dry] [dir]
133
138
  evda preset diff <id> [dir] what the preset has that this record does not
@@ -169,11 +174,11 @@ Writes:
169
174
  grant file per reader and circle
170
175
  .evda/version the schema version actually on disk
171
176
  .evda/due.jsonl derived from the ledger's Due lines
172
- .evda/digest.md derived from the fenced block of AGENTS.md, under 4096 bytes
177
+ .evda/digest.md derived from the fenced block of AGENTS.md, under 6144 bytes
173
178
 
174
179
  The training instructions in the scaffolded README and in setup come from one table, `lib/setup.mjs`, with a link to each provider's page.
175
180
 
176
- The tree above is what the scaffolder creates and not a requirement. `evda.json` carries a `mapping` from roles to paths, and every tool reads roles. An existing folder of notes becomes an EVÐA by adding `evda.json` with a mapping that names where things already are, and nothing moves.
181
+ The tree above is what the scaffolder creates and not a requirement. `evda.json` carries a `mapping` from roles to paths, and every tool reads roles. An existing folder of notes becomes an EVÐA by adding `evda.json` with a mapping that names where things already are, and nothing moves. `evda adopt <dir>` writes that mapping: an Iris Vault (`00_Inbox` through `06_Metadata`) maps without a question, any other folder is placed by its name or named back to the owner to answer with `--map <folder>=<role>`, and it adds `AGENTS.md`, `.evda/`, the Claude Code hooks, one `@AGENTS.md` line at the end of the owner's `CLAUDE.md`, and the `README.md` sections `AGENTS.md` sends the agent to (turning training off, what each AI company says, running a model locally) as one marked block after the owner's own `README.md`, which `evda upgrade` keeps current. Every other file stays byte for byte, and `evda adopt --revert <manifest>` puts the folder back exactly as it was. A chat app runs it through `evda_adopt`, which writes nothing without `confirmed: true`.
177
182
 
178
183
  ### Four roles, and records inside records
179
184
 
@@ -214,6 +219,8 @@ An `evda.json` anywhere below the root starts a sub-record, with no depth limit.
214
219
  evda use <dir> point ~/.evda/current at a record
215
220
  evda mcp [dir] serve the record over MCP on stdio for Claude Code, Codex or the Claude app
216
221
  evda connect [dir] the MCP setup for each client, with this record's path
222
+ evda plugin [dir] [--out <zip>]
223
+ a Claude plugin for this record, for Cowork: its server, hooks and skill
217
224
  evda skill install [--project <dir>]
218
225
  install the evda skill for Claude Code
219
226
  evda hook install [dir] the keep-record Stop hook and the read guard, in the record's .claude/settings.json,
package/bin/evda.mjs CHANGED
@@ -25,7 +25,7 @@ import { grant, revoke, readGrants, access } from "../lib/grants.mjs";
25
25
  import { holderOf, HOLDER_KINDS } from "../lib/manifest.mjs";
26
26
  import { EvdaError, isEntry } from "../lib/errors.mjs";
27
27
  import { publishCommand } from "../lib/publish.mjs";
28
- import { checkForUpdate, summarize, stampInstalledVersion } from "../lib/update-check.mjs";
28
+ import { checkForUpdate, summarize, stampInstalledVersion, isNewer } from "../lib/update-check.mjs";
29
29
  import { planCircles, coverage, declare, deviceCircles, planBeside, defaultParts } from "../lib/circle-plan.mjs";
30
30
  import { circleIndex, grantView } from "../lib/filechanges.mjs";
31
31
  import { mountRoot } from "../lib/declarations.mjs";
@@ -76,6 +76,9 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
76
76
  a one-time owner code for the authorization page, or end every
77
77
  connection, or say what is connected
78
78
  evda connect [dir] how to add that server to each client, with this record's path
79
+ evda plugin [dir] [--out <zip>]
80
+ build a Claude plugin for this record (its MCP server, hooks and skill,
81
+ the path baked in), for Claude Cowork: upload it in Customize, Plugins
79
82
  evda skill install [--project <dir>]
80
83
  install the evda skill for Claude Code (~/.claude/skills, or a project)
81
84
  evda hook install [dir] add the keep-record Stop hook and the read guard to the record's .claude/settings.json,
@@ -102,17 +105,31 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
102
105
  run doctor. Writes a reversal manifest beside the proposal
103
106
  evda gather revert <manifest> [--force]
104
107
  undo an apply byte for byte; refused when a placed file was edited
108
+ evda adopt <dir> [--dry] [--map <folder>=<role>]... [--filing everything|asked] [--owner NAME]
109
+ [--agent-name NAME] [--manifest <file>] [--no-hooks]
110
+ make an existing folder of notes an EVDA where it is: an Iris Vault
111
+ maps without asking, a folder it cannot place is named and answered
112
+ with --map (journal, collections, knowledge, work, or none). Writes
113
+ evda.json, AGENTS.md, one @AGENTS.md line at the end of CLAUDE.md,
114
+ .evda/ and the hooks; every other file stays byte for byte. Writes a
115
+ reversal manifest outside the folder
116
+ evda adopt --revert <manifest> [--force]
117
+ undo an adopt byte for byte; refused when a file it wrote was edited
105
118
  evda secret <file> [dir] [--keychain <path>]
106
119
  a credential note (in Credentials/, or --into a path of its own) with
107
120
  every "in your Keychain" pointer read back and filled in, to this
108
121
  terminal only (HQ-1916). No MCP tool reads a value this way
122
+ evda secret migrate [dir] [--dry] [--keychain <path>]
123
+ once, for Credentials/ files sealed before the Keychain move: each
124
+ value goes into the Keychain and the file keeps a pointer. All or
125
+ nothing per file; a second run changes nothing (HQ-2622)
109
126
  evda look-back [dir] [--model M]
110
127
  the week, read back by the owner's claude -p, appended to a report
111
128
  evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
112
129
  print a scheduled routine for Claude, or a launchd fallback; --install
113
130
  writes the launchd job, --uninstall removes it (setup installs the
114
131
  morning organize unless told no)
115
- evda preset list the organizations this package ships, and who each suits
132
+ evda preset list [--json] the organizations this package ships, and who each suits
116
133
  evda preset show <id> one preset's folders
117
134
  evda preset apply <id> [--dry] [dir]
118
135
  add the preset's folders and manifests. Only adds: an existing
@@ -200,6 +217,19 @@ function packageVersion() {
200
217
  }
201
218
  }
202
219
 
220
+ // One command's lines of HELP, cut from it so the two cannot say different things: every entry that
221
+ // starts with `evda <cmd>`, with the indented lines under it.
222
+ function commandHelp(cmd) {
223
+ const out = [];
224
+ let inside = false;
225
+ for (const line of HELP.split("\n")) {
226
+ if (/^ evda /.test(line)) inside = line.startsWith(` evda ${cmd} `) || line === ` evda ${cmd}`;
227
+ else if (!/^ {4,}\S/.test(line)) inside = false;
228
+ if (inside) out.push(line);
229
+ }
230
+ return [`usage: evda ${cmd}`, "", ...out].join("\n");
231
+ }
232
+
203
233
  // A positional argument is the record when it looks like a path or IS a directory on disk. A bare
204
234
  // name that exists (`evda doctor my-evda`) must not fall through to the pointer: on 2026-09-16 it
205
235
  // did, and `evda doctor scratch` read the record ~/.evda/current pointed at instead of `scratch/`.
@@ -209,7 +239,7 @@ function isDirArg(a) {
209
239
  }
210
240
 
211
241
  // `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
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"];
242
+ const VALUE_FLAGS = ["open", "folder", "to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday", "out", "key", "plan", "device", "shadow", "record-id", "env-file", "port", "host", "public-url", "log", "lifeline", "ttl", "parts", "relay", "keychain"];
213
243
  function flagValue(args, name) {
214
244
  const eq = args.find((a) => a.startsWith(`--${name}=`));
215
245
  if (eq) return eq.slice(name.length + 3);
@@ -305,6 +335,9 @@ async function main(argv) {
305
335
  }
306
336
 
307
337
  case "mcp": {
338
+ // `--help` or `-h` anywhere prints the mcp lines of HELP and starts nothing (HQ-1368): before,
339
+ // `evda mcp --help` took the flag as nothing and served the default record on stdio.
340
+ if (args.includes("--help") || args.includes("-h")) { console.log(commandHelp("mcp")); return 0; }
308
341
  if (args[0] === "token") return mcpToken(args.slice(1));
309
342
  const { dir, flags } = pickDir(args);
310
343
  if (flags.includes("--http")) {
@@ -393,13 +426,74 @@ async function main(argv) {
393
426
  throw new EvdaError("usage: evda gather scan <source>... | questions <proposal> | answer <proposal> <question> <value> | show <proposal> | apply <proposal> --into <record> | revert <manifest>", { exit: 2 });
394
427
  }
395
428
 
429
+ // HQ-2797: an existing folder of notes becomes an EVDA where it is.
430
+ case "adopt": {
431
+ if (args.includes("--help") || args.includes("-h")) { console.log(commandHelp("adopt")); return 0; }
432
+ const ad = await import("../lib/adopt.mjs");
433
+ const revert = flagValue(args, "revert");
434
+ if (revert !== undefined || args.includes("--revert")) {
435
+ if (!revert || revert.startsWith("--")) throw new EvdaError("usage: evda adopt --revert <manifest> [--force]", { exit: 2 });
436
+ const r = ad.revertAdopt(revert, { force: args.includes("--force") });
437
+ for (const l of ad.revertLines(r)) console.log(l);
438
+ return 0;
439
+ }
440
+ const takes = ["map", "filing", "owner", "agent-name", "manifest"];
441
+ const maps = [];
442
+ const positional = [];
443
+ const opts = { dry: false, hooks: true };
444
+ for (let i = 0; i < args.length; i++) {
445
+ const a = args[i];
446
+ const eq = /^--([a-z-]+)=([\s\S]*)$/.exec(a);
447
+ const [flag, inline] = eq ? [eq[1], eq[2]] : a.startsWith("--") ? [a.slice(2), undefined] : [null, undefined];
448
+ if (flag === null) { positional.push(a); continue; }
449
+ if (flag === "dry") { opts.dry = true; continue; }
450
+ if (flag === "no-hooks") { opts.hooks = false; continue; }
451
+ if (!takes.includes(flag)) throw new EvdaError(`unknown option --${flag}\n\n${commandHelp("adopt")}`, { exit: 2 });
452
+ const v = inline ?? args[++i];
453
+ if (v === undefined) throw new EvdaError(`--${flag} needs a value`, { exit: 2 });
454
+ if (flag === "map") maps.push(v);
455
+ else if (flag === "filing") { opts.filing = ad.filingKey(v); if (!opts.filing || !v.trim()) throw new EvdaError(`--filing takes everything or asked, got ${v}`, { exit: 2 }); }
456
+ else if (flag === "owner") opts.owner = v;
457
+ else if (flag === "agent-name") opts.agentName = v;
458
+ else if (flag === "manifest") opts.manifestOut = v;
459
+ }
460
+ if (positional.length !== 1) throw new EvdaError(commandHelp("adopt"), { exit: 2 });
461
+ const target = resolve(positional[0]);
462
+ opts.map = ad.parseMapArgs(maps);
463
+ opts.version = packageVersion();
464
+ // The filing question, asked the way setup asks it, when there is a terminal and no answer.
465
+ if (!opts.dry && opts.filing === undefined && process.stdin.isTTY && process.stdout.isTTY) {
466
+ const { createInterface } = await import("node:readline/promises");
467
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
468
+ try { while (!opts.filing) opts.filing = ad.filingKey(await rl.question(`${ad.FILING_QUESTION(opts.agentName)} `)); } finally { rl.close(); }
469
+ }
470
+ opts.filing ??= null;
471
+ const r = ad.adopt(target, opts);
472
+ for (const l of r.lines) console.log(l);
473
+ if (r.dry) {
474
+ if (r.questions.length) { console.log("\nBefore it can be adopted:"); for (const q of r.questions) console.log(` ${q}`); }
475
+ else console.log(`\nNothing was written. Run it without --dry to adopt ${target}.`);
476
+ return 0;
477
+ }
478
+ console.log("");
479
+ for (const l of ad.connectLines(target, opts.filing)) console.log(l);
480
+ return r.exit;
481
+ }
482
+
396
483
  // HQ-1916: a credential note's Keychain pointers, read back and printed to this terminal only.
397
484
  // No MCP tool calls into this, on purpose -- the whole point of the Keychain move is that no AI
398
485
  // reads the value; only the owner does, on their own Mac.
399
486
  case "secret": {
400
487
  const { dir, rest } = pickDir(args);
401
488
  const file = rest[0];
402
- if (!file) throw new EvdaError("usage: evda secret <file> [dir] [--keychain <path>]", { exit: 2 });
489
+ if (!file) throw new EvdaError("usage: evda secret <file> [dir] [--keychain <path>] | evda secret migrate [dir] [--dry] [--keychain <path>]", { exit: 2 });
490
+ // HQ-2622: the one-time move of Credentials/ files sealed before HQ-1916 into the Keychain.
491
+ if (file === "migrate") {
492
+ const cm = await import("../lib/credentials-migrate.mjs");
493
+ const r = cm.migrateCredentials(dir, { keychainPath: flagValue(args, "keychain") ?? process.env.EVDA_KEYCHAIN_PATH ?? null, dry: args.includes("--dry") });
494
+ for (const l of cm.migrateLines(r)) console.log(l);
495
+ return r.kept ? 1 : 0;
496
+ }
403
497
  const g2 = await import("../lib/gather.mjs");
404
498
  const k = await import("../lib/keychain.mjs");
405
499
  const abs = file.includes("/") ? resolve(dir, file) : join(dir, g2.CREDENTIALS, file);
@@ -464,6 +558,16 @@ async function main(argv) {
464
558
  return st.registered && st.file && st.current && g.registered && g.file && (!ph.registered || (ph.file && ph.current)) ? 0 : 1;
465
559
  }
466
560
 
561
+ case "plugin": {
562
+ // evda plugin [dir] [--out <zip>] the one way Cowork reaches a record (HQ-2802)
563
+ const out = flagValue(args, "out");
564
+ const { dir } = pickDir(args);
565
+ const { buildPlugin, uploadLines } = await import("../lib/plugin.mjs");
566
+ const r = buildPlugin(dir, { out });
567
+ for (const l of uploadLines(r)) console.log(l);
568
+ return 0;
569
+ }
570
+
467
571
  case "connect": {
468
572
  // How to add this record's MCP server to each client, with the record's path filled in.
469
573
  const { dir } = pickDir(args);
@@ -484,6 +588,12 @@ async function main(argv) {
484
588
  const [sub, ...rest] = args;
485
589
  if (sub === "list") {
486
590
  const all = presets.listPresets();
591
+ // --json: one line the EVÐA app's setup reads its choices from, so it offers exactly what
592
+ // create-evda --preset accepts (HQ-1217).
593
+ if (rest.includes("--json")) {
594
+ console.log(JSON.stringify(all.map((p) => ({ id: p.id, title: p.title, for: p.for }))));
595
+ return all.length ? 0 : 1;
596
+ }
487
597
  for (const p of all) console.log(`${p.id.padEnd(20)} ${p.title}: ${p.for}`);
488
598
  console.log(`CHECKED ${all.length} presets`);
489
599
  return all.length ? 0 : 1;
@@ -512,7 +622,7 @@ async function main(argv) {
512
622
  const plan = presets.applyPreset(dir, id, { dry });
513
623
  console.log(`evda preset apply ${id}${dry ? " --dry" : ""} ${plan.root}`);
514
624
  for (const l of presets.planLines(plan)) console.log(l);
515
- const wrote = plan.create.length + plan.write.length + plan.views.length + plan.proposed.length + plan.roles.length;
625
+ const wrote = plan.create.length + plan.write.length + plan.files.length + plan.views.length + plan.proposed.length + plan.roles.length;
516
626
  console.log(`CHECKED ${plan.folders} preset folders ${plan.create.length} ${dry ? "to create" : "created"}, ${plan.write.length} manifest(s) ${dry ? "to write" : "written"}, ${plan.matched.length} skipped, ${plan.conflicts.length} conflict(s)`);
517
627
  if (!dry && wrote === 0 && !plan.conflicts.length) console.log("Nothing was written: the record already has this preset.");
518
628
  if (dry) console.log("Dry run: nothing was written.");
@@ -619,7 +729,8 @@ async function main(argv) {
619
729
 
620
730
  case "doctor": {
621
731
  const { dir, via } = pickDir(args);
622
- return doctor(dir, { via }).exit;
732
+ // The package root this doctor runs from, so it can say whether that is current main (HQ-2736).
733
+ return doctor(dir, { via, checkout: fileURLToPath(new URL("..", import.meta.url)) }).exit;
623
734
  }
624
735
 
625
736
  case "guard":
@@ -836,6 +947,11 @@ async function main(argv) {
836
947
  const parts = splitFence(text);
837
948
  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.`);
838
949
  const shipped = shippedGenerated();
950
+ // HQ-2812: the README.md sections the operating layer sends the agent to, kept as EVÐA's
951
+ // marked block (added when one is missing, refreshed when it is there); the owner's text is
952
+ // never touched.
953
+ const { ensureReadme, readmeLine } = await import("../lib/readme.mjs");
954
+ console.log(`upgrade: ${readmeLine(ensureReadme(dir, { agents: replaceFence(text, shipped) }))}`);
839
955
  if (parts.generated === shipped) {
840
956
  stampInstalledVersion(dir, packageVersion());
841
957
  console.log("upgrade: the generated block already matches what this tool ships. Nothing outside it was read.");
@@ -861,6 +977,13 @@ async function main(argv) {
861
977
  console.log(summarize(result));
862
978
  if (result.status === "CHECK_FAILED") return 1;
863
979
  if (result.status !== "UPDATE_AVAILABLE" && result.status !== "UNSTAMPED") return 0;
980
+ // HQ-2718: upgrade writes the block THIS tool ships and stamps THIS tool's version. Run by an
981
+ // installed evda older than the registry, it used to do that and exit 0, leaving the record
982
+ // behind while the line above told the owner to run the command they had just run.
983
+ if (isNewer(result.latest, packageVersion())) {
984
+ console.log(`update: this evda is ${packageVersion()}, older than ${result.latest}, so it cannot install ${result.latest}. Nothing was changed. Run "npx -y -p create-evda@latest evda update" to take it.`);
985
+ return 1;
986
+ }
864
987
  const upgradeRc = await main(["upgrade", dir]);
865
988
  if (upgradeRc !== 0) return upgradeRc;
866
989
  return main(["migrate", dir]);
package/create-evda.mjs CHANGED
@@ -40,8 +40,9 @@ import { applyPreset, listPresets, planLines, presetIds, NONE } from "./lib/pres
40
40
  import { FILING_MODES } from "./lib/day-file.mjs";
41
41
  import { installPromptHook } from "./lib/hooks.mjs";
42
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";
43
+ import { PLATFORMS, PLATFORM_KEYS, PARTS_LINE, LOADERS, DESKTOP_FILESYSTEM, DICTATION, DEFAULT_AGENT_NAME, platformKey, fillOwnerSection, aiLine, interview, readerFor, INTRO } from "./lib/setup.mjs";
44
44
  import { describeReaders, parseReaders } from "./lib/readers.mjs";
45
+ import { renderReadme } from "./lib/readme.mjs";
45
46
 
46
47
  const HERE = dirname(fileURLToPath(import.meta.url));
47
48
  const TEMPLATES = join(HERE, "templates");
@@ -70,7 +71,14 @@ const FOLDER_TYPES = {
70
71
  messages: "message",
71
72
  };
72
73
 
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]`;
74
+ const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--filing everything|asked] [--no-prompt-hook] [--no-parts] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly|--no-routines] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]
75
+
76
+ On a Mac, setup installs the morning organize as a launchd job (daily at 8:00, your own Claude) unless --no-routines, --yes included, and says so before it does.`;
77
+
78
+ // `--help` and `-h` anywhere print this and nothing else (HQ-1368): before, `--help` was "unknown
79
+ // option" and `-h` was taken as the folder to create.
80
+ export const HELP_FLAGS = ["--help", "-h"];
81
+ export const wantsHelp = (argv) => argv.some((a) => HELP_FLAGS.includes(a));
74
82
 
75
83
  export function parseArgs(argv) {
76
84
  const opts = {
@@ -181,7 +189,7 @@ export function scaffold(opts) {
181
189
  // agent asks at first connect.
182
190
  if (opts.filing) manifest.filing = opts.filing;
183
191
  writeIfAbsent(join(dir, "evda.json"), JSON.stringify(manifest, null, 2) + "\n");
184
- writeIfAbsent(join(dir, "README.md"), render("README.md", { training: trainingSection(), terms: termsSection(), intro: INTRO, localModel: localModelSection() }));
192
+ writeIfAbsent(join(dir, "README.md"), renderReadme());
185
193
  // The owner section of AGENTS.md holds the setup answers in the owner's words. The AI line is
186
194
  // written only when training off was confirmed; empty, it is what makes the agent ask.
187
195
  const answers = {
@@ -224,12 +232,6 @@ export function scaffold(opts) {
224
232
  return { dir, manifest, pointer, guard, hook, promptHook };
225
233
  }
226
234
 
227
- // The local-model doc as a README section: its title dropped and its headings one level down.
228
- function localModelSection() {
229
- const text = readFileSync(join(HERE, "docs", "local-model.md"), "utf8");
230
- return text.split("\n").slice(1).join("\n").replace(/^## /gm, "### ").trim();
231
- }
232
-
233
235
  // Day one. The record's first daily file records the one true thing that has happened to it: it
234
236
  // was created. It says so in three lines and says where the owner's words go, then gets out of the
235
237
  // way. It is not an example note, it asks nothing, and it schedules nothing; every later file
@@ -277,8 +279,9 @@ export function needsAsking(opts) {
277
279
  // only when both stdin and stdout are a terminal. A piped or redirected run is a script, and a
278
280
  // script is never asked anything.
279
281
  export async function main(argv, io = null) {
280
- let opts = parseArgs(argv);
281
282
  const out = io?.out ?? console.log;
283
+ if (wantsHelp(argv)) { out(USAGE); return 0; }
284
+ let opts = parseArgs(argv);
282
285
  const log = opts.quiet ? () => {} : out;
283
286
  const tty = io ? Boolean(io.interactive) : Boolean(process.stdin.isTTY && process.stdout.isTTY);
284
287
  const asked = tty && !opts.yes && needsAsking(opts);
@@ -346,7 +349,7 @@ export async function main(argv, io = null) {
346
349
  // Organizing is on unless the owner said otherwise (HQ-1246): no flag and no answer means the
347
350
  // morning organize is installed.
348
351
  const env = io?.env ?? process.env;
349
- for (const l of installAtSetup(opts.routines ?? defaultRoutines(env), dir, { env, platform: io?.platform ?? process.platform })) log(l);
352
+ for (const l of installAtSetup(opts.routines ?? defaultRoutines(env), dir, { env, platform: io?.platform ?? process.platform, say: out, launchctl: io?.launchctl ?? null })) log(l);
350
353
  log("");
351
354
  for (const l of INTRO.split("\n")) log(l);
352
355
  log("");