@tiagojct/subsub 0.6.1 → 0.8.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 +200 -62
- package/THIRD_PARTY_NOTICES +153 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/dist/bench.js +60 -5
- package/dist/config.js +28 -1
- package/dist/doctor.js +23 -2
- package/dist/init.js +17 -3
- package/dist/notes.js +42 -0
- package/dist/profiles.js +15 -0
- package/dist/prompt.js +3 -0
- package/dist/roles.js +7 -1
- package/dist/subsub.js +94 -9
- package/dist/web.js +17 -3
- package/package.json +7 -3
- package/prompts/compare.md +23 -0
- package/prompts/digest.md +23 -0
- package/prompts/lit.md +87 -0
- package/prompts/request-copy.md +35 -0
- package/prompts/review.md +31 -0
- package/prompts/verify.md +18 -0
- package/roles/researcher.md +18 -5
- package/src/bench.ts +63 -4
- package/src/config.ts +39 -1
- package/src/doctor.ts +32 -2
- package/src/init.ts +33 -4
- package/src/notes.ts +46 -0
- package/src/profiles.ts +17 -0
- package/src/prompt.ts +6 -0
- package/src/roles.ts +8 -1
- package/src/subsub.ts +100 -9
- package/src/web.ts +15 -3
- package/templates/Zotero agent.md +2 -0
- package/themes/subsub-glauca-dark.json +34 -34
- package/themes/subsub-glauca-light.json +32 -32
- package/themes/subsub-try-works-dark.json +34 -34
- package/themes/subsub-try-works-light.json +34 -34
- package/web/app.css +820 -206
- package/web/app.js +109 -17
- package/web/i18n.js +48 -4
- package/web/icon.svg +6 -1
- package/web/index.html +27 -19
- package/web/glauca.css +0 -106
package/src/profiles.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ProfileSpec {
|
|
|
19
19
|
summary: string;
|
|
20
20
|
/** Tools this profile does not offer (full names). */
|
|
21
21
|
without: string[];
|
|
22
|
+
/** Prompt commands this profile does not run (names without the slash). */
|
|
23
|
+
withoutPrompts: string[];
|
|
22
24
|
/** Items per review note and per batch. */
|
|
23
25
|
batch: number;
|
|
24
26
|
/** Added to the system prompt. {{user}} is the user's name or "the user". */
|
|
@@ -36,6 +38,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
36
38
|
label: "Reader",
|
|
37
39
|
summary: "explains each step; reading notes are quotes and questions; no summaries; no bulk library changes",
|
|
38
40
|
without: [...BULK_LIBRARY, ...MANUSCRIPT],
|
|
41
|
+
withoutPrompts: ["lit", "compare", "review", "digest"],
|
|
39
42
|
batch: 10,
|
|
40
43
|
rules: [
|
|
41
44
|
"Profile: Reader. {{User}} is learning to work with the literature and wants to do the reading and the writing.",
|
|
@@ -51,6 +54,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
51
54
|
label: "Scholar",
|
|
52
55
|
summary: "literature notes, syntheses, searches and alerts; the full librarian",
|
|
53
56
|
without: MANUSCRIPT,
|
|
57
|
+
withoutPrompts: ["review"],
|
|
54
58
|
batch: 25,
|
|
55
59
|
rules: [
|
|
56
60
|
"Profile: Scholar.",
|
|
@@ -64,6 +68,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
64
68
|
label: "Author",
|
|
65
69
|
summary: "Scholar plus manuscripts: citation check, bibliography, comments on the argument",
|
|
66
70
|
without: [],
|
|
71
|
+
withoutPrompts: [],
|
|
67
72
|
batch: 25,
|
|
68
73
|
rules: [
|
|
69
74
|
"Profile: Author.",
|
|
@@ -78,6 +83,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
78
83
|
label: "Editor",
|
|
79
84
|
summary: "everything, including drafting manuscript text on request",
|
|
80
85
|
without: [],
|
|
86
|
+
withoutPrompts: [],
|
|
81
87
|
batch: 25,
|
|
82
88
|
rules: [
|
|
83
89
|
"Profile: Editor. All tools.",
|
|
@@ -92,6 +98,17 @@ export function profileSpec(p: Profile): ProfileSpec {
|
|
|
92
98
|
return PROFILE_SPECS[p];
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
/** Why a prompt command does not run in this profile, or undefined when it does. */
|
|
102
|
+
export function promptBlocked(text: string, profile: Profile): string | undefined {
|
|
103
|
+
const m = /^\/([\w-]+)(?:\s|$)/.exec(text.trim());
|
|
104
|
+
if (!m || !PROFILE_SPECS[profile].withoutPrompts.includes(m[1])) return undefined;
|
|
105
|
+
const allowed = Object.values(PROFILE_SPECS)
|
|
106
|
+
.filter((s) => !s.withoutPrompts.includes(m[1]))
|
|
107
|
+
.map((s) => s.label)
|
|
108
|
+
.join(", ");
|
|
109
|
+
return `/${m[1]} is not part of the ${PROFILE_SPECS[profile].label} profile (it is in: ${allowed}). Change the profile with /profile.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
export function profileList(current?: Profile): string {
|
|
96
113
|
return Object.values(PROFILE_SPECS)
|
|
97
114
|
.map((s) => `${s.name === current ? "*" : " "} ${s.label.padEnd(8)} ${s.summary}`)
|
package/src/prompt.ts
CHANGED
|
@@ -74,6 +74,12 @@ export function systemAddition(mode: Mode, cfg: SubsubConfig, cwd: string): stri
|
|
|
74
74
|
if (t) parts.push(`# Vault conventions (${f})`, t);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
if (cfg.setup === "fmup") {
|
|
78
|
+
parts.push(
|
|
79
|
+
"# U.Porto",
|
|
80
|
+
`${capital(user)} works at the University of Porto. Papers that are not open access often open through the university's subscriptions when ${user} uses Sub-Sub on the U.Porto network: say so when a work is not open access.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
77
83
|
// The profile and the language rule come last, so they win over the shared rules and the vault conventions.
|
|
78
84
|
parts.push("# Profile", fill(profileSpec(cfg.profile).rules, cfg));
|
|
79
85
|
parts.push("# Language", languageRule(cfg.language, user));
|
package/src/roles.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { profileSpec } from "./profiles.ts";
|
|
|
7
7
|
|
|
8
8
|
export const FILE_TOOLS = ["read", "edit", "write", "grep", "find", "ls"];
|
|
9
9
|
|
|
10
|
+
/** Prefixes of the tools that Sub-Sub's servers provide (the gate checks the mode for these). */
|
|
11
|
+
export const SERVER_PREFIXES = ["zotero_", "scholar_", "verify_"];
|
|
12
|
+
|
|
10
13
|
export const ZOTERO_READ = [
|
|
11
14
|
"status",
|
|
12
15
|
"library_overview",
|
|
@@ -51,7 +54,11 @@ export function toolsFor(mode: Mode, registered: string[], profile: Profile = "e
|
|
|
51
54
|
const wanted =
|
|
52
55
|
mode === "librarian"
|
|
53
56
|
? [...registered.filter((n) => n.startsWith("zotero_")), ...FILE_TOOLS]
|
|
54
|
-
: [
|
|
57
|
+
: [
|
|
58
|
+
...ZOTERO_READ,
|
|
59
|
+
...registered.filter((n) => n.startsWith("scholar_") || n.startsWith("verify_")),
|
|
60
|
+
...FILE_TOOLS,
|
|
61
|
+
];
|
|
55
62
|
return [...new Set(wanted)].filter((n) => has.has(n) && !without.has(n));
|
|
56
63
|
}
|
|
57
64
|
|
package/src/subsub.ts
CHANGED
|
@@ -8,24 +8,41 @@
|
|
|
8
8
|
* - Profiles (reader, scholar, author, editor) set what Sub-Sub writes and which
|
|
9
9
|
* tools it offers; /profile changes it.
|
|
10
10
|
* - Commands: /librarian, /researcher, /profile, /subsub, /history, /undo.
|
|
11
|
+
* - Add-on: with "addons": ["starbuck"], Starbuck runs as a third server
|
|
12
|
+
* ("verify") for reference checks; its tools belong to researcher mode.
|
|
11
13
|
*
|
|
12
14
|
* The Python servers start in session_start (not in the factory) and stop in
|
|
13
15
|
* session_shutdown. Tools are registered once and use the current bridge.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { readFileSync } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
20
|
import { dirname, join } from "node:path";
|
|
19
21
|
import { fileURLToPath } from "node:url";
|
|
20
22
|
import { Bridge, type ServerSpec } from "./bridge.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
+
import {
|
|
24
|
+
expand,
|
|
25
|
+
findUv,
|
|
26
|
+
isProfile,
|
|
27
|
+
loadConfig,
|
|
28
|
+
type Mode,
|
|
29
|
+
parseEnvFile,
|
|
30
|
+
type Profile,
|
|
31
|
+
saveConfig,
|
|
32
|
+
serverCommand,
|
|
33
|
+
starbuckCommand,
|
|
34
|
+
starbuckEnabled,
|
|
35
|
+
type SubsubConfig,
|
|
36
|
+
who,
|
|
37
|
+
} from "./config.ts";
|
|
38
|
+
import { profileList, profileSpec, promptBlocked } from "./profiles.ts";
|
|
23
39
|
import { coerceArgs, loosenArrays } from "./args.ts";
|
|
40
|
+
import { applyEdits, checkLiteratureNote } from "./notes.ts";
|
|
24
41
|
import { buildPolicy, judgePath, normalizeToolPath, realish, within } from "./paths.ts";
|
|
25
42
|
import { describeArgs, formatPreview } from "./preview.ts";
|
|
26
43
|
import { headerLines, type LibraryState, paintPreview, QUOTES, type Scheme, schemeFromAnsi, themeName } from "./look.ts";
|
|
27
44
|
import { systemAddition } from "./prompt.ts";
|
|
28
|
-
import { gateKind, toolsFor, unavailableReason } from "./roles.ts";
|
|
45
|
+
import { gateKind, SERVER_PREFIXES, toolsFor, unavailableReason } from "./roles.ts";
|
|
29
46
|
|
|
30
47
|
export interface SubsubDeps {
|
|
31
48
|
config?: SubsubConfig;
|
|
@@ -48,12 +65,30 @@ function ownVersion(): string {
|
|
|
48
65
|
|
|
49
66
|
export { findUv };
|
|
50
67
|
|
|
51
|
-
export function serverSpecs(cfg: SubsubConfig): ServerSpec[] {
|
|
68
|
+
export function serverSpecs(cfg: SubsubConfig, processEnv: NodeJS.ProcessEnv = process.env): ServerSpec[] {
|
|
52
69
|
const env: Record<string, string> = cfg.envFile ? { ZOTERO_MCP_ENV: cfg.envFile } : {};
|
|
53
|
-
|
|
54
|
-
{ name: "zotero", ...serverCommand(cfg, "zotero-local-mcp"), env },
|
|
55
|
-
{ name: "scholar", ...serverCommand(cfg, "zotero-scholar-mcp"), env },
|
|
70
|
+
const specs: ServerSpec[] = [
|
|
71
|
+
{ name: "zotero", ...serverCommand(cfg, "zotero-local-mcp", [], processEnv), env },
|
|
72
|
+
{ name: "scholar", ...serverCommand(cfg, "zotero-scholar-mcp", [], processEnv), env },
|
|
56
73
|
];
|
|
74
|
+
if (starbuckEnabled(cfg)) specs.push({ name: "verify", ...starbuckCommand(cfg, processEnv), env: starbuckEnv(cfg, processEnv) });
|
|
75
|
+
return specs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Starbuck reads its contact address and API keys from the environment. Sub-Sub keeps
|
|
80
|
+
* them in the zotero-local-mcp env file, so they are passed on (the environment wins).
|
|
81
|
+
*/
|
|
82
|
+
export function starbuckEnv(cfg: Pick<SubsubConfig, "envFile">, processEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
83
|
+
const file = cfg.envFile ? parseEnvFile(cfg.envFile) : {};
|
|
84
|
+
const out: Record<string, string> = {};
|
|
85
|
+
const email = processEnv.STARBUCK_EMAIL || file.STARBUCK_EMAIL || processEnv.ZOTERO_CONTACT_EMAIL || file.ZOTERO_CONTACT_EMAIL;
|
|
86
|
+
if (email) out.STARBUCK_EMAIL = email;
|
|
87
|
+
for (const key of ["NCBI_API_KEY", "OPENALEX_API_KEY"]) {
|
|
88
|
+
const v = processEnv[key] || file[key];
|
|
89
|
+
if (v) out[key] = v;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Promise<void> {
|
|
@@ -125,6 +160,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
125
160
|
language: cfg.language,
|
|
126
161
|
userName: cfg.userName ?? null,
|
|
127
162
|
vault: cfg.vault ?? null,
|
|
163
|
+
starbuck: starbuckEnabled(cfg),
|
|
128
164
|
};
|
|
129
165
|
try {
|
|
130
166
|
ctx.ui.setStatus("subsub.web", JSON.stringify(state));
|
|
@@ -262,6 +298,15 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
262
298
|
|
|
263
299
|
pi.on("model_select", async (_event, ctx) => status(ctx));
|
|
264
300
|
|
|
301
|
+
// Prompt commands that the profile does not run (/lit for Reader, /review for Reader and Scholar).
|
|
302
|
+
// The input event comes before pi expands a prompt template.
|
|
303
|
+
pi.on("input", async (event, ctx) => {
|
|
304
|
+
const why = promptBlocked(event.text, profile);
|
|
305
|
+
if (!why) return { action: "continue" };
|
|
306
|
+
ctx.ui.notify(`Sub-Sub: ${why}`, "warning");
|
|
307
|
+
return { action: "handled" };
|
|
308
|
+
});
|
|
309
|
+
|
|
265
310
|
pi.on("before_agent_start", async (event, ctx) => ({
|
|
266
311
|
systemPrompt: `${event.systemPrompt}\n\n${systemAddition(mode, { ...cfg, profile }, ctx.cwd)}`,
|
|
267
312
|
}));
|
|
@@ -276,18 +321,21 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
276
321
|
// Previews and the real call must see the same, repaired arguments.
|
|
277
322
|
const schema = schemas.get(name);
|
|
278
323
|
if (schema) coerceArgs(schema, input);
|
|
279
|
-
if (
|
|
324
|
+
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
280
325
|
const why = unavailableReason(name, mode, registered(), profile);
|
|
281
326
|
if (why) {
|
|
282
327
|
if (benchOut) record({ kind: "wrong_mode", tool: name, input });
|
|
283
328
|
return { block: true, reason: why };
|
|
284
329
|
}
|
|
285
330
|
}
|
|
331
|
+
if (name.startsWith("verify_")) return verifyGate(name, input, ctx);
|
|
286
332
|
const kind = benchOut && name === "scholar_queue_imports" ? "confirm" : gateKind(name, input);
|
|
287
333
|
if (!kind) return undefined;
|
|
288
334
|
if (benchOut) return benchGate(name, input, kind, ctx.cwd, ctx.signal);
|
|
289
335
|
if (kind === "path") {
|
|
290
336
|
const target = normalizeToolPath(String(input.path ?? ""), ctx.cwd);
|
|
337
|
+
const noteProblem = literatureNoteProblem(name, target, input);
|
|
338
|
+
if (noteProblem) return { block: true, reason: noteProblem };
|
|
291
339
|
const verdict = judgePath(target, buildPolicy({ vault: cfg.vault, cwd: ctx.cwd, serverDir: cfg.serverDir, packageDir: PACKAGE_DIR }));
|
|
292
340
|
if (verdict.ok) return undefined;
|
|
293
341
|
if (!ctx.hasUI) return { block: true, reason: `Writing ${target} needs ${user}'s approval: ${verdict.why}.` };
|
|
@@ -318,9 +366,52 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
318
366
|
return ok ? undefined : { block: true, reason: `${user} did not approve this change. Ask what to change; do not retry the same call.` };
|
|
319
367
|
});
|
|
320
368
|
|
|
369
|
+
/** A literature note (front matter with a citekey) must state its evidence; see notes.ts. */
|
|
370
|
+
function literatureNoteProblem(name: string, target: string, input: Record<string, unknown>): string | undefined {
|
|
371
|
+
if (!target.toLowerCase().endsWith(".md")) return undefined;
|
|
372
|
+
if (name === "write") return checkLiteratureNote(String(input.content ?? ""));
|
|
373
|
+
if (name === "edit" && Array.isArray(input.edits) && existsSync(target)) {
|
|
374
|
+
const current = readFileSync(target, "utf8");
|
|
375
|
+
return checkLiteratureNote(applyEdits(current, input.edits as Array<{ oldText?: unknown; newText?: unknown }>));
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Starbuck writes its reports into a folder: report_dir, or _starbuck next to the
|
|
382
|
+
* manuscript. Inside the vault or the working folder that needs no approval; elsewhere
|
|
383
|
+
* Sub-Sub asks, as for any file write. record_claims gets the current model's name.
|
|
384
|
+
*/
|
|
385
|
+
async function verifyGate(name: string, input: Record<string, unknown>, ctx: ExtensionContext) {
|
|
386
|
+
if (name === "verify_record_claims") {
|
|
387
|
+
input.judged_by = ctx.model ? [ctx.model.provider, ctx.model.id].filter(Boolean).join("/") : "the agent's model";
|
|
388
|
+
}
|
|
389
|
+
if (name === "verify_check_references") return undefined;
|
|
390
|
+
const dir = input.report_dir
|
|
391
|
+
? normalizeToolPath(String(input.report_dir), ctx.cwd)
|
|
392
|
+
: input.path
|
|
393
|
+
? join(dirname(normalizeToolPath(String(input.path), ctx.cwd)), "_starbuck")
|
|
394
|
+
: undefined;
|
|
395
|
+
if (!dir) return undefined;
|
|
396
|
+
const verdict = judgePath(dir, buildPolicy({ vault: cfg.vault, cwd: ctx.cwd, serverDir: cfg.serverDir, packageDir: PACKAGE_DIR }));
|
|
397
|
+
if (verdict.ok) return undefined;
|
|
398
|
+
if (benchOut) {
|
|
399
|
+
record({ kind: "path_blocked", tool: name, path: dir });
|
|
400
|
+
return { block: true, reason: `Model test: Starbuck reports only inside the vault or ${benchOut}.` };
|
|
401
|
+
}
|
|
402
|
+
if (!ctx.hasUI) return { block: true, reason: `Writing a Starbuck report to ${dir} needs ${user}'s approval: ${verdict.why}.` };
|
|
403
|
+
const ok = await ctx.ui.confirm(`Sub-Sub: write the Starbuck report to ${dir}?`, `Asking because ${verdict.why}.`);
|
|
404
|
+
return ok ? undefined : { block: true, reason: `${user} did not allow writing the report there. Ask where to put it (report_dir).` };
|
|
405
|
+
}
|
|
406
|
+
|
|
321
407
|
async function benchGate(name: string, input: Record<string, unknown>, kind: string, cwd: string, signal?: AbortSignal) {
|
|
322
408
|
if (kind === "path") {
|
|
323
409
|
const target = normalizeToolPath(String(input.path ?? ""), cwd);
|
|
410
|
+
const noteProblem = literatureNoteProblem(name, target, input);
|
|
411
|
+
if (noteProblem) {
|
|
412
|
+
record({ kind: "note_blocked", tool: name, path: target, reason: noteProblem });
|
|
413
|
+
return { block: true, reason: noteProblem };
|
|
414
|
+
}
|
|
324
415
|
if (within(target, realish(benchOut!))) return undefined;
|
|
325
416
|
record({ kind: "path_blocked", tool: name, path: target });
|
|
326
417
|
return { block: true, reason: `Model test: write only inside ${benchOut}.` };
|
package/src/web.ts
CHANGED
|
@@ -22,9 +22,9 @@ 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 } from "./config.ts";
|
|
25
|
+
import { expand, loadConfig, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
26
26
|
import { TESTED_MODELS } from "./init.ts";
|
|
27
|
-
import { within } from "./paths.ts";
|
|
27
|
+
import { realish, within } from "./paths.ts";
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------- options
|
|
30
30
|
|
|
@@ -577,6 +577,15 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
577
577
|
await restartAgent();
|
|
578
578
|
return send(res, 200, { ok: true });
|
|
579
579
|
}
|
|
580
|
+
case "POST /api/addons": {
|
|
581
|
+
// 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" });
|
|
583
|
+
const body = await readJson(req);
|
|
584
|
+
if (typeof body.starbuck !== "boolean") return send(res, 400, { error: "starbuck must be true or false" });
|
|
585
|
+
saveConfig({ addons: withStarbuck(loadConfig(env).addons, body.starbuck) }, env);
|
|
586
|
+
await restartAgent();
|
|
587
|
+
return send(res, 200, { ok: true });
|
|
588
|
+
}
|
|
580
589
|
case "POST /api/restart":
|
|
581
590
|
if (agent.proc) return send(res, 200, { ok: true });
|
|
582
591
|
agent.start();
|
|
@@ -595,7 +604,10 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
595
604
|
const body = await readJson(req);
|
|
596
605
|
const target = resolve(cwd, expand(String(body.path ?? "")));
|
|
597
606
|
const vault = cfg.vault;
|
|
598
|
-
|
|
607
|
+
// Judge the real file, so a link inside the notes folder cannot open a document outside it.
|
|
608
|
+
const real = realish(target);
|
|
609
|
+
const inside = within(real, realish(cwd)) || Boolean(vault && within(real, realish(vault)));
|
|
610
|
+
if (!inside || !existsSync(target)) return send(res, 404, { error: "not found in the notes folder" });
|
|
599
611
|
// Documents only: a file that the model wrote must never be run.
|
|
600
612
|
if (!OPENABLE.has(extname(target).toLowerCase())) return send(res, 403, { error: "Sub-Sub opens only notes and documents (.md, .txt, .pdf, .bib, .csv, .qmd)." });
|
|
601
613
|
if (vault && within(target, vault) && existsSync(join(vault, ".obsidian"))) openExternal(`obsidian://open?path=${encodeURIComponent(target)}`);
|
|
@@ -40,6 +40,7 @@ Shared rules for the Sub-Sub librarian and researcher. Sub-Sub reads this file a
|
|
|
40
40
|
- Folder: Literature/. File name: the citekey (smith2026.md).
|
|
41
41
|
- Create a note only when the user asks. Ask before creating more than 10 notes.
|
|
42
42
|
- Write from the full text (get_fulltext) when it exists. Otherwise use the abstract and the Zotero notes, and write "(from abstract)" after the summary.
|
|
43
|
+
- Front matter field evidence: full text, abstract or metadata (what you read). Page numbers only with evidence: full text. Sub-Sub refuses a literature note without this field.
|
|
43
44
|
- Do not copy the abstract in full. Give page numbers for quotes when the text shows them.
|
|
44
45
|
- If a note for the citekey exists, do not overwrite it. Add a dated section or ask.
|
|
45
46
|
- After the note is written, offer to link it in Zotero with attach_note.
|
|
@@ -58,6 +59,7 @@ item-type: journalArticle
|
|
|
58
59
|
venue: Journal name
|
|
59
60
|
doi: 10.xxxx/xxxxx
|
|
60
61
|
zotero: zotero://select/library/items/ITEMKEY
|
|
62
|
+
evidence: full text
|
|
61
63
|
status: to-read
|
|
62
64
|
tags:
|
|
63
65
|
- topic/example
|
|
@@ -2,28 +2,28 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
3
|
"name": "subsub-glauca-dark",
|
|
4
4
|
"vars": {
|
|
5
|
-
"bg": "#
|
|
6
|
-
"surface": "#
|
|
7
|
-
"raised": "#
|
|
8
|
-
"text": "#
|
|
9
|
-
"muted": "#
|
|
10
|
-
"line": "#
|
|
11
|
-
"mark": "#
|
|
12
|
-
"markBright": "#
|
|
13
|
-
"markDeep": "#
|
|
14
|
-
"toneDeep": "#
|
|
15
|
-
"tone": "#
|
|
16
|
-
"toneBright": "#
|
|
17
|
-
"tonePale": "#
|
|
18
|
-
"green": "#
|
|
19
|
-
"red": "#
|
|
20
|
-
"yellow": "#
|
|
21
|
-
"violet": "#
|
|
22
|
-
"link": "#
|
|
23
|
-
"dim": "#
|
|
24
|
-
"select": "#
|
|
25
|
-
"okBg": "#
|
|
26
|
-
"errBg": "#
|
|
5
|
+
"bg": "#121517",
|
|
6
|
+
"surface": "#181d20",
|
|
7
|
+
"raised": "#20272b",
|
|
8
|
+
"text": "#e5ebec",
|
|
9
|
+
"muted": "#8a9699",
|
|
10
|
+
"line": "#283236",
|
|
11
|
+
"mark": "#5d99be",
|
|
12
|
+
"markBright": "#78b4da",
|
|
13
|
+
"markDeep": "#41789c",
|
|
14
|
+
"toneDeep": "#0e181c",
|
|
15
|
+
"tone": "#263e48",
|
|
16
|
+
"toneBright": "#486e7e",
|
|
17
|
+
"tonePale": "#8cb6cb",
|
|
18
|
+
"green": "#68aa80",
|
|
19
|
+
"red": "#cf5c5c",
|
|
20
|
+
"yellow": "#c49a46",
|
|
21
|
+
"violet": "#9d82b3",
|
|
22
|
+
"link": "#78b4da",
|
|
23
|
+
"dim": "#5d6a6d",
|
|
24
|
+
"select": "#263e48",
|
|
25
|
+
"okBg": "#1b2920",
|
|
26
|
+
"errBg": "#331c19"
|
|
27
27
|
},
|
|
28
28
|
"colors": {
|
|
29
29
|
"accent": "mark",
|
|
@@ -65,15 +65,15 @@
|
|
|
65
65
|
"toolDiffAdded": "green",
|
|
66
66
|
"toolDiffRemoved": "red",
|
|
67
67
|
"toolDiffContext": "muted",
|
|
68
|
-
"syntaxComment": "#
|
|
69
|
-
"syntaxKeyword": "#
|
|
70
|
-
"syntaxFunction": "#
|
|
71
|
-
"syntaxVariable": "#
|
|
72
|
-
"syntaxString": "#
|
|
73
|
-
"syntaxNumber": "#
|
|
74
|
-
"syntaxType": "#
|
|
75
|
-
"syntaxOperator": "#
|
|
76
|
-
"syntaxPunctuation": "#
|
|
68
|
+
"syntaxComment": "#5d6a6d",
|
|
69
|
+
"syntaxKeyword": "#78b4da",
|
|
70
|
+
"syntaxFunction": "#5d99be",
|
|
71
|
+
"syntaxVariable": "#e5ebec",
|
|
72
|
+
"syntaxString": "#68aa80",
|
|
73
|
+
"syntaxNumber": "#9d82b3",
|
|
74
|
+
"syntaxType": "#8cb6cb",
|
|
75
|
+
"syntaxOperator": "#8a9699",
|
|
76
|
+
"syntaxPunctuation": "#8a9699",
|
|
77
77
|
"thinkingOff": "line",
|
|
78
78
|
"thinkingMinimal": "dim",
|
|
79
79
|
"thinkingLow": "toneBright",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"bashMode": "mark"
|
|
85
85
|
},
|
|
86
86
|
"export": {
|
|
87
|
-
"pageBg": "#
|
|
88
|
-
"cardBg": "#
|
|
89
|
-
"infoBg": "#
|
|
87
|
+
"pageBg": "#121517",
|
|
88
|
+
"cardBg": "#181d20",
|
|
89
|
+
"infoBg": "#20272b"
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -2,28 +2,28 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
3
|
"name": "subsub-glauca-light",
|
|
4
4
|
"vars": {
|
|
5
|
-
"bg": "#
|
|
6
|
-
"surface": "#
|
|
5
|
+
"bg": "#f8f6f0",
|
|
6
|
+
"surface": "#f2ede2",
|
|
7
7
|
"raised": "#ffffff",
|
|
8
|
-
"text": "#
|
|
9
|
-
"muted": "#
|
|
10
|
-
"line": "#
|
|
11
|
-
"mark": "#
|
|
12
|
-
"markBright": "#
|
|
13
|
-
"markDeep": "#
|
|
14
|
-
"toneDeep": "#
|
|
15
|
-
"tone": "#
|
|
16
|
-
"toneBright": "#
|
|
17
|
-
"tonePale": "#
|
|
18
|
-
"green": "#
|
|
19
|
-
"red": "#
|
|
20
|
-
"yellow": "#
|
|
21
|
-
"violet": "#
|
|
22
|
-
"link": "#
|
|
23
|
-
"dim": "#
|
|
24
|
-
"select": "#
|
|
25
|
-
"okBg": "#
|
|
26
|
-
"errBg": "#
|
|
8
|
+
"text": "#211e1b",
|
|
9
|
+
"muted": "#5c554a",
|
|
10
|
+
"line": "#ded6c4",
|
|
11
|
+
"mark": "#234b6e",
|
|
12
|
+
"markBright": "#2f648e",
|
|
13
|
+
"markDeep": "#15344e",
|
|
14
|
+
"toneDeep": "#16222b",
|
|
15
|
+
"tone": "#2e4854",
|
|
16
|
+
"toneBright": "#436574",
|
|
17
|
+
"tonePale": "#b5cad6",
|
|
18
|
+
"green": "#205438",
|
|
19
|
+
"red": "#9c281b",
|
|
20
|
+
"yellow": "#8f6b28",
|
|
21
|
+
"violet": "#604e76",
|
|
22
|
+
"link": "#234b6e",
|
|
23
|
+
"dim": "#827768",
|
|
24
|
+
"select": "#ded6c4",
|
|
25
|
+
"okBg": "#dfebe3",
|
|
26
|
+
"errBg": "#f5e4e2"
|
|
27
27
|
},
|
|
28
28
|
"colors": {
|
|
29
29
|
"accent": "mark",
|
|
@@ -65,15 +65,15 @@
|
|
|
65
65
|
"toolDiffAdded": "green",
|
|
66
66
|
"toolDiffRemoved": "red",
|
|
67
67
|
"toolDiffContext": "muted",
|
|
68
|
-
"syntaxComment": "#
|
|
69
|
-
"syntaxKeyword": "#
|
|
70
|
-
"syntaxFunction": "#
|
|
71
|
-
"syntaxVariable": "#
|
|
72
|
-
"syntaxString": "#
|
|
73
|
-
"syntaxNumber": "#
|
|
74
|
-
"syntaxType": "#
|
|
75
|
-
"syntaxOperator": "#
|
|
76
|
-
"syntaxPunctuation": "#
|
|
68
|
+
"syntaxComment": "#827768",
|
|
69
|
+
"syntaxKeyword": "#234b6e",
|
|
70
|
+
"syntaxFunction": "#2e4854",
|
|
71
|
+
"syntaxVariable": "#211e1b",
|
|
72
|
+
"syntaxString": "#205438",
|
|
73
|
+
"syntaxNumber": "#604e76",
|
|
74
|
+
"syntaxType": "#436574",
|
|
75
|
+
"syntaxOperator": "#5c554a",
|
|
76
|
+
"syntaxPunctuation": "#5c554a",
|
|
77
77
|
"thinkingOff": "line",
|
|
78
78
|
"thinkingMinimal": "dim",
|
|
79
79
|
"thinkingLow": "toneBright",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"bashMode": "mark"
|
|
85
85
|
},
|
|
86
86
|
"export": {
|
|
87
|
-
"pageBg": "#
|
|
88
|
-
"cardBg": "#
|
|
87
|
+
"pageBg": "#f8f6f0",
|
|
88
|
+
"cardBg": "#f2ede2",
|
|
89
89
|
"infoBg": "#ffffff"
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -2,28 +2,28 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
3
|
"name": "subsub-try-works-dark",
|
|
4
4
|
"vars": {
|
|
5
|
-
"bg": "#
|
|
6
|
-
"surface": "#
|
|
7
|
-
"raised": "#
|
|
8
|
-
"text": "#
|
|
9
|
-
"muted": "#
|
|
10
|
-
"line": "#
|
|
11
|
-
"mark": "#
|
|
12
|
-
"markBright": "#
|
|
13
|
-
"markDeep": "#
|
|
14
|
-
"toneDeep": "#
|
|
15
|
-
"tone": "#
|
|
16
|
-
"toneBright": "#
|
|
17
|
-
"tonePale": "#
|
|
18
|
-
"green": "#
|
|
19
|
-
"red": "#
|
|
20
|
-
"yellow": "#
|
|
21
|
-
"violet": "#
|
|
22
|
-
"link": "#
|
|
23
|
-
"dim": "#
|
|
24
|
-
"select": "#
|
|
25
|
-
"okBg": "#
|
|
26
|
-
"errBg": "#
|
|
5
|
+
"bg": "#141210",
|
|
6
|
+
"surface": "#1c1916",
|
|
7
|
+
"raised": "#24201c",
|
|
8
|
+
"text": "#eae4d5",
|
|
9
|
+
"muted": "#9e9585",
|
|
10
|
+
"line": "#2e2924",
|
|
11
|
+
"mark": "#cf663d",
|
|
12
|
+
"markBright": "#eb7c52",
|
|
13
|
+
"markDeep": "#ad4c24",
|
|
14
|
+
"toneDeep": "#0e0d0b",
|
|
15
|
+
"tone": "#5c4a3e",
|
|
16
|
+
"toneBright": "#8c7362",
|
|
17
|
+
"tonePale": "#b5a08d",
|
|
18
|
+
"green": "#68aa80",
|
|
19
|
+
"red": "#cf663d",
|
|
20
|
+
"yellow": "#cca047",
|
|
21
|
+
"violet": "#a88bb5",
|
|
22
|
+
"link": "#6ea3cc",
|
|
23
|
+
"dim": "#736b5e",
|
|
24
|
+
"select": "#332c25",
|
|
25
|
+
"okBg": "#1b2920",
|
|
26
|
+
"errBg": "#331c19"
|
|
27
27
|
},
|
|
28
28
|
"colors": {
|
|
29
29
|
"accent": "mark",
|
|
@@ -65,15 +65,15 @@
|
|
|
65
65
|
"toolDiffAdded": "green",
|
|
66
66
|
"toolDiffRemoved": "red",
|
|
67
67
|
"toolDiffContext": "muted",
|
|
68
|
-
"syntaxComment": "#
|
|
69
|
-
"syntaxKeyword": "#
|
|
70
|
-
"syntaxFunction": "#
|
|
71
|
-
"syntaxVariable": "#
|
|
72
|
-
"syntaxString": "#
|
|
73
|
-
"syntaxNumber": "#
|
|
74
|
-
"syntaxType": "#
|
|
75
|
-
"syntaxOperator": "#
|
|
76
|
-
"syntaxPunctuation": "#
|
|
68
|
+
"syntaxComment": "#736b5e",
|
|
69
|
+
"syntaxKeyword": "#cf663d",
|
|
70
|
+
"syntaxFunction": "#6ea3cc",
|
|
71
|
+
"syntaxVariable": "#eae4d5",
|
|
72
|
+
"syntaxString": "#68aa80",
|
|
73
|
+
"syntaxNumber": "#a88bb5",
|
|
74
|
+
"syntaxType": "#8c7362",
|
|
75
|
+
"syntaxOperator": "#9e9585",
|
|
76
|
+
"syntaxPunctuation": "#9e9585",
|
|
77
77
|
"thinkingOff": "line",
|
|
78
78
|
"thinkingMinimal": "dim",
|
|
79
79
|
"thinkingLow": "toneBright",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"bashMode": "mark"
|
|
85
85
|
},
|
|
86
86
|
"export": {
|
|
87
|
-
"pageBg": "#
|
|
88
|
-
"cardBg": "#
|
|
89
|
-
"infoBg": "#
|
|
87
|
+
"pageBg": "#141210",
|
|
88
|
+
"cardBg": "#1c1916",
|
|
89
|
+
"infoBg": "#24201c"
|
|
90
90
|
}
|
|
91
91
|
}
|