@yagni-app/code-staging 1.0.9-staging.1284.1 → 1.0.9-staging.1288.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.
@@ -7,6 +7,6 @@
7
7
  export { McpServerConfig, McpScope, PLUGIN_MCP_ENV, PROJECT_CONFIG_FILENAME, ScopedMcpServerConfig, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readPluginMcpServers, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
8
8
  export { ProjectApprovalState, decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
9
9
  export { McpAuthFile, StoredOAuthEntry, deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
10
- export { revokeTokensOnRemove } from "./auth.js";
10
+ export { authenticate, revokeTokensOnRemove } from "./auth.js";
11
11
  export { probeServer, McpHealthResult, McpHealthStatus } from "./manager.js";
12
12
  //# sourceMappingURL=cliConfig.d.ts.map
@@ -7,6 +7,6 @@
7
7
  export { PLUGIN_MCP_ENV, PROJECT_CONFIG_FILENAME, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readPluginMcpServers, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
8
8
  export { decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
9
9
  export { deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
10
- export { revokeTokensOnRemove } from "./auth.js";
10
+ export { authenticate, revokeTokensOnRemove } from "./auth.js";
11
11
  export { probeServer } from "./manager.js";
12
12
  //# sourceMappingURL=cliConfig.js.map
@@ -36,6 +36,8 @@ export interface McpHttpServerConfig {
36
36
  url: string;
37
37
  headers?: Record<string, string>;
38
38
  oauth?: McpOAuthConfig;
39
+ /** Optional explicit tool names, used by the Worker connection to avoid duplicating existing context tools. */
40
+ tools?: string[];
39
41
  }
40
42
  export type McpServerConfig = McpStdioServerConfig | McpHttpServerConfig;
41
43
  export interface ScopedMcpServerConfig {
@@ -267,6 +267,8 @@ export function validateServerConfig(value) {
267
267
  return { ok: true };
268
268
  }
269
269
  if (type === "http" || type === "sse") {
270
+ if (v["tools"] !== undefined && (!Array.isArray(v["tools"]) || v["tools"].length > 100 || v["tools"].some(name => typeof name !== "string" || name.length === 0 || name.length > 128)))
271
+ return { ok: false, message: "tools must be an array of up to 100 tool names" };
270
272
  if (typeof v["url"] !== "string" || v["url"].length === 0) {
271
273
  return { ok: false, message: `${type} server requires a non-empty "url"` };
272
274
  }
@@ -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,12 @@ export async function registerServerTools(pi, manager, serverName, env = process
26
30
  return result;
27
31
  }
28
32
  const toolTimeout = toolTimeoutFromEnv(env);
33
+ const selectedTools = server.config.type === "http" || server.config.type === "sse"
34
+ ? server.config.tools
35
+ : undefined;
29
36
  for (const tool of toolList.tools ?? []) {
37
+ if (selectedTools && !selectedTools.includes(tool.name))
38
+ continue;
30
39
  const fullToolName = buildMcpToolName(serverName, tool.name);
31
40
  if (!fullToolName) {
32
41
  result.warnings.push(`${serverName}: tool "${tool.name}" produced an empty wire name; skipped`);
@@ -45,7 +54,10 @@ export async function registerServerTools(pi, manager, serverName, env = process
45
54
  throw new Error(`MCP server "${serverName}" is not connected (try /mcp reconnect).`);
46
55
  }
47
56
  const args = (params && typeof params === "object" ? params : {});
48
- const call = active.client.callTool({ name: tool.name, arguments: args });
57
+ const call = active.client.callTool({
58
+ name: tool.name,
59
+ arguments: args,
60
+ });
49
61
  const settled = toolTimeout
50
62
  ? await withTimeout(call, toolTimeout, `MCP tool call timed out after ${toolTimeout}ms`)
51
63
  : await call;
@@ -53,7 +65,12 @@ export async function registerServerTools(pi, manager, serverName, env = process
53
65
  },
54
66
  };
55
67
  pi.registerTool(definition);
56
- 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
+ });
57
74
  if (mutating)
58
75
  result.mutatingToolNames.push(fullToolName);
59
76
  }
@@ -67,6 +84,19 @@ export function capDescription(description) {
67
84
  /** Cheap heuristic in the spirit of Claude Code's input-hint check; per-tool annotations arrive via listTools only in newer servers. */
68
85
  export function looksMutating(toolName, description) {
69
86
  const name = toolName.toLowerCase();
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))
99
+ return true;
70
100
  const writeHints = /^(create|add|update|edit|delete|remove|set|write|send|post|put|patch|deploy|publish|close|merge|assign|move|archive|trash|restore)/;
71
101
  if (writeHints.test(name))
72
102
  return true;
@@ -91,16 +121,49 @@ function schemaFor(inputSchema) {
91
121
  }
92
122
  function renderCallResult(settled, serverName, toolName) {
93
123
  const parts = [];
124
+ const images = [];
125
+ let imageBytes = 0;
94
126
  for (const item of settled.content ?? []) {
95
- if (item && typeof item === "object" && item.type === "text") {
127
+ if (item &&
128
+ typeof item === "object" &&
129
+ item.type === "text") {
96
130
  parts.push(String(item.text ?? ""));
97
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
+ }
98
155
  }
99
- 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)");
100
160
  if (settled.isError === true) {
101
161
  throw new Error(`MCP tool error from "${serverName}.${toolName}": ${text}`);
102
162
  }
103
- 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
+ };
104
167
  }
105
168
  function withTimeout(promise, ms, message) {
106
169
  return new Promise((resolve, reject) => {
@@ -38,6 +38,13 @@ export interface McpDeps {
38
38
  }
39
39
  /** The structural slice of the extension's mcp config surface this file needs. */
40
40
  export interface McpCliModule {
41
+ authenticate?(serverName: string, config: {
42
+ type: "http";
43
+ url: string;
44
+ tools: string[];
45
+ }): Promise<{
46
+ result: "AUTHORIZED";
47
+ }>;
41
48
  loadMcpServers(cwd: string, env: NodeJS.ProcessEnv): McpLoadResult;
42
49
  mcpConfigPath(): string;
43
50
  PROJECT_CONFIG_FILENAME: string;
@@ -21,7 +21,7 @@ import { fileURLToPath } from "node:url";
21
21
  import { CLAUDE_PLUGIN_MCP_ENV, claudeCompatArgs } from "./claudeCompat.js";
22
22
  import { agentDir } from "./credentials.js";
23
23
  import { DISTRIBUTION } from "./distribution.js";
24
- import { getActiveProfileName } from "./profiles.js";
24
+ import { getActiveProfileName, readActiveProfile } from "./profiles.js";
25
25
  export function resolveMcpConfigPath() {
26
26
  const bundled = fileURLToPath(new URL("./extension/mcp/cliConfig.js", import.meta.url));
27
27
  if (existsSync(bundled))
@@ -55,6 +55,9 @@ Scopes:
55
55
  user available in all your projects (~/.yagni-code/mcp.json)
56
56
  project shared via .mcp.json at the repo root (approval-gated)
57
57
 
58
+ Connect YAGNI Workers:
59
+ ${DISTRIBUTION.commandName} mcp connect-workers Browser consent for the active environment
60
+
58
61
  Examples:
59
62
  ${DISTRIBUTION.commandName} mcp add --transport http sentry https://mcp.sentry.dev/mcp
60
63
  ${DISTRIBUTION.commandName} mcp add -e API_KEY=xxx my-server -- npx my-mcp-server
@@ -90,7 +93,10 @@ export function parseMcpArgs(argv) {
90
93
  rest.push(...after.slice(i + 1));
91
94
  break;
92
95
  }
93
- 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") {
94
100
  if (flag === "s") {
95
101
  scope = scopeFrom(arg);
96
102
  scopeExplicit = true;
@@ -204,11 +210,28 @@ export function parseMcpArgs(argv) {
204
210
  }
205
211
  rest.push(arg);
206
212
  }
207
- 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") {
208
217
  throw new Error("Missing flag value.");
209
218
  }
210
219
  const [name, ...commandArgs] = rest;
211
- 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
+ };
212
235
  }
213
236
  function scopeFrom(value) {
214
237
  if (value === "local" || value === "user" || value === "project")
@@ -236,7 +259,11 @@ function describeScopePath(scope, cwd) {
236
259
  async function defaultPluginMcpEnv(cwd) {
237
260
  try {
238
261
  const profile = await getActiveProfileName();
239
- 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
+ });
240
267
  return compat.env[CLAUDE_PLUGIN_MCP_ENV];
241
268
  }
242
269
  catch {
@@ -272,6 +299,8 @@ export async function mcpCommand(args, deps = {}) {
272
299
  return 1;
273
300
  }
274
301
  switch (parsed.subcommand) {
302
+ case "connect-workers":
303
+ return connectWorkers(mod, parsed, { cwd, stdout, stderr }, deps.env ?? process.env);
275
304
  case undefined:
276
305
  case "help":
277
306
  stdout(USAGE);
@@ -283,18 +312,105 @@ export async function mcpCommand(args, deps = {}) {
283
312
  case "remove":
284
313
  return mcpRemove(mod, parsed, { cwd, stdout, stderr });
285
314
  case "list":
286
- 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
+ });
287
322
  case "get":
288
- 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
+ });
289
330
  case "reset-project-choices":
290
331
  return mcpResetChoices(mod, { cwd, stdout, stderr });
291
332
  case "add-from-claude":
292
- 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
+ });
293
339
  default:
294
340
  stderr(`Unknown mcp subcommand "${parsed.subcommand}".\n${USAGE}`);
295
341
  return 1;
296
342
  }
297
343
  }
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
+ ];
363
+ async function connectWorkers(mod, parsed, io, env) {
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) {
372
+ io.stderr("Usage: yagni mcp connect-workers (uses your active environment and private user configuration)\n");
373
+ return 1;
374
+ }
375
+ try {
376
+ if (!mod.authenticate)
377
+ throw new Error("Update YAGNI Code to connect Workers");
378
+ const profile = await readActiveProfile(env);
379
+ const url = new URL("/mcp", profile.baseUrl);
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))))
385
+ throw new Error("Worker connections require HTTPS or a local development server");
386
+ const config = {
387
+ type: "http",
388
+ url: url.href,
389
+ tools: WORKER_TOOL_NAMES,
390
+ };
391
+ const { file, errors } = mod.readUserMcpConfig();
392
+ if (errors.length)
393
+ throw new Error("Fix the existing MCP configuration before connecting Workers");
394
+ const previous = file
395
+ .mcpServers?.["yagni-workers"];
396
+ if (previous && JSON.stringify(previous) !== JSON.stringify(config))
397
+ throw new Error("An existing yagni-workers connection uses different settings. Remove it with yagni mcp remove yagni-workers before reconnecting");
398
+ const shadow = mod
399
+ .loadMcpServers(io.cwd, env)
400
+ .servers.find((server) => server.name === "yagni-workers" && server.scope !== "user");
401
+ if (shadow)
402
+ throw new Error("A project or local yagni-workers entry would override this connection. Remove that entry before connecting");
403
+ io.stdout("Opening YAGNI in your browser. Choose your workspace and permissions; paid Worker requests use that workspace's balance.\n");
404
+ await mod.authenticate("yagni-workers", config);
405
+ writeServerToScope(mod, "yagni-workers", config, "user", io.cwd);
406
+ io.stdout("Workers connected. Start a new YAGNI Code session to use them; /mcp shows connection status.\n");
407
+ return 0;
408
+ }
409
+ catch {
410
+ io.stderr("Worker connection did not complete. Check your environment and existing yagni-workers configuration, then retry browser consent.\n");
411
+ return 1;
412
+ }
413
+ }
298
414
  function mcpAdd(mod, parsed, io) {
299
415
  const { name, rest } = parsed;
300
416
  const commandOrUrl = rest[1];
@@ -308,7 +424,9 @@ function mcpAdd(mod, parsed, io) {
308
424
  serverConfig = {
309
425
  type: parsed.transport,
310
426
  url: commandOrUrl,
311
- ...(Object.keys(parsed.headers).length > 0 ? { headers: parsed.headers } : {}),
427
+ ...(Object.keys(parsed.headers).length > 0
428
+ ? { headers: parsed.headers }
429
+ : {}),
312
430
  ...(oauthBlock(parsed) ? { oauth: oauthBlock(parsed) } : {}),
313
431
  };
314
432
  }
@@ -398,18 +516,29 @@ function readClientSecret(io) {
398
516
  function validateConfigShape(config) {
399
517
  const type = config["type"];
400
518
  if (type === undefined || type === "stdio") {
401
- if (typeof config["command"] !== "string" || config["command"].length === 0) {
402
- 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
+ };
403
525
  }
404
526
  return { ok: true };
405
527
  }
406
528
  if (type === "http" || type === "sse") {
407
- if (typeof config["url"] !== "string" || config["url"].length === 0) {
408
- 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
+ };
409
535
  }
410
536
  return { ok: true };
411
537
  }
412
- 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
+ };
413
542
  }
