autodev-cli 1.4.86 → 1.4.93

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.
@@ -72,6 +72,7 @@ exports.WORKSPACE_DIRS = [
72
72
  '.autodev',
73
73
  path.posix.join('media', 'profile'),
74
74
  path.posix.join('media', 'skills'),
75
+ path.posix.join('.claude', 'skills'),
75
76
  ];
76
77
  /**
77
78
  * Identity/connection settings in `.autodev/settings.json` that are scoped to a
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/agentBackup/layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/agentBackup/layout.ts"],"names":[],"mappings":"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"}
@@ -45,6 +45,7 @@ const readline = __importStar(require("readline"));
45
45
  const child_process_1 = require("child_process");
46
46
  const url_1 = require("url");
47
47
  const settingsLoader_1 = require("../core/settingsLoader");
48
+ const modelInfo_1 = require("../core/modelInfo");
48
49
  const officeSocket_1 = require("../officeSocket");
49
50
  const presenceGuard_1 = require("../presenceGuard");
50
51
  const fileBrowser_1 = require("../fileBrowser");
@@ -52,9 +53,11 @@ const gitRequest_1 = require("../git/gitRequest");
52
53
  const manager_1 = require("../vnc/manager");
53
54
  const manager_2 = require("../rdp/manager");
54
55
  const projectMcp_1 = require("../core/projectMcp");
56
+ const projectSkills_1 = require("../core/projectSkills");
55
57
  const configManager_1 = require("../configManager");
56
58
  const version_1 = require("../version");
57
59
  const liveNarration_1 = require("../core/liveNarration");
60
+ const redactSecrets_1 = require("../core/redactSecrets");
58
61
  /**
59
62
  * `autodev mcp-operate` — run a local stdio MCP server that lets a pure MCP
60
63
  * client (Claude Desktop/Code, etc.) operate a pixel-office character with NO
@@ -67,8 +70,12 @@ const liveNarration_1 = require("../core/liveNarration");
67
70
  * single command — no remote-server approval friction, and the key/url stay in
68
71
  * autodev config instead of being pasted into the client.
69
72
  *
70
- * claude mcp add pixel-office -- autodev mcp-operate --key <api_key> --url <…/api/office-mcp>
73
+ * claude mcp add pixel-office-<agent-slug> -- autodev mcp-operate --key <api_key> --url <…/api/office-mcp>
71
74
  * # or, inside a bound workspace, just: autodev mcp-operate
75
+ *
76
+ * Name each server pixel-office-<agent-slug> (not a bare "pixel-office"): one MCP
77
+ * client / Claude session can then operate SEVERAL office characters at once, each
78
+ * under its own server name, instead of every agent colliding on one shared name.
72
79
  */
73
80
  /** Derive the operator-MCP URL from a bound workspace's serverBaseUrl. */
