create-evda 0.1.8 → 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +20 -7
  3. package/bin/evda.mjs +277 -7
  4. package/create-evda.mjs +17 -4
  5. package/dist/pairing.mjs +2932 -1684
  6. package/hooks/evda-day-writer-guard.mjs +7 -0
  7. package/lib/acknowledge.mjs +187 -0
  8. package/lib/adopt.mjs +488 -0
  9. package/lib/agents.mjs +1 -1
  10. package/lib/attested-host.mjs +269 -0
  11. package/lib/browse.mjs +354 -0
  12. package/lib/circles.mjs +5 -1
  13. package/lib/clients.mjs +29 -2
  14. package/lib/connect.mjs +9 -2
  15. package/lib/connecting-apps.mjs +56 -0
  16. package/lib/connector-tools.mjs +85 -0
  17. package/lib/corrections.mjs +34 -0
  18. package/lib/credentials-migrate.mjs +120 -0
  19. package/lib/day-file.mjs +173 -15
  20. package/lib/day-writer.mjs +110 -0
  21. package/lib/doctor.mjs +45 -3
  22. package/lib/every-chat.mjs +128 -0
  23. package/lib/filechanges.mjs +203 -68
  24. package/lib/gather-apply.mjs +176 -26
  25. package/lib/gather.mjs +119 -17
  26. package/lib/html-notes.mjs +124 -0
  27. package/lib/keep-hook.mjs +61 -8
  28. package/lib/keychain.mjs +181 -0
  29. package/lib/manifest.mjs +5 -1
  30. package/lib/mcp-auth.mjs +41 -1
  31. package/lib/mcp-http.mjs +42 -4
  32. package/lib/mcp.mjs +1198 -76
  33. package/lib/measurement-log.mjs +249 -0
  34. package/lib/messages.mjs +5 -2
  35. package/lib/migrate-people.mjs +54 -20
  36. package/lib/pairing.mjs +243 -5
  37. package/lib/parity.mjs +47 -13
  38. package/lib/person-folders.mjs +89 -0
  39. package/lib/plugin-hook.mjs +109 -0
  40. package/lib/plugin.mjs +208 -0
  41. package/lib/presets.mjs +26 -5
  42. package/lib/publish.mjs +55 -10
  43. package/lib/qr.mjs +317 -0
  44. package/lib/record.mjs +74 -0
  45. package/lib/routines.mjs +54 -4
  46. package/lib/sigstore-trust.json +20 -0
  47. package/lib/skills.mjs +1 -1
  48. package/lib/sync-lock.mjs +143 -0
  49. package/lib/sync-rules.mjs +154 -1
  50. package/lib/takeout.mjs +169 -0
  51. package/lib/tdx-attestation.mjs +412 -0
  52. package/lib/tdx-trust.json +10 -0
  53. package/lib/tools.mjs +1040 -31
  54. package/lib/touches.mjs +427 -0
  55. package/lib/update-check.mjs +30 -0
  56. package/lib/watch.mjs +160 -0
  57. package/lib/yaml.mjs +8 -4
  58. package/package.json +2 -1
  59. package/skills/evda/SKILL.md +14 -0
  60. package/templates/AGENTS.md +81 -10
  61. package/templates/evda-digest.md +5 -1
