@ai-setting/roy-task-plugin 0.7.2 → 0.7.3
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/agents/roy.md +21 -16
- package/dist/claude-plugin/hooks/stop-check.mjs +7 -1
- package/dist/claude-plugin/mcp/bridge.mjs +30 -35
- package/dist/claude-plugin/mcp/roy-task-mcp-server.mjs +31 -7
- package/dist/claude-plugin/mcp/task-notify-hooks.mjs +303 -0
- package/dist/cli.js +1113 -367
- package/dist/index.js +184 -184
- package/dist/index.js.map +20 -19
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,13 @@ 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", "codex"]);
|
|
6
|
+
var RUNTIMES = Object.freeze(["opencode", "claude", "codex", "cursor"]);
|
|
7
7
|
var DEFAULT_RUNTIME = "opencode";
|
|
8
8
|
var DEFAULT_RUNTIME_BIN = Object.freeze({
|
|
9
9
|
opencode: "opencode",
|
|
10
10
|
claude: "claude",
|
|
11
|
-
codex: "codex"
|
|
11
|
+
codex: "codex",
|
|
12
|
+
cursor: "cursor-agent"
|
|
12
13
|
});
|
|
13
14
|
function isRuntimeName(value) {
|
|
14
15
|
return RUNTIMES.includes(value);
|
|
@@ -137,6 +138,10 @@ function isMissingBinary(err) {
|
|
|
137
138
|
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// ../../../packages/cli/src/daemon/runners/codex.ts
|
|
142
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
143
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
144
|
+
|
|
140
145
|
// ../../../packages/cli/src/daemon/task-created.ts
|
|
141
146
|
var CREATED_LINE = /Task created(?:\s+successfully)?\s*:\s*#(\d+)/;
|
|
142
147
|
function extractCreatedTask(text, titleHint) {
|
|
@@ -327,7 +332,7 @@ function titleFromCliSummary(text) {
|
|
|
327
332
|
return title;
|
|
328
333
|
}
|
|
329
334
|
function isTaskCreateTool(name) {
|
|
330
|
-
return /(^|
|
|
335
|
+
return /(^|[-_])task_create$/.test(name);
|
|
331
336
|
}
|
|
332
337
|
|
|
333
338
|
// ../../../packages/cli/src/daemon/ndjson.ts
|
|
@@ -363,6 +368,9 @@ class NdjsonAssembler {
|
|
|
363
368
|
sessionId;
|
|
364
369
|
tools = [];
|
|
365
370
|
createdTask;
|
|
371
|
+
get createdTaskSoFar() {
|
|
372
|
+
return this.createdTask;
|
|
373
|
+
}
|
|
366
374
|
pushChunk(chunk) {
|
|
367
375
|
this.tail += chunk;
|
|
368
376
|
let idx = this.tail.indexOf(`
|
|
@@ -440,12 +448,14 @@ class NdjsonAssembler {
|
|
|
440
448
|
var CODEX_ITEM_TYPES = Object.freeze({
|
|
441
449
|
AGENT_MESSAGE: "agent_message",
|
|
442
450
|
COMMAND_EXECUTION: "command_execution",
|
|
451
|
+
MCP_TOOL_CALL: "mcp_tool_call",
|
|
443
452
|
ERROR: "error"
|
|
444
453
|
});
|
|
445
454
|
|
|
446
455
|
class CodexStreamDecoder {
|
|
447
456
|
lines = new LineSplitter;
|
|
448
457
|
state = { tools: [], errors: [], turnCompleted: false };
|
|
458
|
+
mcpSeen = new Set;
|
|
449
459
|
pushChunk(chunk) {
|
|
450
460
|
for (const line of this.lines.pushChunk(chunk))
|
|
451
461
|
this.consume(line);
|
|
@@ -495,50 +505,550 @@ class CodexStreamDecoder {
|
|
|
495
505
|
}
|
|
496
506
|
return;
|
|
497
507
|
}
|
|
498
|
-
if (item.type === CODEX_ITEM_TYPES.ERROR) {
|
|
499
|
-
if (typeof item.message === "string" && item.message.length > 0) {
|
|
500
|
-
this.state.errors.push(item.message);
|
|
508
|
+
if (item.type === CODEX_ITEM_TYPES.ERROR) {
|
|
509
|
+
if (typeof item.message === "string" && item.message.length > 0) {
|
|
510
|
+
this.state.errors.push(item.message);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
|
|
515
|
+
const id = item.id;
|
|
516
|
+
if (typeof id === "string" && id && this.state.tools.includes(id)) {} else {
|
|
517
|
+
this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
|
|
518
|
+
}
|
|
519
|
+
if (completed && this.state.createdTask === undefined) {
|
|
520
|
+
this.state.createdTask = extractCreatedTask(item.aggregated_output);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (item.type === CODEX_ITEM_TYPES.MCP_TOOL_CALL) {
|
|
525
|
+
const id = item.id;
|
|
526
|
+
if (typeof id === "string" && id) {
|
|
527
|
+
if (this.mcpSeen.has(id))
|
|
528
|
+
return;
|
|
529
|
+
this.mcpSeen.add(id);
|
|
530
|
+
}
|
|
531
|
+
const name = typeof item.tool === "string" && item.tool ? item.tool : undefined;
|
|
532
|
+
this.state.tools.push(name ?? (typeof id === "string" && id ? id : `mcp-${this.state.tools.length}`));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
toResult() {
|
|
537
|
+
const result = {
|
|
538
|
+
text: this.state.text ?? ""
|
|
539
|
+
};
|
|
540
|
+
if (this.state.sessionId)
|
|
541
|
+
result.sessionId = this.state.sessionId;
|
|
542
|
+
if (this.state.tools.length > 0) {
|
|
543
|
+
result.toolCalls = this.state.tools.length;
|
|
544
|
+
result.tools = [...this.state.tools];
|
|
545
|
+
}
|
|
546
|
+
if (this.state.createdTask)
|
|
547
|
+
result.createdTask = this.state.createdTask;
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ../../../packages/cli/src/daemon/runners/codex-sessions.ts
|
|
553
|
+
function createCodexSessionRegistry() {
|
|
554
|
+
const sessions = new Map;
|
|
555
|
+
return {
|
|
556
|
+
get: (key) => sessions.get(key),
|
|
557
|
+
set: (key, id) => {
|
|
558
|
+
sessions.set(key, id);
|
|
559
|
+
},
|
|
560
|
+
delete: (key) => {
|
|
561
|
+
sessions.delete(key);
|
|
562
|
+
},
|
|
563
|
+
size: () => sessions.size
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ../../../packages/cli/src/daemon/runners/claude-agents.ts
|
|
568
|
+
var CLAUDE_ROY_AGENT_NAME = "roy";
|
|
569
|
+
var ROY_AGENT_DESCRIPTION = "Roy — task-as-first-class-citizen agent. Uses the roy-agent task lifecycle " + "(task_tree → task_create → task_update → task_operation_create → task_complete) " + "so every piece of work is tracked in the local task store.";
|
|
570
|
+
var ROY_TASK_MCP_SERVER = "roy-task";
|
|
571
|
+
var ROY_TASK_MCP_TOOLS = Object.freeze([
|
|
572
|
+
"task_create",
|
|
573
|
+
"task_update",
|
|
574
|
+
"task_operation_create",
|
|
575
|
+
"task_complete",
|
|
576
|
+
"task_get",
|
|
577
|
+
"task_list",
|
|
578
|
+
"task_tree",
|
|
579
|
+
"task_search"
|
|
580
|
+
].map((t) => `mcp__${ROY_TASK_MCP_SERVER}__${t}`));
|
|
581
|
+
var CLAUDE_ROY_TOOLS = Object.freeze([
|
|
582
|
+
"Read",
|
|
583
|
+
"Write",
|
|
584
|
+
"Edit",
|
|
585
|
+
"Bash",
|
|
586
|
+
"Grep",
|
|
587
|
+
"Glob",
|
|
588
|
+
"TodoWrite",
|
|
589
|
+
"WebFetch",
|
|
590
|
+
"WebSearch",
|
|
591
|
+
...ROY_TASK_MCP_TOOLS
|
|
592
|
+
]);
|
|
593
|
+
var CLAUDE_ROY_SYSTEM_PROMPT = `# Roy — task-first-class-citizen agent
|
|
594
|
+
|
|
595
|
+
You are **Roy**, an agent whose defining rule is simple:
|
|
596
|
+
|
|
597
|
+
> **Task is a first-class citizen. No task, no work.**
|
|
598
|
+
|
|
599
|
+
You are not a chat assistant that occasionally writes notes. You are a worker
|
|
600
|
+
whose output is a *visible, trackable* stream of tasks. Every non-trivial
|
|
601
|
+
request (more than one tool call / more than a couple of minutes of work) MUST
|
|
602
|
+
become a tracked task before you start doing it, and MUST be closed out
|
|
603
|
+
explicitly when you finish.
|
|
604
|
+
|
|
605
|
+
## The lifecycle (non-negotiable)
|
|
606
|
+
|
|
607
|
+
### 1. Orient — \`task_tree(depth=1)\` (or \`depth=2\` for bigger trees)
|
|
608
|
+
Before creating anything, look at the existing tree. Decide:
|
|
609
|
+
- does this work belong to an existing root (attach via \`parent_task_id\`)?
|
|
610
|
+
- or is it genuinely new?
|
|
611
|
+
|
|
612
|
+
Keep **root tasks ≤ 10**. Prefer attaching to an existing thematic root over
|
|
613
|
+
spawning a new root. Never hardcode ids — read them from \`task_tree\`.
|
|
614
|
+
|
|
615
|
+
### 2. Create — \`task_create\`
|
|
616
|
+
Create the task **before** doing the work. Pass:
|
|
617
|
+
- \`title\` (imperative, specific), \`description\`, \`goals_and_expected_deliverables\`
|
|
618
|
+
- \`priority\` (low / medium / high), \`type\` (normal / cycle / longterm)
|
|
619
|
+
- \`project_path\`, \`context\` (JSON string: worktree_path / branch / type …)
|
|
620
|
+
- \`parent_task_id\` when the tree said so
|
|
621
|
+
|
|
622
|
+
The tool returns a **numeric task id** — that number is your handle for the
|
|
623
|
+
rest of the conversation.
|
|
624
|
+
|
|
625
|
+
### 3. Track — \`task_update\`
|
|
626
|
+
Report progress as you go, not only at the end:
|
|
627
|
+
- \`status\`: todo → active → (paused / cancelled) → completed
|
|
628
|
+
- \`progress\`: 0–100
|
|
629
|
+
- \`current_status\`: one human sentence about where you are right now
|
|
630
|
+
|
|
631
|
+
If the scope changes, \`task_update\` the description/goals instead of silently
|
|
632
|
+
doing something else.
|
|
633
|
+
|
|
634
|
+
### 4. Record milestones — \`task_operation_create\`
|
|
635
|
+
This tool has **two orthogonal dimensions** and you must keep them distinct:
|
|
636
|
+
- **state / milestone dimension** — \`milestone_type\` + \`milestone_title\` +
|
|
637
|
+
\`milestone_description\`: *what* was reached (e.g. "GREEN: all tests pass").
|
|
638
|
+
- **process dimension** — \`process_description\`: *how* you got there —
|
|
639
|
+
strategy + tool steps, strictly **100–150 characters**.
|
|
640
|
+
Too short = rejected (no value); too long = rejected (use the session log).
|
|
641
|
+
|
|
642
|
+
Rules: do **not** use the process field to restate the milestone, and do not
|
|
643
|
+
use milestones as a substitute for progress. Milestones mark *state
|
|
644
|
+
transitions*: RED→GREEN, blocker found, decision made, review passed.
|
|
645
|
+
|
|
646
|
+
### 5. Close — \`task_complete\`
|
|
647
|
+
Before completing:
|
|
648
|
+
1. \`task_get(task_id)\` and re-read goal + delivery.
|
|
649
|
+
2. Verify the goal and delivery actually match what you delivered. If they do
|
|
650
|
+
not, either extend the task (\`task_update\`) or say so honestly.
|
|
651
|
+
3. Call \`task_complete\` with \`deliverable_storage_path\` — a locator for the
|
|
652
|
+
work product: a file path, a git branch/worktree, a doc URL, a commit URL.
|
|
653
|
+
The path is persisted into the task context and as an operation record.
|
|
654
|
+
|
|
655
|
+
\`task_complete\` cannot be used on \`cycle\` tasks (they are periodic by
|
|
656
|
+
design) — use \`task_update\` to reset those.
|
|
657
|
+
|
|
658
|
+
## Other tools
|
|
659
|
+
|
|
660
|
+
- \`task_get(task_id, include_operations)\` — full detail; use it before
|
|
661
|
+
resuming or completing a task.
|
|
662
|
+
- \`task_list(...)\` — flat listing with filters (status / priority / type).
|
|
663
|
+
- \`task_search(keywords)\` — find by title / description / goals / tags.
|
|
664
|
+
**Search before creating**, to avoid duplicates.
|
|
665
|
+
|
|
666
|
+
## Where tasks live
|
|
667
|
+
|
|
668
|
+
All tasks and operations are persisted by the roy-task-plugin in the
|
|
669
|
+
**single** project store — locally that is SQLite at
|
|
670
|
+
\`~/.roy-task-plugin/tasks.db\` (or the remote service when the plugin is
|
|
671
|
+
configured with \`persistence: "remote"\`). Do not create side databases, do
|
|
672
|
+
not maintain your own to-do list in files, and do not track work in chat only:
|
|
673
|
+
if it is not in the task store, it is not tracked.
|
|
674
|
+
|
|
675
|
+
## Tool surface on Claude Code
|
|
676
|
+
|
|
677
|
+
The \`task_*\` tools are served by the **\`roy-task\` MCP server** this runtime
|
|
678
|
+
loads. Call them directly:
|
|
679
|
+
|
|
680
|
+
| lifecycle step | tool |
|
|
681
|
+
|---|---|
|
|
682
|
+
| \`task_tree(depth=1)\` | \`mcp__roy-task__task_tree\` |
|
|
683
|
+
| \`task_create\` | \`mcp__roy-task__task_create\` |
|
|
684
|
+
| \`task_update\` | \`mcp__roy-task__task_update\` |
|
|
685
|
+
| \`task_operation_create\` | \`mcp__roy-task__task_operation_create\` |
|
|
686
|
+
| \`task_complete\` | \`mcp__roy-task__task_complete\` |
|
|
687
|
+
| \`task_get\` | \`mcp__roy-task__task_get\` |
|
|
688
|
+
| \`task_list\` | \`mcp__roy-task__task_list\` |
|
|
689
|
+
| \`task_search\` | \`mcp__roy-task__task_search\` |
|
|
690
|
+
|
|
691
|
+
Same store, same ids, same semantics as the opencode tools — the daemon's
|
|
692
|
+
progress card renders the task these tools create.
|
|
693
|
+
|
|
694
|
+
⚠️ **Do not** shell out to the \`roy-agent tasks\` CLI as a substitute.
|
|
695
|
+
Measured: it has no way to write a milestone (its \`operations <id>\` subcommand
|
|
696
|
+
is a *reader*), and it targets a different database than this server. Use the
|
|
697
|
+
MCP tools above; if they are genuinely unavailable, say so in your answer
|
|
698
|
+
instead of silently skipping the lifecycle.
|
|
699
|
+
|
|
700
|
+
## Style
|
|
701
|
+
|
|
702
|
+
- Be direct and concrete; the user is reading task titles and milestones.
|
|
703
|
+
- Report the task id and its state whenever you finish a step.
|
|
704
|
+
- If a task is blocked, mark it (\`task_update(status="paused")\`) and record a
|
|
705
|
+
\`problem\` milestone explaining the blocker instead of going silent.`;
|
|
706
|
+
function buildClaudeAgentsJson(opts = {}) {
|
|
707
|
+
const def = {
|
|
708
|
+
description: opts.description ?? ROY_AGENT_DESCRIPTION,
|
|
709
|
+
prompt: opts.prompt ?? CLAUDE_ROY_SYSTEM_PROMPT,
|
|
710
|
+
tools: [...opts.tools ?? CLAUDE_ROY_TOOLS]
|
|
711
|
+
};
|
|
712
|
+
if (opts.model)
|
|
713
|
+
def.model = opts.model;
|
|
714
|
+
return JSON.stringify({ [opts.name ?? CLAUDE_ROY_AGENT_NAME]: def });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
718
|
+
import { createHash } from "node:crypto";
|
|
719
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
720
|
+
import { dirname, join } from "node:path";
|
|
721
|
+
import { fileURLToPath } from "node:url";
|
|
722
|
+
|
|
723
|
+
// ../../../packages/cli/src/daemon/runners/claude-sessions.ts
|
|
724
|
+
function createClaudeSessionRegistry() {
|
|
725
|
+
const sessions = new Map;
|
|
726
|
+
return {
|
|
727
|
+
get: (key) => sessions.get(key),
|
|
728
|
+
set: (key, id) => {
|
|
729
|
+
sessions.set(key, id);
|
|
730
|
+
},
|
|
731
|
+
delete: (key) => {
|
|
732
|
+
sessions.delete(key);
|
|
733
|
+
},
|
|
734
|
+
size: () => sessions.size
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ../../../packages/cli/src/daemon/runners/claude-stream.ts
|
|
739
|
+
function errorDetail(result) {
|
|
740
|
+
if (typeof result.result === "string" && result.result.trim() !== "") {
|
|
741
|
+
return result.result.trim();
|
|
742
|
+
}
|
|
743
|
+
if (Array.isArray(result.errors)) {
|
|
744
|
+
const parts = result.errors.map((e) => typeof e === "string" ? e.trim() : "").filter((e) => e !== "");
|
|
745
|
+
if (parts.length > 0)
|
|
746
|
+
return parts.join("; ");
|
|
747
|
+
}
|
|
748
|
+
return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
|
|
749
|
+
}
|
|
750
|
+
function flattenToolResult(content) {
|
|
751
|
+
if (typeof content === "string")
|
|
752
|
+
return content;
|
|
753
|
+
if (!Array.isArray(content))
|
|
754
|
+
return "";
|
|
755
|
+
const parts = [];
|
|
756
|
+
for (const block of content) {
|
|
757
|
+
if (typeof block === "string") {
|
|
758
|
+
parts.push(block);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (typeof block === "object" && block !== null) {
|
|
762
|
+
const t = block.text;
|
|
763
|
+
if (typeof t === "string")
|
|
764
|
+
parts.push(t);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return parts.join(`
|
|
768
|
+
`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
class ClaudeStreamDecoder {
|
|
772
|
+
lines = new LineSplitter;
|
|
773
|
+
state = {
|
|
774
|
+
assistantText: [],
|
|
775
|
+
tools: [],
|
|
776
|
+
pendingCreates: new Map
|
|
777
|
+
};
|
|
778
|
+
pushChunk(chunk) {
|
|
779
|
+
for (const line of this.lines.pushChunk(chunk))
|
|
780
|
+
this.consume(line);
|
|
781
|
+
}
|
|
782
|
+
finish() {
|
|
783
|
+
for (const line of this.lines.finish())
|
|
784
|
+
this.consume(line);
|
|
785
|
+
return this.toResult();
|
|
786
|
+
}
|
|
787
|
+
snapshot() {
|
|
788
|
+
return { ...this.state, assistantText: [...this.state.assistantText], tools: [...this.state.tools] };
|
|
789
|
+
}
|
|
790
|
+
consume(rawLine) {
|
|
791
|
+
const line = rawLine.trim();
|
|
792
|
+
if (!line.startsWith("{"))
|
|
793
|
+
return;
|
|
794
|
+
let event;
|
|
795
|
+
try {
|
|
796
|
+
event = JSON.parse(line);
|
|
797
|
+
} catch {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (typeof event !== "object" || event === null)
|
|
801
|
+
return;
|
|
802
|
+
const ev = event;
|
|
803
|
+
const sessionId = typeof ev.session_id === "string" ? ev.session_id : undefined;
|
|
804
|
+
if (sessionId && !this.state.sessionId)
|
|
805
|
+
this.state.sessionId = sessionId;
|
|
806
|
+
if (ev.type === "assistant") {
|
|
807
|
+
this.consumeAssistant(ev);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (ev.type === "user") {
|
|
811
|
+
this.consumeUser(ev);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (ev.type === "result") {
|
|
815
|
+
this.state.result = ev;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
consumeUser(ev) {
|
|
819
|
+
const message = ev.message;
|
|
820
|
+
const content = message?.content;
|
|
821
|
+
if (!Array.isArray(content))
|
|
822
|
+
return;
|
|
823
|
+
for (const block of content) {
|
|
824
|
+
if (typeof block !== "object" || block === null)
|
|
825
|
+
continue;
|
|
826
|
+
const b = block;
|
|
827
|
+
if (b.type !== "tool_result")
|
|
828
|
+
continue;
|
|
829
|
+
const id = typeof b.tool_use_id === "string" ? b.tool_use_id : undefined;
|
|
830
|
+
const pending = id ? this.state.pendingCreates.get(id) : undefined;
|
|
831
|
+
const text = flattenToolResult(b.content);
|
|
832
|
+
if (this.state.createdTask !== undefined)
|
|
833
|
+
continue;
|
|
834
|
+
if (pending) {
|
|
835
|
+
this.state.pendingCreates.delete(id);
|
|
836
|
+
this.state.createdTask = extractCreatedTask(text, pending.titleHint);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
this.state.createdTask = extractCreatedTask(text);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
consumeAssistant(ev) {
|
|
843
|
+
const message = ev.message;
|
|
844
|
+
const content = message?.content;
|
|
845
|
+
if (!Array.isArray(content))
|
|
846
|
+
return;
|
|
847
|
+
for (const block of content) {
|
|
848
|
+
if (typeof block !== "object" || block === null)
|
|
849
|
+
continue;
|
|
850
|
+
const b = block;
|
|
851
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
852
|
+
this.state.assistantText.push(b.text);
|
|
853
|
+
} else if (b.type === "tool_use") {
|
|
854
|
+
if (typeof b.name === "string") {
|
|
855
|
+
this.state.tools.push(b.name);
|
|
856
|
+
if (isTaskCreateTool(b.name) && typeof b.id === "string") {
|
|
857
|
+
const input = b.input;
|
|
858
|
+
const title = input?.["title"];
|
|
859
|
+
this.state.pendingCreates.set(b.id, {
|
|
860
|
+
tool: b.name,
|
|
861
|
+
titleHint: typeof title === "string" ? title : undefined
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
toResult() {
|
|
869
|
+
const { result } = this.state;
|
|
870
|
+
const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
|
|
871
|
+
const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
|
|
872
|
+
const createdTask = this.state.createdTask;
|
|
873
|
+
if (result?.is_error) {
|
|
874
|
+
const detail = errorDetail(result);
|
|
875
|
+
return {
|
|
876
|
+
text: "",
|
|
877
|
+
sessionId: this.state.sessionId,
|
|
878
|
+
toolCalls,
|
|
879
|
+
tools,
|
|
880
|
+
createdTask,
|
|
881
|
+
error: detail.slice(0, 1000)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (typeof result?.result === "string" && result.result.trim() !== "") {
|
|
885
|
+
return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools, createdTask };
|
|
886
|
+
}
|
|
887
|
+
const fallback = this.state.assistantText.join("").trim();
|
|
888
|
+
return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools, createdTask };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
893
|
+
var DEFAULT_CLAUDE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
894
|
+
var CLAUDE_STDIO = Object.freeze([
|
|
895
|
+
"ignore",
|
|
896
|
+
"pipe",
|
|
897
|
+
"pipe"
|
|
898
|
+
]);
|
|
899
|
+
function buildClaudeArgs(input) {
|
|
900
|
+
const args = ["--print", "--output-format", "stream-json", "--verbose"];
|
|
901
|
+
if (input.agentsJson)
|
|
902
|
+
args.push("--agents", input.agentsJson);
|
|
903
|
+
if (input.pluginDir)
|
|
904
|
+
args.push("--plugin-dir", input.pluginDir);
|
|
905
|
+
if (input.mcpConfig)
|
|
906
|
+
args.push(`--mcp-config=${input.mcpConfig}`);
|
|
907
|
+
args.push("--agent", input.agent ?? "roy");
|
|
908
|
+
args.push("--dangerously-skip-permissions");
|
|
909
|
+
if (input.sessionId)
|
|
910
|
+
args.push("--resume", claudeSessionUuid(input.sessionId));
|
|
911
|
+
args.push(input.prompt);
|
|
912
|
+
return args;
|
|
913
|
+
}
|
|
914
|
+
function resolveClaudePluginDir(roots, explicit) {
|
|
915
|
+
if (explicit)
|
|
916
|
+
return explicit;
|
|
917
|
+
for (const root of roots) {
|
|
918
|
+
const candidate = join(root, "claude-plugin");
|
|
919
|
+
if (existsSync(join(candidate, ".mcp.json")))
|
|
920
|
+
return candidate;
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
function buildRoyTaskMcpConfig(pluginDir) {
|
|
925
|
+
return JSON.stringify({
|
|
926
|
+
mcpServers: {
|
|
927
|
+
"roy-task": {
|
|
928
|
+
command: process.execPath,
|
|
929
|
+
args: [join(pluginDir, "mcp", "roy-task-mcp-server.mjs")]
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function isClaudeSessionUuid(value) {
|
|
935
|
+
if (!value)
|
|
936
|
+
return false;
|
|
937
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
938
|
+
}
|
|
939
|
+
function claudeSessionUuid(sessionKey) {
|
|
940
|
+
if (isClaudeSessionUuid(sessionKey))
|
|
941
|
+
return sessionKey;
|
|
942
|
+
const digest = createHash("sha256").update(sessionKey).digest("hex");
|
|
943
|
+
const h = digest.slice(0, 32);
|
|
944
|
+
return [
|
|
945
|
+
h.slice(0, 8),
|
|
946
|
+
h.slice(8, 12),
|
|
947
|
+
`4${h.slice(13, 16)}`,
|
|
948
|
+
`8${h.slice(17, 20)}`,
|
|
949
|
+
h.slice(20, 32)
|
|
950
|
+
].join("-");
|
|
951
|
+
}
|
|
952
|
+
function makeClaudeRunner(opts = {}) {
|
|
953
|
+
const bin = opts.bin ?? "claude";
|
|
954
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_CLAUDE_TIMEOUT_MS;
|
|
955
|
+
const agentsJson = opts.agentsJson ?? buildClaudeAgentsJson();
|
|
956
|
+
const sessions = opts.sessions ?? createClaudeSessionRegistry();
|
|
957
|
+
const pluginDir = opts.pluginDir ?? resolveClaudePluginDir([dirname(fileURLToPath(import.meta.url))]);
|
|
958
|
+
const mcpConfig = pluginDir ? buildRoyTaskMcpConfig(pluginDir) : undefined;
|
|
959
|
+
return async function runAgent(input) {
|
|
960
|
+
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
961
|
+
const args = buildClaudeArgs({
|
|
962
|
+
prompt: input.prompt,
|
|
963
|
+
agent: input.agent,
|
|
964
|
+
agentsJson,
|
|
965
|
+
...pluginDir ? { pluginDir } : {},
|
|
966
|
+
...mcpConfig ? { mcpConfig } : {},
|
|
967
|
+
sessionId: known
|
|
968
|
+
});
|
|
969
|
+
const decoder = new ClaudeStreamDecoder;
|
|
970
|
+
let stderr = "";
|
|
971
|
+
let reported = false;
|
|
972
|
+
const notifyProgress = () => {
|
|
973
|
+
if (reported || !input.onProgress)
|
|
974
|
+
return;
|
|
975
|
+
const created = decoder.snapshot().createdTask;
|
|
976
|
+
if (!created)
|
|
977
|
+
return;
|
|
978
|
+
reported = true;
|
|
979
|
+
try {
|
|
980
|
+
input.onProgress({ createdTask: created });
|
|
981
|
+
} catch {}
|
|
982
|
+
};
|
|
983
|
+
let handle;
|
|
984
|
+
try {
|
|
985
|
+
handle = spawnStreaming([bin, ...args], {
|
|
986
|
+
stdin: CLAUDE_STDIO[0],
|
|
987
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
988
|
+
timeoutMs,
|
|
989
|
+
onStdout: (chunk) => {
|
|
990
|
+
decoder.pushChunk(chunk);
|
|
991
|
+
notifyProgress();
|
|
992
|
+
if (process.env["ROY_TASK_DUMP_STREAM"]) {
|
|
993
|
+
try {
|
|
994
|
+
appendFileSync(process.env["ROY_TASK_DUMP_STREAM"], chunk);
|
|
995
|
+
} catch {}
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
onStderr: (chunk) => {
|
|
999
|
+
if (stderr.length < 64 * 1024)
|
|
1000
|
+
stderr += chunk;
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
1005
|
+
}
|
|
1006
|
+
let code;
|
|
1007
|
+
try {
|
|
1008
|
+
code = await handle.exited;
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
if (isMissingBinary(err)) {
|
|
1011
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
501
1012
|
}
|
|
502
|
-
return;
|
|
1013
|
+
return { text: "", error: errMsg(err) };
|
|
503
1014
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1015
|
+
const result = decoder.finish();
|
|
1016
|
+
const stderrText = stderr.trim();
|
|
1017
|
+
const combined = result.error ? stderrText && !result.error.includes(stderrText) ? `${result.error} | ${stderrText}` : result.error : undefined;
|
|
1018
|
+
const effective = combined ? { ...result, error: combined.slice(0, 1000) } : result;
|
|
1019
|
+
if (input.sessionId && effective.sessionId) {
|
|
1020
|
+
sessions.set(input.sessionId, effective.sessionId);
|
|
1021
|
+
}
|
|
1022
|
+
if (code === 124) {
|
|
1023
|
+
return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
|
|
1024
|
+
}
|
|
1025
|
+
if (effective.error) {
|
|
1026
|
+
if (input.sessionId && STALE_SESSION_RE.test(effective.error)) {
|
|
1027
|
+
sessions.delete(input.sessionId);
|
|
508
1028
|
}
|
|
509
|
-
|
|
510
|
-
|
|
1029
|
+
return effective;
|
|
1030
|
+
}
|
|
1031
|
+
if (code !== 0 && !effective.text) {
|
|
1032
|
+
const error = `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`;
|
|
1033
|
+
if (input.sessionId && STALE_SESSION_RE.test(error)) {
|
|
1034
|
+
sessions.delete(input.sessionId);
|
|
511
1035
|
}
|
|
1036
|
+
return {
|
|
1037
|
+
text: "",
|
|
1038
|
+
toolCalls: effective.toolCalls,
|
|
1039
|
+
tools: effective.tools,
|
|
1040
|
+
error
|
|
1041
|
+
};
|
|
512
1042
|
}
|
|
513
|
-
|
|
514
|
-
toResult() {
|
|
515
|
-
const result = {
|
|
516
|
-
text: this.state.text ?? ""
|
|
517
|
-
};
|
|
518
|
-
if (this.state.sessionId)
|
|
519
|
-
result.sessionId = this.state.sessionId;
|
|
520
|
-
if (this.state.tools.length > 0)
|
|
521
|
-
result.toolCalls = this.state.tools.length;
|
|
522
|
-
if (this.state.createdTask)
|
|
523
|
-
result.createdTask = this.state.createdTask;
|
|
524
|
-
return result;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ../../../packages/cli/src/daemon/runners/codex-sessions.ts
|
|
529
|
-
function createCodexSessionRegistry() {
|
|
530
|
-
const sessions = new Map;
|
|
531
|
-
return {
|
|
532
|
-
get: (key) => sessions.get(key),
|
|
533
|
-
set: (key, id) => {
|
|
534
|
-
sessions.set(key, id);
|
|
535
|
-
},
|
|
536
|
-
delete: (key) => {
|
|
537
|
-
sessions.delete(key);
|
|
538
|
-
},
|
|
539
|
-
size: () => sessions.size
|
|
1043
|
+
return effective;
|
|
540
1044
|
};
|
|
541
1045
|
}
|
|
1046
|
+
var STALE_SESSION_RE = /No conversation found|not a UUID|session title/i;
|
|
1047
|
+
function errMsg(err) {
|
|
1048
|
+
if (err instanceof Error)
|
|
1049
|
+
return err.message;
|
|
1050
|
+
return String(err);
|
|
1051
|
+
}
|
|
542
1052
|
|
|
543
1053
|
// ../../../packages/cli/src/daemon/runners/codex.ts
|
|
544
1054
|
var DEFAULT_CODEX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -555,12 +1065,44 @@ var CODEX_SANDBOX_MODES = Object.freeze([
|
|
|
555
1065
|
function isCodexSandboxMode(value) {
|
|
556
1066
|
return CODEX_SANDBOX_MODES.includes(value);
|
|
557
1067
|
}
|
|
1068
|
+
function buildRoyTaskCodexConfigArgs(pluginDir) {
|
|
1069
|
+
const args = [
|
|
1070
|
+
`-cmcp_servers.${ROY_TASK_MCP_SERVER}.command=${JSON.stringify(process.execPath)}`,
|
|
1071
|
+
`-cmcp_servers.${ROY_TASK_MCP_SERVER}.args=${JSON.stringify([
|
|
1072
|
+
join2(pluginDir, "mcp", "roy-task-mcp-server.mjs")
|
|
1073
|
+
])}`,
|
|
1074
|
+
`-cmcp_servers.${ROY_TASK_MCP_SERVER}.default_tools_approval_mode="approve"`
|
|
1075
|
+
];
|
|
1076
|
+
const envArg = buildMcpServerEnvArg();
|
|
1077
|
+
if (envArg)
|
|
1078
|
+
args.push(envArg);
|
|
1079
|
+
return args;
|
|
1080
|
+
}
|
|
1081
|
+
function buildMcpServerEnvArg() {
|
|
1082
|
+
const keys = [
|
|
1083
|
+
"ROY_TASK_NOTIFY_ENABLED",
|
|
1084
|
+
"ROY_TASK_NOTIFY_CHAT_ID",
|
|
1085
|
+
"ROY_TASK_NOTIFY_PROFILE"
|
|
1086
|
+
];
|
|
1087
|
+
const pairs = [];
|
|
1088
|
+
for (const key of keys) {
|
|
1089
|
+
const value = process.env[key];
|
|
1090
|
+
if (!value)
|
|
1091
|
+
continue;
|
|
1092
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
1093
|
+
pairs.push(`${key}="${escaped}"`);
|
|
1094
|
+
}
|
|
1095
|
+
if (pairs.length === 0)
|
|
1096
|
+
return;
|
|
1097
|
+
return `-cmcp_servers.${ROY_TASK_MCP_SERVER}.env={${pairs.join(",")}}`;
|
|
1098
|
+
}
|
|
558
1099
|
function buildCodexArgs(input) {
|
|
559
1100
|
const args = ["exec", "--json"];
|
|
560
1101
|
if (input.model)
|
|
561
1102
|
args.push("--model", input.model);
|
|
562
1103
|
args.push("--skip-git-repo-check");
|
|
563
1104
|
args.push("-s", input.sandbox ?? "read-only");
|
|
1105
|
+
args.push(...input.mcpConfigArgs ?? []);
|
|
564
1106
|
args.push(input.prompt);
|
|
565
1107
|
return args;
|
|
566
1108
|
}
|
|
@@ -572,6 +1114,7 @@ function buildCodexResumeArgs(sessionId, input) {
|
|
|
572
1114
|
if (input.sandbox === "danger-full-access") {
|
|
573
1115
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
574
1116
|
}
|
|
1117
|
+
args.push(...input.mcpConfigArgs ?? []);
|
|
575
1118
|
args.push(sessionId);
|
|
576
1119
|
args.push(input.prompt);
|
|
577
1120
|
return args;
|
|
@@ -582,10 +1125,13 @@ function makeCodexRunner(opts = {}) {
|
|
|
582
1125
|
const model = opts.model;
|
|
583
1126
|
const sandbox = opts.sandbox ?? "read-only";
|
|
584
1127
|
const sessions = opts.sessions ?? createCodexSessionRegistry();
|
|
1128
|
+
const pluginDir = opts.pluginDir ?? resolveClaudePluginDir([dirname2(fileURLToPath2(import.meta.url))]);
|
|
1129
|
+
const mcpConfigArgs = pluginDir ? buildRoyTaskCodexConfigArgs(pluginDir) : undefined;
|
|
585
1130
|
return async function runAgent(input) {
|
|
586
1131
|
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
587
|
-
const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox }) : buildCodexArgs({ prompt: input.prompt, model, sandbox });
|
|
1132
|
+
const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox, mcpConfigArgs }) : buildCodexArgs({ prompt: input.prompt, model, sandbox, mcpConfigArgs });
|
|
588
1133
|
const decoder = new CodexStreamDecoder;
|
|
1134
|
+
let reported = false;
|
|
589
1135
|
let stderr = "";
|
|
590
1136
|
let handle;
|
|
591
1137
|
try {
|
|
@@ -593,23 +1139,34 @@ function makeCodexRunner(opts = {}) {
|
|
|
593
1139
|
stdin: CODEX_STDIO[0],
|
|
594
1140
|
env: { ...process.env, NO_COLOR: "1" },
|
|
595
1141
|
timeoutMs,
|
|
596
|
-
onStdout: (chunk) =>
|
|
1142
|
+
onStdout: (chunk) => {
|
|
1143
|
+
decoder.pushChunk(chunk);
|
|
1144
|
+
if (!reported && input.onProgress) {
|
|
1145
|
+
const created = decoder.snapshot().createdTask;
|
|
1146
|
+
if (created) {
|
|
1147
|
+
reported = true;
|
|
1148
|
+
try {
|
|
1149
|
+
input.onProgress({ createdTask: created });
|
|
1150
|
+
} catch {}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
597
1154
|
onStderr: (chunk) => {
|
|
598
1155
|
if (stderr.length < 64 * 1024)
|
|
599
1156
|
stderr += chunk;
|
|
600
1157
|
}
|
|
601
1158
|
});
|
|
602
1159
|
} catch (err) {
|
|
603
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1160
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
604
1161
|
}
|
|
605
1162
|
let code;
|
|
606
1163
|
try {
|
|
607
1164
|
code = await handle.exited;
|
|
608
1165
|
} catch (err) {
|
|
609
1166
|
if (isMissingBinary(err)) {
|
|
610
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1167
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
611
1168
|
}
|
|
612
|
-
return { text: "", error:
|
|
1169
|
+
return { text: "", error: errMsg2(err) };
|
|
613
1170
|
}
|
|
614
1171
|
const result = decoder.finish();
|
|
615
1172
|
if (input.sessionId && result.sessionId && !result.error) {
|
|
@@ -632,7 +1189,7 @@ function makeCodexRunner(opts = {}) {
|
|
|
632
1189
|
return result;
|
|
633
1190
|
};
|
|
634
1191
|
}
|
|
635
|
-
function
|
|
1192
|
+
function errMsg2(err) {
|
|
636
1193
|
if (err instanceof Error)
|
|
637
1194
|
return err.message;
|
|
638
1195
|
return String(err);
|
|
@@ -665,6 +1222,7 @@ var VALUE_FLAGS = new Set([
|
|
|
665
1222
|
"--opencode-bin",
|
|
666
1223
|
"--codex-model",
|
|
667
1224
|
"--codex-sandbox",
|
|
1225
|
+
"--cursor-model",
|
|
668
1226
|
"--max-concurrency",
|
|
669
1227
|
"--timeout-ms"
|
|
670
1228
|
]);
|
|
@@ -785,6 +1343,13 @@ function parseServe(argv) {
|
|
|
785
1343
|
serve.codexModel = v;
|
|
786
1344
|
break;
|
|
787
1345
|
}
|
|
1346
|
+
case "--cursor-model": {
|
|
1347
|
+
const v = take();
|
|
1348
|
+
if (v instanceof Error)
|
|
1349
|
+
return { command: "unknown", error: v.message };
|
|
1350
|
+
serve.cursorModel = v;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
788
1353
|
case "--codex-sandbox": {
|
|
789
1354
|
const v = take();
|
|
790
1355
|
if (v instanceof Error)
|
|
@@ -877,7 +1442,7 @@ function resolveRuntimeBin(options) {
|
|
|
877
1442
|
return;
|
|
878
1443
|
}
|
|
879
1444
|
function usage() {
|
|
880
|
-
return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode, claude or
|
|
1445
|
+
return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode, claude, codex or cursor)
|
|
881
1446
|
|
|
882
1447
|
USAGE
|
|
883
1448
|
roy-task [serve] [options] start the daemon (default command)
|
|
@@ -903,6 +1468,7 @@ SERVE OPTIONS
|
|
|
903
1468
|
--opencode-bin <path> DEPRECATED alias for --command
|
|
904
1469
|
(only applies when --runtime opencode)
|
|
905
1470
|
--codex-model <name> model for codex exec --model (default: codex config)
|
|
1471
|
+
--cursor-model <name> model for cursor-agent --model (default: cursor default)
|
|
906
1472
|
--codex-sandbox <mode> codex exec -s policy (default: read-only)
|
|
907
1473
|
one of: ${CODEX_SANDBOX_MODES.join(" | ")}
|
|
908
1474
|
--max-concurrency <n> max simultaneous agent runs (default: 1)
|
|
@@ -1042,13 +1608,13 @@ class Deduper {
|
|
|
1042
1608
|
}
|
|
1043
1609
|
|
|
1044
1610
|
// ../../../packages/cli/src/daemon/session-map.ts
|
|
1045
|
-
import { createHash } from "node:crypto";
|
|
1611
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1046
1612
|
var DIGEST_LEN = 16;
|
|
1047
1613
|
function deriveSessionId(input) {
|
|
1048
1614
|
const profile = input.profile ?? "";
|
|
1049
1615
|
const chatId = input.chatId ?? "";
|
|
1050
1616
|
const material = `${profile}\x00${chatId}`;
|
|
1051
|
-
const digest =
|
|
1617
|
+
const digest = createHash2("sha256").update(material).digest("hex");
|
|
1052
1618
|
return `ses_${digest.slice(0, DIGEST_LEN)}`;
|
|
1053
1619
|
}
|
|
1054
1620
|
|
|
@@ -1238,6 +1804,7 @@ var TOOL_LABELS = {
|
|
|
1238
1804
|
write: "写入文件",
|
|
1239
1805
|
edit: "修改文件",
|
|
1240
1806
|
bash: "执行命令",
|
|
1807
|
+
runTerminal: "执行命令",
|
|
1241
1808
|
glob: "搜索文件",
|
|
1242
1809
|
grep: "搜索内容",
|
|
1243
1810
|
task: "调用子任务",
|
|
@@ -1250,11 +1817,23 @@ var TOOL_LABELS = {
|
|
|
1250
1817
|
};
|
|
1251
1818
|
function describeToolUse(tool) {
|
|
1252
1819
|
return {
|
|
1253
|
-
title: TOOL_LABELS[tool] ?? tool,
|
|
1820
|
+
title: TOOL_LABELS[normaliseMcpToolName(tool)] ?? tool,
|
|
1254
1821
|
timestamp: new Date().toISOString(),
|
|
1255
1822
|
category: tool
|
|
1256
1823
|
};
|
|
1257
1824
|
}
|
|
1825
|
+
function normaliseMcpToolName(tool) {
|
|
1826
|
+
const mcp = /^mcp__[^_]+(?:_[^_]+)*__(.+)$/.exec(tool);
|
|
1827
|
+
if (mcp)
|
|
1828
|
+
return mcp[1];
|
|
1829
|
+
const dash = /^([a-z0-9]+(?:-[a-z0-9]+)*)-([a-z0-9]+_[a-z0-9_]*)$/.exec(tool);
|
|
1830
|
+
if (dash)
|
|
1831
|
+
return dash[2];
|
|
1832
|
+
const tag = /^([a-z][A-Za-z0-9]*)ToolCall$/.exec(tool);
|
|
1833
|
+
if (tag)
|
|
1834
|
+
return tag[1];
|
|
1835
|
+
return tool;
|
|
1836
|
+
}
|
|
1258
1837
|
|
|
1259
1838
|
// ../../../packages/cli/src/daemon/dispatcher.ts
|
|
1260
1839
|
function cardReport(handle) {
|
|
@@ -1366,18 +1945,31 @@ class Dispatcher {
|
|
|
1366
1945
|
return { status: "dry-run", plan };
|
|
1367
1946
|
}
|
|
1368
1947
|
const run = async () => {
|
|
1369
|
-
|
|
1948
|
+
let earlyCard;
|
|
1949
|
+
const onProgress = (progress) => {
|
|
1950
|
+
if (earlyCard || !progress.createdTask)
|
|
1951
|
+
return;
|
|
1952
|
+
earlyCard = this.trace("dispatch.openCard", () => this.openCard({
|
|
1953
|
+
chatId,
|
|
1954
|
+
messageId,
|
|
1955
|
+
profile,
|
|
1956
|
+
createdTask: progress.createdTask,
|
|
1957
|
+
requestText: extractUserText(raw)
|
|
1958
|
+
}));
|
|
1959
|
+
};
|
|
1960
|
+
const result = await this.traceResult("dispatch.runAgent", async () => this.deps.runAgent({
|
|
1370
1961
|
prompt: plan.prompt,
|
|
1371
1962
|
agent: plan.agent,
|
|
1372
|
-
sessionId: plan.sessionId
|
|
1373
|
-
|
|
1374
|
-
|
|
1963
|
+
sessionId: plan.sessionId,
|
|
1964
|
+
onProgress
|
|
1965
|
+
}), (r) => r.error);
|
|
1966
|
+
const card = earlyCard ? await earlyCard : await this.trace("dispatch.openCard", () => this.openCard({
|
|
1375
1967
|
chatId,
|
|
1376
1968
|
messageId,
|
|
1377
1969
|
profile,
|
|
1378
1970
|
createdTask: result.createdTask,
|
|
1379
1971
|
requestText: extractUserText(raw)
|
|
1380
|
-
});
|
|
1972
|
+
}));
|
|
1381
1973
|
if (card.messageId) {
|
|
1382
1974
|
await this.patchCard(card, {
|
|
1383
1975
|
chatId,
|
|
@@ -1469,7 +2061,8 @@ class Dispatcher {
|
|
|
1469
2061
|
...cardReport(card)
|
|
1470
2062
|
};
|
|
1471
2063
|
};
|
|
1472
|
-
|
|
2064
|
+
const traced = () => this.trace("dispatch.handle", run);
|
|
2065
|
+
return this.deps.gate ? this.deps.gate.run(plan.sessionId, traced) : traced();
|
|
1473
2066
|
}
|
|
1474
2067
|
async openCard(input) {
|
|
1475
2068
|
const handle = { messageId: "" };
|
|
@@ -1556,6 +2149,18 @@ class Dispatcher {
|
|
|
1556
2149
|
profile: input.profile
|
|
1557
2150
|
};
|
|
1558
2151
|
}
|
|
2152
|
+
async trace(name, fn) {
|
|
2153
|
+
const tracer = this.deps.tracer;
|
|
2154
|
+
if (!tracer || !tracer.enabled)
|
|
2155
|
+
return fn();
|
|
2156
|
+
return tracer.span(name, fn);
|
|
2157
|
+
}
|
|
2158
|
+
async traceResult(name, fn, errorIf) {
|
|
2159
|
+
const tracer = this.deps.tracer;
|
|
2160
|
+
if (!tracer || !tracer.enabled)
|
|
2161
|
+
return fn();
|
|
2162
|
+
return tracer.spanResult(name, fn, errorIf);
|
|
2163
|
+
}
|
|
1559
2164
|
}
|
|
1560
2165
|
|
|
1561
2166
|
// ../../../packages/cli/src/daemon/es-listener.ts
|
|
@@ -1600,192 +2205,134 @@ function createEventListener(config) {
|
|
|
1600
2205
|
};
|
|
1601
2206
|
}
|
|
1602
2207
|
|
|
1603
|
-
// ../../../packages/cli/src/daemon/runners/
|
|
1604
|
-
import {
|
|
1605
|
-
import {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
var CLAUDE_ROY_AGENT_NAME = "roy";
|
|
1609
|
-
var ROY_AGENT_DESCRIPTION = "Roy — task-as-first-class-citizen agent. Uses the roy-agent task lifecycle " + "(task_tree → task_create → task_update → task_operation_create → task_complete) " + "so every piece of work is tracked in the local task store.";
|
|
1610
|
-
var CLAUDE_ROY_TOOLS = Object.freeze([
|
|
1611
|
-
"Read",
|
|
1612
|
-
"Write",
|
|
1613
|
-
"Edit",
|
|
1614
|
-
"Bash",
|
|
1615
|
-
"Grep",
|
|
1616
|
-
"Glob",
|
|
1617
|
-
"TodoWrite",
|
|
1618
|
-
"WebFetch",
|
|
1619
|
-
"WebSearch"
|
|
1620
|
-
]);
|
|
1621
|
-
var CLAUDE_ROY_SYSTEM_PROMPT = `# Roy — task-first-class-citizen agent
|
|
1622
|
-
|
|
1623
|
-
You are **Roy**, an agent whose defining rule is simple:
|
|
1624
|
-
|
|
1625
|
-
> **Task is a first-class citizen. No task, no work.**
|
|
1626
|
-
|
|
1627
|
-
You are not a chat assistant that occasionally writes notes. You are a worker
|
|
1628
|
-
whose output is a *visible, trackable* stream of tasks. Every non-trivial
|
|
1629
|
-
request (more than one tool call / more than a couple of minutes of work) MUST
|
|
1630
|
-
become a tracked task before you start doing it, and MUST be closed out
|
|
1631
|
-
explicitly when you finish.
|
|
1632
|
-
|
|
1633
|
-
## The lifecycle (non-negotiable)
|
|
1634
|
-
|
|
1635
|
-
### 1. Orient — \`task_tree(depth=1)\` (or \`depth=2\` for bigger trees)
|
|
1636
|
-
Before creating anything, look at the existing tree. Decide:
|
|
1637
|
-
- does this work belong to an existing root (attach via \`parent_task_id\`)?
|
|
1638
|
-
- or is it genuinely new?
|
|
1639
|
-
|
|
1640
|
-
Keep **root tasks ≤ 10**. Prefer attaching to an existing thematic root over
|
|
1641
|
-
spawning a new root. Never hardcode ids — read them from \`task_tree\`.
|
|
1642
|
-
|
|
1643
|
-
### 2. Create — \`task_create\`
|
|
1644
|
-
Create the task **before** doing the work. Pass:
|
|
1645
|
-
- \`title\` (imperative, specific), \`description\`, \`goals_and_expected_deliverables\`
|
|
1646
|
-
- \`priority\` (low / medium / high), \`type\` (normal / cycle / longterm)
|
|
1647
|
-
- \`project_path\`, \`context\` (JSON string: worktree_path / branch / type …)
|
|
1648
|
-
- \`parent_task_id\` when the tree said so
|
|
1649
|
-
|
|
1650
|
-
The tool returns a **numeric task id** — that number is your handle for the
|
|
1651
|
-
rest of the conversation.
|
|
1652
|
-
|
|
1653
|
-
### 3. Track — \`task_update\`
|
|
1654
|
-
Report progress as you go, not only at the end:
|
|
1655
|
-
- \`status\`: todo → active → (paused / cancelled) → completed
|
|
1656
|
-
- \`progress\`: 0–100
|
|
1657
|
-
- \`current_status\`: one human sentence about where you are right now
|
|
1658
|
-
|
|
1659
|
-
If the scope changes, \`task_update\` the description/goals instead of silently
|
|
1660
|
-
doing something else.
|
|
1661
|
-
|
|
1662
|
-
### 4. Record milestones — \`task_operation_create\`
|
|
1663
|
-
This tool has **two orthogonal dimensions** and you must keep them distinct:
|
|
1664
|
-
- **state / milestone dimension** — \`milestone_type\` + \`milestone_title\` +
|
|
1665
|
-
\`milestone_description\`: *what* was reached (e.g. "GREEN: all tests pass").
|
|
1666
|
-
- **process dimension** — \`process_description\`: *how* you got there —
|
|
1667
|
-
strategy + tool steps, strictly **100–150 characters**.
|
|
1668
|
-
Too short = rejected (no value); too long = rejected (use the session log).
|
|
1669
|
-
|
|
1670
|
-
Rules: do **not** use the process field to restate the milestone, and do not
|
|
1671
|
-
use milestones as a substitute for progress. Milestones mark *state
|
|
1672
|
-
transitions*: RED→GREEN, blocker found, decision made, review passed.
|
|
1673
|
-
|
|
1674
|
-
### 5. Close — \`task_complete\`
|
|
1675
|
-
Before completing:
|
|
1676
|
-
1. \`task_get(task_id)\` and re-read goal + delivery.
|
|
1677
|
-
2. Verify the goal and delivery actually match what you delivered. If they do
|
|
1678
|
-
not, either extend the task (\`task_update\`) or say so honestly.
|
|
1679
|
-
3. Call \`task_complete\` with \`deliverable_storage_path\` — a locator for the
|
|
1680
|
-
work product: a file path, a git branch/worktree, a doc URL, a commit URL.
|
|
1681
|
-
The path is persisted into the task context and as an operation record.
|
|
1682
|
-
|
|
1683
|
-
\`task_complete\` cannot be used on \`cycle\` tasks (they are periodic by
|
|
1684
|
-
design) — use \`task_update\` to reset those.
|
|
1685
|
-
|
|
1686
|
-
## Other tools
|
|
1687
|
-
|
|
1688
|
-
- \`task_get(task_id, include_operations)\` — full detail; use it before
|
|
1689
|
-
resuming or completing a task.
|
|
1690
|
-
- \`task_list(...)\` — flat listing with filters (status / priority / type).
|
|
1691
|
-
- \`task_search(keywords)\` — find by title / description / goals / tags.
|
|
1692
|
-
**Search before creating**, to avoid duplicates.
|
|
1693
|
-
|
|
1694
|
-
## Where tasks live
|
|
1695
|
-
|
|
1696
|
-
All tasks and operations are persisted by the roy-task-plugin in the
|
|
1697
|
-
**single** project store — locally that is SQLite at
|
|
1698
|
-
\`~/.roy-task-plugin/tasks.db\` (or the remote service when the plugin is
|
|
1699
|
-
configured with \`persistence: "remote"\`). Do not create side databases, do
|
|
1700
|
-
not maintain your own to-do list in files, and do not track work in chat only:
|
|
1701
|
-
if it is not in the task store, it is not tracked.
|
|
1702
|
-
|
|
1703
|
-
## Tool surface on Claude Code
|
|
1704
|
-
|
|
1705
|
-
The lifecycle above is expressed through the \`roy-agent tasks\` CLI (run it with
|
|
1706
|
-
the \`Bash\` tool), because Claude Code does not yet expose the \`task_*\` tools
|
|
1707
|
-
natively. The mapping is 1:1 — same store, same ids, same semantics:
|
|
1708
|
-
|
|
1709
|
-
| lifecycle step | Claude Code command |
|
|
1710
|
-
|---|---|
|
|
1711
|
-
| \`task_tree(depth=1)\` | \`roy-agent tasks tree\` |
|
|
1712
|
-
| \`task_create\` | \`roy-agent tasks create "<title>" -d "<description>" -g "<goals>" -p high --project-path "<path>"\` |
|
|
1713
|
-
| \`task_update\` | \`roy-agent tasks update <id> --status active --progress 50 --current-status "…"\` |
|
|
1714
|
-
| \`task_operation_create\` | \`roy-agent tasks operations <id>\` to read; record milestones via the CLI's update/ops surface |
|
|
1715
|
-
| \`task_complete\` | \`roy-agent tasks complete <id> --deliverable "<path>"\` |
|
|
1716
|
-
| \`task_get\` | \`roy-agent tasks get <id>\` |
|
|
1717
|
-
| \`task_list\` | \`roy-agent tasks list\` |
|
|
1718
|
-
| \`task_search\` | \`roy-agent tasks search <keywords>\` |
|
|
1719
|
-
|
|
1720
|
-
Prefer \`--json\` (\`-j\`) when you need to parse ids. The task id printed by
|
|
1721
|
-
\`create\` is your handle for the rest of the turn.
|
|
1722
|
-
|
|
1723
|
-
## Style
|
|
1724
|
-
|
|
1725
|
-
- Be direct and concrete; the user is reading task titles and milestones.
|
|
1726
|
-
- Report the task id and its state whenever you finish a step.
|
|
1727
|
-
- If a task is blocked, mark it (\`task_update(status="paused")\`) and record a
|
|
1728
|
-
\`problem\` milestone explaining the blocker instead of going silent.`;
|
|
1729
|
-
function buildClaudeAgentsJson(opts = {}) {
|
|
1730
|
-
const def = {
|
|
1731
|
-
description: opts.description ?? ROY_AGENT_DESCRIPTION,
|
|
1732
|
-
prompt: opts.prompt ?? CLAUDE_ROY_SYSTEM_PROMPT,
|
|
1733
|
-
tools: [...opts.tools ?? CLAUDE_ROY_TOOLS]
|
|
1734
|
-
};
|
|
1735
|
-
if (opts.model)
|
|
1736
|
-
def.model = opts.model;
|
|
1737
|
-
return JSON.stringify({ [opts.name ?? CLAUDE_ROY_AGENT_NAME]: def });
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
// ../../../packages/cli/src/daemon/runners/claude-sessions.ts
|
|
1741
|
-
function createClaudeSessionRegistry() {
|
|
1742
|
-
const sessions = new Map;
|
|
1743
|
-
return {
|
|
1744
|
-
get: (key) => sessions.get(key),
|
|
1745
|
-
set: (key, id) => {
|
|
1746
|
-
sessions.set(key, id);
|
|
1747
|
-
},
|
|
1748
|
-
delete: (key) => {
|
|
1749
|
-
sessions.delete(key);
|
|
1750
|
-
},
|
|
1751
|
-
size: () => sessions.size
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
2208
|
+
// ../../../packages/cli/src/daemon/runners/cursor.ts
|
|
2209
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2210
|
+
import { tmpdir } from "node:os";
|
|
2211
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2212
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1754
2213
|
|
|
1755
|
-
// ../../../packages/cli/src/daemon/runners/
|
|
1756
|
-
function
|
|
1757
|
-
if (typeof
|
|
1758
|
-
return
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2214
|
+
// ../../../packages/cli/src/daemon/runners/cursor-stream.ts
|
|
2215
|
+
function cursorToolName(toolCall) {
|
|
2216
|
+
if (typeof toolCall !== "object" || toolCall === null)
|
|
2217
|
+
return;
|
|
2218
|
+
const tc = toolCall;
|
|
2219
|
+
for (const key of Object.keys(tc)) {
|
|
2220
|
+
if (!key.endsWith("ToolCall") && !key.endsWith("Tool"))
|
|
2221
|
+
continue;
|
|
2222
|
+
const payload = tc[key];
|
|
2223
|
+
if (typeof payload !== "object" || payload === null)
|
|
2224
|
+
return key;
|
|
2225
|
+
return nameFromPayload(payload) ?? key;
|
|
2226
|
+
}
|
|
2227
|
+
return nameFromPayload(tc);
|
|
2228
|
+
function nameFromPayload(p) {
|
|
2229
|
+
const fn = p["function"];
|
|
2230
|
+
if (typeof fn === "object" && fn !== null) {
|
|
2231
|
+
const f = fn;
|
|
2232
|
+
if (typeof f.name === "string" && f.name !== "")
|
|
2233
|
+
return f.name;
|
|
2234
|
+
if (typeof f.args === "object" && f.args !== null) {
|
|
2235
|
+
const name = f.args.name;
|
|
2236
|
+
if (typeof name === "string" && name !== "")
|
|
2237
|
+
return name;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const payloadArgs = p["args"];
|
|
2241
|
+
if (typeof payloadArgs === "object" && payloadArgs !== null) {
|
|
2242
|
+
const name = payloadArgs.name;
|
|
2243
|
+
if (typeof name === "string" && name !== "")
|
|
2244
|
+
return name;
|
|
2245
|
+
}
|
|
2246
|
+
return;
|
|
1764
2247
|
}
|
|
1765
|
-
return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
|
|
1766
2248
|
}
|
|
1767
|
-
function
|
|
2249
|
+
function flattenCursorToolResult(result) {
|
|
2250
|
+
if (typeof result === "string")
|
|
2251
|
+
return result;
|
|
2252
|
+
if (typeof result !== "object" || result === null)
|
|
2253
|
+
return "";
|
|
2254
|
+
const r = result;
|
|
2255
|
+
const success = r["success"];
|
|
2256
|
+
if (typeof success === "object" && success !== null) {
|
|
2257
|
+
const s = success;
|
|
2258
|
+
const nested2 = flattenContent(s["content"]);
|
|
2259
|
+
if (nested2 !== "")
|
|
2260
|
+
return nested2;
|
|
2261
|
+
if (typeof s["output"] === "string")
|
|
2262
|
+
return s["output"];
|
|
2263
|
+
if (typeof s["text"] === "string")
|
|
2264
|
+
return s["text"];
|
|
2265
|
+
}
|
|
2266
|
+
const nested = flattenContent(r["content"]);
|
|
2267
|
+
if (nested !== "")
|
|
2268
|
+
return nested;
|
|
2269
|
+
if (typeof r["output"] === "string")
|
|
2270
|
+
return r["output"];
|
|
2271
|
+
if (typeof r["text"] === "string")
|
|
2272
|
+
return r["text"];
|
|
2273
|
+
return "";
|
|
2274
|
+
}
|
|
2275
|
+
function flattenContent(content) {
|
|
1768
2276
|
if (typeof content === "string")
|
|
1769
2277
|
return content;
|
|
1770
2278
|
if (!Array.isArray(content))
|
|
1771
2279
|
return "";
|
|
1772
2280
|
const parts = [];
|
|
1773
|
-
for (const
|
|
1774
|
-
if (typeof
|
|
1775
|
-
parts.push(
|
|
2281
|
+
for (const item of content) {
|
|
2282
|
+
if (typeof item === "string") {
|
|
2283
|
+
parts.push(item);
|
|
1776
2284
|
continue;
|
|
1777
2285
|
}
|
|
1778
|
-
if (typeof
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2286
|
+
if (typeof item !== "object" || item === null)
|
|
2287
|
+
continue;
|
|
2288
|
+
const block = item;
|
|
2289
|
+
if (typeof block.text === "string") {
|
|
2290
|
+
parts.push(block.text);
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
if (typeof block.text === "object" && block.text !== null) {
|
|
2294
|
+
const inner = block.text.text;
|
|
2295
|
+
if (typeof inner === "string")
|
|
2296
|
+
parts.push(inner);
|
|
1782
2297
|
}
|
|
1783
2298
|
}
|
|
1784
2299
|
return parts.join(`
|
|
1785
2300
|
`);
|
|
1786
2301
|
}
|
|
2302
|
+
function cursorTitleHint(toolCall) {
|
|
2303
|
+
if (typeof toolCall !== "object" || toolCall === null)
|
|
2304
|
+
return;
|
|
2305
|
+
const tc = toolCall;
|
|
2306
|
+
for (const key of Object.keys(tc)) {
|
|
2307
|
+
const inner = tc[key];
|
|
2308
|
+
if (typeof inner !== "object" || inner === null)
|
|
2309
|
+
continue;
|
|
2310
|
+
const holder = inner;
|
|
2311
|
+
const titleFrom = (value) => {
|
|
2312
|
+
if (typeof value !== "object" || value === null)
|
|
2313
|
+
return;
|
|
2314
|
+
const title = value.title;
|
|
2315
|
+
return typeof title === "string" && title !== "" ? title : undefined;
|
|
2316
|
+
};
|
|
2317
|
+
const direct = titleFrom(holder.args);
|
|
2318
|
+
if (direct)
|
|
2319
|
+
return direct;
|
|
2320
|
+
const deepArgs = holder.args?.args;
|
|
2321
|
+
const nested = titleFrom(deepArgs);
|
|
2322
|
+
if (nested)
|
|
2323
|
+
return nested;
|
|
2324
|
+
if (typeof holder.arguments === "string") {
|
|
2325
|
+
try {
|
|
2326
|
+
const parsed = titleFrom(JSON.parse(holder.arguments));
|
|
2327
|
+
if (parsed)
|
|
2328
|
+
return parsed;
|
|
2329
|
+
} catch {}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
1787
2334
|
|
|
1788
|
-
class
|
|
2335
|
+
class CursorStreamDecoder {
|
|
1789
2336
|
lines = new LineSplitter;
|
|
1790
2337
|
state = {
|
|
1791
2338
|
assistantText: [],
|
|
@@ -1802,7 +2349,11 @@ class ClaudeStreamDecoder {
|
|
|
1802
2349
|
return this.toResult();
|
|
1803
2350
|
}
|
|
1804
2351
|
snapshot() {
|
|
1805
|
-
return {
|
|
2352
|
+
return {
|
|
2353
|
+
...this.state,
|
|
2354
|
+
assistantText: [...this.state.assistantText],
|
|
2355
|
+
tools: [...this.state.tools]
|
|
2356
|
+
};
|
|
1806
2357
|
}
|
|
1807
2358
|
consume(rawLine) {
|
|
1808
2359
|
const line = rawLine.trim();
|
|
@@ -1817,157 +2368,261 @@ class ClaudeStreamDecoder {
|
|
|
1817
2368
|
if (typeof event !== "object" || event === null)
|
|
1818
2369
|
return;
|
|
1819
2370
|
const ev = event;
|
|
1820
|
-
const sessionId = typeof ev
|
|
2371
|
+
const sessionId = typeof ev["session_id"] === "string" ? ev["session_id"] : undefined;
|
|
1821
2372
|
if (sessionId && !this.state.sessionId)
|
|
1822
2373
|
this.state.sessionId = sessionId;
|
|
1823
|
-
if (ev
|
|
2374
|
+
if (ev["type"] === "assistant") {
|
|
1824
2375
|
this.consumeAssistant(ev);
|
|
1825
2376
|
return;
|
|
1826
2377
|
}
|
|
1827
|
-
if (ev
|
|
1828
|
-
this.
|
|
2378
|
+
if (ev["type"] === "tool_call") {
|
|
2379
|
+
this.consumeToolCall(ev);
|
|
1829
2380
|
return;
|
|
1830
2381
|
}
|
|
1831
|
-
if (ev
|
|
2382
|
+
if (ev["type"] === "result") {
|
|
1832
2383
|
this.state.result = ev;
|
|
1833
2384
|
}
|
|
1834
2385
|
}
|
|
1835
|
-
|
|
1836
|
-
const message = ev
|
|
1837
|
-
|
|
2386
|
+
consumeAssistant(ev) {
|
|
2387
|
+
const message = ev["message"];
|
|
2388
|
+
if (typeof message !== "object" || message === null)
|
|
2389
|
+
return;
|
|
2390
|
+
const content = message.content;
|
|
1838
2391
|
if (!Array.isArray(content))
|
|
1839
2392
|
return;
|
|
1840
2393
|
for (const block of content) {
|
|
1841
2394
|
if (typeof block !== "object" || block === null)
|
|
1842
2395
|
continue;
|
|
1843
|
-
const
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2396
|
+
const text = block.text;
|
|
2397
|
+
if (typeof text === "string" && text !== "")
|
|
2398
|
+
this.state.assistantText.push(text);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
consumeToolCall(ev) {
|
|
2402
|
+
const callId = typeof ev["call_id"] === "string" ? ev["call_id"] : undefined;
|
|
2403
|
+
const toolCall = ev["tool_call"];
|
|
2404
|
+
const tool = cursorToolName(toolCall);
|
|
2405
|
+
const subtype = ev["subtype"];
|
|
2406
|
+
if (subtype === "started") {
|
|
2407
|
+
if (tool) {
|
|
2408
|
+
this.state.tools.push(tool);
|
|
2409
|
+
if (callId && isTaskCreateTool(tool)) {
|
|
2410
|
+
this.state.pendingCreates.set(callId, {
|
|
2411
|
+
tool,
|
|
2412
|
+
titleHint: cursorTitleHint(toolCall)
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
1855
2415
|
}
|
|
1856
|
-
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (subtype === "completed") {
|
|
2419
|
+
if (!callId)
|
|
2420
|
+
return;
|
|
2421
|
+
const pending = this.state.pendingCreates.get(callId);
|
|
2422
|
+
const payload = typeof toolCall === "object" && toolCall !== null ? toolCall : undefined;
|
|
2423
|
+
const result = payload ? this.toolResultOf(payload) : undefined;
|
|
2424
|
+
const text = flattenCursorToolResult(result);
|
|
2425
|
+
if (!pending) {
|
|
2426
|
+
const tool2 = payload ? cursorToolName(payload) : undefined;
|
|
2427
|
+
if (!tool2 || !isTaskCreateTool(tool2))
|
|
2428
|
+
return;
|
|
2429
|
+
const created2 = extractCreatedTask(text, payload ? cursorTitleHint(payload) : undefined);
|
|
2430
|
+
if (created2 && !this.state.createdTask)
|
|
2431
|
+
this.state.createdTask = created2;
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
this.state.pendingCreates.delete(callId);
|
|
2435
|
+
const created = extractCreatedTask(text, pending.titleHint);
|
|
2436
|
+
if (created && !this.state.createdTask)
|
|
2437
|
+
this.state.createdTask = created;
|
|
1857
2438
|
}
|
|
1858
2439
|
}
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
return;
|
|
1864
|
-
for (const block of content) {
|
|
1865
|
-
if (typeof block !== "object" || block === null)
|
|
2440
|
+
toolResultOf(toolCall) {
|
|
2441
|
+
for (const key of Object.keys(toolCall)) {
|
|
2442
|
+
const inner = toolCall[key];
|
|
2443
|
+
if (typeof inner !== "object" || inner === null)
|
|
1866
2444
|
continue;
|
|
1867
|
-
const
|
|
1868
|
-
if (
|
|
1869
|
-
|
|
1870
|
-
} else if (b.type === "tool_use") {
|
|
1871
|
-
if (typeof b.name === "string") {
|
|
1872
|
-
this.state.tools.push(b.name);
|
|
1873
|
-
if (isTaskCreateTool(b.name) && typeof b.id === "string") {
|
|
1874
|
-
const input = b.input;
|
|
1875
|
-
const title = input?.["title"];
|
|
1876
|
-
this.state.pendingCreates.set(b.id, {
|
|
1877
|
-
tool: b.name,
|
|
1878
|
-
titleHint: typeof title === "string" ? title : undefined
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
2445
|
+
const result = inner.result;
|
|
2446
|
+
if (result !== undefined)
|
|
2447
|
+
return result;
|
|
1883
2448
|
}
|
|
2449
|
+
return;
|
|
1884
2450
|
}
|
|
1885
2451
|
toResult() {
|
|
1886
|
-
const
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
sessionId: this.state.sessionId,
|
|
1895
|
-
toolCalls,
|
|
1896
|
-
tools,
|
|
1897
|
-
createdTask,
|
|
1898
|
-
error: detail.slice(0, 1000)
|
|
1899
|
-
};
|
|
2452
|
+
const result = this.state.result;
|
|
2453
|
+
const out = { text: "" };
|
|
2454
|
+
if (result) {
|
|
2455
|
+
if (typeof result.result === "string")
|
|
2456
|
+
out.text = result.result;
|
|
2457
|
+
if (result.is_error === true) {
|
|
2458
|
+
out.error = typeof result.result === "string" && result.result.trim() !== "" ? result.result.trim() : `cursor reported ${result.subtype ?? "is_error"}`;
|
|
2459
|
+
}
|
|
1900
2460
|
}
|
|
1901
|
-
if (
|
|
1902
|
-
|
|
2461
|
+
if (out.text === "") {
|
|
2462
|
+
const joined = this.state.assistantText.join(`
|
|
2463
|
+
`).trim();
|
|
2464
|
+
if (joined !== "")
|
|
2465
|
+
out.text = joined;
|
|
1903
2466
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
2467
|
+
if (this.state.sessionId)
|
|
2468
|
+
out.sessionId = this.state.sessionId;
|
|
2469
|
+
if (this.state.tools.length > 0) {
|
|
2470
|
+
out.tools = [...this.state.tools];
|
|
2471
|
+
out.toolCalls = this.state.tools.length;
|
|
2472
|
+
}
|
|
2473
|
+
if (this.state.createdTask)
|
|
2474
|
+
out.createdTask = this.state.createdTask;
|
|
2475
|
+
return out;
|
|
1906
2476
|
}
|
|
1907
2477
|
}
|
|
1908
2478
|
|
|
1909
|
-
// ../../../packages/cli/src/daemon/runners/
|
|
1910
|
-
|
|
1911
|
-
|
|
2479
|
+
// ../../../packages/cli/src/daemon/runners/cursor-sessions.ts
|
|
2480
|
+
function createCursorSessionRegistry() {
|
|
2481
|
+
const sessions = new Map;
|
|
2482
|
+
return {
|
|
2483
|
+
get: (key) => sessions.get(key),
|
|
2484
|
+
set: (key, id) => {
|
|
2485
|
+
sessions.set(key, id);
|
|
2486
|
+
},
|
|
2487
|
+
delete: (key) => {
|
|
2488
|
+
sessions.delete(key);
|
|
2489
|
+
},
|
|
2490
|
+
size: () => sessions.size
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// ../../../packages/cli/src/daemon/runners/cursor.ts
|
|
2495
|
+
var DEFAULT_CURSOR_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2496
|
+
var CURSOR_STDIO = Object.freeze([
|
|
1912
2497
|
"ignore",
|
|
1913
2498
|
"pipe",
|
|
1914
2499
|
"pipe"
|
|
1915
2500
|
]);
|
|
1916
|
-
function
|
|
1917
|
-
const args = ["
|
|
1918
|
-
if (input.
|
|
1919
|
-
args.push("--
|
|
1920
|
-
args.push("--agent", input.agent ?? "roy");
|
|
1921
|
-
args.push("--dangerously-skip-permissions");
|
|
2501
|
+
function buildCursorArgs(input) {
|
|
2502
|
+
const args = ["-p", "--output-format", "stream-json", "--force", "--trust", "--approve-mcps"];
|
|
2503
|
+
if (input.model)
|
|
2504
|
+
args.push("--model", input.model);
|
|
1922
2505
|
if (input.sessionId)
|
|
1923
|
-
args.push("--resume",
|
|
2506
|
+
args.push("--resume", input.sessionId);
|
|
2507
|
+
if (input.workspace)
|
|
2508
|
+
args.push("--workspace", input.workspace);
|
|
2509
|
+
for (const dir of input.addDirs ?? [])
|
|
2510
|
+
args.push("--add-dir", dir);
|
|
1924
2511
|
args.push(input.prompt);
|
|
1925
2512
|
return args;
|
|
1926
2513
|
}
|
|
1927
|
-
function
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2514
|
+
function buildCursorMcpConfig(pluginDir, env) {
|
|
2515
|
+
return JSON.stringify({
|
|
2516
|
+
mcpServers: {
|
|
2517
|
+
"roy-task": {
|
|
2518
|
+
type: "stdio",
|
|
2519
|
+
command: process.execPath,
|
|
2520
|
+
args: [join3(pluginDir, "mcp", "roy-task-mcp-server.mjs")],
|
|
2521
|
+
...env && Object.keys(env).length > 0 ? { env } : {}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
1931
2525
|
}
|
|
1932
|
-
function
|
|
1933
|
-
if (
|
|
1934
|
-
return
|
|
1935
|
-
const
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
`8${h.slice(17, 20)}`,
|
|
1942
|
-
h.slice(20, 32)
|
|
1943
|
-
].join("-");
|
|
2526
|
+
function resolveCursorPluginDir(roots, explicit) {
|
|
2527
|
+
if (explicit)
|
|
2528
|
+
return explicit;
|
|
2529
|
+
for (const root of roots) {
|
|
2530
|
+
const candidate = join3(root, "claude-plugin");
|
|
2531
|
+
if (existsSync2(join3(candidate, "mcp", "roy-task-mcp-server.mjs")))
|
|
2532
|
+
return candidate;
|
|
2533
|
+
}
|
|
2534
|
+
return;
|
|
1944
2535
|
}
|
|
1945
|
-
function
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2536
|
+
function writeCursorMcpConfig(dir, pluginDir, env) {
|
|
2537
|
+
const cursorDir = join3(dir, ".cursor");
|
|
2538
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
2539
|
+
writeFileSync(join3(cursorDir, "mcp.json"), buildCursorMcpConfig(pluginDir, env), "utf8");
|
|
2540
|
+
return dir;
|
|
2541
|
+
}
|
|
2542
|
+
function createCursorWorkspace(pluginDir, env) {
|
|
2543
|
+
const dir = join3(tmpdir(), `roy-task-cursor-${process.pid}`);
|
|
2544
|
+
mkdirSync(dir, { recursive: true });
|
|
2545
|
+
return writeCursorMcpConfig(dir, pluginDir, env);
|
|
2546
|
+
}
|
|
2547
|
+
function removeCursorWorkspace(dir) {
|
|
2548
|
+
try {
|
|
2549
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2550
|
+
} catch {}
|
|
2551
|
+
}
|
|
2552
|
+
function collectMcpNotifyEnv() {
|
|
2553
|
+
const env = {};
|
|
2554
|
+
for (const key of ["ROY_TASK_NOTIFY_ENABLED", "ROY_TASK_NOTIFY_CHAT_ID", "ROY_TASK_NOTIFY_PROFILE"]) {
|
|
2555
|
+
const value = process.env[key];
|
|
2556
|
+
if (value)
|
|
2557
|
+
env[key] = value;
|
|
2558
|
+
}
|
|
2559
|
+
return env;
|
|
2560
|
+
}
|
|
2561
|
+
function makeCursorRunner(opts = {}) {
|
|
2562
|
+
const bin = opts.bin ?? "cursor-agent";
|
|
2563
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_CURSOR_TIMEOUT_MS;
|
|
2564
|
+
const sessions = opts.sessions ?? createCursorSessionRegistry();
|
|
2565
|
+
const pluginDir = opts.pluginDir ?? resolveCursorPluginDir([dirname3(fileURLToPath3(import.meta.url))]);
|
|
2566
|
+
let workspaceDir = opts.workspaceDir;
|
|
2567
|
+
if (!workspaceDir && pluginDir) {
|
|
2568
|
+
try {
|
|
2569
|
+
workspaceDir = createCursorWorkspace(pluginDir, opts.mcpEnv ?? collectMcpNotifyEnv());
|
|
2570
|
+
} catch {
|
|
2571
|
+
workspaceDir = undefined;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (workspaceDir && !opts.workspaceDir) {
|
|
2575
|
+
const cleanup = () => removeCursorWorkspace(workspaceDir);
|
|
2576
|
+
process.once("exit", cleanup);
|
|
2577
|
+
process.once("SIGINT", cleanup);
|
|
2578
|
+
process.once("SIGTERM", cleanup);
|
|
2579
|
+
}
|
|
2580
|
+
const refreshMcpConfig = () => {
|
|
2581
|
+
if (!workspaceDir || !pluginDir)
|
|
2582
|
+
return;
|
|
2583
|
+
if (opts.workspaceDir)
|
|
2584
|
+
return;
|
|
2585
|
+
try {
|
|
2586
|
+
writeCursorMcpConfig(workspaceDir, pluginDir, opts.mcpEnv ?? collectMcpNotifyEnv());
|
|
2587
|
+
} catch {}
|
|
2588
|
+
};
|
|
2589
|
+
const addDirs = opts.addDirs ?? [process.cwd()];
|
|
1950
2590
|
return async function runAgent(input) {
|
|
2591
|
+
refreshMcpConfig();
|
|
1951
2592
|
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
1952
|
-
const args =
|
|
2593
|
+
const args = buildCursorArgs({
|
|
1953
2594
|
prompt: input.prompt,
|
|
1954
|
-
|
|
1955
|
-
|
|
2595
|
+
model: opts.model,
|
|
2596
|
+
workspace: workspaceDir,
|
|
2597
|
+
addDirs,
|
|
1956
2598
|
sessionId: known
|
|
1957
2599
|
});
|
|
1958
|
-
const decoder = new
|
|
2600
|
+
const decoder = new CursorStreamDecoder;
|
|
1959
2601
|
let stderr = "";
|
|
2602
|
+
let reported = false;
|
|
2603
|
+
const notifyProgress = () => {
|
|
2604
|
+
if (reported || !input.onProgress)
|
|
2605
|
+
return;
|
|
2606
|
+
const created = decoder.snapshot().createdTask;
|
|
2607
|
+
if (!created)
|
|
2608
|
+
return;
|
|
2609
|
+
reported = true;
|
|
2610
|
+
try {
|
|
2611
|
+
input.onProgress({ createdTask: created });
|
|
2612
|
+
} catch {}
|
|
2613
|
+
};
|
|
1960
2614
|
let handle;
|
|
1961
2615
|
try {
|
|
1962
2616
|
handle = spawnStreaming([bin, ...args], {
|
|
1963
|
-
stdin:
|
|
2617
|
+
stdin: CURSOR_STDIO[0],
|
|
1964
2618
|
env: { ...process.env, NO_COLOR: "1" },
|
|
1965
2619
|
timeoutMs,
|
|
1966
2620
|
onStdout: (chunk) => {
|
|
1967
2621
|
decoder.pushChunk(chunk);
|
|
2622
|
+
notifyProgress();
|
|
1968
2623
|
if (process.env["ROY_TASK_DUMP_STREAM"]) {
|
|
1969
2624
|
try {
|
|
1970
|
-
|
|
2625
|
+
appendFileSync2(process.env["ROY_TASK_DUMP_STREAM"], chunk);
|
|
1971
2626
|
} catch {}
|
|
1972
2627
|
}
|
|
1973
2628
|
},
|
|
@@ -1977,53 +2632,38 @@ function makeClaudeRunner(opts = {}) {
|
|
|
1977
2632
|
}
|
|
1978
2633
|
});
|
|
1979
2634
|
} catch (err) {
|
|
1980
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
2635
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
|
|
1981
2636
|
}
|
|
1982
2637
|
let code;
|
|
1983
2638
|
try {
|
|
1984
2639
|
code = await handle.exited;
|
|
1985
2640
|
} catch (err) {
|
|
1986
2641
|
if (isMissingBinary(err)) {
|
|
1987
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
2642
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
|
|
1988
2643
|
}
|
|
1989
|
-
return { text: "", error:
|
|
2644
|
+
return { text: "", error: errMsg3(err) };
|
|
1990
2645
|
}
|
|
1991
2646
|
const result = decoder.finish();
|
|
1992
2647
|
const stderrText = stderr.trim();
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
if (input.sessionId && effective.sessionId) {
|
|
1996
|
-
sessions.set(input.sessionId, effective.sessionId);
|
|
2648
|
+
if (result.error === undefined && code !== 0) {
|
|
2649
|
+
result.error = stderrText !== "" ? stderrText : `${bin} exited with code ${code}`;
|
|
1997
2650
|
}
|
|
1998
|
-
if (
|
|
1999
|
-
|
|
2000
|
-
}
|
|
2001
|
-
if (effective.error) {
|
|
2002
|
-
if (input.sessionId && STALE_SESSION_RE.test(effective.error)) {
|
|
2003
|
-
sessions.delete(input.sessionId);
|
|
2004
|
-
}
|
|
2005
|
-
return effective;
|
|
2651
|
+
if (result.error !== undefined && stderrText !== "" && !result.error.includes(stderrText)) {
|
|
2652
|
+
result.error = `${result.error} | ${stderrText}`;
|
|
2006
2653
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
if (
|
|
2654
|
+
const sessionId = result.sessionId;
|
|
2655
|
+
if (input.sessionId) {
|
|
2656
|
+
if (result.error === undefined && sessionId) {
|
|
2657
|
+
sessions.set(input.sessionId, sessionId);
|
|
2658
|
+
} else if (result.error !== undefined) {
|
|
2010
2659
|
sessions.delete(input.sessionId);
|
|
2011
2660
|
}
|
|
2012
|
-
return {
|
|
2013
|
-
text: "",
|
|
2014
|
-
toolCalls: effective.toolCalls,
|
|
2015
|
-
tools: effective.tools,
|
|
2016
|
-
error
|
|
2017
|
-
};
|
|
2018
2661
|
}
|
|
2019
|
-
return
|
|
2662
|
+
return result;
|
|
2020
2663
|
};
|
|
2021
2664
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
if (err instanceof Error)
|
|
2025
|
-
return err.message;
|
|
2026
|
-
return String(err);
|
|
2665
|
+
function errMsg3(err) {
|
|
2666
|
+
return err instanceof Error ? err.message : String(err);
|
|
2027
2667
|
}
|
|
2028
2668
|
|
|
2029
2669
|
// ../../../packages/cli/src/daemon/runners/opencode.ts
|
|
@@ -2039,29 +2679,41 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
2039
2679
|
});
|
|
2040
2680
|
const assembler = new NdjsonAssembler;
|
|
2041
2681
|
let stderr = "";
|
|
2682
|
+
let reported = false;
|
|
2042
2683
|
let handle;
|
|
2043
2684
|
try {
|
|
2044
2685
|
handle = spawnStreaming([bin, ...args], {
|
|
2045
2686
|
stdin: OPENCODE_STDIO[0],
|
|
2046
2687
|
env: { ...process.env, NO_COLOR: "1" },
|
|
2047
2688
|
timeoutMs,
|
|
2048
|
-
onStdout: (chunk) =>
|
|
2689
|
+
onStdout: (chunk) => {
|
|
2690
|
+
assembler.pushChunk(chunk);
|
|
2691
|
+
if (!reported && input.onProgress) {
|
|
2692
|
+
const created = assembler.createdTaskSoFar;
|
|
2693
|
+
if (created) {
|
|
2694
|
+
reported = true;
|
|
2695
|
+
try {
|
|
2696
|
+
input.onProgress({ createdTask: created });
|
|
2697
|
+
} catch {}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
},
|
|
2049
2701
|
onStderr: (chunk) => {
|
|
2050
2702
|
if (stderr.length < 64 * 1024)
|
|
2051
2703
|
stderr += chunk;
|
|
2052
2704
|
}
|
|
2053
2705
|
});
|
|
2054
2706
|
} catch (err) {
|
|
2055
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
2707
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg4(err)}` };
|
|
2056
2708
|
}
|
|
2057
2709
|
let code;
|
|
2058
2710
|
try {
|
|
2059
2711
|
code = await handle.exited;
|
|
2060
2712
|
} catch (err) {
|
|
2061
2713
|
if (isMissingBinary(err)) {
|
|
2062
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
2714
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg4(err)}` };
|
|
2063
2715
|
}
|
|
2064
|
-
return { text: "", error:
|
|
2716
|
+
return { text: "", error: errMsg4(err) };
|
|
2065
2717
|
}
|
|
2066
2718
|
const result = assembler.finish();
|
|
2067
2719
|
if (code === 124) {
|
|
@@ -2102,7 +2754,7 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
2102
2754
|
};
|
|
2103
2755
|
};
|
|
2104
2756
|
}
|
|
2105
|
-
function
|
|
2757
|
+
function errMsg4(err) {
|
|
2106
2758
|
if (err instanceof Error)
|
|
2107
2759
|
return err.message;
|
|
2108
2760
|
return String(err);
|
|
@@ -2121,6 +2773,12 @@ var FACTORIES = {
|
|
|
2121
2773
|
timeoutMs: input.timeoutMs,
|
|
2122
2774
|
model: input.codexModel,
|
|
2123
2775
|
sandbox: isCodexSandboxMode(input.codexSandbox ?? "") ? input.codexSandbox : undefined
|
|
2776
|
+
}),
|
|
2777
|
+
cursor: (input) => makeCursorRunner({
|
|
2778
|
+
bin: input.bin,
|
|
2779
|
+
timeoutMs: input.timeoutMs,
|
|
2780
|
+
pluginDir: input.pluginDir,
|
|
2781
|
+
model: input.cursorModel
|
|
2124
2782
|
})
|
|
2125
2783
|
};
|
|
2126
2784
|
function makeRunner(input = {}) {
|
|
@@ -2216,6 +2874,90 @@ async function defaultReplyRunner(input) {
|
|
|
2216
2874
|
}
|
|
2217
2875
|
}
|
|
2218
2876
|
|
|
2877
|
+
// ../../../packages/cli/src/daemon/trace.ts
|
|
2878
|
+
import {
|
|
2879
|
+
getTracerProvider,
|
|
2880
|
+
resetTracerProvider,
|
|
2881
|
+
wrapFunction
|
|
2882
|
+
} from "@ai-setting/roy-agent-core";
|
|
2883
|
+
async function createTaskTracer(options = {}) {
|
|
2884
|
+
if (options.enabled === false)
|
|
2885
|
+
return disabledTracer();
|
|
2886
|
+
try {
|
|
2887
|
+
const provider = options.dataHome ? isolatedProvider(options.dataHome) : getTracerProvider();
|
|
2888
|
+
await provider.initialize();
|
|
2889
|
+
const storage = provider.getStorage();
|
|
2890
|
+
return {
|
|
2891
|
+
enabled: true,
|
|
2892
|
+
storage: () => storage,
|
|
2893
|
+
async span(spanName, fn) {
|
|
2894
|
+
const wrapped = wrapFunction(fn, spanName, {
|
|
2895
|
+
recordParams: false,
|
|
2896
|
+
recordResult: false,
|
|
2897
|
+
recordError: true
|
|
2898
|
+
});
|
|
2899
|
+
return wrapped();
|
|
2900
|
+
},
|
|
2901
|
+
async spanResult(spanName, fn, errorIf) {
|
|
2902
|
+
const wrapped = wrapFunction(fn, spanName, {
|
|
2903
|
+
recordParams: false,
|
|
2904
|
+
recordResult: false,
|
|
2905
|
+
recordError: true,
|
|
2906
|
+
errorIf: (result) => errorIf(result)
|
|
2907
|
+
});
|
|
2908
|
+
return wrapped();
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
} catch {
|
|
2912
|
+
return disabledTracer();
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
function isolatedProvider(dataHome) {
|
|
2916
|
+
resetTracerProvider();
|
|
2917
|
+
const prevHome = process.env["XDG_DATA_HOME"];
|
|
2918
|
+
const restore = withCleanTraceparent(() => {
|
|
2919
|
+
process.env["XDG_DATA_HOME"] = dataHome;
|
|
2920
|
+
return getTracerProvider();
|
|
2921
|
+
});
|
|
2922
|
+
if (prevHome === undefined)
|
|
2923
|
+
delete process.env["XDG_DATA_HOME"];
|
|
2924
|
+
else
|
|
2925
|
+
process.env["XDG_DATA_HOME"] = prevHome;
|
|
2926
|
+
return restore;
|
|
2927
|
+
}
|
|
2928
|
+
function withCleanTraceparent(fn) {
|
|
2929
|
+
const prev = process.env["TRACEPARENT"];
|
|
2930
|
+
delete process.env["TRACEPARENT"];
|
|
2931
|
+
try {
|
|
2932
|
+
return fn();
|
|
2933
|
+
} finally {
|
|
2934
|
+
if (prev !== undefined)
|
|
2935
|
+
process.env["TRACEPARENT"] = prev;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
function disabledTracer() {
|
|
2939
|
+
return {
|
|
2940
|
+
enabled: false,
|
|
2941
|
+
storage() {
|
|
2942
|
+
throw new Error("roy-task tracing is disabled: no span storage available");
|
|
2943
|
+
},
|
|
2944
|
+
async span(_name, fn) {
|
|
2945
|
+
return fn();
|
|
2946
|
+
},
|
|
2947
|
+
async spanResult(_name, fn, _errorIf) {
|
|
2948
|
+
return fn();
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
var runtime;
|
|
2953
|
+
var runtimeOptions = {};
|
|
2954
|
+
function getTraceRuntime(options) {
|
|
2955
|
+
if (options)
|
|
2956
|
+
runtimeOptions = options;
|
|
2957
|
+
runtime ??= createTaskTracer(runtimeOptions);
|
|
2958
|
+
return runtime;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2219
2961
|
// ../../../packages/cli/src/cmd/serve.ts
|
|
2220
2962
|
function serve(options, deps = {}) {
|
|
2221
2963
|
return startServe(options, deps);
|
|
@@ -2225,18 +2967,21 @@ async function startServe(options, deps) {
|
|
|
2225
2967
|
const gate = new ConcurrencyGate({ maxConcurrency: options.maxConcurrency });
|
|
2226
2968
|
const deduper = new Deduper;
|
|
2227
2969
|
const card = deps.card ?? (options.cardEnabled && !options.dryRun ? createRoyAgentCardPort() : createNoopCardPort());
|
|
2970
|
+
const tracer = deps.tracer ?? await getTraceRuntime();
|
|
2228
2971
|
const dispatcher = new Dispatcher({
|
|
2229
2972
|
runAgent: deps.runAgent ?? makeRunner({
|
|
2230
2973
|
runtime: options.runtime,
|
|
2231
2974
|
bin: resolveRuntimeBin(options),
|
|
2232
2975
|
timeoutMs: options.timeoutMs,
|
|
2233
2976
|
codexModel: options.codexModel,
|
|
2234
|
-
codexSandbox: options.codexSandbox
|
|
2977
|
+
codexSandbox: options.codexSandbox,
|
|
2978
|
+
cursorModel: options.cursorModel
|
|
2235
2979
|
}),
|
|
2236
2980
|
reply: deps.reply ?? defaultReplyRunner,
|
|
2237
2981
|
gate,
|
|
2238
2982
|
deduper,
|
|
2239
|
-
card
|
|
2983
|
+
card,
|
|
2984
|
+
tracer
|
|
2240
2985
|
}, {
|
|
2241
2986
|
dryRun: options.dryRun,
|
|
2242
2987
|
replyEnabled: options.replyEnabled,
|
|
@@ -2260,7 +3005,7 @@ async function startServe(options, deps) {
|
|
|
2260
3005
|
try {
|
|
2261
3006
|
await listener.stop();
|
|
2262
3007
|
} catch (err) {
|
|
2263
|
-
log(`[roy-task] listener.stop() failed: ${
|
|
3008
|
+
log(`[roy-task] listener.stop() failed: ${errMsg5(err)}`);
|
|
2264
3009
|
}
|
|
2265
3010
|
log("[roy-task] stopped");
|
|
2266
3011
|
resolveDone();
|
|
@@ -2282,7 +3027,7 @@ async function startServe(options, deps) {
|
|
|
2282
3027
|
await stop();
|
|
2283
3028
|
}
|
|
2284
3029
|
} catch (err) {
|
|
2285
|
-
log(`[roy-task] ${label} dispatch threw: ${
|
|
3030
|
+
log(`[roy-task] ${label} dispatch threw: ${errMsg5(err)}`);
|
|
2286
3031
|
if (options.once)
|
|
2287
3032
|
await stop();
|
|
2288
3033
|
}
|
|
@@ -2292,7 +3037,7 @@ async function startServe(options, deps) {
|
|
|
2292
3037
|
try {
|
|
2293
3038
|
await listener.start();
|
|
2294
3039
|
} catch (err) {
|
|
2295
|
-
log(`[roy-task] failed to start event listener: ${
|
|
3040
|
+
log(`[roy-task] failed to start event listener: ${errMsg5(err)}`);
|
|
2296
3041
|
clearInterval(sweeper);
|
|
2297
3042
|
resolveDone();
|
|
2298
3043
|
throw err;
|
|
@@ -2315,6 +3060,7 @@ function formatBanner(options, commands, maxConcurrency) {
|
|
|
2315
3060
|
` codex model : ${options.codexModel ?? "(codex config)"}`,
|
|
2316
3061
|
` codex sandbox : ${options.codexSandbox ?? "read-only"}`
|
|
2317
3062
|
] : [],
|
|
3063
|
+
...options.runtime === "cursor" ? [` cursor model : ${options.cursorModel ?? "(cursor default)"}`] : [],
|
|
2318
3064
|
` max concurrency : ${maxConcurrency}`,
|
|
2319
3065
|
` reply enabled : ${options.replyEnabled}`,
|
|
2320
3066
|
` progress cards : ${options.cardEnabled && !options.dryRun}`,
|
|
@@ -2359,7 +3105,7 @@ function formatResult(label, result) {
|
|
|
2359
3105
|
return `[roy-task] ${label} skipped: ${result.reason}`;
|
|
2360
3106
|
}
|
|
2361
3107
|
}
|
|
2362
|
-
function
|
|
3108
|
+
function errMsg5(err) {
|
|
2363
3109
|
return err instanceof Error ? err.message : String(err);
|
|
2364
3110
|
}
|
|
2365
3111
|
|