@zibby/core 0.1.21 → 0.1.23

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 (167) hide show
  1. package/dist/agents/base.js +17 -0
  2. package/dist/backend-client.js +1 -0
  3. package/dist/constants/tool-names.js +1 -0
  4. package/dist/constants/zibby-scratch.js +1 -0
  5. package/dist/constants.js +1 -0
  6. package/dist/enrichment/base.js +1 -0
  7. package/dist/enrichment/enrichers/accessibility-enricher.js +1 -0
  8. package/dist/enrichment/enrichers/dom-enricher.js +1 -0
  9. package/dist/enrichment/enrichers/page-state-enricher.js +1 -0
  10. package/dist/enrichment/enrichers/position-enricher.js +1 -0
  11. package/dist/enrichment/index.js +1 -0
  12. package/dist/enrichment/mcp-integration.js +1 -0
  13. package/dist/enrichment/mcp-ref-enricher.js +1 -0
  14. package/dist/enrichment/pipeline.js +3 -0
  15. package/dist/enrichment/trace-text-enricher.js +1 -0
  16. package/dist/framework/agents/assistant-strategy.js +5 -0
  17. package/dist/framework/agents/base.js +1 -0
  18. package/dist/framework/agents/claude-strategy.js +4 -0
  19. package/dist/framework/agents/codex-strategy.js +4 -0
  20. package/dist/framework/agents/cursor-strategy.js +32 -0
  21. package/dist/framework/agents/gemini-strategy.js +11 -0
  22. package/dist/framework/agents/index.js +13 -0
  23. package/dist/framework/agents/middleware/assistant-round-pipeline.js +3 -0
  24. package/dist/framework/agents/providers/base.js +1 -0
  25. package/dist/framework/agents/providers/index.js +1 -0
  26. package/dist/framework/agents/providers/openai-transport.js +2 -0
  27. package/dist/framework/agents/providers/openai.js +1 -0
  28. package/dist/framework/agents/providers/transport-base.js +1 -0
  29. package/dist/framework/agents/utils/auth-resolver.js +1 -0
  30. package/dist/framework/agents/utils/cursor-output-formatter.js +1 -0
  31. package/dist/framework/agents/utils/openai-proxy-formatter.js +9 -0
  32. package/dist/framework/agents/utils/payload-budget.js +3 -0
  33. package/dist/framework/agents/utils/structured-output-formatter.js +21 -0
  34. package/dist/framework/code-generator.js +10 -0
  35. package/dist/framework/constants.js +1 -0
  36. package/dist/framework/context-loader.js +5 -0
  37. package/dist/framework/function-bridge.js +2 -0
  38. package/dist/framework/function-skill-registry.js +1 -0
  39. package/dist/framework/graph-compiler.js +1 -0
  40. package/dist/framework/graph.js +5 -0
  41. package/dist/framework/index.js +1 -0
  42. package/dist/framework/mcp-client.js +2 -0
  43. package/dist/framework/node-registry.js +9 -0
  44. package/dist/framework/node.js +5 -0
  45. package/dist/framework/output-parser.js +3 -0
  46. package/dist/framework/skill-registry.js +1 -0
  47. package/dist/framework/state-utils.js +1 -0
  48. package/dist/framework/state.js +1 -0
  49. package/dist/framework/tool-resolver.js +1 -0
  50. package/dist/index.js +8 -0
  51. package/dist/runtime/generation/base.js +1 -0
  52. package/dist/runtime/generation/index.js +3 -0
  53. package/dist/runtime/generation/mcp-ref-strategy.js +41 -0
  54. package/dist/runtime/generation/stable-id-strategy.js +16 -0
  55. package/dist/runtime/stable-id-runtime.js +1 -0
  56. package/dist/runtime/verification/base.js +1 -0
  57. package/dist/runtime/verification/index.js +3 -0
  58. package/dist/runtime/verification/playwright-json-strategy.js +1 -0
  59. package/dist/runtime/zibby-runtime.js +1 -0
  60. package/dist/sync/index.js +1 -0
  61. package/dist/sync/uploader.js +1 -0
  62. package/dist/tools/run-playwright-test.js +5 -0
  63. package/dist/utils/adf-converter.js +7 -0
  64. package/dist/utils/ast-utils.js +1 -0
  65. package/dist/utils/ci-setup.js +5 -0
  66. package/dist/utils/cursor-mcp-isolated-home.js +1 -0
  67. package/dist/utils/cursor-utils.js +18 -0
  68. package/dist/utils/live-frame-discovery.js +1 -0
  69. package/dist/utils/logger.js +1 -0
  70. package/dist/utils/mcp-config-writer.js +10 -0
  71. package/dist/utils/mission-control-from-run-states.js +1 -0
  72. package/dist/utils/node-schema-parser.js +1 -0
  73. package/dist/utils/parallel-config.js +1 -0
  74. package/dist/utils/post-process-events.js +1 -0
  75. package/dist/utils/result-handler.js +1 -0
  76. package/{src → dist}/utils/ripple-effect.js +3 -12
  77. package/dist/utils/run-capacity-coordinator.js +1 -0
  78. package/dist/utils/run-capacity-queue.js +2 -0
  79. package/dist/utils/run-index-merge.js +1 -0
  80. package/dist/utils/run-index-post-cli.js +1 -0
  81. package/dist/utils/run-registry.js +3 -0
  82. package/dist/utils/run-state-session.js +2 -0
  83. package/dist/utils/selector-generator.js +4 -0
  84. package/dist/utils/session-state-constants.js +1 -0
  85. package/dist/utils/session-state-live-runs.js +1 -0
  86. package/dist/utils/streaming-parser.js +4 -0
  87. package/dist/utils/test-post-processor.js +18 -0
  88. package/dist/utils/timeline.js +14 -0
  89. package/dist/utils/trace-parser.js +2 -0
  90. package/dist/utils/video-organizer.js +3 -0
  91. package/package.json +49 -35
  92. package/templates/browser-test-automation/README.md +29 -7
  93. package/templates/browser-test-automation/chat.mjs +36 -0
  94. package/templates/browser-test-automation/graph.mjs +5 -9
  95. package/templates/browser-test-automation/nodes/execute-live.mjs +30 -58
  96. package/templates/browser-test-automation/nodes/generate-script.mjs +32 -12
  97. package/templates/browser-test-automation/nodes/utils.mjs +153 -10
  98. package/templates/browser-test-automation/pipeline-ids.js +12 -0
  99. package/templates/browser-test-automation/result-handler.mjs +78 -2
  100. package/templates/browser-test-automation/run-index.mjs +418 -0
  101. package/scripts/export-default-workflows.js +0 -51
  102. package/scripts/patch-cursor-mcp.js +0 -174
  103. package/scripts/setup-ci.sh +0 -115
  104. package/scripts/setup-official-playwright-mcp.sh +0 -226
  105. package/scripts/test-with-video.sh +0 -49
  106. package/src/agents/base.js +0 -361
  107. package/src/constants.js +0 -47
  108. package/src/enrichment/base.js +0 -49
  109. package/src/enrichment/enrichers/accessibility-enricher.js +0 -197
  110. package/src/enrichment/enrichers/dom-enricher.js +0 -171
  111. package/src/enrichment/enrichers/page-state-enricher.js +0 -129
  112. package/src/enrichment/enrichers/position-enricher.js +0 -67
  113. package/src/enrichment/index.js +0 -96
  114. package/src/enrichment/mcp-integration.js +0 -149
  115. package/src/enrichment/mcp-ref-enricher.js +0 -78
  116. package/src/enrichment/pipeline.js +0 -192
  117. package/src/enrichment/trace-text-enricher.js +0 -115
  118. package/src/framework/AGENTS.md +0 -98
  119. package/src/framework/agents/base.js +0 -72
  120. package/src/framework/agents/claude-strategy.js +0 -278
  121. package/src/framework/agents/cursor-strategy.js +0 -544
  122. package/src/framework/agents/index.js +0 -105
  123. package/src/framework/agents/utils/cursor-output-formatter.js +0 -67
  124. package/src/framework/agents/utils/openai-proxy-formatter.js +0 -249
  125. package/src/framework/code-generator.js +0 -301
  126. package/src/framework/constants.js +0 -33
  127. package/src/framework/context-loader.js +0 -101
  128. package/src/framework/function-bridge.js +0 -78
  129. package/src/framework/function-skill-registry.js +0 -20
  130. package/src/framework/graph-compiler.js +0 -342
  131. package/src/framework/graph.js +0 -610
  132. package/src/framework/index.js +0 -28
  133. package/src/framework/node-registry.js +0 -163
  134. package/src/framework/node.js +0 -259
  135. package/src/framework/output-parser.js +0 -71
  136. package/src/framework/skill-registry.js +0 -55
  137. package/src/framework/state-utils.js +0 -52
  138. package/src/framework/state.js +0 -67
  139. package/src/framework/tool-resolver.js +0 -65
  140. package/src/index.js +0 -345
  141. package/src/runtime/generation/base.js +0 -46
  142. package/src/runtime/generation/index.js +0 -70
  143. package/src/runtime/generation/mcp-ref-strategy.js +0 -197
  144. package/src/runtime/generation/stable-id-strategy.js +0 -170
  145. package/src/runtime/stable-id-runtime.js +0 -248
  146. package/src/runtime/verification/base.js +0 -44
  147. package/src/runtime/verification/index.js +0 -67
  148. package/src/runtime/verification/playwright-json-strategy.js +0 -119
  149. package/src/runtime/zibby-runtime.js +0 -299
  150. package/src/sync/index.js +0 -2
  151. package/src/sync/uploader.js +0 -29
  152. package/src/tools/run-playwright-test.js +0 -158
  153. package/src/utils/adf-converter.js +0 -68
  154. package/src/utils/ast-utils.js +0 -37
  155. package/src/utils/ci-setup.js +0 -124
  156. package/src/utils/cursor-utils.js +0 -71
  157. package/src/utils/logger.js +0 -144
  158. package/src/utils/mcp-config-writer.js +0 -115
  159. package/src/utils/node-schema-parser.js +0 -522
  160. package/src/utils/post-process-events.js +0 -55
  161. package/src/utils/result-handler.js +0 -102
  162. package/src/utils/selector-generator.js +0 -239
  163. package/src/utils/streaming-parser.js +0 -387
  164. package/src/utils/test-post-processor.js +0 -211
  165. package/src/utils/timeline.js +0 -217
  166. package/src/utils/trace-parser.js +0 -325
  167. package/src/utils/video-organizer.js +0 -91
