create-evda 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +12 -5
  3. package/bin/evda.mjs +129 -6
  4. package/create-evda.mjs +14 -11
  5. package/lib/adopt.mjs +516 -0
  6. package/lib/agents.mjs +1 -1
  7. package/lib/attested-host.mjs +287 -0
  8. package/lib/attested-wrap.mjs +147 -0
  9. package/lib/checkout-freshness.mjs +88 -0
  10. package/lib/circles.mjs +4 -2
  11. package/lib/clients.mjs +11 -0
  12. package/lib/connect.mjs +1 -1
  13. package/lib/connecting-apps.mjs +56 -0
  14. package/lib/connector-tools.mjs +85 -0
  15. package/lib/corrections.mjs +34 -0
  16. package/lib/credentials-migrate.mjs +120 -0
  17. package/lib/day-file.mjs +121 -12
  18. package/lib/day-writer.mjs +39 -5
  19. package/lib/doctor.mjs +60 -5
  20. package/lib/every-chat.mjs +7 -11
  21. package/lib/folders.mjs +2 -1
  22. package/lib/gather-apply.mjs +133 -24
  23. package/lib/gather.mjs +120 -19
  24. package/lib/html-notes.mjs +124 -0
  25. package/lib/keep-hook.mjs +84 -12
  26. package/lib/keychain.mjs +38 -1
  27. package/lib/manifest.mjs +3 -1
  28. package/lib/mcp-http.mjs +19 -1
  29. package/lib/mcp.mjs +358 -56
  30. package/lib/measurement-log.mjs +3 -0
  31. package/lib/migrate-people.mjs +54 -20
  32. package/lib/person-folders.mjs +90 -0
  33. package/lib/plugin-hook.mjs +109 -0
  34. package/lib/plugin.mjs +208 -0
  35. package/lib/presets.mjs +28 -6
  36. package/lib/prompt-hook.mjs +115 -4
  37. package/lib/publish.mjs +55 -10
  38. package/lib/readme.mjs +145 -0
  39. package/lib/record-walk.mjs +9 -0
  40. package/lib/record.mjs +54 -0
  41. package/lib/routines.mjs +13 -2
  42. package/lib/takeout.mjs +169 -0
  43. package/lib/tdx-attestation.mjs +432 -0
  44. package/lib/tdx-trust.json +10 -0
  45. package/lib/tools.mjs +669 -59
  46. package/lib/touches.mjs +427 -0
  47. package/lib/update-check.mjs +30 -0
  48. package/lib/yaml.mjs +8 -4
  49. package/package.json +2 -1
  50. package/templates/AGENTS.md +56 -6
  51. package/templates/evda-digest.md +2 -0
package/lib/keychain.mjs CHANGED
@@ -15,8 +15,16 @@
15
15
  // EVDA_KEYCHAIN_FALSIFY=<mode,...>, for the tests:
16
16
  // no-keychain keychainAvailable() always says no, so a credential file is never redacted
17
17
  // write-fails every "security add-generic-password" fails, so the value stays in the file
18
+ //
19
+ // HQ-2051: under node's test runner, nothing here may reach the default (login) keychain. A test run
20
+ // that names no keychain, or names one that does not exist (`security` then falls back to the
21
+ // default instead of failing), throws before `security` runs. On Hestia's CI runner the default
22
+ // keychain does not exist under the throwaway HOME, so such a call raised a "Keychain Not Found"
23
+ // dialog that sat on the screen for hours and failed every Mac journey. A test that truly means the
24
+ // login keychain sets EVDA_KEYCHAIN_ALLOW_DEFAULT=1; none does.
18
25
 
19
26
  import { execFileSync } from "node:child_process";
27
+ import { existsSync } from "node:fs";
20
28
  import { CRED_LINE } from "./gather.mjs";
21
29
  import { secretShape } from "./references.mjs";
22
30
 
@@ -40,17 +48,33 @@ export function keychainAvailable() {
40
48
  }
41
49
  }
42
50
 
