create-evda 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -14,7 +14,7 @@ npx create-evda my-evda
14
14
 
15
15
  ## Setup
16
16
 
17
- On a terminal, `create-evda` asks one question at a time: what to call your agent (Iris unless you rename it), who you are, what you want this for, how it should talk to you, and which AI you will use. It shows where that AI's training setting is, links the provider's own page, and does not finish until you type yes. The answers go into "What this EVÐA is" at the top of `AGENTS.md`, in your words. It suggests dictation: Apple's built in dictation, or Wispr Flow with data collection turned off. Last, it offers to bring in what an AI you already use knows about you; see below.
17
+ On a terminal, `create-evda` asks one question at a time: what to call your agent (Iris unless you rename it), who you are, what you want this for, how to organize it (a preset, or start plain), how it should talk to you, and which AI you will use. It shows where that AI's training setting is, links the provider's own page, and does not finish until you type yes. The answers go into "What this EVÐA is" at the top of `AGENTS.md`, in your words. It suggests dictation: Apple's built in dictation, or Wispr Flow with data collection turned off. Last, it offers to bring in what an AI you already use knows about you; see below.
18
18
 
19
19
  Unattended, nothing is asked:
20
20
 
@@ -23,7 +23,7 @@ npx create-evda my-evda --yes --agent-name Iris --owner "Dana" --purpose "why wo
23
23
  --voice "direct" --platform claude --training-off