@@ -1,544 +0,0 @@
1
- import { AgentStrategy } from './base.js';
2
- import { spawn, execSync } from 'child_process';
3
- import { writeFileSync, unlinkSync, mkdtempSync, readFileSync, mkdirSync, existsSync, accessSync, constants } from 'fs';
4
- import { join } from 'path';
5
- import { tmpdir, homedir } from 'os';
6
- import { logger } from '../../utils/logger.js';
7
- import { DEFAULT_MODELS, TIMEOUTS } from '../../constants.js';
8
- import { DEFAULT_OUTPUT_BASE, SESSION_INFO_FILE } from '../constants.js';
9
- import { getAllSkills, getSkill } from '../skill-registry.js';
10
- import { StreamingParser } from '../../utils/streaming-parser.js';
11
- import { CursorOutputFormatter } from './utils/cursor-output-formatter.js';
12
- import { formatWithOpenAIProxy } from './utils/openai-proxy-formatter.js';
13
- import { timeline } from '../../utils/timeline.js';
14
-
15
- export class CursorAgentStrategy extends AgentStrategy {
16
- constructor() {
17
- super(
18
- 'cursor',
19
- 'Cursor Agent (Local IDE Agent)',
20
- 100
21
- );
22
- }
23
-
24
- canHandle(_context) {
25
- const paths = [
26
- // Try absolute paths first (most reliable)
27
- join(homedir(), '.local', 'bin', 'cursor-agent'),
28
- join(homedir(), '.cursor', 'bin', 'cursor-agent'),
29
- '/usr/local/bin/cursor-agent',
30
- '/usr/local/bin/agent',
31
- '/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
32
- // Try PATH last (may have symlink issues)
33
- 'agent',
34
- 'cursor-agent'
35
- ];
36
-
37
- for (const path of paths) {
38
- try {
39
- if (path.startsWith('/')) {
40
- // Check file exists and is executable
41
- accessSync(path, constants.X_OK);
42
- // Verify it actually runs
43
- const out = execSync(`"${path}" --version 2>&1`, { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' });
44
- if (out && out.length > 0) {
45
- logger.debug(`[Cursor] Found agent at: ${path} (version: ${out.trim().slice(0, 50)})`);
46
- return true;
47
- }
48
- } else {
49
- // Check if in PATH and runs
50
- const which = execSync(`which ${path}`, { encoding: 'utf-8', timeout: 2000, stdio: 'pipe' }).trim();
51
- if (!which) continue;
52
- const out = execSync(`${path} --version 2>&1`, { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' });
53
- if (out && out.length > 0) {
54
- logger.debug(`[Cursor] Found '${path}' in PATH at ${which} (version: ${out.trim().slice(0, 50)})`);
55
- return true;
56
- }
57
- }
58
- } catch (_e) {
59
- // Binary doesn't exist or doesn't work
60
- continue;
61
- }
62
- }
63
-
64
- logger.warn('[Cursor] ❌ Cursor Agent CLI not found or not working. Run: agent --version');
65
- return false;
66
- }
67
-
68
- async invoke(prompt, options = {}) {
69
- const {
70
- workspace = process.cwd(),
71
- print: _print = false,
72
- schema = null,
73
- skills = null,
74
- sessionPath = null,
75
- timeout = TIMEOUTS.CURSOR_AGENT_DEFAULT,
76
- config = {}
77
- } = options;
78
-
79
- const strictMode = config?.agent?.strictMode || false;
80
-
81
- // Model: prefer .zibby.config.js agent.cursor.model so CLI uses it instead of IDE default
82
- const model = options.model ?? config?.agent?.cursor?.model ?? DEFAULT_MODELS.CURSOR;
83
-
84
- logger.debug(`[Cursor] Invoking (model: ${model}, timeout: ${timeout / 1000}s, skills: ${JSON.stringify(skills)})`);
85
-
86
- this._setupMcpConfig(sessionPath, workspace, config, skills);
87
-
88
- const possibleBins = [
89
- // Try absolute paths first (most reliable)
90
- join(homedir(), '.local', 'bin', 'cursor-agent'),
91
- join(homedir(), '.cursor', 'bin', 'cursor-agent'),
92
- '/usr/local/bin/cursor-agent',
93
- '/usr/local/bin/agent',
94
- '/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
95
- // Try PATH last (may have symlink issues)
96
- 'agent',
97
- 'cursor-agent'
98
- ];
99
-
100
- let cursorBin = null;
101
- for (const bin of possibleBins) {
102
- try {
103
- if (bin.startsWith('/')) {
104
- // For absolute paths, check file exists and is executable
105
- accessSync(bin, constants.X_OK);
106
- // Also verify it actually runs
107
- execSync(`"${bin}" --version 2>&1`, { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' });
108
- } else {
109
- // For commands in PATH, verify they exist and run
110
- const which = execSync(`which ${bin}`, { encoding: 'utf-8', timeout: 2000 }).trim();
111
- if (!which) throw new Error('not in PATH');
112
- // Verify the binary works
113
- execSync(`${bin} --version 2>&1`, { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' });
114
- }
115
- cursorBin = bin;
116
- logger.debug(`[Agent] Using binary: ${bin}`);
117
- break;
118
- } catch (err) {
119
- logger.debug(`[Agent] Binary '${bin}' check failed: ${err.message}`);
120
- continue;
121
- }
122
- }
123
-
124
- if (!cursorBin) {
125
- throw new Error(
126
- `Cursor Agent CLI not found or not working.\n\n` +
127
- `Checked paths:\n` +
128
- `${possibleBins.map(p => ` - ${p}`).join('\n')}\n\n` +
129
- `Install cursor-agent:\n` +
130
- ` curl https://cursor.com/install -fsS | bash\n\n` +
131
- `Then add to PATH:\n` +
132
- ` echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc\n\n` +
133
- `Test with: agent --version`
134
- );
135
- }
136
-
137
- // File-based structured output: agent writes JSON to this file
138
- let resultFilePath = null;
139
- if (schema) {
140
- const resultFileName = `zibby-result-${Date.now()}.json`;
141
- resultFilePath = join(workspace, '.zibby', 'tmp', resultFileName);
142
- const tmpDir = join(workspace, '.zibby', 'tmp');
143
- if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true });
144
-
145
- const formatInstructions = CursorOutputFormatter.generateFileOutputInstructions(schema, resultFilePath);
146
- prompt = `${prompt}\n\n${formatInstructions}`;
147
- }
148
-
149
- const promptTmpDir = mkdtempSync(join(tmpdir(), 'zibby-prompt-'));
150
- const promptFile = join(promptTmpDir, 'prompt.md');
151
- writeFileSync(promptFile, prompt, 'utf-8');
152
- logger.debug(`📝 [Agent] Prompt written to ${promptFile} (${prompt.length} chars)`);
153
-
154
- // All cursor-agent binaries use the same command structure (no subcommand needed)
155
- const args = [
156
- '--print',
157
- '--force',
158
- '--approve-mcps',
159
- '--output-format', 'stream-json',
160
- '--stream-partial-output',
161
- '--model', model || 'auto',
162
- ];
163
- if (process.env.CURSOR_API_KEY) {
164
- args.push('--api-key', process.env.CURSOR_API_KEY);
165
- }
166
- args.push(`@${promptFile}`);
167
-
168
- const fullCmd = [cursorBin, ...args].join(' ');
169
- logger.debug(`[Agent] Executing: ${fullCmd.slice(0, 200)}`);
170
- logger.debug(`[Agent] Workspace: ${workspace}`);
171
- if (process.env.LOG_LEVEL === 'debug' || process.env.ZIBBY_LOG_CURSOR_CLI === '1') {
172
- try {
173
- console.log(`\n🔧 Cursor CLI --model ${model || 'auto'} (from .zibby.config.js agent.cursor.model)\n`);
174
- if (process.env.ZIBBY_LOG_CURSOR_CLI === '1') {
175
- console.log(` Full command: ${fullCmd}\n`);
176
- }
177
- } catch (_err) {
178
- // Ignore EPIPE errors from console.log
179
- }
180
- }
181
-
182
- let spawnResult;
183
- let spawnError = null;
184
- try {
185
- spawnResult = await this._spawnWithStreaming(cursorBin, args, workspace, timeout, prompt);
186
- } catch (err) {
187
- spawnError = err;
188
- } finally {
189
- try { unlinkSync(promptFile); } catch {}
190
- }
191
-
192
- const rawOutput = spawnResult?.stdout || '';
193
-
194
- if (schema) {
195
- const isZodSchema = typeof schema.parse === 'function';
196
-
197
- // Step 1: Read the file the agent was told to write (even if agent crashed)
198
- let fileContent = null;
199
- if (resultFilePath && existsSync(resultFilePath)) {
200
- try {
201
- const raw = readFileSync(resultFilePath, 'utf-8').trim();
202
- fileContent = JSON.parse(raw);
203
- logger.debug(`[Agent] Read structured output file (${raw.length} chars)`);
204
- if (spawnError) {
205
- logger.debug(`[Agent] Agent exited non-zero but result file was written — recovering`);
206
- }
207
- } catch (parseErr) {
208
- logger.warn(`⚠️ [Agent] Result file exists but failed to parse: ${parseErr.message}`);
209
- }
210
- } else if (!spawnError) {
211
- logger.warn(`⚠️ [Agent] Result file not found at ${resultFilePath}`);
212
- }
213
-
214
- // Step 2: Validate with Zod if we got parsed JSON
215
- if (fileContent && isZodSchema) {
216
- try {
217
- const validated = schema.parse(fileContent);
218
- logger.debug(`✅ [Agent] File-based output validated with Zod — done`);
219
- return { raw: rawOutput, structured: validated };
220
- } catch (zodErr) {
221
- logger.warn(`⚠️ [Agent] File JSON failed Zod validation: ${zodErr.message?.slice(0, 300)}`);
222
- }
223
- } else if (fileContent) {
224
- logger.debug(`✅ [Agent] File-based output extracted (no Zod schema) — done`);
225
- return { raw: rawOutput, structured: fileContent };
226
- }
227
-
228
- // Step 3: If strict mode, fall back to OpenAI proxy to enforce schema
229
- if (strictMode && !spawnError) {
230
- const parsedText = spawnResult.parsedText;
231
- const proxyInput = fileContent ? JSON.stringify(fileContent) : parsedText;
232
- logger.debug(`[Agent] strictMode — falling back to OpenAI proxy (${proxyInput.length} chars)`);
233
- const result = await formatWithOpenAIProxy(proxyInput, schema);
234
- if (isZodSchema) {
235
- const validated = schema.parse(result.structured);
236
- return { raw: rawOutput, structured: validated };
237
- }
238
- return { raw: rawOutput, ...result };
239
- }
240
-
241
- // If agent crashed and we couldn't salvage from file, re-throw original error
242
- if (spawnError) throw spawnError;
243
-
244
- logger.error(`❌ [Agent] No structured output extracted from file`);
245
- logger.error(`💡 Tip: Set strictMode=true in .zibby.config.js for OpenAI proxy fallback`);
246
- throw new Error(`Agent did not produce a valid result file at ${resultFilePath}. Enable strictMode for proxy fallback.`);
247
- }
248
-
249
- // No schema — non-zero exit is a real failure
250
- if (spawnError) throw spawnError;
251
-
252
- return rawOutput;
253
- }
254
-
255
- _setupMcpConfig(sessionPath, workspace, config, skills = null) {
256
- const cursorDir = join(homedir(), '.cursor');
257
- const mcpConfigPath = join(cursorDir, 'mcp.json');
258
-
259
- let existing = {};
260
- if (existsSync(mcpConfigPath)) {
261
- try {
262
- existing = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
263
- } catch { /* start fresh */ }
264
- }
265
- const mcpServers = existing.mcpServers || {};
266
-
267
- const outputBase = config?.paths?.output || DEFAULT_OUTPUT_BASE;
268
- const sessionInfoPath = join(workspace || process.cwd(), outputBase, SESSION_INFO_FILE);
269
-
270
- const skillsToResolve = Array.isArray(skills)
271
- ? skills.map(id => getSkill(id)).filter(Boolean)
272
- : [...getAllSkills()].map(([, skill]) => skill);
273
-
274
- const configured = new Set();
275
- for (const skill of skillsToResolve) {
276
- if (typeof skill.resolve !== 'function') continue;
277
- if (configured.has(skill.serverName)) continue;
278
- configured.add(skill.serverName);
279
- this._ensureSkillConfigured(mcpServers, skill, sessionPath, sessionInfoPath);
280
- }
281
-
282
- if (Object.keys(mcpServers).length === 0) {
283
- logger.debug(`[MCP] No MCP servers configured - agent will run without tool access`);
284
- return;
285
- }
286
-
287
- if (!existsSync(cursorDir)) {
288
- mkdirSync(cursorDir, { recursive: true });
289
- }
290
-
291
- writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2));
292
- logger.debug(`[MCP] Config: ${Object.keys(mcpServers).join(', ')}`);
293
- }
294
-
295
- _ensureSkillConfigured(mcpServers, skill, sessionPath, sessionInfoPath) {
296
- const preferredKey = skill.cursorKey || skill.serverName;
297
- const existingKey = mcpServers[preferredKey] ? preferredKey
298
- : mcpServers[skill.serverName] ? skill.serverName
299
- : null;
300
-
301
- if (existingKey && sessionPath) {
302
- mcpServers[existingKey].args = (mcpServers[existingKey].args || []).map(arg =>
303
- arg.startsWith('--output-dir=') ? `--output-dir=${sessionPath}` : arg
304
- );
305
- if (skill.sessionEnvKey) {
306
- mcpServers[existingKey].env = {
307
- ...(mcpServers[existingKey].env || {}),
308
- [skill.sessionEnvKey]: sessionInfoPath,
309
- };
310
- }
311
- logger.debug(`[MCP] Updated ${existingKey} session → ${sessionPath}`);
312
- return;
313
- }
314
-
315
- if (existingKey) return;
316
-
317
- const resolved = skill.resolve({ sessionPath });
318
- if (!resolved) return;
319
-
320
- mcpServers[preferredKey] = {
321
- ...resolved,
322
- ...(skill.sessionEnvKey && {
323
- env: { ...(resolved.env || {}), [skill.sessionEnvKey]: sessionInfoPath },
324
- }),
325
- };
326
- logger.debug(`[MCP] Configured ${preferredKey}`);
327
- }
328
-
329
- _spawnWithStreaming(bin, args, cwd, timeout, stdinPrompt = null) {
330
- return new Promise((resolve, reject) => {
331
- const startTime = Date.now();
332
- let stdout = '';
333
- let stderr = '';
334
- let lastOutputTime = Date.now();
335
- let lineCount = 0;
336
- let killed = false;
337
- let processStarted = false;
338
- let processClosed = false;
339
-
340
- const proc = spawn(bin, args, {
341
- cwd,
342
- shell: false,
343
- stdio: ['pipe', 'pipe', 'pipe']
344
- });
345
-
346
- logger.debug(`[Agent] PID: ${proc.pid}`);
347
-
348
- // Register error handlers on ALL child stdio streams to prevent
349
- // unhandled EPIPE crashes (Node v23+ throws these as uncaught exceptions)
350
- proc.stdin.on('error', (err) => {
351
- if (err.code === 'EPIPE') return;
352
- logger.warn(`[Agent] stdin error: ${err.message}`);
353
- });
354
- proc.stdout.on('error', (err) => {
355
- if (err.code === 'EPIPE') return;
356
- logger.warn(`[Agent] stdout error: ${err.message}`);
357
- });
358
- proc.stderr.on('error', (err) => {
359
- if (err.code === 'EPIPE') return;
360
- logger.warn(`[Agent] stderr error: ${err.message}`);
361
- });
362
-
363
- if (stdinPrompt) {
364
- proc.stdin.write(stdinPrompt, (err) => {
365
- if (err && err.code !== 'EPIPE') {
366
- logger.warn(`[Agent] Failed to write to stdin: ${err.message}`);
367
- }
368
- proc.stdin.end();
369
- });
370
- logger.debug(`[Agent] Prompt also piped to stdin (${stdinPrompt.length} chars)`);
371
- } else {
372
- proc.stdin.end();
373
- }
374
-
375
- const modifiedFiles = new Set();
376
- const _startIso = new Date(startTime).toISOString().replace(/\.\d+Z$/, '');
377
-
378
- const heartbeat = setInterval(() => {
379
- const elapsed = Math.round((Date.now() - startTime) / 1000);
380
- const _silent = Math.round((Date.now() - lastOutputTime) / 1000);
381
-
382
- const newFiles = [];
383
- try {
384
- const elapsedMin = Math.ceil(elapsed / 60) + 1;
385
- const raw = execSync(
386
- `find "${cwd}" -type f -mmin -${elapsedMin} -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/target/*' 2>/dev/null | head -20`,
387
- { encoding: 'utf-8', timeout: 5000 }
388
- ).trim();
389
- if (raw) {
390
- for (const f of raw.split('\n')) {
391
- const rel = f.replace(`${cwd }/`, '');
392
- if (!modifiedFiles.has(rel)) {
393
- modifiedFiles.add(rel);
394
- newFiles.push(rel);
395
- }
396
- }
397
- }
398
- } catch {}
399
-
400
- let activity = '';
401
- if (newFiles.length > 0) {
402
- const short = newFiles.map(f => f.split('/').pop());
403
- activity = ` | 📁 new: ${short.join(', ')}`;
404
- }
405
- if (modifiedFiles.size > 0) {
406
- activity += ` | 📦 total: ${modifiedFiles.size} files`;
407
- }
408
-
409
- logger.debug(`💓 [Agent] Running for ${elapsed}s | ${lineCount} lines output${activity}`);
410
-
411
- if (lineCount === 0 && elapsed >= 30 && modifiedFiles.size === 0) {
412
- if (elapsed < 35) {
413
- logger.warn(`⚠️ [Agent] No output after ${elapsed}s — agent may be stuck. Check your CURSOR_API_KEY.`);
414
- }
415
- if (elapsed >= 60) {
416
- killed = true;
417
- logger.error(`❌ [Agent] No response after ${elapsed}s — killing. Verify CURSOR_API_KEY is valid and agent CLI works: agent --version`);
418
- proc.kill('SIGTERM');
419
- setTimeout(() => { if (!proc.killed) proc.kill('SIGKILL'); }, 3000);
420
- }
421
- }
422
- }, 30000);
423
-
424
- const timer = setTimeout(() => {
425
- killed = true;
426
- const elapsed = Math.round((Date.now() - startTime) / 1000);
427
- logger.error(`⏱️ [Agent] Timeout after ${elapsed}s — killing process (PID: ${proc.pid})`);
428
- if (stdout.trim()) {
429
- logger.warn(`📤 [Agent] Partial output (${stdout.length} chars) before timeout:\n${stdout.slice(-2000)}`);
430
- }
431
- proc.kill('SIGTERM');
432
- setTimeout(() => {
433
- if (!proc.killed) proc.kill('SIGKILL');
434
- }, 5000);
435
- }, timeout);
436
-
437
- const streamParser = new StreamingParser();
438
- streamParser.onToolCall = (name, input) => {
439
- let displayName = name;
440
- let displayInput = input;
441
- if (name === 'mcpToolCall' && input?.name) {
442
- displayName = input.name.replace(/^mcp_+[^_]+_+/, '');
443
- if (displayName.includes('-') && displayName.split('-')[0] === displayName.split('-')[1]) {
444
- displayName = displayName.split('-')[0];
445
- }
446
- displayInput = input.args ?? input.input ?? input;
447
- } else if (name === 'readToolCall' || name === 'editToolCall' || name === 'writeToolCall') {
448
- return;
449
- } else if (name.startsWith('mcp__') || name.includes('ToolCall')) {
450
- displayName = name.replace(/^mcp_+[^_]+_+/, '').replace(/ToolCall$/, '');
451
- }
452
- const isMemoryTool = displayName.includes('memory');
453
- if (isMemoryTool) {
454
- timeline.stepMemory(`Tool: ${displayName}`);
455
- } else {
456
- timeline.stepTool(`Tool: ${displayName}`);
457
- }
458
- if (displayInput != null && typeof displayInput === 'object' && Object.keys(displayInput).length > 0 && !processClosed) {
459
- const raw = JSON.stringify(displayInput);
460
- const preview = raw.length > 100 ? `${raw.substring(0, 100)}...` : raw;
461
- console.log(` Input: ${preview}`);
462
- }
463
- };
464
-
465
- proc.stdout.on('data', (data) => {
466
- const chunk = data.toString();
467
- stdout += chunk;
468
- lastOutputTime = Date.now();
469
-
470
- if (!processStarted) {
471
- processStarted = true;
472
- }
473
-
474
- const displayText = streamParser.processChunk(chunk);
475
- if (displayText && !processClosed) {
476
- process.stdout.write(displayText);
477
- }
478
-
479
- const lines = chunk.split('\n').filter(l => l.trim());
480
- lineCount += lines.length;
481
- });
482
-
483
- proc.stderr.on('data', (data) => {
484
- const chunk = data.toString();
485
- stderr += chunk;
486
- lastOutputTime = Date.now();
487
-
488
- if (!processStarted) {
489
- processStarted = true;
490
- }
491
-
492
- const lines = chunk.split('\n').filter(l => l.trim());
493
- for (const line of lines) {
494
- logger.warn(`⚠️ [Agent stderr] ${line}`);
495
- }
496
- });
497
-
498
- proc.on('close', (code, signal) => {
499
- processClosed = true;
500
- clearTimeout(timer);
501
- clearInterval(heartbeat);
502
- streamParser.flush();
503
- const elapsed = Math.round((Date.now() - startTime) / 1000);
504
- logger.debug(`[Agent] Exited: code=${code}, signal=${signal}, elapsed=${elapsed}s, output=${stdout.length} chars`);
505
- timeline.step(`Agent completed (${elapsed}s)`);
506
-
507
- if (killed) {
508
- reject(new Error(
509
- `Cursor Agent timed out after ${elapsed}s (limit: ${timeout / 1000}s). ` +
510
- `${lineCount} lines produced. Last output ${Math.round((Date.now() - lastOutputTime) / 1000)}s ago. ${
511
- stdout.trim() ? `\nPartial output (last 500 chars):\n${stdout.slice(-500)}` : 'No output captured.'}`
512
- ));
513
- return;
514
- }
515
-
516
- if (code !== 0) {
517
- reject(new Error(
518
- `Cursor Agent failed: exit code ${code}, signal ${signal}. ${
519
- stderr.trim() ? `\nStderr: ${stderr.slice(-1000)}` : ''
520
- }${stdout.trim() ? `\nStdout (last 500 chars): ${stdout.slice(-500)}` : ''}`
521
- ));
522
- return;
523
- }
524
-
525
- const extractedJson = streamParser.getResult();
526
- const parsedText = extractedJson
527
- ? JSON.stringify(extractedJson, null, 2)
528
- : (streamParser.getRawText() || stdout || '');
529
- resolve({ stdout: stdout || stderr || '', parsedText });
530
- });
531
-
532
- proc.on('error', (err) => {
533
- clearTimeout(timer);
534
- clearInterval(heartbeat);
535
- reject(new Error(
536
- `Cursor Agent spawn error: ${err.message}\n` +
537
- `Binary: ${bin}\n` +
538
- `This usually means the binary is not in PATH. Try:\n` +
539
- ` echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc`
540
- ));
541
- });
542
- });
543
- }
544
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * Agent Strategy Factory
3
- *
4
- * Selects the best available agent strategy based on:
5
- * 1. User preference (state.agentType or AGENT_TYPE env)
6
- * 2. Availability (can the strategy be used?)
7
- * 3. Priority (which strategy is preferred?)
8
- */
9
-
10
- import { CursorAgentStrategy } from './cursor-strategy.js';
11
- import { ClaudeAgentStrategy } from './claude-strategy.js';
12
- import { logger } from '../../utils/logger.js';
13
-
14
- // Registry of all available agent strategies
15
- const AGENT_STRATEGIES = [
16
- new CursorAgentStrategy(),
17
- new ClaudeAgentStrategy()
18
- ];
19
-
20
- /**
21
- * Get the best available agent strategy
22
- * @param {Object} context - Context containing state, env, etc.
23
- * @returns {AgentStrategy}
24
- */
25
- export function getAgentStrategy(context = {}) {
26
- const { state = {}, preferredAgent = null } = context;
27
-
28
- // Agent must be explicitly specified - no auto-selection
29
- const requestedAgent = preferredAgent ||
30
- state.agentType ||
31
- process.env.AGENT_TYPE;
32
-
33
- if (!requestedAgent) {
34
- throw new Error('No agent specified. Set agent.claude or agent.cursor in .zibby.config.js');
35
- }
36
-
37
- logger.debug(`Agent selection: requested=${requestedAgent}`);
38
-
39
- const strategy = AGENT_STRATEGIES.find(s => s.getName() === requestedAgent);
40
-
41
- if (!strategy) {
42
- throw new Error(`Unknown agent '${requestedAgent}'. Available: ${AGENT_STRATEGIES.map(s => s.getName()).join(', ')}`);
43
- }
44
-
45
- logger.debug(`Checking if ${requestedAgent} can handle this environment...`);
46
- if (!strategy.canHandle(context)) {
47
- const hint = requestedAgent === 'claude'
48
- ? 'Set ANTHROPIC_API_KEY in .env'
49
- : 'Install cursor-agent CLI or set CURSOR_API_KEY';
50
- throw new Error(`Agent '${requestedAgent}' is not available. ${hint}`);
51
- }
52
-
53
- logger.debug(`Using agent: ${strategy.getName()}`);
54
- return strategy;
55
- }
56
-
57
- /**
58
- * Invoke an agent with automatic strategy selection
59
- * @param {string} prompt - The prompt to send
60
- * @param {Object} context - Context for strategy selection
61
- * @param {Object} options - Options for invoke (model, workspace, etc.)
62
- * @returns {Promise<string>}
63
- */
64
- export async function invokeAgent(prompt, context = {}, options = {}) {
65
- const strategy = getAgentStrategy(context);
66
-
67
- const config = context.state?.config || options.config || {};
68
- const finalOptions = {
69
- model: options.model ?? context.state?.model ?? config?.agent?.claude?.model ?? config?.agent?.cursor?.model,
70
- workspace: context.state?.workspace || options.workspace,
71
- schema: options.schema || context.schema,
72
- images: options.images || context.images || [],
73
- skills: options.skills || context.skills || [],
74
- config,
75
- ...options
76
- };
77
- if (!finalOptions.model) {
78
- finalOptions.model = finalOptions.config?.agent?.cursor ? (finalOptions.config.agent.cursor.model ?? 'auto') : (finalOptions.config?.agent?.claude?.model ?? 'auto');
79
- }
80
-
81
- const extraInstructions = context.state?._currentNodeConfig?.extraPromptInstructions?.trim();
82
- if (extraInstructions) {
83
- prompt += `\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
- ⚠️ PRIORITY OVERRIDE — THE FOLLOWING INSTRUCTIONS TAKE PRECEDENCE OVER ALL PREVIOUS CONTENT
85
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86
-
87
- ${extraInstructions}
88
- `;
89
- }
90
-
91
- logger.debug(`Prompt length: ${prompt.length} chars`);
92
- if (process.env.STAGE !== 'prod') {
93
- logger.debug(`Full prompt:\n${prompt}`);
94
- }
95
-
96
- const rawOutput = await strategy.invoke(prompt, finalOptions);
97
-
98
- // Return the output as-is (strategy handles formatting)
99
- // For schemas: { raw: string, structured: object }
100
- // For text: string
101
- return rawOutput;
102
- }
103
-
104
- export { CursorAgentStrategy, ClaudeAgentStrategy };
105
- export { AgentStrategy } from './base.js';