@yagni-app/code-staging 1.0.9-staging.1286.1 → 1.0.9-staging.1292.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.
@@ -14,7 +14,11 @@ import { buildMcpToolName } from "./names.js";
14
14
  export const MAX_MCP_DESCRIPTION_LENGTH = 2048;
15
15
  export async function registerServerTools(pi, manager, serverName, env = process.env) {
16
16
  const server = manager.get(serverName);
17
- const result = { tools: [], mutatingToolNames: [], warnings: [] };
17
+ const result = {
18
+ tools: [],
19
+ mutatingToolNames: [],
20
+ warnings: [],
21
+ };
18
22
  if (!server?.client)
19
23
  return result;
20
24
  let toolList;
@@ -26,7 +30,9 @@ export async function registerServerTools(pi, manager, serverName, env = process
26
30
  return result;
27
31
  }
28
32
  const toolTimeout = toolTimeoutFromEnv(env);
29
- const selectedTools = server.config.type === "http" || server.config.type === "sse" ? server.config.tools : undefined;
33
+ const selectedTools = server.config.type === "http" || server.config.type === "sse"
34
+ ? server.config.tools
35
+ : undefined;
30
36
  for (const tool of toolList.tools ?? []) {
31
37
  if (selectedTools && !selectedTools.includes(tool.name))
32
38
  continue;
@@ -48,7 +54,10 @@ export async function registerServerTools(pi, manager, serverName, env = process
48
54
  throw new Error(`MCP server "${serverName}" is not connected (try /mcp reconnect).`);
49
55
  }
50
56
  const args = (params && typeof params === "object" ? params : {});
51
- const call = active.client.callTool({ name: tool.name, arguments: args });
57
+ const call = active.client.callTool({
58
+ name: tool.name,
59
+ arguments: args,
60
+ });
52
61
  const settled = toolTimeout
53
62
  ? await withTimeout(call, toolTimeout, `MCP tool call timed out after ${toolTimeout}ms`)
54
63
  : await call;
@@ -56,7 +65,12 @@ export async function registerServerTools(pi, manager, serverName, env = process
56
65
  },
57
66
  };
58
67
  pi.registerTool(definition);
59
- result.tools.push({ toolName: fullToolName, serverName, originalName: tool.name, description });
68
+ result.tools.push({
69
+ toolName: fullToolName,
70
+ serverName,
71
+ originalName: tool.name,
72
+ description,
73
+ });
60
74
  if (mutating)
61
75
  result.mutatingToolNames.push(fullToolName);
62
76
  }
