@ai-setting/roy-task-plugin 0.4.2 → 0.6.0
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/dist/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/dist/claude-plugin/.mcp.json +6 -0
- package/dist/claude-plugin/agents/roy.md +115 -0
- package/dist/claude-plugin/commands/task-complete.md +25 -0
- package/dist/claude-plugin/commands/task-create.md +19 -0
- package/dist/claude-plugin/commands/task-list.md +17 -0
- package/dist/claude-plugin/commands/task-milestone.md +39 -0
- package/dist/claude-plugin/commands/task-tree.md +20 -0
- package/dist/claude-plugin/commands/task-update.md +26 -0
- package/dist/claude-plugin/hooks/hooks.json +29 -0
- package/dist/claude-plugin/hooks/session-start.mjs +49 -0
- package/dist/claude-plugin/hooks/stop-check.mjs +222 -0
- package/dist/claude-plugin/mcp/bridge.mjs +482 -0
- package/dist/claude-plugin/mcp/roy-task-mcp-server.mjs +281 -0
- package/dist/claude-plugin/mcp/tools.mjs +240 -0
- package/dist/cli.js +922 -335
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -3,28 +3,666 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// ../../../packages/cli/src/daemon/runners/types.ts
|
|
6
|
-
var RUNTIMES = Object.freeze(["opencode", "claude"]);
|
|
6
|
+
var RUNTIMES = Object.freeze(["opencode", "claude", "codex"]);
|
|
7
7
|
var DEFAULT_RUNTIME = "opencode";
|
|
8
8
|
var DEFAULT_RUNTIME_BIN = Object.freeze({
|
|
9
9
|
opencode: "opencode",
|
|
10
|
-
claude: "claude"
|
|
10
|
+
claude: "claude",
|
|
11
|
+
codex: "codex"
|
|
11
12
|
});
|
|
12
13
|
function isRuntimeName(value) {
|
|
13
14
|
return RUNTIMES.includes(value);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// ../../../packages/cli/src/runtime/spawn.ts
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
function runCapture(cmd, opts = {}) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let child;
|
|
22
|
+
try {
|
|
23
|
+
child = spawn(cmd[0], cmd.slice(1), {
|
|
24
|
+
stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
|
|
25
|
+
env: opts.env ? normalizeEnv(opts.env) : process.env,
|
|
26
|
+
cwd: opts.cwd,
|
|
27
|
+
windowsHide: true
|
|
28
|
+
});
|
|
29
|
+
} catch (err) {
|
|
30
|
+
reject(err);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
let timedOut = false;
|
|
36
|
+
child.stdout?.setEncoding("utf8");
|
|
37
|
+
child.stderr?.setEncoding("utf8");
|
|
38
|
+
child.stdout?.on("data", (chunk) => {
|
|
39
|
+
stdout += chunk;
|
|
40
|
+
});
|
|
41
|
+
child.stderr?.on("data", (chunk) => {
|
|
42
|
+
stderr += chunk;
|
|
43
|
+
});
|
|
44
|
+
const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
|
|
45
|
+
timedOut = true;
|
|
46
|
+
try {
|
|
47
|
+
child.kill("SIGTERM");
|
|
48
|
+
} catch {}
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
try {
|
|
51
|
+
child.kill("SIGKILL");
|
|
52
|
+
} catch {}
|
|
53
|
+
}, 2000).unref?.();
|
|
54
|
+
}, opts.timeoutMs) : undefined;
|
|
55
|
+
timer?.unref?.();
|
|
56
|
+
child.on("error", (err) => {
|
|
57
|
+
if (timer)
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
reject(err);
|
|
60
|
+
});
|
|
61
|
+
child.on("close", (code, signal) => {
|
|
62
|
+
if (timer)
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
if (timedOut) {
|
|
65
|
+
resolve({ code: 124, stdout, stderr });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve({ code: code ?? (signal ? 1 : 0), stdout, stderr });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function spawnStreaming(cmd, opts = {}) {
|
|
73
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
74
|
+
stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
|
|
75
|
+
env: opts.env ? normalizeEnv(opts.env) : process.env,
|
|
76
|
+
cwd: opts.cwd,
|
|
77
|
+
windowsHide: true
|
|
78
|
+
});
|
|
79
|
+
let timedOut = false;
|
|
80
|
+
const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
|
|
81
|
+
timedOut = true;
|
|
82
|
+
try {
|
|
83
|
+
child.kill("SIGTERM");
|
|
84
|
+
} catch {}
|
|
85
|
+
}, opts.timeoutMs) : undefined;
|
|
86
|
+
timer?.unref?.();
|
|
87
|
+
if (opts.onStdout || opts.onStderr) {
|
|
88
|
+
child.stdout?.setEncoding("utf8");
|
|
89
|
+
child.stderr?.setEncoding("utf8");
|
|
90
|
+
child.stdout?.on("data", (chunk) => opts.onStdout?.(chunk));
|
|
91
|
+
child.stderr?.on("data", (chunk) => opts.onStderr?.(chunk));
|
|
92
|
+
} else {
|
|
93
|
+
child.stdout?.resume();
|
|
94
|
+
child.stderr?.resume();
|
|
95
|
+
}
|
|
96
|
+
const exited = new Promise((resolve, reject) => {
|
|
97
|
+
child.on("error", (err) => {
|
|
98
|
+
if (timer)
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
child.on("close", (code, signal) => {
|
|
103
|
+
if (timer)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (timedOut) {
|
|
106
|
+
resolve(124);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
exited,
|
|
114
|
+
pid: child.pid,
|
|
115
|
+
kill(signal = "SIGTERM") {
|
|
116
|
+
try {
|
|
117
|
+
child.kill(signal);
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function assertEofStdin(value) {
|
|
123
|
+
if (value !== undefined && value !== "ignore") {
|
|
124
|
+
throw new Error(`refusing to spawn with stdin=${String(value)}: fd 0 must be an EOF source ` + '(use "ignore"); an inherited socket hangs `opencode run` (see #4376)');
|
|
125
|
+
}
|
|
126
|
+
return "ignore";
|
|
127
|
+
}
|
|
128
|
+
function normalizeEnv(env) {
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const [k, v] of Object.entries(env)) {
|
|
131
|
+
if (typeof v === "string")
|
|
132
|
+
out[k] = v;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function isMissingBinary(err) {
|
|
137
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ../../../packages/cli/src/daemon/task-created.ts
|
|
141
|
+
var CREATED_LINE = /Task created(?:\s+successfully)?\s*:\s*#(\d+)/;
|
|
142
|
+
function extractCreatedTask(text, titleHint) {
|
|
143
|
+
if (!text)
|
|
144
|
+
return;
|
|
145
|
+
return fromJson(text) ?? fromEmbeddedTaskObject(text) ?? fromTaskObjectBlob(text) ?? fromSuccessLine(text, titleHint);
|
|
146
|
+
}
|
|
147
|
+
function uniqueTaskId(text) {
|
|
148
|
+
let id;
|
|
149
|
+
for (const m of text.matchAll(/"taskId"\s*:\s*(\d+)/g)) {
|
|
150
|
+
const n = Number(m[1]);
|
|
151
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
152
|
+
return;
|
|
153
|
+
if (id === undefined)
|
|
154
|
+
id = n;
|
|
155
|
+
else if (id !== n)
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
return id;
|
|
159
|
+
}
|
|
160
|
+
function fromJson(text) {
|
|
161
|
+
const trimmed = text.trim();
|
|
162
|
+
if (!trimmed.startsWith("{"))
|
|
163
|
+
return;
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(trimmed);
|
|
167
|
+
} catch {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
171
|
+
return;
|
|
172
|
+
const obj = parsed;
|
|
173
|
+
const id = obj["id"];
|
|
174
|
+
const title = obj["title"];
|
|
175
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id <= 0)
|
|
176
|
+
return;
|
|
177
|
+
if (typeof title !== "string" || title.trim() === "")
|
|
178
|
+
return;
|
|
179
|
+
return { id, title: title.trim() };
|
|
180
|
+
}
|
|
181
|
+
function fromTaskObjectBlob(text) {
|
|
182
|
+
if (!BLOB_GATE.every((marker) => text.includes(marker)))
|
|
183
|
+
return;
|
|
184
|
+
const id = uniqueTaskId(text);
|
|
185
|
+
if (id === undefined)
|
|
186
|
+
return;
|
|
187
|
+
const title = titleFromCreatedActionTitle(text);
|
|
188
|
+
if (title === undefined)
|
|
189
|
+
return;
|
|
190
|
+
return { id, title };
|
|
191
|
+
}
|
|
192
|
+
function titleFromCreatedActionTitle(text) {
|
|
193
|
+
const m = /"actionTitle"\s*:\s*"Task created:\s*([^"]*)"/.exec(text);
|
|
194
|
+
const title = m?.[1]?.trim();
|
|
195
|
+
if (!title || title.length > 200)
|
|
196
|
+
return;
|
|
197
|
+
return title;
|
|
198
|
+
}
|
|
199
|
+
var BLOB_GATE = ['"taskId"', '"actionTitle"', "Task created:"];
|
|
200
|
+
function fromEmbeddedTaskObject(text) {
|
|
201
|
+
let found;
|
|
202
|
+
for (const obj of topLevelObjects(text)) {
|
|
203
|
+
const task = taskFromObject(obj);
|
|
204
|
+
if (!task)
|
|
205
|
+
continue;
|
|
206
|
+
if (found === undefined)
|
|
207
|
+
found = task;
|
|
208
|
+
else if (found.id !== task.id)
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
function taskFromObject(obj) {
|
|
214
|
+
const id = obj["id"];
|
|
215
|
+
const title = obj["title"];
|
|
216
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id <= 0)
|
|
217
|
+
return;
|
|
218
|
+
if (typeof title !== "string" || title.trim() === "")
|
|
219
|
+
return;
|
|
220
|
+
return { id, title: title.trim() };
|
|
221
|
+
}
|
|
222
|
+
function topLevelObjects(text) {
|
|
223
|
+
const out = [];
|
|
224
|
+
let depth = 0;
|
|
225
|
+
let inString = false;
|
|
226
|
+
let escaped = false;
|
|
227
|
+
let start = -1;
|
|
228
|
+
for (let i = 0;i < text.length; i++) {
|
|
229
|
+
const ch = text[i];
|
|
230
|
+
if (inString) {
|
|
231
|
+
if (escaped)
|
|
232
|
+
escaped = false;
|
|
233
|
+
else if (ch === "\\")
|
|
234
|
+
escaped = true;
|
|
235
|
+
else if (ch === '"')
|
|
236
|
+
inString = false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (ch === '"') {
|
|
240
|
+
inString = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (ch === "{") {
|
|
244
|
+
if (depth === 0 && !insideArray(text, i))
|
|
245
|
+
start = i;
|
|
246
|
+
depth++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ch === "]" && depth === 0)
|
|
250
|
+
continue;
|
|
251
|
+
if (ch !== "}")
|
|
252
|
+
continue;
|
|
253
|
+
depth--;
|
|
254
|
+
if (depth !== 0 || start < 0)
|
|
255
|
+
continue;
|
|
256
|
+
const slice = text.slice(start, i + 1);
|
|
257
|
+
start = -1;
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(slice);
|
|
260
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
261
|
+
out.push(parsed);
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
function insideArray(text, at) {
|
|
268
|
+
let brackets = 0;
|
|
269
|
+
let braces = 0;
|
|
270
|
+
let inString = false;
|
|
271
|
+
let escaped = false;
|
|
272
|
+
for (let i = 0;i < at; i++) {
|
|
273
|
+
const ch = text[i];
|
|
274
|
+
if (inString) {
|
|
275
|
+
if (escaped)
|
|
276
|
+
escaped = false;
|
|
277
|
+
else if (ch === "\\")
|
|
278
|
+
escaped = true;
|
|
279
|
+
else if (ch === '"')
|
|
280
|
+
inString = false;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (ch === '"')
|
|
284
|
+
inString = true;
|
|
285
|
+
else if (ch === "{")
|
|
286
|
+
braces++;
|
|
287
|
+
else if (ch === "}")
|
|
288
|
+
braces--;
|
|
289
|
+
else if (ch === "[")
|
|
290
|
+
brackets++;
|
|
291
|
+
else if (ch === "]")
|
|
292
|
+
brackets--;
|
|
293
|
+
}
|
|
294
|
+
return brackets > 0 && braces <= 0;
|
|
295
|
+
}
|
|
296
|
+
function fromSuccessLine(text, titleHint) {
|
|
297
|
+
const m = CREATED_LINE.exec(text);
|
|
298
|
+
if (!m)
|
|
299
|
+
return;
|
|
300
|
+
const id = Number(m[1]);
|
|
301
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
302
|
+
return;
|
|
303
|
+
const title = titleFromCliSummary(text) || titleHint?.trim();
|
|
304
|
+
return title ? { id, title } : { id };
|
|
305
|
+
}
|
|
306
|
+
function titleFromCliSummary(text) {
|
|
307
|
+
const at = CREATED_LINE.exec(text);
|
|
308
|
+
if (!at)
|
|
309
|
+
return;
|
|
310
|
+
const rest = text.slice(at.index + at[0].length);
|
|
311
|
+
const m = /^\s*Title:\s*(.+)$/m.exec(rest);
|
|
312
|
+
const title = m?.[1]?.trim();
|
|
313
|
+
if (!title || title.length > 200)
|
|
314
|
+
return;
|
|
315
|
+
return title;
|
|
316
|
+
}
|
|
317
|
+
function isTaskCreateTool(name) {
|
|
318
|
+
return /(^|__)task_create$/.test(name);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ../../../packages/cli/src/daemon/ndjson.ts
|
|
322
|
+
class LineSplitter {
|
|
323
|
+
tail = "";
|
|
324
|
+
pushChunk(chunk) {
|
|
325
|
+
this.tail += chunk;
|
|
326
|
+
const out = [];
|
|
327
|
+
let idx = this.tail.indexOf(`
|
|
328
|
+
`);
|
|
329
|
+
while (idx !== -1) {
|
|
330
|
+
out.push(this.tail.slice(0, idx));
|
|
331
|
+
this.tail = this.tail.slice(idx + 1);
|
|
332
|
+
idx = this.tail.indexOf(`
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
finish() {
|
|
338
|
+
if (!this.tail)
|
|
339
|
+
return [];
|
|
340
|
+
const line = this.tail;
|
|
341
|
+
this.tail = "";
|
|
342
|
+
return [line];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
class NdjsonAssembler {
|
|
347
|
+
tail = "";
|
|
348
|
+
texts = [];
|
|
349
|
+
error;
|
|
350
|
+
toolCalls = 0;
|
|
351
|
+
sessionId;
|
|
352
|
+
tools = [];
|
|
353
|
+
createdTask;
|
|
354
|
+
pushChunk(chunk) {
|
|
355
|
+
this.tail += chunk;
|
|
356
|
+
let idx = this.tail.indexOf(`
|
|
357
|
+
`);
|
|
358
|
+
while (idx !== -1) {
|
|
359
|
+
const line = this.tail.slice(0, idx);
|
|
360
|
+
this.tail = this.tail.slice(idx + 1);
|
|
361
|
+
this.parseLine(line);
|
|
362
|
+
idx = this.tail.indexOf(`
|
|
363
|
+
`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
pushLine(line) {
|
|
367
|
+
this.parseLine(line);
|
|
368
|
+
}
|
|
369
|
+
finishIncomplete() {
|
|
370
|
+
return this.tail.trim();
|
|
371
|
+
}
|
|
372
|
+
parseLine(line) {
|
|
373
|
+
const trimmed = line.trim();
|
|
374
|
+
if (!trimmed || trimmed[0] !== "{")
|
|
375
|
+
return;
|
|
376
|
+
let parsed;
|
|
377
|
+
try {
|
|
378
|
+
parsed = JSON.parse(trimmed);
|
|
379
|
+
} catch {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (parsed.sessionID)
|
|
383
|
+
this.sessionId = parsed.sessionID;
|
|
384
|
+
if (parsed.type === "text" && typeof parsed.part?.text === "string") {
|
|
385
|
+
this.texts.push(parsed.part.text);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (parsed.type === "tool_use") {
|
|
389
|
+
this.toolCalls += 1;
|
|
390
|
+
const name = parsed.part?.tool ?? parsed.tool;
|
|
391
|
+
if (typeof name === "string" && name) {
|
|
392
|
+
this.tools.push(name);
|
|
393
|
+
}
|
|
394
|
+
if (this.createdTask === undefined && typeof name === "string") {
|
|
395
|
+
const part = parsed.part;
|
|
396
|
+
const output = part?.state?.output;
|
|
397
|
+
const title = part?.state?.input?.["title"];
|
|
398
|
+
if (isTaskCreateTool(name) && typeof output === "string") {
|
|
399
|
+
this.createdTask = extractCreatedTask(output, typeof title === "string" ? title : undefined);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (parsed.type === "error") {
|
|
405
|
+
if (this.error === undefined) {
|
|
406
|
+
this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
finish() {
|
|
411
|
+
if (this.tail.trim()) {
|
|
412
|
+
this.parseLine(this.tail);
|
|
413
|
+
this.tail = "";
|
|
414
|
+
}
|
|
415
|
+
const text = this.texts.join("").trim();
|
|
416
|
+
return {
|
|
417
|
+
text,
|
|
418
|
+
error: this.error,
|
|
419
|
+
toolCalls: this.toolCalls,
|
|
420
|
+
sessionId: this.sessionId,
|
|
421
|
+
tools: this.tools,
|
|
422
|
+
createdTask: this.createdTask
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ../../../packages/cli/src/daemon/runners/codex-stream.ts
|
|
428
|
+
var CODEX_ITEM_TYPES = Object.freeze({
|
|
429
|
+
AGENT_MESSAGE: "agent_message",
|
|
430
|
+
COMMAND_EXECUTION: "command_execution",
|
|
431
|
+
ERROR: "error"
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
class CodexStreamDecoder {
|
|
435
|
+
lines = new LineSplitter;
|
|
436
|
+
state = { tools: [], errors: [], turnCompleted: false };
|
|
437
|
+
pushChunk(chunk) {
|
|
438
|
+
for (const line of this.lines.pushChunk(chunk))
|
|
439
|
+
this.consume(line);
|
|
440
|
+
}
|
|
441
|
+
finish() {
|
|
442
|
+
for (const line of this.lines.finish())
|
|
443
|
+
this.consume(line);
|
|
444
|
+
return this.toResult();
|
|
445
|
+
}
|
|
446
|
+
snapshot() {
|
|
447
|
+
return { ...this.state, tools: [...this.state.tools], errors: [...this.state.errors] };
|
|
448
|
+
}
|
|
449
|
+
consume(rawLine) {
|
|
450
|
+
const line = rawLine.trim();
|
|
451
|
+
if (!line.startsWith("{"))
|
|
452
|
+
return;
|
|
453
|
+
let event;
|
|
454
|
+
try {
|
|
455
|
+
event = JSON.parse(line);
|
|
456
|
+
} catch {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (typeof event !== "object" || event === null)
|
|
460
|
+
return;
|
|
461
|
+
const ev = event;
|
|
462
|
+
if (ev.type === "thread.started") {
|
|
463
|
+
const id = ev.thread_id;
|
|
464
|
+
if (typeof id === "string" && id)
|
|
465
|
+
this.state.sessionId = id;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (ev.type === "turn.completed") {
|
|
469
|
+
this.state.turnCompleted = true;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (ev.type === "item.started" || ev.type === "item.completed") {
|
|
473
|
+
this.consumeItem(ev.item, ev.type === "item.completed");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
consumeItem(raw, completed) {
|
|
477
|
+
if (typeof raw !== "object" || raw === null)
|
|
478
|
+
return;
|
|
479
|
+
const item = raw;
|
|
480
|
+
if (item.type === CODEX_ITEM_TYPES.AGENT_MESSAGE) {
|
|
481
|
+
if (typeof item.text === "string" && item.text.length > 0) {
|
|
482
|
+
this.state.text = item.text;
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (item.type === CODEX_ITEM_TYPES.ERROR) {
|
|
487
|
+
if (typeof item.message === "string" && item.message.length > 0) {
|
|
488
|
+
this.state.errors.push(item.message);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
|
|
493
|
+
const id = item.id;
|
|
494
|
+
if (typeof id === "string" && id && this.state.tools.includes(id)) {} else {
|
|
495
|
+
this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
|
|
496
|
+
}
|
|
497
|
+
if (completed && this.state.createdTask === undefined) {
|
|
498
|
+
this.state.createdTask = extractCreatedTask(item.aggregated_output);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
toResult() {
|
|
503
|
+
const result = {
|
|
504
|
+
text: this.state.text ?? ""
|
|
505
|
+
};
|
|
506
|
+
if (this.state.sessionId)
|
|
507
|
+
result.sessionId = this.state.sessionId;
|
|
508
|
+
if (this.state.tools.length > 0)
|
|
509
|
+
result.toolCalls = this.state.tools.length;
|
|
510
|
+
if (this.state.createdTask)
|
|
511
|
+
result.createdTask = this.state.createdTask;
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ../../../packages/cli/src/daemon/runners/codex-sessions.ts
|
|
517
|
+
function createCodexSessionRegistry() {
|
|
518
|
+
const sessions = new Map;
|
|
519
|
+
return {
|
|
520
|
+
get: (key) => sessions.get(key),
|
|
521
|
+
set: (key, id) => {
|
|
522
|
+
sessions.set(key, id);
|
|
523
|
+
},
|
|
524
|
+
delete: (key) => {
|
|
525
|
+
sessions.delete(key);
|
|
526
|
+
},
|
|
527
|
+
size: () => sessions.size
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ../../../packages/cli/src/daemon/runners/codex.ts
|
|
532
|
+
var DEFAULT_CODEX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
533
|
+
var CODEX_STDIO = Object.freeze([
|
|
534
|
+
"ignore",
|
|
535
|
+
"pipe",
|
|
536
|
+
"pipe"
|
|
537
|
+
]);
|
|
538
|
+
var CODEX_SANDBOX_MODES = Object.freeze([
|
|
539
|
+
"read-only",
|
|
540
|
+
"workspace-write",
|
|
541
|
+
"danger-full-access"
|
|
542
|
+
]);
|
|
543
|
+
function isCodexSandboxMode(value) {
|
|
544
|
+
return CODEX_SANDBOX_MODES.includes(value);
|
|
545
|
+
}
|
|
546
|
+
function buildCodexArgs(input) {
|
|
547
|
+
const args = ["exec", "--json"];
|
|
548
|
+
if (input.model)
|
|
549
|
+
args.push("--model", input.model);
|
|
550
|
+
args.push("--skip-git-repo-check");
|
|
551
|
+
args.push("-s", input.sandbox ?? "read-only");
|
|
552
|
+
args.push(input.prompt);
|
|
553
|
+
return args;
|
|
554
|
+
}
|
|
555
|
+
function buildCodexResumeArgs(sessionId, input) {
|
|
556
|
+
const args = ["exec", "resume", "--json"];
|
|
557
|
+
if (input.model)
|
|
558
|
+
args.push("--model", input.model);
|
|
559
|
+
args.push("--skip-git-repo-check");
|
|
560
|
+
if (input.sandbox === "danger-full-access") {
|
|
561
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
562
|
+
}
|
|
563
|
+
args.push(sessionId);
|
|
564
|
+
args.push(input.prompt);
|
|
565
|
+
return args;
|
|
566
|
+
}
|
|
567
|
+
function makeCodexRunner(opts = {}) {
|
|
568
|
+
const bin = opts.bin ?? "codex";
|
|
569
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_CODEX_TIMEOUT_MS;
|
|
570
|
+
const model = opts.model;
|
|
571
|
+
const sandbox = opts.sandbox ?? "read-only";
|
|
572
|
+
const sessions = opts.sessions ?? createCodexSessionRegistry();
|
|
573
|
+
return async function runAgent(input) {
|
|
574
|
+
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
575
|
+
const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox }) : buildCodexArgs({ prompt: input.prompt, model, sandbox });
|
|
576
|
+
const decoder = new CodexStreamDecoder;
|
|
577
|
+
let stderr = "";
|
|
578
|
+
let handle;
|
|
579
|
+
try {
|
|
580
|
+
handle = spawnStreaming([bin, ...args], {
|
|
581
|
+
stdin: CODEX_STDIO[0],
|
|
582
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
583
|
+
timeoutMs,
|
|
584
|
+
onStdout: (chunk) => decoder.pushChunk(chunk),
|
|
585
|
+
onStderr: (chunk) => {
|
|
586
|
+
if (stderr.length < 64 * 1024)
|
|
587
|
+
stderr += chunk;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
} catch (err) {
|
|
591
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
592
|
+
}
|
|
593
|
+
let code;
|
|
594
|
+
try {
|
|
595
|
+
code = await handle.exited;
|
|
596
|
+
} catch (err) {
|
|
597
|
+
if (isMissingBinary(err)) {
|
|
598
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
599
|
+
}
|
|
600
|
+
return { text: "", error: errMsg(err) };
|
|
601
|
+
}
|
|
602
|
+
const result = decoder.finish();
|
|
603
|
+
if (input.sessionId && result.sessionId && !result.error) {
|
|
604
|
+
sessions.set(input.sessionId, result.sessionId);
|
|
605
|
+
}
|
|
606
|
+
if (code === 124) {
|
|
607
|
+
return { ...result, error: `codex timed out after ${timeoutMs}ms` };
|
|
608
|
+
}
|
|
609
|
+
if (result.error)
|
|
610
|
+
return result;
|
|
611
|
+
if (code !== 0 && !result.text) {
|
|
612
|
+
return {
|
|
613
|
+
text: "",
|
|
614
|
+
toolCalls: result.toolCalls,
|
|
615
|
+
tools: result.tools,
|
|
616
|
+
createdTask: result.createdTask,
|
|
617
|
+
error: `codex exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function errMsg(err) {
|
|
624
|
+
if (err instanceof Error)
|
|
625
|
+
return err.message;
|
|
626
|
+
return String(err);
|
|
627
|
+
}
|
|
628
|
+
|
|
16
629
|
// ../../../packages/cli/src/args.ts
|
|
17
630
|
var DEFAULT_PROFILE = "default";
|
|
18
631
|
var DEFAULT_AGENT = "roy";
|
|
19
632
|
var DEFAULT_EVENT_KEY = "im.message.receive_v1";
|
|
20
633
|
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
634
|
+
function findSubcommand(argv) {
|
|
635
|
+
for (let i = 0;i < argv.length; i++) {
|
|
636
|
+
const raw = argv[i];
|
|
637
|
+
if (raw.startsWith("-")) {
|
|
638
|
+
if (!raw.includes("=") && VALUE_FLAGS.has(raw))
|
|
639
|
+
i++;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
return raw;
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
var VALUE_FLAGS = new Set([
|
|
647
|
+
"--profile",
|
|
648
|
+
"--agent",
|
|
649
|
+
"--event-key",
|
|
650
|
+
"--runtime",
|
|
651
|
+
"--command",
|
|
652
|
+
"--runtime-bin",
|
|
653
|
+
"--opencode-bin",
|
|
654
|
+
"--codex-model",
|
|
655
|
+
"--codex-sandbox",
|
|
656
|
+
"--max-concurrency",
|
|
657
|
+
"--timeout-ms"
|
|
658
|
+
]);
|
|
21
659
|
var SUBCOMMANDS = new Set(["serve", "status", "help", "version"]);
|
|
22
660
|
function parseArgs(argv) {
|
|
23
661
|
if (argv.includes("--help") || argv.includes("-h"))
|
|
24
662
|
return { command: "help" };
|
|
25
663
|
if (argv.includes("--version") || argv.includes("-v"))
|
|
26
664
|
return { command: "version" };
|
|
27
|
-
const head = argv
|
|
665
|
+
const head = findSubcommand(argv);
|
|
28
666
|
const command = head ?? "serve";
|
|
29
667
|
if (!SUBCOMMANDS.has(command)) {
|
|
30
668
|
return { command: "unknown", error: `unknown command "${command}"` };
|
|
@@ -107,6 +745,13 @@ function parseServe(argv) {
|
|
|
107
745
|
serve.runtime = v;
|
|
108
746
|
break;
|
|
109
747
|
}
|
|
748
|
+
case "--command": {
|
|
749
|
+
const v = take();
|
|
750
|
+
if (v instanceof Error)
|
|
751
|
+
return { command: "unknown", error: v.message };
|
|
752
|
+
serve.command = v;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
110
755
|
case "--runtime-bin": {
|
|
111
756
|
const v = take();
|
|
112
757
|
if (v instanceof Error)
|
|
@@ -121,6 +766,26 @@ function parseServe(argv) {
|
|
|
121
766
|
serve.opencodeBin = v;
|
|
122
767
|
break;
|
|
123
768
|
}
|
|
769
|
+
case "--codex-model": {
|
|
770
|
+
const v = take();
|
|
771
|
+
if (v instanceof Error)
|
|
772
|
+
return { command: "unknown", error: v.message };
|
|
773
|
+
serve.codexModel = v;
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
case "--codex-sandbox": {
|
|
777
|
+
const v = take();
|
|
778
|
+
if (v instanceof Error)
|
|
779
|
+
return { command: "unknown", error: v.message };
|
|
780
|
+
if (!isCodexSandboxMode(v)) {
|
|
781
|
+
return {
|
|
782
|
+
command: "unknown",
|
|
783
|
+
error: `--codex-sandbox must be one of ${CODEX_SANDBOX_MODES.join("|")}, got "${v}"`
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
serve.codexSandbox = v;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
124
789
|
case "--max-concurrency": {
|
|
125
790
|
const v = take();
|
|
126
791
|
if (v instanceof Error)
|
|
@@ -190,6 +855,8 @@ function readValue(argv, flag) {
|
|
|
190
855
|
return;
|
|
191
856
|
}
|
|
192
857
|
function resolveRuntimeBin(options) {
|
|
858
|
+
if (options.command !== undefined)
|
|
859
|
+
return options.command;
|
|
193
860
|
if (options.runtimeBin !== undefined)
|
|
194
861
|
return options.runtimeBin;
|
|
195
862
|
if (options.runtime === "opencode" && options.opencodeBin !== "opencode") {
|
|
@@ -198,7 +865,7 @@ function resolveRuntimeBin(options) {
|
|
|
198
865
|
return;
|
|
199
866
|
}
|
|
200
867
|
function usage() {
|
|
201
|
-
return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode or
|
|
868
|
+
return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode, claude or codex)
|
|
202
869
|
|
|
203
870
|
USAGE
|
|
204
871
|
roy-task [serve] [options] start the daemon (default command)
|
|
@@ -217,9 +884,15 @@ SERVE OPTIONS
|
|
|
217
884
|
--agent <name> agent to dispatch to (default: ${DEFAULT_AGENT})
|
|
218
885
|
--runtime <name> which harness runs the agent (default: ${DEFAULT_RUNTIME})
|
|
219
886
|
one of: ${RUNTIMES.join(" | ")}
|
|
220
|
-
--
|
|
221
|
-
|
|
887
|
+
--command <name|path> executable to spawn, whatever the dialect
|
|
888
|
+
is (default: the runtime's own binary).
|
|
889
|
+
e.g. --runtime claude --command claude-minimax
|
|
890
|
+
--runtime-bin <path> DEPRECATED alias for --command
|
|
891
|
+
--opencode-bin <path> DEPRECATED alias for --command
|
|
222
892
|
(only applies when --runtime opencode)
|
|
893
|
+
--codex-model <name> model for codex exec --model (default: codex config)
|
|
894
|
+
--codex-sandbox <mode> codex exec -s policy (default: read-only)
|
|
895
|
+
one of: ${CODEX_SANDBOX_MODES.join(" | ")}
|
|
223
896
|
--max-concurrency <n> max simultaneous agent runs (default: 1)
|
|
224
897
|
⚠️ >1 only safe across DIFFERENT chats;
|
|
225
898
|
same-session concurrency cross-contaminates.
|
|
@@ -234,6 +907,12 @@ SERVE OPTIONS
|
|
|
234
907
|
never block the text reply.
|
|
235
908
|
|
|
236
909
|
NOTES
|
|
910
|
+
• --runtime vs --command: --runtime picks the *dialect* (which flags and
|
|
911
|
+
which stream decoder), --command picks the *executable* that speaks it.
|
|
912
|
+
So \`--runtime claude --command claude-minimax\` keeps every claude flag
|
|
913
|
+
(--print / --output-format stream-json / --agents / --resume <uuid>) and
|
|
914
|
+
merely spawns the wrapper instead of the bare binary. Use this whenever
|
|
915
|
+
the default binary is not the one that actually works on your machine.
|
|
237
916
|
• stdin is forced to EOF for the agent child (stdio[0]="ignore"). Without it
|
|
238
917
|
both \`opencode run\` and \`claude --print\` wait for EOF on a socket and the
|
|
239
918
|
run hangs forever.
|
|
@@ -387,6 +1066,24 @@ function buildDispatchPrompt(input) {
|
|
|
387
1066
|
"3. 需要落任务就用 `task_*` 工具(task_create / task_update / …),",
|
|
388
1067
|
" 这是你的主要职责;不要只在回答里“描述”你打算做什么。",
|
|
389
1068
|
"",
|
|
1069
|
+
"## 什么时候该建任务(Task #4433)",
|
|
1070
|
+
"",
|
|
1071
|
+
"任务由**你**决定建不建,守护进程不会替你建。判断标准:",
|
|
1072
|
+
"",
|
|
1073
|
+
"- **该建**:需要多步操作、要动文件/跑命令、要调研并产出结论、",
|
|
1074
|
+
" 耗时预计超过一两分钟、或用户明确要求「跟踪 / 建个任务 / 后续跟进」。",
|
|
1075
|
+
" → 用 `task_create` 建任务,**然后**再开始干活(先立任务,再动手)。",
|
|
1076
|
+
"- **不必建**:寒暄、谢谢、纯问答(「今天几号」「这个参数叫啥」)、",
|
|
1077
|
+
" 一两句话就能答完且不需要任何工具的问题。",
|
|
1078
|
+
" → 直接回答,不要为了「显得勤快」而建一个空任务。",
|
|
1079
|
+
"- **建完要更新**:动手过程中用 `task_update` 推进度 / 改 current_status,",
|
|
1080
|
+
" 干完用 `task_complete` 收尾。建了任务却从不更新,比不建更糟——",
|
|
1081
|
+
" 看板上会留下一个卡住的任务。",
|
|
1082
|
+
"",
|
|
1083
|
+
"⚠️ **卡片与任务一一对应**:只有你建了任务,用户才会收到一张进展卡片;",
|
|
1084
|
+
"没建任务就只有文字回复。所以**不要**为了「想要一张卡片」而去建任务,",
|
|
1085
|
+
"也不要建了任务又不用它——卡片上显示的就是你建的那个任务的真 id 和标题。",
|
|
1086
|
+
"",
|
|
390
1087
|
"## 环境事实(必须使用下面这些值,不要自己猜)",
|
|
391
1088
|
"",
|
|
392
1089
|
`- lark-cli profile:\`${profile}\`(飞书 openId 是 per-app 隔离的,换 profile 会失败)`,
|
|
@@ -510,21 +1207,14 @@ function createNoopCardPort() {
|
|
|
510
1207
|
async finish() {}
|
|
511
1208
|
};
|
|
512
1209
|
}
|
|
513
|
-
function
|
|
514
|
-
const maxTitle = input.maxTitleChars ?? 60;
|
|
515
|
-
const normalized = input.text.replace(/\r\n/g, `
|
|
516
|
-
`).trim();
|
|
517
|
-
const lines = normalized.split(`
|
|
518
|
-
`).map((l) => l.trim()).filter(Boolean);
|
|
519
|
-
const first = lines[0] ?? "(非文本消息)";
|
|
520
|
-
const rest = lines.slice(1).join(`
|
|
521
|
-
`);
|
|
522
|
-
const title = first.length > maxTitle ? `${first.slice(0, maxTitle - 1)}…` : first;
|
|
1210
|
+
function buildCardMeta(input) {
|
|
523
1211
|
return {
|
|
524
|
-
id:
|
|
525
|
-
title
|
|
526
|
-
description:
|
|
527
|
-
|
|
1212
|
+
id: input.task.id,
|
|
1213
|
+
title: input.task.title ?? `Task #${input.task.id}`,
|
|
1214
|
+
description: input.description ?? `由 roy-task 派发,触发消息 ${input.messageId}`,
|
|
1215
|
+
type: "normal",
|
|
1216
|
+
priority: "high",
|
|
1217
|
+
currentStatus: "由 agent 处理中"
|
|
528
1218
|
};
|
|
529
1219
|
}
|
|
530
1220
|
var TOOL_LABELS = {
|
|
@@ -558,8 +1248,13 @@ function cardReport(handle) {
|
|
|
558
1248
|
out.cardMessageId = handle.messageId;
|
|
559
1249
|
if (handle.error)
|
|
560
1250
|
out.cardError = handle.error;
|
|
1251
|
+
if (handle.meta && typeof handle.meta.id === "number")
|
|
1252
|
+
out.taskId = handle.meta.id;
|
|
561
1253
|
return out;
|
|
562
1254
|
}
|
|
1255
|
+
function createdTaskReport(createdTask) {
|
|
1256
|
+
return createdTask ? { taskId: createdTask.id } : {};
|
|
1257
|
+
}
|
|
563
1258
|
function errorMessage(err) {
|
|
564
1259
|
return err instanceof Error ? err.message : String(err);
|
|
565
1260
|
}
|
|
@@ -654,13 +1349,18 @@ class Dispatcher {
|
|
|
654
1349
|
return { status: "dry-run", plan };
|
|
655
1350
|
}
|
|
656
1351
|
const run = async () => {
|
|
657
|
-
const cardPromise = this.openCard({ chatId, messageId, profile, raw });
|
|
658
1352
|
const result = await this.deps.runAgent({
|
|
659
1353
|
prompt: plan.prompt,
|
|
660
1354
|
agent: plan.agent,
|
|
661
1355
|
sessionId: plan.sessionId
|
|
662
1356
|
});
|
|
663
|
-
const card = await
|
|
1357
|
+
const card = await this.openCard({
|
|
1358
|
+
chatId,
|
|
1359
|
+
messageId,
|
|
1360
|
+
profile,
|
|
1361
|
+
createdTask: result.createdTask,
|
|
1362
|
+
requestText: extractUserText(raw)
|
|
1363
|
+
});
|
|
664
1364
|
if (card.messageId) {
|
|
665
1365
|
await this.patchCard(card, {
|
|
666
1366
|
chatId,
|
|
@@ -676,7 +1376,13 @@ class Dispatcher {
|
|
|
676
1376
|
status: "failed",
|
|
677
1377
|
finalStep: `agent 报错:${result.error}`
|
|
678
1378
|
});
|
|
679
|
-
return {
|
|
1379
|
+
return {
|
|
1380
|
+
status: "agent-error",
|
|
1381
|
+
plan,
|
|
1382
|
+
error: result.error,
|
|
1383
|
+
...createdTaskReport(result.createdTask),
|
|
1384
|
+
...cardReport(card)
|
|
1385
|
+
};
|
|
680
1386
|
}
|
|
681
1387
|
if (!result.text.trim()) {
|
|
682
1388
|
await this.closeCard(card, {
|
|
@@ -689,6 +1395,7 @@ class Dispatcher {
|
|
|
689
1395
|
status: "no-output",
|
|
690
1396
|
plan,
|
|
691
1397
|
toolCalls: result.toolCalls ?? 0,
|
|
1398
|
+
...createdTaskReport(result.createdTask),
|
|
692
1399
|
...cardReport(card)
|
|
693
1400
|
};
|
|
694
1401
|
}
|
|
@@ -699,7 +1406,13 @@ class Dispatcher {
|
|
|
699
1406
|
status: "completed",
|
|
700
1407
|
finalStep: "--no-reply:agent 已完成,未发送回复"
|
|
701
1408
|
});
|
|
702
|
-
return {
|
|
1409
|
+
return {
|
|
1410
|
+
status: "no-reply",
|
|
1411
|
+
plan,
|
|
1412
|
+
text: result.text,
|
|
1413
|
+
...createdTaskReport(result.createdTask),
|
|
1414
|
+
...cardReport(card)
|
|
1415
|
+
};
|
|
703
1416
|
}
|
|
704
1417
|
const sent = await this.deps.reply({
|
|
705
1418
|
messageId,
|
|
@@ -743,23 +1456,18 @@ class Dispatcher {
|
|
|
743
1456
|
const handle = { messageId: "" };
|
|
744
1457
|
if (!this.deps.card)
|
|
745
1458
|
return handle;
|
|
1459
|
+
if (!input.createdTask)
|
|
1460
|
+
return handle;
|
|
1461
|
+
const meta = buildCardMeta({
|
|
1462
|
+
task: input.createdTask,
|
|
1463
|
+
messageId: input.messageId,
|
|
1464
|
+
...input.requestText ? { description: input.requestText } : {}
|
|
1465
|
+
});
|
|
746
1466
|
try {
|
|
747
|
-
const identity = buildCardIdentity({
|
|
748
|
-
messageId: input.messageId,
|
|
749
|
-
text: extractUserText(input.raw)
|
|
750
|
-
});
|
|
751
1467
|
const started = await this.deps.card.start({
|
|
752
1468
|
chatId: input.chatId,
|
|
753
1469
|
profile: input.profile,
|
|
754
|
-
meta
|
|
755
|
-
id: identity.id,
|
|
756
|
-
title: identity.title,
|
|
757
|
-
description: identity.description,
|
|
758
|
-
goals: identity.goals,
|
|
759
|
-
type: "normal",
|
|
760
|
-
priority: "high",
|
|
761
|
-
currentStatus: "已收到消息,正在处理"
|
|
762
|
-
},
|
|
1470
|
+
meta,
|
|
763
1471
|
milestones: [
|
|
764
1472
|
{
|
|
765
1473
|
title: "收到消息",
|
|
@@ -770,6 +1478,7 @@ class Dispatcher {
|
|
|
770
1478
|
});
|
|
771
1479
|
handle.messageId = started.messageId;
|
|
772
1480
|
handle.pinned = started.pinned;
|
|
1481
|
+
handle.meta = meta;
|
|
773
1482
|
} catch (err) {
|
|
774
1483
|
handle.error = errorMessage(err);
|
|
775
1484
|
}
|
|
@@ -787,6 +1496,7 @@ class Dispatcher {
|
|
|
787
1496
|
chatId: input.chatId,
|
|
788
1497
|
profile: input.profile,
|
|
789
1498
|
status: input.status,
|
|
1499
|
+
...handle.meta ? { meta: handle.meta } : {},
|
|
790
1500
|
processSteps: tools.map(describeToolUse)
|
|
791
1501
|
});
|
|
792
1502
|
} catch (err) {
|
|
@@ -803,7 +1513,8 @@ class Dispatcher {
|
|
|
803
1513
|
profile: input.profile,
|
|
804
1514
|
status: input.status,
|
|
805
1515
|
finalStep: input.finalStep,
|
|
806
|
-
progress: input.status === "completed" ? 100 : undefined
|
|
1516
|
+
progress: input.status === "completed" ? 100 : undefined,
|
|
1517
|
+
...handle.meta ? { meta: handle.meta } : {}
|
|
807
1518
|
});
|
|
808
1519
|
} catch (err) {
|
|
809
1520
|
handle.error ??= errorMessage(err);
|
|
@@ -811,188 +1522,66 @@ class Dispatcher {
|
|
|
811
1522
|
}
|
|
812
1523
|
buildPlan(input) {
|
|
813
1524
|
const derive = this.opts.sessionIdFor ?? deriveSessionId;
|
|
814
|
-
return {
|
|
815
|
-
agent: this.opts.agent ?? "roy",
|
|
816
|
-
sessionId: derive({ profile: input.profile, chatId: input.chatId }),
|
|
817
|
-
prompt: buildDispatchPrompt({
|
|
818
|
-
rawEvent: input.raw,
|
|
819
|
-
profile: input.profile,
|
|
820
|
-
recommendedAction: input.recommendedAction
|
|
821
|
-
}),
|
|
822
|
-
chatId: input.chatId,
|
|
823
|
-
messageId: input.messageId,
|
|
824
|
-
profile: input.profile
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// ../../../packages/cli/src/daemon/es-listener.ts
|
|
830
|
-
import { larkCliHandler } from "@ai-setting/roy-agent-core/env/event-source";
|
|
831
|
-
function createEventListener(config) {
|
|
832
|
-
const eventKeys = config.eventKeys?.length ? config.eventKeys : ["im.message.receive_v1"];
|
|
833
|
-
const sourceId = config.sourceId ?? "roy-task-es";
|
|
834
|
-
const instance = larkCliHandler.createInstance({
|
|
835
|
-
id: sourceId,
|
|
836
|
-
name: "roy-task event source",
|
|
837
|
-
type: "lark-cli",
|
|
838
|
-
enabled: true,
|
|
839
|
-
profile: config.profile,
|
|
840
|
-
eventKeys
|
|
841
|
-
});
|
|
842
|
-
let callback;
|
|
843
|
-
instance.onEvent((event) => {
|
|
844
|
-
const typed = event;
|
|
845
|
-
if (!callback)
|
|
846
|
-
return;
|
|
847
|
-
Promise.resolve(callback(typed)).catch((err) => {
|
|
848
|
-
console.error("[roy-task] event handler threw:", err instanceof Error ? err.message : err);
|
|
849
|
-
});
|
|
850
|
-
});
|
|
851
|
-
return {
|
|
852
|
-
onEvent(cb) {
|
|
853
|
-
callback = cb;
|
|
854
|
-
},
|
|
855
|
-
async start() {
|
|
856
|
-
await instance.start();
|
|
857
|
-
},
|
|
858
|
-
async stop() {
|
|
859
|
-
await instance.stop();
|
|
860
|
-
},
|
|
861
|
-
status() {
|
|
862
|
-
return instance.getStatus();
|
|
863
|
-
},
|
|
864
|
-
plannedCommands() {
|
|
865
|
-
const profileFlag = config.profile ? `--profile ${config.profile} ` : "";
|
|
866
|
-
return eventKeys.map((k) => `lark-cli ${profileFlag}event consume ${k} --as bot`);
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
872
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
873
|
-
|
|
874
|
-
// ../../../packages/cli/src/runtime/spawn.ts
|
|
875
|
-
import { spawn } from "node:child_process";
|
|
876
|
-
function runCapture(cmd, opts = {}) {
|
|
877
|
-
return new Promise((resolve, reject) => {
|
|
878
|
-
let child;
|
|
879
|
-
try {
|
|
880
|
-
child = spawn(cmd[0], cmd.slice(1), {
|
|
881
|
-
stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
|
|
882
|
-
env: opts.env ? normalizeEnv(opts.env) : process.env,
|
|
883
|
-
cwd: opts.cwd,
|
|
884
|
-
windowsHide: true
|
|
885
|
-
});
|
|
886
|
-
} catch (err) {
|
|
887
|
-
reject(err);
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
let stdout = "";
|
|
891
|
-
let stderr = "";
|
|
892
|
-
let timedOut = false;
|
|
893
|
-
child.stdout?.setEncoding("utf8");
|
|
894
|
-
child.stderr?.setEncoding("utf8");
|
|
895
|
-
child.stdout?.on("data", (chunk) => {
|
|
896
|
-
stdout += chunk;
|
|
897
|
-
});
|
|
898
|
-
child.stderr?.on("data", (chunk) => {
|
|
899
|
-
stderr += chunk;
|
|
900
|
-
});
|
|
901
|
-
const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
|
|
902
|
-
timedOut = true;
|
|
903
|
-
try {
|
|
904
|
-
child.kill("SIGTERM");
|
|
905
|
-
} catch {}
|
|
906
|
-
setTimeout(() => {
|
|
907
|
-
try {
|
|
908
|
-
child.kill("SIGKILL");
|
|
909
|
-
} catch {}
|
|
910
|
-
}, 2000).unref?.();
|
|
911
|
-
}, opts.timeoutMs) : undefined;
|
|
912
|
-
timer?.unref?.();
|
|
913
|
-
child.on("error", (err) => {
|
|
914
|
-
if (timer)
|
|
915
|
-
clearTimeout(timer);
|
|
916
|
-
reject(err);
|
|
917
|
-
});
|
|
918
|
-
child.on("close", (code, signal) => {
|
|
919
|
-
if (timer)
|
|
920
|
-
clearTimeout(timer);
|
|
921
|
-
if (timedOut) {
|
|
922
|
-
resolve({ code: 124, stdout, stderr });
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
resolve({ code: code ?? (signal ? 1 : 0), stdout, stderr });
|
|
926
|
-
});
|
|
927
|
-
});
|
|
1525
|
+
return {
|
|
1526
|
+
agent: this.opts.agent ?? "roy",
|
|
1527
|
+
sessionId: derive({ profile: input.profile, chatId: input.chatId }),
|
|
1528
|
+
prompt: buildDispatchPrompt({
|
|
1529
|
+
rawEvent: input.raw,
|
|
1530
|
+
profile: input.profile,
|
|
1531
|
+
recommendedAction: input.recommendedAction
|
|
1532
|
+
}),
|
|
1533
|
+
chatId: input.chatId,
|
|
1534
|
+
messageId: input.messageId,
|
|
1535
|
+
profile: input.profile
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
928
1538
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1539
|
+
|
|
1540
|
+
// ../../../packages/cli/src/daemon/es-listener.ts
|
|
1541
|
+
import { larkCliHandler } from "@ai-setting/roy-agent-core/env/event-source";
|
|
1542
|
+
function createEventListener(config) {
|
|
1543
|
+
const eventKeys = config.eventKeys?.length ? config.eventKeys : ["im.message.receive_v1"];
|
|
1544
|
+
const sourceId = config.sourceId ?? "roy-task-es";
|
|
1545
|
+
const instance = larkCliHandler.createInstance({
|
|
1546
|
+
id: sourceId,
|
|
1547
|
+
name: "roy-task event source",
|
|
1548
|
+
type: "lark-cli",
|
|
1549
|
+
enabled: true,
|
|
1550
|
+
profile: config.profile,
|
|
1551
|
+
eventKeys
|
|
935
1552
|
});
|
|
936
|
-
let
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
timer?.unref?.();
|
|
944
|
-
if (opts.onStdout || opts.onStderr) {
|
|
945
|
-
child.stdout?.setEncoding("utf8");
|
|
946
|
-
child.stderr?.setEncoding("utf8");
|
|
947
|
-
child.stdout?.on("data", (chunk) => opts.onStdout?.(chunk));
|
|
948
|
-
child.stderr?.on("data", (chunk) => opts.onStderr?.(chunk));
|
|
949
|
-
} else {
|
|
950
|
-
child.stdout?.resume();
|
|
951
|
-
child.stderr?.resume();
|
|
952
|
-
}
|
|
953
|
-
const exited = new Promise((resolve, reject) => {
|
|
954
|
-
child.on("error", (err) => {
|
|
955
|
-
if (timer)
|
|
956
|
-
clearTimeout(timer);
|
|
957
|
-
reject(err);
|
|
958
|
-
});
|
|
959
|
-
child.on("close", (code, signal) => {
|
|
960
|
-
if (timer)
|
|
961
|
-
clearTimeout(timer);
|
|
962
|
-
if (timedOut) {
|
|
963
|
-
resolve(124);
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
resolve(code ?? (signal ? 1 : 0));
|
|
1553
|
+
let callback;
|
|
1554
|
+
instance.onEvent((event) => {
|
|
1555
|
+
const typed = event;
|
|
1556
|
+
if (!callback)
|
|
1557
|
+
return;
|
|
1558
|
+
Promise.resolve(callback(typed)).catch((err) => {
|
|
1559
|
+
console.error("[roy-task] event handler threw:", err instanceof Error ? err.message : err);
|
|
967
1560
|
});
|
|
968
1561
|
});
|
|
969
1562
|
return {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1563
|
+
onEvent(cb) {
|
|
1564
|
+
callback = cb;
|
|
1565
|
+
},
|
|
1566
|
+
async start() {
|
|
1567
|
+
await instance.start();
|
|
1568
|
+
},
|
|
1569
|
+
async stop() {
|
|
1570
|
+
await instance.stop();
|
|
1571
|
+
},
|
|
1572
|
+
status() {
|
|
1573
|
+
return instance.getStatus();
|
|
1574
|
+
},
|
|
1575
|
+
plannedCommands() {
|
|
1576
|
+
const profileFlag = config.profile ? `--profile ${config.profile} ` : "";
|
|
1577
|
+
return eventKeys.map((k) => `lark-cli ${profileFlag}event consume ${k} --as bot`);
|
|
976
1578
|
}
|
|
977
1579
|
};
|
|
978
1580
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
return "ignore";
|
|
984
|
-
}
|
|
985
|
-
function normalizeEnv(env) {
|
|
986
|
-
const out = {};
|
|
987
|
-
for (const [k, v] of Object.entries(env)) {
|
|
988
|
-
if (typeof v === "string")
|
|
989
|
-
out[k] = v;
|
|
990
|
-
}
|
|
991
|
-
return out;
|
|
992
|
-
}
|
|
993
|
-
function isMissingBinary(err) {
|
|
994
|
-
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
995
|
-
}
|
|
1581
|
+
|
|
1582
|
+
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
1583
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1584
|
+
import { appendFileSync } from "node:fs";
|
|
996
1585
|
|
|
997
1586
|
// ../../../packages/cli/src/daemon/runners/claude-agents.ts
|
|
998
1587
|
var CLAUDE_ROY_AGENT_NAME = "roy";
|
|
@@ -1142,102 +1731,6 @@ function createClaudeSessionRegistry() {
|
|
|
1142
1731
|
};
|
|
1143
1732
|
}
|
|
1144
1733
|
|
|
1145
|
-
// ../../../packages/cli/src/daemon/ndjson.ts
|
|
1146
|
-
class LineSplitter {
|
|
1147
|
-
tail = "";
|
|
1148
|
-
pushChunk(chunk) {
|
|
1149
|
-
this.tail += chunk;
|
|
1150
|
-
const out = [];
|
|
1151
|
-
let idx = this.tail.indexOf(`
|
|
1152
|
-
`);
|
|
1153
|
-
while (idx !== -1) {
|
|
1154
|
-
out.push(this.tail.slice(0, idx));
|
|
1155
|
-
this.tail = this.tail.slice(idx + 1);
|
|
1156
|
-
idx = this.tail.indexOf(`
|
|
1157
|
-
`);
|
|
1158
|
-
}
|
|
1159
|
-
return out;
|
|
1160
|
-
}
|
|
1161
|
-
finish() {
|
|
1162
|
-
if (!this.tail)
|
|
1163
|
-
return [];
|
|
1164
|
-
const line = this.tail;
|
|
1165
|
-
this.tail = "";
|
|
1166
|
-
return [line];
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
class NdjsonAssembler {
|
|
1171
|
-
tail = "";
|
|
1172
|
-
texts = [];
|
|
1173
|
-
error;
|
|
1174
|
-
toolCalls = 0;
|
|
1175
|
-
sessionId;
|
|
1176
|
-
tools = [];
|
|
1177
|
-
pushChunk(chunk) {
|
|
1178
|
-
this.tail += chunk;
|
|
1179
|
-
let idx = this.tail.indexOf(`
|
|
1180
|
-
`);
|
|
1181
|
-
while (idx !== -1) {
|
|
1182
|
-
const line = this.tail.slice(0, idx);
|
|
1183
|
-
this.tail = this.tail.slice(idx + 1);
|
|
1184
|
-
this.parseLine(line);
|
|
1185
|
-
idx = this.tail.indexOf(`
|
|
1186
|
-
`);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
pushLine(line) {
|
|
1190
|
-
this.parseLine(line);
|
|
1191
|
-
}
|
|
1192
|
-
finishIncomplete() {
|
|
1193
|
-
return this.tail.trim();
|
|
1194
|
-
}
|
|
1195
|
-
parseLine(line) {
|
|
1196
|
-
const trimmed = line.trim();
|
|
1197
|
-
if (!trimmed || trimmed[0] !== "{")
|
|
1198
|
-
return;
|
|
1199
|
-
let parsed;
|
|
1200
|
-
try {
|
|
1201
|
-
parsed = JSON.parse(trimmed);
|
|
1202
|
-
} catch {
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
if (parsed.sessionID)
|
|
1206
|
-
this.sessionId = parsed.sessionID;
|
|
1207
|
-
if (parsed.type === "text" && typeof parsed.part?.text === "string") {
|
|
1208
|
-
this.texts.push(parsed.part.text);
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
if (parsed.type === "tool_use") {
|
|
1212
|
-
this.toolCalls += 1;
|
|
1213
|
-
const name = parsed.part?.tool ?? parsed.tool;
|
|
1214
|
-
if (typeof name === "string" && name) {
|
|
1215
|
-
this.tools.push(name);
|
|
1216
|
-
}
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
if (parsed.type === "error") {
|
|
1220
|
-
if (this.error === undefined) {
|
|
1221
|
-
this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
finish() {
|
|
1226
|
-
if (this.tail.trim()) {
|
|
1227
|
-
this.parseLine(this.tail);
|
|
1228
|
-
this.tail = "";
|
|
1229
|
-
}
|
|
1230
|
-
const text = this.texts.join("").trim();
|
|
1231
|
-
return {
|
|
1232
|
-
text,
|
|
1233
|
-
error: this.error,
|
|
1234
|
-
toolCalls: this.toolCalls,
|
|
1235
|
-
sessionId: this.sessionId,
|
|
1236
|
-
tools: this.tools
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
1734
|
// ../../../packages/cli/src/daemon/runners/claude-stream.ts
|
|
1242
1735
|
function errorDetail(result) {
|
|
1243
1736
|
if (typeof result.result === "string" && result.result.trim() !== "") {
|
|
@@ -1250,10 +1743,34 @@ function errorDetail(result) {
|
|
|
1250
1743
|
}
|
|
1251
1744
|
return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
|
|
1252
1745
|
}
|
|
1746
|
+
function flattenToolResult(content) {
|
|
1747
|
+
if (typeof content === "string")
|
|
1748
|
+
return content;
|
|
1749
|
+
if (!Array.isArray(content))
|
|
1750
|
+
return "";
|
|
1751
|
+
const parts = [];
|
|
1752
|
+
for (const block of content) {
|
|
1753
|
+
if (typeof block === "string") {
|
|
1754
|
+
parts.push(block);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
if (typeof block === "object" && block !== null) {
|
|
1758
|
+
const t = block.text;
|
|
1759
|
+
if (typeof t === "string")
|
|
1760
|
+
parts.push(t);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return parts.join(`
|
|
1764
|
+
`);
|
|
1765
|
+
}
|
|
1253
1766
|
|
|
1254
1767
|
class ClaudeStreamDecoder {
|
|
1255
1768
|
lines = new LineSplitter;
|
|
1256
|
-
state = {
|
|
1769
|
+
state = {
|
|
1770
|
+
assistantText: [],
|
|
1771
|
+
tools: [],
|
|
1772
|
+
pendingCreates: new Map
|
|
1773
|
+
};
|
|
1257
1774
|
pushChunk(chunk) {
|
|
1258
1775
|
for (const line of this.lines.pushChunk(chunk))
|
|
1259
1776
|
this.consume(line);
|
|
@@ -1286,10 +1803,38 @@ class ClaudeStreamDecoder {
|
|
|
1286
1803
|
this.consumeAssistant(ev);
|
|
1287
1804
|
return;
|
|
1288
1805
|
}
|
|
1806
|
+
if (ev.type === "user") {
|
|
1807
|
+
this.consumeUser(ev);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1289
1810
|
if (ev.type === "result") {
|
|
1290
1811
|
this.state.result = ev;
|
|
1291
1812
|
}
|
|
1292
1813
|
}
|
|
1814
|
+
consumeUser(ev) {
|
|
1815
|
+
const message = ev.message;
|
|
1816
|
+
const content = message?.content;
|
|
1817
|
+
if (!Array.isArray(content))
|
|
1818
|
+
return;
|
|
1819
|
+
for (const block of content) {
|
|
1820
|
+
if (typeof block !== "object" || block === null)
|
|
1821
|
+
continue;
|
|
1822
|
+
const b = block;
|
|
1823
|
+
if (b.type !== "tool_result")
|
|
1824
|
+
continue;
|
|
1825
|
+
const id = typeof b.tool_use_id === "string" ? b.tool_use_id : undefined;
|
|
1826
|
+
const pending = id ? this.state.pendingCreates.get(id) : undefined;
|
|
1827
|
+
const text = flattenToolResult(b.content);
|
|
1828
|
+
if (this.state.createdTask !== undefined)
|
|
1829
|
+
continue;
|
|
1830
|
+
if (pending) {
|
|
1831
|
+
this.state.pendingCreates.delete(id);
|
|
1832
|
+
this.state.createdTask = extractCreatedTask(text, pending.titleHint);
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
this.state.createdTask = extractCreatedTask(text);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1293
1838
|
consumeAssistant(ev) {
|
|
1294
1839
|
const message = ev.message;
|
|
1295
1840
|
const content = message?.content;
|
|
@@ -1302,8 +1847,17 @@ class ClaudeStreamDecoder {
|
|
|
1302
1847
|
if (b.type === "text" && typeof b.text === "string") {
|
|
1303
1848
|
this.state.assistantText.push(b.text);
|
|
1304
1849
|
} else if (b.type === "tool_use") {
|
|
1305
|
-
if (typeof b.name === "string")
|
|
1850
|
+
if (typeof b.name === "string") {
|
|
1306
1851
|
this.state.tools.push(b.name);
|
|
1852
|
+
if (isTaskCreateTool(b.name) && typeof b.id === "string") {
|
|
1853
|
+
const input = b.input;
|
|
1854
|
+
const title = input?.["title"];
|
|
1855
|
+
this.state.pendingCreates.set(b.id, {
|
|
1856
|
+
tool: b.name,
|
|
1857
|
+
titleHint: typeof title === "string" ? title : undefined
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1307
1861
|
}
|
|
1308
1862
|
}
|
|
1309
1863
|
}
|
|
@@ -1311,15 +1865,23 @@ class ClaudeStreamDecoder {
|
|
|
1311
1865
|
const { result } = this.state;
|
|
1312
1866
|
const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
|
|
1313
1867
|
const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
|
|
1868
|
+
const createdTask = this.state.createdTask;
|
|
1314
1869
|
if (result?.is_error) {
|
|
1315
1870
|
const detail = errorDetail(result);
|
|
1316
|
-
return {
|
|
1871
|
+
return {
|
|
1872
|
+
text: "",
|
|
1873
|
+
sessionId: this.state.sessionId,
|
|
1874
|
+
toolCalls,
|
|
1875
|
+
tools,
|
|
1876
|
+
createdTask,
|
|
1877
|
+
error: detail.slice(0, 1000)
|
|
1878
|
+
};
|
|
1317
1879
|
}
|
|
1318
1880
|
if (typeof result?.result === "string" && result.result.trim() !== "") {
|
|
1319
|
-
return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools };
|
|
1881
|
+
return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools, createdTask };
|
|
1320
1882
|
}
|
|
1321
1883
|
const fallback = this.state.assistantText.join("").trim();
|
|
1322
|
-
return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools };
|
|
1884
|
+
return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools, createdTask };
|
|
1323
1885
|
}
|
|
1324
1886
|
}
|
|
1325
1887
|
|
|
@@ -1380,23 +1942,30 @@ function makeClaudeRunner(opts = {}) {
|
|
|
1380
1942
|
stdin: CLAUDE_STDIO[0],
|
|
1381
1943
|
env: { ...process.env, NO_COLOR: "1" },
|
|
1382
1944
|
timeoutMs,
|
|
1383
|
-
onStdout: (chunk) =>
|
|
1945
|
+
onStdout: (chunk) => {
|
|
1946
|
+
decoder.pushChunk(chunk);
|
|
1947
|
+
if (process.env["ROY_TASK_DUMP_STREAM"]) {
|
|
1948
|
+
try {
|
|
1949
|
+
appendFileSync(process.env["ROY_TASK_DUMP_STREAM"], chunk);
|
|
1950
|
+
} catch {}
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1384
1953
|
onStderr: (chunk) => {
|
|
1385
1954
|
if (stderr.length < 64 * 1024)
|
|
1386
1955
|
stderr += chunk;
|
|
1387
1956
|
}
|
|
1388
1957
|
});
|
|
1389
1958
|
} catch (err) {
|
|
1390
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1959
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
1391
1960
|
}
|
|
1392
1961
|
let code;
|
|
1393
1962
|
try {
|
|
1394
1963
|
code = await handle.exited;
|
|
1395
1964
|
} catch (err) {
|
|
1396
1965
|
if (isMissingBinary(err)) {
|
|
1397
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1966
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
1398
1967
|
}
|
|
1399
|
-
return { text: "", error:
|
|
1968
|
+
return { text: "", error: errMsg2(err) };
|
|
1400
1969
|
}
|
|
1401
1970
|
const result = decoder.finish();
|
|
1402
1971
|
const stderrText = stderr.trim();
|
|
@@ -1430,7 +1999,7 @@ function makeClaudeRunner(opts = {}) {
|
|
|
1430
1999
|
};
|
|
1431
2000
|
}
|
|
1432
2001
|
var STALE_SESSION_RE = /No conversation found|not a UUID|session title/i;
|
|
1433
|
-
function
|
|
2002
|
+
function errMsg2(err) {
|
|
1434
2003
|
if (err instanceof Error)
|
|
1435
2004
|
return err.message;
|
|
1436
2005
|
return String(err);
|
|
@@ -1462,16 +2031,16 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1462
2031
|
}
|
|
1463
2032
|
});
|
|
1464
2033
|
} catch (err) {
|
|
1465
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
2034
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
|
|
1466
2035
|
}
|
|
1467
2036
|
let code;
|
|
1468
2037
|
try {
|
|
1469
2038
|
code = await handle.exited;
|
|
1470
2039
|
} catch (err) {
|
|
1471
2040
|
if (isMissingBinary(err)) {
|
|
1472
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
2041
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
|
|
1473
2042
|
}
|
|
1474
|
-
return { text: "", error:
|
|
2043
|
+
return { text: "", error: errMsg3(err) };
|
|
1475
2044
|
}
|
|
1476
2045
|
const result = assembler.finish();
|
|
1477
2046
|
if (code === 124) {
|
|
@@ -1480,7 +2049,8 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1480
2049
|
error: `opencode timed out after ${timeoutMs}ms`,
|
|
1481
2050
|
sessionId: result.sessionId,
|
|
1482
2051
|
toolCalls: result.toolCalls,
|
|
1483
|
-
tools: result.tools
|
|
2052
|
+
tools: result.tools,
|
|
2053
|
+
createdTask: result.createdTask
|
|
1484
2054
|
};
|
|
1485
2055
|
}
|
|
1486
2056
|
if (result.error) {
|
|
@@ -1489,7 +2059,8 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1489
2059
|
error: result.error,
|
|
1490
2060
|
sessionId: result.sessionId,
|
|
1491
2061
|
toolCalls: result.toolCalls,
|
|
1492
|
-
tools: result.tools
|
|
2062
|
+
tools: result.tools,
|
|
2063
|
+
createdTask: result.createdTask
|
|
1493
2064
|
};
|
|
1494
2065
|
}
|
|
1495
2066
|
if (code !== 0 && !result.text) {
|
|
@@ -1497,18 +2068,20 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1497
2068
|
text: "",
|
|
1498
2069
|
error: `opencode exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`,
|
|
1499
2070
|
toolCalls: result.toolCalls,
|
|
1500
|
-
tools: result.tools
|
|
2071
|
+
tools: result.tools,
|
|
2072
|
+
createdTask: result.createdTask
|
|
1501
2073
|
};
|
|
1502
2074
|
}
|
|
1503
2075
|
return {
|
|
1504
2076
|
text: result.text,
|
|
1505
2077
|
sessionId: result.sessionId,
|
|
1506
2078
|
toolCalls: result.toolCalls,
|
|
1507
|
-
tools: result.tools
|
|
2079
|
+
tools: result.tools,
|
|
2080
|
+
createdTask: result.createdTask
|
|
1508
2081
|
};
|
|
1509
2082
|
};
|
|
1510
2083
|
}
|
|
1511
|
-
function
|
|
2084
|
+
function errMsg3(err) {
|
|
1512
2085
|
if (err instanceof Error)
|
|
1513
2086
|
return err.message;
|
|
1514
2087
|
return String(err);
|
|
@@ -1521,6 +2094,12 @@ var FACTORIES = {
|
|
|
1521
2094
|
bin: input.bin,
|
|
1522
2095
|
timeoutMs: input.timeoutMs,
|
|
1523
2096
|
agentsJson: input.claudeAgentsJson
|
|
2097
|
+
}),
|
|
2098
|
+
codex: (input) => makeCodexRunner({
|
|
2099
|
+
bin: input.bin,
|
|
2100
|
+
timeoutMs: input.timeoutMs,
|
|
2101
|
+
model: input.codexModel,
|
|
2102
|
+
sandbox: isCodexSandboxMode(input.codexSandbox ?? "") ? input.codexSandbox : undefined
|
|
1524
2103
|
})
|
|
1525
2104
|
};
|
|
1526
2105
|
function makeRunner(input = {}) {
|
|
@@ -1629,7 +2208,9 @@ async function startServe(options, deps) {
|
|
|
1629
2208
|
runAgent: deps.runAgent ?? makeRunner({
|
|
1630
2209
|
runtime: options.runtime,
|
|
1631
2210
|
bin: resolveRuntimeBin(options),
|
|
1632
|
-
timeoutMs: options.timeoutMs
|
|
2211
|
+
timeoutMs: options.timeoutMs,
|
|
2212
|
+
codexModel: options.codexModel,
|
|
2213
|
+
codexSandbox: options.codexSandbox
|
|
1633
2214
|
}),
|
|
1634
2215
|
reply: deps.reply ?? defaultReplyRunner,
|
|
1635
2216
|
gate,
|
|
@@ -1658,7 +2239,7 @@ async function startServe(options, deps) {
|
|
|
1658
2239
|
try {
|
|
1659
2240
|
await listener.stop();
|
|
1660
2241
|
} catch (err) {
|
|
1661
|
-
log(`[roy-task] listener.stop() failed: ${
|
|
2242
|
+
log(`[roy-task] listener.stop() failed: ${errMsg4(err)}`);
|
|
1662
2243
|
}
|
|
1663
2244
|
log("[roy-task] stopped");
|
|
1664
2245
|
resolveDone();
|
|
@@ -1680,7 +2261,7 @@ async function startServe(options, deps) {
|
|
|
1680
2261
|
await stop();
|
|
1681
2262
|
}
|
|
1682
2263
|
} catch (err) {
|
|
1683
|
-
log(`[roy-task] ${label} dispatch threw: ${
|
|
2264
|
+
log(`[roy-task] ${label} dispatch threw: ${errMsg4(err)}`);
|
|
1684
2265
|
if (options.once)
|
|
1685
2266
|
await stop();
|
|
1686
2267
|
}
|
|
@@ -1690,7 +2271,7 @@ async function startServe(options, deps) {
|
|
|
1690
2271
|
try {
|
|
1691
2272
|
await listener.start();
|
|
1692
2273
|
} catch (err) {
|
|
1693
|
-
log(`[roy-task] failed to start event listener: ${
|
|
2274
|
+
log(`[roy-task] failed to start event listener: ${errMsg4(err)}`);
|
|
1694
2275
|
clearInterval(sweeper);
|
|
1695
2276
|
resolveDone();
|
|
1696
2277
|
throw err;
|
|
@@ -1708,7 +2289,11 @@ function formatBanner(options, commands, maxConcurrency) {
|
|
|
1708
2289
|
` profile : ${options.profile}`,
|
|
1709
2290
|
` agent : ${options.agent}`,
|
|
1710
2291
|
` runtime : ${options.runtime}`,
|
|
1711
|
-
|
|
2292
|
+
" command : " + (resolveRuntimeBin(options) ?? "(default: " + options.runtime + ")"),
|
|
2293
|
+
...options.runtime === "codex" ? [
|
|
2294
|
+
` codex model : ${options.codexModel ?? "(codex config)"}`,
|
|
2295
|
+
` codex sandbox : ${options.codexSandbox ?? "read-only"}`
|
|
2296
|
+
] : [],
|
|
1712
2297
|
` max concurrency : ${maxConcurrency}`,
|
|
1713
2298
|
` reply enabled : ${options.replyEnabled}`,
|
|
1714
2299
|
` progress cards : ${options.cardEnabled && !options.dryRun}`,
|
|
@@ -1727,13 +2312,15 @@ function cardSuffix(result) {
|
|
|
1727
2312
|
return "";
|
|
1728
2313
|
if (result.cardError)
|
|
1729
2314
|
return ` card=FAILED(${result.cardError})`;
|
|
1730
|
-
|
|
2315
|
+
if (result.cardMessageId)
|
|
2316
|
+
return ` card=${result.cardMessageId}`;
|
|
2317
|
+
return " card=none(no task created)";
|
|
1731
2318
|
}
|
|
1732
2319
|
function formatResult(label, result) {
|
|
1733
2320
|
const card = cardSuffix(result);
|
|
1734
2321
|
switch (result.status) {
|
|
1735
2322
|
case "dispatched":
|
|
1736
|
-
return `[roy-task] ${label} dispatched chat=${result.plan.chatId} session=${result.plan.sessionId} ` + `reply=${result.replyMessageId ?? "(no id)"} chars=${result.text.length}${card}`;
|
|
2323
|
+
return `[roy-task] ${label} dispatched chat=${result.plan.chatId} session=${result.plan.sessionId} ` + `reply=${result.replyMessageId ?? "(no id)"} chars=${result.text.length}` + ` task=${result.taskId ?? "(none)"}${card}`;
|
|
1737
2324
|
case "dry-run":
|
|
1738
2325
|
return `[roy-task] ${label} DRY-RUN would run agent=${result.plan.agent} ` + `session=${result.plan.sessionId} chat=${result.plan.chatId} prompt=${result.plan.prompt.length}chars`;
|
|
1739
2326
|
case "duplicate":
|
|
@@ -1750,7 +2337,7 @@ function formatResult(label, result) {
|
|
|
1750
2337
|
return `[roy-task] ${label} skipped: ${result.reason}`;
|
|
1751
2338
|
}
|
|
1752
2339
|
}
|
|
1753
|
-
function
|
|
2340
|
+
function errMsg4(err) {
|
|
1754
2341
|
return err instanceof Error ? err.message : String(err);
|
|
1755
2342
|
}
|
|
1756
2343
|
|
|
@@ -1875,7 +2462,7 @@ function formatStatus(report) {
|
|
|
1875
2462
|
}
|
|
1876
2463
|
|
|
1877
2464
|
// ../../../packages/cli/src/main.ts
|
|
1878
|
-
var VERSION = "0.
|
|
2465
|
+
var VERSION = "0.6.0";
|
|
1879
2466
|
async function main(argv) {
|
|
1880
2467
|
const parsed = parseArgs(argv);
|
|
1881
2468
|
switch (parsed.command) {
|