@zibby/core 0.1.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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Generate Code Node - Generate code implementation with optional commit/push
3
+ * Used by: analysisGraph (preview mode), implementationGraph (commit mode)
4
+ * Also generates PR title and description (Why/How format) for use when creating PRs.
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+ import { join, resolve } from 'path';
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import Handlebars from 'handlebars';
11
+ import { invokeAgent } from '@zibby/core';
12
+ import { generatePRMeta } from './services/prMetaService.js';
13
+ import { adfToText } from '../../../src/utils/adf-converter.js';
14
+ import { z } from 'zod';
15
+
16
+ const CodeImplementationOutputSchema = z.object({
17
+ success: z.boolean(),
18
+ codeImplementation: z.object({
19
+ branchName: z.string().optional(),
20
+ committedRepos: z.array(z.string()).optional(),
21
+ diff: z.string(),
22
+ diffStat: z.string(),
23
+ changedFiles: z.array(z.string()),
24
+ fileContents: z.array(z.object({
25
+ path: z.string(),
26
+ content: z.string()
27
+ })),
28
+ prTitle: z.string().optional(),
29
+ prDescription: z.string().optional(),
30
+ metrics: z.object({
31
+ filesChanged: z.number(),
32
+ additions: z.number(),
33
+ deletions: z.number(),
34
+ netChange: z.number()
35
+ }),
36
+ commitMessage: z.string().optional(),
37
+ agentOutput: z.string(),
38
+ committed: z.boolean(),
39
+ timestamp: z.string()
40
+ })
41
+ });
42
+
43
+ /**
44
+ * Factory function to create code generation node
45
+ * @param {Object} options - Configuration options
46
+ * @param {boolean} options.commitAndPush - If true, creates branch, commits, and pushes
47
+ * @param {string} options.nodeName - Name for the node (default: 'generate_code')
48
+ */
49
+ export function createCodeGenerationNode(options = {}) {
50
+ const {
51
+ commitAndPush = false,
52
+ nodeName = 'generate_code'
53
+ } = options;
54
+
55
+ return {
56
+ name: nodeName,
57
+ outputSchema: CodeImplementationOutputSchema,
58
+ execute: async (state) => {
59
+ const mode = commitAndPush ? 'implementing' : 'generating preview of';
60
+ console.log(`\n💻 ${commitAndPush ? 'Implementing' : 'Generating'} code implementation...`);
61
+
62
+ const { workspace, ticketContext, repos, promptsDir, model, nodeConfigs = {} } = state;
63
+ const aiModel = model || ticketContext.model || 'auto';
64
+ const _nodeConfig = nodeConfigs[nodeName] || {};
65
+ const analysis = state.analyze_ticket?.analysis;
66
+
67
+ // Build implementation prompt from template
68
+ const implementPrompt = buildImplementationPrompt(
69
+ promptsDir,
70
+ ticketContext,
71
+ analysis,
72
+ commitAndPush,
73
+ repos,
74
+ workspace
75
+ );
76
+
77
+ console.log(`🚀 Running AI Agent to ${mode} changes with model: ${aiModel}...`);
78
+
79
+ const implementOutput = await invokeAgent(implementPrompt, { state, model: aiModel });
80
+
81
+ let branchName = null;
82
+ let commitMessage = null;
83
+ const committedRepos = [];
84
+
85
+ // If commit mode, create branch and commit in every repo that has changes
86
+ if (commitAndPush) {
87
+ const reposToCommit = repos && repos.length > 0 ? repos : [];
88
+ if (reposToCommit.length === 0) {
89
+ throw new Error('No repositories configured for commit/push');
90
+ }
91
+
92
+ branchName = `feature/${ticketContext.ticketKey.toLowerCase()}-${generateShortId()}`;
93
+ const commitDesc = typeof ticketContext.description === 'object'
94
+ ? adfToText(ticketContext.description)
95
+ : (ticketContext.description || 'No description');
96
+ commitMessage = `${ticketContext.ticketKey}: ${ticketContext.summary}\n\n${commitDesc}\n\nImplemented by Zibby Agent`;
97
+
98
+ for (const repo of reposToCommit) {
99
+ const repoPath = join(workspace, repo.name);
100
+
101
+ // Check if this repo has any changes
102
+ const status = await execCommand('git', ['status', '--porcelain'], repoPath);
103
+ if (!status.trim()) {
104
+ console.log(`⏭️ No changes in ${repo.name}, skipping`);
105
+ continue;
106
+ }
107
+
108
+ console.log(`📊 Committing changes in ${repo.name}...`);
109
+
110
+ await execCommand('git', ['checkout', '-b', branchName], repoPath);
111
+ await execCommand('git', ['add', '.'], repoPath);
112
+ await execCommand('git', ['commit', '-m', commitMessage], repoPath);
113
+
114
+ // Set remote URL with token for push
115
+ const githubToken = state.githubToken;
116
+ const gitlabToken = process.env.GITLAB_TOKEN || '';
117
+ const gitlabUrl = process.env.GITLAB_URL || '';
118
+
119
+ if (repo.provider === 'gitlab' && gitlabToken && gitlabUrl) {
120
+ try {
121
+ const gitlabHost = new URL(gitlabUrl).host;
122
+ const remoteUrl = repo.url.replace(`https://${gitlabHost}`, `https://oauth2:${gitlabToken}@${gitlabHost}`);
123
+ await execCommand('git', ['remote', 'set-url', 'origin', remoteUrl], repoPath);
124
+ } catch (e) {
125
+ console.warn(`⚠️ Failed to set GitLab remote for ${repo.name}: ${e.message}`);
126
+ }
127
+ } else if (githubToken && repo.url?.includes('github.com')) {
128
+ const remoteUrl = repo.url.replace('https://github.com', `https://x-access-token:${githubToken}@github.com`);
129
+ await execCommand('git', ['remote', 'set-url', 'origin', remoteUrl], repoPath);
130
+ }
131
+
132
+ console.log(`📤 Pushing ${repo.name} to remote...`);
133
+ await execCommand('git', ['push', '-u', 'origin', branchName], repoPath);
134
+ committedRepos.push(repo.name);
135
+ }
136
+
137
+ if (committedRepos.length === 0) {
138
+ console.warn('⚠️ No repos had changes to commit');
139
+ }
140
+ }
141
+
142
+ console.log('📊 Capturing code changes...');
143
+
144
+ // Capture git diff from ALL repos (multi-repo support)
145
+ // - Commit mode: compare last commit to previous (HEAD~1)
146
+ // - Preview mode: show all uncommitted changes (staged + unstaged)
147
+ const allDiffs = [];
148
+ const allDiffStats = [];
149
+ const allChangedFiles = [];
150
+
151
+ const reposToCheck = repos && repos.length > 0
152
+ ? repos
153
+ : [{ name: '.' }];
154
+
155
+ for (const repo of reposToCheck) {
156
+ const currentRepoPath = repo.name === '.'
157
+ ? workspace
158
+ : join(workspace, repo.name);
159
+
160
+ try {
161
+ let repoDiff, repoDiffStat, repoChangedFiles;
162
+
163
+ if (commitAndPush) {
164
+ repoDiff = await execCommand('git', ['diff', '-U10', 'HEAD~1'], currentRepoPath);
165
+ repoDiffStat = await execCommand('git', ['diff', '--stat', 'HEAD~1'], currentRepoPath);
166
+ const diffNameOutput = await execCommand('git', ['diff', '--name-only', 'HEAD~1'], currentRepoPath);
167
+ repoChangedFiles = diffNameOutput.split('\n').filter(f => f.trim());
168
+ } else {
169
+ repoDiff = await execCommand('git', ['diff', '-U10', 'HEAD'], currentRepoPath);
170
+ repoDiffStat = await execCommand('git', ['diff', '--stat', 'HEAD'], currentRepoPath);
171
+
172
+ const modifiedOutput = await execCommand('git', ['diff', '--name-only', 'HEAD'], currentRepoPath);
173
+ const modifiedFiles = modifiedOutput.split('\n').filter(f => f.trim());
174
+ const untrackedOutput = await execCommand('git', ['ls-files', '--others', '--exclude-standard'], currentRepoPath);
175
+ const untrackedFiles = untrackedOutput.split('\n').filter(f => f.trim());
176
+ repoChangedFiles = [...new Set([...modifiedFiles, ...untrackedFiles])];
177
+ }
178
+
179
+ const prefix = repos && repos.length > 1 ? `${repo.name}/` : '';
180
+
181
+ if (repoDiff.trim()) {
182
+ allDiffs.push(`# ${repo.name}\n${repoDiff}`);
183
+ }
184
+ if (repoDiffStat.trim()) {
185
+ allDiffStats.push(`# ${repo.name}\n${repoDiffStat}`);
186
+ }
187
+ if (repoChangedFiles.length > 0) {
188
+ allChangedFiles.push(...repoChangedFiles.map(f => `${prefix}${f}`));
189
+ }
190
+ } catch (err) {
191
+ console.warn(`⚠️ Could not capture diff for repo ${repo.name}:`, err.message);
192
+ }
193
+ }
194
+
195
+ const diff = allDiffs.join('\n\n');
196
+ const diffStat = allDiffStats.join('\n');
197
+ const changedFiles = allChangedFiles;
198
+
199
+ // Store file contents for expandable diff view
200
+ // Only store for reasonable number of files to avoid huge payloads
201
+ const fileContents = [];
202
+ if (changedFiles.length <= 20) {
203
+ for (const filePath of changedFiles) {
204
+ try {
205
+ let fullPath;
206
+ if (repos && repos.length > 1) {
207
+ fullPath = resolve(workspace, filePath);
208
+ } else {
209
+ const repoDir = repos?.[0] ? resolve(workspace, repos[0].name) : workspace;
210
+ fullPath = resolve(repoDir, filePath);
211
+ }
212
+
213
+ if (!fullPath.startsWith(resolve(workspace))) continue;
214
+ if (existsSync(fullPath)) {
215
+ const content = readFileSync(fullPath, 'utf-8');
216
+ if (content.length < 100000) {
217
+ fileContents.push({ path: filePath, content });
218
+ }
219
+ }
220
+ } catch (_err) {
221
+ // Skip files we can't read
222
+ }
223
+ }
224
+ }
225
+
226
+ // Parse diff for metrics
227
+ const additions = (diff.match(/^\+[^+]/gm) || []).length;
228
+ const deletions = (diff.match(/^-[^-]/gm) || []).length;
229
+ const filesChanged = changedFiles.length;
230
+
231
+ // Generate PR title and description (Why/How format) when we have changes
232
+ let prTitle = null;
233
+ let prDescription = null;
234
+ if (diff.trim() && ticketContext?.ticketKey) {
235
+ console.log('📝 Generating PR title and description...');
236
+ try {
237
+ const prMeta = await generatePRMeta({
238
+ ticketContext,
239
+ changedFiles,
240
+ diffStat,
241
+ state,
242
+ model: aiModel
243
+ }).catch(err => {
244
+ console.error(' ⚠️ Promise rejection in generatePRMeta:', err.message);
245
+ throw err;
246
+ });
247
+
248
+ console.log(' 🔍 generatePRMeta returned:', typeof prMeta, prMeta ? Object.keys(prMeta) : 'null/undefined');
249
+
250
+ if (prMeta && typeof prMeta === 'object') {
251
+ prTitle = prMeta.prTitle;
252
+ prDescription = prMeta.prDescription;
253
+ console.log(` ✅ PR title: ${prTitle}`);
254
+ console.log(` ✅ PR description preview: ${prDescription?.substring(0, 100)}...`);
255
+ } else {
256
+ console.error(' ⚠️ generatePRMeta returned invalid result:', prMeta);
257
+ }
258
+ } catch (err) {
259
+ console.error(' ❌ Failed to generate PR meta:', err.message);
260
+ console.error(' Error details:', err.stack);
261
+ } finally {
262
+ console.log(' ✅ PR meta generation attempt complete');
263
+ }
264
+ } else {
265
+ console.log('📝 Skipping PR meta generation (no diff or no ticketKey)');
266
+ }
267
+
268
+ console.log(`✅ Code ${commitAndPush ? 'implementation' : 'generation'} complete`);
269
+ if (branchName) {
270
+ console.log(` 📁 Branch: ${branchName}`);
271
+ if (committedRepos.length > 0) {
272
+ console.log(` 📦 Committed repos: ${committedRepos.join(', ')}`);
273
+ }
274
+ }
275
+ console.log(` 📁 Files changed: ${filesChanged}`);
276
+ console.log(` ➕ Additions: ${additions} lines`);
277
+ console.log(` ➖ Deletions: ${deletions} lines`);
278
+
279
+ return {
280
+ success: true,
281
+ codeImplementation: {
282
+ ...(branchName && { branchName }),
283
+ ...(committedRepos.length > 0 && { committedRepos }),
284
+ diff,
285
+ diffStat,
286
+ changedFiles,
287
+ fileContents,
288
+ ...(prTitle && { prTitle }),
289
+ ...(prDescription && { prDescription }),
290
+ metrics: {
291
+ filesChanged,
292
+ additions,
293
+ deletions,
294
+ netChange: additions - deletions
295
+ },
296
+ ...(commitMessage && { commitMessage }),
297
+ agentOutput: implementOutput,
298
+ committed: commitAndPush,
299
+ timestamp: new Date().toISOString()
300
+ }
301
+ };
302
+ }
303
+ };
304
+ }
305
+
306
+ // Convenience exports for common use cases
307
+ export const generateCodeNode = createCodeGenerationNode({
308
+ commitAndPush: false,
309
+ nodeName: 'generate_code'
310
+ });
311
+
312
+ export const implementCodeNode = createCodeGenerationNode({
313
+ commitAndPush: true,
314
+ nodeName: 'implement_code'
315
+ });
316
+
317
+ function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, workspace) {
318
+ const templatePath = join(promptsDir, 'generate-code.md');
319
+ if (!existsSync(templatePath)) {
320
+ throw new Error(`Template not found: ${templatePath}`);
321
+ }
322
+
323
+ const templateContent = readFileSync(templatePath, 'utf-8');
324
+ const template = Handlebars.compile(templateContent);
325
+
326
+ let descriptionText = ticketContext.description || 'No description provided';
327
+ console.log(`🔍 [generateCode] description type: ${typeof descriptionText}, value: ${typeof descriptionText === 'object' ? JSON.stringify(descriptionText).slice(0, 200) : String(descriptionText).slice(0, 200)}`);
328
+ if (typeof descriptionText === 'object') {
329
+ descriptionText = adfToText(descriptionText);
330
+ console.log(`🔍 [generateCode] Converted ADF to text (${descriptionText.length} chars): ${descriptionText.slice(0, 200)}...`);
331
+ }
332
+
333
+ let planContext = null;
334
+ if (analysis) {
335
+ try {
336
+ const parsed = typeof analysis.raw === 'string' ? JSON.parse(analysis.raw) : analysis.raw;
337
+ const inner = parsed?.analysis?.structured || parsed;
338
+ planContext = inner?.implementationPlan || null;
339
+ } catch (_) {}
340
+ if (!planContext) {
341
+ planContext = typeof analysis.raw === 'string' ? analysis.raw.slice(0, 3000) : null;
342
+ }
343
+ }
344
+
345
+ // Build repo metadata for the template
346
+ const repositories = (repos && repos.length > 0) ? repos.map(r => {
347
+ const info = detectRepoLanguage(workspace ? join(workspace, r.name) : null);
348
+ return { name: r.name, ...info };
349
+ }) : null;
350
+
351
+ return template({
352
+ ticketKey: ticketContext.ticketKey,
353
+ ticketSummary: ticketContext.summary,
354
+ ticketDescription: descriptionText,
355
+ acceptanceCriteria: ticketContext.acceptanceCriteria || null,
356
+ analysisContext: planContext,
357
+ repositories,
358
+ isCommitting
359
+ });
360
+ }
361
+
362
+ function detectRepoLanguage(repoPath) {
363
+ if (!repoPath) return { language: 'Unknown', framework: null };
364
+ try {
365
+ if (existsSync(join(repoPath, 'package.json'))) {
366
+ const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf-8'));
367
+ const framework = pkg.dependencies?.react ? 'React'
368
+ : pkg.dependencies?.next ? 'Next.js'
369
+ : pkg.dependencies?.vue ? 'Vue'
370
+ : pkg.dependencies?.express ? 'Express'
371
+ : null;
372
+ return { language: 'JavaScript/TypeScript', framework };
373
+ }
374
+ if (existsSync(join(repoPath, 'pom.xml')) || existsSync(join(repoPath, 'build.gradle'))) {
375
+ return { language: 'Java', framework: null };
376
+ }
377
+ if (existsSync(join(repoPath, 'requirements.txt')) || existsSync(join(repoPath, 'setup.py'))) {
378
+ return { language: 'Python', framework: null };
379
+ }
380
+ } catch (_) {}
381
+ return { language: 'Unknown', framework: null };
382
+ }
383
+
384
+ function generateShortId() {
385
+ return Math.random().toString(36).substring(2, 6);
386
+ }
387
+
388
+ /**
389
+ * Safe spawn wrapper — takes an explicit args array, never uses shell: true.
390
+ * @param {string} bin - executable (e.g. 'git')
391
+ * @param {string[]} args - argument list
392
+ * @param {string} cwd - working directory
393
+ */
394
+ async function execCommand(bin, args, cwd) {
395
+ return new Promise((res, reject) => {
396
+ const proc = spawn(bin, args, { cwd, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
397
+
398
+ let stdout = '';
399
+ let stderr = '';
400
+
401
+ proc.stdout.on('data', (data) => {
402
+ const output = data.toString();
403
+ stdout += output;
404
+ console.log(output.trimEnd());
405
+ });
406
+
407
+ proc.stderr.on('data', (data) => {
408
+ const output = data.toString();
409
+ stderr += output;
410
+ console.log(output.trimEnd());
411
+ });
412
+
413
+ proc.on('close', (code) => {
414
+ if (code !== 0) {
415
+ reject(new Error(`Command failed (exit ${code}): ${bin} ${args.join(' ')}`));
416
+ } else {
417
+ res(stdout || stderr || '');
418
+ }
419
+ });
420
+
421
+ proc.on('error', (err) => {
422
+ reject(new Error(`Command error: ${bin} - ${err.message}`));
423
+ });
424
+ });
425
+ }