skimpyclaw 0.3.15 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +100 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +348 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  // Code Agent Utilities
2
2
  import { execSync } from 'child_process';
3
+ import { existsSync, readFileSync } from 'fs';
3
4
  import { resolve, join } from 'path';
4
5
  import { homedir } from 'os';
5
6
  import { buildValidationCommand } from './executor.js';
@@ -21,9 +22,25 @@ function isCommandAvailable(name) {
21
22
  return false;
22
23
  }
23
24
  }
24
- const CLAUDE_CLI_PATH = resolveCliPath('claude');
25
+ export const CLAUDE_CLI_PATH = resolveCliPath('claude');
25
26
  const CODEX_CLI_PATH = resolveCliPath('codex');
26
- const KIMI_CLI_PATH = resolveCliPath('kimi');
27
+ const DEFAULT_CODEX_HOME = join(homedir(), '.codex');
28
+ /**
29
+ * Prepare env for spawning a coding-agent CLI (claude/codex):
30
+ * - Drops CLAUDECODE so nested `claude` invocations start cleanly.
31
+ * - Drops GH_TOKEN/GITHUB_TOKEN so `gh` falls back to keychain auth instead
32
+ * of a stale process token.
33
+ * - Pins Codex to the standard CLI state directory so auth/config come from ~/.codex.
34
+ */
35
+ export function buildCodeAgentSpawnEnv(base = process.env) {
36
+ const env = { ...base };
37
+ env.HOME ||= homedir();
38
+ env.CODEX_HOME = DEFAULT_CODEX_HOME;
39
+ delete env.CLAUDECODE;
40
+ delete env.GH_TOKEN;
41
+ delete env.GITHUB_TOKEN;
42
+ return env;
43
+ }
27
44
  /** Return supported coding CLIs currently available on PATH. */
28
45
  export function getAvailableCodingCliTools(commandChecker = isCommandAvailable) {
29
46
  const available = [];
@@ -31,19 +48,17 @@ export function getAvailableCodingCliTools(commandChecker = isCommandAvailable)
31
48
  available.push('codex');
32
49
  if (commandChecker('claude') || commandChecker('claude-code'))
33
50
  available.push('claude');
34
- if (commandChecker('kimi'))
35
- available.push('kimi');
36
51
  return available;
37
52
  }
38
53
  /** Return preflight error when no supported coding CLI is installed. */
39
54
  export function getCodingCliPreflightError(commandChecker = isCommandAvailable) {
40
55
  if (getAvailableCodingCliTools(commandChecker).length > 0)
41
56
  return null;
42
- return 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`), Claude Code CLI (`claude` or `claude-code`), or Kimi CLI (`kimi`).';
57
+ return 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`) or Claude Code CLI (`claude` or `claude-code`).';
43
58
  }
44
59
  /**
45
60
  * Normalize legacy/default agent values to supported CLI agent IDs.
46
- * Accepts strict IDs and older alias-like values (e.g. "claude-think").
61
+ * Accepts strict IDs and older alias-like values (e.g. "claude-coder").
47
62
  */
