@ai-setting/roy-task-plugin 0.7.1 → 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/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 /(^|__)task_create$/.test(name);
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
- if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
505
- const id = item.id;
506
- if (typeof id === "string" && id && this.state.tools.includes(id)) {} else {
507
- this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
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
- if (completed && this.state.createdTask === undefined) {
510
- this.state.createdTask = extractCreatedTask(item.aggregated_output);
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) => decoder.pushChunk(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}: ${errMsg(err)}` };
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}: ${errMsg(err)}` };
1167
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
611
1168
  }
612
- return { text: "", error: errMsg(err) };
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 errMsg(err) {
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 codex)
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 = createHash("sha256").update(material).digest("hex");
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
- const result = await this.deps.runAgent({
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
- const card = await this.openCard({
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
- return this.deps.gate ? this.deps.gate.run(plan.sessionId, run) : run();
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/claude.ts
1604
- import { createHash as createHash2 } from "node:crypto";
1605
- import { appendFileSync } from "node:fs";
1606
-
1607
- // ../../../packages/cli/src/daemon/runners/claude-agents.ts
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/claude-stream.ts
1756
- function errorDetail(result) {
1757
- if (typeof result.result === "string" && result.result.trim() !== "") {
1758
- return result.result.trim();
1759
- }
1760
- if (Array.isArray(result.errors)) {
1761
- const parts = result.errors.map((e) => typeof e === "string" ? e.trim() : "").filter((e) => e !== "");
1762
- if (parts.length > 0)
1763
- return parts.join("; ");
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 flattenToolResult(content) {
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 block of content) {
1774
- if (typeof block === "string") {
1775
- parts.push(block);
2281
+ for (const item of content) {
2282
+ if (typeof item === "string") {
2283
+ parts.push(item);
1776
2284
  continue;
1777
2285
  }
1778
- if (typeof block === "object" && block !== null) {
1779
- const t = block.text;
1780
- if (typeof t === "string")
1781
- parts.push(t);
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 ClaudeStreamDecoder {
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 { ...this.state, assistantText: [...this.state.assistantText], tools: [...this.state.tools] };
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.session_id === "string" ? ev.session_id : undefined;
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.type === "assistant") {
2374
+ if (ev["type"] === "assistant") {
1824
2375
  this.consumeAssistant(ev);
1825
2376
  return;
1826
2377
  }
1827
- if (ev.type === "user") {
1828
- this.consumeUser(ev);
2378
+ if (ev["type"] === "tool_call") {
2379
+ this.consumeToolCall(ev);
1829
2380
  return;
1830
2381
  }
1831
- if (ev.type === "result") {
2382
+ if (ev["type"] === "result") {
1832
2383
  this.state.result = ev;
1833
2384
  }
1834
2385
  }
1835
- consumeUser(ev) {
1836
- const message = ev.message;
1837
- const content = message?.content;
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 b = block;
1844
- if (b.type !== "tool_result")
1845
- continue;
1846
- const id = typeof b.tool_use_id === "string" ? b.tool_use_id : undefined;
1847
- const pending = id ? this.state.pendingCreates.get(id) : undefined;
1848
- const text = flattenToolResult(b.content);
1849
- if (this.state.createdTask !== undefined)
1850
- continue;
1851
- if (pending) {
1852
- this.state.pendingCreates.delete(id);
1853
- this.state.createdTask = extractCreatedTask(text, pending.titleHint);
1854
- continue;
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
- this.state.createdTask = extractCreatedTask(text);
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
- consumeAssistant(ev) {
1860
- const message = ev.message;
1861
- const content = message?.content;
1862
- if (!Array.isArray(content))
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 b = block;
1868
- if (b.type === "text" && typeof b.text === "string") {
1869
- this.state.assistantText.push(b.text);
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 { result } = this.state;
1887
- const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
1888
- const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
1889
- const createdTask = this.state.createdTask;
1890
- if (result?.is_error) {
1891
- const detail = errorDetail(result);
1892
- return {
1893
- text: "",
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 (typeof result?.result === "string" && result.result.trim() !== "") {
1902
- return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools, createdTask };
2461
+ if (out.text === "") {
2462
+ const joined = this.state.assistantText.join(`
2463
+ `).trim();
2464
+ if (joined !== "")
2465
+ out.text = joined;
1903
2466
  }
1904
- const fallback = this.state.assistantText.join("").trim();
1905
- return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools, createdTask };
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/claude.ts
1910
- var DEFAULT_CLAUDE_TIMEOUT_MS = 10 * 60 * 1000;
1911
- var CLAUDE_STDIO = Object.freeze([
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 buildClaudeArgs(input) {
1917
- const args = ["--print", "--output-format", "stream-json", "--verbose"];
1918
- if (input.agentsJson)
1919
- args.push("--agents", input.agentsJson);
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", claudeSessionUuid(input.sessionId));
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 isClaudeSessionUuid(value) {
1928
- if (!value)
1929
- return false;
1930
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
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 claudeSessionUuid(sessionKey) {
1933
- if (isClaudeSessionUuid(sessionKey))
1934
- return sessionKey;
1935
- const digest = createHash2("sha256").update(sessionKey).digest("hex");
1936
- const h = digest.slice(0, 32);
1937
- return [
1938
- h.slice(0, 8),
1939
- h.slice(8, 12),
1940
- `4${h.slice(13, 16)}`,
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 makeClaudeRunner(opts = {}) {
1946
- const bin = opts.bin ?? "claude";
1947
- const timeoutMs = opts.timeoutMs ?? DEFAULT_CLAUDE_TIMEOUT_MS;
1948
- const agentsJson = opts.agentsJson ?? buildClaudeAgentsJson();
1949
- const sessions = opts.sessions ?? createClaudeSessionRegistry();
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 = buildClaudeArgs({
2593
+ const args = buildCursorArgs({
1953
2594
  prompt: input.prompt,
1954
- agent: input.agent,
1955
- agentsJson,
2595
+ model: opts.model,
2596
+ workspace: workspaceDir,
2597
+ addDirs,
1956
2598
  sessionId: known
1957
2599
  });
1958
- const decoder = new ClaudeStreamDecoder;
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: CLAUDE_STDIO[0],
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
- appendFileSync(process.env["ROY_TASK_DUMP_STREAM"], chunk);
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}: ${errMsg2(err)}` };
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}: ${errMsg2(err)}` };
2642
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
1988
2643
  }
1989
- return { text: "", error: errMsg2(err) };
2644
+ return { text: "", error: errMsg3(err) };
1990
2645
  }
1991
2646
  const result = decoder.finish();
1992
2647
  const stderrText = stderr.trim();
1993
- const combined = result.error ? stderrText && !result.error.includes(stderrText) ? `${result.error} | ${stderrText}` : result.error : undefined;
1994
- const effective = combined ? { ...result, error: combined.slice(0, 1000) } : result;
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 (code === 124) {
1999
- return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
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
- if (code !== 0 && !effective.text) {
2008
- const error = `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`;
2009
- if (input.sessionId && STALE_SESSION_RE.test(error)) {
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 effective;
2662
+ return result;
2020
2663
  };
2021
2664
  }
2022
- var STALE_SESSION_RE = /No conversation found|not a UUID|session title/i;
2023
- function errMsg2(err) {
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) => assembler.pushChunk(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}: ${errMsg3(err)}` };
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}: ${errMsg3(err)}` };
2714
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg4(err)}` };
2063
2715
  }
2064
- return { text: "", error: errMsg3(err) };
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 errMsg3(err) {
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: ${errMsg4(err)}`);
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: ${errMsg4(err)}`);
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: ${errMsg4(err)}`);
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 errMsg4(err) {
3108
+ function errMsg5(err) {
2363
3109
  return err instanceof Error ? err.message : String(err);
2364
3110
  }
2365
3111
 
@@ -2484,7 +3230,7 @@ function formatStatus(report) {
2484
3230
  }
2485
3231
 
2486
3232
  // ../../../packages/cli/src/main.ts
2487
- var VERSION = "0.7.1";
3233
+ var VERSION = "0.7.2";
2488
3234
  async function main(argv) {
2489
3235
  const parsed = parseArgs(argv);
2490
3236
  switch (parsed.command) {