@tiagojct/subsub 0.21.0 → 0.22.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 +8 -0
- package/README.md +5 -1
- package/dist/cli.js +10 -0
- package/dist/rpc.js +185 -0
- package/dist/screens.js +111 -0
- package/dist/subsub.js +13 -6
- package/dist/tui-view.js +228 -0
- package/dist/tui.js +651 -0
- package/dist/web.js +9 -158
- package/package.json +1 -1
- package/src/cli.ts +9 -0
- package/src/rpc.ts +198 -0
- package/src/screens.ts +156 -0
- package/src/subsub.ts +12 -6
- package/src/tui-view.ts +270 -0
- package/src/tui.ts +710 -0
- package/src/web.ts +10 -160
package/src/web.ts
CHANGED
|
@@ -14,20 +14,15 @@
|
|
|
14
14
|
* stops by itself, so a shortcut can start it without a terminal.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
18
17
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
19
18
|
import { createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, appendFileSync } from "node:fs";
|
|
20
19
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
21
20
|
import { homedir } from "node:os";
|
|
22
21
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
23
|
-
import { StringDecoder } from "node:string_decoder";
|
|
24
22
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
25
|
-
import { configPath,
|
|
23
|
+
import { configPath, loadConfig, RECOMMENDED_MODELS, readConfigFile, saveConfig, type SubsubConfig, uiLanguage, withStarbuck } from "./config.ts";
|
|
26
24
|
import { TESTED_MODELS } from "./init.ts";
|
|
27
25
|
import { KEY_PROVIDERS, saveApiKey } from "./keys.ts";
|
|
28
|
-
import { NOTE_DIRS, obsidianRoot, SETTINGS_DIR } from "./layout.ts";
|
|
29
|
-
import { field, frontMatter } from "./notes.ts";
|
|
30
|
-
import { realish, within } from "./paths.ts";
|
|
31
26
|
|
|
32
27
|
// ---------------------------------------------------------------- options
|
|
33
28
|
|
|
@@ -68,130 +63,10 @@ export { KEY_PROVIDERS, saveApiKey } from "./keys.ts";
|
|
|
68
63
|
import { allowedFile, listNotes, openExternal, openFile, OPENABLE, wikilinkTarget } from "./files.ts";
|
|
69
64
|
export { allowedFile, listNotes, openExternal, OPENABLE, wikilinkTarget };
|
|
70
65
|
|
|
71
|
-
// ---------------------------------------------------------------- pi in RPC mode
|
|
72
|
-
|
|
73
|
-
type Listener = (ev: any) => void;
|
|
74
|
-
|
|
75
|
-
/** pi as a child process in RPC mode. Strict JSONL: records end at LF only. */
|
|
76
|
-
export class Agent {
|
|
77
|
-
proc?: ChildProcess;
|
|
78
|
-
private decoder = new StringDecoder("utf8");
|
|
79
|
-
private buf = "";
|
|
80
|
-
private n = 0;
|
|
81
|
-
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }>();
|
|
82
|
-
private listeners = new Set<Listener>();
|
|
83
|
-
stderr = "";
|
|
84
|
-
onExit?: (code: number | null) => void;
|
|
85
|
-
|
|
86
|
-
private readonly command: string;
|
|
87
|
-
private readonly args: string[];
|
|
88
|
-
private readonly cwd: string;
|
|
89
|
-
private readonly env: NodeJS.ProcessEnv;
|
|
90
|
-
|
|
91
|
-
constructor(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
|
92
|
-
this.command = command;
|
|
93
|
-
this.args = args;
|
|
94
|
-
this.cwd = cwd;
|
|
95
|
-
this.env = env;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
start(extra: string[] = []): void {
|
|
99
|
-
this.buf = "";
|
|
100
|
-
this.decoder = new StringDecoder("utf8");
|
|
101
|
-
const proc = spawn(this.command, [...this.args, ...extra], { cwd: this.cwd, env: this.env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
102
|
-
this.proc = proc;
|
|
103
|
-
proc.stdout!.on("data", (chunk: Buffer) => this.feed(this.decoder.write(chunk)));
|
|
104
|
-
proc.stderr!.on("data", (d: Buffer) => {
|
|
105
|
-
this.stderr = (this.stderr + d.toString()).slice(-20_000);
|
|
106
|
-
});
|
|
107
|
-
proc.once("exit", (code) => {
|
|
108
|
-
if (this.proc !== proc) return;
|
|
109
|
-
this.proc = undefined;
|
|
110
|
-
for (const p of this.pending.values()) {
|
|
111
|
-
clearTimeout(p.timer);
|
|
112
|
-
p.reject(new Error(`pi stopped (code ${code}). ${this.stderr.slice(-500)}`));
|
|
113
|
-
}
|
|
114
|
-
this.pending.clear();
|
|
115
|
-
this.onExit?.(code);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private feed(text: string): void {
|
|
120
|
-
this.buf += text;
|
|
121
|
-
let i: number;
|
|
122
|
-
while ((i = this.buf.indexOf("\n")) >= 0) {
|
|
123
|
-
const line = this.buf.slice(0, i).replace(/\r$/, "");
|
|
124
|
-
this.buf = this.buf.slice(i + 1);
|
|
125
|
-
if (!line.trim()) continue;
|
|
126
|
-
let rec: any;
|
|
127
|
-
try {
|
|
128
|
-
rec = JSON.parse(line);
|
|
129
|
-
} catch {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (rec.type === "response" && typeof rec.id === "string" && this.pending.has(rec.id)) {
|
|
133
|
-
const p = this.pending.get(rec.id)!;
|
|
134
|
-
this.pending.delete(rec.id);
|
|
135
|
-
clearTimeout(p.timer);
|
|
136
|
-
if (rec.success === false) p.reject(new Error(rec.error ?? `${rec.command} failed`));
|
|
137
|
-
else p.resolve(rec.data);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
for (const l of this.listeners) l(rec);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
on(l: Listener): () => void {
|
|
145
|
-
this.listeners.add(l);
|
|
146
|
-
return () => this.listeners.delete(l);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
write(rec: unknown): void {
|
|
150
|
-
const stdin = this.proc?.stdin;
|
|
151
|
-
if (!stdin || !stdin.writable) throw new Error("pi is not running");
|
|
152
|
-
stdin.write(`${JSON.stringify(rec)}\n`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
request<T = any>(cmd: Record<string, unknown>, timeoutMs = 30_000): Promise<T> {
|
|
156
|
-
const id = `w${++this.n}`;
|
|
157
|
-
return new Promise<T>((resolvePromise, reject) => {
|
|
158
|
-
const timer = setTimeout(() => {
|
|
159
|
-
this.pending.delete(id);
|
|
160
|
-
reject(new Error(`pi did not answer ${String(cmd.type)} in time`));
|
|
161
|
-
}, timeoutMs);
|
|
162
|
-
this.pending.set(id, { resolve: resolvePromise, reject, timer });
|
|
163
|
-
try {
|
|
164
|
-
this.write({ ...cmd, id });
|
|
165
|
-
} catch (err) {
|
|
166
|
-
clearTimeout(timer);
|
|
167
|
-
this.pending.delete(id);
|
|
168
|
-
reject(err as Error);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
}
|
|
66
|
+
// ---------------------------------------------------------------- pi in RPC mode (src/rpc.ts)
|
|
172
67
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!proc) return;
|
|
176
|
-
this.proc = undefined;
|
|
177
|
-
for (const p of this.pending.values()) {
|
|
178
|
-
clearTimeout(p.timer);
|
|
179
|
-
p.reject(new Error("pi was stopped"));
|
|
180
|
-
}
|
|
181
|
-
this.pending.clear();
|
|
182
|
-
await new Promise<void>((done) => {
|
|
183
|
-
const t = setTimeout(() => {
|
|
184
|
-
proc.kill("SIGKILL");
|
|
185
|
-
done();
|
|
186
|
-
}, 3000);
|
|
187
|
-
proc.once("exit", () => {
|
|
188
|
-
clearTimeout(t);
|
|
189
|
-
done();
|
|
190
|
-
});
|
|
191
|
-
proc.kill();
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
68
|
+
import { Agent, findLibrary, isDialog, Side, withHints } from "./rpc.ts";
|
|
69
|
+
export { Agent };
|
|
195
70
|
|
|
196
71
|
// ---------------------------------------------------------------- helpers
|
|
197
72
|
|
|
@@ -223,14 +98,6 @@ const CSP = [
|
|
|
223
98
|
"frame-ancestors 'none'",
|
|
224
99
|
].join("; ");
|
|
225
100
|
|
|
226
|
-
function readText(path: string): string {
|
|
227
|
-
try {
|
|
228
|
-
return readFileSync(path, "utf8");
|
|
229
|
-
} catch {
|
|
230
|
-
return "";
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
101
|
function same(a: string, b: string): boolean {
|
|
235
102
|
const x = Buffer.from(a);
|
|
236
103
|
const y = Buffer.from(b);
|
|
@@ -319,7 +186,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
319
186
|
const clients = new Set<ServerResponse>();
|
|
320
187
|
const dialogs = new Map<string, any>();
|
|
321
188
|
let subsubState: unknown = null;
|
|
322
|
-
|
|
189
|
+
const side = new Side();
|
|
323
190
|
let busy = false;
|
|
324
191
|
let lastSeen = Date.now();
|
|
325
192
|
let restarting = false;
|
|
@@ -344,22 +211,12 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
344
211
|
|
|
345
212
|
agent.on((ev) => {
|
|
346
213
|
if (ev.type === "extension_ui_request") {
|
|
347
|
-
if (
|
|
214
|
+
if (isDialog(ev)) {
|
|
348
215
|
dialogs.set(ev.id, ev);
|
|
349
216
|
broadcast(ev);
|
|
350
|
-
} else if (
|
|
351
|
-
|
|
352
|
-
subsubState = ev.statusText ? JSON.parse(ev.statusText) : null;
|
|
353
|
-
} catch {
|
|
354
|
-
subsubState = null;
|
|
355
|
-
}
|
|
217
|
+
} else if (side.take(ev) === "state") {
|
|
218
|
+
subsubState = side.state;
|
|
356
219
|
broadcast({ type: "subsub_state", state: subsubState });
|
|
357
|
-
} else if (ev.method === "setStatus" && ev.statusKey === "subsub.find") {
|
|
358
|
-
try {
|
|
359
|
-
lastFind = ev.statusText ? JSON.parse(ev.statusText) : null;
|
|
360
|
-
} catch {
|
|
361
|
-
lastFind = null;
|
|
362
|
-
}
|
|
363
220
|
} else if (ev.method === "notify") {
|
|
364
221
|
broadcast(ev);
|
|
365
222
|
}
|
|
@@ -401,13 +258,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
401
258
|
out.session = await agent.request({ type: "get_state" });
|
|
402
259
|
lastSession = (out.session as { sessionFile?: string }).sessionFile ?? lastSession;
|
|
403
260
|
out.messages = ((await agent.request({ type: "get_messages" })) as { messages: unknown[] }).messages;
|
|
404
|
-
|
|
405
|
-
// RPC has no argument hints: read them from the prompt files.
|
|
406
|
-
out.commands = commands.map((c) => {
|
|
407
|
-
const front = c.source === "prompt" && c.sourceInfo?.path ? frontMatter(readText(c.sourceInfo.path)) : undefined;
|
|
408
|
-
const hint = front && field(front, "argument-hint");
|
|
409
|
-
return hint ? { ...c, hint } : c;
|
|
410
|
-
});
|
|
261
|
+
out.commands = withHints(((await agent.request({ type: "get_commands" })) as { commands: Array<Record<string, any>> }).commands);
|
|
411
262
|
} catch (err) {
|
|
412
263
|
out.error = (err as Error).message;
|
|
413
264
|
}
|
|
@@ -658,8 +509,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
658
509
|
case "GET /api/library": {
|
|
659
510
|
const q = (url.searchParams.get("q") ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
660
511
|
if (!q) return send(res, 200, { q, items: [] });
|
|
661
|
-
|
|
662
|
-
return send(res, 200, lastFind?.q === q ? lastFind : { q, items: [] });
|
|
512
|
+
return send(res, 200, await findLibrary(agent, side, q));
|
|
663
513
|
}
|
|
664
514
|
case "POST /api/session/name": {
|
|
665
515
|
const body = await readJson(req);
|