74
81
  function officeMcpUrl(serverBaseUrl) {
@@ -169,15 +176,18 @@ function proxy(endpoint, key, body) {
169
176
  function mcpOperateCommand(program) {
170
177
  program
171
178
  .command('mcp-operate [path]')
172
- .description('Run a stdio MCP server that operates a pixel-office agent (bridges to …/api/office-mcp). Add it with: claude mcp add pixel-office -- autodev mcp-operate --key <api_key> --url <url>')
179
+ .description('Run an MCP server that operates a pixel-office agent (bridges to …/api/office-mcp). Adds two bridge-synthesized tools on top of the office tools: wait_for_events (block on live office activity) and ask_user (ask the user a decision / multi-step wizard and BLOCK for their answer). Speaks stdio by default; add --http-port to run a PERSISTENT streamable-HTTP sidecar an opencode `type: remote` MCP can attach to. Add stdio with: claude mcp add pixel-office-<agent-slug> -- autodev mcp-operate --key <api_key> --url <url> (per-agent name lets one client run several characters at once). All outbound agent content (hook events, tool output, assistant narration, task results / A2A replies) is secret-redacted before it leaves the machine.')
173
180
  .option('--url <url>', 'Operator MCP URL (…/api/office-mcp). Default: derived from the workspace binding.')
174
181
  .option('--key <apiKey>', 'The character api_key (Bearer). Default: the workspace serverApiKey.')
182
+ .option('--http-port <port>', 'Run a persistent MCP-over-HTTP (Streamable HTTP) server on this port instead of stdio. Point an opencode `type: remote` MCP at http://<host>:<port>/mcp.', (v) => parseInt(v, 10))
183
+ .option('--http-host <host>', 'Bind address for --http-port (default 127.0.0.1).')
175
184
  .option('--no-socket', 'Do not open the presence WebSocket (stay on poll-based presence only).')
176
185
  .option('--file-browser', 'Serve the office file browser for this MCP-only agent (read/write files in the workspace over the office file browser).')
177
186
  .option('--git', 'Serve the office git panel for this MCP-only agent (status/diff/stage/commit/branch in the workspace over the office git panel).')
178
187
  .option('--vnc', 'Serve office VNC remote-desktop sessions for this MCP-only agent (input forwarding + framebuffer streaming).')
179
188
  .option('--rdp', 'Serve office RDP remote-desktop sessions for this MCP-only agent (input forwarding + framebuffer streaming).')
180
189
  .option('--mcp-update', 'Honor mcp_update frames: sync remote-supplied MCP config into the workspace (relaunch to pick up spawn changes).')
190
+ .option('--skill-update', 'Honor skill_update frames: sync remote-supplied Claude Code skills into the workspace (.claude/skills/<slug>/SKILL.md; live-reloads, no relaunch).')
181
191
  .action(async (workspacePath, opts) => {
182
192
  const cwd = workspacePath ? path.resolve(workspacePath) : process.cwd();
183
193
  let settings = (0, settingsLoader_1.loadSettingsForRoot)(cwd);
@@ -191,6 +201,7 @@ function mcpOperateCommand(program) {
191
201
  let vncEnabled = opts.vnc === true || settings.vncEnabled === true;
192
202
  let rdpEnabled = opts.rdp === true || settings.rdpEnabled === true;
193
203
  let mcpUpdateEnabled = opts.mcpUpdate === true || settings.mcpUpdateEnabled === true;
204
+ let skillUpdateEnabled = opts.skillUpdate === true || settings.skillUpdateEnabled === true;
194
205
  if (!endpoint || !key) {
195
206
  process.stderr.write('autodev mcp-operate: need --url and --key (or run inside a workspace bound to an office).\n');
196
207
  process.exit(1);
@@ -229,6 +240,29 @@ function mcpOperateCommand(program) {
229
240
  description: "Block until new office activity arrives over the live socket (or a timeout), then return it. Teammates' messages, status changes, task assignments and tool activity stream in as they happen — call this in a loop to react in real time. Returns any buffered events immediately.",
230
241
  inputSchema: { type: 'object', properties: { timeout_seconds: { type: 'integer', description: 'Max seconds to wait for the next event (default 25, max 55).' } }, required: [] },
231
242
  };
243
+ // ── Blocking user-decision tool (ask_user) ───────────────────────────────
244
+ // Ask the user to make a real decision — a single question OR a multi-step
245
+ // wizard — and BLOCK until they answer in the pixel-office chat, then return
246
+ // their answer so the model continues. Bridge-synthesized like WAIT_TOOL, so
247
+ // it reaches every provider (claude/opencode/grok/copilot) uniformly. The
248
+ // office holds the request state; this tool just creates it and polls.
249
+ const ASK_USER_TOOL = {
250
+ name: 'ask_user',
251
+ description: "Ask the user to make a decision and BLOCK for their answer. Use for genuine either/or choices the user must make. Provide clear options; set allow_other to let them type a custom answer; use steps for a multi-step wizard. Single question: pass { question, options, allow_other?, multi_select? }. Wizard: pass { title?, steps: [{ question, options, allow_other?, multi_select? }] }. Each option is a string OR an object { label, description }. IMPORTANT: if the call returns 'Still waiting...', call ask_user AGAIN with the SAME { request_id } it gives you (do NOT create a new question) — keep re-calling until you get the user's answer.",
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {
255
+ question: { type: 'string', description: 'The decision to ask (single-question form).' },
256
+ options: { type: 'array', description: 'Choices for the single question — strings, or { label, description } objects.', items: {} },
257
+ allow_other: { type: 'boolean', description: 'Let the user type a custom free-text answer in addition to the listed options.' },
258
+ multi_select: { type: 'boolean', description: 'Let the user pick more than one option.' },
259
+ title: { type: 'string', description: 'Optional title shown above a multi-step wizard.' },
260
+ steps: { type: 'array', description: 'Wizard steps (multi-step form) — each { question, options, allow_other?, multi_select? }.', items: { type: 'object' } },
261
+ request_id: { type: 'string', description: 'Resume waiting for an EXISTING request (do NOT create a new one). Pass the id from a prior "Still waiting" ask_user result to keep polling until the user answers.' },
262
+ },
263
+ required: [],
264
+ },
265
+ };
232
266
  // ── Presence WebSocket (optional; --no-socket disables) ──────────────────
233
267
  // Holds a live connection so the office shows this MCP agent genuinely
234
268
  // online, and surfaces task/message pushes to the client as MCP
@@ -257,7 +291,20 @@ function mcpOperateCommand(program) {
257
291
  const id = body.id !== undefined && body.id !== null ? body.id : `wsreq-${++wsReqSeq}`;
258
292
  const k = String(id);
259
293
  const timer = setTimeout(() => { wsPending.delete(k); reject(new Error('operator_request timed out')); }, 20_000);
260
- wsPending.set(k, (resp) => { clearTimeout(timer); resolve(resp); });
294
+ wsPending.set(k, (resp) => {
295
+ clearTimeout(timer);
296
+ // A JSON-RPC error with no result (e.g. a stale long-running WS server
297
+ // missing a newly-added office tool) → REJECT so callOffice fails over
298
+ // to the HTTP proxy, which php-fpm always serves with fresh code. A
299
+ // normal result resolves. Without this, a stale WS silently returns an
300
+ // empty result and callers like ask_user hard-fail on JSON.parse('').
301
+ if (resp && resp['error'] && (resp['result'] === undefined || resp['result'] === null)) {
302
+ reject(new Error(String(resp['error']?.message ?? 'operator_response error')));
303
+ }
304
+ else {
305
+ resolve(resp);
306
+ }
307
+ });
261
308
  socket.sendFrame({ type: 'operator_request', id, method: body.method, params: body.params ?? {} });
262
309
  });
263
310
  // Prefer the socket when ready; fall back to HTTP otherwise (startup handshake,
@@ -299,6 +346,7 @@ function mcpOperateCommand(program) {
299
346
  vncEnabled = opts.vnc === true || settings.vncEnabled === true;
300
347
  rdpEnabled = opts.rdp === true || settings.rdpEnabled === true;
301
348
  mcpUpdateEnabled = opts.mcpUpdate === true || settings.mcpUpdateEnabled === true;
349
+ skillUpdateEnabled = opts.skillUpdate === true || settings.skillUpdateEnabled === true;
302
350
  applyRemoteDesktopSettings();
303
351
  };
304
352
  // ── Live MCP-config reload (mcp_update frame) ────────────────────────────
@@ -333,6 +381,33 @@ function mcpOperateCommand(program) {
333
381
  logErr(`⚠️ MCP update failed: ${err instanceof Error ? err.message : String(err)}`);
334
382
  }
335
383
  };
384
+ // ── Live skill reload (skill_update frame) ───────────────────────────────
385
+ // The office pushes the agent's FULL effective skill set. Skills live-reload
386
+ // (Claude re-reads .claude/skills each run), so there is nothing to relaunch —
387
+ // sanitize, full-replace on disk, fold prose for non-Claude providers, report.
388
+ const handleSkillUpdate = (skills) => {
389
+ reloadBridgeSettings();
390
+ if (!skillUpdateEnabled) {
391
+ logErr('🔒 skill_update ignored — skillUpdateEnabled is off (set it in .autodev/settings.json or pass --skill-update to allow)');
392
+ return;
393
+ }
394
+ logErr('🧩 skill_update received — validating and writing .claude/skills…');
395
+ const { safe, rejected } = (0, projectSkills_1.sanitizeRemoteSkills)(cwd, skills);
396
+ if (rejected.length) {
397
+ logErr(`⚠️ skill_update dropped ${rejected.length} entr${rejected.length === 1 ? 'y' : 'ies'}: ${rejected.map(r => `${r.name} (${r.reason})`).join(', ')}`);
398
+ }
399
+ try {
400
+ const { written, removed } = (0, projectSkills_1.saveProjectSkills)(cwd, safe);
401
+ if (!(0, projectSkills_1.providerConsumesSkills)(settings.provider)) {
402
+ (0, projectSkills_1.foldSkillsIntoProfile)(cwd, safe);
403
+ }
404
+ void configManager_1.ConfigManager.reportProjectSkills(cwd, written, logErr);
405
+ logErr(`✅ skills synced — wrote ${written.length}, removed ${removed.length} (.claude/skills). Live-reloads on the next run — no relaunch.`);
406
+ }
407
+ catch (err) {
408
+ logErr(`⚠️ skill update failed: ${err instanceof Error ? err.message : String(err)}`);
409
+ }
410
+ };
336
411
  // ── Presence socket lifecycle (runtime-driven, self-healing) ─────────────
337
412
  // Whether the bridge holds the presence socket is decided at RUNTIME from
338
413
  // ACTUAL loop presence, NOT a static config flag. A co-located `autodev
@@ -377,6 +452,10 @@ function mcpOperateCommand(program) {
377
452
  log: (l) => process.stderr.write(l + '\n'),
378
453
  meta: {
379
454
  provider: 'mcp-operator', cliVersion: version_1.CLI_VERSION,
455
+ // The MCP-only agent's worker provider drives real turns; announce its
456
+ // effective model (undefined when it runs an account default) so the
457
+ // office can badge the model. 'mcp-operator' is only the transport marker.
458
+ model: (0, modelInfo_1.resolveConfiguredModel)(settings),
380
459
  fileBrowserEnabled, gitEnabled, vncEnabled, rdpEnabled,
381
460
  // Announce the desktop host/port too (parity with the loop's meta) so
382
461
  // the office persists the real target instead of defaulting to :5900.
@@ -458,6 +537,14 @@ function mcpOperateCommand(program) {
458
537
  }
459
538
  return;
460
539
  }
540
+ // Live skill reload frame from the office (full effective skill set).
541
+ if (msgType === 'skill_update') {
542
+ const skills = msg['skills'];
543
+ if (Array.isArray(skills)) {
544
+ handleSkillUpdate(skills);
545
+ }
546
+ return;
547
+ }
461
548
  const notice = describePush(msg);
462
549
  if (notice) {
463
550
  // Buffer for wait_for_events (the reliable real-time path)…
@@ -586,7 +673,9 @@ function mcpOperateCommand(program) {
586
673
  let transcriptOffset = 0;
587
674
  const seenAsst = new Set(); // assistant-entry uuids already forwarded
588
675
  const forwardAssistant = (text) => {
589
- const msg = text.trim();
676
+ // Redact secrets from the assistant prose BEFORE it leaves the machine
677
+ // — this bubble is stored + rendered in the office chat.
678
+ const msg = (0, redactSecrets_1.redactSecrets)(text.trim());
590
679
  if (!msg || !socket) {
591
680
  return;
592
681
  }
@@ -721,7 +810,9 @@ function mcpOperateCommand(program) {
721
810
  continue;
722
811
  }
723
812
  ev._session_name = sessionNameForHooks; // so the office can label it
724
- socket.sendFrame({ type: 'hook_event', data: ev });
813
+ // Redact secrets from the whole hook object (tool_input.command,
814
+ // tool_response, file contents, …) before it leaves the machine.
815
+ socket.sendFrame({ type: 'hook_event', data: (0, redactSecrets_1.redactDeep)(ev) });
725
816
  const name = ev['hook_event_name'] || ev['hook'] || ev['event'] || '';
726
817
  if (WORKING_HOOKS.has(name)) {
727
818
  sawWork = true;
@@ -906,7 +997,8 @@ function mcpOperateCommand(program) {
906
997
  }
907
998
  await officeTool('start_task', { task_id: t.id });
908
999
  const result = await runWorker(`You are working as the office agent in this workspace. Complete this task using your own tools (edit/create files as needed), then briefly summarize what you did.\n\nTASK: ${t.title}`);
909
- await officeTool('complete_task', { task_id: t.id, result: (result || 'Done.').slice(0, 1500) });
1000
+ // The worker summary is stored + shown in the office task feed — redact secrets.
1001
+ await officeTool('complete_task', { task_id: t.id, result: (0, redactSecrets_1.redactSecrets)((result || 'Done.').slice(0, 1500)) });
910
1002
  logErr(`🤖 autonomy: completed task ${t.id} (${t.title.slice(0, 40)})`);
911
1003
  }
912
1004
  // A2A: reply to direct messages (check_messages). The worker composes a
@@ -920,7 +1012,8 @@ function mcpOperateCommand(program) {
920
1012
  }
921
1013
  const reply = (await runWorker(`A teammate "${m.from}" sent you this message in the office:\n\n"${m.body}"\n\nIf a reply is warranted, output ONLY the reply text — concise, and do NOT ask a question back unless truly essential. If the message needs work in this workspace, do it, then reply with what you did. If no reply is needed, output exactly: NO_REPLY`)).trim();
922
1014
  if (reply && !/^NO_REPLY/i.test(reply)) {
923
- await officeTool('send_message', { to: m.from, message: reply.slice(0, 1000) });
1015
+ // The A2A reply is delivered + shown in the office chat — redact secrets.
1016
+ await officeTool('send_message', { to: m.from, message: (0, redactSecrets_1.redactSecrets)(reply.slice(0, 1000)) });
924
1017
  logErr(`🤖 autonomy: replied to ${m.from}`);
925
1018
  replied++;
926
1019
  }
@@ -974,20 +1067,15 @@ function mcpOperateCommand(program) {
974
1067
  process.stdout.write('', () => process.exit(0));
975
1068
  }
976
1069
  };
977
- rl.on('line', async (line) => {
978
- const t = line.trim();
979
- if (!t) {
980
- return;
981
- }
982
- let req;
983
- try {
984
- req = JSON.parse(t);
985
- }
986
- catch {
987
- return;
988
- }
1070
+ // ── Single JSON-RPC request path (shared by stdio + HTTP) ────────────────
1071
+ // Process ONE inbound MCP message and return the response object to send
1072
+ // back, or null for a notification / message with no reply. Both the stdio
1073
+ // line reader and the HTTP handler call this, so wait_for_events blocking,
1074
+ // tools/list augmentation, and notification forwarding all live in ONE place
1075
+ // regardless of transport.
1076
+ const dispatch = async (req) => {
989
1077
  // Notifications (no id) are one-way — nothing to reply. Forward the
990
- // handshake ones opportunistically but never write a response for them.
1078
+ // handshake ones opportunistically but never return a response for them.
991
1079
  if (req.id === undefined || req.id === null) {
992
1080
  if (typeof req.method === 'string' && req.method.startsWith('notifications/')) {
993
1081
  // Fire-and-forget over the socket when ready, else HTTP.
@@ -998,7 +1086,7 @@ function mcpOperateCommand(program) {
998
1086
  proxy(endpoint, key, req).catch(() => { });
999
1087
  }
1000
1088
  }
1001
- return;
1089
+ return null;
1002
1090
  }
1003
1091
  // Local tool: wait_for_events — stream office activity from the socket
1004
1092
  // instead of proxying to the server. Blocks until an event (or timeout).
@@ -1014,13 +1102,151 @@ function mcpOperateCommand(program) {
1014
1102
  const text = events.length
1015
1103
  ? `Office activity (${events.length} event${events.length === 1 ? '' : 's'}):\n${events.join('\n')}\n\nUse get_events / check_messages for detail, then call wait_for_events again to keep listening.`
1016
1104
  : `No new events in ${secs}s. Call wait_for_events again to keep listening.`;
1017
- send({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text }] } });
1105
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text }] } };
1106
+ }
1107
+ finally {
1108
+ inflight--;
1109
+ maybeExit();
1110
+ }
1111
+ }
1112
+ // Local blocking tool: ask_user — create a user-decision request in the
1113
+ // office and BLOCK until the user answers in pixel-office chat (or a cap
1114
+ // elapses). Poll get_choice_answer in SHORT calls (never one long call:
1115
+ // the WS proxy caps at 20s, HTTP at 120s) and keep the agent's status
1116
+ // fresh so it doesn't flip to idle while blocked. inflight++/maybeExit
1117
+ // exactly like wait_for_events keeps the process alive while waiting.
1118
+ if (req.method === 'tools/call' && params?.name === 'ask_user') {
1119
+ inflight++;
1120
+ // Tunables (mirror wait_for_events' small consts).
1121
+ const ASK_INVOCATION_MS = 50_000; // cap ONE call under opencode's ~60s remote-MCP client timeout; the model re-calls with request_id to keep waiting
1122
+ const ASK_POLL_MS = 3_000; // short poll cadence (office is fast)
1123
+ const ASK_HEARTBEAT_MS = 60_000; // refresh 'waiting' status well under the 120s idle flip
1124
+ try {
1125
+ const a = (req.params?.arguments) ?? {};
1126
+ // Call an office tool over the same WS/HTTP channel and read its first
1127
+ // text-content block (same extraction as officeTool / wait_for_events).
1128
+ const askOfficeTool = async (name, args) => {
1129
+ const r = await callOffice({ id: `ask-${name}-${++wsReqSeq}`, method: 'tools/call', params: { name, arguments: args } });
1130
+ return (r['result']?.content?.[0]?.text) ?? '';
1131
+ };
1132
+ // Build create_choice_request arguments: pass through single OR wizard
1133
+ // form untouched (the office validates the shape).
1134
+ const createArgs = {};
1135
+ const isWizard = Array.isArray(a['steps']);
1136
+ if (isWizard) {
1137
+ createArgs.steps = a['steps'];
1138
+ if (typeof a['title'] === 'string') {
1139
+ createArgs.title = a['title'];
1140
+ }
1141
+ }
1142
+ else {
1143
+ if (a['question'] !== undefined) {
1144
+ createArgs.question = a['question'];
1145
+ }
1146
+ if (a['options'] !== undefined) {
1147
+ createArgs.options = a['options'];
1148
+ }
1149
+ if (a['allow_other'] !== undefined) {
1150
+ createArgs.allow_other = a['allow_other'];
1151
+ }
1152
+ if (a['multi_select'] !== undefined) {
1153
+ createArgs.multi_select = a['multi_select'];
1154
+ }
1155
+ }
1156
+ // Format the office's answer array into readable text for the model.
1157
+ const fmtOne = (ans) => {
1158
+ const other = ans?.otherText ? String(ans.otherText) : '';
1159
+ if (Array.isArray(ans?.values) && ans.values.length > 0) {
1160
+ const v = ans.values.map((x) => `"${String(x)}"`).join(', ');
1161
+ return other ? `${v} (other: "${other}")` : v;
1162
+ }
1163
+ const hasValue = ans?.value !== undefined && ans?.value !== null && String(ans.value) !== '';
1164
+ if (hasValue) {
1165
+ return other ? `"${String(ans.value)}" (other: "${other}")` : `"${String(ans.value)}"`;
1166
+ }
1167
+ // Free-text only (no preset option chosen): show the typed answer directly.
1168
+ return other ? `"${other}"` : '"(no answer)"';
1169
+ };
1170
+ const formatAnswers = (answers) => {
1171
+ if (!Array.isArray(answers) || answers.length === 0) {
1172
+ return 'The user answered, but no selection was returned.';
1173
+ }
1174
+ if (!isWizard && answers.length === 1) {
1175
+ return `The user chose: ${fmtOne(answers[0])}`;
1176
+ }
1177
+ const steps = a['steps'] ?? [];
1178
+ const lines = answers.map((ans) => {
1179
+ const idx = typeof ans?.stepIndex === 'number' ? ans.stepIndex : 0;
1180
+ const q = steps[idx]?.question ?? `step ${idx + 1}`;
1181
+ return `Step ${idx + 1} (${q}): ${fmtOne(ans)}`;
1182
+ });
1183
+ return `The user completed the wizard:\n${lines.join('\n')}`;
1184
+ };
1185
+ // 1) Create the request — UNLESS resuming an existing one (request_id
1186
+ // passed back after a prior "Still waiting" return). Resuming avoids
1187
+ // re-asking and lets a long wait span several short sub-60s calls.
1188
+ let requestId = typeof a['request_id'] === 'string' ? String(a['request_id']).trim() : '';
1189
+ if (!requestId) {
1190
+ try {
1191
+ const createText = await askOfficeTool('create_choice_request', createArgs);
1192
+ const parsed = JSON.parse(createText);
1193
+ requestId = (parsed?.request_id ?? '').toString();
1194
+ }
1195
+ catch (e) {
1196
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: 'Could not create the user-decision request: ' + (e?.message ?? String(e)) + '. Nothing was shown to the user.' }] } };
1197
+ }
1198
+ if (!requestId) {
1199
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: 'The office did not return a request id — the user-decision request was not created, so no question reached the user.' }] } };
1200
+ }
1201
+ }
1202
+ // 2) BLOCK: re-poll get_choice_answer in short calls; heartbeat status.
1203
+ const started = Date.now();
1204
+ let lastBeat = Date.now();
1205
+ while (!closed) {
1206
+ // Return BEFORE the client's request timeout (opencode's remote MCP
1207
+ // caps a single call at ~60s). The request stays open server-side;
1208
+ // the model re-calls ask_user with this request_id to keep waiting.
1209
+ if (Date.now() - started > ASK_INVOCATION_MS) {
1210
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: `Still waiting for the user to answer — the question is live in the office chat. To keep waiting, call ask_user AGAIN with { "request_id": "${requestId}" } (do NOT create a new question).` }] } };
1211
+ }
1212
+ let statusText = '';
1213
+ try {
1214
+ statusText = await askOfficeTool('get_choice_answer', { request_id: requestId });
1215
+ }
1216
+ catch {
1217
+ statusText = '';
1218
+ }
1219
+ let status = 'pending';
1220
+ let answers = null;
1221
+ if (statusText) {
1222
+ try {
1223
+ const p = JSON.parse(statusText);
1224
+ status = p.status ?? 'pending';
1225
+ answers = p.answers ?? null;
1226
+ }
1227
+ catch { /* keep pending */ }
1228
+ }
1229
+ if (status === 'answered') {
1230
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: formatAnswers(answers) }] } };
1231
+ }
1232
+ if (status === 'cancelled') {
1233
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: 'The user dismissed the request without answering.' }] } };
1234
+ }
1235
+ // Heartbeat: keep the agent marked 'waiting' (best-effort) so it does
1236
+ // not flip to idle after ~120s while genuinely blocked on the user.
1237
+ if (Date.now() - lastBeat > ASK_HEARTBEAT_MS) {
1238
+ lastBeat = Date.now();
1239
+ askOfficeTool('set_status', { status: 'waiting' }).catch(() => { });
1240
+ }
1241
+ await new Promise((res) => { const t = setTimeout(res, ASK_POLL_MS); t.unref?.(); });
1242
+ }
1243
+ // Tool torn down (stdin closed / shutdown) before the user answered.
1244
+ return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: 'The ask_user request was interrupted before the user answered.' }] } };
1018
1245
  }
