create-evda 0.1.8 → 0.1.9
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 +37 -1
- package/README.md +8 -2
- package/bin/evda.mjs +157 -4
- package/create-evda.mjs +6 -1
- package/dist/pairing.mjs +2932 -1684
- package/hooks/evda-day-writer-guard.mjs +7 -0
- package/lib/acknowledge.mjs +187 -0
- package/lib/browse.mjs +354 -0
- package/lib/circles.mjs +5 -1
- package/lib/clients.mjs +18 -2
- package/lib/connect.mjs +8 -1
- package/lib/day-file.mjs +58 -9
- package/lib/day-writer.mjs +103 -0
- package/lib/doctor.mjs +13 -0
- package/lib/every-chat.mjs +132 -0
- package/lib/filechanges.mjs +203 -68
- package/lib/gather-apply.mjs +44 -3
- package/lib/gather.mjs +3 -1
- package/lib/keychain.mjs +144 -0
- package/lib/manifest.mjs +3 -1
- package/lib/mcp-auth.mjs +41 -1
- package/lib/mcp-http.mjs +24 -4
- package/lib/mcp.mjs +893 -73
- package/lib/measurement-log.mjs +246 -0
- package/lib/messages.mjs +5 -2
- package/lib/pairing.mjs +243 -5
- package/lib/parity.mjs +47 -13
- package/lib/qr.mjs +317 -0
- package/lib/record.mjs +20 -0
- package/lib/routines.mjs +42 -3
- 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/tools.mjs +423 -24
- package/lib/watch.mjs +160 -0
- package/package.json +1 -1
- package/skills/evda/SKILL.md +14 -0
- package/templates/AGENTS.md +25 -4
- package/templates/evda-digest.md +3 -1
package/lib/watch.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Watching a record for saves (HQ-2068), so the computer sends a saved note in seconds instead of
|
|
2
|
+
// at the next five-minute sync.
|
|
3
|
+
//
|
|
4
|
+
// macOS reports every change under a folder through FSEvents, which Node's recursive `fs.watch`
|
|
5
|
+
// uses on darwin. This turns those reports into bursts: a run of saves close together is one burst,
|
|
6
|
+
// said once it has been quiet for `quietMs`, or once it has run `maxWaitMs`, so an editor that
|
|
7
|
+
// saves every keystroke still gets a sync every few seconds. What the scan would never send starts
|
|
8
|
+
// nothing: the sync rules' exclusions (`.evda`, `.git`, `node_modules`, `.DS_Store`, an Office owner
|
|
9
|
+
// file, the plan's own paths), an iCloud placeholder, and a dataless file iCloud has not brought
|
|
10
|
+
// down (HQ-2017), or a folder (what changed inside it is reported on its own). A path that is gone
|
|
11
|
+
// counts, since its removal is news.
|
|
12
|
+
//
|
|
13
|
+
// The watcher decides only WHEN a sync runs. The sync it starts is the ordinary bounded one, with
|
|
14
|
+
// its own lock and guards (HQ-2041); the five-minute sync stays as the backstop for anything a
|
|
15
|
+
// watcher misses (a change while the app was closed, an FSEvents drop).
|
|
16
|
+
|
|
17
|
+
import { lstatSync, watch } from "node:fs";
|
|
18
|
+
import { join, sep } from "node:path";
|
|
19
|
+
import { datalessAmong, excluder, isPlaceholder } from "./sync-rules.mjs";
|
|
20
|
+
|
|
21
|
+
export const WATCH_QUIET_MS = 1_000;
|
|
22
|
+
export const WATCH_MAX_WAIT_MS = 5_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether a change at `rel` (relative to `root`) is one a sync would act on: not excluded, not an
|
|
26
|
+
* iCloud placeholder, not a link or a folder, not a dataless file. A path that no longer exists counts.
|
|
27
|
+
*/
|
|
28
|
+
export function watchCounts(root, rel, { exclude = excluder(), env = process.env } = {}) {
|
|
29
|
+
const r = String(rel).split(sep).join("/").normalize("NFC");
|
|
30
|
+
if (r === "" || exclude(r)) return false;
|
|
31
|
+
if (isPlaceholder(r.slice(r.lastIndexOf("/") + 1))) return false;
|
|
32
|
+
let st;
|
|
33
|
+
try {
|
|
34
|
+
st = lstatSync(join(root, r));
|
|
35
|
+
} catch {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (st.isSymbolicLink() || st.isDirectory()) return false;
|
|
39
|
+
if (st.isFile() && st.size > 0 && st.blocks === 0 && datalessAmong(root, [r], { env }).has(r)) return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Watch `root` and call `onBurst(paths)` once per burst of counted changes, with the paths in it.
|
|
45
|
+
* Returns `{ close() }`. `watchImpl` replaces `fs.watch` for a test that feeds it events.
|
|
46
|
+
*/
|
|
47
|
+
export function watchRecord(root, onBurst, { quietMs = WATCH_QUIET_MS, maxWaitMs = WATCH_MAX_WAIT_MS, exclude = excluder(), env = process.env, watchImpl = watch, now = () => Date.now() } = {}) {
|
|
48
|
+
let pending = new Set();
|
|
49
|
+
let first = 0;
|
|
50
|
+
let timer = null;
|
|
51
|
+
const fire = () => {
|
|
52
|
+
timer = null;
|
|
53
|
+
if (pending.size === 0) return;
|
|
54
|
+
const paths = [...pending];
|
|
55
|
+
pending = new Set();
|
|
56
|
+
first = 0;
|
|
57
|
+
onBurst(paths);
|
|
58
|
+
};
|
|
59
|
+
const watcher = watchImpl(root, { recursive: true, persistent: true }, (_event, name) => {
|
|
60
|
+
if (name == null) return;
|
|
61
|
+
const rel = String(name);
|
|
62
|
+
if (!watchCounts(root, rel, { exclude, env })) return;
|
|
63
|
+
pending.add(rel.split(sep).join("/").normalize("NFC"));
|
|
64
|
+
const t = now();
|
|
65
|
+
if (first === 0) first = t;
|
|
66
|
+
if (timer) clearTimeout(timer);
|
|
67
|
+
timer = setTimeout(fire, Math.max(0, Math.min(quietMs, first + maxWaitMs - t)));
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
close() {
|
|
71
|
+
if (timer) clearTimeout(timer);
|
|
72
|
+
timer = null;
|
|
73
|
+
watcher.close();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── A device's note, taken in within seconds (HQ-2068) ─────────────────────
|
|
79
|
+
//
|
|
80
|
+
// The file watcher sees only saves on this computer, so a note a phone sent waited for the
|
|
81
|
+
// five-minute sync. This looks at the relay instead: every `everyMs` it asks each mailbox this
|
|
82
|
+
// computer reads for its first waiting envelope, takes nothing and acknowledges nothing, and says
|
|
83
|
+
// so once per envelope it has not seen at the head of that mailbox. The sync it starts drains the
|
|
84
|
+
// box; an envelope the sync leaves where it is stays the head and starts nothing a second time.
|
|
85
|
+
|
|
86
|
+
export const RELAY_PEEK_MS = 2_000;
|
|
87
|
+
// The mailboxes are re-derived this often, so a device paired while the app was open is looked at too.
|
|
88
|
+
export const RELAY_REFRESH_MS = 60_000;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The seq of the first envelope waiting in one mailbox, or null when it is empty. Reads only:
|
|
92
|
+
* the relay's GET takes nothing off the box. Throws when the relay cannot be reached.
|
|
93
|
+
*/
|
|
94
|
+
export async function mailboxHead(box, { fetchImpl = fetch, timeoutMs = 10_000 } = {}) {
|
|
95
|
+
const res = await fetchImpl(`${box.relayUrl}/mailbox/${encodeURIComponent(box.address)}?limit=1`, {
|
|
96
|
+
headers: { authorization: `Bearer ${box.pollCredential}` },
|
|
97
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) throw new Error(`relay answered ${res.status}`);
|
|
100
|
+
const body = await res.json();
|
|
101
|
+
const first = (body.envelopes ?? [])[0];
|
|
102
|
+
if (!first) return null;
|
|
103
|
+
return String(first.seq ?? first.id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Look at the relay every `everyMs` and call `onIncoming()` once per new envelope at the head of
|
|
108
|
+
* any mailbox. `mailboxes()` gives the boxes (null when it cannot look now, tried again next time).
|
|
109
|
+
* A relay that cannot be reached is skipped until the next look; the five-minute sync covers it.
|
|
110
|
+
* Returns `{ close(), look() }`; `look` runs one look now, for a test.
|
|
111
|
+
*/
|
|
112
|
+
export function watchRelay(mailboxes, onIncoming, { everyMs = RELAY_PEEK_MS, refreshMs = RELAY_REFRESH_MS, head = mailboxHead, now = () => Date.now() } = {}) {
|
|
113
|
+
let boxes = null;
|
|
114
|
+
let derivedAt = 0;
|
|
115
|
+
const seen = new Map();
|
|
116
|
+
let running = null;
|
|
117
|
+
let closed = false;
|
|
118
|
+
const once = async () => {
|
|
119
|
+
if (boxes === null || now() - derivedAt >= refreshMs) {
|
|
120
|
+
let next = null;
|
|
121
|
+
try {
|
|
122
|
+
next = await mailboxes();
|
|
123
|
+
} catch {
|
|
124
|
+
next = null;
|
|
125
|
+
}
|
|
126
|
+
if (next !== null) {
|
|
127
|
+
boxes = next;
|
|
128
|
+
derivedAt = now();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!boxes) return false;
|
|
132
|
+
let fresh = false;
|
|
133
|
+
for (const box of boxes) {
|
|
134
|
+
let h;
|
|
135
|
+
try {
|
|
136
|
+
h = await head(box);
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const prev = seen.get(box.address);
|
|
141
|
+
seen.set(box.address, h);
|
|
142
|
+
if (h !== null && h !== prev) fresh = true;
|
|
143
|
+
}
|
|
144
|
+
if (fresh && !closed) onIncoming();
|
|
145
|
+
return fresh;
|
|
146
|
+
};
|
|
147
|
+
const look = () => {
|
|
148
|
+
if (!running) running = once().finally(() => { running = null; });
|
|
149
|
+
return running;
|
|
150
|
+
};
|
|
151
|
+
const timer = setInterval(look, everyMs);
|
|
152
|
+
timer.unref?.();
|
|
153
|
+
return {
|
|
154
|
+
look,
|
|
155
|
+
close() {
|
|
156
|
+
closed = true;
|
|
157
|
+
clearInterval(timer);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-evda",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The person layer over the substrate: scaffold and operate an EVDA, a directory that knows what it is. Plain files are the truth, the tooling is optional, and there are no runtime dependencies.",
|
|
6
6
|
"bin": {
|
package/skills/evda/SKILL.md
CHANGED
|
@@ -25,6 +25,20 @@ calls no model; you are their own AI, and everything you keep goes through the t
|
|
|
25
25
|
If the tools are not there, say so plainly and point them at `evda connect`, which prints the
|
|
26
26
|
setup for each client. Do not keep the record some other way.
|
|
27
27
|
|
|
28
|
+
## The every-chat setting
|
|
29
|
+
|
|
30
|
+
A chat that never calls an evda tool never reaches the record, and in a chat app most chats are
|
|
31
|
+
like that. The only thing every chat reads is the app's own every-chat setting, so the owner is
|
|
32
|
+
asked once per app to put one line there. When a tool result carries "One setting for every chat",
|
|
33
|
+
give them that line whole after answering what they asked, and record their answer with
|
|
34
|
+
`evda_every_chat` ("set" or "declined", `confirmed: true`), so that app never asks again. The
|
|
35
|
+
result has the line in the form their filing answer needs; for reference, the two forms are:
|
|
36
|
+
|
|
37
|
+
<!-- every-chat:begin (lib/every-chat.mjs EVERY_CHAT_LINE; test/every-chat.test.mjs fails on drift) -->
|
|
38
|
+
- Filing everything: `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.`
|
|
39
|
+
- Filing only when asked: `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.`
|
|
40
|
+
<!-- every-chat:end -->
|
|
41
|
+
|
|
28
42
|
## Keeping the record
|
|
29
43
|
|
|
30
44
|
- **File their words in the same turn they say them**, with `evda_write`: verbatim, filler, hedges
|
package/templates/AGENTS.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
## What this EVÐA is
|
|
4
4
|
|
|
5
5
|
<!-- Owner: filled in during setup, in your own words. Iris asks the first session if it is empty.
|
|
6
|
-
The agent is called Iris unless you name it something else here. Change any of it whenever.
|
|
6
|
+
The agent is called Iris unless you name it something else here. Change any of it whenever.
|
|
7
|
+
A section of your own that only makes sense with a terminal: on the line after its ## heading,
|
|
8
|
+
put the HTML comment "evda:surface terminal", and chat apps will not be sent it. -->
|
|
7
9
|
|
|
8
10
|
- **Agent name:**
|
|
9
11
|
- **Who I am:**
|
|
@@ -23,8 +25,9 @@ genuinely examines their patterns, assumptions and needs, what they need from li
|
|
|
23
25
|
without being prescribed. This file works with any AI or platform that can read files. If you cannot
|
|
24
26
|
operate the record from this file alone, the file is wrong; say so rather than guessing.
|
|
25
27
|
|
|
26
|
-
**You write exactly one section of this file: "What I have been taught", at the end
|
|
27
|
-
|
|
28
|
+
**You write exactly one section of this file: "What I have been taught", at the end** (`evda_record_correction`
|
|
29
|
+
through the MCP server, when that is the only way you can reach the file). Every other line is the
|
|
30
|
+
owner's or the installer's.
|
|
28
31
|
|
|
29
32
|
## How you think
|
|
30
33
|
|
|
@@ -165,6 +168,14 @@ They are mechanisms, never rules about their life.
|
|
|
165
168
|
conclusion about their life is fitted to a person who will not be there in ten years. Asking what the
|
|
166
169
|
view assumes stays fixed, and what happens if it does not, finds where it breaks. Some futures change
|
|
167
170
|
the one who would judge them, and no thinking settles those in advance.
|
|
171
|
+
- **How it lands tells them what it weighed.** A correction, a decision or a hard moment
|
|
172
|
+
answered in the same register as a lookup reads as ordinary, and that erases the one signal they
|
|
173
|
+
had for what registered. So take the weight on explicitly, once, in their own words back to them.
|
|
174
|
+
The same holds across a gap: a conversation that opens with no sign of the last one says the last
|
|
175
|
+
one did not register. And name what is out of pattern, meaning out of pattern for *them*, from
|
|
176
|
+
what the record shows they have said and done before, never from what people generally do. Say
|
|
177
|
+
what it departs from and when they said it, so it is checkable; an unsourced claim about their
|
|
178
|
+
state is the move this replaces, not a version of it.
|
|
168
179
|
- **Most of the good you do comes from making them get specific.** A vague claim about their life,
|
|
169
180
|
negative or positive, shapes how they think, and having to say exactly what they mean is what moves
|
|
170
181
|
them. So go for insight, not more reflection: going over a feeling without getting anywhere new
|
|
@@ -259,7 +270,9 @@ help with that too. There is almost always a way to live well in the situation t
|
|
|
259
270
|
organize it, offer the presets by name and who each suits, or none, and apply the one they pick
|
|
260
271
|
only after their yes (see "Where things go").
|
|
261
272
|
3. Read the last few days of `record/` and anything they point you at.
|
|
262
|
-
4.
|
|
273
|
+
4. **Open on where they were,** in one line, built from their own words in the record and naming the
|
|
274
|
+
thing itself. A list of what you read does not do it. If something in there is out of pattern for
|
|
275
|
+
them, say so and say what it departs from.
|
|
263
276
|
5. Run `evda check-update` once. It is throttled to once an hour, so most sessions read a
|
|
264
277
|
cached result rather than asking anything. Say nothing if it is current or was skipped for
|
|
265
278
|
being within the hour. If an update is available or the check failed, say the one line it
|
|
@@ -295,6 +308,13 @@ help with that too. There is almost always a way to live well in the situation t
|
|
|
295
308
|
motive as fact. One that has no entry is offered, never created: say in one line that you can start
|
|
296
309
|
a page for them, and start it only on their yes. A noted entry, or a part set to be read by a
|
|
297
310
|
different AI, is not yours to add to.
|
|
311
|
+
- **Whoever reaches a weighty entry first acknowledges it.** You are not the only thing reading this
|
|
312
|
+
record: a conversation, an agent that runs on its own and a scheduled run each read it, and none
|
|
313
|
+
of them sees the others' notes, so the same entry gets taken on three times or no times. When you
|
|
314
|
+
have taken one on with them, mark it (`evda_acknowledge`, or `evda acknowledge`) with one line on
|
|
315
|
+
what you did; `evda_recent` shows you the marks. An entry already marked is handled, so do not
|
|
316
|
+
raise it again unless they do. Nothing marks itself and nothing scores weight for you: which
|
|
317
|
+
entries carry weight is your read of them.
|
|
298
318
|
- **Never cite a date, quote or event that is not in the record.** Check the file before you cite it.
|
|
299
319
|
- **A written claim is a claim, not a measurement.** Before relying on an earlier note as proof,
|
|
300
320
|
check it against current reality if a cheap check exists.
|
|
@@ -334,6 +354,7 @@ help with that too. There is almost always a way to live well in the situation t
|
|
|
334
354
|
circles/ who may see which part: one folder per circle, each with an eco.yaml
|
|
335
355
|
grants/ which people or agents hold which circle
|
|
336
356
|
folder.evda.json per folder: what kind of entries live there and where they come from
|
|
357
|
+
.acknowledged.json which entries a reader already took on with them, so the next one does not
|
|
337
358
|
.evda/ derived indexes, rebuildable. Never the truth.
|
|
338
359
|
|
|
339
360
|
Roles may map to folders the owner already had; `evda.json` says where. Put an entry where its type
|
package/templates/evda-digest.md
CHANGED
|
@@ -24,6 +24,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
|
|
|
24
24
|
- Getting the thing under-delivers, for two reasons
|
|
25
25
|
- A goal that was never theirs does close to nothing when reached
|
|
26
26
|
- A view is built for a self who will not stay
|
|
27
|
+
- How it lands tells them what it weighed
|
|
27
28
|
- Most of the good you do comes from making them get specific
|
|
28
29
|
## Meet them where they are
|
|
29
30
|
## From recognition to action
|
|
@@ -55,7 +56,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
|
|
|
55
56
|
- Read this file to the end, including "What I have been taught"
|
|
56
57
|
- If any field in "What this EVÐA is" is empty, set it up first,
|
|
57
58
|
- Read the last few days of `record/` and anything they point you at
|
|
58
|
-
-
|
|
59
|
+
- Open on where they were,
|
|
59
60
|
- Run `evda check-update` once
|
|
60
61
|
## Keeping the record
|
|
61
62
|
- Act first, in the same turn, without being asked
|
|
@@ -65,6 +66,7 @@ Instruction-only digest of the generated block of AGENTS.md. Read AGENTS.md in f
|
|
|
65
66
|
- An import is what another AI said about them, not their own words
|
|
66
67
|
- Report filings in the past tense, naming the path
|
|
67
68
|
- Entries grow on their own
|
|
69
|
+
- Whoever reaches a weighty entry first acknowledges it
|
|
68
70
|
- Never cite a date, quote or event that is not in the record
|
|
69
71
|
- A written claim is a claim, not a measurement
|
|
70
72
|
- An idea that needs hours of real thinking
|