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.
- package/CHANGELOG.md +95 -0
- package/README.md +12 -5
- package/bin/evda.mjs +129 -6
- package/create-evda.mjs +14 -11
- package/lib/adopt.mjs +516 -0
- package/lib/agents.mjs +1 -1
- package/lib/attested-host.mjs +287 -0
- package/lib/attested-wrap.mjs +147 -0
- package/lib/checkout-freshness.mjs +88 -0
- package/lib/circles.mjs +4 -2
- package/lib/clients.mjs +11 -0
- package/lib/connect.mjs +1 -1
- package/lib/connecting-apps.mjs +56 -0
- package/lib/connector-tools.mjs +85 -0
- package/lib/corrections.mjs +34 -0
- package/lib/credentials-migrate.mjs +120 -0
- package/lib/day-file.mjs +121 -12
- package/lib/day-writer.mjs +39 -5
- package/lib/doctor.mjs +60 -5
- package/lib/every-chat.mjs +7 -11
- package/lib/folders.mjs +2 -1
- package/lib/gather-apply.mjs +133 -24
- package/lib/gather.mjs +120 -19
- package/lib/html-notes.mjs +124 -0
- package/lib/keep-hook.mjs +84 -12
- package/lib/keychain.mjs +38 -1
- package/lib/manifest.mjs +3 -1
- package/lib/mcp-http.mjs +19 -1
- package/lib/mcp.mjs +358 -56
- package/lib/measurement-log.mjs +3 -0
- package/lib/migrate-people.mjs +54 -20
- package/lib/person-folders.mjs +90 -0
- package/lib/plugin-hook.mjs +109 -0
- package/lib/plugin.mjs +208 -0
- package/lib/presets.mjs +28 -6
- package/lib/prompt-hook.mjs +115 -4
- package/lib/publish.mjs +55 -10
- package/lib/readme.mjs +145 -0
- package/lib/record-walk.mjs +9 -0
- package/lib/record.mjs +54 -0
- package/lib/routines.mjs +13 -2
- package/lib/takeout.mjs +169 -0
- package/lib/tdx-attestation.mjs +432 -0
- package/lib/tdx-trust.json +10 -0
- package/lib/tools.mjs +669 -59
- package/lib/touches.mjs +427 -0
- package/lib/update-check.mjs +30 -0
- package/lib/yaml.mjs +8 -4
- package/package.json +2 -1
- package/templates/AGENTS.md +56 -6
- package/templates/evda-digest.md +2 -0
package/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
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
}
|