414
543
  function writeServerToScope(mod, name, config, scope, cwd) {
415
544
  if (scope === "project") {
@@ -584,7 +713,9 @@ async function mcpGet(mod, parsed, io) {
584
713
  else {
585
714
  io.stdout(` Type: stdio\n`);
586
715
  io.stdout(` Command: ${config["command"]}\n`);
587
- const args = Array.isArray(config["args"]) ? config["args"] : [];
716
+ const args = Array.isArray(config["args"])
717
+ ? config["args"]
718
+ : [];
588
719
  if (args.length > 0)
589
720
  io.stdout(` Args: ${args.join(" ")}\n`);
590
721
  for (const [key, value] of Object.entries(config["env"] ?? {})) {
@@ -610,7 +741,9 @@ function printOAuthDetail(mod, name, config, stdout) {
610
741
  const cfg = config;
611
742
  const oauth = cfg["oauth"] ?? {};
612
743
  const clientId = typeof oauth["clientId"] === "string" ? oauth["clientId"] : undefined;
613
- const callbackPort = typeof oauth["callbackPort"] === "number" ? oauth["callbackPort"] : undefined;
744
+ const callbackPort = typeof oauth["callbackPort"] === "number"
745
+ ? oauth["callbackPort"]
746
+ : undefined;
614
747
  const stored = mod.getStoredOAuthEntry(name, config);
615
748
  if (clientId || callbackPort || stored?.clientSecret) {
616
749
  stdout(` OAuth: client_id ${clientId ? "configured" : "(DCR)"}, client_secret ${stored?.clientSecret ? "configured" : "not set"}${callbackPort ? `, callback_port ${callbackPort}` : ""}\n`);
@@ -642,10 +775,20 @@ async function mcpList(mod, io) {
642
775
  return 0;
643
776
  }
644
777
  const lines = [];
645
- const byScope = { user: [], project: [], local: [], plugin: [] };
778
+ const byScope = {
779
+ user: [],
780
+ project: [],
781
+ local: [],
782
+ plugin: [],
783
+ };
646
784
  for (const s of servers)
647
785
  (byScope[s.scope] ??= []).push(s);
648
- 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
+ };
649
792
  for (const scope of ["local", "project", "user", "plugin"]) {
650
793
  const group = byScope[scope];
651
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.1284.1",
3
+ "version": "1.0.9-staging.1288.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": "286c10887150e0d8d4abf763d44c780830a7710e"
61
+ "yagniSourceSha": "8afdf669270c6b0b2cd82437939af8d18427f1c6"
62
62
  }