@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 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.24.1",
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.4"
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.4",
64
+ "@earendil-works/pi-coding-agent": "1.4.0",
65
65
  "@types/node": "^24.19.0",
66
66
  "typescript": "^5.9.3"
67
67
  },
@@ -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://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json";
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) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-glauca-dark-256",
4
4
  "vars": {
5
5
  "ink": 254,
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-glauca-dark",
4
4
  "vars": {
5
5
  "sheet": "#12171c",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-glauca-light-256",
4
4
  "vars": {
5
5
  "ink": 234,
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-glauca-light",
4
4
  "vars": {
5
5
  "sheet": "#e9ece6",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-try-works-dark-256",
4
4
  "vars": {
5
5
  "ink": 254,
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-try-works-dark",
4
4
  "vars": {
5
5
  "sheet": "#12171c",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-try-works-light-256",
4
4
  "vars": {
5
5
  "ink": 234,
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "subsub-try-works-light",
4
4
  "vars": {
5
5
  "sheet": "#e9ece6",