create-evda 0.1.5 → 0.1.7
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 +114 -0
- package/README.md +74 -7
- package/bin/evda.mjs +427 -9
- package/create-evda.mjs +64 -16
- package/docs/local-model.md +34 -0
- package/hooks/evda-read-guard.mjs +7 -0
- package/lib/chat-export.mjs +198 -0
- package/lib/circle-plan.mjs +277 -0
- package/lib/circles.mjs +479 -28
- package/lib/connect.mjs +46 -0
- package/lib/day-file.mjs +93 -0
- package/lib/declarations.mjs +235 -0
- package/lib/devices.mjs +202 -0
- package/lib/doctor.mjs +346 -13
- package/lib/filechanges.mjs +622 -0
- package/lib/folders.mjs +1 -1
- package/lib/gather-apply.mjs +640 -0
- package/lib/gather.mjs +858 -0
- package/lib/grants.mjs +7 -1
- package/lib/guard.mjs +132 -0
- package/lib/hooks.mjs +124 -0
- package/lib/keep-hook.mjs +185 -0
- package/lib/kinds.mjs +23 -6
- package/lib/kinds.schema.json +2 -1
- package/lib/manifest.mjs +14 -1
- package/lib/mcp-auth.mjs +370 -0
- package/lib/mcp-http.mjs +336 -0
- package/lib/mcp.mjs +456 -36
- package/lib/messages.mjs +205 -0
- package/lib/migrate-people.mjs +231 -0
- package/lib/organize.mjs +176 -32
- package/lib/parity.mjs +184 -0
- package/lib/presets.mjs +58 -7
- package/lib/prompt-hook.mjs +88 -0
- package/lib/publish.mjs +264 -0
- package/lib/readers.mjs +319 -0
- package/lib/records.mjs +52 -0
- package/lib/references.mjs +5 -51
- package/lib/render.mjs +158 -0
- package/lib/roles.mjs +106 -1
- package/lib/routines.mjs +142 -18
- package/lib/secret-shape.mjs +58 -0
- package/lib/setup.mjs +124 -4
- package/lib/skills.mjs +266 -0
- package/lib/subrecord.mjs +121 -0
- package/lib/sync-rules.mjs +93 -0
- package/lib/tools.mjs +133 -27
- package/lib/update-check.mjs +146 -0
- package/lib/work.mjs +180 -0
- package/lib/yaml.mjs +117 -42
- package/package.json +25 -15
- package/presets/journal/preset.json +9 -1
- package/presets/projects-and-areas/preset.json +16 -9
- package/skills/evda/SKILL.md +67 -8
- package/templates/AGENTS.md +122 -14
- package/templates/README.md +15 -1
- package/templates/circles-README.md +3 -3
- package/templates/evda-digest.md +31 -6
- package/LICENSE +0 -21
- package/presets/student/preset.json +0 -13
package/lib/yaml.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The substrate writes an eco as `eco.yaml`, and this package has no runtime dependencies, so the
|
|
4
4
|
// subset of YAML an eco manifest uses is read and written here: comments, block mappings, block
|
|
5
|
-
// sequences, plain and quoted scalars, block scalars (`|`,
|
|
5
|
+
// sequences, plain and quoted scalars, block scalars (`|`, `>`, with chomping and indentation
|
|
6
|
+
// indicators), and one level of flow collections.
|
|
6
7
|
// Anchors, aliases, tags, multi-document streams and tab indentation are refused with a line
|
|
7
8
|
// number rather than guessed at. `test/yaml.test.mjs` checks this reader against the `yaml`
|
|
8
9
|
// package over the substrate's own manifests, so the two agree on every file that matters.
|
|
@@ -48,6 +49,13 @@ function lines(text) {
|
|
|
48
49
|
return out;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Set a parsed key as an own property. A plain `out[key] = v` treats "__proto__" as the prototype,
|
|
53
|
+
// and `key in out` finds "toString" and "constructor" on every object, so a frontmatter key named
|
|
54
|
+
// like an Object method read as a duplicate (HQ-1406).
|
|
55
|
+
function put(out, key, value) {
|
|
56
|
+
Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
const PLAIN_BOOL = { true: true, false: false, True: true, False: false, TRUE: true, FALSE: false };
|
|
52
60
|
const NULLS = new Set(["null", "Null", "NULL", "~", ""]);
|
|
53
61
|
|
|
@@ -125,7 +133,7 @@ function flow(t, no) {
|
|
|
125
133
|
skip();
|
|
126
134
|
if (peek() !== ":") fail(no, "expected : in flow mapping");
|
|
127
135
|
i++;
|
|
128
|
-
out
|
|
136
|
+
put(out, String(k), value(), no);
|
|
129
137
|
skip();
|
|
130
138
|
if (peek() === ",") { i++; continue; }
|
|
131
139
|
if (peek() === "}") { i++; return out; }
|
|
@@ -147,8 +155,10 @@ function splitKey(text, no) {
|
|
|
147
155
|
let key;
|
|
148
156
|
if (text[0] === '"' || text[0] === "'") {
|
|
149
157
|
const q = text[0];
|
|
158
|
+
// An escaped quote inside a double-quoted key does not end it: the writer quotes a key with
|
|
159
|
+
// JSON.stringify, so `"a\"b"` is one key (HQ-1406).
|
|
150
160
|
let j = 1;
|
|
151
|
-
while (j < text.length && text[j] !== q) j
|
|
161
|
+
while (j < text.length && text[j] !== q) j += q === '"' && text[j] === "\\" ? 2 : 1;
|
|
152
162
|
if (j >= text.length) fail(no, "unterminated quoted key");
|
|
153
163
|
key = scalar(text.slice(0, j + 1), no);
|
|
154
164
|
i = j + 1;
|
|
@@ -162,39 +172,92 @@ function splitKey(text, no) {
|
|
|
162
172
|
return { key: String(key), rest: text.slice(i + 1).trim() };
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
// A block scalar (`|` or `>`) is read from the raw text lines, not from lines(): its body keeps
|
|
176
|
+
// what lines() drops, a `#` line, a leading or trailing blank line, a whitespace-only line (HQ-1409).
|
|
177
|
+
// YAML 1.2 rules: the indentation is the header's indicator over the parent's, or else the first
|
|
178
|
+
// non-blank line's; the body ends at the first non-blank line indented less; `>` folds a line break
|
|
179
|
+
// between two lines that do not start with whitespace; `-` strips the final newlines, `+` keeps
|
|
180
|
+
// them all, and the default keeps one. Where the spec leaves a corner open (a whitespace-only line
|
|
181
|
+
// at the end, a text with no final line break) this reads it as the `yaml` package does, since
|
|
182
|
+
// every other tool that reads these files uses it. `src` is the text's lines, `no` the header's.
|
|
183
|
+
function blockScalar(header, ls, i, parentIndent, no, src) {
|
|
184
|
+
const m = /^([|>])(?:([-+]?)([1-9]?)|([1-9])([-+]))$/.exec(header);
|
|
167
185
|
if (!m) fail(no, `unsupported block scalar header ${header}`);
|
|
168
186
|
const fold = m[1] === ">";
|
|
169
|
-
const chomp = m[2];
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
const chomp = m[2] || m[5] || "";
|
|
188
|
+
const indicator = Number(m[3] || m[4] || 0);
|
|
189
|
+
// The text's final line break ends its last line; it does not open another one.
|
|
190
|
+
const last = src.length > 0 && src[src.length - 1].replace(/\r$/, "") === "" ? src.length - 1 : src.length;
|
|
191
|
+
const raw = (k) => src[k].replace(/\r$/, "");
|
|
192
|
+
const spaces = (l) => l.length - l.replace(/^ +/, "").length;
|
|
193
|
+
const blank = (l) => /^ *$/.test(l);
|
|
194
|
+
let first = null; // the first non-blank line's indentation
|
|
195
|
+
for (let k = no; k < last; k++) {
|
|
196
|
+
const l = raw(k);
|
|
197
|
+
if (blank(l)) continue;
|
|
198
|
+
if (spaces(l) > parentIndent) first = spaces(l);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
// With no content line, every line up to the next less-indented one is blank, however long.
|
|
202
|
+
const indent = indicator ? parentIndent + indicator : first ?? Infinity;
|
|
203
|
+
const lines = []; // each line: its text past the indentation, or null when blank
|
|
204
|
+
let k = no;
|
|
205
|
+
for (; k < last; k++) {
|
|
206
|
+
const l = raw(k);
|
|
207
|
+
if (blank(l)) { lines.push({ text: l.length > indent ? l.slice(indent) : null, len: l.length }); continue; }
|
|
208
|
+
if (spaces(l) < indent) {
|
|
209
|
+
if (/^ *\t/.test(l)) fail(k + 1, "tab indentation");
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
lines.push({ text: l.slice(indent), len: l.length });
|
|
213
|
+
}
|
|
214
|
+
const open = k === last && last === src.length; // the block ends the text, with no final line break
|
|
215
|
+
const hasContent = lines.some((l) => l.text !== null && !blank(l.text));
|
|
216
|
+
if (!hasContent) {
|
|
217
|
+
if (chomp !== "+" || lines.length === 0) return done("");
|
|
218
|
+
return done("\n".repeat(Math.max(1, lines.length - (open ? 1 : 0))));
|
|
219
|
+
}
|
|
220
|
+
// YAML refuses a leading blank line indented past the block, since it would decide the indentation.
|
|
221
|
+
if (!indicator) {
|
|
222
|
+
for (let b = 0; b < lines.length && (lines[b].text === null || blank(lines[b].text)); b++) {
|
|
223
|
+
if (lines[b].text !== null) fail(no + b + 1, "a leading all-space line is indented past the block; give the header an indentation indicator");
|
|
224
|
+
}
|
|
177
225
|
}
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
for (
|
|
181
|
-
|
|
182
|
-
|
|
226
|
+
// Without `+`, a whitespace-only last line no longer than the first content line is not content.
|
|
227
|
+
let end = lines.length;
|
|
228
|
+
for (;;) {
|
|
229
|
+
while (end > 0 && lines[end - 1].text === null) end--;
|
|
230
|
+
if (chomp !== "+" && end > 0 && blank(lines[end - 1].text) && lines[end - 1].len <= first) { lines[--end].text = null; continue; }
|
|
231
|
+
break;
|
|
183
232
|
}
|
|
184
|
-
|
|
233
|
+
const content = lines.slice(0, end).map((l) => l.text ?? "");
|
|
234
|
+
let text = "";
|
|
185
235
|
if (fold) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
const moreIndented = (l) => /^[ \t]/.test(l);
|
|
237
|
+
let prev = null;
|
|
238
|
+
let empties = 0;
|
|
239
|
+
for (const l of content) {
|
|
240
|
+
if (l === "") { empties++; continue; }
|
|
241
|
+
if (prev === null) text += "\n".repeat(empties);
|
|
242
|
+
else if (!moreIndented(prev) && !moreIndented(l)) text += empties === 0 ? " " : "\n".repeat(empties);
|
|
243
|
+
else text += "\n".repeat(empties + 1);
|
|
244
|
+
text += l;
|
|
245
|
+
prev = l;
|
|
246
|
+
empties = 0;
|
|
247
|
+
}
|
|
191
248
|
} else {
|
|
192
|
-
text =
|
|
249
|
+
text = content.join("\n");
|
|
250
|
+
}
|
|
251
|
+
if (chomp === "-") return done(text);
|
|
252
|
+
if (chomp === "") return done(text + "\n");
|
|
253
|
+
// `+` keeps the content line's break and one per blank line after it that ends in a line break.
|
|
254
|
+
return done(text + "\n" + "\n".repeat(Math.max(0, lines.length - end - (open ? 1 : 0))));
|
|
255
|
+
|
|
256
|
+
function done(value) {
|
|
257
|
+
let j = i;
|
|
258
|
+
while (j < ls.length && ls[j].no <= k) j++;
|
|
259
|
+
return { value, next: j };
|
|
193
260
|
}
|
|
194
|
-
if (chomp === "-") text = text.replace(/\n+$/, "");
|
|
195
|
-
else if (chomp === "+") text = text + "\n";
|
|
196
|
-
else text = text.replace(/\n+$/, "") + "\n";
|
|
197
|
-
return { value: text, next: j };
|
|
198
261
|
}
|
|
199
262
|
|
|
200
263
|
function parseBlock(ls, i, indent) {
|
|
@@ -220,7 +283,7 @@ function parseSeq(ls, i, indent) {
|
|
|
220
283
|
// `- key: value` opens a mapping whose remaining keys sit two columns in.
|
|
221
284
|
const itemIndent = indent + 2;
|
|
222
285
|
const synthetic = new Line(itemIndent, rest, l.no);
|
|
223
|
-
synthetic.
|
|
286
|
+
synthetic.src = l.src;
|
|
224
287
|
const sub = [synthetic];
|
|
225
288
|
let j = i + 1;
|
|
226
289
|
while (j < ls.length && ls[j].indent >= itemIndent) sub.push(ls[j++]);
|
|
@@ -229,8 +292,8 @@ function parseSeq(ls, i, indent) {
|
|
|
229
292
|
i = j;
|
|
230
293
|
continue;
|
|
231
294
|
}
|
|
232
|
-
if (/^[|>][-+]
|
|
233
|
-
const r = blockScalar(rest, ls, i + 1, indent, l.no);
|
|
295
|
+
if (/^[|>][-+1-9]*$/.test(rest)) {
|
|
296
|
+
const r = blockScalar(rest, ls, i + 1, indent, l.no, l.src);
|
|
234
297
|
out.push(r.value);
|
|
235
298
|
i = r.next;
|
|
236
299
|
continue;
|
|
@@ -248,30 +311,30 @@ function parseMap(ls, i, indent) {
|
|
|
248
311
|
const l = ls[i];
|
|
249
312
|
const kv = splitKey(l.text, l.no);
|
|
250
313
|
if (!kv) fail(l.no, `expected a key, got: ${l.text}`);
|
|
251
|
-
if (kv.key
|
|
314
|
+
if (Object.hasOwn(out, kv.key)) fail(l.no, `duplicate key ${kv.key}`);
|
|
252
315
|
if (kv.rest === "") {
|
|
253
316
|
const nx = ls[i + 1];
|
|
254
317
|
if (nx && nx.indent > indent) {
|
|
255
318
|
const r = parseBlock(ls, i + 1, nx.indent);
|
|
256
|
-
out
|
|
319
|
+
put(out, kv.key, r.value);
|
|
257
320
|
i = r.next;
|
|
258
321
|
} else if (nx && nx.indent === indent && (nx.text.startsWith("- ") || nx.text === "-")) {
|
|
259
322
|
const r = parseSeq(ls, i + 1, indent);
|
|
260
|
-
out
|
|
323
|
+
put(out, kv.key, r.value);
|
|
261
324
|
i = r.next;
|
|
262
325
|
} else {
|
|
263
|
-
out
|
|
326
|
+
put(out, kv.key, null);
|
|
264
327
|
i++;
|
|
265
328
|
}
|
|
266
329
|
continue;
|
|
267
330
|
}
|
|
268
|
-
if (/^[|>][-+]
|
|
269
|
-
const r = blockScalar(kv.rest, ls, i + 1, indent, l.no);
|
|
270
|
-
out
|
|
331
|
+
if (/^[|>][-+1-9]*$/.test(kv.rest)) {
|
|
332
|
+
const r = blockScalar(kv.rest, ls, i + 1, indent, l.no, l.src);
|
|
333
|
+
put(out, kv.key, r.value);
|
|
271
334
|
i = r.next;
|
|
272
335
|
continue;
|
|
273
336
|
}
|
|
274
|
-
out
|
|
337
|
+
put(out, kv.key, scalar(kv.rest, l.no));
|
|
275
338
|
i++;
|
|
276
339
|
}
|
|
277
340
|
if (i < ls.length && ls[i].indent > indent) fail(ls[i].no, `unexpected indentation (${ls[i].indent} columns)`);
|
|
@@ -281,7 +344,7 @@ function parseMap(ls, i, indent) {
|
|
|
281
344
|
export function parseYaml(text) {
|
|
282
345
|
const ls = lines(text);
|
|
283
346
|
const raws = text.split("\n");
|
|
284
|
-
for (const l of ls) l.
|
|
347
|
+
for (const l of ls) l.src = raws;
|
|
285
348
|
if (ls.length === 0) return null;
|
|
286
349
|
const r = parseBlock(ls, 0, ls[0].indent);
|
|
287
350
|
if (r.next < ls.length) fail(ls[r.next].no, `unexpected content after the document: ${ls[r.next].text}`);
|
|
@@ -304,6 +367,18 @@ function scalarOut(v) {
|
|
|
304
367
|
return v;
|
|
305
368
|
}
|
|
306
369
|
|
|
370
|
+
// Whether a multi-line string is written as a `|` block. A string whose block would need a header
|
|
371
|
+
// indicator (a leading blank or indented first line, kept trailing newlines) or would carry a `#`,
|
|
372
|
+
// a carriage return or trailing whitespace is written double-quoted instead, which always reads
|
|
373
|
+
// back and reads the same in every YAML tool (HQ-1406: "\na" read back as "a").
|
|
374
|
+
function blockSafe(v) {
|
|
375
|
+
const body = v.endsWith("\n") ? v.slice(0, -1) : v;
|
|
376
|
+
if (body.endsWith("\n") || body.includes("\r") || body.includes("#")) return false;
|
|
377
|
+
const ls = body.split("\n");
|
|
378
|
+
if (ls[0] === "" || /^\s/.test(ls[0])) return false;
|
|
379
|
+
return ls.every((l) => !/\s$/.test(l));
|
|
380
|
+
}
|
|
381
|
+
|
|
307
382
|
function isScalar(v) {
|
|
308
383
|
return v === null || v === undefined || ["string", "number", "boolean"].includes(typeof v);
|
|
309
384
|
}
|
|
@@ -330,7 +405,7 @@ function write(v, indent, out) {
|
|
|
330
405
|
for (const k of keys) {
|
|
331
406
|
const val = v[k];
|
|
332
407
|
const key = scalarOut(k);
|
|
333
|
-
if (typeof val === "string" && val.includes("\n")) {
|
|
408
|
+
if (typeof val === "string" && val.includes("\n") && blockSafe(val)) {
|
|
334
409
|
const body = val.replace(/\n$/, "");
|
|
335
410
|
out.push(`${pad}${key}: ${val.endsWith("\n") ? "|" : "|-"}`);
|
|
336
411
|
for (const l of body.split("\n")) out.push(l === "" ? "" : `${pad} ${l}`);
|
package/package.json
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-evda",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Make your own EVÐA: a running journal of your life with Iris, as plain files on your machine, for any AI. Connect Claude Code, Codex or the Claude app over MCP. No runtime dependencies.",
|
|
3
|
+
"version": "0.1.7",
|
|
5
4
|
"type": "module",
|
|
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": {
|
|
7
|
-
"create-evda": "create-evda.mjs",
|
|
8
|
-
"evda": "bin/evda.mjs"
|
|
7
|
+
"create-evda": "./create-evda.mjs",
|
|
8
|
+
"evda": "./bin/evda.mjs"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
11
|
"node": ">=20"
|
|
12
12
|
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.test.mjs",
|
|
15
|
+
"lint": "node scripts/dashes-check.mjs && node scripts/build-templates.mjs --check && node scripts/build-kinds.mjs --check && node scripts/check-installed-core.mjs",
|
|
16
|
+
"templates:build": "node scripts/build-templates.mjs",
|
|
17
|
+
"templates:check": "node scripts/build-templates.mjs --check",
|
|
18
|
+
"installed-core:check": "node scripts/check-installed-core.mjs",
|
|
19
|
+
"kinds:build": "node scripts/build-kinds.mjs",
|
|
20
|
+
"kinds:check": "node scripts/build-kinds.mjs --check",
|
|
21
|
+
"dashes:check": "node scripts/dashes-check.mjs",
|
|
22
|
+
"vectors:both": "bash scripts/vectors-both.sh",
|
|
23
|
+
"doctor": "node bin/evda.mjs doctor",
|
|
24
|
+
"publish:dry": "node scripts/publish.mjs",
|
|
25
|
+
"publish:live": "node scripts/publish.mjs --publish"
|
|
26
|
+
},
|
|
13
27
|
"files": [
|
|
14
28
|
"bin",
|
|
29
|
+
"hooks",
|
|
30
|
+
"docs",
|
|
15
31
|
"lib",
|
|
16
32
|
"templates",
|
|
17
33
|
"skills",
|
|
@@ -19,16 +35,10 @@
|
|
|
19
35
|
"presets",
|
|
20
36
|
"create-evda.mjs",
|
|
21
37
|
"README.md",
|
|
22
|
-
"
|
|
23
|
-
],
|
|
24
|
-
"keywords": [
|
|
25
|
-
"evda",
|
|
26
|
-
"personal-record",
|
|
27
|
-
"agents",
|
|
28
|
-
"markdown",
|
|
29
|
-
"mcp"
|
|
38
|
+
"CHANGELOG.md"
|
|
30
39
|
],
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
"atlas": {
|
|
41
|
+
"tier": "block",
|
|
42
|
+
"domain": "neutral"
|
|
43
|
+
}
|
|
34
44
|
}
|
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
"folders": [
|
|
6
6
|
{ "path": "Journal", "type": "record", "note": "One file per day, named YYYY-MM-DD.md." },
|
|
7
7
|
{ "path": "Captures", "type": "capture", "note": "A thought as it arrived." },
|
|
8
|
+
{ "path": "Work", "type": "ticket", "note": "One file per thing being done, with its status and the day it is planned for." },
|
|
8
9
|
{ "path": "People", "type": "person", "connectors": ["contacts"], "note": "One file per person." },
|
|
9
10
|
{ "path": "Notes", "type": "concept", "note": "Ideas worth keeping past the day." }
|
|
10
|
-
]
|
|
11
|
+
],
|
|
12
|
+
"mapping": {
|
|
13
|
+
"journal": "Journal",
|
|
14
|
+
"journal/captures": "Captures",
|
|
15
|
+
"work": "Work",
|
|
16
|
+
"collections": "People",
|
|
17
|
+
"knowledge": "Notes"
|
|
18
|
+
}
|
|
11
19
|
}
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "projects-and-areas",
|
|
3
3
|
"title": "Projects and areas",
|
|
4
|
-
"for": "Someone running several projects and parts of life at once, who captures fast, sorts later, and looks back each week.",
|
|
4
|
+
"for": "Someone running several projects and parts of life at once, courses included, who captures fast, sorts later, and looks back each week.",
|
|
5
5
|
"folders": [
|
|
6
|
-
{ "path": "
|
|
7
|
-
{ "path": "01_Projects", "type": "project", "note": "Work with an end: one folder per project." },
|
|
6
|
+
{ "path": "01_Projects", "type": "project", "note": "One folder per project; a finished one stays here with its status." },
|
|
8
7
|
{ "path": "02_Areas", "note": "Parts of life with no end date: one folder per area." },
|
|
9
|
-
{ "path": "02_Areas/Academics", "note": "One folder per course
|
|
8
|
+
{ "path": "02_Areas/Academics", "type": "course", "note": "One folder per course; a course is a collection like any other." },
|
|
10
9
|
{ "path": "02_Areas/Relationships/People", "type": "person", "connectors": ["contacts"], "note": "One file per person." },
|
|
11
|
-
{ "path": "03_Resources", "note": "
|
|
10
|
+
{ "path": "03_Resources", "note": "What is known, each page citing where it came from." },
|
|
12
11
|
{ "path": "03_Resources/Raw", "type": "source", "connectors": ["url"], "note": "Sources as they arrived, unedited." },
|
|
13
12
|
{ "path": "03_Resources/Wiki/Concepts", "type": "concept", "note": "One page per recurring idea, named." },
|
|
14
13
|
{ "path": "03_Resources/Wiki/Syntheses", "type": "concept", "note": "Connections worked out across ideas." },
|
|
15
14
|
{ "path": "03_Resources/Wiki/Sources", "type": "source", "connectors": ["url"], "note": "A summary per source cited often." },
|
|
16
|
-
{ "path": "
|
|
15
|
+
{ "path": "04_Work", "type": "ticket", "note": "One file per effort, with its status and the day it is planned for; a board can take its place." },
|
|
17
16
|
{ "path": "05_Process", "type": "capture", "note": "Captures waiting to be filed." },
|
|
18
17
|
{ "path": "05_Process/Processed", "type": "capture", "note": "Captures already filed, kept." },
|
|
19
18
|
{ "path": "05_Process/_media", "type": "blob", "note": "Photos and audio a capture carried." },
|
|
20
|
-
{ "path": "06_Metadata", "note": "The record about the record." },
|
|
21
19
|
{ "path": "06_Metadata/Daily Journal", "type": "record", "note": "One file per day, named YYYY-MM-DD.md." },
|
|
22
20
|
{ "path": "06_Metadata/Weekly Reviews", "type": "concept", "note": "One look back per week." },
|
|
23
|
-
{ "path": "
|
|
24
|
-
{ "path": "07_Attachments", "note": "Files notes link to." }
|
|
21
|
+
{ "path": "07_Attachments", "type": "blob", "note": "Files notes link to." }
|
|
25
22
|
],
|
|
23
|
+
"mapping": {
|
|
24
|
+
"journal": "06_Metadata/Daily Journal",
|
|
25
|
+
"journal/captures": "05_Process",
|
|
26
|
+
"work": "04_Work",
|
|
27
|
+
"collections": "01_Projects",
|
|
28
|
+
"collections/areas": "02_Areas",
|
|
29
|
+
"collections/attachments": "07_Attachments",
|
|
30
|
+
"knowledge": "03_Resources",
|
|
31
|
+
"knowledge/reviews": "06_Metadata/Weekly Reviews"
|
|
32
|
+
},
|
|
26
33
|
"routines": ["morning", "weekly"]
|
|
27
34
|
}
|
package/skills/evda/SKILL.md
CHANGED
|
@@ -16,6 +16,12 @@ calls no model; you are their own AI, and everything you keep goes through the t
|
|
|
16
16
|
Where it and this skill disagree, it wins.
|
|
17
17
|
2. Call `evda_recent` for the last few days, and read what they point you at with `evda_search`.
|
|
18
18
|
3. Say in one line what you read, then answer them.
|
|
19
|
+
4. Call `evda_check_update` once (throttled to once an hour; most sessions read a cached result).
|
|
20
|
+
Say nothing if it is current or was skipped. If an update is available or the check failed, say
|
|
21
|
+
the one line it returns, briefly, and hand them the command it names to run in a terminal. The
|
|
22
|
+
tool only tells: nothing in this server installs an update. It asks the npm registry for a
|
|
23
|
+
version number and sends nothing from the record, and it is the only tool here that reaches the
|
|
24
|
+
network at all. `evda check-update` is the same check from a shell, for a session that has one.
|
|
19
25
|
|
|
20
26
|
If the tools are not there, say so plainly and point them at `evda connect`, which prints the
|
|
21
27
|
setup for each client. Do not keep the record some other way.
|
|
@@ -24,6 +30,20 @@ setup for each client. Do not keep the record some other way.
|
|
|
24
30
|
|
|
25
31
|
- **File their words in the same turn they say them**, with `evda_write`: verbatim, filler, hedges
|
|
26
32
|
and typos intact. Never a summary, never your words.
|
|
33
|
+
- **Without being asked.** They should never have to say "save this". When a tool result says
|
|
34
|
+
nothing from this conversation is filed yet, or a Stop hook asks before you finish, that is this
|
|
35
|
+
rule: file what they said that is worth keeping, then stop. When `evda.json` says
|
|
36
|
+
`"filing": "asked"`, they chose to file only when they ask; when it says nothing, ask them once.
|
|
37
|
+
- **One section per conversation.** Your first `evda_write` opens it with one line of `context`;
|
|
38
|
+
later ones go under it.
|
|
39
|
+
- **Entries grow on their own.** When a person, project or place comes up that already has an
|
|
40
|
+
entry (check with `evda_search`), file their words with `evda_write` naming it, and if this session
|
|
41
|
+
can edit files, add a dated section (`## YYYY-MM-DD`) to that entry too; over the tools alone, the
|
|
42
|
+
daily file is where it lands and `evda organize` proposes its place. One with no entry is offered
|
|
43
|
+
in one line, never created without their yes. A noted entry is never added to.
|
|
44
|
+
- **The day's entry is their words plus one line of context**: pass `context` to `evda_write`, one
|
|
45
|
+
line in your words on what the conversation was about. It is written labeled as not theirs; never
|
|
46
|
+
put your words in `words`.
|
|
27
47
|
- **Report every filing in the past tense, naming the path the tool returned.** Never "I'll file".
|
|
28
48
|
- **`evda_write` only appends.** There is no edit and no rewrite. A correction is a new write.
|
|
29
49
|
- **What another AI said about them is an import**, filed with `evda_import` and its source. It is
|
|
@@ -31,6 +51,16 @@ setup for each client. Do not keep the record some other way.
|
|
|
31
51
|
- **Never write a password, key or token.** The tools refuse one; tell them to keep it in their
|
|
32
52
|
password manager.
|
|
33
53
|
|
|
54
|
+
## Skills and records inside this one
|
|
55
|
+
|
|
56
|
+
- **When a process repeats** (the same steps done or explained twice, the same correction twice),
|
|
57
|
+
write it once with `evda_write_skill`: a kebab-case name, a description saying when to use it, the
|
|
58
|
+
steps. Tell them in one line. A tool result that says steps ran twice is that moment. Follow the
|
|
59
|
+
skills `evda_skills` lists; revise one (`revise: true`, `why`) when a run of it goes wrong. Never a
|
|
60
|
+
rule about their life: no threshold, count or streak about a person.
|
|
61
|
+
- **When they ask for an EVÐA inside this one**, make it with `evda_create_subrecord` and tell them
|
|
62
|
+
what doctor said. Only pass `bounded: true` with `confirmed: true` after their yes to it.
|
|
63
|
+
|
|
34
64
|
## How to talk
|
|
35
65
|
|
|
36
66
|
- **Never quiz them.** No lists of questions, no check-ins about open items, no "how did X go"
|
|
@@ -38,6 +68,16 @@ setup for each client. Do not keep the record some other way.
|
|
|
38
68
|
- Take a position and show why; they push back and you get more specific. Never flatter.
|
|
39
69
|
- Never cite a date, quote or event that is not in the record. Check with `evda_search` first.
|
|
40
70
|
|
|
71
|
+
## Who reads what
|
|
72
|
+
|
|
73
|
+
- **"Just note this" goes to `evda_note`, not `evda_write`.** No AI reads a noted entry, you
|
|
74
|
+
included, until they say it can; then confirm which one and call `evda_release_note`.
|
|
75
|
+
- **When a tool says something was not shown** (a noted entry, or a part set to be read by a
|
|
76
|
+
different AI), tell them in one plain sentence what is there and how they open it. Never guess
|
|
77
|
+
at what it says.
|
|
78
|
+
- **When they ask which AI is safest,** answer from "What each AI company says" in their README,
|
|
79
|
+
with its date. Never rank companies by how likely they are to misuse data.
|
|
80
|
+
|
|
41
81
|
## Circles
|
|
42
82
|
|
|
43
83
|
- Everything is private by default.
|
|
@@ -64,22 +104,41 @@ Organization comes from the preset library, never from you making one up.
|
|
|
64
104
|
is theirs and shared with specific people; its own EVÐA when someone else holds it or it will
|
|
65
105
|
outlive their involvement (a company, a team's project).
|
|
66
106
|
|
|
107
|
+
## Gathering a pile they already have
|
|
108
|
+
|
|
109
|
+
When they point you at something they already keep (a Documents folder, a Notes or Obsidian
|
|
110
|
+
export, a contacts export), call `evda_gather` with those paths. It writes a proposal outside the
|
|
111
|
+
pile and outside the record and changes nothing in either. Tell them in a few lines what it found:
|
|
112
|
+
the preset it fits, the folders it needs, what it kept as references, and what it left for them.
|
|
113
|
+
|
|
114
|
+
Then take `evda_gather_questions` one at a time, in your own words, in the flow of the
|
|
115
|
+
conversation. Each one changes where something goes; nothing else is ever asked, so never add a
|
|
116
|
+
question of your own and never read them the list. Record each answer with `evda_gather_answer`,
|
|
117
|
+
as they gave it, and say what it moved. Nothing is moved into the record in this step.
|
|
118
|
+
|
|
119
|
+
When every question is answered, show them the summary once more and ask whether to apply it.
|
|
120
|
+
Only on their yes call `evda_gather_apply` with `confirmed: true` (copying unless they asked to
|
|
121
|
+
move). Tell them what it placed, what it left for them, and the reversal manifest it names:
|
|
122
|
+
`evda gather revert <manifest>` undoes it. When they bring a second pile later, call
|
|
123
|
+
`evda_gather` with `into` set to their record, so only what is new is asked about and placed.
|
|
124
|
+
|
|
67
125
|
## Organizing (scheduled or asked)
|
|
68
126
|
|
|
69
127
|
`evda organize` does the mechanical part itself: it copies each capture, verbatim, into the daily
|
|
70
128
|
file of its date and writes a report under `reports/organize/`. With `--with-ai` it then asks you,
|
|
71
129
|
through `claude -p`, for the part that needs judgment, and appends your answer to that report.
|
|
72
130
|
|
|
73
|
-
When you are asked for that judgment
|
|
131
|
+
When you are asked for that judgment, you are given their entries from the last two weeks, each
|
|
132
|
+
under its date. Hand back one thing that comes back across different days, in their own words:
|
|
74
133
|
|
|
75
|
-
1.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
1. One or two plain sentences naming what comes back, then each place it came from as
|
|
135
|
+
`- YYYY-MM-DD: "words copied exactly from that day"`, from two days or more. A quote that is not
|
|
136
|
+
in the entry of its date drops the whole answer, so copy, never paraphrase.
|
|
137
|
+
2. Say only what the entries say: no advice, no question back, no score, no count, and nothing about
|
|
138
|
+
filing, the report or yourself. The filing is already listed above your answer.
|
|
80
139
|
3. You write nothing yourself here. Move nothing, rename nothing, delete nothing, and never touch
|
|
81
|
-
`circles/` or `grants/`.
|
|
82
|
-
4. If nothing
|
|
140
|
+
`circles/` or `grants/`.
|
|
141
|
+
4. If nothing honestly comes back across two days, answer `NOTHING`; the report then says nothing.
|
|
83
142
|
|
|
84
143
|
## Look back (weekly)
|
|
85
144
|
|