newo 3.4.2 → 3.6.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 (64) hide show
  1. package/.env.example +5 -0
  2. package/CHANGELOG.md +21 -0
  3. package/dist/api.d.ts +18 -0
  4. package/dist/api.js +28 -0
  5. package/dist/cli/commands/export.d.ts +3 -0
  6. package/dist/cli/commands/export.js +62 -0
  7. package/dist/cli/commands/help.js +54 -42
  8. package/dist/cli/commands/pull.js +38 -14
  9. package/dist/cli/commands/push.js +32 -32
  10. package/dist/cli/commands/status.js +46 -7
  11. package/dist/cli-new/bootstrap.d.ts +7 -1
  12. package/dist/cli-new/bootstrap.js +11 -5
  13. package/dist/cli-new/di/tokens.d.ts +1 -0
  14. package/dist/cli-new/di/tokens.js +1 -0
  15. package/dist/cli.js +4 -0
  16. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +5 -0
  17. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +97 -8
  18. package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +80 -0
  19. package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +725 -0
  20. package/dist/env.d.ts +1 -0
  21. package/dist/env.js +1 -0
  22. package/dist/format/detect.d.ts +14 -0
  23. package/dist/format/detect.js +105 -0
  24. package/dist/format/extensions.d.ts +26 -0
  25. package/dist/format/extensions.js +45 -0
  26. package/dist/format/index.d.ts +11 -0
  27. package/dist/format/index.js +11 -0
  28. package/dist/format/paths-v2.d.ts +31 -0
  29. package/dist/format/paths-v2.js +104 -0
  30. package/dist/format/types.d.ts +28 -0
  31. package/dist/format/types.js +21 -0
  32. package/dist/format/v2-yaml.d.ts +143 -0
  33. package/dist/format/v2-yaml.js +222 -0
  34. package/dist/format/yaml-patch.d.ts +14 -0
  35. package/dist/format/yaml-patch.js +184 -0
  36. package/dist/fsutil.d.ts +10 -0
  37. package/dist/fsutil.js +25 -0
  38. package/dist/sync/attributes.js +3 -3
  39. package/dist/sync/skill-files.js +2 -2
  40. package/dist/types.d.ts +5 -0
  41. package/package.json +1 -1
  42. package/src/api.ts +64 -0
  43. package/src/cli/commands/export.ts +78 -0
  44. package/src/cli/commands/help.ts +54 -42
  45. package/src/cli/commands/pull.ts +46 -15
  46. package/src/cli/commands/push.ts +38 -31
  47. package/src/cli/commands/status.ts +59 -9
  48. package/src/cli-new/bootstrap.ts +19 -7
  49. package/src/cli-new/di/tokens.ts +1 -0
  50. package/src/cli.ts +5 -0
  51. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +122 -8
  52. package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +1007 -0
  53. package/src/env.ts +2 -0
  54. package/src/format/detect.ts +123 -0
  55. package/src/format/extensions.ts +61 -0
  56. package/src/format/index.ts +66 -0
  57. package/src/format/paths-v2.ts +207 -0
  58. package/src/format/types.ts +40 -0
  59. package/src/format/v2-yaml.ts +345 -0
  60. package/src/format/yaml-patch.ts +208 -0
  61. package/src/fsutil.ts +37 -0
  62. package/src/sync/attributes.ts +3 -3
  63. package/src/sync/skill-files.ts +2 -2
  64. package/src/types.ts +6 -0