51
+ // True inside a `node --test` run: its children carry NODE_TEST_CONTEXT, and so does every CLI a
52
+ // test spawns with the inherited env; --test-isolation=none runs in-process with --test in execArgv.
53
+ export function underTestRunner(env = process.env, execArgv = process.execArgv) {
54
+ return Boolean(env.NODE_TEST_CONTEXT) || execArgv.some((a) => a === "--test" || a.startsWith("--test-"));
55
+ }
56
+
57
+ // Throws when a test run would reach the default keychain. Every Keychain call below goes through it.
58
+ export function assertKeychainAllowed(keychainPath, env = process.env) {
59
+ if (!underTestRunner(env) || env.EVDA_KEYCHAIN_ALLOW_DEFAULT === "1") return;
60
+ if (!keychainPath) throw new Error("evda refuses the default keychain under a test run: pass a throwaway keychainPath or set EVDA_KEYCHAIN_FALSIFY (HQ-2051)");
61
+ if (!existsSync(keychainPath)) throw new Error(`evda refuses a keychain that does not exist under a test run (${keychainPath}): \`security\` would fall back to the default keychain (HQ-2051)`);
62
+ }
63
+
43
64
  const keychainArgs = (keychainPath) => (keychainPath ? [keychainPath] : []);
44
65
 
45
66
  // `security add-generic-password`, overwriting an item already at this account (a second gather of
46
67
  // the same file lands on the same value). Throws on failure; the caller decides what that means.
47
68
  export function addSecret({ account, value, keychainPath = null } = {}) {
48
69
  if (falsified("write-fails")) throw new Error("EVDA_KEYCHAIN_FALSIFY=write-fails");
70
+ assertKeychainAllowed(keychainPath);
49
71
  execFileSync("security", ["add-generic-password", "-a", account, "-s", SERVICE, "-w", value, "-U", ...keychainArgs(keychainPath)], { stdio: ["ignore", "ignore", "pipe"] });
50
72
  }
51
73
 
