@brianmichel/pi-noodle 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/LICENSE +21 -0
- package/README.md +231 -0
- package/index.ts +1 -0
- package/package.json +70 -0
- package/src/AGENTS.md +33 -0
- package/src/commands/index.ts +51 -0
- package/src/commands/memory-crud.ts +136 -0
- package/src/commands/review.ts +291 -0
- package/src/commands/setup.ts +189 -0
- package/src/commands/status.ts +32 -0
- package/src/commands/ui.ts +14 -0
- package/src/commands/web.ts +40 -0
- package/src/commands.ts +1 -0
- package/src/config/schema.ts +234 -0
- package/src/config-screen.ts +439 -0
- package/src/config.ts +159 -0
- package/src/constants.ts +1 -0
- package/src/debug-overlay.ts +230 -0
- package/src/extension.ts +166 -0
- package/src/index.ts +1 -0
- package/src/memory/backend.ts +22 -0
- package/src/memory/embedder.ts +7 -0
- package/src/memory/embedders/lm-studio.ts +25 -0
- package/src/memory/embedders/openai.ts +66 -0
- package/src/memory/extractor.ts +189 -0
- package/src/memory/policy.ts +325 -0
- package/src/memory/project-identity.ts +51 -0
- package/src/memory/runtime.ts +70 -0
- package/src/memory/service.ts +761 -0
- package/src/memory/turso-backend.ts +716 -0
- package/src/memory/types.ts +192 -0
- package/src/notifications.ts +11 -0
- package/src/queue.ts +42 -0
- package/src/session.ts +72 -0
- package/src/tools.ts +172 -0
- package/src/types.ts +81 -0
- package/src/utils.ts +68 -0
- package/src/web/dev.ts +7 -0
- package/src/web/index.html +1963 -0
- package/src/web/manager.ts +92 -0
- package/src/web/run.ts +33 -0
- package/src/web/server.ts +212 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { platform } from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const RUN_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "run.ts");
|
|
9
|
+
const STATE_PATH = join(homedir(), ".pi", "noodle", "explorer.json");
|
|
10
|
+
|
|
11
|
+
export type ExplorerState = {
|
|
12
|
+
pid: number;
|
|
13
|
+
port: number;
|
|
14
|
+
dev?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function explorerStatePath(): string {
|
|
18
|
+
return STATE_PATH;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readExplorerState(): ExplorerState | null {
|
|
22
|
+
if (!existsSync(STATE_PATH)) return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(STATE_PATH, "utf8")) as ExplorerState;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearExplorerState(): void {
|
|
31
|
+
try {
|
|
32
|
+
unlinkSync(STATE_PATH);
|
|
33
|
+
} catch {
|
|
34
|
+
/* already gone */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isExplorerRunning(): boolean {
|
|
39
|
+
const state = readExplorerState();
|
|
40
|
+
if (!state) return false;
|
|
41
|
+
try {
|
|
42
|
+
process.kill(state.pid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
clearExplorerState();
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function openExplorerBrowser(port: number): void {
|
|
51
|
+
const url = `http://localhost:${port}`;
|
|
52
|
+
const cmd =
|
|
53
|
+
platform === "darwin"
|
|
54
|
+
? "open"
|
|
55
|
+
: platform === "win32"
|
|
56
|
+
? "start"
|
|
57
|
+
: "xdg-open";
|
|
58
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function spawnExplorer(port: number, dev = false): ExplorerState | null {
|
|
62
|
+
if (isExplorerRunning()) {
|
|
63
|
+
return readExplorerState();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const args = [RUN_SCRIPT, String(port)];
|
|
67
|
+
if (dev) args.push("--dev");
|
|
68
|
+
|
|
69
|
+
const child = spawn("bun", args, {
|
|
70
|
+
detached: true,
|
|
71
|
+
stdio: "ignore",
|
|
72
|
+
env: process.env,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!child.pid) return null;
|
|
76
|
+
child.unref();
|
|
77
|
+
return { pid: child.pid, port, dev };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function stopExplorer(): boolean {
|
|
81
|
+
const state = readExplorerState();
|
|
82
|
+
if (!state) return false;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
process.kill(state.pid, "SIGTERM");
|
|
86
|
+
} catch {
|
|
87
|
+
/* process may already be gone */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
clearExplorerState();
|
|
91
|
+
return true;
|
|
92
|
+
}
|
package/src/web/run.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { memoryService } from "../memory/runtime.ts";
|
|
5
|
+
import {
|
|
6
|
+
clearExplorerState,
|
|
7
|
+
explorerStatePath,
|
|
8
|
+
type ExplorerState,
|
|
9
|
+
} from "./manager.ts";
|
|
10
|
+
import { startMemoryExplorer } from "./server.ts";
|
|
11
|
+
|
|
12
|
+
const port = parseInt(process.argv[2] ?? "3000", 10);
|
|
13
|
+
const dev = process.argv.includes("--dev");
|
|
14
|
+
|
|
15
|
+
mkdirSync(dirname(explorerStatePath()), { recursive: true });
|
|
16
|
+
const state: ExplorerState = { pid: process.pid, port, dev };
|
|
17
|
+
writeFileSync(explorerStatePath(), JSON.stringify(state));
|
|
18
|
+
|
|
19
|
+
function cleanup(): void {
|
|
20
|
+
clearExplorerState();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const server = startMemoryExplorer(memoryService, port, { dev, openBrowser: true });
|
|
24
|
+
|
|
25
|
+
function shutdown(): void {
|
|
26
|
+
cleanup();
|
|
27
|
+
server.stop();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.on("SIGTERM", shutdown);
|
|
32
|
+
process.on("SIGINT", shutdown);
|
|
33
|
+
process.on("exit", cleanup);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readFileSync, watch } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { platform } from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type { ServerWebSocket } from "bun";
|
|
7
|
+
import type { MemoryRecord } from "../memory/types.ts";
|
|
8
|
+
|
|
9
|
+
const WEB_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const HTML_PATH = join(WEB_DIR, "index.html");
|
|
11
|
+
|
|
12
|
+
const PING_INTERVAL_MS = 4_000;
|
|
13
|
+
const STALE_TIMEOUT_MS = 12_000;
|
|
14
|
+
const SHUTDOWN_DELAY_MS = 1_500;
|
|
15
|
+
|
|
16
|
+
export type MemoryExplorerOptions = {
|
|
17
|
+
dev?: boolean;
|
|
18
|
+
openBrowser?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function startMemoryExplorer(
|
|
22
|
+
service: {
|
|
23
|
+
list: () => Promise<MemoryRecord[]>;
|
|
24
|
+
update: (id: string, input: { text?: string; metadata?: Record<string, unknown> }) => Promise<void>;
|
|
25
|
+
delete: (id: string) => Promise<void>;
|
|
26
|
+
},
|
|
27
|
+
port = 3000,
|
|
28
|
+
options: MemoryExplorerOptions = {},
|
|
29
|
+
): ReturnType<typeof Bun.serve> {
|
|
30
|
+
const dev = options.dev ?? false;
|
|
31
|
+
const openBrowser = options.openBrowser ?? !dev;
|
|
32
|
+
let shutdownTimer: ReturnType<typeof setTimeout> | null = null;
|
|
33
|
+
let html = readFileSync(HTML_PATH, "utf8");
|
|
34
|
+
const sockets = new Set<ServerWebSocket<unknown>>();
|
|
35
|
+
const lastSeen = new Map<ServerWebSocket<unknown>, number>();
|
|
36
|
+
|
|
37
|
+
function scheduleShutdownIfIdle(): void {
|
|
38
|
+
if (dev) return;
|
|
39
|
+
if (sockets.size > 0) {
|
|
40
|
+
if (shutdownTimer) {
|
|
41
|
+
clearTimeout(shutdownTimer);
|
|
42
|
+
shutdownTimer = null;
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (shutdownTimer) return;
|
|
47
|
+
shutdownTimer = setTimeout(() => {
|
|
48
|
+
shutdownTimer = null;
|
|
49
|
+
if (sockets.size > 0) return;
|
|
50
|
+
console.log("All tabs closed. Shutting down.");
|
|
51
|
+
server.stop();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}, SHUTDOWN_DELAY_MS);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function touch(ws: ServerWebSocket<unknown>): void {
|
|
57
|
+
lastSeen.set(ws, Date.now());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pruneStaleSockets(): void {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
for (const ws of sockets) {
|
|
63
|
+
if (now - (lastSeen.get(ws) ?? 0) > STALE_TIMEOUT_MS) {
|
|
64
|
+
ws.close(1000, "stale");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function broadcastReload(): void {
|
|
70
|
+
const payload = JSON.stringify({ type: "reload" });
|
|
71
|
+
for (const ws of sockets) {
|
|
72
|
+
ws.send(payload);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function invalidateAndReload(): void {
|
|
77
|
+
html = readFileSync(HTML_PATH, "utf8");
|
|
78
|
+
console.log("[dev] UI updated — reloading browser");
|
|
79
|
+
broadcastReload();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (dev) {
|
|
83
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
|
+
watch(WEB_DIR, { recursive: true }, (_event, filename) => {
|
|
85
|
+
if (!filename || filename === "server.ts" || filename === "dev.ts")
|
|
86
|
+
return;
|
|
87
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
88
|
+
reloadTimer = setTimeout(() => {
|
|
89
|
+
reloadTimer = null;
|
|
90
|
+
invalidateAndReload();
|
|
91
|
+
}, 100);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const server = Bun.serve({
|
|
96
|
+
port,
|
|
97
|
+
fetch: async (req, server) => {
|
|
98
|
+
const url = new URL(req.url);
|
|
99
|
+
if (server.upgrade(req)) return;
|
|
100
|
+
|
|
101
|
+
if (url.pathname === "/") {
|
|
102
|
+
return new Response(html, {
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "text/html",
|
|
105
|
+
...(dev ? { "Cache-Control": "no-store" } : {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (url.pathname === "/api/memories" && req.method === "GET") {
|
|
111
|
+
try {
|
|
112
|
+
const memories = (await service.list()).map(memoryToApi);
|
|
113
|
+
return Response.json(memories);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const match = url.pathname.match(/^\/api\/memories\/([^/]+)$/);
|
|
120
|
+
if (match && req.method === "PATCH") {
|
|
121
|
+
try {
|
|
122
|
+
const body = await req.json().catch(() => ({}));
|
|
123
|
+
const text = typeof body?.text === "string" ? body.text.trim() : undefined;
|
|
124
|
+
if (!text) return Response.json({ error: "text is required" }, { status: 400 });
|
|
125
|
+
await service.update(decodeURIComponent(match[1] ?? ""), { text });
|
|
126
|
+
return Response.json({ updated: true });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (match && req.method === "DELETE") {
|
|
133
|
+
try {
|
|
134
|
+
await service.delete(decodeURIComponent(match[1] ?? ""));
|
|
135
|
+
return Response.json({ deleted: true });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return new Response("Not found", { status: 404 });
|
|
142
|
+
},
|
|
143
|
+
websocket: {
|
|
144
|
+
open(ws) {
|
|
145
|
+
touch(ws);
|
|
146
|
+
sockets.add(ws);
|
|
147
|
+
if (shutdownTimer) {
|
|
148
|
+
clearTimeout(shutdownTimer);
|
|
149
|
+
shutdownTimer = null;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
close(ws) {
|
|
153
|
+
sockets.delete(ws);
|
|
154
|
+
lastSeen.delete(ws);
|
|
155
|
+
scheduleShutdownIfIdle();
|
|
156
|
+
},
|
|
157
|
+
message(ws, raw) {
|
|
158
|
+
touch(ws);
|
|
159
|
+
try {
|
|
160
|
+
const msg = JSON.parse(String(raw)) as { type?: string };
|
|
161
|
+
if (msg.type === "ping") {
|
|
162
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (msg.type === "bye") {
|
|
166
|
+
ws.close(1000, "client bye");
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
/* ignore malformed messages */
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!dev) {
|
|
176
|
+
setInterval(() => {
|
|
177
|
+
pruneStaleSockets();
|
|
178
|
+
scheduleShutdownIfIdle();
|
|
179
|
+
}, PING_INTERVAL_MS);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const url = `http://localhost:${port}`;
|
|
183
|
+
console.log(`Memory Explorer: ${url}${dev ? " (dev — hot reload on)" : ""}`);
|
|
184
|
+
|
|
185
|
+
if (openBrowser) {
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
const cmd =
|
|
188
|
+
platform === "darwin"
|
|
189
|
+
? "open"
|
|
190
|
+
: platform === "win32"
|
|
191
|
+
? "start"
|
|
192
|
+
: "xdg-open";
|
|
193
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true });
|
|
194
|
+
}, 100);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return server;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function memoryToApi(memory: MemoryRecord): Record<string, unknown> {
|
|
201
|
+
return {
|
|
202
|
+
id: memory.id,
|
|
203
|
+
text: memory.text,
|
|
204
|
+
category: memory.category,
|
|
205
|
+
categories: memory.categories,
|
|
206
|
+
scope: memory.scope ?? {},
|
|
207
|
+
metadata: memory.metadata,
|
|
208
|
+
createdAt: memory.createdAt ?? null,
|
|
209
|
+
retrievalCount: memory.retrievalCount ?? 0,
|
|
210
|
+
lastRetrieved: memory.lastRetrieved ?? null,
|
|
211
|
+
};
|
|
212
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"types": ["node", "bun"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["index.ts", "src/**/*.ts", "test/**/*.ts"]
|
|
17
|
+
}
|