@tiagojct/subsub 0.24.1 → 0.25.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/dist/cli.js +13 -1
- package/dist/roles.js +29 -0
- package/dist/subsub.js +7 -1
- package/package.json +3 -3
- package/scripts/make-themes.mjs +1 -1
- package/src/cli.ts +14 -1
- package/src/roles.ts +26 -0
- package/src/subsub.ts +6 -1
- package/themes/subsub-glauca-dark-256.json +1 -1
- package/themes/subsub-glauca-dark.json +1 -1
- package/themes/subsub-glauca-light-256.json +1 -1
- package/themes/subsub-glauca-light.json +1 -1
- package/themes/subsub-try-works-dark-256.json +1 -1
- package/themes/subsub-try-works-dark.json +1 -1
- package/themes/subsub-try-works-light-256.json +1 -1
- package/themes/subsub-try-works-light.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Sub-Sub follows the version numbers of npm: a change in the second number (0.13) adds features or changes how you use it; a change in the third (0.13.1) only fixes.
|
|
4
4
|
|
|
5
|
+
## 0.25.0 (2026-11-01)
|
|
6
|
+
|
|
7
|
+
- Sub-Sub runs on pi 1.4.0 (was 1.0.4). Install pi 1.4 to use the package in plain pi.
|
|
8
|
+
- The `subsub` command, `subsub web` and `subsub tui` turn off pi's MCP support (`--no-mcp`). A server in an `mcp.json` file would get your library content and could change the library without Sub-Sub's previews. To use other MCP servers with Sub-Sub, add the Sub-Sub package to plain pi.
|
|
9
|
+
- Inside plain pi, Sub-Sub blocks its own Zotero servers when pi reaches them through MCP (for example zotero-local-mcp in your `mcp.json`, as `mcp__zotero__tag_items`). These calls skipped the mode check and the previews. The model is told to use the Sub-Sub tool (`zotero_tag_items`) instead.
|
|
10
|
+
- The themes use pi's new schema address, `https://pi.dev/schemas/theme.schema.json`.
|
|
11
|
+
- Tests: `--no-mcp` for Sub-Sub sessions and not for pi's own commands, the block of the MCP copies of Sub-Sub's tools, and the themes against pi's theme schema.
|
|
12
|
+
|
|
5
13
|
## 0.24.1 (2026-11-01)
|
|
6
14
|
|
|
7
15
|
- In the terminal, the tool line of a library search with several queries (your words and their synonyms, 0.24.0) shows the words it searched. It showed nothing after the tool name.
|
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,18 @@ function sourceOf(entry) {
|
|
|
62
62
|
* Make sure settings.json in the agent folder declares the Sub-Sub package.
|
|
63
63
|
* Other settings are kept. Returns what happened, for tests and messages.
|
|
64
64
|
*/
|
|
65
|
+
/** pi's own commands (pi install, pi mcp list, ...): their arguments go to pi unchanged. */
|
|
66
|
+
const PI_COMMANDS = new Set(["install", "remove", "uninstall", "update", "list", "config", "auth", "mcp"]);
|
|
67
|
+
/**
|
|
68
|
+
* Arguments for a Sub-Sub session: pi's MCP support is off. A server from an mcp.json file would get
|
|
69
|
+
* the library content and could change the library without Sub-Sub's previews. To use other MCP
|
|
70
|
+
* servers with Sub-Sub, add the Sub-Sub package to plain pi.
|
|
71
|
+
*/
|
|
72
|
+
export function withoutMcp(args) {
|
|
73
|
+
if (PI_COMMANDS.has(args[0] ?? "") || args.includes("--no-mcp"))
|
|
74
|
+
return args;
|
|
75
|
+
return ["--no-mcp", ...args];
|
|
76
|
+
}
|
|
65
77
|
export function ensureSettings(agentDir, packageDir = PACKAGE_DIR) {
|
|
66
78
|
mkdirSync(agentDir, { recursive: true });
|
|
67
79
|
const file = join(agentDir, "settings.json");
|
|
@@ -264,7 +276,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
264
276
|
env.PI_SKIP_VERSION_CHECK ??= "1";
|
|
265
277
|
// pi reports a first install to pi.dev and names itself to some providers; Sub-Sub sends no usage data.
|
|
266
278
|
env.PI_TELEMETRY ??= "0";
|
|
267
|
-
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
279
|
+
process.argv = [process.argv[0], process.argv[1], ...withoutMcp(args)];
|
|
268
280
|
const index = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
269
281
|
await import(__rewriteRelativeImportExtension(pathToFileURL(join(dirname(index), "bundle", "cli.js")).href));
|
|
270
282
|
}
|
package/dist/roles.js
CHANGED
|
@@ -80,6 +80,35 @@ export function activeTools(current, next, standalone) {
|
|
|
80
80
|
const kept = current.filter((n) => !SERVER_PREFIXES.some((p) => n.startsWith(p)));
|
|
81
81
|
return [...new Set([...kept, ...next])];
|
|
82
82
|
}
|
|
83
|
+
/** Tool names of Sub-Sub's servers without the prefix, for when the servers are not running. */
|
|
84
|
+
const KNOWN_SERVER_TOOLS = [...ZOTERO_READ, ...ZOTERO_CHECKS, ...PREVIEW_WRITES, ...CONFIRM_WRITES];
|
|
85
|
+
/**
|
|
86
|
+
* The Sub-Sub name of a tool that pi's MCP support reaches on Sub-Sub's own servers (for example
|
|
87
|
+
* zotero-local-mcp in the user's mcp.json), or undefined. pi names these tools mcp__<server>__<tool>.
|
|
88
|
+
* Such a call skips the mode check and the preview, so the gate blocks it. A tool matches when its
|
|
89
|
+
* name and description are those of a Sub-Sub tool, or when the server name has "zotero" or
|
|
90
|
+
* "scholar" and the tool name is one of Sub-Sub's.
|
|
91
|
+
*/
|
|
92
|
+
export function duplicateServerTool(name, description, own) {
|
|
93
|
+
if (!name.startsWith("mcp__"))
|
|
94
|
+
return undefined;
|
|
95
|
+
const rest = name.slice("mcp__".length);
|
|
96
|
+
const cut = rest.indexOf("__");
|
|
97
|
+
if (cut < 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
const server = rest.slice(0, cut).toLowerCase();
|
|
100
|
+
const tool = rest.slice(cut + 2);
|
|
101
|
+
const norm = (s) => s.replace(/-/g, "_");
|
|
102
|
+
const same = own.find((t) => norm(t.name) === tool && description !== undefined && t.description !== "" && description.trim() === t.description.trim());
|
|
103
|
+
if (same)
|
|
104
|
+
return same.fullName;
|
|
105
|
+
if (!/zotero|scholar/.test(server))
|
|
106
|
+
return undefined;
|
|
107
|
+
const byName = own.find((t) => norm(t.name) === tool);
|
|
108
|
+
if (byName)
|
|
109
|
+
return byName.fullName;
|
|
110
|
+
return KNOWN_SERVER_TOOLS.find((n) => n.slice(n.indexOf("_") + 1) === tool);
|
|
111
|
+
}
|
|
83
112
|
/** Why a Sub-Sub tool is not available now, or undefined when it is. */
|
|
84
113
|
export function unavailableReason(name, mode, registered, opts = {}) {
|
|
85
114
|
if (toolsFor(mode, registered, opts).includes(name))
|
package/dist/subsub.js
CHANGED
|
@@ -38,7 +38,7 @@ import { glyphs, paintFromTheme, workingFrames } from "./skin.js";
|
|
|
38
38
|
import { loadWords, toolWord } from "./words.js";
|
|
39
39
|
import { systemAddition } from "./prompt.js";
|
|
40
40
|
import { logUsage } from "./usage.js";
|
|
41
|
-
import { activeTools, gateKind, SERVER_PREFIXES, toolsFor, unavailableReason, outboundProblem } from "./roles.js";
|
|
41
|
+
import { activeTools, duplicateServerTool, gateKind, SERVER_PREFIXES, toolsFor, unavailableReason, outboundProblem } from "./roles.js";
|
|
42
42
|
const MODE_ENTRY = "subsub-mode";
|
|
43
43
|
const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
44
|
const BENCH_ENTRY = "subsub-bench";
|
|
@@ -596,6 +596,12 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
596
596
|
if (schema)
|
|
597
597
|
coerceArgs(schema, input);
|
|
598
598
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
599
|
+
const twin = duplicateServerTool(name, pi.getAllTools().find((t) => t.name === name)?.description, bridge?.tools ?? []);
|
|
600
|
+
if (twin) {
|
|
601
|
+
if (benchOut)
|
|
602
|
+
record({ kind: "duplicate_server_blocked", tool: name });
|
|
603
|
+
return { block: true, reason: `${name} reaches Sub-Sub's server outside Sub-Sub, without the mode check and the previews. Use ${twin} instead.` };
|
|
604
|
+
}
|
|
599
605
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
600
606
|
const why = unavailableReason(name, mode, registered(), toolOpts());
|
|
601
607
|
if (why) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiagojct/subsub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Sub-Sub: a research assistant that works from your own Zotero library, in the browser or the terminal, built on pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.30.1"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@earendil-works/pi-coding-agent": "~1.0
|
|
32
|
+
"@earendil-works/pi-coding-agent": "~1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=22.19.0"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"THIRD_PARTY_NOTICES"
|
|
62
62
|
],
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
64
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
65
65
|
"@types/node": "^24.19.0",
|
|
66
66
|
"typescript": "^5.9.3"
|
|
67
67
|
},
|
package/scripts/make-themes.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
|
|
12
12
|
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
-
const SCHEMA = "https://
|
|
13
|
+
const SCHEMA = "https://pi.dev/schemas/theme.schema.json";
|
|
14
14
|
|
|
15
15
|
/** The `colors:` block of the DESIGN.md front matter as {token: "#rrggbb"}. */
|
|
16
16
|
export function designColors(markdown) {
|
package/src/cli.ts
CHANGED
|
@@ -58,6 +58,19 @@ function sourceOf(entry: unknown): string | undefined {
|
|
|
58
58
|
* Make sure settings.json in the agent folder declares the Sub-Sub package.
|
|
59
59
|
* Other settings are kept. Returns what happened, for tests and messages.
|
|
60
60
|
*/
|
|
61
|
+
/** pi's own commands (pi install, pi mcp list, ...): their arguments go to pi unchanged. */
|
|
62
|
+
const PI_COMMANDS = new Set(["install", "remove", "uninstall", "update", "list", "config", "auth", "mcp"]);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Arguments for a Sub-Sub session: pi's MCP support is off. A server from an mcp.json file would get
|
|
66
|
+
* the library content and could change the library without Sub-Sub's previews. To use other MCP
|
|
67
|
+
* servers with Sub-Sub, add the Sub-Sub package to plain pi.
|
|
68
|
+
*/
|
|
69
|
+
export function withoutMcp(args: string[]): string[] {
|
|
70
|
+
if (PI_COMMANDS.has(args[0] ?? "") || args.includes("--no-mcp")) return args;
|
|
71
|
+
return ["--no-mcp", ...args];
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
export function ensureSettings(agentDir: string, packageDir: string = PACKAGE_DIR): "created" | "added" | "present" | "unreadable" {
|
|
62
75
|
mkdirSync(agentDir, { recursive: true });
|
|
63
76
|
const file = join(agentDir, "settings.json");
|
|
@@ -255,7 +268,7 @@ export async function run(argv: string[] = process.argv.slice(2)): Promise<void>
|
|
|
255
268
|
// pi reports a first install to pi.dev and names itself to some providers; Sub-Sub sends no usage data.
|
|
256
269
|
env.PI_TELEMETRY ??= "0";
|
|
257
270
|
|
|
258
|
-
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
271
|
+
process.argv = [process.argv[0], process.argv[1], ...withoutMcp(args)];
|
|
259
272
|
const index = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
260
273
|
await import(pathToFileURL(join(dirname(index), "bundle", "cli.js")).href);
|
|
261
274
|
}
|
package/src/roles.ts
CHANGED
|
@@ -98,6 +98,32 @@ export function activeTools(current: string[], next: string[], standalone: boole
|
|
|
98
98
|
return [...new Set([...kept, ...next])];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** Tool names of Sub-Sub's servers without the prefix, for when the servers are not running. */
|
|
102
|
+
const KNOWN_SERVER_TOOLS = [...ZOTERO_READ, ...ZOTERO_CHECKS, ...PREVIEW_WRITES, ...CONFIRM_WRITES];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The Sub-Sub name of a tool that pi's MCP support reaches on Sub-Sub's own servers (for example
|
|
106
|
+
* zotero-local-mcp in the user's mcp.json), or undefined. pi names these tools mcp__<server>__<tool>.
|
|
107
|
+
* Such a call skips the mode check and the preview, so the gate blocks it. A tool matches when its
|
|
108
|
+
* name and description are those of a Sub-Sub tool, or when the server name has "zotero" or
|
|
109
|
+
* "scholar" and the tool name is one of Sub-Sub's.
|
|
110
|
+
*/
|
|
111
|
+
export function duplicateServerTool(name: string, description: string | undefined, own: Array<{ name: string; fullName: string; description: string }>): string | undefined {
|
|
112
|
+
if (!name.startsWith("mcp__")) return undefined;
|
|
113
|
+
const rest = name.slice("mcp__".length);
|
|
114
|
+
const cut = rest.indexOf("__");
|
|
115
|
+
if (cut < 0) return undefined;
|
|
116
|
+
const server = rest.slice(0, cut).toLowerCase();
|
|
117
|
+
const tool = rest.slice(cut + 2);
|
|
118
|
+
const norm = (s: string) => s.replace(/-/g, "_");
|
|
119
|
+
const same = own.find((t) => norm(t.name) === tool && description !== undefined && t.description !== "" && description.trim() === t.description.trim());
|
|
120
|
+
if (same) return same.fullName;
|
|
121
|
+
if (!/zotero|scholar/.test(server)) return undefined;
|
|
122
|
+
const byName = own.find((t) => norm(t.name) === tool);
|
|
123
|
+
if (byName) return byName.fullName;
|
|
124
|
+
return KNOWN_SERVER_TOOLS.find((n) => n.slice(n.indexOf("_") + 1) === tool);
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
/** Why a Sub-Sub tool is not available now, or undefined when it is. */
|
|
102
128
|
export function unavailableReason(name: string, mode: Mode, registered: string[], opts: ToolOptions = {}): string | undefined {
|
|
103
129
|
if (toolsFor(mode, registered, opts).includes(name)) return undefined;
|
package/src/subsub.ts
CHANGED
|
@@ -53,7 +53,7 @@ import { glyphs, paintFromTheme, workingFrames } from "./skin.ts";
|
|
|
53
53
|
import { loadWords, toolWord, type Words } from "./words.ts";
|
|
54
54
|
import { systemAddition } from "./prompt.ts";
|
|
55
55
|
import { logUsage } from "./usage.ts";
|
|
56
|
-
import { activeTools, gateKind, SERVER_PREFIXES, toolsFor, unavailableReason, outboundProblem } from "./roles.ts";
|
|
56
|
+
import { activeTools, duplicateServerTool, gateKind, SERVER_PREFIXES, toolsFor, unavailableReason, outboundProblem } from "./roles.ts";
|
|
57
57
|
|
|
58
58
|
const MODE_ENTRY = "subsub-mode";
|
|
59
59
|
|
|
@@ -647,6 +647,11 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
647
647
|
const schema = schemas.get(name);
|
|
648
648
|
if (schema) coerceArgs(schema, input);
|
|
649
649
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
650
|
+
const twin = duplicateServerTool(name, pi.getAllTools().find((t) => t.name === name)?.description, bridge?.tools ?? []);
|
|
651
|
+
if (twin) {
|
|
652
|
+
if (benchOut) record({ kind: "duplicate_server_blocked", tool: name });
|
|
653
|
+
return { block: true, reason: `${name} reaches Sub-Sub's server outside Sub-Sub, without the mode check and the previews. Use ${twin} instead.` };
|
|
654
|
+
}
|
|
650
655
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
651
656
|
const why = unavailableReason(name, mode, registered(), toolOpts());
|
|
652
657
|
if (why) {
|