24
24
  ```
25
25
 
26
- `--platform` takes `claude`, `chatgpt` (ChatGPT and Codex), `gemini`, `cursor` or `other`. Without `--training-off` the AI line stays empty and the agent asks about training in the first session. A run with no terminal on stdin is treated as `--yes`.
26
+ `--platform` takes `claude`, `chatgpt` (ChatGPT and Codex), `gemini`, `cursor` or `other`. `--preset` takes a preset id or `none`; unattended, no flag means none. Without `--training-off` the AI line stays empty and the agent asks about training in the first session. A run with no terminal on stdin is treated as `--yes`.
27
27
 
28
28
  ## Any AI
29
29
 
@@ -33,13 +33,57 @@ npx create-evda my-evda --yes --agent-name Iris --owner "Dana" --purpose "why wo
33
33
 
34
34
  Setup ends by asking whether to bring in what an AI you already talk to knows about you. Say yes and it prints a prompt: paste that into ChatGPT, Claude, or whatever you use, and it writes out what it remembers or has guessed about you, in plain text. Bring the answer to your first session and your agent files it verbatim under `record/imports/YYYY-MM-DD-<source>.md`, with the source and date at the top. It is filed as what that AI said, never as your own words, and every line in it is a claim to check with you over time, not a fact.
35
35
 
36
+ ## Talk to it from the AI you already use
37
+
38
+ `evda mcp` serves your record as an MCP server on stdio, so Claude Code, Codex and the Claude desktop app can read and write it from any folder. EVÐA holds no API key and calls no model: the AI is yours, signed in as you, and it reaches the record only through these tools.
39
+
40
+ evda_operating_layer AGENTS.md, read first every session
41
+ evda_recent the last few days of the record
42
+ evda_write your words, verbatim, appended to today's daily file. It takes no path and never edits
43
+ evda_import what another AI said about you, filed under record/imports with its source and date
44
+ evda_search plain text search inside the record, on this machine
45
+ evda_circles your circles and their policies
46
+ evda_propose_circle a proposal file under proposals/. Never a circle, never a grant
47
+ evda_presets the preset library, or one preset's folders
48
+ evda_propose_structure what a preset would add to this record, in plain words. Writes nothing
49
+ evda_apply_preset adds a preset's folders; refused unless confirmed: true, the owner's yes
50
+ evda_doctor evda doctor, as a report
51
+
52
+ Every write appends, refuses anything shaped like a password, key or token, and says the path it wrote. `evda connect` prints the setup for your record, with its path filled in:
53
+
54
+ ```bash
55
+ claude mcp add --scope user evda -- /path/to/node /path/to/evda.mjs mcp ~/my-evda # Claude Code
56
+ codex mcp add evda -- /path/to/node /path/to/evda.mjs mcp ~/my-evda # Codex
57
+ ```
58
+
59
+ 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.
60
+
61
+ **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.
62
+
63
+ **The organizer.** `evda organize` goes through what you left inward. It copies each dated capture, verbatim, into the daily file of its date, once, and appends a report to `reports/organize/<date>.md`: people a recent note mentions, imports to check, undated notes, notes under no role. It moves nothing. With `--with-ai` it hands the judgment to your own `claude -p`, with the skill and read-only EVÐA tools, and appends its answer to the report. `evda look-back` does the same for the week.
64
+
65
+ **Routines.** Setup offers a morning organize and a weekly look back, each optional, and schedules nothing without a yes. `evda routine morning` prints what to put in a local routine in the Claude desktop app (Code tab, then Routines, then New routine, then Local). `evda routine morning --install` writes and loads a launchd job instead, on a Mac. A cloud routine cannot read local files, so it cannot reach a record.
66
+
67
+ ## Presets
68
+
69
+ 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) and optional `routines` (offered, never scheduled). Three ship: `projects-and-areas` (numbered top folders for inbox, projects, areas, resources, archive, captures, metadata and attachments), `journal`, and `student`. Setup offers them after purpose, with "none, start plain".
70
+
71
+ evda preset list
72
+ evda preset show <id>
73
+ evda preset apply <id> [--dry] [dir]
74
+ evda preset diff <id> [dir] what the preset has that this record does not
75
+
76
+ Applying only adds: missing directories are created and missing manifests written; a manifest that already says the same is skipped, one that says something else (or a file where a folder would go) is a `CONFLICT` that is left as it is, and nothing is moved, renamed or deleted. A second apply writes nothing. Exit 1 when there was a conflict. `test/presets.test.mjs` validates every shipped preset, so a malformed one fails a test rather than a person.
77
+
78
+ `evda doctor` reads folder manifests across the whole record root, not only under the record roles, skipping dot-directories and `node_modules` and not following symlinks.
79
+
36
80
  ## Circles and other people
37
81
 
38
82
  Everything is private until you share it. A circle is a part of the record you choose to share, plus who may read it. Audience and content are set separately: a public circle has the widest audience and the least content. Ask the agent to share something and it proposes a circle with a plain policy and waits for your yes; it never creates one, adds a reader, or moves an entry on its own. Another person reads a circle through a grant naming their own record (`evda grant family --to sam.atlas --kind person`), and `evda revoke` takes it back. A part can be more private than its circle, never less.
39
83
 
40
84
  ## The package
41
85
 
42
- `@evda/person-record` in this repository, published as `create-evda`. The person layer over the substrate. An EVÐA is a directory that knows what it is: a person's record in their own words, a ledger of reads about them held loosely, and the operating instructions beside them, so that any agent on any model can open it and act correctly with no outside context. The files are the truth. Nothing here is required to read or write a record; `evda doctor` says whether one is well formed and `evda reindex` rebuilds the derived directory, and both can be thrown away. It has no runtime dependencies (`test/deps.test.mjs` asserts it) and never makes a network call.
86
+ `@evda/person-record` in this repository, published as `create-evda`. The person layer over the substrate. An EVÐA is a directory that knows what it is: a person's record in their own words, a ledger of reads about them held loosely, and the operating instructions beside them, so that any agent on any model can open it and act correctly with no outside context. The files are the truth. Nothing here is required to read or write a record; `evda doctor` says whether one is well formed and `evda reindex` rebuilds the derived directory, and both can be thrown away. It has no runtime dependencies (`test/deps.test.mjs` asserts it) and never makes a network call. The MCP server is a small JSON-RPC implementation in `lib/mcp.mjs`, not the SDK, for the same reason.
43
87
 
44
88
  Design contract: Spec 04 (the hypothesis ledger), Spec 05 (the data layer), Spec 06 (the guarantee) and Spec 07 (holders, circles, references and views) in the EVÐA project. A short copy of what this package settles is in `docs/canon/person-record.md`.
45
89
 
@@ -97,6 +141,17 @@ The tree above is what the scaffolder creates and not a requirement. `evda.json`
97
141
  evda status [dir] one screen
98
142
  evda where which record the locator resolves to, and by which rule
99
143
  evda use <dir> point ~/.evda/current at a record
144
+ evda mcp [dir] serve the record over MCP on stdio for Claude Code, Codex or the Claude app
145
+ evda connect [dir] the MCP setup for each client, with this record's path
146
+ evda skill install [--project <dir>]
147
+ install the evda skill for Claude Code
148
+ evda organize [dir] [--dry-run] [--with-ai] [--model M]
149
+ file captures into their day and report what belongs where; moves nothing
150
+ evda look-back [dir] the week, read back by your own claude -p, appended to a report
151
+ evda routine <morning|weekly> [dir] [--launchd] [--install]
152
+ a Claude desktop routine, or a launchd job with --install
153
+ evda preset list | show <id> | apply <id> [--dry] [dir] | diff <id> [dir]
154
+ organize the record from the preset library; apply only adds
100
155
 
101
156
  The record is found through the locator: a directory argument, then `$EVDA_HOME`, then the nearest ancestor of the working directory holding `evda.json`, then `~/.evda/current`. Nothing assumes it was launched inside one.
102
157
 
package/bin/evda.mjs CHANGED
@@ -48,16 +48,44 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
48
48
  evda revoke <circle> --to <record-id> [dir]
49
49
  evda access <circle> --as <record-id> [dir]
50
50
  may that record read the circle? exit 0 yes, 2 refused, with why
51
+ evda mcp [dir] serve the record over MCP on stdio, for Claude Code, Codex or the
52
+ Claude desktop app. No key and no model call: the AI is the owner's
53
+ evda connect [dir] how to add that server to each client, with this record's path
54
+ evda skill install [--project <dir>]
55
+ install the evda skill for Claude Code (~/.claude/skills, or a project)
56
+ evda organize [dir] [--dry-run] [--with-ai] [--model M]
57
+ file captures into their day and write a report of what belongs where.
58
+ Moves nothing. --with-ai hands the judgment to the owner's claude -p
59
+ evda look-back [dir] [--model M]
60
+ the week, read back by the owner's claude -p, appended to a report
61
+ evda routine <morning|weekly> [dir] [--launchd] [--install]
62
+ print a scheduled routine for Claude, or a launchd fallback; --install
63
+ writes the launchd job, and nothing is scheduled without it
64
+ evda preset list the organizations this package ships, and who each suits
65
+ evda preset show <id> one preset's folders
66
+ evda preset apply <id> [--dry] [dir]
67
+ add the preset's folders and manifests. Only adds: an existing
68
+ manifest that differs is a CONFLICT and is left as it is
69
+ evda preset diff <id> [dir] what the preset has that this record does not
51
70
  evda status [dir] one screen summary
52
71
  evda where which EVDA the locator resolves to, and by which rule
53
72
  evda use <dir> point ~/.evda/current at a record
54
73
  evda init <dir> [--yes] [--agent-name N] [--owner O] [--purpose P] [--voice V]
55
74
  [--platform claude|chatgpt|gemini|cursor|other] [--training-off] [--layers record,ledger]
75
+ [--preset <id>|none]
56
76
  the same as create-evda: asks on a terminal, runs from flags otherwise
57
77
 
58
78
  Locator order: directory argument, $EVDA_HOME, the working directory's ancestors, ~/.evda/current.
59
79
  Exit codes: 0 ok, 1 the record has a problem and it was named, 2 refused (nothing written).`;
