@wierdbytes/pi-dream 0.0.0-stage → 0.1.0
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 +12 -0
- package/LICENSE +21 -0
- package/README.md +102 -2
- package/agentsmd.ts +171 -0
- package/commands.ts +92 -0
- package/config.ts +194 -0
- package/curate.test.ts +138 -0
- package/curate.ts +610 -0
- package/digest.test.ts +62 -0
- package/digest.ts +323 -0
- package/gitops.ts +69 -0
- package/inbox.ts +56 -0
- package/index.ts +33 -0
- package/layout.ts +99 -0
- package/llm.ts +145 -0
- package/lock.ts +74 -0
- package/memindex.test.ts +74 -0
- package/memindex.ts +126 -0
- package/misc.test.ts +141 -0
- package/notes.test.ts +69 -0
- package/notes.ts +235 -0
- package/package.json +54 -4
- package/pipeline.ts +1149 -0
- package/project.ts +115 -0
- package/projects.test.ts +79 -0
- package/projects.ts +74 -0
- package/prompts.ts +159 -0
- package/report.ts +287 -0
- package/runner.ts +22 -0
- package/runtime.ts +428 -0
- package/scheduler.ts +133 -0
- package/search.test.ts +34 -0
- package/search.ts +153 -0
- package/secrets.ts +74 -0
- package/sessions.ts +176 -0
- package/setup.ts +148 -0
- package/state.ts +84 -0
- package/tools.ts +75 -0
package/notes.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory notes: one Markdown file per note with YAML front matter.
|
|
3
|
+
*
|
|
4
|
+
* Front matter values are written as JSON (a subset of YAML), so they parse
|
|
5
|
+
* back exactly and survive hand edits in a pull request. Parsing is lenient:
|
|
6
|
+
* a note written by hand without an id or with unknown fields still loads;
|
|
7
|
+
* the next dream fills in what is missing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { scopeDir } from "./layout.ts";
|
|
15
|
+
|
|
16
|
+
export const NOTE_TYPES = ["preference", "procedure", "pitfall", "fact", "decision", "reference"] as const;
|
|
17
|
+
export const NOTE_STATUSES = ["active", "superseded", "deprecated"] as const;
|
|
18
|
+
export const INDEX_MODES = ["always", "pointer", "none"] as const;
|
|
19
|
+
export const EVIDENCE_KINDS = ["user", "observed", "inbox", "reviewer"] as const;
|
|
20
|
+
|
|
21
|
+
export type NoteType = (typeof NOTE_TYPES)[number];
|
|
22
|
+
export type NoteStatus = (typeof NOTE_STATUSES)[number];
|
|
23
|
+
export type IndexMode = (typeof INDEX_MODES)[number];
|
|
24
|
+
export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
|
|
25
|
+
|
|
26
|
+
export interface Evidence {
|
|
27
|
+
/** Session id, `inbox:<id>` or `pr#<n>`. */
|
|
28
|
+
s: string;
|
|
29
|
+
/** Entry ids (or comment ids). */
|
|
30
|
+
e: string[];
|
|
31
|
+
date?: string;
|
|
32
|
+
kind?: EvidenceKind;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Note {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
type: NoteType;
|
|
39
|
+
status: NoteStatus;
|
|
40
|
+
index: IndexMode;
|
|
41
|
+
when?: string;
|
|
42
|
+
pinned?: boolean;
|
|
43
|
+
created: string;
|
|
44
|
+
updated: string;
|
|
45
|
+
confirmed?: string;
|
|
46
|
+
validUntil?: string;
|
|
47
|
+
supersededBy?: string;
|
|
48
|
+
supersedes?: string[];
|
|
49
|
+
stats: { helpful: number; harmful: number };
|
|
50
|
+
evidence: Evidence[];
|
|
51
|
+
body: string;
|
|
52
|
+
/** Path relative to the memory root. */
|
|
53
|
+
file: string;
|
|
54
|
+
/** Scope id: `global` or `projects/<dir>`. */
|
|
55
|
+
scope: string;
|
|
56
|
+
/** Unknown front matter keys, preserved on rewrite. */
|
|
57
|
+
extra: Record<string, unknown>;
|
|
58
|
+
/** Problems found while parsing (missing id, bad yaml…). */
|
|
59
|
+
problems: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const KNOWN = new Set([
|
|
63
|
+
"id",
|
|
64
|
+
"title",
|
|
65
|
+
"type",
|
|
66
|
+
"status",
|
|
67
|
+
"index",
|
|
68
|
+
"when",
|
|
69
|
+
"pinned",
|
|
70
|
+
"created",
|
|
71
|
+
"updated",
|
|
72
|
+
"confirmed",
|
|
73
|
+
"valid_until",
|
|
74
|
+
"superseded_by",
|
|
75
|
+
"supersedes",
|
|
76
|
+
"stats",
|
|
77
|
+
"evidence",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const oneOf = <T extends string>(list: readonly T[], v: unknown): v is T =>
|
|
81
|
+
typeof v === "string" && (list as readonly string[]).includes(v);
|
|
82
|
+
|
|
83
|
+
function dateStr(v: unknown): string | undefined {
|
|
84
|
+
if (v instanceof Date && !Number.isNaN(v.getTime())) return v.toISOString().slice(0, 10);
|
|
85
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseEvidence(v: unknown): Evidence[] {
|
|
90
|
+
if (!Array.isArray(v)) return [];
|
|
91
|
+
const out: Evidence[] = [];
|
|
92
|
+
for (const item of v) {
|
|
93
|
+
if (!item || typeof item !== "object") continue;
|
|
94
|
+
const o = item as Record<string, unknown>;
|
|
95
|
+
if (typeof o.s !== "string" || !o.s) continue;
|
|
96
|
+
const e = Array.isArray(o.e) ? o.e.filter((x): x is string => typeof x === "string") : typeof o.e === "string" ? [o.e] : [];
|
|
97
|
+
out.push({ s: o.s, e, date: dateStr(o.date), kind: oneOf(EVIDENCE_KINDS, o.kind) ? o.kind : undefined });
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isNoteId(v: unknown): v is string {
|
|
103
|
+
return typeof v === "string" && /^m-[0-9a-f]{6,12}$/.test(v);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function parseNote(content: string, file: string, scope: string): Note {
|
|
107
|
+
const problems: string[] = [];
|
|
108
|
+
let fm: Record<string, unknown> = {};
|
|
109
|
+
let body = content;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
112
|
+
fm = parsed.frontmatter ?? {};
|
|
113
|
+
body = parsed.body;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
problems.push(`front matter: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
}
|
|
117
|
+
body = body.trim();
|
|
118
|
+
const id = isNoteId(fm.id) ? fm.id : "";
|
|
119
|
+
if (!id) problems.push("missing or invalid id");
|
|
120
|
+
const firstLine = body.split("\n").find((l) => l.trim())?.replace(/^#+\s*/, "").trim() ?? "";
|
|
121
|
+
const title = typeof fm.title === "string" && fm.title.trim() ? fm.title.trim() : firstLine;
|
|
122
|
+
if (!title) problems.push("missing title");
|
|
123
|
+
const stats = (fm.stats && typeof fm.stats === "object" ? fm.stats : {}) as Record<string, unknown>;
|
|
124
|
+
const extra: Record<string, unknown> = {};
|
|
125
|
+
for (const [k, v] of Object.entries(fm)) if (!KNOWN.has(k)) extra[k] = v;
|
|
126
|
+
const created = dateStr(fm.created) ?? "";
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
title,
|
|
130
|
+
type: oneOf(NOTE_TYPES, fm.type) ? fm.type : "fact",
|
|
131
|
+
status: oneOf(NOTE_STATUSES, fm.status) ? fm.status : "active",
|
|
132
|
+
index: oneOf(INDEX_MODES, fm.index) ? fm.index : "none",
|
|
133
|
+
when: typeof fm.when === "string" && fm.when.trim() ? fm.when.trim() : undefined,
|
|
134
|
+
pinned: fm.pinned === true ? true : undefined,
|
|
135
|
+
created,
|
|
136
|
+
updated: dateStr(fm.updated) ?? created,
|
|
137
|
+
confirmed: dateStr(fm.confirmed),
|
|
138
|
+
validUntil: dateStr(fm.valid_until),
|
|
139
|
+
supersededBy: typeof fm.superseded_by === "string" ? fm.superseded_by : undefined,
|
|
140
|
+
supersedes: Array.isArray(fm.supersedes) ? fm.supersedes.filter((x): x is string => typeof x === "string") : undefined,
|
|
141
|
+
stats: {
|
|
142
|
+
helpful: typeof stats.helpful === "number" ? stats.helpful : 0,
|
|
143
|
+
harmful: typeof stats.harmful === "number" ? stats.harmful : 0,
|
|
144
|
+
},
|
|
145
|
+
evidence: parseEvidence(fm.evidence),
|
|
146
|
+
body,
|
|
147
|
+
file,
|
|
148
|
+
scope,
|
|
149
|
+
extra,
|
|
150
|
+
problems,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const j = (v: unknown) => JSON.stringify(v);
|
|
155
|
+
|
|
156
|
+
export function serializeNote(n: Note): string {
|
|
157
|
+
const lines = ["---", `id: ${n.id}`, `title: ${j(n.title)}`, `type: ${n.type}`, `status: ${n.status}`, `index: ${n.index}`];
|
|
158
|
+
if (n.when) lines.push(`when: ${j(n.when)}`);
|
|
159
|
+
if (n.pinned) lines.push("pinned: true");
|
|
160
|
+
lines.push(`created: ${j(n.created)}`, `updated: ${j(n.updated)}`);
|
|
161
|
+
if (n.confirmed) lines.push(`confirmed: ${j(n.confirmed)}`);
|
|
162
|
+
if (n.validUntil) lines.push(`valid_until: ${j(n.validUntil)}`);
|
|
163
|
+
if (n.supersededBy) lines.push(`superseded_by: ${j(n.supersededBy)}`);
|
|
164
|
+
if (n.supersedes?.length) lines.push(`supersedes: ${j(n.supersedes)}`);
|
|
165
|
+
if (n.stats.helpful || n.stats.harmful) lines.push(`stats: ${j({ helpful: n.stats.helpful, harmful: n.stats.harmful })}`);
|
|
166
|
+
if (n.evidence.length) {
|
|
167
|
+
lines.push("evidence:");
|
|
168
|
+
for (const ev of n.evidence) {
|
|
169
|
+
const o: Record<string, unknown> = { s: ev.s, e: ev.e };
|
|
170
|
+
if (ev.date) o.date = ev.date;
|
|
171
|
+
if (ev.kind) o.kind = ev.kind;
|
|
172
|
+
lines.push(` - ${j(o)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const [k, v] of Object.entries(n.extra)) if (v !== undefined) lines.push(`${k}: ${j(v)}`);
|
|
176
|
+
lines.push("---", "", n.body.trim(), "");
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function loadNotes(root: string, scope: string): Note[] {
|
|
181
|
+
const dir = join(scopeDir(root, scope), "notes");
|
|
182
|
+
if (!existsSync(dir)) return [];
|
|
183
|
+
const out: Note[] = [];
|
|
184
|
+
for (const name of readdirSync(dir).sort()) {
|
|
185
|
+
if (!name.endsWith(".md")) continue;
|
|
186
|
+
const rel = join("scopes", scope, "notes", name);
|
|
187
|
+
try {
|
|
188
|
+
out.push(parseNote(readFileSync(join(root, rel), "utf-8"), rel, scope));
|
|
189
|
+
} catch {
|
|
190
|
+
// unreadable file: skip
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** All scope ids present in the repo (`global`, `projects/<dir>`…). */
|
|
197
|
+
export function listScopes(root: string): string[] {
|
|
198
|
+
const scopes: string[] = [];
|
|
199
|
+
if (existsSync(join(root, "scopes", "global"))) scopes.push("global");
|
|
200
|
+
const projects = join(root, "scopes", "projects");
|
|
201
|
+
if (existsSync(projects)) {
|
|
202
|
+
for (const d of readdirSync(projects, { withFileTypes: true })) if (d.isDirectory()) scopes.push(`projects/${d.name}`);
|
|
203
|
+
}
|
|
204
|
+
return scopes;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function newNoteId(taken: Set<string>): string {
|
|
208
|
+
for (;;) {
|
|
209
|
+
const id = `m-${randomBytes(3).toString("hex")}`;
|
|
210
|
+
if (!taken.has(id)) {
|
|
211
|
+
taken.add(id);
|
|
212
|
+
return id;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const TRANSLIT: Record<string, string> = {
|
|
218
|
+
а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "e", ж: "zh", з: "z", и: "i", й: "y", к: "k", л: "l", м: "m",
|
|
219
|
+
н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f", х: "h", ц: "ts", ч: "ch", ш: "sh", щ: "sch", ъ: "",
|
|
220
|
+
ы: "y", ь: "", э: "e", ю: "yu", я: "ya",
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export function slugify(title: string, max = 60): string {
|
|
224
|
+
const t = [...title.toLowerCase()].map((c) => TRANSLIT[c] ?? c).join("");
|
|
225
|
+
let s = t
|
|
226
|
+
.normalize("NFKD")
|
|
227
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
228
|
+
.replace(/^-+|-+$/g, "");
|
|
229
|
+
if (s.length > max) {
|
|
230
|
+
s = s.slice(0, max);
|
|
231
|
+
const cut = s.lastIndexOf("-");
|
|
232
|
+
if (cut > max / 2) s = s.slice(0, cut);
|
|
233
|
+
}
|
|
234
|
+
return s || "note";
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wierdbytes/pi-dream",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Offline memory consolidation (\"dreaming\") for pi: distills past sessions into small, evidence-backed memory notes, sends every change to a GitHub pull request for review, injects the approved index into new sessions and adds memory search / read / inbox tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"*.ts",
|
|
8
|
+
"*.md",
|
|
9
|
+
"*.json",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi",
|
|
15
|
+
"coding-agent",
|
|
16
|
+
"extension",
|
|
17
|
+
"memory",
|
|
18
|
+
"dreaming",
|
|
19
|
+
"agent-memory"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "wierdbytes",
|
|
23
|
+
"homepage": "https://github.com/wierdbytes/pi-wierd-stuff/tree/master/packages/dream",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/wierdbytes/pi-wierd-stuff.git",
|
|
27
|
+
"directory": "packages/dream"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/wierdbytes/pi-wierd-stuff/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./index.ts"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@earendil-works/pi-ai": ">=0.99.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0",
|
|
47
|
+
"@earendil-works/pi-tui": ">=0.99.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@earendil-works/pi-ai": "^1.5.0",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
52
|
+
"@earendil-works/pi-tui": "^1.5.0",
|
|
53
|
+
"typebox": "^1.1.24",
|
|
54
|
+
"vitest": "^4.0.18"
|
|
55
|
+
}
|
|
56
|
+
}
|