create-evda 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +12 -5
- package/bin/evda.mjs +122 -5
- package/create-evda.mjs +11 -3
- package/lib/adopt.mjs +488 -0
- package/lib/agents.mjs +1 -1
- package/lib/attested-host.mjs +269 -0
- package/lib/clients.mjs +11 -0
- package/lib/connect.mjs +1 -1
- package/lib/connecting-apps.mjs +56 -0
- package/lib/connector-tools.mjs +85 -0
- package/lib/corrections.mjs +34 -0
- package/lib/credentials-migrate.mjs +120 -0
- package/lib/day-file.mjs +121 -12
- package/lib/day-writer.mjs +8 -1
- package/lib/doctor.mjs +32 -3
- package/lib/every-chat.mjs +7 -11
- package/lib/gather-apply.mjs +133 -24
- package/lib/gather.mjs +116 -16
- package/lib/html-notes.mjs +124 -0
- package/lib/keep-hook.mjs +61 -8
- package/lib/keychain.mjs +38 -1
- package/lib/manifest.mjs +3 -1
- package/lib/mcp-http.mjs +19 -1
- package/lib/mcp.mjs +358 -56
- package/lib/measurement-log.mjs +3 -0
- package/lib/migrate-people.mjs +54 -20
- package/lib/person-folders.mjs +89 -0
- package/lib/plugin-hook.mjs +109 -0
- package/lib/plugin.mjs +208 -0
- package/lib/presets.mjs +26 -5
- package/lib/publish.mjs +55 -10
- package/lib/record.mjs +54 -0
- package/lib/routines.mjs +13 -2
- package/lib/takeout.mjs +169 -0
- package/lib/tdx-attestation.mjs +412 -0
- package/lib/tdx-trust.json +10 -0
- package/lib/tools.mjs +669 -59
- package/lib/touches.mjs +427 -0
- package/lib/update-check.mjs +30 -0
- package/lib/yaml.mjs +8 -4
- package/package.json +2 -1
- package/templates/AGENTS.md +56 -6
- package/templates/evda-digest.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,78 @@ Plain-English notes on how Iris behaves differently after each version. Not a co
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
When a filing names pages your EVÐA already has (a person, a project, a topic) or open tickets on your
|
|
8
|
+
board, and the conversation adds to none of them, Iris is now asked once, naming them, whether what you said adds to one; adding
|
|
9
|
+
nothing is a fine answer. In Claude Code the record's Stop hook asks before the session stops, and in
|
|
10
|
+
the chat apps the next tool result asks. A record with the Stop hook installed shows it as older in
|
|
11
|
+
`evda doctor` until `evda hook install` refreshes it.
|
|
12
|
+
|
|
13
|
+
Claude Cowork can reach your EVÐA now. Cowork does not start the servers in the desktop app's
|
|
14
|
+
config and does not read your folder's hooks, so `evda plugin` builds a plugin for your record (its
|
|
15
|
+
evda tools, the hooks that keep it, and a skill that has Iris read the operating layer first), and
|
|
16
|
+
you upload the zip in Customize, then Plugins, then Upload plugin. `create-evda` and `evda connect`
|
|
17
|
+
print the command. The plugin's hooks act only in Cowork, so the same plugin syncing into Claude Code
|
|
18
|
+
files nothing twice.
|
|
19
|
+
|
|
20
|
+
`evda secret migrate` moves the passwords in Credentials/ files sealed before they went to the
|
|
21
|
+
Keychain: each value goes into the Keychain, and the file keeps a pointer in its place, the same as a
|
|
22
|
+
new gather does. A file whose Keychain write fails stays exactly as it was, an item already in the
|
|
23
|
+
Keychain is never overwritten, and running it again changes nothing. `--dry` says what it would move.
|
|
24
|
+
Two lines with the same label in one file (two "Password:" lines) now get separate Keychain items,
|
|
25
|
+
where before the second overwrote the first.
|
|
26
|
+
|
|
27
|
+
The EVÐA app's setup now asks how to organize your EVÐA right after what you want it for, offering
|
|
28
|
+
the same presets as `create-evda`, or starting plain. `evda preset list --json` prints the list it
|
|
29
|
+
reads.
|
|
30
|
+
|
|
31
|
+
`create-evda --help`, `create-evda -h`, `evda init --help` and `evda mcp --help` now print how to use
|
|
32
|
+
them and stop. Before, `create-evda -h` made a folder named `-h` and `evda mcp --help` started serving
|
|
33
|
+
your record. On a Mac, `create-evda` now says it is installing the daily morning organize as a
|
|
34
|
+
launchd job before it does, with `--yes` too, and names `--no-routines` to skip it. Gathering a pile
|
|
35
|
+
now skips what apps leave behind (Office lock files, empty files, a Recently Deleted folder, Google
|
|
36
|
+
Takeout's `archive_browser.html`) and lists each one, with why, in the proposal.
|
|
37
|
+
|
|
38
|
+
`evda publish public` now leaves a markdown file's `sources:` frontmatter out of what it exports. That
|
|
39
|
+
field lists the private files a public page was written from, so it never belonged in public, and it
|
|
40
|
+
was making the publish refuse every run once a page carried one. A private path in a page's body
|
|
41
|
+
still refuses the publish exactly as before.
|
|
42
|
+
|
|
43
|
+
When Iris files something, it is now told which pages you already have that the words name (a person,
|
|
44
|
+
a topic, a project) and which shared circles that reaches, such as a public entry derived from the
|
|
45
|
+
folder it just wrote to. It is information for Iris to judge, so it still decides what to write, and
|
|
46
|
+
nothing noted, sealed, or set for a different AI is ever named.
|
|
47
|
+
|
|
48
|
+
When your work lives on a board (`"work": "harlequin:"`), filing also names the open tickets the
|
|
49
|
+
words reference, by `HQ-` number or by a distinctive run of a ticket's title, at most five, so a
|
|
50
|
+
conversation about a ticket's subject points Iris at the ticket. A done or obsolete ticket is never
|
|
51
|
+
named. The board is read in the background with the same credentials as `evda work` (or the env file
|
|
52
|
+
`EVDA_WORK_ENV_FILE` names), so a slow or unreachable board never slows a filing.
|
|
53
|
+
|
|
54
|
+
A people folder now keeps one file per person, and `evda doctor` says so when it does not: a folder
|
|
55
|
+
inside it, a name with a parenthetical when nobody else shares the name, and a stub that only
|
|
56
|
+
points at another page each fail, and so does a page with no person-profile frontmatter, which
|
|
57
|
+
doctor cannot read. Gathering a pile writes every page it puts in a people folder as a profile, and
|
|
58
|
+
migrating people now types a page with no dated section (its one observation is the day it was
|
|
59
|
+
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
|
|
60
|
+
for the docs where you work something through, and a README in the folder saying the rule.
|
|
61
|
+
|
|
62
|
+
In the Claude app, Iris starts a chat knowing what your last days were about, not only when you
|
|
63
|
+
wrote: each recent entry comes with its one line of context and the opening of your words, and the
|
|
64
|
+
people whose pages gained a dated section this week are listed with those sections. Searching your
|
|
65
|
+
record shows the most recent matches first. Iris answers you in the reply itself and says what it
|
|
66
|
+
filed in one line after, instead of writing its answer into a page. And the Claude app now reads
|
|
67
|
+
your whole instructions file at the start of a chat, up to about 100,000 characters, where it used
|
|
68
|
+
to get some sections as a heading to fetch. Noted and sealed entries still never appear.
|
|
69
|
+
|
|
70
|
+
In a chat app, Iris now writes where it writes from a terminal. A correction you give it lands in
|
|
71
|
+
the file you keep corrections in, when `evda.json` names it as `"corrections"`, as a dated section
|
|
72
|
+
in your words; before, a record without a "What I have been taught" heading lost every one. Iris can
|
|
73
|
+
add a row to a log kept as a table (a food log, a spike log, a weekends board) with
|
|
74
|
+
`evda_append_row`, and put a new section inside the part of a file it belongs in with
|
|
75
|
+
`evda_append`'s `under`, so a queued idea no longer lands under "Completed". When Iris offers to
|
|
76
|
+
start a page, a person's page or a folder and you say yes, `evda_create` makes it, and never where
|
|
77
|
+
something already is.
|
|
78
|
+
|
|
7
79
|
When two of your machines share one record through iCloud, one of them can be the only one that
|
|
8
80
|
writes the daily note. Set EVDA_DAY_WRITER on every other machine and add hooks/evda-day-writer-guard.mjs
|
|
9
81
|
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
|
-
|
|
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
|
|
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 and one `@AGENTS.md` line at the end of the owner's `CLAUDE.md`. 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
|
|
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.");
|
|
@@ -861,6 +971,13 @@ async function main(argv) {
|
|
|
861
971
|
console.log(summarize(result));
|
|
862
972
|
if (result.status === "CHECK_FAILED") return 1;
|
|
863
973
|
if (result.status !== "UPDATE_AVAILABLE" && result.status !== "UNSTAMPED") return 0;
|
|
974
|
+
// HQ-2718: upgrade writes the block THIS tool ships and stamps THIS tool's version. Run by an
|
|
975
|
+
// installed evda older than the registry, it used to do that and exit 0, leaving the record
|
|
976
|
+
// behind while the line above told the owner to run the command they had just run.
|
|
977
|
+
if (isNewer(result.latest, packageVersion())) {
|
|
978
|
+
console.log(`update: this evda is ${packageVersion()}, older than ${result.latest}, so it cannot install ${result.latest}. Nothing was changed. Run "npx -y -p create-evda@latest evda update" to take it.`);
|
|
979
|
+
return 1;
|
|
980
|
+
}
|
|
864
981
|
const upgradeRc = await main(["upgrade", dir]);
|
|
865
982
|
if (upgradeRc !== 0) return upgradeRc;
|
|
866
983
|
return main(["migrate", dir]);
|
package/create-evda.mjs
CHANGED
|
@@ -70,7 +70,14 @@ const FOLDER_TYPES = {
|
|
|
70
70
|
messages: "message",
|
|
71
71
|
};
|
|
72
72
|
|
|
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]
|
|
73
|
+
const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--filing everything|asked] [--no-prompt-hook] [--no-parts] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly|--no-routines] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]
|
|
74
|
+
|
|
75
|
+
On a Mac, setup installs the morning organize as a launchd job (daily at 8:00, your own Claude) unless --no-routines, --yes included, and says so before it does.`;
|
|
76
|
+
|
|
77
|
+
// `--help` and `-h` anywhere print this and nothing else (HQ-1368): before, `--help` was "unknown
|
|
78
|
+
// option" and `-h` was taken as the folder to create.
|
|
79
|
+
export const HELP_FLAGS = ["--help", "-h"];
|
|
80
|
+
export const wantsHelp = (argv) => argv.some((a) => HELP_FLAGS.includes(a));
|
|
74
81
|
|
|
75
82
|
export function parseArgs(argv) {
|
|
76
83
|
const opts = {
|
|
@@ -277,8 +284,9 @@ export function needsAsking(opts) {
|
|
|
277
284
|
// only when both stdin and stdout are a terminal. A piped or redirected run is a script, and a
|
|
278
285
|
// script is never asked anything.
|
|
279
286
|
export async function main(argv, io = null) {
|
|
280
|
-
let opts = parseArgs(argv);
|
|
281
287
|
const out = io?.out ?? console.log;
|
|
288
|
+
if (wantsHelp(argv)) { out(USAGE); return 0; }
|
|
289
|
+
let opts = parseArgs(argv);
|
|
282
290
|
const log = opts.quiet ? () => {} : out;
|
|
283
291
|
const tty = io ? Boolean(io.interactive) : Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
284
292
|
const asked = tty && !opts.yes && needsAsking(opts);
|
|
@@ -346,7 +354,7 @@ export async function main(argv, io = null) {
|
|
|
346
354
|
// Organizing is on unless the owner said otherwise (HQ-1246): no flag and no answer means the
|
|
347
355
|
// morning organize is installed.
|
|
348
356
|
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);
|
|
357
|
+
for (const l of installAtSetup(opts.routines ?? defaultRoutines(env), dir, { env, platform: io?.platform ?? process.platform, say: out, launchctl: io?.launchctl ?? null })) log(l);
|
|
350
358
|
log("");
|
|
351
359
|
for (const l of INTRO.split("\n")) log(l);
|
|
352
360
|
log("");
|