@seangle/pi-remember 0.2.0 → 0.3.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 +41 -9
- package/README.md +149 -223
- package/dist/command.js +56 -0
- package/dist/context.js +57 -0
- package/dist/index.js +124 -0
- package/dist/tools.js +552 -0
- package/package.json +23 -24
- package/command.ts +0 -36
- package/index.ts +0 -751
- package/inject.ts +0 -45
- package/migration.ts +0 -458
- package/notes.ts +0 -131
- package/search.ts +0 -250
- package/store.ts +0 -730
- package/summary.ts +0 -70
- package/workspace.ts +0 -158
package/summary.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// Pure helpers for the opt-in exit summary. No pi imports, so they are
|
|
2
|
-
// unit-testable with plain node.
|
|
3
|
-
|
|
4
|
-
interface ContentBlock {
|
|
5
|
-
type?: string;
|
|
6
|
-
text?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface SessionEntry {
|
|
10
|
-
type?: string;
|
|
11
|
-
message?: {
|
|
12
|
-
role?: string;
|
|
13
|
-
content?: unknown;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function extractText(content: unknown): string {
|
|
18
|
-
if (typeof content === "string") return content;
|
|
19
|
-
if (!Array.isArray(content)) return "";
|
|
20
|
-
return (content as ContentBlock[])
|
|
21
|
-
.filter((c) => c && typeof c === "object" && c.type === "text" && typeof c.text === "string")
|
|
22
|
-
.map((c) => c.text as string)
|
|
23
|
-
.join("\n");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Flatten the session tail into `User:` / `Assistant:` lines, capped. */
|
|
27
|
-
export function gatherConversationText(entries: SessionEntry[], maxChars = 20_000): string {
|
|
28
|
-
const parts: string[] = [];
|
|
29
|
-
for (const entry of entries) {
|
|
30
|
-
if (!entry || entry.type !== "message" || !entry.message?.role) continue;
|
|
31
|
-
const role = entry.message.role;
|
|
32
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
33
|
-
const text = extractText(entry.message.content).trim();
|
|
34
|
-
if (text.length === 0) continue;
|
|
35
|
-
parts.push(`${role === "user" ? "User" : "Assistant"}: ${text}`);
|
|
36
|
-
}
|
|
37
|
-
const joined = parts.join("\n\n");
|
|
38
|
-
return joined.length > maxChars ? joined.slice(-maxChars) : joined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function buildSummaryPrompt(conversationText: string): string {
|
|
42
|
-
return [
|
|
43
|
-
"Extract durable facts from this coding session that are worth remembering in future sessions.",
|
|
44
|
-
"Capture only knowledge that was expensive to obtain: user preferences, decisions, lessons learned, environment quirks, research findings.",
|
|
45
|
-
"Skip trivial or session-specific details, todos, and in-progress work. If nothing is worth remembering, output exactly: NONE",
|
|
46
|
-
"Output at most 6 lines. Each line is ONE concise fact (max ~30 words, one line, max 300 chars) starting with a #tag (#preference, #decision, #lesson, or #fact).",
|
|
47
|
-
"",
|
|
48
|
-
"<session>",
|
|
49
|
-
conversationText,
|
|
50
|
-
"</session>",
|
|
51
|
-
].join("\n");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Turn the model's reply into clean fact lines. Overlong lines are dropped. */
|
|
55
|
-
export function parseFacts(summaryText: string): string[] {
|
|
56
|
-
const trimmed = summaryText.trim();
|
|
57
|
-
if (trimmed.length === 0 || trimmed.toUpperCase() === "NONE") return [];
|
|
58
|
-
return trimmed
|
|
59
|
-
.split(/\r?\n/)
|
|
60
|
-
.map((s) => s.replace(/^\s*[-*]\s*/, "").trim())
|
|
61
|
-
.filter(
|
|
62
|
-
(s) =>
|
|
63
|
-
s.length >= 8 &&
|
|
64
|
-
s.length <= 300 &&
|
|
65
|
-
!s.startsWith("```") &&
|
|
66
|
-
!s.startsWith("# ") &&
|
|
67
|
-
!/^none$/i.test(s) &&
|
|
68
|
-
!/^here are/i.test(s),
|
|
69
|
-
);
|
|
70
|
-
}
|
package/workspace.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { resolveStores, type Scope, type StorePaths } from "./store.ts";
|
|
6
|
-
|
|
7
|
-
type WorkspaceResult =
|
|
8
|
-
| { status: "resolved"; root: string; stores: Record<Scope, StorePaths> }
|
|
9
|
-
| { status: "error"; message: string };
|
|
10
|
-
|
|
11
|
-
type InitializationResult =
|
|
12
|
-
| { status: "initialized" | "existing" }
|
|
13
|
-
| { status: "error"; message: string };
|
|
14
|
-
|
|
15
|
-
// StorePaths intentionally has no workspace field. Retain the exact root even
|
|
16
|
-
// for nested configDirName values without changing that shared store contract.
|
|
17
|
-
const workspaceRoots = new WeakMap<StorePaths, string>();
|
|
18
|
-
|
|
19
|
-
function errorCode(error: unknown): string | undefined {
|
|
20
|
-
return typeof error === "object" && error !== null && "code" in error &&
|
|
21
|
-
typeof error.code === "string" ? error.code : undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Resolve only existing workspaces; resolution itself never creates anything. */
|
|
25
|
-
export function resolveWorkspaceStores(
|
|
26
|
-
cwd: string,
|
|
27
|
-
globalDir: string,
|
|
28
|
-
configDirName: string,
|
|
29
|
-
root?: string,
|
|
30
|
-
): WorkspaceResult {
|
|
31
|
-
const input = root ?? cwd;
|
|
32
|
-
if (!input.trim()) return { status: "error", message: "Workspace root must not be blank." };
|
|
33
|
-
if (input.includes("\0")) return { status: "error", message: "Workspace root must not contain NUL characters." };
|
|
34
|
-
if (input.startsWith("~") && input !== "~" && !input.startsWith("~/")) {
|
|
35
|
-
return { status: "error", message: "Unsupported workspace root: use ~ or ~/path, not ~username." };
|
|
36
|
-
}
|
|
37
|
-
const expanded = input === "~" ? homedir() : input.startsWith("~/")
|
|
38
|
-
? join(homedir(), input.slice(2)) : input;
|
|
39
|
-
const candidate = root === undefined ? resolve(cwd) : resolve(cwd, expanded);
|
|
40
|
-
try {
|
|
41
|
-
const canonicalRoot = realpathSync(candidate);
|
|
42
|
-
if (!statSync(canonicalRoot).isDirectory()) {
|
|
43
|
-
return { status: "error", message: `Workspace root is not a directory: ${JSON.stringify(candidate)}.` };
|
|
44
|
-
}
|
|
45
|
-
const stores = resolveStores(canonicalRoot, globalDir, configDirName);
|
|
46
|
-
workspaceRoots.set(stores.local, canonicalRoot);
|
|
47
|
-
return { status: "resolved", root: canonicalRoot, stores };
|
|
48
|
-
} catch (error) {
|
|
49
|
-
return { status: "error", message: `Cannot resolve workspace root ${JSON.stringify(candidate)}: ${String(error)}` };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function workspaceRoot(store: StorePaths): string {
|
|
54
|
-
return workspaceRoots.get(store) ?? dirname(dirname(store.dir));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function canonicalRootError(store: StorePaths): string | null {
|
|
58
|
-
const root = workspaceRoot(store);
|
|
59
|
-
try {
|
|
60
|
-
if (realpathSync(root) !== resolve(root)) {
|
|
61
|
-
return `Workspace root changed since resolution: ${JSON.stringify(root)}. Refusing to follow its replacement.`;
|
|
62
|
-
}
|
|
63
|
-
if (!statSync(root).isDirectory()) {
|
|
64
|
-
return `Workspace root is not a directory: ${JSON.stringify(root)}.`;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
return `Cannot access workspace root ${JSON.stringify(root)}: ${String(error)}`;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Recheck after queue waits, before any local memory access or mutation. */
|
|
73
|
-
export function workspaceAccessError(
|
|
74
|
-
stores: Record<Scope, StorePaths>,
|
|
75
|
-
scopes: readonly Scope[],
|
|
76
|
-
requireInitialized = true,
|
|
77
|
-
): string | null {
|
|
78
|
-
if (!scopes.includes("local")) return null;
|
|
79
|
-
return canonicalRootError(stores.local) ??
|
|
80
|
-
(requireInitialized ? initializationError(stores, scopes) : null);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Tool-boundary guard only: low-level reads may still treat missing memory as empty. */
|
|
84
|
-
export function initializationError(
|
|
85
|
-
stores: Record<Scope, StorePaths>,
|
|
86
|
-
scopes: readonly Scope[],
|
|
87
|
-
): string | null {
|
|
88
|
-
if (!scopes.includes("local")) return null;
|
|
89
|
-
const store = stores.local;
|
|
90
|
-
const root = workspaceRoot(store);
|
|
91
|
-
const guidance = `Local memory is not initialized at ${JSON.stringify(store.memoryFile)}. ` +
|
|
92
|
-
`Run memory_init({root:${JSON.stringify(root)}}).`;
|
|
93
|
-
try {
|
|
94
|
-
return statSync(store.memoryFile).isFile() ? null : guidance + " MEMORY.md must be a regular file.";
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (errorCode(error) === "ENOENT") return guidance;
|
|
97
|
-
return `${guidance} Cannot inspect MEMORY.md: ${String(error)}`;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Exclusively create an empty index; never truncate or replace an existing entry. */
|
|
102
|
-
export async function initializeMemory(store: StorePaths): Promise<InitializationResult> {
|
|
103
|
-
try {
|
|
104
|
-
const rootError = store.scope === "local" ? canonicalRootError(store) : null;
|
|
105
|
-
if (rootError) return { status: "error", message: rootError };
|
|
106
|
-
try {
|
|
107
|
-
if ((await stat(store.memoryFile)).isFile()) {
|
|
108
|
-
const error = store.scope === "local" ? canonicalRootError(store) : null;
|
|
109
|
-
return error ? { status: "error", message: error } : { status: "existing" };
|
|
110
|
-
}
|
|
111
|
-
return { status: "error", message: `Memory index is not a regular file: ${JSON.stringify(store.memoryFile)}.` };
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (errorCode(error) !== "ENOENT") throw error;
|
|
114
|
-
}
|
|
115
|
-
if (store.scope === "global") {
|
|
116
|
-
await mkdir(store.dir, { recursive: true });
|
|
117
|
-
} else {
|
|
118
|
-
const root = workspaceRoot(store);
|
|
119
|
-
const belowRoot = relative(root, store.dir);
|
|
120
|
-
if (!belowRoot || isAbsolute(belowRoot) || belowRoot.split(sep).includes("..")) {
|
|
121
|
-
return { status: "error", message: `Memory directory must be below workspace root: ${JSON.stringify(store.dir)}.` };
|
|
122
|
-
}
|
|
123
|
-
// Never use recursive mkdir locally: it could recreate a vanished root.
|
|
124
|
-
// Create only the config/memory components, leaving notes/recovery lazy.
|
|
125
|
-
let directory = root;
|
|
126
|
-
for (const component of belowRoot.split(sep)) {
|
|
127
|
-
const error = canonicalRootError(store);
|
|
128
|
-
if (error) return { status: "error", message: error };
|
|
129
|
-
directory = join(directory, component);
|
|
130
|
-
try {
|
|
131
|
-
await mkdir(directory);
|
|
132
|
-
} catch (error) {
|
|
133
|
-
if (errorCode(error) !== "EEXIST") throw error;
|
|
134
|
-
if (!(await stat(directory)).isDirectory()) {
|
|
135
|
-
return { status: "error", message: `Memory path is not a directory: ${JSON.stringify(directory)}.` };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const error = canonicalRootError(store);
|
|
140
|
-
if (error) return { status: "error", message: error };
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
await writeFile(store.memoryFile, "", { flag: "wx" });
|
|
144
|
-
return { status: "initialized" };
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (errorCode(error) !== "EEXIST") throw error;
|
|
147
|
-
// A concurrent initializer may have won. stat follows symlinks, but a
|
|
148
|
-
// dangling link or a directory must not count as an initialized index.
|
|
149
|
-
if ((await stat(store.memoryFile)).isFile()) {
|
|
150
|
-
const error = store.scope === "local" ? canonicalRootError(store) : null;
|
|
151
|
-
return error ? { status: "error", message: error } : { status: "existing" };
|
|
152
|
-
}
|
|
153
|
-
return { status: "error", message: `Memory index is not a regular file: ${JSON.stringify(store.memoryFile)}.` };
|
|
154
|
-
}
|
|
155
|
-
} catch (error) {
|
|
156
|
-
return { status: "error", message: `Cannot initialize memory at ${JSON.stringify(store.memoryFile)}: ${String(error)}` };
|
|
157
|
-
}
|
|
158
|
-
}
|