@@ -0,0 +1,110 @@
1
+ // One writer for the daily file (HQ-2165): a PreToolUse hook that refuses a direct write to the
2
+ // record's daily file on any machine that is not the one writing it.
3
+ //
4
+ // iCloud (and any folder sync) gives each machine its own local copy of the daily file. When two
5
+ // machines append to their own copies within one sync window, the sync keeps one whole copy and
6
+ // drops the other, with no conflict copy (2026-10-03: the connector's evda_write on Hestia and a
7
+ // laptop session's `cat >>` 35 seconds apart; the earlier words are in no file). No lock fixes it,
8
+ // because taking a lock in the synced folder is itself a write with the same delay. What fixes it is
9
+ // one machine doing every write to that file, and every other machine sending its words there:
10
+ // through the evda MCP tool evda_write, which the writing machine serves (test/single-writer.test.mjs
11
+ // test 2 is the proof that one replica keeps both writes).
12
+ //
13
+ // EVDA_DAY_WRITER names that machine's evda MCP endpoint. Set it on every machine that is NOT the
14
+ // writer; leave it unset on the writer and on every install with one machine, where this allows
15
+ // everything. The hook lives in the user's own Claude Code settings on that machine, never in the
16
+ // record's synced .claude/settings.json, so the writer never sees it.
17
+ //
18
+ // It refuses Write, Edit, MultiEdit and NotebookEdit of a file directly in the daily folder, and a
19
+ // shell command that names such a file next to a write-shaped operator (>, >>, tee, sed -i, cp, mv,
20
+ // rm and the rest). Like the read guard, it stops a cooperating agent on the ordinary paths; a
21
+ // command built to hide its target gets past it. Anything it cannot parse is allowed.
22
+ //
23
+ // EVDA_DAY_WRITER_FALSIFY=allow-all every decision is allow, so the tests can be watched failing
24
+
25
+ import { realpathSync } from "node:fs";
26
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
27
+ import { openEvda } from "./manifest.mjs";
28
+ import { dayRole, findRecord } from "./day-file.mjs";
29
+ import { shellPaths } from "./guard.mjs";
30
+
31
+ export const DAY_WRITER_ENV = "EVDA_DAY_WRITER";
32
+
33
+ // Shell commands that change the files they name, and the target of a redirect. A day file named
34
+ // only by a reader (cat, grep, or a reader with 2>/dev/null) is a read, left to the read guard.
35
+ const WRITERS = /((^|[\s;&|(`])(tee|cp|mv|rm|truncate|touch|install|ln|dd|ed|ex)(\s|$)|\b(sed|perl|ruby)\s+(-[A-Za-z]*\s+)*-[A-Za-z]*i)/;
36
+ const REDIRECT = />>?\|?\s*("[^"]*"|'[^']*'|[^\s;&|<>()`]+)/g;
37
+
38
+ // The paths a shell command may write: every path when a writing command runs, else redirect targets.
39
+ export function shellWriteTargets(command) {
40
+ const c = String(command);
41
+ if (WRITERS.test(c)) return shellPaths(c);
42
+ return [...c.matchAll(REDIRECT)].map((m) => m[1].replace(/^["']|["']$/g, "")).filter((w) => w && !w.startsWith("&"));
43
+ }
44
+ const FILE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
45
+
46
+ const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
47
+
48
+ // The daily folder of the record holding `abs`, real path, or null when `abs` is in no record or the
49
+ // record maps no daily role.
50
+ function dailyDirOf(abs) {
51
+ const root = findRecord(dirname(abs));
52
+ if (!root) return null;
53
+ let evda;
54
+ try { evda = openEvda(real(root)); } catch { return null; }
55
+ const role = dayRole(evda.manifest.mapping);
56
+ if (!role) return null;
57
+ return real(join(evda.dir, evda.manifest.mapping[role]));
58
+ }
59
+
60
+ // True when `abs` is a markdown file directly in its record's daily folder. A file there need not
61
+ // exist yet: creating today's file is the first write.
62
+ export function isDayFile(abs) {
63
+ if (!/\.md$/i.test(abs)) return false;
64
+ const day = dailyDirOf(abs);
65
+ if (!day) return false;
66
+ return real(dirname(abs)) === day;
67
+ }
68
+
69
+ // HQ-2596: where words wait when the writer cannot be reached. One JSON file per write in the daily
70
+ // folder's `.outbox/`, so each file has one writer and no sync can drop it; the writer files them,
71
+ // oldest first, once its own copy of the day has synced. A file in that folder is not a day file, so
72
+ // this guard allows writing it.
73
+ export const OUTBOX_DIR = ".outbox";
74
+
75
+ function sentence(writer, file) {
76
+ const outbox = `${join(dirname(file), OUTBOX_DIR)}/`;
77
+ return `EVÐA: ${basename(file)} is the daily file, and this machine does not write it. ${DAY_WRITER_ENV} names the machine that does (${writer}). Two machines writing one daily file through a synced folder lose one of the writes with no conflict copy. Append the words with the evda MCP tool evda_write, which that machine serves. If evda_write is not connected or that machine cannot be reached, queue the words in the outbox instead: write one new file per message to ${outbox}<UTC timestamp>-<host>-evda_write.json holding {tool, words, context, at, host} (tool "evda_write", words verbatim, at in UTC ISO), and the writer files them into the day when it is back. Tell the owner the words are queued, not filed, and do not write the daily file here.`;
78
+ }
79
+
80
+ // The decision for one PreToolUse input. { decision: "allow" } or { decision: "deny", reason }.
81
+ export function decide(input, { env = process.env } = {}) {
82
+ const writer = (env[DAY_WRITER_ENV] ?? "").trim();
83
+ if (!writer) return { decision: "allow" };
84
+ if (env.EVDA_DAY_WRITER_FALSIFY === "allow-all") return { decision: "allow" };
85
+ const tool = input?.tool_name;
86
+ const args = input?.tool_input ?? {};
87
+ const cwd = typeof input?.cwd === "string" && input.cwd ? input.cwd : process.cwd();
88
+ const at = (p) => (isAbsolute(p) ? p : join(cwd, p));
89
+ let paths = [];
90
+ if (FILE_TOOLS.has(tool)) paths = [args.file_path ?? args.notebook_path].filter((p) => typeof p === "string" && p);
91
+ else if (tool === "Bash") {
92
+ paths = shellWriteTargets(args.command ?? "");
93
+ } else return { decision: "allow" };
94
+ const hit = paths.map(at).find(isDayFile);
95
+ if (!hit) return { decision: "allow" };
96
+ return { decision: "deny", reason: sentence(writer, hit) };
97
+ }
98
+
99
+ // The hook's whole life: read stdin, answer on stdout, exit 0.
100
+ export async function runHook({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
101
+ let raw = "";
102
+ for await (const chunk of stdin) raw += chunk;
103
+ let input;
104
+ try { input = JSON.parse(raw); } catch { return 0; }
105
+ const d = decide(input, { env });
106
+ if (d.decision === "deny") {
107
+ stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.reason } }) + "\n");
108
+ }
109
+ return 0;
110
+ }
package/lib/doctor.mjs CHANGED
@@ -69,9 +69,12 @@ import { folderManifests, folderFor, viewNamedIn, FOLDER_MANIFEST } from "./fold
69
69
  import { referencesIn } from "./references.mjs";
70
70
  import { KIND_FOR_TYPE, PERSON_KINDS, entryFromFile, readEntry } from "./kinds.mjs";
71
71
  import { datedHeadings } from "./migrate-people.mjs";
72
+ import { personFolderProblems, README_NAME } from "./person-folders.mjs";
72
73
  import { EvdaError } from "./errors.mjs";
73
74
  import { hookStatus, promptHookStatus, HOOK_REL, PROMPT_HOOK_REL, SETTINGS_REL } from "./hooks.mjs";
74
75
  import { FILING_MODES, filingOf } from "./day-file.mjs";
76
+ import { correctionsTarget } from "./corrections.mjs";
77
+ import { EVERY_CHAT_KEY } from "./every-chat.mjs";
75
78
  import { listSkills, SKILLS_REL } from "./skills.mjs";
76
79
  import { guardStatus } from "./connect.mjs";
77
80
 
@@ -236,7 +239,12 @@ export function doctor(dir, { out = console.log, via = "argument", depth = 0 } =
236
239
  fail(rel, e.message);
237
240
  }
238
241
  if (fence === null) warn(rel, "has no evda:generated fence. The owner has taken the whole file; `evda upgrade` has nothing it owns to replace.");
239
- if (!/## What I have been taught/.test(agents)) warn(rel, 'has no "## What I have been taught" section, so a correction has nowhere to land.');
242
+ // HQ-2325: a record that keeps its corrections in their own file names it in evda.json, and
243
+ // then the heading is not where they land, so its absence is not a warning.
244
+ const corrections = correctionsTarget({ dir, manifest });
245
+ if (corrections?.problem) warn("evda.json", `${corrections.problem} evda_record_correction refuses until it does.`);
246
+ else if (corrections) ok(1, "corrections", `evda.json names ${corrections.rel}: a correction lands there as a dated section`);
247
+ else if (!/## What I have been taught/.test(agents)) warn(rel, 'has no "## What I have been taught" section, and evda.json names no "corrections" file, so a correction has nowhere to land.');
240
248
  }
241
249
  // Claude Code reads CLAUDE.md and nothing else. If the operating layer is elsewhere, CLAUDE.md
242
250
  // has to import it or a Claude Code session here starts with no instructions.
@@ -265,6 +273,18 @@ export function doctor(dir, { out = console.log, via = "argument", depth = 0 } =
265
273
  else if (manifest.filing) ok(1, "filing", `evda.json says "${manifest.filing}": ${manifest.filing === "everything" ? "every message the owner sends is filed without being asked" : "their words are filed only when they ask"}`);
266
274
  else skip("filing", "evda.json has no answer yet. The agent asks the owner once, at first connect, and records it with evda_filing.");
267
275
 
276
+ // 5b2b. every_chat (HQ-2205): per chat app, whether the owner put the every-chat line in its
277
+ // settings. An entry the server cannot read is asked about again, so it warns rather than fails.
278
+ const everyChat = manifest[EVERY_CHAT_KEY];
279
+ if (everyChat === undefined) skip("every-chat line", "evda.json records no app yet. The connector asks the owner once per chat app, the first time they connect from it, and records the answer with evda_every_chat.");
280
+ else if (!everyChat || typeof everyChat !== "object" || Array.isArray(everyChat)) warn("evda.json", `"${EVERY_CHAT_KEY}" must map each chat app to its answer, got ${JSON.stringify(everyChat)}. Every app is asked again until it does.`);
281
+ else {
282
+ const bad = Object.entries(everyChat).filter(([, e]) => !e || typeof e !== "object" || !["set", "declined"].includes(e.state));
283
+ if (bad.length) warn("evda.json", `"${EVERY_CHAT_KEY}" has an entry with no "state" of "set" or "declined" for ${bad.map(([k]) => k).join(", ")}, so that app is asked again.`);
284
+ const good = Object.entries(everyChat).filter(([k]) => !bad.some(([b]) => b === k));
285
+ if (good.length) ok(1, "every-chat line", good.map(([k, e]) => `${k}: ${e.state}${e.date ? ` ${e.date}` : ""}`).join("; "));
286
+ }
287
+
268
288
  // 5b3. the prompt hook (HQ-1385): a UserPromptSubmit hook that files each message, for an owner
269
289
  // who chose everything. Installed and current is a check; installed while filing says otherwise
270
290
  // warns, because it then files nothing and the settings say it does.
@@ -660,10 +680,21 @@ export function doctor(dir, { out = console.log, via = "argument", depth = 0 } =
660
680
  if (name === FOLDER_MANIFEST) continue;
661
681
  const rel = relative(dir, p);
662
682
  const read = entryFromFile(name, readFileSync(p, "utf8"));
663
- if (read === null) continue;
683
+ // In a person folder every file directly inside is a person's profile (HQ-2024), so one
684
+ // with no kind is a profile doctor would otherwise never read, and fails. Its README, its
685
+ // manifest and its declared views are not people; Processing/ sits beside the folder.
686
+ const unread = want === "person-profile" && p.slice(0, p.lastIndexOf(sep)) === f.dir && name !== README_NAME;
687
+ if (read === null) {
688
+ if (unread && /\.md$/i.test(name)) fail(rel, `has no person-profile frontmatter, so doctor never reads it. ${where} declares type person: every file in it opens with kind: person-profile (scripts/migrate-people.mjs adds it).`);
689
+ continue;
690
+ }
664
691
  if (read.error) { fail(rel, `${read.error} ${where} holds ${want} entries.`); continue; }
665
692
  const kind = read.raw?.kind;
666
- if (kind === undefined) { warn(rel, `has no "kind", so it is not read as a ${want}. ${where} declares type ${f.type}.`); continue; }
693
+ if (kind === undefined) {
694
+ if (unread) fail(rel, `has no "kind", so doctor never reads it as a person-profile. ${where} declares type person.`);
695
+ else warn(rel, `has no "kind", so it is not read as a ${want}. ${where} declares type ${f.type}.`);
696
+ continue;
697
+ }
667
698
  entries += 1;
668
699
  if (kind !== want) { fail(rel, `is a ${kind}, and ${where} declares type ${f.type}, whose entries are ${want}. Move it, or say what the folder holds.`); continue; }
669
700
  counts[want] = (counts[want] ?? 0) + 1;
@@ -682,6 +713,17 @@ export function doctor(dir, { out = console.log, via = "argument", depth = 0 } =
682
713
  }
683
714
  }
684
715
  }
716
+ // Person folders (HQ-2024, lib/person-folders.mjs): one file per person, named for the
717
+ // person, no subfolder, no lone parenthetical, no redirect stub. Read across every person
718
+ // folder at once, because two people who share a name can live in two folders.
719
+ const people = typed.filter((f) => f.type === "person");
720
+ if (people.length) {
721
+ const skipPaths = new Set(people.flatMap((f) => [join(f.dir, FOLDER_MANIFEST), ...f.views.map((v) => resolve(dir, v.path))]));
722
+ const layout = personFolderProblems(people.map((f) => f.dir), { skip: skipPaths });
723
+ for (const p of layout.problems) fail(relative(dir, p.path), p.message);
724
+ if (layout.files === 0) skip("person folder layout", `${people.length} person folder(s) hold no person files yet`);
725
+ else ok(layout.files, "person files", `across ${people.length} person folder(s): one file each, no subfolder, no lone parenthetical, no redirect stub`);
726
+ }
685
727
  if (entries === 0) skip("typed entries", `${typed.length} typed folder(s) hold no entry files yet`);
686
728
  else ok(entries, "typed entries", `across ${typed.length} folder(s): ${PERSON_KINDS.filter((k) => counts[k]).map((k) => `${counts[k]} ${k}`).join(", ") || "none of the declared kind"}`);
687
729
  }
@@ -0,0 +1,128 @@
1
+ // The every-chat line (HQ-2205, layer 4): the one sentence an owner puts in their chat app's
2
+ // account-wide instructions so that every chat loads the operating layer before it answers.
3
+ //
4
+ // Why it exists. Hestia's evda-mcp log (2026-09-23 to 2026-10-05) holds 153 claude.ai sessions, and
5
+ // 145 of them never called a single EVÐA tool. A chat that calls no tool never sees a tool
6
+ // description, a tool result or the server's instructions (which claude.ai does not act on anyway),
7
+ // so nothing the server does can reach it. The one thing every chat in an app reads is that app's
8
+ // own account-wide setting. So the connector asks the owner once, at the moment it first sees them in
9
+ // that app, to put this line there, and evda.json remembers the answer per app.
10
+ //
11
+ // One source. create-evda, `evda connect`, the shipped skill and the server all print the line from
12
+ // here, so no surface can carry a different copy. No node: imports, because lib/clients.mjs is
13
+ // rendered in the EVDA app's webview and reads this file.
14
+
15
+ import { appLike } from "./connecting-apps.mjs";
16
+
17
+ export const EVERY_CHAT_KEY = "every_chat";
18
+
19
+ // Bumped when the line's meaning changes. An owner who set an older version is asked once more,
20
+ // because the line in their app no longer says what the record needs; one who declined is not.
21
+ export const EVERY_CHAT_VERSION = 1;
22
+
23
+ // The line, by the owner's filing answer (evda.json "filing"). It is written in the first person
24
+ // because the app shows it to the model as the owner's own words. Each clause carries its reason,
25
+ // since a model follows a reason into cases a bare order did not foresee:
26
+ // - "before your first reply" is the whole point: a chat that answers first answers as a generic
27
+ // assistant, and the first message is the one most often lost.
28
+ // - the tool is named exactly, so an app that defers connector tools can search for it by name.
29
+ // - "if those tools are not in this chat, ignore this" keeps a chat with the connector switched
30
+ // off from stalling or apologizing.
31
+ // An owner who chose "asked" gets the same load and no instruction to file everything (HQ-1385).
32
+ export const EVERY_CHAT_LINE = {
33
+ everything:
34
+ "My EVÐA (my personal record) is connected through the evda tools. In every chat, before your first reply, call evda_operating_layer and follow it, because it says who you are for me and how my record is kept; then file each message I send with evda_write, in my exact words, in the same turn, so nothing I say is lost. If the evda tools are not in this chat, ignore this.",
35
+ asked:
36
+ "My EVÐA (my personal record) is connected through the evda tools. In every chat, before your first reply, call evda_operating_layer and follow it, because it says who you are for me and how my record is kept. Save something to my record only when I ask you to. If the evda tools are not in this chat, ignore this.",
37
+ };
38
+
39
+ export function everyChatLine(filing) {
40
+ return EVERY_CHAT_LINE[filing === "asked" ? "asked" : "everything"];
41
+ }
42
+
43
+ // Where each chat app keeps its every-chat setting, only as its own help page says it, with the
44
+ // page and the date it was read. An app not listed here, or one whose page could not be read, gets
45
+ // GENERIC_WHERE: naming a menu that does not exist would send the owner hunting for it.
46
+ // claude: support.claude.com 10185728, read 2026-10-05: "Click your initials in the lower left
47
+ // corner. Select 'Settings.' Under 'Instructions for Claude,' describe your instructions" and
48
+ // "Any instructions you add here will be applied to all of your conversations with Claude."
49
+ // One account setting, so claude.ai, the desktop app and the phone app share it.
50
+ // chatgpt: help.openai.com 8096356, read 2026-10-05: web and desktop, Settings, Personalization,
51
+ // Enable customization on, the Custom Instructions field; iOS and Android, Settings, Customize
52
+ // ChatGPT. Free and Go plans hold up to 1,500 characters, which the line fits inside.
53
+ export const CHAT_APPS = {
54
+ claude: {
55
+ label: "Claude",
56
+ where: "Click your initials in the lower left corner, open Settings, and add it under \"Instructions for Claude\". It applies to every chat on claude.ai, the desktop app and the phone app.",
57
+ limit: null,
58
+ source: "https://support.claude.com/en/articles/10185728-understanding-claude-s-personalization-features",
59
+ checked: "2026-10-05",
60
+ },
61
+ chatgpt: {
62
+ label: "ChatGPT",
63
+ where: "On the web or desktop, open Settings, then Personalization, make sure Enable customization is on, and add it to the Custom Instructions field. On a phone it is Settings, then Customize ChatGPT.",
64
+ limit: 1500,
65
+ source: "https://help.openai.com/en/articles/8096356-chatgpt-custom-instructions",
66
+ checked: "2026-10-05",
67
+ },
68
+ };
69
+ export const GENERIC_WHERE = "Add it to your app's every-chat instructions: the setting that applies to all of your conversations, not just this one.";
70
+
71
+ // Which chat app is calling, from initialize's clientInfo.name: the every-chat key of the first row
72
+ // in lib/connecting-apps.mjs (HQ-2218) whose pattern matches it. A coding agent's row has none
73
+ // (null), and a name no row matches is "other", asked with GENERIC_WHERE rather than skipped.
74
+ export function chatAppOf(clientName) {
75
+ const row = appLike(clientName);
76
+ return row ? row.chat : "other";
77
+ }
78
+
79
+ // What evda.json says for one app: { state: "set" | "declined", form, version, date } or null.
80
+ export function everyChatEntry(manifest, app) {
81
+ const all = (manifest?.manifest ?? manifest)?.[EVERY_CHAT_KEY];
82
+ const e = all && typeof all === "object" && !Array.isArray(all) ? all[app] : null;
83
+ return e && typeof e === "object" && !Array.isArray(e) ? e : null;
84
+ }
85
+
86
+ // Whether this app's owner still needs to be asked. A "declined" stands: the owner said no, and
87
+ // asking again would be nagging. A "set" stands while the line they set says what the record needs
88
+ // now: the same filing form and the same version.
89
+ export function needsEveryChat(manifest, app, filing) {
90
+ if (!app) return false;
91
+ const e = everyChatEntry(manifest, app);
92
+ if (e?.state === "declined") return false;
93
+ if (e?.state === "set") return !(e.form === (filing === "asked" ? "asked" : "everything") && e.version === EVERY_CHAT_VERSION);
94
+ return true;
95
+ }
96
+
97
+ // The walkthrough the model gives the owner, in the result of the first tool it calls in a chat.
98
+ // It is written to the model: what the setting is for, the line to hand over whole, where it goes in
99
+ // this app, and how the answer is kept, so the owner is asked once per app and never again.
100
+ export function everyChatWalkthrough(app, filing) {
101
+ const a = CHAT_APPS[app];
102
+ const line = everyChatLine(filing);
103
+ return [
104
+ `## One setting for every chat${a ? ` in ${a.label}` : ""}`,
105
+ "",
106
+ `Chats that never call an evda tool never reach this record: they answer as a generic assistant and keep nothing the owner says. The one thing every chat in ${a ? a.label : "their app"} reads is its every-chat setting, and nothing in the record shows that this owner has put the line there yet. So once, in this reply and after answering what they asked, tell them in a sentence or two why it matters and give them this line to copy, whole and exactly as written:`,
107
+ "",
108
+ "```",
109
+ line,
110
+ "```",
111
+ "",
112
+ `Where it goes: ${a ? a.where : GENERIC_WHERE}`,
113
+ "",
114
+ `When they say it is set, or that the line is already in place (set in an earlier chat, or added by hand), record it with evda_every_chat (app "${app}", state "set", confirmed: true), so no later chat in ${a ? a.label : "this app"} asks again. If they say they do not want it, record state "declined" the same way, and it is never raised again in ${a ? a.label : "this app"}. If they leave it for now, say nothing more about it in this chat.`,
115
+ ].join("\n");
116
+ }
117
+
118
+ // The line and where it goes in every app, for the setup surfaces: create-evda, `evda connect`.
119
+ export function everyChatSetupLines(filing) {
120
+ return [
121
+ "So every chat starts as your agent, even one that never touches your record, add this line to your chat app's every-chat instructions:",
122
+ "",
123
+ ` ${everyChatLine(filing)}`,
124
+ "",
125
+ ...Object.values(CHAT_APPS).map((a) => ` ${a.label}: ${a.where}`),
126
+ ` Any other app: ${GENERIC_WHERE}`,
127
+ ];
128
+ }