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.
- package/CHANGELOG.md +109 -1
- package/README.md +20 -7
- package/bin/evda.mjs +277 -7
- package/create-evda.mjs +17 -4
- package/dist/pairing.mjs +2932 -1684
- package/hooks/evda-day-writer-guard.mjs +7 -0
- package/lib/acknowledge.mjs +187 -0
- package/lib/adopt.mjs +488 -0
- package/lib/agents.mjs +1 -1
- package/lib/attested-host.mjs +269 -0
- package/lib/browse.mjs +354 -0
- package/lib/circles.mjs +5 -1
- package/lib/clients.mjs +29 -2
- package/lib/connect.mjs +9 -2
- 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 +173 -15
- package/lib/day-writer.mjs +110 -0
- package/lib/doctor.mjs +45 -3
- package/lib/every-chat.mjs +128 -0
- package/lib/filechanges.mjs +203 -68
- package/lib/gather-apply.mjs +176 -26
- package/lib/gather.mjs +119 -17
- package/lib/html-notes.mjs +124 -0
- package/lib/keep-hook.mjs +61 -8
- package/lib/keychain.mjs +181 -0
- package/lib/manifest.mjs +5 -1
- package/lib/mcp-auth.mjs +41 -1
- package/lib/mcp-http.mjs +42 -4
- package/lib/mcp.mjs +1198 -76
- package/lib/measurement-log.mjs +249 -0
- package/lib/messages.mjs +5 -2
- package/lib/migrate-people.mjs +54 -20
- package/lib/pairing.mjs +243 -5
- package/lib/parity.mjs +47 -13
- package/lib/person-folders.mjs +89 -0
- package/lib/plugin-hook.mjs +109 -0
- package/lib/plugin.mjs +208 -0
- package/lib/presets.mjs +26 -5
- package/lib/publish.mjs +55 -10
- package/lib/qr.mjs +317 -0
- package/lib/record.mjs +74 -0
- package/lib/routines.mjs +54 -4
- package/lib/sigstore-trust.json +20 -0
- package/lib/skills.mjs +1 -1
- package/lib/sync-lock.mjs +143 -0
- package/lib/sync-rules.mjs +154 -1
- package/lib/takeout.mjs +169 -0
- package/lib/tdx-attestation.mjs +412 -0
- package/lib/tdx-trust.json +10 -0
- package/lib/tools.mjs +1040 -31
- package/lib/touches.mjs +427 -0
- package/lib/update-check.mjs +30 -0
- package/lib/watch.mjs +160 -0
- package/lib/yaml.mjs +8 -4
- package/package.json +2 -1
- package/skills/evda/SKILL.md +14 -0
- package/templates/AGENTS.md +81 -10
- package/templates/evda-digest.md +5 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code PreToolUse hook: on a machine with EVDA_DAY_WRITER set, refuse a direct write to the
|
|
3
|
+
// record's daily file and send the agent to evda_write on the machine that writes it (HQ-2165). The
|
|
4
|
+
// logic is lib/day-writer.mjs; this file is only the entry point a user-level settings.json names.
|
|
5
|
+
import { runHook } from "../lib/day-writer.mjs";
|
|
6
|
+
|
|
7
|
+
runHook().then((code) => process.exit(code), () => process.exit(0));
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Acknowledging what the owner put in the record (HQ-2060): the first reader to reach a weighty
|
|
2
|
+
// entry says it back to them once, and every other reader can see that it did.
|
|
3
|
+
//
|
|
4
|
+
// THE PROBLEM. An owner learns how much something weighed from how it was received. Today every
|
|
5
|
+
// agent over one record writes its own record and reads only its own: a live conversation, an
|
|
6
|
+
// always-on agent, a scheduled run. So a correction the owner filed on Monday gets reacted to three
|
|
7
|
+
// times, once by each, or zero times, because each assumes another already did. Mike, 2026-10-01:
|
|
8
|
+
// "whoever sees it first acts on it whetehr its phroneiss or plan pass or a next session".
|
|
9
|
+
//
|
|
10
|
+
// WHERE THE MARK LIVES, AND WHY NOT ON THE ENTRY.
|
|
11
|
+
// - A record file is append only, and `evda_write` takes no path on purpose. The one tool that
|
|
12
|
+
// edits a file is `evda_release_note`, which edits one frontmatter line and only on the owner's
|
|
13
|
+
// explicit yes. A mark written into the entry would make every reader a writer of the owner's
|
|
14
|
+
// words.
|
|
15
|
+
// - The unit of weight is a section, not a file: one conversation is one `## HH:MM` inside a day
|
|
16
|
+
// file that holds several. Frontmatter cannot address a section.
|
|
17
|
+
// - Their words stay their words. A line the agent appends inside the day file is the agent's text
|
|
18
|
+
// in the owner's file, and a later reader can quote it back as something they said.
|
|
19
|
+
// So the mark is a sidecar the readers share, in the shape `messages/.seen.json` already uses for
|
|
20
|
+
// "what this agent has seen" (lib/messages.mjs): a dotfile of bookkeeping, keyed by what it tracks,
|
|
21
|
+
// beside the thing it tracks. It is at the record root rather than under `.evda/`, because `.evda/`
|
|
22
|
+
// is derived and rebuildable and an acknowledgement is derivable from nothing.
|
|
23
|
+
//
|
|
24
|
+
// The format, one file per record:
|
|
25
|
+
//
|
|
26
|
+
// .acknowledged.json
|
|
27
|
+
// {
|
|
28
|
+
// "record/daily/2026-10-01.md#14:03": {
|
|
29
|
+
// "by": "Hestia",
|
|
30
|
+
// "at": "2026-10-01T06:12:00-04:00",
|
|
31
|
+
// "note": "said back to him that the Greek trade is settled and I will not reopen it"
|
|
32
|
+
// }
|
|
33
|
+
// }
|
|
34
|
+
//
|
|
35
|
+
// The key is the entry: its path relative to the record, and the exact heading of the section when
|
|
36
|
+
// the weight is in one section. A heading never changes once written, so the key is stable.
|
|
37
|
+
//
|
|
38
|
+
// Append only, in the same sense as the record: a key that is present is never written over. The
|
|
39
|
+
// second reader of an entry gets { first: false, already } and nothing is written, which is the
|
|
40
|
+
// whole mechanism. "Weighty" is never decided here; weight is the reader's judgment and this file
|
|
41
|
+
// only remembers that one of them acted.
|
|
42
|
+
//
|
|
43
|
+
// EVDA_ACK_FALSIFY=<mode,...>, for the tests. Each breaks exactly one rule:
|
|
44
|
+
// overwrite a second acknowledgement of the same entry overwrites the first and reports first
|
|
45
|
+
// any-section a section that is not a heading in that file is accepted
|
|
46
|
+
// strict-read an .acknowledged.json that does not parse throws instead of reading as empty
|
|
47
|
+
// no-secrets the secret check is skipped
|
|
48
|
+
// hide acknowledgementsFor returns nothing, so a later reader cannot see the mark
|
|
49
|
+
|
|
50
|
+
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
51
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
52
|
+
import { agentName } from "./messages.mjs";
|
|
53
|
+
import { secretShape } from "./references.mjs";
|
|
54
|
+
import { isoLocal } from "./messages.mjs";
|
|
55
|
+
import { EvdaError } from "./errors.mjs";
|
|
56
|
+
|
|
57
|
+
export const ACK_FILE = ".acknowledged.json";
|
|
58
|
+
export const NOTE_MAX = 400;
|
|
59
|
+
export const BY_MAX = 60;
|
|
60
|
+
export const SECTION_MAX = 200;
|
|
61
|
+
|
|
62
|
+
export const ACK_FALSIFY_MODES = ["overwrite", "any-section", "strict-read", "no-secrets", "hide"];
|
|
63
|
+
const falsified = (mode) => (process.env.EVDA_ACK_FALSIFY ?? "").split(",").map((s) => s.trim()).includes(mode);
|
|
64
|
+
|
|
65
|
+
const refuse = (message, code) => new EvdaError(`REFUSED: ${message} Nothing was written.`, { code, exit: 2 });
|
|
66
|
+
|
|
67
|
+
// The entry a mark is about: a path, and the section heading inside it when there is one.
|
|
68
|
+
export function entryKey(path, section = null) {
|
|
69
|
+
const p = String(path).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
70
|
+
return section ? `${p}#${section}` : p;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A section as it is keyed: the heading's text, with its leading #s and surrounding space gone, so
|
|
74
|
+
// "## 14:03", "##14:03" and "14:03" are one section.
|
|
75
|
+
export function normalizeSection(section) {
|
|
76
|
+
return String(section).replace(/^\s*#+\s*/, "").trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ackPath(evda) {
|
|
80
|
+
return join(evda.dir, ACK_FILE);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Every mark this record holds, as { key: { by, at, note } }. Missing, unreadable, or not a JSON
|
|
84
|
+
// object: empty, so a reader with no file acts like the first reader rather than throwing.
|
|
85
|
+
export function readAcknowledged(evda) {
|
|
86
|
+
const p = ackPath(evda);
|
|
87
|
+
if (!existsSync(p)) return {};
|
|
88
|
+
let v;
|
|
89
|
+
try { v = JSON.parse(readFileSync(p, "utf8")); } catch (e) { if (falsified("strict-read")) throw e; return {}; }
|
|
90
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) return {};
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [k, m] of Object.entries(v)) {
|
|
93
|
+
if (m === null || typeof m !== "object" || Array.isArray(m)) continue;
|
|
94
|
+
if (typeof m.at !== "string" || typeof m.note !== "string") continue;
|
|
95
|
+
out[k] = { by: typeof m.by === "string" ? m.by : null, at: m.at, note: m.note };
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeAcknowledged(evda, all) {
|
|
101
|
+
const p = ackPath(evda);
|
|
102
|
+
const keys = Object.keys(all).sort();
|
|
103
|
+
const ordered = Object.fromEntries(keys.map((k) => [k, all[k]]));
|
|
104
|
+
const t = `${p}.tmp-${process.pid}`;
|
|
105
|
+
writeFileSync(t, JSON.stringify(ordered, null, 2) + "\n");
|
|
106
|
+
renameSync(t, p);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// What is already acknowledged in one file, newest-written last: [{ section, by, at, note }]. A mark
|
|
110
|
+
// on the file as a whole comes back with section null.
|
|
111
|
+
export function acknowledgementsFor(path, all) {
|
|
112
|
+
if (falsified("hide")) return [];
|
|
113
|
+
const p = String(path).replace(/\\/g, "/");
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const [key, m] of Object.entries(all)) {
|
|
116
|
+
const hash = key.indexOf("#");
|
|
117
|
+
const keyPath = hash === -1 ? key : key.slice(0, hash);
|
|
118
|
+
if (keyPath !== p) continue;
|
|
119
|
+
out.push({ section: hash === -1 ? null : key.slice(hash + 1), by: m.by, at: m.at, note: m.note });
|
|
120
|
+
}
|
|
121
|
+
out.sort((a, b) => String(a.section ?? "").localeCompare(String(b.section ?? "")));
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Is this entry already acknowledged? The read every other reader does before it says anything.
|
|
126
|
+
export function isAcknowledged(evda, { path, section = null } = {}, all = null) {
|
|
127
|
+
const marks = all ?? readAcknowledged(evda);
|
|
128
|
+
const key = entryKey(path, section === null || section === undefined ? null : normalizeSection(section));
|
|
129
|
+
return marks[key] ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function oneLine(value, name, max) {
|
|
133
|
+
if (typeof value !== "string" || value.trim() === "") throw refuse(`evda_acknowledge needs "${name}".`, "EMPTY");
|
|
134
|
+
const v = value.trim();
|
|
135
|
+
if (/[\r\n]/.test(v)) throw refuse(`evda_acknowledge "${name}" is one line.`, "LINES");
|
|
136
|
+
if (v.length > max) throw refuse(`evda_acknowledge "${name}" is ${v.length} characters; at most ${max}.`, "TOO_LONG");
|
|
137
|
+
return v;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function refuseSecrets(value, name) {
|
|
141
|
+
if (falsified("no-secrets")) return;
|
|
142
|
+
const shape = secretShape(value);
|
|
143
|
+
if (shape) throw refuse(`evda_acknowledge "${name}" holds ${shape}. A password, key or token is never written into the record.`, "SECRET");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Does `section` exist as a markdown heading in `text`?
|
|
147
|
+
export function hasHeading(text, section) {
|
|
148
|
+
const want = normalizeSection(section);
|
|
149
|
+
return text.split("\n").some((line) => /^#{1,6}\s+/.test(line) && normalizeSection(line) === want);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Record that this reader acted on one weighty entry. Returns
|
|
153
|
+
// { key, path, section, first, by, at, note, already }.
|
|
154
|
+
//
|
|
155
|
+
// first true : nothing was marked before, this reader is the one that acts, and the mark is written.
|
|
156
|
+
// first false : another reader already did. Nothing is written, `already` says who, when and what
|
|
157
|
+
// they did, and this reader says nothing about the entry.
|
|
158
|
+
export function acknowledge(evda, args = {}, { now = new Date() } = {}) {
|
|
159
|
+
if (typeof args.path !== "string" || args.path.trim() === "") throw refuse('evda_acknowledge needs "path", the entry in the record you acted on.', "NO_PATH");
|
|
160
|
+
const abs = resolve(evda.dir, args.path.trim());
|
|
161
|
+
if (abs !== evda.dir && !abs.startsWith(evda.dir + sep)) throw refuse(`${args.path} is outside the record.`, "OUTSIDE");
|
|
162
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) throw refuse(`nothing is at ${args.path} in the record, so there is no entry to acknowledge.`, "MISSING");
|
|
163
|
+
const path = relative(evda.dir, abs).split(sep).join("/");
|
|
164
|
+
|
|
165
|
+
let section = null;
|
|
166
|
+
if (args.section !== undefined && args.section !== null && String(args.section).trim() !== "") {
|
|
167
|
+
section = normalizeSection(oneLine(String(args.section), "section", SECTION_MAX));
|
|
168
|
+
if (!falsified("any-section") && !hasHeading(readFileSync(abs, "utf8"), section)) {
|
|
169
|
+
throw refuse(`${path} has no heading "${section}". Acknowledge the heading as it is written in the file, or leave section out to acknowledge the whole entry.`, "NO_SECTION");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const key = entryKey(path, section);
|
|
174
|
+
const all = readAcknowledged(evda);
|
|
175
|
+
if (all[key] && !falsified("overwrite")) {
|
|
176
|
+
return { key, path, section, first: false, by: all[key].by, at: all[key].at, note: all[key].note, already: all[key] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const note = oneLine(args.note, "note", NOTE_MAX);
|
|
180
|
+
refuseSecrets(note, "note");
|
|
181
|
+
const by = args.by === undefined || args.by === null ? agentName(evda) : oneLine(String(args.by), "by", BY_MAX);
|
|
182
|
+
refuseSecrets(by, "by");
|
|
183
|
+
const at = isoLocal(now);
|
|
184
|
+
all[key] = { by, at, note };
|
|
185
|
+
writeAcknowledged(evda, all);
|
|
186
|
+
return { key, path, section, first: true, by, at, note, already: null };
|
|
187
|
+
}
|