pennyrouter 0.3.53 → 0.3.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.53",
3
+ "version": "0.3.55",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
package/src/mcp.js CHANGED
@@ -11,6 +11,9 @@
11
11
  thread costs the caller nothing. That asymmetry is the whole reason for two
12
12
  tools rather than one with a flag.
13
13
  thread_transfer capture the current local session and write it for the other harness.
14
+ load_tools make tools that PennyRouter left out of the request available. The gateway
15
+ reads this call from the conversation history and includes the named tools
16
+ on the next request; the call itself does no work here.
14
17
 
15
18
  Because the destination is a parameter, "resume this Perplexity thread in Codex" and "…in
16
19
  Claude Code" are the same call with a different `harness`. Each harness has its own writer:
@@ -119,6 +122,24 @@ const TOOLS = [
119
122
  required: ["target_harness"],
120
123
  },
121
124
  },
125
+ {
126
+ name: "load_tools",
127
+ description:
128
+ "Load tools listed under \"Tools available on request\" in your instructions. Pass the " +
129
+ "exact tool names, or a namespace name to load all of its tools. They are callable on " +
130
+ "your next step. Only load what the current task needs.",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ names: {
135
+ type: "array",
136
+ items: { type: "string" },
137
+ description: "Tool or namespace names from the available-on-request list.",
138
+ },
139
+ },
140
+ required: ["names"],
141
+ },
142
+ },
122
143
  ];
123
144
 
124
145
  function flattenClaudeProject(cwd) {
@@ -294,6 +315,14 @@ async function callTool(name, args, context) {
294
315
  ].join("\n");
295
316
  }
296
317
 
318
+ if (name === "load_tools") {
319
+ const names = (Array.isArray(args?.names) ? args.names : [])
320
+ .map((value) => String(value).trim())
321
+ .filter(Boolean);
322
+ if (!names.length) throw new Error("names is required");
323
+ return `Loaded: ${names.join(", ")}. Call them directly now.`;
324
+ }
325
+
297
326
  throw new Error(`unknown tool: ${name}`);
298
327
  }
299
328
 
package/src/mcp.test.js CHANGED
@@ -116,7 +116,7 @@ const workspace = mkdtempSync(join(tmpdir(), "pr-mcp-ws-"));
116
116
 
117
117
  assert.deepEqual(
118
118
  byId.get(2).result.tools.map((t) => t.name),
119
- ["thread_read", "thread_resume", "thread_transfer"],
119
+ ["thread_read", "thread_resume", "thread_transfer", "load_tools"],
120
120
  );
121
121
 
122
122
  // Observed in a real session: a blocked tool call is indistinguishable from an empty one, so
@@ -153,6 +153,34 @@ const workspace = mkdtempSync(join(tmpdir(), "pr-mcp-ws-"));
153
153
  assert.equal(byId.get(4).result.isError, undefined);
154
154
  }
155
155
 
156
+ // -- load_tools ------------------------------------------------------------
157
+ // The gateway loads the tools by reading this call from history; the server only confirms, and
158
+ // must not contact the gateway.
159
+ {
160
+ let contacted = false;
161
+ const { server, port } = await stubGateway((req, body, res) => {
162
+ contacted = true;
163
+ res.end("{}");
164
+ });
165
+ const { messages } = await rpc(
166
+ [
167
+ {
168
+ jsonrpc: "2.0", id: 1, method: "tools/call",
169
+ params: { name: "load_tools", arguments: { names: ["list_database_connections", " "] } },
170
+ },
171
+ { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "load_tools", arguments: {} } },
172
+ ],
173
+ { port, home },
174
+ );
175
+ server.close();
176
+ const byId = new Map(messages.map((m) => [m.id, m]));
177
+ assert.equal(byId.get(1).result.content[0].text,
178
+ "Loaded: list_database_connections. Call them directly now.");
179
+ assert.equal(byId.get(1).result.isError, undefined);
180
+ assert.equal(byId.get(2).result.isError, true);
181
+ assert.equal(contacted, false);
182
+ }
183
+
156
184
  // -- harness routing ------------------------------------------------------
157
185
  // "Resume this in Codex" is the same call with a different `harness`, so the tool must dispatch
158
186
  // to the right writer and print the right resume command. Skipped when codex is not installed:
@@ -616,7 +616,7 @@
616
616
  const setFocus = (side) => {
617
617
  for (const each of sides) $(`${each}-pane`).classList.toggle('focused', each === side);
618
618
  };
619
- let sync = !hosted;
619
+ let sync = true;
620
620
  const setSync = (value) => {
621
621
  sync = value;
622
622
  const button = $('sync');