@tiagojct/subsub 0.11.0 → 0.11.1
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/README.md +114 -179
- package/dist/bench.js +2 -2
- package/dist/cli.js +34 -1
- package/dist/config.js +3 -1
- package/dist/doctor.js +2 -1
- package/dist/init.js +19 -4
- package/dist/paths.js +20 -0
- package/dist/prompt.js +3 -0
- package/dist/shortcut.js +2 -2
- package/dist/subsub.js +32 -6
- package/dist/web.js +56 -25
- package/package.json +2 -3
- package/prompts/lit-note.md +1 -1
- package/src/bench.ts +2 -2
- package/src/cli.ts +34 -1
- package/src/config.ts +3 -1
- package/src/doctor.ts +2 -1
- package/src/init.ts +18 -4
- package/src/paths.ts +21 -0
- package/src/prompt.ts +6 -0
- package/src/shortcut.ts +2 -2
- package/src/subsub.ts +32 -7
- package/src/web.ts +53 -24
- package/web/app.js +28 -1
- package/web/i18n.js +7 -1
- package/docs/Subsub.md +0 -140
package/src/web.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from "node:os";
|
|
|
22
22
|
import { extname, join, normalize, resolve, sep } from "node:path";
|
|
23
23
|
import { StringDecoder } from "node:string_decoder";
|
|
24
24
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
25
|
-
import { expand, loadConfig, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
25
|
+
import { configPath, expand, loadConfig, readConfigFile, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
26
26
|
import { TESTED_MODELS } from "./init.ts";
|
|
27
27
|
import { obsidianRoot } from "./layout.ts";
|
|
28
28
|
import { realish, within } from "./paths.ts";
|
|
@@ -336,7 +336,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
336
336
|
env.PI_CODING_AGENT_DIR = agentDir; // for the session list below
|
|
337
337
|
|
|
338
338
|
const childEnv: NodeJS.ProcessEnv = { ...env, SUBSUB_WEB: "1" };
|
|
339
|
-
const baseArgs = [bin, "--mode", "rpc", "--here", ...
|
|
339
|
+
const baseArgs = [bin, "--mode", "rpc", "--here", ...opts.piArgs];
|
|
340
340
|
const agent = new Agent(process.execPath, baseArgs, cwd, childEnv);
|
|
341
341
|
|
|
342
342
|
// ---- state shared with the pages
|
|
@@ -346,8 +346,20 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
346
346
|
let busy = false;
|
|
347
347
|
let lastSeen = Date.now();
|
|
348
348
|
let restarting = false;
|
|
349
|
+
let switching = false; // a restart for a new model, add-on or key is under way
|
|
350
|
+
let lastSession: string | undefined;
|
|
349
351
|
let refreshAfterRun = false;
|
|
350
352
|
|
|
353
|
+
/** The mode to start pi in: the one on screen, else the --librarian flag. A session's own mode still wins. */
|
|
354
|
+
function modeArgs(): string[] {
|
|
355
|
+
const shown = (subsubState as { mode?: string } | null)?.mode;
|
|
356
|
+
if (shown === "librarian" || shown === "researcher") return [`--${shown}`];
|
|
357
|
+
return opts.librarian ? ["--librarian"] : [];
|
|
358
|
+
}
|
|
359
|
+
function startAgent(): void {
|
|
360
|
+
agent.start([...modeArgs(), ...(lastSession && existsSync(lastSession) ? ["--session", lastSession] : [])]);
|
|
361
|
+
}
|
|
362
|
+
|
|
351
363
|
function broadcast(ev: unknown): void {
|
|
352
364
|
const data = `data: ${JSON.stringify(ev)}\n\n`;
|
|
353
365
|
for (const c of clients) c.write(data);
|
|
@@ -384,12 +396,13 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
384
396
|
agent.onExit = (code) => {
|
|
385
397
|
if (restarting) return;
|
|
386
398
|
busy = false;
|
|
399
|
+
for (const id of dialogs.keys()) broadcast({ type: "dialog_closed", id });
|
|
387
400
|
dialogs.clear();
|
|
388
401
|
broadcast({ type: "agent_exit", code, stderr: agent.stderr.slice(-1500) });
|
|
389
402
|
log(`pi stopped (code ${code}).\n${agent.stderr.slice(-2000)}`);
|
|
390
403
|
};
|
|
391
404
|
|
|
392
|
-
|
|
405
|
+
startAgent();
|
|
393
406
|
|
|
394
407
|
async function snapshot(): Promise<Record<string, unknown>> {
|
|
395
408
|
const out: Record<string, unknown> = {
|
|
@@ -403,6 +416,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
403
416
|
};
|
|
404
417
|
try {
|
|
405
418
|
out.session = await agent.request({ type: "get_state" });
|
|
419
|
+
lastSession = (out.session as { sessionFile?: string }).sessionFile ?? lastSession;
|
|
406
420
|
out.messages = ((await agent.request({ type: "get_messages" })) as { messages: unknown[] }).messages;
|
|
407
421
|
out.commands = ((await agent.request({ type: "get_commands" })) as { commands: unknown[] }).commands;
|
|
408
422
|
} catch (err) {
|
|
@@ -422,11 +436,10 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
422
436
|
}
|
|
423
437
|
|
|
424
438
|
async function restartAgent(): Promise<void> {
|
|
425
|
-
let sessionFile: string | undefined;
|
|
426
439
|
try {
|
|
427
|
-
|
|
440
|
+
lastSession = ((await agent.request({ type: "get_state" })) as { sessionFile?: string }).sessionFile ?? lastSession;
|
|
428
441
|
} catch {
|
|
429
|
-
/*
|
|
442
|
+
/* keep the last one known */
|
|
430
443
|
}
|
|
431
444
|
restarting = true;
|
|
432
445
|
for (const id of dialogs.keys()) broadcast({ type: "dialog_closed", id });
|
|
@@ -434,10 +447,23 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
434
447
|
await agent.stop();
|
|
435
448
|
restarting = false;
|
|
436
449
|
busy = false;
|
|
437
|
-
|
|
450
|
+
startAgent();
|
|
438
451
|
broadcast(await snapshot());
|
|
439
452
|
}
|
|
440
453
|
|
|
454
|
+
/** Run a change that restarts pi, one at a time. */
|
|
455
|
+
async function withRestart(res: ServerResponse, change: () => Promise<void> | void): Promise<void> {
|
|
456
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
457
|
+
switching = true;
|
|
458
|
+
try {
|
|
459
|
+
await change();
|
|
460
|
+
await restartAgent();
|
|
461
|
+
} finally {
|
|
462
|
+
switching = false;
|
|
463
|
+
}
|
|
464
|
+
return send(res, 200, { ok: true });
|
|
465
|
+
}
|
|
466
|
+
|
|
441
467
|
// ---- idle stop
|
|
442
468
|
let closing = false;
|
|
443
469
|
const idleTimer = setInterval(() => {
|
|
@@ -565,40 +591,43 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
565
591
|
return send(res, 200, { models: Array.isArray(data) ? data : (data.models ?? []) });
|
|
566
592
|
}
|
|
567
593
|
case "POST /api/model": {
|
|
568
|
-
if (busy) return send(res, 409, { error: "busy" });
|
|
594
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
569
595
|
const body = await readJson(req);
|
|
570
596
|
const spec = `${String(body.provider)}/${String(body.id)}`;
|
|
571
597
|
// Remember the model for the current mode (or both), then restart pi so the extension reads it.
|
|
572
598
|
const mode = (subsubState as { mode?: string } | null)?.mode === "librarian" ? "librarian" : "researcher";
|
|
573
|
-
|
|
599
|
+
// Start from what the file says, not from the built-in defaults, so a model the user never chose is not saved.
|
|
600
|
+
const raw = readConfigFile(configPath(env));
|
|
601
|
+
const saved = raw.models && typeof raw.models === "object" ? (raw.models as Record<string, string>) : typeof raw.model === "string" && raw.model ? { librarian: raw.model, researcher: raw.model } : {};
|
|
602
|
+
const models: Record<string, string> = { ...saved, [mode]: spec };
|
|
574
603
|
if (body.both) models[mode === "librarian" ? "researcher" : "librarian"] = spec;
|
|
575
604
|
saveConfig({ models, model: undefined }, env);
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
605
|
+
return withRestart(res, async () => {
|
|
606
|
+
await agent.request({ type: "set_model", provider: String(body.provider), modelId: String(body.id) });
|
|
607
|
+
});
|
|
579
608
|
}
|
|
580
609
|
case "POST /api/addons": {
|
|
581
610
|
// Turn an add-on on or off, then restart pi so the extension starts or stops its server.
|
|
582
|
-
if (busy) return send(res, 409, { error: "busy" });
|
|
611
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
583
612
|
const body = await readJson(req);
|
|
584
613
|
if (typeof body.starbuck !== "boolean") return send(res, 400, { error: "starbuck must be true or false" });
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
614
|
+
return withRestart(res, () => {
|
|
615
|
+
saveConfig({ addons: withStarbuck(loadConfig(env).addons, body.starbuck) }, env);
|
|
616
|
+
});
|
|
588
617
|
}
|
|
589
618
|
case "POST /api/restart":
|
|
590
|
-
if (agent.proc) return send(res, 200, { ok: true });
|
|
591
|
-
|
|
619
|
+
if (agent.proc || switching) return send(res, 200, { ok: true });
|
|
620
|
+
startAgent();
|
|
592
621
|
broadcast(await snapshot());
|
|
593
622
|
return send(res, 200, { ok: true });
|
|
594
623
|
case "POST /api/login": {
|
|
595
|
-
if (busy) return send(res, 409, { error: "busy" });
|
|
624
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
596
625
|
const body = await readJson(req);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
626
|
+
return withRestart(res, () => {
|
|
627
|
+
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
628
|
+
const current = loadConfig(env);
|
|
629
|
+
if (body.provider === "opencode-go" && Object.keys(current.models).length === 0) saveConfig({ models: TESTED_MODELS, model: undefined }, env);
|
|
630
|
+
});
|
|
602
631
|
}
|
|
603
632
|
case "POST /api/open": {
|
|
604
633
|
const body = await readJson(req);
|
package/web/app.js
CHANGED
|
@@ -650,6 +650,7 @@ function onEvent(ev) {
|
|
|
650
650
|
setBusy(!!ev.busy);
|
|
651
651
|
dialogs.length = 0;
|
|
652
652
|
for (const d of ev.dialogs ?? []) dialogs.push(d);
|
|
653
|
+
dropStaleDialog();
|
|
653
654
|
nextDialog();
|
|
654
655
|
loadSessions();
|
|
655
656
|
break;
|
|
@@ -743,6 +744,8 @@ function onEvent(ev) {
|
|
|
743
744
|
break;
|
|
744
745
|
case "agent_exit":
|
|
745
746
|
setBusy(false);
|
|
747
|
+
dialogs.length = 0;
|
|
748
|
+
dropStaleDialog();
|
|
746
749
|
log.append(
|
|
747
750
|
el(
|
|
748
751
|
"div",
|
|
@@ -776,6 +779,7 @@ function dialogTitle(d) {
|
|
|
776
779
|
if ((m = raw.match(/^apply (\S+)\?$/))) return t("apply", { x: toolLabel(m[1]) });
|
|
777
780
|
if ((m = raw.match(/^run (\S+)\?$/))) return t("run", { x: toolLabel(m[1]) });
|
|
778
781
|
if (raw === "undo?") return t("undo");
|
|
782
|
+
if ((m = raw.match(/^write the Starbuck report to (.+)\?$/))) return t("starbuckReport", { x: m[1] });
|
|
779
783
|
if ((m = raw.match(/^(write|edit) (.+)\?$/))) return t("write", { x: m[2] });
|
|
780
784
|
return raw;
|
|
781
785
|
}
|
|
@@ -797,6 +801,14 @@ function paintPreview(pre, text) {
|
|
|
797
801
|
}
|
|
798
802
|
}
|
|
799
803
|
|
|
804
|
+
/** Close the dialog on screen if Sub-Sub no longer waits for it (pi restarted, or another tab answered). */
|
|
805
|
+
function dropStaleDialog() {
|
|
806
|
+
if (shownDialog && !dialogs.some((d) => d.id === shownDialog.id)) {
|
|
807
|
+
shownDialog = null;
|
|
808
|
+
if ($("dialog").open) $("dialog").close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
800
812
|
function nextDialog() {
|
|
801
813
|
if (shownDialog || !dialogs.length) return;
|
|
802
814
|
const d = dialogs[0];
|
|
@@ -821,6 +833,7 @@ function nextDialog() {
|
|
|
821
833
|
note(err.message, "error");
|
|
822
834
|
}
|
|
823
835
|
};
|
|
836
|
+
$("dialog-form").onsubmit = (e) => e.preventDefault();
|
|
824
837
|
if (d.method === "confirm") {
|
|
825
838
|
buttons.append(
|
|
826
839
|
el("span", { class: "modal-shortcut-hint", text: t("confirmShortcutHint") }),
|
|
@@ -834,14 +847,25 @@ function nextDialog() {
|
|
|
834
847
|
const field = d.method === "editor" ? el("textarea", {}) : el("input", { type: "text", placeholder: d.placeholder ?? "" });
|
|
835
848
|
field.value = d.prefill ?? "";
|
|
836
849
|
body.append(field);
|
|
850
|
+
$("dialog-form").onsubmit = (e) => {
|
|
851
|
+
e.preventDefault();
|
|
852
|
+
answer({ value: field.value });
|
|
853
|
+
};
|
|
837
854
|
buttons.append(
|
|
838
855
|
el("button", { type: "button", class: "ghost", text: t("cancel"), onclick: () => answer({ cancelled: true }) }),
|
|
839
856
|
el("button", { type: "button", class: "primary", text: t("ok"), onclick: () => answer({ value: field.value }) }),
|
|
840
857
|
);
|
|
841
858
|
}
|
|
859
|
+
// Keys typed just before the dialog opened (the user was writing a message) must not answer it.
|
|
860
|
+
const openedAt = performance.now();
|
|
861
|
+
const yesKeys = lang === "pt" ? ["s", "S", "y", "Y"] : ["y", "Y"];
|
|
842
862
|
dlg.onkeydown = (e) => {
|
|
843
863
|
if (d.method === "confirm") {
|
|
844
|
-
if (e.
|
|
864
|
+
if (e.repeat || performance.now() - openedAt < 700) {
|
|
865
|
+
if (!e.metaKey && !e.ctrlKey && e.key.length === 1) e.preventDefault();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (yesKeys.includes(e.key) || (e.key === "Enter" && (e.metaKey || e.ctrlKey))) {
|
|
845
869
|
e.preventDefault();
|
|
846
870
|
answer({ confirmed: true });
|
|
847
871
|
} else if (e.key === "n" || e.key === "N") {
|
|
@@ -867,6 +891,9 @@ function showModels() {
|
|
|
867
891
|
|
|
868
892
|
async function openModels() {
|
|
869
893
|
const dlg = $("model-dialog");
|
|
894
|
+
// Name the mode the choice is saved for; "both modes" is a choice for this time only.
|
|
895
|
+
dlg.querySelector("h2").textContent = t(mode() === "librarian" ? "modelTitleLibrarian" : "modelTitleResearcher");
|
|
896
|
+
$("model-both").checked = false;
|
|
870
897
|
const ul = $("models");
|
|
871
898
|
ul.replaceChildren(el("li", { class: "empty-models", text: "…" }));
|
|
872
899
|
const sel = $("login-provider");
|
package/web/i18n.js
CHANGED
|
@@ -21,6 +21,7 @@ export const STRINGS = {
|
|
|
21
21
|
run: "Run {x}?",
|
|
22
22
|
undo: "Undo this change?",
|
|
23
23
|
write: "Allow writing to {x}?",
|
|
24
|
+
starbuckReport: "Write the Starbuck report to {x}?",
|
|
24
25
|
approveHint: "Nothing changes until you select Yes.",
|
|
25
26
|
zoteroChecking: "Zotero: checking",
|
|
26
27
|
zoteroDown: "Zotero is not running",
|
|
@@ -39,6 +40,8 @@ export const STRINGS = {
|
|
|
39
40
|
connectModel: "Connect a model to start.",
|
|
40
41
|
connect: "Connect",
|
|
41
42
|
modelTitle: "Model",
|
|
43
|
+
modelTitleResearcher: "Model for the Researcher",
|
|
44
|
+
modelTitleLibrarian: "Model for the Librarian",
|
|
42
45
|
chooseModel: "Choose a model",
|
|
43
46
|
noModels: "No models yet. Connect a provider below.",
|
|
44
47
|
connectProvider: "Connect a provider",
|
|
@@ -110,6 +113,7 @@ export const STRINGS = {
|
|
|
110
113
|
run: "Executar {x}?",
|
|
111
114
|
undo: "Desfazer esta alteração?",
|
|
112
115
|
write: "Permitir escrever em {x}?",
|
|
116
|
+
starbuckReport: "Escrever o relatório do Starbuck em {x}?",
|
|
113
117
|
approveHint: "Nada muda até selecionar Sim.",
|
|
114
118
|
zoteroChecking: "Zotero: a verificar",
|
|
115
119
|
zoteroDown: "O Zotero não está aberto",
|
|
@@ -128,6 +132,8 @@ export const STRINGS = {
|
|
|
128
132
|
connectModel: "Ligue um modelo para começar.",
|
|
129
133
|
connect: "Ligar",
|
|
130
134
|
modelTitle: "Modelo",
|
|
135
|
+
modelTitleResearcher: "Modelo do Investigador",
|
|
136
|
+
modelTitleLibrarian: "Modelo do Bibliotecário",
|
|
131
137
|
chooseModel: "Escolha um modelo",
|
|
132
138
|
noModels: "Ainda não há modelos. Ligue um fornecedor abaixo.",
|
|
133
139
|
connectProvider: "Ligar um fornecedor",
|
|
@@ -161,7 +167,7 @@ export const STRINGS = {
|
|
|
161
167
|
searchConversations: "Procurar conversas",
|
|
162
168
|
export: "Exportar",
|
|
163
169
|
exportTitle: "Exportar conversa",
|
|
164
|
-
confirmShortcutHint: "
|
|
170
|
+
confirmShortcutHint: "S para aprovar, N para recusar",
|
|
165
171
|
starbuck: "Verificação de referências (Starbuck)",
|
|
166
172
|
starbuckHint: "Verifica se as obras citadas existem, correspondem à citação e não foram retratadas. O Sub-Sub reinicia quando muda esta opção.",
|
|
167
173
|
starbuckOn: "A verificação de referências está ligada. Use /verify, ou peça ao investigador que verifique as referências de um manuscrito.",
|
package/docs/Subsub.md
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
Sub-Sub is a Zotero librarian and research assistant that runs in pi. It uses the Python servers of zotero-local-mcp for the library: previews, version checks, journal and undo, the tag vocabulary.
|
|
2
|
-
|
|
3
|
-
- Code: ~/Projects/subsub (git). Servers: ~/Projects/zotero-local-mcp
|
|
4
|
-
- Shared rules: [[Zotero agent]]. Tag vocabulary: [[Zotero tags]]
|
|
5
|
-
- Settings: ~/.config/subsub/config.json (written by `subsub init`) and ~/.config/zotero-local-mcp/env (also read by the weekly alerts in launchd)
|
|
6
|
-
- Sub-Sub folder: ~/Obsidian/Powerslave/Sub-Sub, with Inbox/, Literature/, Syntheses/, Research/ and Zotero/ (tag list, note formats, alert searches). Sub-Sub writes only there without asking.
|
|
7
|
-
- Models: Librarian opencode-go/mimo-v2.6-flash, Researcher opencode-go/mimo-v2.6-pro
|
|
8
|
-
- Public docs: the README in ~/Projects/subsub (source for subsub.tiagojacinto.eu)
|
|
9
|
-
|
|
10
|
-
## Safety
|
|
11
|
-
|
|
12
|
-
- Approval is enforced in code. Before any library change, Sub-Sub asks the server for the preview and shows it in a dialog. The change runs only after you select Yes. The model cannot skip this.
|
|
13
|
-
- The model does not need to do a dry run first. You see one dialog per change.
|
|
14
|
-
- Library tools run one at a time, so each preview includes the effect of earlier changes in the same step.
|
|
15
|
-
- The Librarian has no search tools. The Researcher has all tools; every library change still needs a Yes.
|
|
16
|
-
- bash is off. File writes outside the Sub-Sub folder, and writes to the rule files, config folders and code folders, need a Yes.
|
|
17
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
1. Make sure that Node is version 22.19 or later: type `node --version`.
|
|
21
|
-
2. Type `cd ~/Projects/subsub && npm install --omit=dev`.
|
|
22
|
-
3. Type `npm link`. This adds the `subsub` command.
|
|
23
|
-
4. Optional: type `pi remove ~/Projects/subsub`. Then plain `pi` stays a plain coding agent without Sub-Sub.
|
|
24
|
-
5. Type `subsub`. If pi has no OpenCode Go key yet, type `/login`, select OpenCode Zen and Go, and paste the key.
|
|
25
|
-
6. Type `/subsub`. Make sure that the result shows "Zotero: reachable" and the model for the mode (mimo-v2.6-pro in the Researcher).
|
|
26
|
-
7. Type `subsub init` once. Keep the Sub-Sub folder, the tag list, the email and the models. For the profile, select Editor.
|
|
27
|
-
8. Type `subsub doctor`. Make sure that no line shows FIX.
|
|
28
|
-
|
|
29
|
-
If you change the Node version with fnm, do step 3 again.
|
|
30
|
-
|
|
31
|
-
## Use
|
|
32
|
-
|
|
33
|
-
1. Start Zotero.
|
|
34
|
-
2. Type `subsub`. From the home folder, Sub-Sub starts in the Sub-Sub folder. From a different folder (for example a manuscript folder), it starts in that folder. To stay in the home folder, type `subsub --here`.
|
|
35
|
-
3. Sub-Sub starts in the Researcher. The Researcher reads, searches, writes notes and changes the library. The Librarian only manages the library. To start in the Librarian, type `subsub --librarian`.
|
|
36
|
-
4. To change the mode, type `/librarian` or `/researcher`. The mode also changes the model. After research, ask the Researcher to import the works you choose. It imports them in the same conversation, with one preview, and offers to tag them.
|
|
37
|
-
5. When a dialog shows a preview, read it. Select Yes to apply the change, or No to stop it. After No, tell Sub-Sub what to change.
|
|
38
|
-
6. To continue the last session, type `subsub -c`. To select an older session, type `subsub -r`.
|
|
39
|
-
|
|
40
|
-
Sub-Sub always replies in English and writes its notes in English. Quotations and titles of works stay in their original language.
|
|
41
|
-
|
|
42
|
-
Sub-Sub keeps its pi settings, sessions and packages in ~/.subsub/agent, separate from plain pi (~/.pi/agent).
|
|
43
|
-
|
|
44
|
-
Commands:
|
|
45
|
-
|
|
46
|
-
- `/subsub`: Zotero connection and library overview
|
|
47
|
-
- `/profile`: shows or changes the profile
|
|
48
|
-
- `/history`: recent library changes
|
|
49
|
-
- `/undo`: shows the last change, asks, and reverts it. `/undo <id>` reverts a specific change from `/history`.
|
|
50
|
-
|
|
51
|
-
Templates (type the name, then the argument):
|
|
52
|
-
|
|
53
|
-
- Library (both modes): `/tag-batch [size]`, `/clean-tags`, `/import-queue [note path]`
|
|
54
|
-
- Research (Researcher): `/lit-note <citekey>`, `/synthesis <topic>`, `/gaps <topic>`, `/manuscript <path>`, `/alert`
|
|
55
|
-
|
|
56
|
-
## Web view
|
|
57
|
-
|
|
58
|
-
1. Type `subsub web`. Sub-Sub opens in the browser, with the same profiles, tools and approvals. The mode buttons are at the top. The side panel shows the actions of the mode. The approval dialog shows the server preview; the change runs only after Yes.
|
|
59
|
-
2. To add a shortcut (Applications on macOS), type `subsub shortcut`. To remove it, type `subsub shortcut --remove`.
|
|
60
|
-
3. The server listens only on 127.0.0.1 and needs the random key in the address that `subsub web` opens. A second `subsub web` opens the running one.
|
|
61
|
-
4. The server stops 10 minutes after the last page closes. `subsub web --stay` keeps it running.
|
|
62
|
-
5. The labels are in European Portuguese when the language setting is Portuguese. A theme toggle button in the header switches between light, dark and system themes.
|
|
63
|
-
6. Faster review: in approval dialogs, press `y` to apply or `n` to cancel without using the mouse.
|
|
64
|
-
7. One-click copy buttons appear on every code block and assistant note. The sidebar includes real-time conversation search and an Export button to download the discussion as Markdown.
|
|
65
|
-
8. The model button lists the models and saves an API key for a provider (the same auth.json as `/login`). It saves the model for the current mode. To use the model in both modes, select "Use it in both modes".
|
|
66
|
-
|
|
67
|
-
The log is in ~/.subsub/web.log.
|
|
68
|
-
|
|
69
|
-
## Profiles
|
|
70
|
-
|
|
71
|
-
The profile sets how much Sub-Sub does. To see the profile, type `/profile`. To change it, type `/profile reader` (or scholar, author, editor). The change is saved.
|
|
72
|
-
|
|
73
|
-
- Reader: explains each step. Reading notes are quotes with page numbers, plus questions. No summaries or syntheses. No bulk library changes.
|
|
74
|
-
- Scholar: literature notes, syntheses, searches and alerts. All library changes.
|
|
75
|
-
- Author: Scholar plus manuscripts: citation check, bibliography, comments on the argument. It does not write the paragraphs.
|
|
76
|
-
- Editor: everything, including drafted manuscript text when you ask.
|
|
77
|
-
|
|
78
|
-
## Tag review notes
|
|
79
|
-
|
|
80
|
-
Sub-Sub writes proposed tags in review notes in Inbox/ ("Zotero tag review NN.md"). Edit the Proposed tags column. Write skip to leave an item as it is.
|
|
81
|
-
|
|
82
|
-
To apply one note, in either mode, type: apply the tag review Inbox/Zotero tag review NN.md.
|
|
83
|
-
|
|
84
|
-
To apply many notes:
|
|
85
|
-
|
|
86
|
-
1. Start Zotero.
|
|
87
|
-
2. Type `subsub review preview 13-31` (the numbers of the notes). This shows what each note will change. Nothing changes.
|
|
88
|
-
3. Type `subsub review apply 13-31`. Read the preview, then type y.
|
|
89
|
-
4. If Zotero asks for permission, select Always Allow.
|
|
90
|
-
|
|
91
|
-
Use note numbers, not a pattern such as "*.md": a pattern also takes older notes that have no Applied line, and applying an old note again replaces later tag changes.
|
|
92
|
-
|
|
93
|
-
Notes that were already applied are left out. Each applied note gets an "Applied" line with the journal ID. To revert a note, type `/undo <journal ID>` in Sub-Sub. If a note shows "changed since the note was written", the tags of those items changed in Zotero after the note was made; applying the note replaces those changes.
|
|
94
|
-
|
|
95
|
-
## Look
|
|
96
|
-
|
|
97
|
-
When you start Sub-Sub with the `subsub` command, it shows a banner, a status line (mode, model, Zotero items, items to review) and a line from Moby-Dick. The theme is Glauca, in the dark or light version that matches your terminal (set "themes": "subsub-try-works" for the other one). The theme changes for the session only; your pi theme setting stays.
|
|
98
|
-
|
|
99
|
-
To change this, add to ~/.config/subsub/config.json:
|
|
100
|
-
|
|
101
|
-
- `"quotes": false`: no Moby-Dick line.
|
|
102
|
-
- `"themes": false`: keep your own pi theme.
|
|
103
|
-
- `"look": false`: no banner and no themes.
|
|
104
|
-
|
|
105
|
-
The themes are in ~/Projects/subsub/themes. To make them again after a change in Gam, see scripts/make-themes.py.
|
|
106
|
-
|
|
107
|
-
## Model test
|
|
108
|
-
|
|
109
|
-
The model test runs several models on the same tasks against your library and compares them. Nothing in the library changes: each library change is recorded with its preview and then blocked. Notes go to ~/Projects/subsub/bench-results/<date>/, not to the Sub-Sub folder.
|
|
110
|
-
|
|
111
|
-
Tasks:
|
|
112
|
-
|
|
113
|
-
- Library tasks: tag 20 items against a blind reference (reference.md shows the items without their tags; the tags go in reference.json); fix the items without topic or status; import identifiers, one of them already in the library.
|
|
114
|
-
- Research tasks: a literature note on one item with full text; a synthesis on one topic; a search for recent papers that are not in the library.
|
|
115
|
-
|
|
116
|
-
Procedure:
|
|
117
|
-
|
|
118
|
-
1. Start Zotero.
|
|
119
|
-
2. Type `subsub-bench prepare`. This picks the tasks.
|
|
120
|
-
3. Type `caffeinate -i subsub-bench run`. This takes 1 to 2 hours. caffeinate keeps the Mac awake. If the run stops, type the same command again: finished runs are not repeated.
|
|
121
|
-
4. Type `subsub-bench score`. The result is results.md in the results folder.
|
|
122
|
-
|
|
123
|
-
Options for `run`: `--models a,b` for all tasks, or `--librarian a,b` and `--researcher c,d` for each group of tasks (OpenCode Go model ids), `--only tagging,import`, `--parallel 3`, `--timeout 900`. Library tasks run in the Librarian. Research tasks run in the Researcher.
|
|
124
|
-
|
|
125
|
-
Each model gets a code (for example M417), so the notes can be judged blind. key.json has the codes.
|
|
126
|
-
|
|
127
|
-
## Problems
|
|
128
|
-
|
|
129
|
-
- To find a setup problem, type `subsub doctor`. Each line marked FIX says what to do.
|
|
130
|
-
- "the zotero server did not start": run `cd ~/Projects/zotero-local-mcp && uv sync`. If pi does not find uv, add `"uvPath": "/path/to/uv"` to ~/.config/subsub/config.json (type `which uv` to see the path).
|
|
131
|
-
- "cannot use opencode-go/...": the OpenCode Go key is missing. In Sub-Sub, type `/login` again.
|
|
132
|
-
- "command not found: subsub": type `cd ~/Projects/subsub && npm link`.
|
|
133
|
-
- To update Sub-Sub after new code arrives, type `cd ~/Projects/subsub && npm install --omit=dev`. Do not use `subsub update`.
|
|
134
|
-
- A tool call is refused with "part of the Researcher": type `/researcher`.
|
|
135
|
-
- `subsub review` says "No note matches": check the note numbers in Inbox/. A path is written from the Sub-Sub folder, in quotes (for example "Inbox/Zotero tag review 13.md").
|
|
136
|
-
|
|
137
|
-
## Tests
|
|
138
|
-
|
|
139
|
-
- `cd ~/Projects/subsub && npm install && npm test`: unit tests.
|
|
140
|
-
- `npm run test:e2e`: real pi and the `subsub` command with a scripted model and a fake Zotero.
|