48
63
  export function normalizeCodeAgent(agent) {
49
64
  if (!agent)
@@ -53,16 +68,25 @@ export function normalizeCodeAgent(agent) {
53
68
  return 'claude';
54
69
  if (value === 'codex' || value.startsWith('codex'))
55
70
  return 'codex';
56
- if (value === 'kimi' || value.startsWith('kimi'))
57
- return 'kimi';
58
71
  return null;
59
72
  }
60
73
  /**
61
74
  * Resolve requested/default agent selection to a supported CLI agent ID.
62
75
  * Preference order: explicit request -> configured default -> "claude".
63
- * If no agent is explicit and the model is a GPT/OpenAI model, auto-select codex.
64
- * If the model is a kimi model, auto-select kimi.
76
+ * If no agent is explicit and the model is a GPT/Codex model, auto-select codex.
65
77
  */
78
+ /**
79
+ * Check if a model string is compatible with a given agent CLI.
80
+ * e.g. gpt-5.3-codex is NOT compatible with 'claude', claude-opus IS.
81
+ */
82
+ export function isModelCompatibleWithAgent(model, agent) {
83
+ const m = model.toLowerCase();
84
+ if (agent === 'codex') {
85
+ return /^(gpt|codex|o[134]|openai\/)/i.test(m);
86
+ }
87
+ // claude: compatible if NOT a known non-Claude model
88
+ return !/^(gpt|codex|o[134]|openai\/)/i.test(m);
89
+ }
66
90
  export function resolveSelectedCodeAgent(requestedAgent, defaultAgent, model) {
67
91
  // If no explicit agent was requested, infer from model
68
92
  if (!requestedAgent && model) {
@@ -70,8 +94,6 @@ export function resolveSelectedCodeAgent(requestedAgent, defaultAgent, model) {
70
94
  if (m.includes('gpt') || m.includes('codex') || m.startsWith('openai/') || m.startsWith('o1') || m.startsWith('o3') || m.startsWith('o4')) {
71
95
  return 'codex';
72
96
  }
73
- if (m.includes('kimi'))
74
- return 'kimi';
75
97
  }
76
98
  const candidate = requestedAgent || defaultAgent || 'claude';
77
99
  return normalizeCodeAgent(candidate);
@@ -85,6 +107,41 @@ export function setCodeAgentConfig(config) {
85
107
  export function getCodeAgentConfig() {
86
108
  return _codeAgentConfig;
87
109
  }
110
+ function stripProviderPrefix(model) {
111
+ return model.includes('/') ? model.split('/').slice(1).join('/') : model;
112
+ }
113
+ /** Read Claude Code's configured default model, if present. */
114
+ export function readClaudeCodeDefaultModel(settingsPath = join(homedir(), '.claude', 'settings.json')) {
115
+ try {
116
+ if (!existsSync(settingsPath))
117
+ return undefined;
118
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
119
+ const model = settings?.model;
120
+ return typeof model === 'string' && model.trim() ? model.trim() : undefined;
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ /** Resolve the model text shown in dashboards/reports without changing CLI args. */
127
+ export function resolveCodeAgentModelLabel(agent, model) {
128
+ if (model)
129
+ return stripProviderPrefix(model);
130
+ const normalizedAgent = normalizeCodeAgent(agent);
131
+ if (normalizedAgent === 'claude') {
132
+ const defaultModel = readClaudeCodeDefaultModel();
133
+ return defaultModel ? `${defaultModel} default` : 'claude default';
134
+ }
135
+ if (normalizedAgent === 'codex')
136
+ return 'codex default';
137
+ return `${agent || 'agent'} default`;
138
+ }
139
+ export function withCodeAgentModelLabel(task) {
140
+ return {
141
+ ...task,
142
+ modelLabel: task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model),
143
+ };
144
+ }
88
145
  /** Build CLI args for code_with_agent. Exported for testing. */
89
146
  export function buildCodeAgentArgs(input) {
90
147
  const agent = input.agent || 'claude';
@@ -101,51 +158,32 @@ export function buildCodeAgentArgs(input) {
101
158
  args.push('-C', input.workdir);
102
159
  if (input.model)
103
160
  args.push('-m', input.model);
161
+ if (input.effort)
162
+ args.push('-c', `model_reasoning_effort=${input.effort}`);
104
163
  args.push(input.task);
105
164
  return { cmd: CODEX_CLI_PATH, args };
106
165
  }
107
- if (agent === 'kimi') {
108
- const args = [
109
- '--yolo',
110
- '-p', input.task,
111
- ];
112
- if (input.workdir)
113
- args.push('-w', input.workdir);
114
- if (input.model)
115
- args.push('-m', input.model);
116
- return { cmd: KIMI_CLI_PATH, args };
117
- }
118
166
  // Default: claude
119
167
  // Each --allowedTools flag takes one tool name — repeat the flag per tool
120
168
  // --allowedTools restricts which tools are available (not just permissions).
121
- // Include Playwright MCP tools so the agent can use the browser.
122
- const allowedTools = ['Edit', 'Read', 'Write', 'Bash', 'Glob', 'Grep', 'mcp__playwright__*'];
169
+ const allowedTools = ['Edit', 'Read', 'Write', 'Bash', 'Glob', 'Grep'];
123
170
  const toolArgs = allowedTools.flatMap(t => ['--allowedTools', t]);
124
- // Pass Playwright MCP server so coding agents share SkimpyClaw's browser profile
125
- // Must use chromium (not chrome) to match SkimpyClaw's browser-tool.ts persistent context
126
- const playwrightMcp = JSON.stringify({
127
- mcpServers: {
128
- playwright: {
129
- command: 'npx',
130
- args: ['-y', '@playwright/mcp@latest', '--browser', 'chromium',
131
- '--user-data-dir', join(homedir(), '.skimpyclaw', 'browser-profile'),
132
- '--caps', 'vision'],
133
- },
134
- },
135
- });
136
171
  const args = [
137
172
  '-p',
138
173
  '--verbose',
139
174
  '--output-format', 'stream-json',
140
175
  '--dangerously-skip-permissions',
141
- '--mcp-config', playwrightMcp,
142
176
  ...toolArgs,
143
177
  '--max-turns', maxTurns,
144
178
  '--append-system-prompt', `Output text only. Never use say or TTS. Focus on the coding task. Run ${buildValidationCommand(input.workdir || process.cwd())} to verify changes.`,
145
179
  ];
180
+ // Interactive mode: pin this turn to a known session UUID so follow-ups can --resume it.
181
+ if (input.sessionId) {
182
+ args.push('--session-id', input.sessionId);
183
+ }
146
184
  // Only pass model to Claude CLI if it's not a known non-Claude model.
147
- // GPT/Codex/Kimi/o-series models would be rejected by the Claude CLI.
148
- if (input.model && !/^(gpt|codex|kimi|o[134]|openai\/)/i.test(input.model)) {
185
+ // GPT/Codex/o-series models would be rejected by the Claude CLI.
186
+ if (input.model && !/^(gpt|codex|o[134]|openai\/)/i.test(input.model)) {
149
187
  args.push('--model', input.model);
150
188
  }
151
189
  args.push(input.task);
@@ -157,42 +195,6 @@ export function formatDuration(seconds) {
157
195
  return '?';
158
196
  return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
159
197
  }
160
- /** Build notification for a team-coordinator task with child results. */
161
- export function buildTeamNotification(task, getChildTask) {
162
- const dur = formatDuration(task.durationSeconds);
163
- const taskPreview = task.task.length > 100 ? task.task.slice(0, 100) + '...' : task.task;
164
- const statusIcon = task.status === 'completed' ? '✅' : task.status === 'timeout' ? '⏰' : '❌';
165
- const validation = task.validationPassed ? ' Tests pass.' : '';
166
- const lines = [];
167
- lines.push(`${statusIcon} Team ${task.id} ${task.status} (${dur}).${validation}`);
168
- lines.push(`Task: ${taskPreview}`);
169
- // Per-child summary
170
- const childIds = task.childTaskIds || [];
171
- if (childIds.length > 0) {
172
- lines.push('');
173
- for (const childId of childIds) {
174
- const child = getChildTask(childId);
175
- if (!child)
176
- continue;
177
- const childIcon = child.status === 'completed' ? '✅' : child.status === 'failed' ? '❌' : child.status === 'timeout' ? '⏰' : '❓';
178
- const childDur = formatDuration(child.durationSeconds);
179
- const subtask = (child.subtask || child.task || '').slice(0, 80);
180
- lines.push(` ${childIcon} ${child.id} (${childDur}): ${subtask}`);
181
- }
182
- }
183
- // Synthesis result
184
- if (task.outputPreview) {
185
- lines.push(`\nResult: ${task.outputPreview}`);
186
- }
187
- // Errors
188
- if (task.error && task.error !== 'Validation failed') {
189
- lines.push(`\nError: ${task.error}`);
190
- }
191
- if (task.validationOutput) {
192
- lines.push(`\nValidation:\n${task.validationOutput.slice(0, 800)}`);
193
- }
194
- return lines.join('\n');
195
- }
196
198
  /** Build notification for a single code agent. */
197
199
  export function buildSoloNotification(task) {
198
200
  const dur = formatDuration(task.durationSeconds);
@@ -219,30 +221,282 @@ export function buildSoloNotification(task) {
219
221
  return message;
220
222
  }
221
223
  }
224
+ function shorten(value, maxChars) {
225
+ const trimmed = value.trim();
226
+ if (trimmed.length <= maxChars)
227
+ return trimmed;
228
+ return `${trimmed.slice(0, maxChars - 3).trim()}...`;
229
+ }
230
+ function normalizeOutputPaths(value, workdir) {
231
+ if (!value)
232
+ return value;
233
+ const normalizedWorkdir = workdir.replace(/\/+$/, '');
234
+ if (!normalizedWorkdir)
235
+ return value;
236
+ return value
237
+ .split(`${normalizedWorkdir}/`).join('')
238
+ .split(normalizedWorkdir).join('.');
239
+ }
240
+ function extractStreamJsonText(line) {
241
+ let event;
242
+ try {
243
+ event = JSON.parse(line);
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ if (!event || typeof event !== 'object' || typeof event.type !== 'string') {
249
+ return null;
250
+ }
251
+ if ((event.type === 'output_text' || event.output_text) && (event.output_text || event.text)) {
252
+ return String(event.output_text || event.text).trim() || null;
253
+ }
254
+ if (event.type === 'result' && typeof event.result === 'string') {
255
+ return event.result.trim() || null;
256
+ }
257
+ if ((event.type === 'item.completed' || event.type === 'item.delta') && event.item?.type === 'agent_message' && event.item?.text) {
258
+ return String(event.item.text).trim() || null;
259
+ }
260
+ return '';
261
+ }
262
+ function looksLikeRawStreamJson(line) {
263
+ const trimmed = line.trim();
264
+ return trimmed.startsWith('{') && /"type"\s*:|"item"\s*:|"thread_id"\s*:/.test(trimmed);
265
+ }
266
+ function stripStreamJsonNoise(value) {
267
+ const lines = value.split('\n');
268
+ const cleaned = [];
269
+ let sawStreamJson = false;
270
+ for (const line of lines) {
271
+ const extracted = extractStreamJsonText(line);
272
+ if (extracted !== null) {
273
+ sawStreamJson = true;
274
+ if (extracted)
275
+ cleaned.push(extracted);
276
+ continue;
277
+ }
278
+ if (looksLikeRawStreamJson(line)) {
279
+ sawStreamJson = true;
280
+ continue;
281
+ }
282
+ cleaned.push(line);
283
+ }
284
+ const result = cleaned.join('\n').trim();
285
+ return sawStreamJson ? result : value.trim();
286
+ }
287
+ function normalizeHeading(line) {
288
+ const normalized = line
289
+ .trim()
290
+ .replace(/^#{1,6}\s*/, '')
291
+ .replace(/^\*\*/, '')
292
+ .replace(/\*\*$/, '')
293
+ .replace(/:$/, '')
294
+ .trim()
295
+ .toLowerCase();
296
+ const allowed = new Set(['decision', 'summary', 'findings', 'risk checks', 'tests', 'recommendation']);
297
+ return allowed.has(normalized) ? normalized : null;
298
+ }
299
+ function extractReportSections(value) {
300
+ const sections = {};
301
+ let current = 'summary';
302
+ for (const line of value.split('\n')) {
303
+ const heading = normalizeHeading(line);
304
+ if (heading) {
305
+ current = heading;
306
+ sections[current] ||= [];
307
+ continue;
308
+ }
309
+ sections[current] ||= [];
310
+ sections[current].push(line);
311
+ }
312
+ return Object.fromEntries(Object.entries(sections)
313
+ .map(([key, lines]) => [key, lines.join('\n').trim()])
314
+ .filter(([, text]) => text));
315
+ }
316
+ function firstMeaningfulLines(value, maxChars) {
317
+ const lines = value
318
+ .split('\n')
319
+ .map(line => line.trim())
320
+ .filter(Boolean)
321
+ .filter(line => !looksLikeRawStreamJson(line))
322
+ .filter(line => !/^i('|’)ll\b/i.test(line))
323
+ .filter(line => !/^i am\b/i.test(line));
324
+ return shorten(lines.join('\n'), maxChars);
325
+ }
326
+ function formatAgentDisplay(task) {
327
+ const agent = task.agent === 'team-coordinator' ? 'TEAM' : task.agent.toUpperCase();
328
+ const model = task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model);
329
+ const effort = task.effort ? ` · effort ${task.effort}` : '';
330
+ return `${agent} · ${model}${effort}`;
331
+ }
332
+ function buildReportMarkdown(task, result) {
333
+ const validation = task.validationPassed === true
334
+ ? 'pass'
335
+ : task.validationPassed === false
336
+ ? 'fail'
337
+ : 'not run';
338
+ return [
339
+ `# Code Agent ${task.id}`,
340
+ '',
341
+ `- Status: ${task.status}`,
342
+ `- Agent: ${task.agent}`,
343
+ `- Model: ${task.modelLabel || resolveCodeAgentModelLabel(task.agent, task.model)}`,
344
+ `- Effort: ${task.effort || 'default'}`,
345
+ `- Duration: ${formatDuration(task.durationSeconds)}`,
346
+ `- Validation: ${validation}`,
347
+ `- Workdir: ${task.workdir}`,
348
+ task.sourceWorkdir ? `- Source Workdir: ${task.sourceWorkdir}` : '',
349
+ task.worktreePath ? `- Worktree: ${task.worktreePath}` : '',
350
+ task.worktreeCleanup ? `- Worktree Cleanup: ${task.worktreeCleanup.status}${task.worktreeCleanup.reason ? ` (${task.worktreeCleanup.reason})` : ''}` : '',
351
+ '',
352
+ '## Task',
353
+ '',
354
+ task.task,
355
+ '',
356
+ '## Result',
357
+ '',
358
+ result || task.error || '(No result captured.)',
359
+ task.validationOutput ? `\n## Validation Output\n\n${task.validationOutput}` : '',
360
+ ].filter(Boolean).join('\n');
361
+ }
362
+ function buildDiscordResultSummary(task, result) {
363
+ if (!result.trim())
364
+ return task.error ? `Error: ${shorten(task.error, 700)}` : '';
365
+ const sections = extractReportSections(result);
366
+ const lines = [];
367
+ const decision = sections.decision || sections.recommendation;
368
+ if (decision) {
369
+ lines.push(`**Decision**\n${shorten(firstMeaningfulLines(decision, 450), 450)}`);
370
+ }
371
+ if (sections.findings) {
372
+ lines.push(`**Findings**\n${shorten(firstMeaningfulLines(sections.findings, 650), 650)}`);
373
+ }
374
+ if (sections['risk checks']) {
375
+ lines.push(`**Risk Checks**\n${shorten(firstMeaningfulLines(sections['risk checks'], 420), 420)}`);
376
+ }
377
+ if (sections.tests) {
378
+ lines.push(`**Tests**\n${shorten(firstMeaningfulLines(sections.tests, 240), 240)}`);
379
+ }
380
+ if (lines.length > 0)
381
+ return lines.join('\n\n');
382
+ return shorten(firstMeaningfulLines(result, 950), 950);
383
+ }
384
+ export function buildCodeAgentDiscordNotification(task) {
385
+ const dur = formatDuration(task.durationSeconds);
386
+ const icon = task.status === 'completed' ? '✅' : task.status === 'timeout' ? '⏰' : '❌';
387
+ const status = task.status === 'completed'
388
+ ? 'completed'
389
+ : task.status === 'timeout'
390
+ ? 'timed out'
391
+ : 'failed';
392
+ const validation = task.validationPassed === true
393
+ ? 'Build/tests pass'
394
+ : task.validationPassed === false
395
+ ? 'Build/tests failed'
396
+ : task.status === 'completed'
397
+ ? 'Validation not run'
398
+ : undefined;
399
+ const rawResult = normalizeOutputPaths(stripStreamJsonNoise(task.outputPreview || task.liveOutput || task.validationOutput || task.error || ''), task.workdir);
400
+ const taskPreview = normalizeOutputPaths(shorten(task.task, 350), task.workdir);
401
+ const summary = buildDiscordResultSummary(task, rawResult);
402
+ const contentParts = [
403
+ `${icon} \`${task.id}\` ${status} · ${formatAgentDisplay(task)} · ${dur}`,
404
+ validation ? `**Validation:** ${validation}` : undefined,
405
+ task.retryCount ? `**Retries:** ${task.retryCount}` : undefined,
406
+ `**Task:** ${taskPreview}`,
407
+ summary ? `\n${summary}` : undefined,
408
+ ].filter(Boolean);
409
+ const needsAttachment = rawResult.length > 1_200 || task.validationOutput || task.task.length > 350;
410
+ const attachment = needsAttachment
411
+ ? {
412
+ name: `${task.id}-report.md`,
413
+ description: `Full report for ${task.id}`,
414
+ content: buildReportMarkdown(task, rawResult),
415
+ }
416
+ : undefined;
417
+ const content = [
418
+ contentParts.join('\n'),
419
+ attachment ? '\nFull report attached.' : undefined,
420
+ ].filter(Boolean).join('\n');
421
+ return { content, attachment };
422
+ }
423
+ function resolveDiscordThreadId(task) {
424
+ return task.discordThreadId;
425
+ }
426
+ function resolveDiscordChannelId(task) {
427
+ return task.discordChannelId;
428
+ }
429
+ /**
430
+ * Try to send a notification to a Discord thread associated with this task.
431
+ * Returns true if successfully sent to thread.
432
+ */
433
+ async function trySendToDiscordThread(task, notification) {
434
+ const threadId = resolveDiscordThreadId(task);
435
+ if (!threadId)
436
+ return false;
437
+ try {
438
+ const { sendToDiscordThread, sendToDiscordThreadWithAttachments } = await import('../channels/discord/index.js');
439
+ const sent = notification.attachment
440
+ ? await sendToDiscordThreadWithAttachments(threadId, notification.content, [notification.attachment])
441
+ : await sendToDiscordThread(threadId, notification.content);
442
+ if (sent) {
443
+ console.log(`[code-agent] Notification for ${task.id} sent to thread ${threadId}`);
444
+ }
445
+ return sent;
446
+ }
447
+ catch (err) {
448
+ console.error(`[code-agent] Failed to send to Discord thread for ${task.id}:`, err);
449
+ return false;
450
+ }
451
+ }
452
+ /**
453
+ * Try to send a notification to the originating Discord channel for this task.
454
+ * Returns true if successfully sent to channel.
455
+ */
456
+ async function trySendToDiscordChannel(task, notification) {
457
+ const channelId = resolveDiscordChannelId(task);
458
+ if (!channelId)
459
+ return false;
460
+ try {
461
+ const { sendDiscordProactiveMessage, sendDiscordProactiveMessageWithAttachments } = await import('../channels/discord/index.js');
462
+ if (notification.attachment) {
463
+ await sendDiscordProactiveMessageWithAttachments(channelId, notification.content, [notification.attachment]);
464
+ }
465
+ else {
466
+ await sendDiscordProactiveMessage(channelId, notification.content);
467
+ }
468
+ console.log(`[code-agent] Notification for ${task.id} sent to Discord channel ${channelId}`);
469
+ return true;
470
+ }
471
+ catch (err) {
472
+ console.error(`[code-agent] Failed to send to Discord channel for ${task.id}:`, err);
473
+ return false;
474
+ }
475
+ }
476
+ function hasDiscordRouting(task) {
477
+ return !!(resolveDiscordThreadId(task) || resolveDiscordChannelId(task));
478
+ }
222
479
  /** Send auto-notification to active channel on completion/failure. */
223
- export async function notifyCodeAgentResult(task, getChildTask) {
480
+ export async function notifyCodeAgentResult(task) {
224
481
  if (!_codeAgentConfig)
225
482
  return;
226
483
  const { sendActiveChannelProactiveMessage } = await import('../channels.js');
227
- let message;
228
- // Team coordinator gets a structured notification
229
- if (task.agent === 'team-coordinator') {
230
- message = buildTeamNotification(task, getChildTask);
484
+ const message = buildSoloNotification(task);
485
+ const discordNotification = buildCodeAgentDiscordNotification(task);
486
+ // Prefer task-scoped Discord routing over global active-channel fallback.
487
+ const threadSent = await trySendToDiscordThread(task, discordNotification);
488
+ const channelSent = threadSent ? true : await trySendToDiscordChannel(task, discordNotification);
489
+ if (!threadSent && !channelSent && !hasDiscordRouting(task)) {
231
490
  const sent = await sendActiveChannelProactiveMessage(_codeAgentConfig, message).catch((err) => {
232
- console.error(`[code-agent] Failed to send team notification for ${task.id}:`, err);
491
+ console.error(`[code-agent] Failed to send notification for ${task.id}:`, err);
233
492
  return false;
234
493
  });
235
494
  if (!sent)
236
- console.warn(`[code-agent] Team notification not delivered for ${task.id} (no active channel or target)`);
237
- return;
495
+ console.warn(`[code-agent] Notification not delivered for ${task.id} (no active channel or target)`);
496
+ }
497
+ else if (!threadSent && !channelSent) {
498
+ console.warn(`[code-agent] Notification not delivered for ${task.id} (Discord thread/channel unavailable)`);
238
499
  }
239
- message = buildSoloNotification(task);
240
- const sent = await sendActiveChannelProactiveMessage(_codeAgentConfig, message).catch((err) => {
241
- console.error(`[code-agent] Failed to send notification for ${task.id}:`, err);
242
- return false;
243
- });
244
- if (!sent)
245
- console.warn(`[code-agent] Notification not delivered for ${task.id} (no active channel or target)`);
246
500
  }
247
501
  /** Check workdir against allowed paths. */
248
502
  export function resolveWorkdir(rawWorkdir, projects, skimpyclawRoot) {
@@ -269,9 +523,8 @@ export function resolveModelAlias(model, aliases) {
269
523
  if (/^claude[-.]3[-.]5[-.]haiku(?:[-_.].*)?$/i.test(model)) {
270
524
  return 'claude-haiku-4-5';
271
525
  }
526
+ if (/^claude[-.]opus[-.]4[-_.]6$/i.test(model)) {
527
+ return 'claude-opus-4-6';
528
+ }
272
529
  return model;
273
530
  }
274
- /** @deprecated Removed — old Claude CLI team state reader. Kept for backward compat. */
275
- export function readTeamState() {
276
- return null;
277
- }
@@ -0,0 +1,37 @@
1
+ export type CodeAgentWorktreeMode = 'off' | 'auto' | 'always';
2
+ export type CodeAgentWorktreeRequest = boolean | 'auto' | undefined;
3
+ export interface CodeAgentWorktreeConfig {
4
+ enabled?: boolean;
5
+ mode?: CodeAgentWorktreeMode;
6
+ root?: string;
7
+ cleanup?: boolean;
8
+ }
9
+ export interface PreparedCodeAgentWorktree {
10
+ sourceWorkdir: string;
11
+ worktreePath: string;
12
+ runWorkdir: string;
13
+ worktreeRef: string;
14
+ gitRoot: string;
15
+ }
16
+ export interface CodeAgentWorktreeCleanupResult {
17
+ status: 'removed' | 'preserved' | 'skipped' | 'failed';
18
+ path?: string;
19
+ reason?: string;
20
+ at: string;
21
+ }
22
+ export declare function normalizeWorktreeRequest(value: unknown): CodeAgentWorktreeRequest;
23
+ export declare function shouldAutoWorktreeTask(task: string): boolean;
24
+ export declare function shouldUseCodeAgentWorktree(task: string, request: CodeAgentWorktreeRequest, config?: CodeAgentWorktreeConfig): boolean;
25
+ export declare function findGitRoot(workdir: string): string | null;
26
+ export declare function prepareCodeAgentWorktree(options: {
27
+ id: string;
28
+ sourceWorkdir: string;
29
+ config?: CodeAgentWorktreeConfig;
30
+ required?: boolean;
31
+ }): PreparedCodeAgentWorktree | undefined;
32
+ export declare function cleanupCodeAgentWorktree(options: {
33
+ sourceWorkdir?: string;
34
+ worktreePath?: string;
35
+ worktreeRef?: string;
36
+ config?: CodeAgentWorktreeConfig;
37
+ }): CodeAgentWorktreeCleanupResult;