@zibby/core 0.1.21 → 0.1.22
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/agents/base.js +17 -0
- package/dist/backend-client.js +1 -0
- package/dist/constants/tool-names.js +1 -0
- package/dist/constants/zibby-scratch.js +1 -0
- package/dist/constants.js +1 -0
- package/dist/enrichment/base.js +1 -0
- package/dist/enrichment/enrichers/accessibility-enricher.js +1 -0
- package/dist/enrichment/enrichers/dom-enricher.js +1 -0
- package/dist/enrichment/enrichers/page-state-enricher.js +1 -0
- package/dist/enrichment/enrichers/position-enricher.js +1 -0
- package/dist/enrichment/index.js +1 -0
- package/dist/enrichment/mcp-integration.js +1 -0
- package/dist/enrichment/mcp-ref-enricher.js +1 -0
- package/dist/enrichment/pipeline.js +3 -0
- package/dist/enrichment/trace-text-enricher.js +1 -0
- package/dist/framework/agents/assistant-strategy.js +5 -0
- package/dist/framework/agents/base.js +1 -0
- package/dist/framework/agents/claude-strategy.js +4 -0
- package/dist/framework/agents/codex-strategy.js +4 -0
- package/dist/framework/agents/cursor-strategy.js +32 -0
- package/dist/framework/agents/gemini-strategy.js +11 -0
- package/dist/framework/agents/index.js +13 -0
- package/dist/framework/agents/middleware/assistant-round-pipeline.js +3 -0
- package/dist/framework/agents/providers/base.js +1 -0
- package/dist/framework/agents/providers/index.js +1 -0
- package/dist/framework/agents/providers/openai-transport.js +2 -0
- package/dist/framework/agents/providers/openai.js +1 -0
- package/dist/framework/agents/providers/transport-base.js +1 -0
- package/dist/framework/agents/utils/auth-resolver.js +1 -0
- package/dist/framework/agents/utils/cursor-output-formatter.js +1 -0
- package/dist/framework/agents/utils/openai-proxy-formatter.js +9 -0
- package/dist/framework/agents/utils/payload-budget.js +3 -0
- package/dist/framework/agents/utils/structured-output-formatter.js +21 -0
- package/dist/framework/code-generator.js +10 -0
- package/dist/framework/constants.js +1 -0
- package/dist/framework/context-loader.js +5 -0
- package/dist/framework/function-bridge.js +2 -0
- package/dist/framework/function-skill-registry.js +1 -0
- package/dist/framework/graph-compiler.js +1 -0
- package/dist/framework/graph.js +5 -0
- package/dist/framework/index.js +1 -0
- package/dist/framework/mcp-client.js +2 -0
- package/dist/framework/node-registry.js +9 -0
- package/dist/framework/node.js +5 -0
- package/dist/framework/output-parser.js +3 -0
- package/dist/framework/skill-registry.js +1 -0
- package/dist/framework/state-utils.js +1 -0
- package/dist/framework/state.js +1 -0
- package/dist/framework/tool-resolver.js +1 -0
- package/dist/index.js +8 -0
- package/dist/runtime/generation/base.js +1 -0
- package/dist/runtime/generation/index.js +3 -0
- package/dist/runtime/generation/mcp-ref-strategy.js +41 -0
- package/dist/runtime/generation/stable-id-strategy.js +16 -0
- package/dist/runtime/stable-id-runtime.js +1 -0
- package/dist/runtime/verification/base.js +1 -0
- package/dist/runtime/verification/index.js +3 -0
- package/dist/runtime/verification/playwright-json-strategy.js +1 -0
- package/dist/runtime/zibby-runtime.js +1 -0
- package/dist/sync/index.js +1 -0
- package/dist/sync/uploader.js +1 -0
- package/dist/tools/run-playwright-test.js +5 -0
- package/dist/utils/adf-converter.js +7 -0
- package/dist/utils/ast-utils.js +1 -0
- package/dist/utils/ci-setup.js +5 -0
- package/dist/utils/cursor-mcp-isolated-home.js +1 -0
- package/dist/utils/cursor-utils.js +18 -0
- package/dist/utils/live-frame-discovery.js +1 -0
- package/dist/utils/logger.js +1 -0
- package/dist/utils/mcp-config-writer.js +10 -0
- package/dist/utils/mission-control-from-run-states.js +1 -0
- package/dist/utils/node-schema-parser.js +1 -0
- package/dist/utils/parallel-config.js +1 -0
- package/dist/utils/post-process-events.js +1 -0
- package/dist/utils/result-handler.js +1 -0
- package/{src → dist}/utils/ripple-effect.js +3 -12
- package/dist/utils/run-capacity-coordinator.js +1 -0
- package/dist/utils/run-capacity-queue.js +2 -0
- package/dist/utils/run-index-merge.js +1 -0
- package/dist/utils/run-index-post-cli.js +1 -0
- package/dist/utils/run-registry.js +3 -0
- package/dist/utils/run-state-session.js +2 -0
- package/dist/utils/selector-generator.js +4 -0
- package/dist/utils/session-state-constants.js +1 -0
- package/dist/utils/session-state-live-runs.js +1 -0
- package/dist/utils/streaming-parser.js +4 -0
- package/dist/utils/test-post-processor.js +18 -0
- package/dist/utils/timeline.js +14 -0
- package/dist/utils/trace-parser.js +2 -0
- package/dist/utils/video-organizer.js +3 -0
- package/package.json +49 -35
- package/templates/browser-test-automation/README.md +29 -7
- package/templates/browser-test-automation/chat.mjs +36 -0
- package/templates/browser-test-automation/graph.mjs +5 -9
- package/templates/browser-test-automation/nodes/execute-live.mjs +30 -58
- package/templates/browser-test-automation/nodes/generate-script.mjs +32 -12
- package/templates/browser-test-automation/nodes/utils.mjs +153 -10
- package/templates/browser-test-automation/pipeline-ids.js +12 -0
- package/templates/browser-test-automation/result-handler.mjs +78 -2
- package/templates/browser-test-automation/run-index.mjs +418 -0
- package/scripts/export-default-workflows.js +0 -51
- package/scripts/patch-cursor-mcp.js +0 -174
- package/scripts/setup-ci.sh +0 -115
- package/scripts/setup-official-playwright-mcp.sh +0 -226
- package/scripts/test-with-video.sh +0 -49
- package/src/agents/base.js +0 -361
- package/src/constants.js +0 -47
- package/src/enrichment/base.js +0 -49
- package/src/enrichment/enrichers/accessibility-enricher.js +0 -197
- package/src/enrichment/enrichers/dom-enricher.js +0 -171
- package/src/enrichment/enrichers/page-state-enricher.js +0 -129
- package/src/enrichment/enrichers/position-enricher.js +0 -67
- package/src/enrichment/index.js +0 -96
- package/src/enrichment/mcp-integration.js +0 -149
- package/src/enrichment/mcp-ref-enricher.js +0 -78
- package/src/enrichment/pipeline.js +0 -192
- package/src/enrichment/trace-text-enricher.js +0 -115
- package/src/framework/AGENTS.md +0 -98
- package/src/framework/agents/base.js +0 -72
- package/src/framework/agents/claude-strategy.js +0 -278
- package/src/framework/agents/cursor-strategy.js +0 -544
- package/src/framework/agents/index.js +0 -105
- package/src/framework/agents/utils/cursor-output-formatter.js +0 -67
- package/src/framework/agents/utils/openai-proxy-formatter.js +0 -249
- package/src/framework/code-generator.js +0 -301
- package/src/framework/constants.js +0 -33
- package/src/framework/context-loader.js +0 -101
- package/src/framework/function-bridge.js +0 -78
- package/src/framework/function-skill-registry.js +0 -20
- package/src/framework/graph-compiler.js +0 -342
- package/src/framework/graph.js +0 -610
- package/src/framework/index.js +0 -28
- package/src/framework/node-registry.js +0 -163
- package/src/framework/node.js +0 -259
- package/src/framework/output-parser.js +0 -71
- package/src/framework/skill-registry.js +0 -55
- package/src/framework/state-utils.js +0 -52
- package/src/framework/state.js +0 -67
- package/src/framework/tool-resolver.js +0 -65
- package/src/index.js +0 -345
- package/src/runtime/generation/base.js +0 -46
- package/src/runtime/generation/index.js +0 -70
- package/src/runtime/generation/mcp-ref-strategy.js +0 -197
- package/src/runtime/generation/stable-id-strategy.js +0 -170
- package/src/runtime/stable-id-runtime.js +0 -248
- package/src/runtime/verification/base.js +0 -44
- package/src/runtime/verification/index.js +0 -67
- package/src/runtime/verification/playwright-json-strategy.js +0 -119
- package/src/runtime/zibby-runtime.js +0 -299
- package/src/sync/index.js +0 -2
- package/src/sync/uploader.js +0 -29
- package/src/tools/run-playwright-test.js +0 -158
- package/src/utils/adf-converter.js +0 -68
- package/src/utils/ast-utils.js +0 -37
- package/src/utils/ci-setup.js +0 -124
- package/src/utils/cursor-utils.js +0 -71
- package/src/utils/logger.js +0 -144
- package/src/utils/mcp-config-writer.js +0 -115
- package/src/utils/node-schema-parser.js +0 -522
- package/src/utils/post-process-events.js +0 -55
- package/src/utils/result-handler.js +0 -102
- package/src/utils/selector-generator.js +0 -239
- package/src/utils/streaming-parser.js +0 -387
- package/src/utils/test-post-processor.js +0 -211
- package/src/utils/timeline.js +0 -217
- package/src/utils/trace-parser.js +0 -325
- 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';
|