newo 2.0.5 → 3.0.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 (60) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +146 -0
  3. package/dist/api.d.ts +17 -1
  4. package/dist/api.js +78 -0
  5. package/dist/cli/commands/create-agent.d.ts +3 -0
  6. package/dist/cli/commands/create-agent.js +75 -0
  7. package/dist/cli/commands/create-attribute.d.ts +3 -0
  8. package/dist/cli/commands/create-attribute.js +63 -0
  9. package/dist/cli/commands/create-event.d.ts +3 -0
  10. package/dist/cli/commands/create-event.js +66 -0
  11. package/dist/cli/commands/create-flow.d.ts +3 -0
  12. package/dist/cli/commands/create-flow.js +100 -0
  13. package/dist/cli/commands/create-parameter.d.ts +3 -0
  14. package/dist/cli/commands/create-parameter.js +47 -0
  15. package/dist/cli/commands/create-persona.d.ts +3 -0
  16. package/dist/cli/commands/create-persona.js +43 -0
  17. package/dist/cli/commands/create-project.d.ts +3 -0
  18. package/dist/cli/commands/create-project.js +55 -0
  19. package/dist/cli/commands/create-skill.d.ts +3 -0
  20. package/dist/cli/commands/create-skill.js +115 -0
  21. package/dist/cli/commands/create-state.d.ts +3 -0
  22. package/dist/cli/commands/create-state.js +58 -0
  23. package/dist/cli/commands/delete-agent.d.ts +3 -0
  24. package/dist/cli/commands/delete-agent.js +70 -0
  25. package/dist/cli/commands/delete-flow.d.ts +3 -0
  26. package/dist/cli/commands/delete-flow.js +83 -0
  27. package/dist/cli/commands/delete-skill.d.ts +3 -0
  28. package/dist/cli/commands/delete-skill.js +87 -0
  29. package/dist/cli/commands/help.js +104 -22
  30. package/dist/cli/commands/push.js +4 -3
  31. package/dist/cli.js +48 -0
  32. package/dist/sync/diff-utils.d.ts +18 -0
  33. package/dist/sync/diff-utils.js +152 -0
  34. package/dist/sync/push.d.ts +1 -1
  35. package/dist/sync/push.js +372 -4
  36. package/dist/sync/skill-files.js +22 -49
  37. package/dist/sync/status.js +178 -1
  38. package/dist/types.d.ts +100 -1
  39. package/package.json +1 -1
  40. package/src/api.ts +118 -1
  41. package/src/cli/commands/create-agent.ts +96 -0
  42. package/src/cli/commands/create-attribute.ts +75 -0
  43. package/src/cli/commands/create-event.ts +79 -0
  44. package/src/cli/commands/create-flow.ts +124 -0
  45. package/src/cli/commands/create-parameter.ts +59 -0
  46. package/src/cli/commands/create-persona.ts +54 -0
  47. package/src/cli/commands/create-project.ts +66 -0
  48. package/src/cli/commands/create-skill.ts +144 -0
  49. package/src/cli/commands/create-state.ts +71 -0
  50. package/src/cli/commands/delete-agent.ts +90 -0
  51. package/src/cli/commands/delete-flow.ts +105 -0
  52. package/src/cli/commands/delete-skill.ts +110 -0
  53. package/src/cli/commands/help.ts +104 -22
  54. package/src/cli/commands/push.ts +5 -3
  55. package/src/cli.ts +60 -0
  56. package/src/sync/diff-utils.ts +168 -0
  57. package/src/sync/push.ts +413 -5
  58. package/src/sync/skill-files.ts +22 -57
  59. package/src/sync/status.ts +183 -1
  60. package/src/types.ts +122 -2
package/src/sync/push.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Push operations for changed files
3
3
  */
4
- import { updateSkill } from '../api.js';
4
+ import { updateSkill, createAgent, createFlow, createSkill, publishFlow } from '../api.js';
5
5
  import {
6
6
  ensureState,
7
7
  mapPath,
8
- skillMetadataPath
8
+ skillMetadataPath,
9
+ projectDir,
10
+ agentMetadataPath
9
11
  } from '../fsutil.js';