@@ -0,0 +1,725 @@
1
+ /**
2
+ * V2ProjectSyncStrategy - Handles synchronization in newo_v2 format
3
+ *
4
+ * Uses the SAME V1 API endpoints as ProjectSyncStrategy but writes/reads
5
+ * files in the newo_v2 directory layout:
6
+ * {CustomerIdn}/
7
+ * import_version.txt
8
+ * {ProjectIdn}/
9
+ * {project_idn}.yaml
10
+ * agents/{AgentIdn}/
11
+ * agent.yaml
12
+ * flows/{FlowIdn}/
13
+ * {FlowIdn}.yaml (inline skill defs, events, state_fields)
14
+ * skills/{SkillIdn}.nsl|.nslg
15
+ */
16
+ import fs from 'fs-extra';
17
+ import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, } from '../../../api.js';
18
+ import { ensureStateOnly, writeFileSafe, mapPath, } from '../../../fsutil.js';
19
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
20
+ import { v2ImportVersionPath, v2ProjectYamlPath, v2AgentYamlPath, v2FlowYamlPath, v2SkillScriptPath, v2SkillRelativePath, v2ProjectAttributesPath, v2CustomerAttributesPath, v2AkbDir, v2AkbPath, v2LibraryYamlPath, v2LibrarySkillScriptPath, v2LibrarySkillRelativePath, } from '../../../format/paths-v2.js';
21
+ import { V2_IMPORT_VERSION, } from '../../../format/types.js';
22
+ import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField, } from '../../../format/v2-yaml.js';
23
+ import { isContentDifferent } from '../../../sync/skill-files.js';
24
+ import yaml from 'js-yaml';
25
+ import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
26
+ /**
27
+ * V2ProjectSyncStrategy - same API, newo_v2 file layout
28
+ */
29
+ export class V2ProjectSyncStrategy {
30
+ apiClientFactory;
31
+ logger;
32
+ resourceType = 'projects';
33
+ displayName = 'Projects (newo_v2)';
34
+ constructor(apiClientFactory, logger) {
35
+ this.apiClientFactory = apiClientFactory;
36
+ this.logger = logger;
37
+ }
38
+ // ──────────────────────────────────────
39
+ // PULL
40
+ // ──────────────────────────────────────
41
+ async pull(customer, options = {}) {
42
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
43
+ const hashes = {};
44
+ const projects = [];
45
+ this.logger.verbose(`[newo_v2] Loading project list for customer ${customer.idn}...`);
46
+ // Use V2 state init (no V1 projects/ dir)
47
+ await ensureStateOnly(customer.idn);
48
+ // Write import_version.txt marker
49
+ const versionPath = v2ImportVersionPath(customer.idn);
50
+ await writeFileSafe(versionPath, V2_IMPORT_VERSION);
51
+ // Write V2 customer attributes: attributes.yaml (sorted, with !enum ValueType.X)
52
+ try {
53
+ const custAttrs = await getCustomerAttributes(client, true);
54
+ const attrs = custAttrs.attributes || [];
55
+ if (attrs.length > 0) {
56
+ const attrYaml = formatV2AttributesYaml(attrs);
57
+ const custAttrPath = v2CustomerAttributesPath(customer.idn);
58
+ await writeFileSafe(custAttrPath, attrYaml);
59
+ hashes[custAttrPath] = sha256(attrYaml);
60
+ }
61
+ }
62
+ catch {
63
+ this.logger.verbose(` Could not pull customer attributes`);
64
+ }
65
+ // Fetch projects from API (same V1 endpoints)
66
+ const apiProjects = options.projectId
67
+ ? [{ id: options.projectId, idn: 'unknown', title: 'Project' }]
68
+ : await listProjects(client);
69
+ if (apiProjects.length === 0) {
70
+ this.logger.info(`No projects found for customer ${customer.idn}`);
71
+ return { items: [], count: 0, hashes: {} };
72
+ }
73
+ // Load existing map for reference
74
+ let existingMap = { projects: {} };
75
+ const mapFile = mapPath(customer.idn);
76
+ if (await fs.pathExists(mapFile)) {
77
+ try {
78
+ const mapData = await fs.readJson(mapFile);
79
+ if (mapData && typeof mapData === 'object' && 'projects' in mapData) {
80
+ existingMap = mapData;
81
+ }
82
+ }
83
+ catch {
84
+ // Start fresh
85
+ }
86
+ }
87
+ // Count total skills for progress
88
+ let totalSkills = 0;
89
+ let processedSkills = 0;
90
+ for (const project of apiProjects) {
91
+ const agents = await listAgents(client, project.id);
92
+ for (const agent of agents) {
93
+ const flows = agent.flows || [];
94
+ for (const flow of flows) {
95
+ const skills = await listFlowSkills(client, flow.id);
96
+ totalSkills += skills.length;
97
+ }
98
+ }
99
+ }
100
+ this.logger.verbose(`[newo_v2] Total skills to process: ${totalSkills}`);
101
+ // Process each project
102
+ for (const project of apiProjects) {
103
+ this.logger.verbose(`[newo_v2] Processing project: ${project.title} (${project.idn})`);
104
+ // Write V2 project YAML: {project_idn}.yaml
105
+ // The API returns registry_idn (not registry) - map to V2 field name
106
+ const projectYaml = generateV2ProjectYaml({
107
+ idn: project.idn,
108
+ name: project.title || project.idn,
109
+ version: project.version || '1.0.0',
110
+ description: project.description || '',
111
+ is_auto_update_enabled: project.is_auto_update_enabled ?? false,
112
+ registry: project.registry_idn || project.registry || '',
113
+ registry_item_idn: project.registry_item_idn || '',
114
+ });
115
+ const projectYamlPath = v2ProjectYamlPath(customer.idn, project.idn);
116
+ await writeFileSafe(projectYamlPath, projectYaml);
117
+ hashes[projectYamlPath] = sha256(projectYaml);
118
+ const localProject = {
119
+ projectId: project.id,
120
+ projectIdn: project.idn,
121
+ metadata: {
122
+ id: project.id,
123
+ idn: project.idn,
124
+ title: project.title,
125
+ description: project.description || '',
126
+ created_at: project.created_at || '',
127
+ updated_at: project.updated_at || '',
128
+ },
129
+ agents: []
130
+ };
131
+ const agents = await listAgents(client, project.id);
132
+ this.logger.verbose(` Found ${agents.length} agents in project ${project.title}`);
133
+ const projectData = {
134
+ projectId: project.id,
135
+ projectIdn: project.idn,
136
+ agents: {}
137
+ };
138
+ // Process each agent
139
+ for (const agent of agents) {
140
+ const localAgent = await this.pullAgent(client, customer, project, agent, hashes, options, () => {
141
+ processedSkills++;
142
+ if (!options.verbose && totalSkills > 0) {
143
+ if (processedSkills % 10 === 0 || processedSkills === totalSkills) {
144
+ this.logger.progress(processedSkills, totalSkills, '[newo_v2] Processing skills');
145
+ }
146
+ }
147
+ });
148
+ localProject.agents.push(localAgent);
149
+ // Build project data for map
150
+ projectData.agents[agent.idn] = {
151
+ id: agent.id,
152
+ flows: {}
153
+ };
154
+ for (const flow of localAgent.flows) {
155
+ projectData.agents[agent.idn].flows[flow.idn] = {
156
+ id: flow.id,
157
+ skills: {}
158
+ };
159
+ for (const skill of flow.skills) {
160
+ projectData.agents[agent.idn].flows[flow.idn].skills[skill.idn] = skill.metadata;
161
+ }
162
+ }
163
+ }
164
+ // Pull libraries for this project
165
+ try {
166
+ const libraries = await listLibraries(client, project.id);
167
+ if (libraries.length > 0) {
168
+ this.logger.verbose(` Found ${libraries.length} libraries in project ${project.idn}`);
169
+ projectData.libraries = {};
170
+ for (const lib of libraries) {
171
+ await this.pullLibrary(client, customer, project, lib, hashes, options);
172
+ projectData.libraries[lib.idn] = {
173
+ id: lib.id,
174
+ skills: {}
175
+ };
176
+ for (const skill of lib.skills) {
177
+ projectData.libraries[lib.idn].skills[skill.idn] = {
178
+ id: skill.id,
179
+ idn: skill.idn,
180
+ title: skill.title,
181
+ runner_type: skill.runner_type,
182
+ model: skill.model,
183
+ parameters: [...skill.parameters],
184
+ path: skill.path
185
+ };
186
+ }
187
+ }
188
+ }
189
+ }
190
+ catch {
191
+ this.logger.verbose(` Could not pull libraries for project ${project.idn}`);
192
+ }
193
+ // Write V2 project attributes: {project_idn}/attributes.yaml
194
+ try {
195
+ const projAttrs = await getProjectAttributes(client, project.id, true);
196
+ const attrs = projAttrs.attributes || [];
197
+ if (attrs.length > 0) {
198
+ const attrYaml = formatV2AttributesYaml(attrs);
199
+ const attrPath = v2ProjectAttributesPath(customer.idn, project.idn);
200
+ await writeFileSafe(attrPath, attrYaml);
201
+ hashes[attrPath] = sha256(attrYaml);
202
+ }
203
+ }
204
+ catch {
205
+ this.logger.verbose(` Could not pull attributes for project ${project.idn}`);
206
+ }
207
+ existingMap.projects[project.idn] = projectData;
208
+ projects.push(localProject);
209
+ }
210
+ // Write AKB stub files for all agents: akb/{AgentIdn}.yaml
211
+ // V2 format creates an empty [] file for every agent persona
212
+ const akbDirPath = v2AkbDir(customer.idn);
213
+ await fs.ensureDir(akbDirPath);
214
+ for (const project of projects) {
215
+ for (const agent of project.agents) {
216
+ const akbFilePath = v2AkbPath(customer.idn, agent.idn);
217
+ if (!(await fs.pathExists(akbFilePath))) {
218
+ await writeFileSafe(akbFilePath, '[]\n');
219
+ }
220
+ // Don't overwrite existing AKB files that may have content from AkbSyncStrategy
221
+ }
222
+ }
223
+ // Save updated project map
224
+ await writeFileSafe(mapFile, JSON.stringify(existingMap, null, 2));
225
+ // Save hashes
226
+ await saveHashes(hashes, customer.idn);
227
+ return {
228
+ items: projects,
229
+ count: projects.length,
230
+ hashes
231
+ };
232
+ }
233
+ /**
234
+ * Pull a single agent in V2 format
235
+ */
236
+ async pullAgent(client, customer, project, agent, hashes, options, onSkillProcessed) {
237
+ this.logger.verbose(` [newo_v2] Processing agent: ${agent.title} (${agent.idn})`);
238
+ // Write V2 agent YAML: agents/{AgentIdn}/agent.yaml
239
+ // Preserve exact API values (null title stays null, "" description stays "")
240
+ const agentYaml = generateV2AgentYaml({
241
+ idn: agent.idn,
242
+ title: agent.title ?? null,
243
+ description: agent.description ?? null,
244
+ });
245
+ const agentYamlFilePath = v2AgentYamlPath(customer.idn, project.idn, agent.idn);
246
+ await writeFileSafe(agentYamlFilePath, agentYaml);
247
+ hashes[agentYamlFilePath] = sha256(agentYaml);
248
+ const localAgent = {
249
+ id: agent.id,
250
+ idn: agent.idn,
251
+ metadata: {
252
+ id: agent.id,
253
+ idn: agent.idn,
254
+ title: agent.title || '',
255
+ description: agent.description || '',
256
+ },
257
+ flows: []
258
+ };
259
+ const flows = agent.flows || [];
260
+ this.logger.verbose(` Found ${flows.length} flows in agent ${agent.title}`);
261
+ for (const flow of flows) {
262
+ const localFlow = await this.pullFlow(client, customer, project, agent, flow, hashes, options, onSkillProcessed);
263
+ localAgent.flows.push(localFlow);
264
+ }
265
+ return localAgent;
266
+ }
267
+ /**
268
+ * Pull a single flow in V2 format
269
+ *
270
+ * In V2, the flow YAML contains inline skill definitions, events, and state_fields.
271
+ * Skills are written to flows/{FlowIdn}/skills/{SkillIdn}.nsl|.nslg
272
+ */
273
+ async pullFlow(client, customer, project, agent, flow, hashes, options, onSkillProcessed) {
274
+ this.logger.verbose(` [newo_v2] Processing flow: ${flow.title} (${flow.idn})`);
275
+ // Get flow events and states
276
+ const [events, states] = await Promise.all([
277
+ listFlowEvents(client, flow.id).catch(() => []),
278
+ listFlowStates(client, flow.id).catch(() => [])
279
+ ]);
280
+ // Process skills
281
+ const skills = await listFlowSkills(client, flow.id);
282
+ this.logger.verbose(` Found ${skills.length} skills in flow ${flow.title}`);
283
+ // Build V2 inline skill definitions
284
+ const v2Skills = [];
285
+ const localFlow = {
286
+ id: flow.id,
287
+ idn: flow.idn,
288
+ metadata: {
289
+ id: flow.id,
290
+ idn: flow.idn,
291
+ title: flow.title,
292
+ description: flow.description || '',
293
+ default_runner_type: flow.default_runner_type,
294
+ default_model: flow.default_model,
295
+ events,
296
+ state_fields: states
297
+ },
298
+ skills: []
299
+ };
300
+ for (const skill of skills) {
301
+ const localSkill = await this.pullSkill(customer, project, agent, flow, skill, hashes, options);
302
+ localFlow.skills.push(localSkill);
303
+ onSkillProcessed();
304
+ // Build inline skill definition for flow YAML
305
+ const relPath = v2SkillRelativePath(flow.idn, skill.idn, skill.runner_type);
306
+ v2Skills.push(buildV2InlineSkill(skill.idn, skill.title || '', skill.runner_type, skill.model?.model_idn || flow.default_model?.model_idn || '', skill.model?.provider_idn || flow.default_model?.provider_idn || '', skill.parameters.map(p => ({
307
+ name: p.name,
308
+ default_value: p.default_value ?? '',
309
+ })), relPath));
310
+ }
311
+ // Build V2 events
312
+ const v2Events = events.map(e => buildV2FlowEvent(e.idn, e.skill_selector || 'skill_idn', e.skill_idn || null, e.state_idn || null, e.integration_idn || null, e.connector_idn || null, e.interrupt_mode || 'queue'));
313
+ // Build V2 state fields
314
+ const v2States = states.map(s => buildV2StateField(s.idn, s.title || '', s.default_value ?? '', s.scope || 'user'));
315
+ // Write V2 flow YAML: flows/{FlowIdn}/{FlowIdn}.yaml
316
+ const flowYaml = generateV2FlowYaml(flow.idn, flow.title || flow.idn, flow.description ?? null, flow.default_runner_type || 'guidance', flow.default_model?.provider_idn || '', flow.default_model?.model_idn || '', v2Skills, v2Events, v2States);
317
+ const flowYamlFilePath = v2FlowYamlPath(customer.idn, project.idn, agent.idn, flow.idn);
318
+ await writeFileSafe(flowYamlFilePath, flowYaml);
319
+ hashes[flowYamlFilePath] = sha256(flowYaml);
320
+ return localFlow;
321
+ }
322
+ /**
323
+ * Pull a single skill script in V2 format
324
+ *
325
+ * Script goes to: flows/{FlowIdn}/skills/{SkillIdn}.nsl|.nslg
326
+ * No separate metadata.yaml - metadata is inline in the flow YAML
327
+ */
328
+ async pullSkill(customer, project, agent, flow, skill, hashes, options) {
329
+ this.logger.verbose(` [newo_v2] Processing skill: ${skill.title} (${skill.idn})`);
330
+ const scriptContent = skill.prompt_script || '';
331
+ const targetPath = v2SkillScriptPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn, skill.runner_type);
332
+ // Check for existing file and handle overwrites
333
+ let shouldWrite = true;
334
+ if (await fs.pathExists(targetPath)) {
335
+ const existingContent = await fs.readFile(targetPath, 'utf8');
336
+ if (!isContentDifferent(existingContent, scriptContent)) {
337
+ shouldWrite = false;
338
+ hashes[targetPath] = sha256(scriptContent);
339
+ }
340
+ else if (!options.silentOverwrite) {
341
+ // In non-silent mode, we overwrite (interactive mode handled in CLI layer)
342
+ shouldWrite = true;
343
+ }
344
+ }
345
+ if (shouldWrite) {
346
+ await writeFileSafe(targetPath, scriptContent);
347
+ hashes[targetPath] = sha256(scriptContent);
348
+ }
349
+ const skillMeta = {
350
+ id: skill.id,
351
+ idn: skill.idn,
352
+ title: skill.title,
353
+ runner_type: skill.runner_type,
354
+ model: skill.model,
355
+ parameters: [...skill.parameters],
356
+ path: skill.path
357
+ };
358
+ return {
359
+ id: skill.id,
360
+ idn: skill.idn,
361
+ metadata: skillMeta,
362
+ scriptPath: targetPath,
363
+ scriptContent
364
+ };
365
+ }
366
+ /**
367
+ * Pull a library and its skills in V2 format
368
+ *
369
+ * Writes:
370
+ * {project}/libraries/{lib}/{lib}.yaml (with inline skill list)
371
+ * {project}/libraries/{lib}/skills/{skill}.nsl|.nslg
372
+ */
373
+ async pullLibrary(_client, customer, project, lib, hashes, _options) {
374
+ this.logger.verbose(` [newo_v2] Processing library: ${lib.idn} (${lib.skills.length} skills)`);
375
+ // Build V2 inline skill definitions for library YAML
376
+ const v2Skills = [];
377
+ for (const skill of lib.skills) {
378
+ const relPath = v2LibrarySkillRelativePath(project.idn, lib.idn, skill.idn, skill.runner_type);
379
+ v2Skills.push(buildV2InlineSkill(skill.idn, skill.title || '', skill.runner_type, skill.model?.model_idn || '', skill.model?.provider_idn || '', skill.parameters.map(p => ({
380
+ name: p.name,
381
+ default_value: p.default_value ?? '',
382
+ })), relPath));
383
+ }
384
+ // Sort skills same as flows
385
+ const { sortV2Skills, sortV2Parameters } = await import('../../../format/v2-yaml.js');
386
+ const sortedSkills = sortV2Skills(v2Skills).map(s => ({
387
+ ...s,
388
+ parameters: sortV2Parameters(s.parameters),
389
+ }));
390
+ // Write library YAML: libraries/{lib}/{lib}.yaml
391
+ const libDef = {
392
+ library: {
393
+ idn: lib.idn,
394
+ skills: sortedSkills,
395
+ }
396
+ };
397
+ const libYaml = yaml.dump(libDef, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false });
398
+ const libYamlPath = v2LibraryYamlPath(customer.idn, project.idn, lib.idn);
399
+ await writeFileSafe(libYamlPath, libYaml);
400
+ hashes[libYamlPath] = sha256(libYaml);
401
+ // Write skill scripts
402
+ for (const skill of lib.skills) {
403
+ const scriptContent = skill.prompt_script || '';
404
+ const scriptPath = v2LibrarySkillScriptPath(customer.idn, project.idn, lib.idn, skill.idn, skill.runner_type);
405
+ let shouldWrite = true;
406
+ if (await fs.pathExists(scriptPath)) {
407
+ const existing = await fs.readFile(scriptPath, 'utf8');
408
+ if (!isContentDifferent(existing, scriptContent)) {
409
+ shouldWrite = false;
410
+ hashes[scriptPath] = sha256(scriptContent);
411
+ }
412
+ }
413
+ if (shouldWrite) {
414
+ await writeFileSafe(scriptPath, scriptContent);
415
+ hashes[scriptPath] = sha256(scriptContent);
416
+ }
417
+ }
418
+ }
419
+ // ──────────────────────────────────────
420
+ // PUSH
421
+ // ──────────────────────────────────────
422
+ async push(customer, changes) {
423
+ const result = { created: 0, updated: 0, deleted: 0, errors: [] };
424
+ if (!changes) {
425
+ changes = await this.getChanges(customer);
426
+ }
427
+ if (changes.length === 0) {
428
+ return result;
429
+ }
430
+ const client = await this.apiClientFactory(customer, false);
431
+ const existingHashes = await loadHashes(customer.idn);
432
+ const newHashes = { ...existingHashes };
433
+ // Load project map
434
+ const mapFile = mapPath(customer.idn);
435
+ if (!(await fs.pathExists(mapFile))) {
436
+ result.errors.push('No project map found. Run pull first.');
437
+ return result;
438
+ }
439
+ const mapData = await fs.readJson(mapFile);
440
+ for (const change of changes) {
441
+ try {
442
+ if (change.operation === 'modified') {
443
+ // Detect if this is a library skill or flow skill by path
444
+ const isLibrary = change.path.includes('/libraries/');
445
+ const count = isLibrary
446
+ ? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
447
+ : await this.pushV2SkillUpdate(client, change, mapData, newHashes);
448
+ result.updated += count;
449
+ }
450
+ }
451
+ catch (error) {
452
+ result.errors.push(`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
453
+ }
454
+ }
455
+ await saveHashes(newHashes, customer.idn);
456
+ if (result.created > 0 || result.updated > 0) {
457
+ await this.publishAllFlows(client, mapData);
458
+ }
459
+ return result;
460
+ }
461
+ /**
462
+ * Push a V2 skill update
463
+ *
464
+ * V2 path: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skill}.nsl
465
+ */
466
+ async pushV2SkillUpdate(client, change, mapData, newHashes) {
467
+ // Parse V2 path to extract entity hierarchy
468
+ // Path: .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
469
+ const pathParts = change.path.split('/');
470
+ const skillFileName = pathParts[pathParts.length - 1] || '';
471
+ const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
472
+ // skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
473
+ const flowIdn = pathParts[pathParts.length - 3] || '';
474
+ const agentIdn = pathParts[pathParts.length - 5] || '';
475
+ const projectIdn = pathParts[pathParts.length - 7] || '';
476
+ // Look up skill in map
477
+ const projectData = mapData.projects[projectIdn];
478
+ const agentData = projectData?.agents[agentIdn];
479
+ const flowData = agentData?.flows[flowIdn];
480
+ const skillData = flowData?.skills[skillIdn];
481
+ if (!skillData) {
482
+ throw new Error(`Skill ${skillIdn} not found in project map (path: ${change.path})`);
483
+ }
484
+ // Read updated script content
485
+ const content = await fs.readFile(change.path, 'utf8');
486
+ // Update via V1 API
487
+ await updateSkill(client, {
488
+ id: skillData.id,
489
+ title: skillData.title,
490
+ idn: skillData.idn,
491
+ prompt_script: content,
492
+ runner_type: skillData.runner_type,
493
+ model: skillData.model,
494
+ parameters: skillData.parameters,
495
+ path: skillData.path
496
+ });
497
+ newHashes[change.path] = sha256(content);
498
+ this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
499
+ return 1;
500
+ }
501
+ /**
502
+ * Push a V2 library skill update
503
+ * Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
504
+ */
505
+ async pushV2LibrarySkillUpdate(client, change, mapData, newHashes) {
506
+ const pathParts = change.path.split('/');
507
+ const skillFileName = pathParts[pathParts.length - 1] || '';
508
+ const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
509
+ // skills/ -> lib/ -> libraries/ -> project/
510
+ const libIdn = pathParts[pathParts.length - 3] || '';
511
+ const projectIdn = pathParts[pathParts.length - 5] || '';
512
+ const projectData = mapData.projects[projectIdn];
513
+ const libData = projectData?.libraries?.[libIdn];
514
+ const skillData = libData?.skills[skillIdn];
515
+ if (!skillData || !libData) {
516
+ throw new Error(`Library skill ${skillIdn} not found in project map (path: ${change.path})`);
517
+ }
518
+ const content = await fs.readFile(change.path, 'utf8');
519
+ await updateLibrarySkill(client, libData.id, skillData.id, {
520
+ prompt_script: content,
521
+ });
522
+ newHashes[change.path] = sha256(content);
523
+ this.logger.info(`[newo_v2] Pushed library skill: ${libIdn}/${skillIdn}`);
524
+ return 1;
525
+ }
526
+ /**
527
+ * Publish all flows
528
+ */
529
+ async publishAllFlows(client, mapData) {
530
+ for (const projectData of Object.values(mapData.projects)) {
531
+ for (const agentData of Object.values(projectData.agents)) {
532
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
533
+ if (flowData.id) {
534
+ try {
535
+ await publishFlow(client, flowData.id, {
536
+ version: '1.0',
537
+ description: 'Published via NEWO CLI (newo_v2)',
538
+ type: 'public'
539
+ });
540
+ this.logger.verbose(`[newo_v2] Published flow: ${flowIdn}`);
541
+ }
542
+ catch {
543
+ this.logger.warn(`[newo_v2] Failed to publish flow ${flowIdn}`);
544
+ }
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ // ──────────────────────────────────────
551
+ // STATUS / CHANGES
552
+ // ──────────────────────────────────────
553
+ async getChanges(customer) {
554
+ const changes = [];
555
+ const mapFile = mapPath(customer.idn);
556
+ if (!(await fs.pathExists(mapFile))) {
557
+ return changes;
558
+ }
559
+ const hashes = await loadHashes(customer.idn);
560
+ const mapData = await fs.readJson(mapFile);
561
+ // Scan V2 directory structure for changed skill scripts
562
+ for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
563
+ // Flow skills
564
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
565
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
566
+ for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
567
+ const scriptPath = v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
568
+ if (await fs.pathExists(scriptPath)) {
569
+ const content = await fs.readFile(scriptPath, 'utf8');
570
+ const currentHash = sha256(content);
571
+ const storedHash = hashes[scriptPath];
572
+ if (storedHash !== currentHash) {
573
+ changes.push({
574
+ item: {},
575
+ operation: 'modified',
576
+ path: scriptPath
577
+ });
578
+ }
579
+ }
580
+ }
581
+ }
582
+ }
583
+ // Library skills
584
+ if (projectData.libraries) {
585
+ for (const [libIdn, libData] of Object.entries(projectData.libraries)) {
586
+ for (const [skillIdn, skillMeta] of Object.entries(libData.skills)) {
587
+ const scriptPath = v2LibrarySkillScriptPath(customer.idn, projectIdn, libIdn, skillIdn, skillMeta.runner_type);
588
+ if (await fs.pathExists(scriptPath)) {
589
+ const content = await fs.readFile(scriptPath, 'utf8');
590
+ const currentHash = sha256(content);
591
+ const storedHash = hashes[scriptPath];
592
+ if (storedHash !== currentHash) {
593
+ changes.push({
594
+ item: {},
595
+ operation: 'modified',
596
+ path: scriptPath
597
+ });
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ }
604
+ return changes;
605
+ }
606
+ async validate(customer, _items) {
607
+ const errors = [];
608
+ const mapFile = mapPath(customer.idn);
609
+ if (!(await fs.pathExists(mapFile))) {
610
+ errors.push({
611
+ field: 'projectMap',
612
+ message: 'No project map found. Run pull first.'
613
+ });
614
+ return { valid: false, errors };
615
+ }
616
+ const mapData = await fs.readJson(mapFile);
617
+ // Validate V2 skill files exist
618
+ for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
619
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
620
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
621
+ for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
622
+ const scriptPath = v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
623
+ if (!(await fs.pathExists(scriptPath))) {
624
+ errors.push({
625
+ field: `skill.${skillIdn}`,
626
+ message: `Script file not found: ${scriptPath}`,
627
+ path: scriptPath
628
+ });
629
+ }
630
+ }
631
+ }
632
+ }
633
+ }
634
+ return { valid: errors.length === 0, errors };
635
+ }
636
+ async getStatus(customer) {
637
+ const changes = await this.getChanges(customer);
638
+ return {
639
+ resourceType: this.resourceType,
640
+ displayName: this.displayName,
641
+ changedCount: changes.length,
642
+ changes: changes.map(c => ({
643
+ path: c.path,
644
+ operation: c.operation
645
+ }))
646
+ };
647
+ }
648
+ }
649
+ // ── V2 Attributes Formatting ──
650
+ /**
651
+ * Map V1 API value_type to V2 export format
652
+ * V1 API: "string", "bool", "AttributeValueTypes.string", etc.
653
+ * V2 export: "ValueType.STRING", "ValueType.BOOL", etc.
654
+ */
655
+ function toV2ValueType(apiValueType) {
656
+ // Already in V2 format
657
+ if (apiValueType.startsWith('ValueType.'))
658
+ return apiValueType;
659
+ // Strip AttributeValueTypes. prefix if present
660
+ const raw = apiValueType.replace(/^AttributeValueTypes\./, '');
661
+ const mapping = {
662
+ 'string': 'ValueType.STRING',
663
+ 'bool': 'ValueType.BOOL',
664
+ 'number': 'ValueType.NUMBER',
665
+ 'enum': 'ValueType.ENUM',
666
+ 'json': 'ValueType.JSON',
667
+ };
668
+ return mapping[raw.toLowerCase()] || `ValueType.${raw.toUpperCase()}`;
669
+ }
670
+ /**
671
+ * Format attributes as V2 YAML with:
672
+ * - Sorted by idn alphabetically
673
+ * - value_type as !enum "ValueType.X"
674
+ * - Proper quoting
675
+ */
676
+ function formatV2AttributesYaml(attrs) {
677
+ // Sort alphabetically by idn
678
+ const sorted = [...attrs].sort((a, b) => a.idn.localeCompare(b.idn));
679
+ const cleaned = sorted.map(a => ({
680
+ idn: a.idn,
681
+ value: a.value,
682
+ title: a.title || '',
683
+ description: a.description || '',
684
+ group: a.group || '',
685
+ is_hidden: a.is_hidden ?? false,
686
+ possible_values: a.possible_values || [],
687
+ value_type: new V2EnumValue(toV2ValueType(a.value_type || 'string')),
688
+ }));
689
+ const enumType = new yaml.Type('!enum', {
690
+ kind: 'scalar',
691
+ instanceOf: V2EnumValue,
692
+ resolve: () => true,
693
+ construct: (data) => new V2EnumValue(data),
694
+ represent: (data) => data instanceof V2EnumValue ? data.value : String(data),
695
+ });
696
+ const schema = yaml.DEFAULT_SCHEMA.extend([enumType]);
697
+ // Use lineWidth: -1 to prevent folding multiline strings (preserve |- literal block style)
698
+ const rawYaml = yaml.dump({ attributes: cleaned }, {
699
+ indent: 2,
700
+ quotingType: '"',
701
+ forceQuotes: false,
702
+ lineWidth: -1,
703
+ noRefs: true,
704
+ sortKeys: false,
705
+ schema,
706
+ });
707
+ // Fix !enum quoting: js-yaml outputs `!enum ValueType.STRING` but V2 ZIP uses `!enum "ValueType.STRING"`
708
+ const enumFixed = rawYaml.replace(/!enum (\S+)/g, '!enum "$1"');
709
+ // Patch long-line wrapping to match pyyaml style
710
+ return patchYamlToPyyaml(enumFixed);
711
+ }
712
+ /** Wrapper class for !enum YAML tag */
713
+ class V2EnumValue {
714
+ value;
715
+ constructor(value) {
716
+ this.value = value;
717
+ }
718
+ }
719
+ /**
720
+ * Factory function for creating V2ProjectSyncStrategy
721
+ */
722
+ export function createV2ProjectSyncStrategy(apiClientFactory, logger) {
723
+ return new V2ProjectSyncStrategy(apiClientFactory, logger);
724
+ }
725
+ //# sourceMappingURL=V2ProjectSyncStrategy.js.map