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.
- package/out/agentBackup/layout.js +1 -0
- package/out/agentBackup/layout.js.map +1 -1
- package/out/commands/mcpOperate.js +368 -28
- package/out/commands/mcpOperate.js.map +1 -1
- package/out/configManager.d.ts +7 -0
- package/out/configManager.js +36 -0
- package/out/configManager.js.map +1 -1
- package/out/core/modelInfo.d.ts +28 -0
- package/out/core/modelInfo.js +97 -0
- package/out/core/modelInfo.js.map +1 -0
- package/out/core/projectSkills.d.ts +44 -0
- package/out/core/projectSkills.js +266 -0
- package/out/core/projectSkills.js.map +1 -0
- package/out/core/redactSecrets.d.ts +15 -0
- package/out/core/redactSecrets.js +110 -0
- package/out/core/redactSecrets.js.map +1 -0
- package/out/core/settingsLoader.d.ts +9 -0
- package/out/core/settingsLoader.js +1 -0
- package/out/core/settingsLoader.js.map +1 -1
- package/out/taskLoop.d.ts +8 -0
- package/out/taskLoop.js +55 -2
- package/out/taskLoop.js.map +1 -1
- package/out/webSocketPoller.d.ts +3 -0
- package/out/webSocketPoller.js +11 -0
- package/out/webSocketPoller.js.map +1 -1
- package/out/webhookPoller.d.ts +1 -0
- package/out/webhookPoller.js +5 -0
- package/out/webhookPoller.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/agentBackup/layout.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,oCAEC;
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/agentBackup/layout.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,oCAEC;AAmCD,sCAEC;AASD,gCAEC;AAGD,wDAEC;AAWD,kDAaC;AA9FD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAEzB,uEAAuE;AAC1D,QAAA,UAAU,GAAG,cAAc,CAAC;AAEzC,+DAA+D;AAClD,QAAA,aAAa,GAAG;IAC3B,SAAS,EAAE,GAAG,kBAAU,YAAY;IACpC,YAAY,EAAE,GAAG,kBAAU,WAAW;IACtC,QAAQ,EAAE,GAAG,kBAAU,4BAA4B;CAC3C,CAAC;AAEX,0DAA0D;AAC1D,SAAgB,YAAY,CAAC,UAAkB;IAC7C,OAAO,GAAG,qBAAa,CAAC,YAAY,IAAI,UAAU,EAAE,CAAC;AACvD,CAAC;AAED;;;GAGG;AACU,QAAA,SAAS,GAAsB;IAC1C,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS;IAC5E,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe;CACjE,CAAC;AAEF;;;;;GAKG;AACU,QAAA,cAAc,GAAsB;IAC/C,UAAU;IACV,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC;IACnC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;IAClC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC;CACrC,CAAC;AAEF;;;;;;;GAOG;AACU,QAAA,aAAa,GAAG,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,CAAU,CAAC;AAE3G,iFAAiF;AACjF,SAAgB,aAAa,CAAC,CAAS;IACrC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AACjE,CAAC;AAED,8EAA8E;AAC9E,0DAA0D;AAC1D,wEAAwE;AACxE,8EAA8E;AAC9E,8EAA8E;AAE9E,qCAAqC;AACrC,SAAgB,UAAU;IACxB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED,8EAA8E;AAC9E,SAAgB,sBAAsB;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,eAAe,CAAC,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,uDAAuD;AACvD,yEAAyE;AACzE,iFAAiF;AACjF,6EAA6E;AAC7E,oEAAoE;AACpE,8EAA8E;AAE9E,4EAA4E;AAC5E,SAAgB,mBAAmB;IACjC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACzC,IAAI,GAAG,EAAE,CAAC;QAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC;IAAC,CAAC;IACzD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACxE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,OAAO,EAAE,CAAC;QAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IAAC,CAAC;IACjE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACjD,IAAI,YAAY,EAAE,CAAC;QAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;IAAC,CAAC;IAC3E,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QACzB,IAAI,CAAC;YAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,KAAK,CAAC;QAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -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
|
|
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) => {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1261
|
+
return res;
|
|
1036
1262
|
}
|
|
1037
1263
|
catch (e) {
|
|
1038
|
-
|
|
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
|
-
|
|
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
|