10
12
  import {
11
13
  validateSkillFolder,
12
- getSingleSkillFile
14
+ getSingleSkillFile,
15
+ getExtensionForRunner
13
16
  } from './skill-files.js';
14
17
  import fs from 'fs-extra';
15
18
  import { sha256, loadHashes, saveHashes } from '../hash.js';
@@ -22,13 +25,135 @@ import type {
22
25
  ProjectData,
23
26
  ProjectMap,
24
27
  CustomerConfig,
25
- SkillMetadata
28
+ SkillMetadata,
29
+ AgentMetadata,
30
+ FlowMetadata,
31
+ CreateAgentRequest,
32
+ CreateFlowRequest,
33
+ CreateSkillRequest,
34
+ PublishFlowRequest
26
35
  } from '../types.js';
27
36
 
37
+ /**
38
+ * Scan filesystem for local-only entities not in the project map yet
39
+ */
40
+ async function scanForLocalOnlyEntities(customer: CustomerConfig, projects: Record<string, ProjectData>, verbose: boolean = false): Promise<{ agentCount: number; flowCount: number; skillCount: number; entities: Array<{ type: 'agent' | 'flow' | 'skill'; path: string; idn: string; projectIdn: string; agentIdn?: string; flowIdn?: string }> }> {
41
+ const localEntities: Array<{ type: 'agent' | 'flow' | 'skill'; path: string; idn: string; projectIdn: string; agentIdn?: string; flowIdn?: string }> = [];
42
+ let agentCount = 0;
43
+ let flowCount = 0;
44
+ let skillCount = 0;
45
+
46
+ // Scan each project directory
47
+ for (const [projectIdn] of Object.entries(projects)) {
48
+ const projDir = projectDir(customer.idn, projectIdn);
49
+ if (!(await fs.pathExists(projDir))) continue;
50
+
51
+ if (verbose) console.log(`🔍 Scanning project directory: ${projDir}`);
52
+
53
+ // Get all subdirectories in the project (these should be agents)
54
+ const agentDirs = await fs.readdir(projDir);
55
+
56
+ for (const agentIdn of agentDirs) {
57
+ const agentPath = `${projDir}/${agentIdn}`;
58
+ const agentStat = await fs.stat(agentPath);
59
+
60
+ // Skip files, only process directories
61
+ if (!agentStat.isDirectory()) continue;
62
+
63
+ // Skip if it's not really an agent directory (no metadata.yaml)
64
+ const agentMetaPath = agentMetadataPath(customer.idn, projectIdn, agentIdn);
65
+ if (!(await fs.pathExists(agentMetaPath))) continue;
66
+
67
+ // Check if this agent is already in the project map
68
+ const projectData = projects[projectIdn];
69
+ if (!projectData?.agents[agentIdn]) {
70
+ // This is a local-only agent!
71
+ localEntities.push({
72
+ type: 'agent',
73
+ path: agentMetaPath,
74
+ idn: agentIdn,
75
+ projectIdn
76
+ });
77
+ agentCount++;
78
+ if (verbose) console.log(` 🆕 Found local-only agent: ${agentIdn}`);
79
+ }
80
+
81
+ // Now scan for flows within this agent (regardless of whether agent is local-only or not)
82
+ try {
83
+ const flowDirs = await fs.readdir(agentPath);
84
+ for (const flowIdn of flowDirs) {
85
+ const flowPath = `${agentPath}/${flowIdn}`;
86
+ const flowStat = await fs.stat(flowPath);
87
+
88
+ // Skip files, only process directories
89
+ if (!flowStat.isDirectory()) continue;
90
+
91
+ // Skip if it's not really a flow directory (no metadata.yaml)
92
+ const flowMetaPath = `${flowPath}/metadata.yaml`;
93
+ if (!(await fs.pathExists(flowMetaPath))) continue;
94
+
95
+ // Check if this flow exists in the project map
96
+ const agentData = projectData?.agents[agentIdn];
97
+ if (!agentData?.flows[flowIdn]) {
98
+ // This is a local-only flow!
99
+ localEntities.push({
100
+ type: 'flow',
101
+ path: flowMetaPath,
102
+ idn: flowIdn,
103
+ projectIdn,
104
+ agentIdn
105
+ });
106
+ flowCount++;
107
+ if (verbose) console.log(` 🆕 Found local-only flow: ${agentIdn}/${flowIdn}`);
108
+ }
109
+
110
+ // Now scan for skills within this flow (regardless of whether flow is local-only or not)
111
+ try {
112
+ const skillDirs = await fs.readdir(flowPath);
113
+ for (const skillIdn of skillDirs) {
114
+ const skillPath = `${flowPath}/${skillIdn}`;
115
+ const skillStat = await fs.stat(skillPath);
116
+
117
+ // Skip files, only process directories
118
+ if (!skillStat.isDirectory()) continue;
119
+
120
+ // Skip if it's not really a skill directory (no metadata.yaml)
121
+ const skillMetaPath = `${skillPath}/metadata.yaml`;
122
+ if (!(await fs.pathExists(skillMetaPath))) continue;
123
+
124
+ // Check if this skill exists in the project map
125
+ const flowData = agentData?.flows[flowIdn];
126
+ if (!flowData?.skills[skillIdn]) {
127
+ // This is a local-only skill!
128
+ localEntities.push({
129
+ type: 'skill',
130
+ path: skillMetaPath,
131
+ idn: skillIdn,
132
+ projectIdn,
133
+ agentIdn,
134
+ flowIdn
135
+ });
136
+ skillCount++;
137
+ if (verbose) console.log(` 🆕 Found local-only skill: ${agentIdn}/${flowIdn}/${skillIdn}`);
138
+ }
139
+ }
140
+ } catch (error) {
141
+ // Ignore errors reading flow directory
142
+ }
143
+ }
144
+ } catch (error) {
145
+ // Ignore errors reading agent directory
146
+ }
147
+ }
148
+ }
149
+
150
+ return { agentCount, flowCount, skillCount, entities: localEntities };
151
+ }
152
+
28
153
  /**
29
154
  * Push changed files to NEWO platform
30
155
  */