1019
1246
  finally {
1020
1247
  inflight--;
1021
1248
  maybeExit();
1022
1249
  }
1023
- return;
1024
1250
  }
1025
1251
  inflight++;
1026
1252
  try {
@@ -1029,20 +1255,134 @@ function mcpOperateCommand(program) {
1029
1255
  if (req.method === 'tools/list') {
1030
1256
  const result = res['result'];
1031
1257
  if (result && Array.isArray(result.tools)) {
1032
- result.tools.push(WAIT_TOOL);
1258
+ result.tools.push(WAIT_TOOL, ASK_USER_TOOL);
1033
1259
  }
1034
1260
  }
1035
- send(res);
1261
+ return res;
1036
1262
  }
1037
1263
  catch (e) {
1038
- send({ jsonrpc: '2.0', id: req.id, error: { code: -32603, message: 'proxy error: ' + (e?.message ?? String(e)) } });
1264
+ return { jsonrpc: '2.0', id: req.id, error: { code: -32603, message: 'proxy error: ' + (e?.message ?? String(e)) } };
1039
1265
  }
1040
1266
  finally {
1041
1267
  inflight--;
1042
1268
  maybeExit();
1043
1269
  }
1044
- });
1045
- rl.on('close', () => { closed = true; maybeExit(); });
1270
+ };
1271
+ // ── Transport: persistent HTTP (Streamable HTTP) OR stdio ────────────────
1272
+ // opencode's `type: remote` MCP client (MCP.connectRemote) tries Streamable
1273
+ // HTTP first — POST JSON-RPC to the configured `url`, Accept
1274
+ // `application/json, text/event-stream` — then falls back to legacy SSE. It
1275
+ // also opens a standalone GET SSE stream after `initialized` but TOLERATES a
1276
+ // 405 there. So the persistent sidecar needs only: POST → single JSON-RPC
1277
+ // reply as `application/json` (202 for notification-only bodies), GET → 405
1278
+ // (no server-initiated stream; office activity reaches the model via the
1279
+ // wait_for_events tool, exactly as over stdio). This lets ONE always-on
1280
+ // bridge serve both `opencode serve` and every `opencode run` with no
1281
+ // stdio-spawn dependency and no flap.
1282
+ if (opts.httpPort) {
1283
+ const httpHost = opts.httpHost || '127.0.0.1';
1284
+ const sessionId = crypto.randomUUID();
1285
+ const httpServer = http.createServer((hreq, hres) => {
1286
+ // No standalone server→client SSE stream: the client tolerates 405 on
1287
+ // the GET it opens after `initialized` (if(status===405)return).
1288
+ if (hreq.method === 'GET') {
1289
+ hres.writeHead(405, { 'Content-Type': 'text/plain' });
1290
+ hres.end('Method Not Allowed');
1291
+ return;
1292
+ }
1293
+ // Session teardown — acknowledge and move on (stateless bridge).
1294
+ if (hreq.method === 'DELETE') {
1295
+ hres.writeHead(204);
1296
+ hres.end();
1297
+ return;
1298
+ }
1299
+ if (hreq.method !== 'POST') {
1300
+ hres.writeHead(405, { 'Content-Type': 'text/plain' });
1301
+ hres.end('Method Not Allowed');
1302
+ return;
1303
+ }
1304
+ let body = '';
1305
+ hreq.on('data', (c) => { body += c; if (body.length > 8 * 1024 * 1024) {
1306
+ hreq.destroy();
1307
+ } });
1308
+ hreq.on('end', async () => {
1309
+ let parsed;
1310
+ try {
1311
+ parsed = JSON.parse(body);
1312
+ }
1313
+ catch {
1314
+ hres.writeHead(400, { 'Content-Type': 'application/json' });
1315
+ hres.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
1316
+ return;
1317
+ }
1318
+ const batch = Array.isArray(parsed);
1319
+ const msgs = (batch ? parsed : [parsed]);
1320
+ const isInit = msgs.some((m) => m && m.method === 'initialize');
1321
+ const responses = [];
1322
+ for (const m of msgs) {
1323
+ try {
1324
+ const r = await dispatch(m);
1325
+ if (r) {
1326
+ responses.push(r);
1327
+ }
1328
+ }
1329
+ catch (e) {
1330
+ if (m && m.id !== undefined && m.id !== null) {
1331
+ responses.push({ jsonrpc: '2.0', id: m.id, error: { code: -32603, message: 'bridge error: ' + (e?.message ?? String(e)) } });
1332
+ }
1333
+ }
1334
+ }
1335
+ // Notification/response-only body → 202 Accepted, no content.
1336
+ if (responses.length === 0) {
1337
+ hres.writeHead(202, isInit ? { 'Mcp-Session-Id': sessionId } : {});
1338
+ hres.end();
1339
+ return;
1340
+ }
1341
+ const headers = { 'Content-Type': 'application/json' };
1342
+ if (isInit) {
1343
+ headers['Mcp-Session-Id'] = sessionId;
1344
+ }
1345
+ hres.writeHead(200, headers);
1346
+ hres.end(JSON.stringify(batch ? responses : responses[0]));
1347
+ });
1348
+ hreq.on('error', () => { try {
1349
+ hres.writeHead(400);
1350
+ hres.end();
1351
+ }
1352
+ catch { /* ignore */ } });
1353
+ });
1354
+ httpServer.on('error', (e) => {
1355
+ process.stderr.write(`autodev mcp-operate: HTTP server error — ${e instanceof Error ? e.message : String(e)}\n`);
1356
+ process.exit(1);
1357
+ });
1358
+ httpServer.listen(opts.httpPort, httpHost, () => {
1359
+ process.stderr.write(`autodev mcp-operate: persistent Streamable-HTTP MCP listening on http://${httpHost}:${opts.httpPort}/mcp (point an opencode \`type: remote\` MCP at this URL).\n`);
1360
+ });
1361
+ // No stdin client in HTTP mode — do NOT read/close on stdin (a detached
1362
+ // sidecar has stdin closed, which would otherwise trip maybeExit). The
1363
+ // HTTP server keeps the process alive.
1364
+ rl.close();
1365
+ }
1366
+ else {
1367
+ rl.on('line', async (line) => {
1368
+ const t = line.trim();
1369
+ if (!t) {
1370
+ return;
1371
+ }
1372
+ let req;
1373
+ try {
1374
+ req = JSON.parse(t);
1375
+ }
1376
+ catch {
1377
+ return;
1378
+ }
1379
+ const resp = await dispatch(req);
1380
+ if (resp) {
1381
+ send(resp);
1382
+ }
1383
+ });
1384
+ rl.on('close', () => { closed = true; maybeExit(); });
1385
+ }
1046
1386
  });
1047
1387
  }
1048
1388
  //# sourceMappingURL=mcpOperate.js.map