60
80
 
81
+ function packageVersion() {
82
+ try {
83
+ return JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")).version;
84
+ } catch {
85
+ return "0.0.0";
86
+ }
87
+ }
88
+
61
89
  // A positional argument is the record when it looks like a path or IS a directory on disk. A bare
62
90
  // name that exists (`evda doctor my-evda`) must not fall through to the pointer: on 2026-09-16 it
63
91
  // did, and `evda doctor scratch` read the record ~/.evda/current pointed at instead of `scratch/`.
@@ -67,7 +95,7 @@ function isDirArg(a) {
67
95
  }
68
96
 
69
97
  // `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
70
- const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as"];
98
+ const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday"];
71
99
  function flagValue(args, name) {
72
100
  const eq = args.find((a) => a.startsWith(`--${name}=`));
73
101
  if (eq) return eq.slice(name.length + 3);
@@ -105,6 +133,94 @@ async function main(argv) {
105
133
  return mod.main(args);
106
134
  }
107
135
 
136
+ case "mcp": {
137
+ // The record as an MCP server on stdio. stdout is the protocol; everything else is stderr.
138
+ const { dir } = pickDir(args);
139
+ const { serve } = await import("../lib/mcp.mjs");
140
+ await serve({ dir, version: packageVersion() });
141
+ return 0;
142
+ }
143
+
144
+ case "organize": {
145
+ const { dir, flags } = pickDir(args);
146
+ const { organize } = await import("../lib/organize.mjs");
147
+ const r = await organize(openEvda(dir), { withAi: flags.includes("--with-ai"), dryRun: flags.includes("--dry-run"), model: flagValue(args, "model") });
148
+ console.log(r.summary);
149
+ return r.exit;
150
+ }
151
+
152
+ case "look-back": {
153
+ const { dir } = pickDir(args);
154
+ const { lookBack } = await import("../lib/organize.mjs");
155
+ const r = await lookBack(openEvda(dir), { model: flagValue(args, "model") });
156
+ console.log(r.summary);
157
+ return r.exit;
158
+ }
159
+
160
+ case "skill": {
161
+ // evda skill install [--project <dir>] copy the evda skill for Claude Code
162
+ const { installSkill } = await import("../lib/connect.mjs");
163
+ if (args[0] !== "install") throw new EvdaError("usage: evda skill install [--project <dir>]", { exit: 2 });
164
+ const project = flagValue(args, "project");
165
+ const r = installSkill({ project: project ? resolve(project) : null });
166
+ console.log(`${r.action} ${r.path}`);
167
+ return 0;
168
+ }
169
+
170
+ case "connect": {
171
+ // How to add this record's MCP server to each client, with the record's path filled in.
172
+ const { dir } = pickDir(args);
173
+ const { connectText } = await import("../lib/connect.mjs");
174
+ console.log(connectText(dir));
175
+ return 0;
176
+ }
177
+
178
+ case "routine": {
179
+ // evda routine <morning|weekly> [dir] [--launchd] [--install]
180
+ const { routineMain } = await import("../lib/routines.mjs");
181
+ return routineMain(args, { pickDir, flagValue });
182
+ }
183
+
184
+ case "preset": {
185
+ // evda preset list | show <id> | apply <id> [--dry] [dir] | diff <id> [dir]
186
+ const presets = await import("../lib/presets.mjs");
187
+ const [sub, ...rest] = args;
188
+ if (sub === "list") {
189
+ const all = presets.listPresets();
190
+ for (const p of all) console.log(`${p.id.padEnd(20)} ${p.title}: ${p.for}`);
191
+ console.log(`CHECKED ${all.length} presets`);
192
+ return all.length ? 0 : 1;
193
+ }
194
+ const id = rest.find((a) => !a.startsWith("--"));
195
+ if (!["show", "apply", "diff"].includes(sub) || !id) throw new EvdaError("usage: evda preset list | show <id> | apply <id> [--dry] [dir] | diff <id> [dir]", { exit: 2 });
196
+ if (sub === "show") {
197
+ const p = presets.loadPreset(id);
198
+ console.log(`${p.title} (${p.id}): ${p.for}`);
199
+ for (const f of p.folders) console.log(` ${`${f.path}/`.padEnd(32)} ${(f.type ?? "untyped").padEnd(8)}${f.connectors?.length ? ` connectors ${f.connectors.join(", ")}` : ""}${f.note ? ` ${f.note}` : ""}`);
200
+ for (const c of p.circles ?? []) console.log(` circle ${c.name} (${c.public ? "public" : "private"}, proposed only): ${c.description}`);
201
+ for (const r of p.routines ?? []) console.log(` routine ${r} (offered, never scheduled without a yes)`);
202
+ return 0;
203
+ }
204
+ const { dir } = pickDir(rest.filter((a, i) => i !== rest.indexOf(id)));
205
+ if (sub === "diff") {
206
+ const plan = presets.diffPreset(dir, id);
207
+ console.log(`evda preset diff ${id} ${plan.root}`);
208
+ for (const l of presets.planLines(plan, { diff: true })) console.log(l);
209
+ if (presets.planIsEmpty(plan)) console.log(`SKIPPED preset diff: this record already has everything ${id} makes`);
210
+ console.log(`CHECKED ${plan.folders} preset folders ${plan.create.length} missing, ${plan.write.length} manifest(s) missing, ${plan.matched.length} matching, ${plan.conflicts.length} conflict(s)`);
211
+ return 0;
212
+ }
213
+ const dry = args.includes("--dry");
214
+ const plan = presets.applyPreset(dir, id, { dry });
215
+ console.log(`evda preset apply ${id}${dry ? " --dry" : ""} ${plan.root}`);
216
+ for (const l of presets.planLines(plan)) console.log(l);
217
+ const wrote = plan.create.length + plan.write.length + plan.views.length + plan.proposed.length;
218
+ console.log(`CHECKED ${plan.folders} preset folders ${plan.create.length} ${dry ? "to create" : "created"}, ${plan.write.length} manifest(s) ${dry ? "to write" : "written"}, ${plan.matched.length} skipped, ${plan.conflicts.length} conflict(s)`);
219
+ if (!dry && wrote === 0 && !plan.conflicts.length) console.log("Nothing was written: the record already has this preset.");
220
+ if (dry) console.log("Dry run: nothing was written.");
221
+ return plan.conflicts.length ? 1 : 0;
222
+ }
223
+
108
224
  case "where": {
109
225
  const found = locate({});
110
226
  console.log(`${found.dir} (via ${found.via})`);
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
- // [--name NAME] [--layers record,ledger[,circles]] [--force]
6
+ // [--name NAME] [--layers record,ledger[,circles]] [--preset ID|none] [--force]
7
7
  //
8
8
  // On a terminal, any setup answer not given as a flag is asked, one question at a time, and setup
9
9
  // does not finish until the owner confirms model training is off for the AI they chose. With
@@ -28,6 +28,9 @@ import { doctor } from "./lib/doctor.mjs";
28
28
  import { readPointer, setPointer, pointerPath } from "./lib/locate.mjs";
29
29
  import { FOLDER_MANIFEST } from "./lib/folders.mjs";
30
30
  import { EvdaError } from "./lib/errors.mjs";
31
+ import { clients, installSkill } from "./lib/connect.mjs";
32
+ import { desktopInstructions } from "./lib/routines.mjs";
33
+ import { applyPreset, listPresets, planLines, presetIds, NONE } from "./lib/presets.mjs";
31
34
  import { PLATFORMS, PLATFORM_KEYS, LOADERS, DESKTOP_FILESYSTEM, DICTATION, DEFAULT_AGENT_NAME, platformKey, fillOwnerSection, aiLine, interview, trainingSection } from "./lib/setup.mjs";
32
35
 
33
36
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -54,12 +57,13 @@ const FOLDER_TYPES = {
54
57
  "record/people": "person",
55
58
  };
56
59
 
57
- const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--use] [--no-pointer]`;
60
+ const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer]`;
58
61
 
59
62
  export function parseArgs(argv) {
60
63
  const opts = {
61
64
  layers: ["record", "ledger"], force: false, name: null, owner: "", kind: "person", id: null, pointer: true, use: false,
62
65
  yes: false, trainingOff: false, agentName: null, purpose: "", voice: "", platform: null, platformName: null,
66
+ skill: null, routines: null, preset: null,
63
67
  };
64
68
  const args = argv.flatMap((a) => {
65
69
  const eq = /^(--[a-z-]+)=([\s\S]*)$/.exec(a);
@@ -77,6 +81,18 @@ export function parseArgs(argv) {
77
81
  else if (a === "--use") opts.use = true;
78
82
  else if (a === "--yes" || a === "-y") opts.yes = true;
79
83
  else if (a === "--training-off") opts.trainingOff = true;
84
+ else if (a === "--skill") opts.skill = true;
85
+ else if (a === "--no-skill") opts.skill = false;
86
+ else if (a === "--no-routines") opts.routines = [];
87
+ else if (a === "--routines") {
88
+ opts.routines = value(++i, a).split(",").map((s) => s.trim()).filter(Boolean);
89
+ const bad = opts.routines.filter((r) => !["morning", "weekly"].includes(r));
90
+ if (bad.length) throw new EvdaError(`--routines takes morning, weekly or both, got ${bad.join(", ")}`, { exit: 2 });
91
+ }
92
+ else if (a === "--preset") {
93
+ opts.preset = value(++i, a).trim();
94
+ if (opts.preset !== NONE && !presetIds().includes(opts.preset)) throw new EvdaError(`--preset takes ${[...presetIds(), NONE].join(", ")}, got ${opts.preset}`, { exit: 2 });
95
+ }
80
96
  else if (a === "--name") opts.name = value(++i, a);
81
97
  else if (a === "--owner") opts.owner = value(++i, a);
82
98
  else if (a === "--agent-name") opts.agentName = value(++i, a);
@@ -223,7 +239,7 @@ export async function main(argv, io = null) {
223
239
  refuseNonEmpty(opts.dir, opts.force);
224
240
  const term = io ?? (await terminalIO());
225
241
  try {
226
- opts = await interview(opts, term);
242
+ opts = await interview(opts, term, { presets: listPresets() });
227
243
  } finally {
228
244
  term.close?.();
229
245
  }
@@ -232,6 +248,13 @@ export async function main(argv, io = null) {
232
248
  const { dir, pointer } = scaffold(opts);
233
249
  log(`created an EVDA at ${dir}`);
234
250
  log(`locator ${pointer.path}: ${pointer.action}\n`);
251
+ // The chosen preset only adds folders to the fresh record, and doctor below reads what it wrote.
252
+ if (opts.preset && opts.preset !== NONE) {
253
+ const plan = applyPreset(dir, opts.preset);
254
+ log(`preset ${plan.id}: ${plan.title}`);
255
+ for (const l of planLines(plan)) log(l);
256
+ log("");
257
+ }
235
258
  const report = doctor(dir, { out: log, via: "create-evda" });
236
259
  if (report.exit !== 0) {
237
260
  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.");
@@ -250,6 +273,19 @@ export async function main(argv, io = null) {
250
273
  for (const k of chosen) log(` ${PLATFORMS[k].label}: ${PLATFORMS[k].training[0]}${PLATFORMS[k].url ? ` ${PLATFORMS[k].url}` : ""}`);
251
274
  }
252
275
  log("");
276
+ 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:");
277
+ for (const c of clients(dir)) if (c.key !== "project") { log(` ${c.label}:`); for (const l of c.text.split("\n")) log(` ${l}`); }
278
+ if (opts.skill) {
279
+ const r = installSkill({ env: io?.env ?? process.env });
280
+ log("");
281
+ log(`Skill: ${r.action} ${r.path}`);
282
+ }
283
+ for (const key of opts.routines ?? []) {
284
+ log("");
285
+ for (const l of desktopInstructions(key, dir).split("\n")) log(l);
286
+ log(`Or schedule it with launchd on a Mac: evda routine ${key} ${dir} --install`);
287
+ }
288
+ log("");
253
289
  log(`Then freewrite: what happened today, what is on your mind, what felt slightly off. Your words go into record/ as you talk. README.md says the rest.`);
254
290
  if (!asked) log(DICTATION);
255
291
  return 0;
@@ -0,0 +1,48 @@
1
+ // How each AI client adds the EVDA MCP server, given the command that starts it. No node: imports,
2
+ // so the EVDA app's webview renders the same text `evda connect` prints (lib/connect.mjs).
3
+
4
+ export const SERVER_NAME = "evda";
5
+
6
+ const shellQuote = (s) => (/^[A-Za-z0-9_./:@=+-]+$/.test(s) ? s : `'${String(s).replace(/'/g, "'\\''")}'`);
7
+ const tomlString = (s) => JSON.stringify(s);
8
+
9
+ // `desktopConfig` is where claude_desktop_config.json lives on this machine, when known.
10
+ export function clientsFor(command, args, { desktopConfig = null } = {}) {
11
+ const argv = [command, ...args].map(shellQuote).join(" ");
12
+ return [
13
+ {
14
+ key: "claude-code",
15
+ label: "Claude Code",
16
+ steps: ["Run this once, from any folder. --scope user makes your EVÐA available in every Claude Code session."],
17
+ text: `claude mcp add --scope user ${SERVER_NAME} -- ${argv}`,
18
+ file: null,
19
+ },
20
+ {
21
+ key: "codex",
22
+ label: "Codex",
23
+ steps: ["Run this once, or add the block under it to ~/.codex/config.toml."],
24
+ text: [
25
+ `codex mcp add ${SERVER_NAME} -- ${argv}`,
26
+ "",
27
+ `[mcp_servers.${SERVER_NAME}]`,
28
+ `command = ${tomlString(command)}`,
29
+ `args = [${args.map(tomlString).join(", ")}]`,
30
+ ].join("\n"),
31
+ file: "~/.codex/config.toml",
32
+ },
33
+ {
34
+ key: "claude-desktop",
35
+ label: "Claude desktop app",
36
+ steps: [`Open Settings, then Developer, then Edit Config, and add "${SERVER_NAME}" under "mcpServers". Quit the app fully and open it again.`],
37
+ text: JSON.stringify({ mcpServers: { [SERVER_NAME]: { command, args } } }, null, 2),
38
+ file: desktopConfig ?? "claude_desktop_config.json",
39
+ },
40
+ {
41
+ key: "project",
42
+ label: "One folder only (.mcp.json)",
43
+ steps: ["Put this in .mcp.json at the root of a folder, and Claude Code offers the server in that folder only."],
44
+ text: JSON.stringify({ mcpServers: { [SERVER_NAME]: { type: "stdio", command, args } } }, null, 2),
45
+ file: ".mcp.json",
46
+ },
47
+ ];
48
+ }
@@ -0,0 +1,84 @@
1
+ // Connecting an AI to a record: the MCP server command, how each client adds it, and the evda skill.
2
+ //
3
+ // One source for every surface that shows these steps: `evda connect`, the end of create-evda, the
4
+ // README and the EVDA app all read from here, so no two of them can give different config.
5
+ //
6
+ // Checked against each client's own docs on 2026-09-17:
7
+ // Claude Code https://code.claude.com/docs/en/mcp (`claude mcp add ... -- cmd args`, .mcp.json)
8
+ // Codex https://developers.openai.com/codex/mcp (`codex mcp add ... -- cmd`, config.toml)
9
+ // Claude desktop https://modelcontextprotocol.io/quickstart/user (claude_desktop_config.json)
10
+ // Skills https://code.claude.com/docs/en/skills (~/.claude/skills/<name>/SKILL.md)
11
+
12
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
13
+ import { delimiter, dirname, join, resolve } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { fileURLToPath } from "node:url";
16
+ import { clientsFor, SERVER_NAME } from "./clients.mjs";
17
+
18
+ const HERE = dirname(fileURLToPath(import.meta.url));
19
+ export const PACKAGE_ROOT = resolve(HERE, "..");
20
+ export const SKILL_NAME = "evda";
21
+ export const SKILL_SOURCE = join(PACKAGE_ROOT, "skills", SKILL_NAME, "SKILL.md");
22
+ export { SERVER_NAME };
23
+
24
+ // The command a client runs to start the server for one record. When this package is installed
25
+ // somewhere stable, the absolute node and bin paths: a desktop app starts servers with a minimal
26
+ // PATH, so a bare `node` or `npx` may not be found. When it is running out of an npx cache, which
27
+ // npm may clear, the npx form instead.
28
+ export function serverCommand(recordDir, { execPath = process.execPath, bin = join(PACKAGE_ROOT, "bin", "evda.mjs"), env = process.env } = {}) {
29
+ const dir = resolve(recordDir);
30
+ let real = bin;
31
+ try { real = realpathSync(bin); } catch { /* not on disk yet, as in a test */ }
32
+ if (/[\\/]_npx[\\/]/.test(real)) return { command: "npx", args: ["-y", "-p", "create-evda@latest", "evda", "mcp", dir] };
33
+ return { command: stableNode(execPath, env), args: [real, "mcp", dir] };
34
+ }
35
+
36
+ // A `node` on PATH that is this same binary, when there is one: Homebrew's /opt/homebrew/bin/node
37
+ // survives an upgrade, and the versioned Cellar path process.execPath reports does not.
38
+ function stableNode(execPath, env) {
39
+ let target;
40
+ try { target = realpathSync(execPath); } catch { return execPath; }
41
+ for (const d of String(env.PATH ?? "").split(delimiter)) {
42
+ if (!d) continue;
43
+ const candidate = join(d, process.platform === "win32" ? "node.exe" : "node");
44
+ try { if (realpathSync(candidate) === target) return candidate; } catch { /* not here */ }
45
+ }
46
+ return execPath;
47
+ }
48
+
49
+ export function desktopConfigPath(platform = process.platform, env = process.env) {
50
+ if (platform === "darwin") return join(env.HOME ?? homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
51
+ if (platform === "win32") return join(env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
52
+ return null;
53
+ }
54
+
55
+ // Every client's setup for one record, as data. `text` fields are what a person copies.
56
+ export function clients(recordDir, opts = {}) {
57
+ const { command, args } = serverCommand(recordDir, opts);
58
+ return clientsFor(command, args, { desktopConfig: desktopConfigPath() });
59
+ }
60
+
61
+ export function connectText(recordDir, opts = {}) {
62
+ const lines = [`Connect your AI to ${resolve(recordDir)}. No key and no model call in EVDA: your AI reads and writes the record through these tools.`, ""];
63
+ for (const c of clients(recordDir, opts)) {
64
+ lines.push(`${c.label}:`);
65
+ for (const s of c.steps) lines.push(` ${s}`);
66
+ lines.push("", ...c.text.split("\n").map((l) => ` ${l}`), "");
67
+ }
68
+ lines.push(`Then install the skill for Claude Code so it acts as Iris through these tools: evda skill install`);
69
+ return lines.join("\n");
70
+ }
71
+
72
+ // Copy the evda skill into Claude Code's skills folder: the person's (~/.claude/skills) or one
73
+ // project's (<dir>/.claude/skills). An existing copy with the same bytes is left alone; a different
74
+ // one is replaced, because the skill is the installer's and carries no owner edits.
75
+ export function installSkill({ project = null, env = process.env } = {}) {
76
+ const base = project ? join(project, ".claude", "skills") : join(env.HOME ?? homedir(), ".claude", "skills");
77
+ const path = join(base, SKILL_NAME, "SKILL.md");
78
+ const shipped = readFileSync(SKILL_SOURCE, "utf8");
79
+ if (existsSync(path) && readFileSync(path, "utf8") === shipped) return { path, action: "already current:" };
80
+ const action = existsSync(path) ? "updated" : "installed";
81
+ mkdirSync(dirname(path), { recursive: true });
82
+ writeFileSync(path, shipped);
83
+ return { path, action };
84
+ }
package/lib/doctor.mjs CHANGED
@@ -318,12 +318,11 @@ export function doctor(dir, { out = console.log, via = "argument" } = {}) {
318
318
  else ok(grants.length, "grants", `across ${new Set(grants.map((g) => g.circle).filter(Boolean)).size} circle(s), ${grants.filter((g) => g.holder?.kind === "agent").length} to agents`);
319
319
  }
320
320
 
321
- // 12. folder manifests (Spec 07, section 2): each record folder says what its entries are and
322
- // which connectors feed its references. A manifest that is not JSON refuses the run.
321
+ // 12. folder manifests (Spec 07, section 2): each folder says what its entries are and which
322
+ // connectors feed its references. Read across the whole record root, not only the record roles
323
+ // (lib/folders.mjs folderManifests says why). A manifest that is not JSON refuses the run.
323
324
  let folders = [];
324
- if (recordRoles.length === 0) {
325
- skip("folder manifests", "no record role resolved to a directory");
326
- } else {
325
+ {
327
326
  try {
328
327
  folders = folderManifests(evda);
329
328
  } catch (e) {
@@ -335,7 +334,7 @@ export function doctor(dir, { out = console.log, via = "argument" } = {}) {
335
334
  for (const p of f.problems) (p.level === "fail" ? fail : warn)(`folder ${relative(dir, f.dir) || "."}`, p.message);
336
335
  out(` ${(relative(dir, f.dir) || ".").padEnd(24)} type ${f.type ?? "?"} connectors [${f.connectors.join(", ")}] views [${f.views.map((v) => v.view).join(", ")}]`);
337
336
  }
338
- if (folders.length === 0) skip("folder manifests", `no record folder carries ${FOLDER_MANIFEST}`);
337
+ if (folders.length === 0) skip("folder manifests", `no folder in the record carries ${FOLDER_MANIFEST}`);
339
338
  else ok(folders.length, "folder manifests", `${folders.filter((f) => f.connectors.length).length} with connectors`);
340
339
 
341
340
  // 12b. views (Spec 07, section 5): a manifest's `views` declare the markdown files the renderer
package/lib/folders.mjs CHANGED
@@ -112,24 +112,38 @@ export function validateFolderManifest(m, rel) {
112
112
  return out;
113
113
  }
114
114
 
115
- // Every folder manifest under the record's record roles, deepest last. Each item is
116
- // { rel, abs, dir, ...validateFolderManifest }. Throws only on a manifest that is not JSON.
115
+ // Every folder manifest in the record. Each item is { rel, abs, dir, ...validateFolderManifest }.
116
+ // Throws only on a manifest that is not JSON.
117
+ //
118
+ // The record roles are walked first, in role order and deepest last, then the rest of the record
119
+ // root, because a folder a preset or the owner made outside every role still says what it holds and
120
+ // is still read. Until HQ-1216 only the roles were walked, and a malformed manifest one level outside
121
+ // them printed OK. The walk skips dot-directories and node_modules and does not follow a symlink
122
+ // below a role or the root, so a link cannot pull another tree, or the same tree twice, into the count.
123
+ //
124
+ // FALSIFIED 2026-09-17: a fresh scaffold with Outside/folder.evda.json holding {"kind":"person"}
125
+ // (no type). The roles-only walk (main at 2788455) printed "CHECKED 3 folder manifests" and
126
+ // "OK 9 check(s), 0 warning(s)", exit 0. This walk prints "FAIL folder Outside
127
+ // Outside/folder.evda.json: has no "type". ..." and "CHECKED 4 folder manifests", exit 1.
117
128
  export function folderManifests(evda) {
118
129
  const out = [];
119
- const roots = resolveAll(evda).filter((r) => r.known && r.mapped && r.layer === "record" && r.exists && r.isDir);
130
+ const seen = new Set();
120
131
  const walk = (dir) => {
132
+ if (seen.has(dir)) return;
133
+ seen.add(dir);
121
134
  const m = readFolderManifest(dir);
122
135
  if (m !== null) {
123
136
  const abs = join(dir, FOLDER_MANIFEST);
124
137
  out.push({ rel: relative(evda.dir, abs), abs, dir, ...validateFolderManifest(m, relative(evda.dir, abs)) });
125
138
  }
126
- for (const name of readdirSync(dir).sort()) {
127
- if (name.startsWith(".")) continue;
128
- const p = join(dir, name);
129
- if (statSync(p).isDirectory()) walk(p);
139
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
140
+ for (const e of entries) {
141
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
142
+ if (e.isDirectory()) walk(join(dir, e.name));
130
143
  }
131
144
  };
132
- for (const r of roots) walk(r.abs);
145
+ for (const r of resolveAll(evda).filter((x) => x.known && x.mapped && x.layer === "record" && x.exists && x.isDir)) walk(r.abs);
146
+ walk(evda.dir);
133
147
  return out;
134
148
  }
135
149