create-evda 0.1.7 → 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 +45 -0
- package/README.md +11 -2
- package/bin/evda.mjs +217 -6
- package/create-evda.mjs +22 -6
- package/dist/pairing.mjs +5494 -0
- package/hooks/evda-day-writer-guard.mjs +7 -0
- package/lib/acknowledge.mjs +187 -0
- package/lib/browse.mjs +354 -0
- package/lib/circle-plan.mjs +24 -1
- 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 +931 -67
- package/lib/measurement-log.mjs +246 -0
- package/lib/messages.mjs +5 -2
- package/lib/pairing.mjs +403 -0
- 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/setup.mjs +41 -0
- 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 +4 -2
- package/skills/evda/SKILL.md +15 -2
- package/templates/AGENTS.md +25 -4
- package/templates/evda-digest.md +3 -1
package/lib/parity.mjs
CHANGED
|
@@ -47,7 +47,17 @@
|
|
|
47
47
|
// that path's newest Change, not the global one: a file written after the source
|
|
48
48
|
// last sent THIS path is LAG. Measured 2026-09-27 on Hestia: the two DIFFERS files'
|
|
49
49
|
// newest Changes were 06:20:41Z, their record mtime 06:48:41Z, the global newest
|
|
50
|
-
// Change 06:53:08Z.
|
|
50
|
+
// Change 06:53:08Z.
|
|
51
|
+
// LAG, first a path the source has never sent has no mark of its own, and the global one says
|
|
52
|
+
// appearance nothing about it: a file written on this device reaches the source as an iCloud
|
|
53
|
+
// placeholder, which the source skips, while it mints other paths and moves
|
|
54
|
+
// `settled` past it. Measured 2026-10-04 on Hestia (HQ-2169): Overnight's daily record
|
|
55
|
+
// written here 06:02:12Z, the laptop's 07:09:56Z tick logged it "in iCloud only" and
|
|
56
|
+
// minted four other paths, the newest 07:10:27.816Z, and the file read MISSING. So a
|
|
57
|
+
// MISSING path with no mark, when the marks were given, is judged against its own
|
|
58
|
+
// write time: LAG while the source's newest Change is within `staleAfterMs` of it,
|
|
59
|
+
// MISSING past that. An unmarked DIFFERS keeps the global line: the shadow holds the
|
|
60
|
+
// path, so its bytes came from somewhere the marks do not explain.
|
|
51
61
|
// The per-path excuse is bounded: once the source's newest Change is more than
|
|
52
62
|
// `staleAfterMs` past the file's mtime, the source has been scanning (the laptop's
|
|
53
63
|
// daily full pass reads every file) and still has not sent it, so it fails as what
|
|
@@ -63,7 +73,7 @@ import { createHash } from "node:crypto";
|
|
|
63
73
|
import { readFileSync, statSync } from "node:fs";
|
|
64
74
|
import { join } from "node:path";
|
|
65
75
|
import { circleForPath } from "./filechanges.mjs";
|
|
66
|
-
import { excluder, walk } from "./sync-rules.mjs";
|
|
76
|
+
import { excluder, fileClass, walk } from "./sync-rules.mjs";
|
|
67
77
|
|
|
68
78
|
const nfc = (rel) => rel.normalize("NFC");
|
|
69
79
|
// A path iCloud Drive does not sync, so it cannot be compared across devices while iCloud carries
|
|
@@ -88,21 +98,36 @@ const finite = (n) => typeof n === "number" && Number.isFinite(n);
|
|
|
88
98
|
|
|
89
99
|
// The rule for one DIFFERS or MISSING record file, pure so it can be property-tested:
|
|
90
100
|
// "FAIL" it is the difference it looks like
|
|
91
|
-
// "LAG" the source had not seen these bytes when it last sent this path
|
|
92
|
-
//
|
|
101
|
+
// "LAG" the source had not seen these bytes when it last sent this path, or it never sent this
|
|
102
|
+
// path and its newest Change is within `staleAfterMs` of the write; and the source was
|
|
103
|
+
// heard from within `staleAfterMs`
|
|
93
104
|
// "STALE" it would be LAG, but the source has been silent for longer than `staleAfterMs`
|
|
94
105
|
// "BOUND" it would be LAG by its path's mark, but the source has sent other Changes for longer
|
|
95
106
|
// than `staleAfterMs` since the file was written, so it is a difference after all
|
|
96
|
-
|
|
107
|
+
//
|
|
108
|
+
// `neverSent` is true only when the caller holds the source's per-path marks and none is for this
|
|
109
|
+
// path, and the file is MISSING from the shadow. Without it, an unmarked path keeps the global line.
|
|
110
|
+
export function lateVerdict({ mtimeMs, settled, pathSettled, neverSent = false, now, staleAfterMs = STALE_AFTER_MS }) {
|
|
97
111
|
if (!finite(settled)) return "FAIL";
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
if (finite(pathSettled)) {
|
|
113
|
+
if (!(mtimeMs > pathSettled)) return "FAIL";
|
|
114
|
+
} else if (neverSent) {
|
|
115
|
+
// The source's newest Change has been more than the window past the write without this path.
|
|
116
|
+
if (settled - mtimeMs > staleAfterMs) return "FAIL";
|
|
117
|
+
} else if (!(mtimeMs > settled)) {
|
|
118
|
+
return "FAIL";
|
|
119
|
+
}
|
|
100
120
|
if (now - settled > staleAfterMs) return "STALE";
|
|
101
121
|
if (settled - mtimeMs > staleAfterMs) return "BOUND";
|
|
102
122
|
return "LAG";
|
|
103
123
|
}
|
|
104
124
|
|
|
105
|
-
|
|
125
|
+
// `carry` is the manifest's `sync.carry` (sync-rules `carryList`). A file whose bytes do not travel
|
|
126
|
+
// (a reference, HQ-2017) is compared on neither side and never read: counted as REFERENCED.
|
|
127
|
+
export function parity({ source, shadow, index, holds, never = [], exclude = excluder(), settled, pathSettled, staleAfterMs = STALE_AFTER_MS, now = Date.now(), carry = [] }) {
|
|
128
|
+
const classOf = fileClass(carry);
|
|
129
|
+
let referenced = 0;
|
|
130
|
+
const references = new Set();
|
|
106
131
|
const marked = finite(settled);
|
|
107
132
|
const lag = [];
|
|
108
133
|
const hours = Math.round(staleAfterMs / 3600000);
|
|
@@ -111,9 +136,12 @@ export function parity({ source, shadow, index, holds, never = [], exclude = exc
|
|
|
111
136
|
// A DIFFERS or MISSING on a record file written after the source last sent it: LAG, or STALE.
|
|
112
137
|
const late = (want, code, why) => {
|
|
113
138
|
const mark = marked ? marks.get(nfc(want.rel)) : undefined;
|
|
114
|
-
|
|
139
|
+
// HQ-2169: a MISSING path the source has never sent, when its marks were given, is judged against
|
|
140
|
+
// its own write time rather than another path's Change.
|
|
141
|
+
const neverSent = marked && pathSettled != null && code === "MISSING" && !finite(mark);
|
|
142
|
+
const v = lateVerdict({ mtimeMs: want.mtimeMs, settled, pathSettled: mark, neverSent, now, staleAfterMs });
|
|
115
143
|
if (v === "LAG") { lag.push({ rel: want.rel, circle: want.circle, was: code }); return; }
|
|
116
|
-
if (v === "STALE") { problems.push({ code: "STALE", rel: want.rel, circle: want.circle, why: `${why}, written
|
|
144
|
+
if (v === "STALE") { problems.push({ code: "STALE", rel: want.rel, circle: want.circle, why: `${why}, written ${new Date(want.mtimeMs).toISOString()}, and the source's newest Change, ${new Date(settled).toISOString()}, is older than ${hours} h` }); return; }
|
|
117
145
|
if (v === "BOUND") { problems.push({ code, rel: want.rel, circle: want.circle, why: `${why}; written ${new Date(want.mtimeMs).toISOString()}, after the source last sent this path (${new Date(mark).toISOString()}), but the source has sent Changes for more than ${hours} h since without it` }); return; }
|
|
118
146
|
problems.push({ code, rel: want.rel, circle: want.circle, why });
|
|
119
147
|
};
|
|
@@ -129,6 +157,9 @@ export function parity({ source, shadow, index, holds, never = [], exclude = exc
|
|
|
129
157
|
for (const e of walk(source, { exclude })) {
|
|
130
158
|
if (e.kind === "excluded") { excluded += 1; continue; }
|
|
131
159
|
if (notCarried(e.rel)) { notCarriedCount += 1; continue; }
|
|
160
|
+
// A placeholder is judged by the file it stands for: `.x.m4a.icloud` is media, `.x.md.icloud` is not.
|
|
161
|
+
const named = e.kind === "placeholder" ? e.rel.replace(/(^|\/)\.([^/]+)\.icloud$/, "$1$2") : e.rel;
|
|
162
|
+
if (e.kind !== "symlink" && classOf(named) === "reference") { referenced += 1; references.add(nfc(e.rel)); continue; }
|
|
132
163
|
const c = circleOf(index, e.rel);
|
|
133
164
|
if (!c || !held.has(c.name)) { notHeld += 1; continue; }
|
|
134
165
|
if (e.kind !== "file") {
|
|
@@ -144,6 +175,9 @@ export function parity({ source, shadow, index, holds, never = [], exclude = exc
|
|
|
144
175
|
for (const e of walk(shadow, { exclude: excluder() })) {
|
|
145
176
|
if (e.kind === "excluded") continue;
|
|
146
177
|
if (notCarried(e.rel)) { notCarriedCount += 1; continue; }
|
|
178
|
+
// A reference the record holds may have bytes in a shadow from before HQ-2017; they are not
|
|
179
|
+
// compared. A reference-class file the record does not hold is still EXTRA.
|
|
180
|
+
if (references.has(nfc(e.rel))) continue;
|
|
147
181
|
shadowFiles += 1;
|
|
148
182
|
const c = circleOf(index, e.rel);
|
|
149
183
|
if (c && forbidden.has(c.name)) { problems.push({ code: "FORBIDDEN", rel: e.rel, circle: c.name, why: `this device must never hold circle ${c.name}` }); continue; }
|
|
@@ -163,18 +197,18 @@ export function parity({ source, shadow, index, holds, never = [], exclude = exc
|
|
|
163
197
|
|
|
164
198
|
const checked = Object.values(perCircle).reduce((a, b) => a + b, 0);
|
|
165
199
|
if (checked === 0) problems.push({ code: "CHECKED_0", rel: "", why: "nothing was compared; zero is a broken discovery step, not a pass" });
|
|
166
|
-
return { ok: problems.length === 0, checked, perCircle, expected: expected.size, shadowFiles, notHeld, excluded, notCarried: notCarriedCount, settled: marked ? settled : null, lag, problems };
|
|
200
|
+
return { ok: problems.length === 0, checked, perCircle, expected: expected.size, shadowFiles, notHeld, excluded, notCarried: notCarriedCount, referenced, settled: marked ? settled : null, lag, problems };
|
|
167
201
|
}
|
|
168
202
|
|
|
169
203
|
// The lines a person or a log reads. The first line always says what was examined.
|
|
170
204
|
export function formatParity(r, { limit = 20 } = {}) {
|
|
171
205
|
const lines = [`CHECKED ${r.checked} files: ${Object.entries(r.perCircle).map(([n, k]) => `${n} ${k}`).join(", ")}`];
|
|
172
|
-
lines.push(` record side: ${r.expected} expected, ${r.notHeld} in circles this device does not hold, ${r.excluded} excluded${r.notCarried ? `, NOT_CARRIED ${r.notCarried} (paths iCloud does not sync, both sides)` : ""}; shadow side: ${r.shadowFiles} files${r.settled ? `; source's newest Change ${new Date(r.settled).toISOString()}` : ""}`);
|
|
206
|
+
lines.push(` record side: ${r.expected} expected, ${r.notHeld} in circles this device does not hold, ${r.excluded} excluded${r.referenced ? `, REFERENCED ${r.referenced} (named, not carried)` : ""}${r.notCarried ? `, NOT_CARRIED ${r.notCarried} (paths iCloud does not sync, both sides)` : ""}; shadow side: ${r.shadowFiles} files${r.settled ? `; source's newest Change ${new Date(r.settled).toISOString()}` : ""}`);
|
|
173
207
|
const byCode = {};
|
|
174
208
|
for (const p of r.problems) byCode[p.code] = (byCode[p.code] ?? 0) + 1;
|
|
175
209
|
const lag = r.lag ?? [];
|
|
176
210
|
if (lag.length) {
|
|
177
|
-
lines.push(` LAG ${lag.length} (written on this device after the source last sent that path, or
|
|
211
|
+
lines.push(` LAG ${lag.length} (written on this device after the source last sent that path, or, for a path it never sent, within ${Math.round(STALE_AFTER_MS / 3600000)} h of its newest Change, ${new Date(r.settled).toISOString()}; the source has not seen them yet)`);
|
|
178
212
|
for (const l of lag.slice(0, limit)) lines.push(` LAG ${l.rel} [${l.circle}] (${l.was})`);
|
|
179
213
|
}
|
|
180
214
|
for (const p of r.problems.slice(0, limit)) lines.push(`FAIL ${p.code} ${p.rel}${p.circle ? ` [${p.circle}]` : ""}: ${p.why}`);
|
package/lib/qr.mjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// A QR code for the pairing link, drawn here (HQ-2040), so asking Claude to pair a phone can hand
|
|
2
|
+
// the person something to scan. No network QR service sees the link (the code in it is a key until
|
|
3
|
+
// it is used), and the package keeps its zero runtime dependencies (test/deps.test.mjs).
|
|
4
|
+
//
|
|
5
|
+
// Byte mode, versions 1 to 40, error correction L, M, Q or H, following ISO/IEC 18004 the way
|
|
6
|
+
// Project Nayuki's reference encoder lays it out. test/qr.test.mjs holds the matrices against a
|
|
7
|
+
// second, independent encoder's output, module for module.
|
|
8
|
+
//
|
|
9
|
+
// Out: the module matrix, a PNG (8-bit grey, deflated by node:zlib), and the same code as text in
|
|
10
|
+
// half blocks, which a chat can show in a code block when it cannot show an image.
|
|
11
|
+
|
|
12
|
+
import { deflateSync } from "node:zlib";
|
|
13
|
+
|
|
14
|
+
const ECL = {
|
|
15
|
+
L: { bits: 1, i: 0 },
|
|
16
|
+
M: { bits: 0, i: 1 },
|
|
17
|
+
Q: { bits: 3, i: 2 },
|
|
18
|
+
H: { bits: 2, i: 3 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Error correction codewords per block, and the number of blocks, by level then version (index 0 unused).
|
|
22
|
+
const ECC_PER_BLOCK = [
|
|
23
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
24
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
25
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
26
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
27
|
+
];
|
|
28
|
+
const NUM_BLOCKS = [
|
|
29
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
30
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
31
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
32
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const bit = (x, i) => ((x >>> i) & 1) !== 0;
|
|
36
|
+
|
|
37
|
+
function rawDataModules(ver) {
|
|
38
|
+
let n = (16 * ver + 128) * ver + 64;
|
|
39
|
+
if (ver >= 2) {
|
|
40
|
+
const align = Math.floor(ver / 7) + 2;
|
|
41
|
+
n -= (25 * align - 10) * align - 55;
|
|
42
|
+
if (ver >= 7) n -= 36;
|
|
43
|
+
}
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const dataCodewords = (ver, e) => Math.floor(rawDataModules(ver) / 8) - ECC_PER_BLOCK[e.i][ver] * NUM_BLOCKS[e.i][ver];
|
|
48
|
+
|
|
49
|
+
function gfMul(x, y) {
|
|
50
|
+
let z = 0;
|
|
51
|
+
for (let i = 7; i >= 0; i--) {
|
|
52
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
53
|
+
z ^= ((y >>> i) & 1) * x;
|
|
54
|
+
}
|
|
55
|
+
return z;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rsDivisor(degree) {
|
|
59
|
+
const r = new Array(degree).fill(0);
|
|
60
|
+
r[degree - 1] = 1;
|
|
61
|
+
let root = 1;
|
|
62
|
+
for (let i = 0; i < degree; i++) {
|
|
63
|
+
for (let j = 0; j < r.length; j++) {
|
|
64
|
+
r[j] = gfMul(r[j], root);
|
|
65
|
+
if (j + 1 < r.length) r[j] ^= r[j + 1];
|
|
66
|
+
}
|
|
67
|
+
root = gfMul(root, 0x02);
|
|
68
|
+
}
|
|
69
|
+
return r;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rsRemainder(data, divisor) {
|
|
73
|
+
const r = divisor.map(() => 0);
|
|
74
|
+
for (const b of data) {
|
|
75
|
+
const factor = b ^ r.shift();
|
|
76
|
+
r.push(0);
|
|
77
|
+
divisor.forEach((coef, i) => { r[i] ^= gfMul(coef, factor); });
|
|
78
|
+
}
|
|
79
|
+
return r;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function alignmentPositions(ver) {
|
|
83
|
+
if (ver === 1) return [];
|
|
84
|
+
const size = ver * 4 + 17;
|
|
85
|
+
const n = Math.floor(ver / 7) + 2;
|
|
86
|
+
const step = Math.floor((ver * 8 + n * 3 + 5) / (n * 4 - 4)) * 2;
|
|
87
|
+
const out = [6];
|
|
88
|
+
for (let pos = size - 7; out.length < n; pos -= step) out.splice(1, 0, pos);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const MASKS = [
|
|
93
|
+
(x, y) => (x + y) % 2 === 0,
|
|
94
|
+
(x, y) => y % 2 === 0,
|
|
95
|
+
(x) => x % 3 === 0,
|
|
96
|
+
(x, y) => (x + y) % 3 === 0,
|
|
97
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
|
|
98
|
+
(x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
99
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
100
|
+
(x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Encode `text` (UTF-8, byte mode) as a QR code. Returns `{ version, ecl, mask, size, modules }`,
|
|
105
|
+
* with `modules[y][x]` true for a dark module. Picks the smallest version that fits; picks the mask
|
|
106
|
+
* by the standard's penalty unless `mask` (0 to 7) is given. Throws when the text does not fit.
|
|
107
|
+
*/
|
|
108
|
+
export function encodeQr(text, { ecl = "M", mask = null, minVersion = 1 } = {}) {
|
|
109
|
+
const e = ECL[ecl];
|
|
110
|
+
if (!e) throw new Error(`QR error correction is L, M, Q or H, not ${ecl}.`);
|
|
111
|
+
if (mask !== null && !(Number.isInteger(mask) && mask >= 0 && mask <= 7)) throw new Error("A QR mask is 0 to 7.");
|
|
112
|
+
const bytes = [...Buffer.from(String(text), "utf8")];
|
|
113
|
+
let ver = Math.max(1, minVersion);
|
|
114
|
+
for (; ver <= 40; ver++) {
|
|
115
|
+
const countBits = ver <= 9 ? 8 : 16;
|
|
116
|
+
if (bytes.length < 2 ** countBits && 4 + countBits + bytes.length * 8 <= dataCodewords(ver, e) * 8) break;
|
|
117
|
+
}
|
|
118
|
+
if (ver > 40) throw new Error("That is too long for a QR code.");
|
|
119
|
+
|
|
120
|
+
// The data bits: mode, length, bytes, terminator, then pad to the capacity.
|
|
121
|
+
const bits = [];
|
|
122
|
+
const put = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); };
|
|
123
|
+
put(0b0100, 4);
|
|
124
|
+
put(bytes.length, ver <= 9 ? 8 : 16);
|
|
125
|
+
for (const b of bytes) put(b, 8);
|
|
126
|
+
const capacity = dataCodewords(ver, e) * 8;
|
|
127
|
+
put(0, Math.min(4, capacity - bits.length));
|
|
128
|
+
put(0, (8 - (bits.length % 8)) % 8);
|
|
129
|
+
for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) put(pad, 8);
|
|
130
|
+
const data = [];
|
|
131
|
+
for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0));
|
|
132
|
+
|
|
133
|
+
// Split into blocks, add error correction to each, interleave.
|
|
134
|
+
const numBlocks = NUM_BLOCKS[e.i][ver];
|
|
135
|
+
const eccLen = ECC_PER_BLOCK[e.i][ver];
|
|
136
|
+
const rawCodewords = Math.floor(rawDataModules(ver) / 8);
|
|
137
|
+
const numShort = numBlocks - (rawCodewords % numBlocks);
|
|
138
|
+
const shortLen = Math.floor(rawCodewords / numBlocks);
|
|
139
|
+
const divisor = rsDivisor(eccLen);
|
|
140
|
+
const blocks = [];
|
|
141
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
142
|
+
const dat = data.slice(k, k + shortLen - eccLen + (i < numShort ? 0 : 1));
|
|
143
|
+
k += dat.length;
|
|
144
|
+
const ecc = rsRemainder(dat, divisor);
|
|
145
|
+
if (i < numShort) dat.push(0);
|
|
146
|
+
blocks.push(dat.concat(ecc));
|
|
147
|
+
}
|
|
148
|
+
const codewords = [];
|
|
149
|
+
for (let i = 0; i < blocks[0].length; i++) {
|
|
150
|
+
blocks.forEach((b, j) => {
|
|
151
|
+
if (i !== shortLen - eccLen || j >= numShort) codewords.push(b[i]);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const size = ver * 4 + 17;
|
|
156
|
+
const modules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
157
|
+
const fn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
158
|
+
const set = (x, y, dark) => { modules[y][x] = dark; fn[y][x] = true; };
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
|
161
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
|
|
162
|
+
for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
|
|
163
|
+
const d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
164
|
+
const x = cx + dx;
|
|
165
|
+
const y = cy + dy;
|
|
166
|
+
if (x >= 0 && x < size && y >= 0 && y < size) set(x, y, d !== 2 && d !== 4);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const al = alignmentPositions(ver);
|
|
170
|
+
for (let i = 0; i < al.length; i++) for (let j = 0; j < al.length; j++) {
|
|
171
|
+
if ((i === 0 && j === 0) || (i === 0 && j === al.length - 1) || (i === al.length - 1 && j === 0)) continue;
|
|
172
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(al[i] + dx, al[j] + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
173
|
+
}
|
|
174
|
+
const drawFormat = (m) => {
|
|
175
|
+
const d = (e.bits << 3) | m;
|
|
176
|
+
let rem = d;
|
|
177
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
178
|
+
const b = ((d << 10) | rem) ^ 0x5412;
|
|
179
|
+
for (let i = 0; i <= 5; i++) set(8, i, bit(b, i));
|
|
180
|
+
set(8, 7, bit(b, 6));
|
|
181
|
+
set(8, 8, bit(b, 7));
|
|
182
|
+
set(7, 8, bit(b, 8));
|
|
183
|
+
for (let i = 9; i < 15; i++) set(14 - i, 8, bit(b, i));
|
|
184
|
+
for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(b, i));
|
|
185
|
+
for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(b, i));
|
|
186
|
+
set(8, size - 8, true);
|
|
187
|
+
};
|
|
188
|
+
drawFormat(0);
|
|
189
|
+
if (ver >= 7) {
|
|
190
|
+
let rem = ver;
|
|
191
|
+
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
|
|
192
|
+
const b = (ver << 12) | rem;
|
|
193
|
+
for (let i = 0; i < 18; i++) {
|
|
194
|
+
const a = size - 11 + (i % 3);
|
|
195
|
+
const c = Math.floor(i / 3);
|
|
196
|
+
set(a, c, bit(b, i));
|
|
197
|
+
set(c, a, bit(b, i));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The codewords, in the zigzag.
|
|
202
|
+
let n = 0;
|
|
203
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
204
|
+
if (right === 6) right = 5;
|
|
205
|
+
for (let v = 0; v < size; v++) for (let j = 0; j < 2; j++) {
|
|
206
|
+
const x = right - j;
|
|
207
|
+
const y = ((right + 1) & 2) === 0 ? size - 1 - v : v;
|
|
208
|
+
if (!fn[y][x] && n < codewords.length * 8) {
|
|
209
|
+
modules[y][x] = bit(codewords[n >>> 3], 7 - (n & 7));
|
|
210
|
+
n++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const applyMask = (m) => {
|
|
216
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (!fn[y][x] && MASKS[m](x, y)) modules[y][x] = !modules[y][x];
|
|
217
|
+
};
|
|
218
|
+
let chosen = mask;
|
|
219
|
+
if (chosen === null) {
|
|
220
|
+
let best = Infinity;
|
|
221
|
+
for (let m = 0; m < 8; m++) {
|
|
222
|
+
applyMask(m);
|
|
223
|
+
drawFormat(m);
|
|
224
|
+
const p = penalty(modules);
|
|
225
|
+
if (p < best) { best = p; chosen = m; }
|
|
226
|
+
applyMask(m);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
applyMask(chosen);
|
|
230
|
+
drawFormat(chosen);
|
|
231
|
+
return { version: ver, ecl, mask: chosen, size, modules };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// The standard's four penalty rules; the lowest total picks the mask.
|
|
235
|
+
function penalty(m) {
|
|
236
|
+
const size = m.length;
|
|
237
|
+
let score = 0;
|
|
238
|
+
const lines = [];
|
|
239
|
+
for (let y = 0; y < size; y++) lines.push(m[y]);
|
|
240
|
+
for (let x = 0; x < size; x++) lines.push(m.map((row) => row[x]));
|
|
241
|
+
const finder = [true, false, true, true, true, false, true];
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
for (let i = 0, run = 1; i < size; i++) {
|
|
244
|
+
if (i + 1 < size && line[i + 1] === line[i]) { run++; continue; }
|
|
245
|
+
if (run >= 5) score += 3 + (run - 5);
|
|
246
|
+
run = 1;
|
|
247
|
+
}
|
|
248
|
+
for (let i = 0; i + 7 <= size; i++) {
|
|
249
|
+
if (!finder.every((v, k) => line[i + k] === v)) continue;
|
|
250
|
+
const lightBefore = i >= 4 && [1, 2, 3, 4].every((k) => !line[i - k]);
|
|
251
|
+
const lightAfter = i + 11 <= size && [0, 1, 2, 3].every((k) => !line[i + 7 + k]);
|
|
252
|
+
if (lightBefore || lightAfter) score += 40;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (let y = 0; y + 1 < size; y++) for (let x = 0; x + 1 < size; x++) {
|
|
256
|
+
const c = m[y][x];
|
|
257
|
+
if (c === m[y][x + 1] && c === m[y + 1][x] && c === m[y + 1][x + 1]) score += 3;
|
|
258
|
+
}
|
|
259
|
+
const dark = m.reduce((a, row) => a + row.filter(Boolean).length, 0);
|
|
260
|
+
score += Math.floor(Math.abs(dark * 20 - size * size * 10) / (size * size)) * 10;
|
|
261
|
+
return score;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
265
|
+
let c = n;
|
|
266
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
267
|
+
return c >>> 0;
|
|
268
|
+
});
|
|
269
|
+
function crc32(buf) {
|
|
270
|
+
let c = 0xffffffff;
|
|
271
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
272
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
273
|
+
}
|
|
274
|
+
function chunk(type, data) {
|
|
275
|
+
const len = Buffer.alloc(4);
|
|
276
|
+
len.writeUInt32BE(data.length);
|
|
277
|
+
const td = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
278
|
+
const crc = Buffer.alloc(4);
|
|
279
|
+
crc.writeUInt32BE(crc32(td));
|
|
280
|
+
return Buffer.concat([len, td, crc]);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The code as a PNG: black on white, `scale` pixels a module, a quiet zone of `border` modules. */
|
|
284
|
+
export function qrPng(qr, { scale = 8, border = 4 } = {}) {
|
|
285
|
+
const px = (qr.size + border * 2) * scale;
|
|
286
|
+
const raw = Buffer.alloc((px + 1) * px, 0xff);
|
|
287
|
+
for (let y = 0; y < px; y++) {
|
|
288
|
+
raw[y * (px + 1)] = 0; // filter: none
|
|
289
|
+
const my = Math.floor(y / scale) - border;
|
|
290
|
+
for (let x = 0; x < px; x++) {
|
|
291
|
+
const mx = Math.floor(x / scale) - border;
|
|
292
|
+
if (my >= 0 && my < qr.size && mx >= 0 && mx < qr.size && qr.modules[my][mx]) raw[y * (px + 1) + 1 + x] = 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const ihdr = Buffer.alloc(13);
|
|
296
|
+
ihdr.writeUInt32BE(px, 0);
|
|
297
|
+
ihdr.writeUInt32BE(px, 4);
|
|
298
|
+
ihdr[8] = 8; // bit depth
|
|
299
|
+
ihdr[9] = 0; // greyscale
|
|
300
|
+
return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw)), chunk("IEND", Buffer.alloc(0))]);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The code as text, two rows a line in half blocks, dark modules drawn, with a quiet zone of `border`. */
|
|
304
|
+
export function qrText(qr, { border = 2 } = {}) {
|
|
305
|
+
const at = (x, y) => x >= 0 && y >= 0 && x < qr.size && y < qr.size && qr.modules[y][x];
|
|
306
|
+
const lines = [];
|
|
307
|
+
for (let y = -border; y < qr.size + border; y += 2) {
|
|
308
|
+
let line = "";
|
|
309
|
+
for (let x = -border; x < qr.size + border; x++) {
|
|
310
|
+
const top = at(x, y);
|
|
311
|
+
const bottom = at(x, y + 1);
|
|
312
|
+
line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
|
|
313
|
+
}
|
|
314
|
+
lines.push(line);
|
|
315
|
+
}
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
package/lib/record.mjs
CHANGED
|
@@ -47,6 +47,26 @@ export function headings(text) {
|
|
|
47
47
|
return text.split("\n").filter((l) => /^#{1,6} /.test(l)).map((l) => l.replace(/^#+ /, "").trim());
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// The first line of the file's most recent section: past any frontmatter block, past heading
|
|
51
|
+
// lines (those are already in `headings()`), and past every heading but the last. A day file
|
|
52
|
+
// accumulates one heading per entry through the day, so the last heading is the newest entry --
|
|
53
|
+
// a "recent" preview wants that one, not the file's opening paragraph, which for a fresh day file
|
|
54
|
+
// is create-evda's own scaffold text and was never the owner's. A file with no heading at all uses
|
|
55
|
+
// its first non-blank line, same as before.
|
|
56
|
+
export function firstLine(text) {
|
|
57
|
+
let body = text;
|
|
58
|
+
if (body.startsWith("---")) {
|
|
59
|
+
const end = body.indexOf("\n---", 3);
|
|
60
|
+
if (end !== -1) body = body.slice(end + 4);
|
|
61
|
+
}
|
|
62
|
+
const lines = body.split("\n");
|
|
63
|
+
let lastHeadingAt = -1;
|
|
64
|
+
for (let i = 0; i < lines.length; i++) if (/^#{1,6} /.test(lines[i])) lastHeadingAt = i;
|
|
65
|
+
const after = lastHeadingAt === -1 ? lines : lines.slice(lastHeadingAt + 1);
|
|
66
|
+
const line = after.find((l) => l.trim() !== "" && !/^#{1,6} /.test(l));
|
|
67
|
+
return line ? line.trim() : "";
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
export function moment(evda, { from, to = from } = {}) {
|
|
51
71
|
if (!DATE.test(from ?? "") || !DATE.test(to ?? "")) {
|
|
52
72
|
throw new EvdaError(`moment needs YYYY-MM-DD dates, got ${from}..${to}`);
|
package/lib/routines.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { mkdirSync, writeFileSync, existsSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
15
15
|
import { delimiter, join, resolve, sep } from "node:path";
|
|
16
|
-
import { homedir } from "node:os";
|
|
16
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
17
17
|
import { spawnSync } from "node:child_process";
|
|
18
18
|
import { openEvda, holderOf } from "./manifest.mjs";
|
|
19
19
|
import { serverCommand } from "./connect.mjs";
|
|
@@ -165,10 +165,42 @@ function bootout(target, env, launchctl) {
|
|
|
165
165
|
// loaded job (113 when there is none).
|
|
166
166
|
const isLoaded = (target, launchctl) => launchctl(["print", target]).status === 0;
|
|
167
167
|
|
|
168
|
+
// Is this record under a temp folder? Such a folder goes away, and a job for it runs daily on
|
|
169
|
+
// nothing (HQ-2006: the Mac app's Rust tests left two loaded in his launchd). Both the path as given
|
|
170
|
+
// and its realpath are checked, since /var and /tmp are symlinks into /private on a Mac.
|
|
171
|
+
const TEMP_ROOTS = ["/tmp", "/private/tmp", "/var/folders", "/private/var/folders"];
|
|
172
|
+
export function underTempDir(dir) {
|
|
173
|
+
const paths = [resolve(dir)];
|
|
174
|
+
try { paths.push(realpathSync(dir)); } catch {}
|
|
175
|
+
const roots = [...TEMP_ROOTS, resolve(tmpdir())];
|
|
176
|
+
try { roots.push(realpathSync(tmpdir())); } catch {}
|
|
177
|
+
return paths.some((p) => roots.some((r) => p === r || p.startsWith(r.endsWith(sep) ? r : r + sep)));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The owner's real home, from the account database rather than $HOME, which a test or a spawned
|
|
181
|
+
// process can point anywhere.
|
|
182
|
+
function accountHome() {
|
|
183
|
+
try { return userInfo().homedir; } catch { return homedir(); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const samePath = (a, b) => {
|
|
187
|
+
const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
188
|
+
return resolve(a) === resolve(b) || real(a) === real(b);
|
|
189
|
+
};
|
|
190
|
+
|
|
168
191
|
// Write the launchd job and load it. `agentsDir`, `load` and `launchctl` exist so tests never touch
|
|
169
192
|
// the real ~/Library/LaunchAgents or launchctl. A job already loaded under this label is unloaded
|
|
170
193
|
// first, so a second install replaces it rather than failing to load a duplicate.
|
|
171
|
-
|
|
194
|
+
//
|
|
195
|
+
// A record under a temp folder is refused before anything is written when the job would land in the
|
|
196
|
+
// real ~/Library/LaunchAgents or be loaded by the real launchctl, whoever the caller is. `realHome`
|
|
197
|
+
// is the seam that lets a test name a stand-in for the real home.
|
|
198
|
+
export function installLaunchd(key, dir, { env = process.env, home = env.HOME ?? homedir(), realHome = accountHome(), agentsDir = env.EVDA_LAUNCH_AGENTS ?? process.env.EVDA_LAUNCH_AGENTS ?? join(home, "Library", "LaunchAgents"), load = !noLaunchctl(env), claude = findClaude(env), launchctl } = {}) {
|
|
199
|
+
const realLaunchctl = !launchctl && !env.EVDA_LAUNCHCTL;
|
|
200
|
+
if (underTempDir(dir) && (samePath(agentsDir, join(realHome, "Library", "LaunchAgents")) || (load && realLaunchctl))) {
|
|
201
|
+
throw new EvdaError(`REFUSED: ${resolve(dir)} is in a temp folder, so no launchd job is scheduled for it (it would run daily on a folder that goes away). Nothing was written.`, { code: "TEMP_RECORD", exit: 2 });
|
|
202
|
+
}
|
|
203
|
+
launchctl ??= (a) => runLaunchctl(a, env);
|
|
172
204
|
if (process.platform !== "darwin" && load && !env.EVDA_LAUNCHCTL) throw new EvdaError("REFUSED: launchd is macOS only. Use the Claude desktop routine instead. Nothing was written.", { exit: 2 });
|
|
173
205
|
const evda = openEvda(dir);
|
|
174
206
|
const plist = launchdPlist(key, dir, { home, claude });
|
|
@@ -217,7 +249,14 @@ export function installAtSetup(keys, dir, { env = process.env, platform = proces
|
|
|
217
249
|
const r = ROUTINES[key];
|
|
218
250
|
lines.push("");
|
|
219
251
|
if (platform === "darwin") {
|
|
220
|
-
|
|
252
|
+
let done;
|
|
253
|
+
try {
|
|
254
|
+
done = installLaunchd(key, dir, { env });
|
|
255
|
+
} catch (e) {
|
|
256
|
+
if (e.code !== "TEMP_RECORD") throw e;
|
|
257
|
+
lines.push(e.message);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
221
260
|
const lead = key === "morning" ? "Organizing is on" : `${r.title} is on`;
|
|
222
261
|
lines.push(`${lead}: ${r.when.toLowerCase()} your own Claude ${key === "morning" ? "files your notes into their day and writes what belongs where" : "reads your week back to you"}, in reports/. Turn it off: evda routine ${key} ${resolve(dir)} --uninstall`);
|
|
223
262
|
lines.push(` launchd job ${done.path}: ${done.loaded}. It runs while this computer is on; asleep at that hour, it runs when it wakes.`);
|
package/lib/setup.mjs
CHANGED
|
@@ -405,3 +405,44 @@ export async function interview(opts, io, { presets = [] } = {}) {
|
|
|
405
405
|
}
|
|
406
406
|
return a;
|
|
407
407
|
}
|
|
408
|
+
|
|
409
|
+
// ─── The parts a record starts with (HQ-1925) ────────────────────────
|
|
410
|
+
//
|
|
411
|
+
// Decided by the owner: privacy is set per folder when the record is made, everything is
|
|
412
|
+
// accessible by default, and the AI reads on request. So a new record is split into two parts on
|
|
413
|
+
// day one, and a phone can be added with no terminal step:
|
|
414
|
+
// journal the day files and captures, wherever the manifest maps them
|
|
415
|
+
// private the record root: everything else, including any folder made later
|
|
416
|
+
// The computer holds both as owner. A phone is offered the journal and nothing else.
|
|
417
|
+
// Pure, like the rest of this file, so the app's window reads the same words; circle-plan.mjs
|
|
418
|
+
// `defaultParts` writes the plan.
|
|
419
|
+
|
|
420
|
+
export const JOURNAL_PART = "journal";
|
|
421
|
+
export const PRIVATE_PART = "private";
|
|
422
|
+
/** What Add a phone offers a phone. */
|
|
423
|
+
export const PHONE_PARTS = [JOURNAL_PART];
|
|
424
|
+
/** The roles whose folders are the journal. */
|
|
425
|
+
const JOURNAL_ROLES = ["record/daily", "record/captures"];
|
|
426
|
+
|
|
427
|
+
/** The circle plan for a record's default parts, from its manifest mapping and its id. */
|
|
428
|
+
export function defaultPartsPlan(mapping = {}, recordId) {
|
|
429
|
+
const roots = [...new Set(JOURNAL_ROLES.map((r) => mapping[r]).filter((p) => typeof p === "string").map((p) => p.replace(/^\/+|\/+$/g, "")).filter(Boolean))];
|
|
430
|
+
// Each starts with an empty policy: the owner's corrections about a part land there.
|
|
431
|
+
const circles = roots.length ? [{ name: JOURNAL_PART, roots, policy: [] }] : [];
|
|
432
|
+
circles.push({ name: PRIVATE_PART, roots: [""], policy: [] });
|
|
433
|
+
return { circles, devices: { computer: { atlasId: recordId, grants: { "*": "owner" } } } };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** One line for create-evda's report. */
|
|
437
|
+
export const PARTS_LINE = "parts: journal (your daily pages and captures) and private (everything else). A phone you add gets the journal.";
|
|
438
|
+
|
|
439
|
+
/** What the app shows when an adopted record has no parts, and the one button that splits it. */
|
|
440
|
+
export const PARTS_OFFER = {
|
|
441
|
+
title: "Your EVÐA is not split into parts yet",
|
|
442
|
+
lines: [
|
|
443
|
+
"A phone only gets the parts of your EVÐA you give it, so this Mac needs to know which part is your journal.",
|
|
444
|
+
"This makes two parts. Journal is your daily pages and captures, and it is what a phone gets. Private is everything else, and it stays on this Mac.",
|
|
445
|
+
"Nothing in your files moves or changes, and nothing reaches a phone until you add one.",
|
|
446
|
+
],
|
|
447
|
+
button: "Split it into journal and private",
|
|
448
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": "https://raw.githubusercontent.com/sigstore/root-signing/main/targets/trusted_root.json (Sigstore public-good instance), fetched 2026-10-04",
|
|
3
|
+
"tlogs": [
|
|
4
|
+
{
|
|
5
|
+
"baseUrl": "https://rekor.sigstore.dev",
|
|
6
|
+
"logId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=",
|
|
7
|
+
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
|
|
8
|
+
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
|
9
|
+
"validFrom": "2021-01-12T11:53:27Z"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"fulcio": {
|
|
13
|
+
"baseUrl": "https://fulcio.sigstore.dev",
|
|
14
|
+
"validFrom": "2022-04-13T20:06:15Z",
|
|
15
|
+
"chain": [
|
|
16
|
+
"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=",
|
|
17
|
+
"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/lib/skills.mjs
CHANGED
|
@@ -226,7 +226,7 @@ export function writeSkill(evda, args = {}, { now = new Date(), env = process.en
|
|
|
226
226
|
// Tools that are how a session starts or keeps itself, never a step of a process the owner asked
|
|
227
227
|
// for. And the plain read-and-file loop: search, read, write is the record's ordinary use, not a
|
|
228
228
|
// process worth a skill, so a run made only of those is never pointed at.
|
|
229
|
-
const META = new Set(["evda_operating_layer", "evda_skills", "evda_write_skill", "evda_check_update"]);
|
|
229
|
+
const META = new Set(["evda_operating_layer", "evda_skills", "evda_write_skill", "evda_check_update", "evda_acknowledge", "evda_acknowledgements"]);
|
|
230
230
|
const ORDINARY = new Set(["evda_recent", "evda_search", "evda_read", "evda_write", "evda_note", "evda_circles", "evda_presets"]);
|
|
231
231
|
export const MIN_STEPS = 3;
|
|
232
232
|
export const MAX_STEPS = 8;
|