31
- export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false): Promise<void> {
156
+ export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false, shouldPublish: boolean = true): Promise<void> {
32
157
  await ensureState(customer.idn);
33
158
  if (!(await fs.pathExists(mapPath(customer.idn)))) {
34
159
  console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
@@ -50,6 +175,205 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
50
175
  ? { '': idMapData as ProjectData }
51
176
  : (() => { throw new Error('Invalid project map format'); })();
52
177
 
178
+ // First, handle any local-only entities (created locally but not yet pushed)
179
+ const localScan = await scanForLocalOnlyEntities(customer, projects, verbose);
180
+ const totalLocalEntities = localScan.agentCount + localScan.flowCount + localScan.skillCount;
181
+
182
+ if (totalLocalEntities > 0) {
183
+ console.log(`📤 Found ${localScan.agentCount} new agent(s), ${localScan.flowCount} new flow(s), ${localScan.skillCount} new skill(s) to create...`);
184
+
185
+ // Process in order: agents first, then flows, then skills
186
+ const sortedEntities = localScan.entities.sort((a, b) => {
187
+ const typeOrder = { 'agent': 0, 'flow': 1, 'skill': 2 };
188
+ return typeOrder[a.type] - typeOrder[b.type];
189
+ });
190
+
191
+ for (const entity of sortedEntities) {
192
+ if (entity.type === 'agent') {
193
+ try {
194
+ // Read agent metadata
195
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
196
+ const metadata = yaml.load(metadataContent) as AgentMetadata;
197
+
198
+ if (verbose) console.log(`📤 Creating agent: ${entity.idn}`);
199
+
200
+ // Get project ID from the project map
201
+ const projectData = projects[entity.projectIdn];
202
+ if (!projectData?.projectId) {
203
+ console.error(`❌ Project ID not found for project: ${entity.projectIdn}`);
204
+ continue;
205
+ }
206
+
207
+ // Create agent on NEWO platform using project-specific v2 endpoint
208
+ const createAgentRequest: CreateAgentRequest = {
209
+ idn: metadata.idn,
210
+ title: metadata.title || metadata.idn,
211
+ description: metadata.description || null,
212
+ persona_id: metadata.persona_id || null
213
+ };
214
+
215
+ const createResponse = await createAgent(client, projectData.projectId, createAgentRequest);
216
+ console.log(`✅ Agent created: ${entity.idn} (ID: ${createResponse.id})`);
217
+ pushed++;
218
+ metadataChanged = true;
219
+
220
+ // Update the metadata with the new ID
221
+ metadata.id = createResponse.id;
222
+ metadata.updated_at = new Date().toISOString();
223
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
224
+ await fs.writeFile(entity.path, updatedMetadataYaml);
225
+
226
+ // Update the project map to include the new agent
227
+ if (!projectData.agents[entity.idn]) {
228
+ projectData.agents[entity.idn] = {
229
+ id: createResponse.id,
230
+ flows: {}
231
+ };
232
+ }
233
+
234
+ } catch (error: any) {
235
+ console.error(`❌ Failed to create agent ${entity.idn}:`, error.response?.data?.message || error.message);
236
+ }
237
+
238
+ } else if (entity.type === 'flow') {
239
+ try {
240
+ // Read flow metadata
241
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
242
+ const metadata = yaml.load(metadataContent) as FlowMetadata;
243
+
244
+ if (verbose) console.log(`📤 Creating flow: ${entity.agentIdn}/${entity.idn}`);
245
+
246
+ // Get agent ID from the project map
247
+ const projectData = projects[entity.projectIdn];
248
+ if (!entity.agentIdn) {
249
+ console.error(`❌ Agent IDN missing for flow: ${entity.idn}`);
250
+ continue;
251
+ }
252
+ const agentData = projectData?.agents[entity.agentIdn];
253
+ if (!agentData?.id) {
254
+ console.error(`❌ Agent ID not found for agent: ${entity.agentIdn}`);
255
+ continue;
256
+ }
257
+
258
+ // Create flow on NEWO platform
259
+ const createFlowRequest: CreateFlowRequest = {
260
+ idn: metadata.idn,
261
+ title: metadata.title || metadata.idn
262
+ };
263
+
264
+ const createResponse = await createFlow(client, agentData.id, createFlowRequest);
265
+ console.log(`✅ Flow created: ${entity.idn} (ID: ${createResponse.id})`);
266
+ pushed++;
267
+ metadataChanged = true;
268
+
269
+ // Handle the special case where NEWO flow API returns empty response
270
+ if (createResponse.id === 'pending-sync') {
271
+ console.log(`✅ Flow created: ${entity.idn} (ID will be synced on next pull)`);
272
+ // Mark flow as created but pending ID sync
273
+ metadata.id = ''; // Keep empty until sync
274
+ metadata.updated_at = new Date().toISOString();
275
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
276
+ await fs.writeFile(entity.path, updatedMetadataYaml);
277
+
278
+ // Update the project map with empty ID (will be filled by pull)
279
+ if (!agentData.flows[entity.idn]) {
280
+ agentData.flows[entity.idn] = {
281
+ id: '', // Empty until synced
282
+ skills: {}
283
+ };
284
+ }
285
+ } else {
286
+ // Normal case with ID returned
287
+ metadata.id = createResponse.id;
288
+ metadata.updated_at = new Date().toISOString();
289
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
290
+ await fs.writeFile(entity.path, updatedMetadataYaml);
291
+
292
+ // Update the project map to include the new flow
293
+ if (!agentData.flows[entity.idn]) {
294
+ agentData.flows[entity.idn] = {
295
+ id: createResponse.id,
296
+ skills: {}
297
+ };
298
+ }
299
+ }
300
+
301
+ } catch (error: any) {
302
+ console.error(`❌ Failed to create flow ${entity.idn}:`, error.response?.data?.message || error.message);
303
+ }
304
+
305
+ } else if (entity.type === 'skill') {
306
+ try {
307
+ // Read skill metadata
308
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
309
+ const metadata = yaml.load(metadataContent) as SkillMetadata;
310
+
311
+ if (verbose) console.log(`📤 Creating skill: ${entity.agentIdn}/${entity.flowIdn}/${entity.idn}`);
312
+
313
+ // Get flow ID from the project map
314
+ const projectData = projects[entity.projectIdn];
315
+ if (!entity.agentIdn || !entity.flowIdn) {
316
+ console.error(`❌ Agent IDN or Flow IDN missing for skill: ${entity.idn}`);
317
+ continue;
318
+ }
319
+ const agentData = projectData?.agents[entity.agentIdn];
320
+ const flowData = agentData?.flows[entity.flowIdn];
321
+ if (!flowData?.id) {
322
+ console.error(`❌ Flow ID not found for flow: ${entity.flowIdn}`);
323
+ continue;
324
+ }
325
+
326
+ // Read the skill script content
327
+ const skillFolderBase = entity.path.replace('/metadata.yaml', '');
328
+ const scriptExtension = getExtensionForRunner(metadata.runner_type);
329
+ const scriptPath = `${skillFolderBase}/${entity.idn}.${scriptExtension}`;
330
+
331
+ let scriptContent = '';
332
+ if (await fs.pathExists(scriptPath)) {
333
+ scriptContent = await fs.readFile(scriptPath, 'utf8');
334
+ }
335
+
336
+ // Create skill on NEWO platform
337
+ const createSkillRequest: CreateSkillRequest = {
338
+ idn: metadata.idn,
339
+ title: metadata.title || metadata.idn,
340
+ prompt_script: scriptContent,
341
+ runner_type: metadata.runner_type,
342
+ model: metadata.model,
343
+ path: "", // Empty path as shown in curl example
344
+ parameters: metadata.parameters || []
345
+ };
346
+
347
+ const createResponse = await createSkill(client, flowData.id, createSkillRequest);
348
+ console.log(`✅ Skill created: ${entity.idn} (ID: ${createResponse.id})`);
349
+ pushed++;
350
+ metadataChanged = true;
351
+
352
+ // Update the metadata with the new ID
353
+ metadata.id = createResponse.id;
354
+ metadata.updated_at = new Date().toISOString();
355
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
356
+ await fs.writeFile(entity.path, updatedMetadataYaml);
357
+
358
+ // Update the project map to include the new skill
359
+ if (!flowData.skills[entity.idn]) {
360
+ flowData.skills[entity.idn] = {
361
+ id: createResponse.id,
362
+ idn: metadata.idn,
363
+ title: metadata.title || metadata.idn,
364
+ runner_type: metadata.runner_type,
365
+ model: metadata.model,
366
+ parameters: metadata.parameters || []
367
+ };
368
+ }
369
+
370
+ } catch (error: any) {
371
+ console.error(`❌ Failed to create skill ${entity.idn}:`, error.response?.data?.message || error.message);
372
+ }
373
+ }
374
+ }
375
+ }
376
+
53
377
  for (const [projectIdn, projectData] of Object.entries(projects)) {
54
378
  if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
55
379
 
@@ -193,8 +517,92 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
193
517
  newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
194
518
  }
195
519
 
520
+ // Save updated project map if metadata changed (new agents added)
521
+ if (metadataChanged) {
522
+ const updatedMapData = isProjectMap(idMapData)
523
+ ? { projects } as ProjectMap
524
+ : projects[''] as ProjectData; // Legacy format
525
+
526
+ if (verbose) console.log(`💾 Saving updated project map...`);
527
+ await fs.writeJson(mapPath(customer.idn), updatedMapData, { spaces: 2 });
528
+ }
529
+
196
530
  // Save updated hashes
197
531
  await saveHashes(newHashes, customer.idn);
198
532
 
199
533
  console.log(pushed ? `${pushed} file(s) pushed.` : 'No changes to push.');
534
+
535
+ // Publish flows if requested (default behavior)
536
+ if (shouldPublish && pushed > 0) {
537
+ if (verbose) console.log('\n🚀 Publishing flows...');
538
+
539
+ let publishedFlows = 0;
540
+ let failedFlows = 0;
541
+ const publishErrors: Array<{ flowIdn: string; error: string; details?: any }> = [];
542
+
543
+ for (const [, projectData] of Object.entries(projects)) {
544
+ for (const [, agentObj] of Object.entries(projectData.agents)) {
545
+ for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
546
+ if (flowObj.id) {
547
+ try {
548
+ const publishData: PublishFlowRequest = {
549
+ version: "1.0",
550
+ description: "Published via NEWO CLI",
551
+ type: "public"
552
+ };
553
+
554
+ await publishFlow(client, flowObj.id, publishData);
555
+ if (verbose) console.log(`📤 Published flow: ${flowIdn} (${flowObj.id})`);
556
+ publishedFlows++;
557
+ } catch (error: any) {
558
+ failedFlows++;
559
+
560
+ // Extract detailed error information from API response
561
+ const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
562
+ const errorDetails = error.response?.data?.reasons || error.response?.data?.errors || error.response?.data?.detail;
563
+
564
+ publishErrors.push({
565
+ flowIdn,
566
+ error: errorMessage,
567
+ details: errorDetails
568
+ });
569
+
570
+ // Always show publish errors (not just in verbose mode)
571
+ console.error(`❌ Failed to publish flow '${flowIdn}': ${errorMessage}`);
572
+
573
+ if (errorDetails) {
574
+ if (Array.isArray(errorDetails)) {
575
+ console.error(` Reasons:`);
576
+ errorDetails.forEach((reason: string) => {
577
+ console.error(` • ${reason}`);
578
+ });
579
+ } else if (typeof errorDetails === 'object') {
580
+ console.error(` Details: ${JSON.stringify(errorDetails, null, 2)}`);
581
+ } else {
582
+ console.error(` Details: ${errorDetails}`);
583
+ }
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+
591
+ // Summary message
592
+ if (publishedFlows > 0 || failedFlows > 0) {
593
+ console.log(`\n🚀 Publish summary: ${publishedFlows} succeeded, ${failedFlows} failed.`);
594
+
595
+ if (failedFlows > 0) {
596
+ console.log(`\n⚠️ ${failedFlows} flow(s) failed to publish due to validation errors.`);
597
+ console.log(` Fix the errors above and run 'npm run push' again.`);
598
+ }
599
+ } else if (verbose) {
600
+ console.log('\n💡 No flows to publish.');
601
+ }
602
+ }
603
+
604
+ // If we created flows, recommend a pull to sync flow IDs
605
+ if (localScan.flowCount > 0) {
606
+ console.log('\n💡 Tip: Run "newo pull" to sync flow IDs and enable skill creation.');
607
+ }
200
608
  }
@@ -162,71 +162,36 @@ export async function askForOverwrite(skillIdn: string, existingContent: string,
162
162
  const gray = '\x1b[90m';
163
163
  const reset = '\x1b[0m';
164
164
 
165
- // Show a GitHub-style colored diff with line numbers and context
166
- const localLines = existingContent.trim().split('\n');
167
- const remoteLines = newContent.trim().split('\n');
168
-
169
- // Find the first differing section and show it with context
170
- let diffStart = -1;
171
- let diffEnd = -1;
172
-
173
- // Find first difference
174
- for (let i = 0; i < Math.max(localLines.length, remoteLines.length); i++) {
175
- if (localLines[i] !== remoteLines[i]) {
176
- diffStart = i;
177
- break;
178
- }
179
- }
165
+ // Generate proper diff using LCS algorithm
166
+ const { generateDiff, filterDiffWithContext } = await import('./diff-utils.js');
167
+ const fullDiff = generateDiff(existingContent, newContent);
168
+ const contextDiff = filterDiffWithContext(fullDiff, 2);
180
169
 
181
- if (diffStart === -1) {
182
- // No differences found (shouldn't happen, but handle gracefully)
170
+ if (contextDiff.length === 0) {
183
171
  console.log(`${gray} No differences found${reset}`);
184
172
  return 'no';
185
173
  }
186
174
 
187
- // Find end of difference section
188
- diffEnd = diffStart;
189
- while (diffEnd < Math.max(localLines.length, remoteLines.length) &&
190
- (localLines[diffEnd] !== remoteLines[diffEnd] ||
191
- localLines[diffEnd] === undefined ||
192
- remoteLines[diffEnd] === undefined)) {
193
- diffEnd++;
194
- }
195
-
196
- // Show context before (2 lines)
197
- const contextStart = Math.max(0, diffStart - 2);
198
- for (let i = contextStart; i < diffStart; i++) {
199
- if (localLines[i] !== undefined) {
200
- console.log(` ${String(i + 1).padStart(3)} ${localLines[i]}`);
201
- }
202
- }
203
-
204
- // Show the differences
205
- const maxDiffLine = Math.min(diffEnd, Math.max(localLines.length, remoteLines.length));
206
- for (let i = diffStart; i < maxDiffLine; i++) {
207
- const localLine = localLines[i];
208
- const remoteLine = remoteLines[i];
209
-
210
- if (localLine !== undefined && (remoteLine === undefined || localLine !== remoteLine)) {
211
- console.log(`${redBg} - ${String(i + 1).padStart(3)} ${localLine} ${reset}`);
212
- }
213
- if (remoteLine !== undefined && (localLine === undefined || localLine !== remoteLine)) {
214
- console.log(`${greenBg} + ${String(i + 1).padStart(3)} ${remoteLine} ${reset}`);
215
- }
216
- }
217
-
218
- // Show context after (2 lines)
219
- const contextEnd = Math.min(localLines.length, diffEnd + 2);
220
- for (let i = diffEnd; i < contextEnd; i++) {
221
- if (localLines[i] !== undefined) {
222
- console.log(` ${String(i + 1).padStart(3)} ${localLines[i]}`);
175
+ // Display the diff with proper GitHub-style formatting
176
+ for (const line of contextDiff) {
177
+ if (line.type === 'context') {
178
+ // Show context lines in gray
179
+ const lineNum = line.localLineNum !== -1 ? line.localLineNum : line.remoteLineNum;
180
+ console.log(` ${String(lineNum).padStart(3)} ${line.content}`);
181
+ } else if (line.type === 'remove') {
182
+ // Show local content being removed (red background)
183
+ console.log(`${redBg} - ${String(line.localLineNum).padStart(3)} ${line.content} ${reset}`);
184
+ } else if (line.type === 'add') {
185
+ // Show remote content being added (green background)
186
+ console.log(`${greenBg} + ${String(line.remoteLineNum).padStart(3)} ${line.content} ${reset}`);
223
187
  }
224
188
  }
225
189
 
226
- // Show if there are more differences
227
- const totalDiffs = Math.abs(localLines.length - remoteLines.length) + (diffEnd - diffStart);
228
- if (totalDiffs > (diffEnd - diffStart)) {
229
- console.log(`${gray}... (${totalDiffs - (diffEnd - diffStart)} more differences)${reset}`);
190
+ // Show if there are more changes beyond what we're displaying
191
+ const totalChanges = fullDiff.filter(line => line.type !== 'context').length;
192
+ const displayedChanges = contextDiff.filter(line => line.type !== 'context').length;
193
+ if (totalChanges > displayedChanges) {
194
+ console.log(`${gray}... (${totalChanges - displayedChanges} more changes)${reset}`);
230
195
  }
231
196
 
232
197
  const answer = await new Promise<string>((resolve) => {