52
- // Never throws: revert removes what it can and says what it removed.
74
+ // Never throws on a missing item: revert removes what it can and says what it removed. Under a test
75
+ // run it does throw when it would reach the default keychain (HQ-2051).
53
76
  export function removeSecret({ account, keychainPath = null } = {}) {
77
+ assertKeychainAllowed(keychainPath);
54
78
  try {
55
79
  execFileSync("security", ["delete-generic-password", "-a", account, "-s", SERVICE, ...keychainArgs(keychainPath)], { stdio: ["ignore", "ignore", "ignore"] });
56
80
  return true;
@@ -61,6 +85,7 @@ export function removeSecret({ account, keychainPath = null } = {}) {
61
85
 
62
86
  // The value at an account, or throws (no item, or the Keychain refused to read it).
63
87
  export function readSecret({ account, keychainPath = null } = {}) {
88
+ assertKeychainAllowed(keychainPath);
64
89
  return execFileSync("security", ["find-generic-password", "-a", account, "-s", SERVICE, "-w", ...keychainArgs(keychainPath)], { stdio: ["ignore", "pipe", "ignore"] }).toString("utf8").replace(/\n$/, "");
65
90
  }
66
91
 
@@ -70,12 +95,17 @@ export function readSecret({ account, keychainPath = null } = {}) {
70
95
  // label of its own, so it is numbered "secret", "secret 2", ... in the order it appears. A labeled
71
96
  // line whose value is empty (the code sits on the next line instead) is left alone: multi-line values
72
97
  // are out of this first version's scope.
98
+ // A line already holding a pointer is not a value, so a file sealed once has nothing left to move
99
+ // (HQ-2622: the migration's second run is a no-op because of this). Two lines with the same label
100
+ // ("password" twice, or a "secret:" line beside a bare token) get "<label> 2", "<label> 3", ... so
101
+ // each lands on its own Keychain account and neither overwrites the other.
73
102
  export function extractSecrets(text) {
74
103
  const lines = text.split("\n");
75
104
  const out = [];
76
105
  let bare = 0;
77
106
  for (let i = 0; i < lines.length; i++) {
78
107
  const line = lines[i];
108
+ if (POINTER.test(line)) continue;
79
109
  const m = CRED_LINE.exec(line);
80
110
  if (m) {
81
111
  const label = (m[1] ?? m[2]).toLowerCase().replace(/[\s_-]+/g, " ");
@@ -89,6 +119,13 @@ export function extractSecrets(text) {
89
119
  out.push({ index: i, label: bare === 1 ? "secret" : `secret ${bare}`, prefix: "", value: line, whole: true });
90
120
  }
91
121
  }
122
+ const used = new Set();
123
+ for (const s of out) {
124
+ let label = s.label;
125
+ for (let n = 2; used.has(label); n++) label = `${s.label} ${n}`;
126
+ used.add(label);
127
+ s.label = label;
128
+ }
92
129
  return out;
93
130
  }
94
131
 
package/lib/manifest.mjs CHANGED
@@ -18,7 +18,9 @@ export const KNOWN_LAYERS = ["record", "ledger", "circles"];
18
18
  // lib/day-file.mjs filingOf reads it for every agent and hook.
19
19
  // `every_chat` (HQ-2205) is, per chat app, whether the owner put the every-chat line in that app's
20
20
  // settings ("set") or does not want it ("declined"); lib/every-chat.mjs reads it, evda_every_chat writes it.
21
- export const KNOWN_KEYS = ["evda", "id", "name", "owner", "created", "layers", "mapping", "subrecords", "read_by", "filing", "every_chat", "sync"];
21
+ // `corrections` (HQ-2325) is the file a correction is appended to, when the owner keeps them in
22
+ // their own file rather than the operating layer's "What I have been taught"; lib/corrections.mjs reads it.
23
+ export const KNOWN_KEYS = ["evda", "id", "name", "owner", "created", "layers", "mapping", "subrecords", "read_by", "filing", "every_chat", "sync", "corrections"];
22
24
 
23
25
  // Whether a record's circles stop at its sub-records (HQ-1371). A sub-record is a directory below
24
26
  // the root holding its own evda.json. "bounded": the parent's circles, coverage, scan and release
package/lib/mcp-http.mjs CHANGED
@@ -54,6 +54,23 @@ function limiter() {
54
54
  };
55
55
  }
56
56
 
57
+ // HQ-2330: what a request carries beyond the protocol, by NAME only, so the log can show whether an
58
+ // app sends something that names the conversation (claude.ai reuses one Mcp-Session-Id across chats).
59
+ // Values are never logged: a header or a _meta value can carry anything.
60
+ const PLAIN_HEADERS = new Set(["host", "content-type", "content-length", "authorization", "accept", "accept-encoding", "accept-language", "user-agent", "connection", "origin", "mcp-session-id", "mcp-protocol-version", "last-event-id", "via", "forwarded", "x-real-ip", "cache-control", "pragma", "te", "keep-alive"]);
61
+ const clipName = (k) => String(k).replace(/[^\x21-\x7e]/g, "").slice(0, 64);
62
+ export function extraNames(headers, params) {
63
+ const out = {};
64
+ const h = Object.keys(headers ?? {}).map((k) => k.toLowerCase()).filter((k) => !PLAIN_HEADERS.has(k) && !k.startsWith("x-forwarded-") && !k.startsWith("tailscale-") && !k.startsWith("sec-fetch-")).map(clipName).slice(0, 20);
65
+ if (h.length) out.header_names = h;
66
+ const meta = params?._meta;
67
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
68
+ const m = Object.keys(meta).map(clipName).slice(0, 20);
69
+ if (m.length) out.meta_keys = m;
70
+ }
71
+ return out;
72
+ }
73
+
57
74
  const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
58
75
  const shortId = (s) => auth.sha256(String(s)).slice(0, 8);
59
76
 
@@ -167,7 +184,7 @@ export function createMcpHttpServer({ dir, version, publicUrl, env = process.env
167
184
  if (batch.length !== 1) return send(400, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "initialize must be sent alone" } });
168
185
  evictSessions(now);
169
186
  newSid = randomBytes(24).toString("hex");
170
- session = { family: grant.family, last: now, ctx: { version, log: (s) => log(s), clock, env: readerEnv, evda: () => openEvda(dir), session: newSession(), caller: callerFrom({ env: readerEnv }) } };
187
+ session = { family: grant.family, last: now, ctx: { version, log: (s) => log(s), clock, env: readerEnv, evda: () => openEvda(dir), session: newSession(), caller: callerFrom({ env: readerEnv }), keepsToolList: true } };
171
188
  sessions.set(newSid, session);
172
189
  } else {
173
190
  if (!sid) return send(400, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Mcp-Session-Id header required; initialize first" } });
@@ -188,6 +205,7 @@ export function createMcpHttpServer({ dir, version, publicUrl, env = process.env
188
205
  const line = { call: method === "tools/call" ? (TOOL_NAMES.has(tool) ? tool : "unknown_tool") : METHODS.has(method) ? method : "unknown_method", status: st, ms: clock() - t0, session: shortId(sid ?? newSid), conn: note.who };
189
206
  // Which app connected (HQ-2207): initialize's clientInfo name and version, nothing else of it.
190
207
  if (method === "initialize") { const ci = clientInfoOf(one.params); line.client_name = ci.name; line.client_version = ci.version; }
208
+ Object.assign(line, extraNames(req.headers, one.params));
191
209
  if (auth.FALSIFY("log-content", env)) line.args = one.params?.arguments;
192
210
  record(line);
193
211
  }