@@ -70,7 +84,18 @@ export function capDescription(description) {
70
84
  /** Cheap heuristic in the spirit of Claude Code's input-hint check; per-tool annotations arrive via listTools only in newer servers. */
71
85
  export function looksMutating(toolName, description) {
72
86
  const name = toolName.toLowerCase();
73
- if (["critique_plan", "review_pr", "engage_worker", "propose_instruction_change", "prepare_review_publication", "resolve_decision"].includes(name))
87
+ if ([
88
+ "critique_plan",
89
+ "review_pr",
90
+ "test_pr",
91
+ "validate_qa_replay",
92
+ "accept_qa_replay",
93
+ "authorize_qa_fork",
94
+ "engage_worker",
95
+ "propose_instruction_change",
96
+ "prepare_review_publication",
97
+ "resolve_decision",
98
+ ].includes(name))
74
99
  return true;
75
100
  const writeHints = /^(create|add|update|edit|delete|remove|set|write|send|post|put|patch|deploy|publish|close|merge|assign|move|archive|trash|restore)/;
76
101
  if (writeHints.test(name))
@@ -96,16 +121,49 @@ function schemaFor(inputSchema) {
96
121
  }
97
122
  function renderCallResult(settled, serverName, toolName) {
98
123
  const parts = [];
124
+ const images = [];
125
+ let imageBytes = 0;
99
126
  for (const item of settled.content ?? []) {
100
- if (item && typeof item === "object" && item.type === "text") {
127
+ if (item &&
128
+ typeof item === "object" &&
129
+ item.type === "text") {
101
130
  parts.push(String(item.text ?? ""));
102
131
  }
132
+ else if (item &&
133
+ typeof item === "object" &&
134
+ "type" in item &&
135
+ item.type === "image") {
136
+ const value = item;
137
+ if (typeof value.data !== "string" ||
138
+ typeof value.mimeType !== "string" ||
139
+ !["image/png", "image/jpeg", "image/webp"].includes(value.mimeType) ||
140
+ value.data.length > 12 * 1024 * 1024 ||
141
+ images.length >= 8 ||
142
+ value.data.length % 4 !== 0 ||
143
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(value.data))
144
+ throw new Error("MCP image is unsupported or exceeds its limit.");
145
+ const bytes = Buffer.byteLength(value.data, "base64");
146
+ imageBytes += bytes;
147
+ if (!bytes || imageBytes > 8 * 1024 * 1024)
148
+ throw new Error("MCP image is unsupported or exceeds its limit.");
149
+ images.push({
150
+ type: "image",
151
+ data: value.data,
152
+ mimeType: value.mimeType,
153
+ });
154
+ }
103
155
  }
104
- const text = parts.join("\n") || "(no text content)";
156
+ const text = parts.join("\n") ||
157
+ (images.length
158
+ ? `Image returned by ${serverName}.${toolName} (untrusted tool content).`
159
+ : "(no text content)");
105
160
  if (settled.isError === true) {
106
161
  throw new Error(`MCP tool error from "${serverName}.${toolName}": ${text}`);
107
162
  }
108
- return { content: [{ type: "text", text }], details: { server: serverName, tool: toolName } };
163
+ return {
164
+ content: [{ type: "text", text }, ...images],
165
+ details: { server: serverName, tool: toolName },
166
+ };
109
167
  }
110
168
  function withTimeout(promise, ms, message) {
111
169
  return new Promise((resolve, reject) => {
@@ -93,7 +93,10 @@ export function parseMcpArgs(argv) {
93
93
  rest.push(...after.slice(i + 1));
94
94
  break;
95
95
  }
96
- if (flag === "s" || flag === "t" || flag === "client-id" || flag === "callback-port") {
96
+ if (flag === "s" ||
97
+ flag === "t" ||
98
+ flag === "client-id" ||
99
+ flag === "callback-port") {
97
100
  if (flag === "s") {
98
101
  scope = scopeFrom(arg);
99
102
  scopeExplicit = true;
@@ -207,11 +210,28 @@ export function parseMcpArgs(argv) {
207
210
  }
208
211
  rest.push(arg);
209
212
  }
210
- if (flag === "s" || flag === "t" || flag === "client-id" || flag === "callback-port") {
213
+ if (flag === "s" ||
214
+ flag === "t" ||
215
+ flag === "client-id" ||
216
+ flag === "callback-port") {
211
217
  throw new Error("Missing flag value.");
212
218
  }
213
219
  const [name, ...commandArgs] = rest;
214
- return { subcommand, name, rest, scope, scopeExplicit, transport, transportExplicit, env, headers, commandArgs, clientId, clientSecret, callbackPort };
220
+ return {
221
+ subcommand,
222
+ name,
223
+ rest,
224
+ scope,
225
+ scopeExplicit,
226
+ transport,
227
+ transportExplicit,
228
+ env,
229
+ headers,
230
+ commandArgs,
231
+ clientId,
232
+ clientSecret,
233
+ callbackPort,
234
+ };
215
235
  }
216
236
  function scopeFrom(value) {
217
237
  if (value === "local" || value === "user" || value === "project")
@@ -239,7 +259,11 @@ function describeScopePath(scope, cwd) {
239
259
  async function defaultPluginMcpEnv(cwd) {
240
260
  try {
241
261
  const profile = await getActiveProfileName();
242
- const compat = await claudeCompatArgs({ cwd, agentDir: agentDir(profile), interactive: false });
262
+ const compat = await claudeCompatArgs({
263
+ cwd,
264
+ agentDir: agentDir(profile),
265
+ interactive: false,
266
+ });
243
267
  return compat.env[CLAUDE_PLUGIN_MCP_ENV];
244
268
  }
245
269
  catch {
@@ -288,21 +312,63 @@ export async function mcpCommand(args, deps = {}) {
288
312
  case "remove":
289
313
  return mcpRemove(mod, parsed, { cwd, stdout, stderr });
290
314
  case "list":
291
- return mcpList(mod, { cwd, stdout, stderr, env: await envWithPluginMcp(deps, cwd), probeServer: deps.probeServer });
315
+ return mcpList(mod, {
316
+ cwd,
317
+ stdout,
318
+ stderr,
319
+ env: await envWithPluginMcp(deps, cwd),
320
+ probeServer: deps.probeServer,
321
+ });
292
322
  case "get":
293
- return mcpGet(mod, parsed, { cwd, stdout, stderr, env: await envWithPluginMcp(deps, cwd), probeServer: deps.probeServer });
323
+ return mcpGet(mod, parsed, {
324
+ cwd,
325
+ stdout,
326
+ stderr,
327
+ env: await envWithPluginMcp(deps, cwd),
328
+ probeServer: deps.probeServer,
329
+ });
294
330
  case "reset-project-choices":
295
331
  return mcpResetChoices(mod, { cwd, stdout, stderr });
296
332
  case "add-from-claude":
297
- return mcpAddFromClaude(mod, parsed, { cwd, stdout, stderr, home: deps.home ?? homedir() });
333
+ return mcpAddFromClaude(mod, parsed, {
334
+ cwd,
335
+ stdout,
336
+ stderr,
337
+ home: deps.home ?? homedir(),
338
+ });
298
339
  default:
299
340
  stderr(`Unknown mcp subcommand "${parsed.subcommand}".\n${USAGE}`);
300
341
  return 1;
301
342
  }
302
343
  }
303
- const WORKER_TOOL_NAMES = ["get_context", "create_team", "engage_worker", "critique_plan", "review_pr", "list_work", "get_work", "add_feedback", "propose_instruction_change", "prepare_review_publication", "resolve_decision"];
344
+ const WORKER_TOOL_NAMES = [
345
+ "get_context",
346
+ "create_team",
347
+ "engage_worker",
348
+ "critique_plan",
349
+ "review_pr",
350
+ "test_pr",
351
+ "get_qa_evidence",
352
+ "get_qa_replay",
353
+ "validate_qa_replay",
354
+ "accept_qa_replay",
355
+ "authorize_qa_fork",
356
+ "list_work",
357
+ "get_work",
358
+ "add_feedback",
359
+ "propose_instruction_change",
360
+ "prepare_review_publication",
361
+ "resolve_decision",
362
+ ];
304
363
  async function connectWorkers(mod, parsed, io, env) {
305
- if (parsed.rest.length || parsed.scopeExplicit || parsed.transportExplicit || Object.keys(parsed.headers).length || Object.keys(parsed.env).length || parsed.clientId || parsed.clientSecret || parsed.callbackPort) {
364
+ if (parsed.rest.length ||
365
+ parsed.scopeExplicit ||
366
+ parsed.transportExplicit ||
367
+ Object.keys(parsed.headers).length ||
368
+ Object.keys(parsed.env).length ||
369
+ parsed.clientId ||
370
+ parsed.clientSecret ||
371
+ parsed.callbackPort) {
306
372
  io.stderr("Usage: yagni mcp connect-workers (uses your active environment and private user configuration)\n");
307
373
  return 1;
308
374
  }
@@ -311,16 +377,27 @@ async function connectWorkers(mod, parsed, io, env) {
311
377
  throw new Error("Update YAGNI Code to connect Workers");
312
378
  const profile = await readActiveProfile(env);
313
379
  const url = new URL("/mcp", profile.baseUrl);
314
- if (url.username || url.password || (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
380
+ if (url.username ||
381
+ url.password ||
382
+ (url.protocol !== "https:" &&
383
+ !(url.protocol === "http:" &&
384
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
315
385
  throw new Error("Worker connections require HTTPS or a local development server");
316
- const config = { type: "http", url: url.href, tools: WORKER_TOOL_NAMES };
386
+ const config = {
387
+ type: "http",
388
+ url: url.href,
389
+ tools: WORKER_TOOL_NAMES,
390
+ };
317
391
  const { file, errors } = mod.readUserMcpConfig();
318
392
  if (errors.length)
319
393
  throw new Error("Fix the existing MCP configuration before connecting Workers");
320
- const previous = file.mcpServers?.["yagni-workers"];
394
+ const previous = file
395
+ .mcpServers?.["yagni-workers"];
321
396
  if (previous && JSON.stringify(previous) !== JSON.stringify(config))
322
397
  throw new Error("An existing yagni-workers connection uses different settings. Remove it with yagni mcp remove yagni-workers before reconnecting");
323
- const shadow = mod.loadMcpServers(io.cwd, env).servers.find(server => server.name === "yagni-workers" && server.scope !== "user");
398
+ const shadow = mod
399
+ .loadMcpServers(io.cwd, env)
400
+ .servers.find((server) => server.name === "yagni-workers" && server.scope !== "user");
324
401
  if (shadow)
325
402
  throw new Error("A project or local yagni-workers entry would override this connection. Remove that entry before connecting");
326
403
  io.stdout("Opening YAGNI in your browser. Choose your workspace and permissions; paid Worker requests use that workspace's balance.\n");
@@ -347,7 +424,9 @@ function mcpAdd(mod, parsed, io) {
347
424
  serverConfig = {
348
425
  type: parsed.transport,
349
426
  url: commandOrUrl,
350
- ...(Object.keys(parsed.headers).length > 0 ? { headers: parsed.headers } : {}),
427
+ ...(Object.keys(parsed.headers).length > 0
428
+ ? { headers: parsed.headers }
429
+ : {}),
351
430
  ...(oauthBlock(parsed) ? { oauth: oauthBlock(parsed) } : {}),
352
431
  };
353
432
  }
@@ -437,18 +516,29 @@ function readClientSecret(io) {
437
516
  function validateConfigShape(config) {
438
517
  const type = config["type"];
439
518
  if (type === undefined || type === "stdio") {
440
- if (typeof config["command"] !== "string" || config["command"].length === 0) {
441
- return { ok: false, message: 'stdio server requires a non-empty "command"' };
519
+ if (typeof config["command"] !== "string" ||
520
+ config["command"].length === 0) {
521
+ return {
522
+ ok: false,
523
+ message: 'stdio server requires a non-empty "command"',
524
+ };
442
525
  }
443
526
  return { ok: true };
444
527
  }
445
528
  if (type === "http" || type === "sse") {
446
- if (typeof config["url"] !== "string" || config["url"].length === 0) {
447
- return { ok: false, message: `${type} server requires a non-empty "url"` };
529
+ if (typeof config["url"] !== "string" ||
530
+ config["url"].length === 0) {
531
+ return {
532
+ ok: false,
533
+ message: `${type} server requires a non-empty "url"`,
534
+ };
448
535
  }
449
536
  return { ok: true };
450
537
  }
451
- return { ok: false, message: 'unknown "type" — expected stdio, http, or sse' };
538
+ return {
539
+ ok: false,
540
+ message: 'unknown "type" — expected stdio, http, or sse',
541
+ };
452
542
  }
453
543
  function writeServerToScope(mod, name, config, scope, cwd) {
454
544
  if (scope === "project") {
@@ -623,7 +713,9 @@ async function mcpGet(mod, parsed, io) {
623
713
  else {
624
714
  io.stdout(` Type: stdio\n`);
625
715
  io.stdout(` Command: ${config["command"]}\n`);
626
- const args = Array.isArray(config["args"]) ? config["args"] : [];
716
+ const args = Array.isArray(config["args"])
717
+ ? config["args"]
718
+ : [];
627
719
  if (args.length > 0)
628
720
  io.stdout(` Args: ${args.join(" ")}\n`);
629
721
  for (const [key, value] of Object.entries(config["env"] ?? {})) {
@@ -649,7 +741,9 @@ function printOAuthDetail(mod, name, config, stdout) {
649
741
  const cfg = config;
650
742
  const oauth = cfg["oauth"] ?? {};
651
743
  const clientId = typeof oauth["clientId"] === "string" ? oauth["clientId"] : undefined;
652
- const callbackPort = typeof oauth["callbackPort"] === "number" ? oauth["callbackPort"] : undefined;
744
+ const callbackPort = typeof oauth["callbackPort"] === "number"
745
+ ? oauth["callbackPort"]
746
+ : undefined;
653
747
  const stored = mod.getStoredOAuthEntry(name, config);
654
748
  if (clientId || callbackPort || stored?.clientSecret) {
655
749
  stdout(` OAuth: client_id ${clientId ? "configured" : "(DCR)"}, client_secret ${stored?.clientSecret ? "configured" : "not set"}${callbackPort ? `, callback_port ${callbackPort}` : ""}\n`);
@@ -681,10 +775,20 @@ async function mcpList(mod, io) {
681
775
  return 0;
682
776
  }
683
777
  const lines = [];
684
- const byScope = { user: [], project: [], local: [], plugin: [] };
778
+ const byScope = {
779
+ user: [],
780
+ project: [],
781
+ local: [],
782
+ plugin: [],
783
+ };
685
784
  for (const s of servers)
686
785
  (byScope[s.scope] ??= []).push(s);
687
- const labels = { local: "Local", project: "Project", user: "User", plugin: "Plugin" };
786
+ const labels = {
787
+ local: "Local",
788
+ project: "Project",
789
+ user: "User",
790
+ plugin: "Plugin",
791
+ };
688
792
  for (const scope of ["local", "project", "user", "plugin"]) {
689
793
  const group = byScope[scope];
690
794
  if (!group?.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.0.9-staging.1286.1",
3
+ "version": "1.0.9-staging.1292.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "e0d42d2e96ab52ea3116681d53f9c11924d6859c"
61
+ "yagniSourceSha": "be851fcdc82a066edbe8ebb394685bc1b61169f8"
62
62
  }