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 +58 -3
- package/bin/evda.mjs +117 -1
- package/create-evda.mjs +39 -3
- package/lib/clients.mjs +48 -0
- package/lib/connect.mjs +84 -0
- package/lib/doctor.mjs +5 -6
- package/lib/folders.mjs +22 -8
- package/lib/mcp.mjs +301 -0
- package/lib/organize.mjs +224 -0
- package/lib/presets.mjs +328 -0
- package/lib/routines.mjs +143 -0
- package/lib/setup.mjs +76 -11
- package/lib/tools.mjs +274 -0
- package/package.json +6 -3
- package/presets/journal/preset.json +11 -0
- package/presets/projects-and-areas/preset.json +27 -0
- package/presets/student/preset.json +13 -0
- package/skills/evda/SKILL.md +88 -0
- package/templates/AGENTS.md +12 -2
- package/templates/evda-digest.md +3 -0
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;
|
package/lib/clients.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/connect.mjs
ADDED
|
@@ -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
|
|
322
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
116
|
-
//
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
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
|
|
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
|
|