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,345 @@
1
+ /**
2
+ * V2 YAML parsers and generators
3
+ *
4
+ * Handles reading/writing the newo_v2 format YAML files:
5
+ * - Flow YAML: {FlowIdn}.yaml (inline skill definitions, events, state_fields)
6
+ * - Project YAML: {project_idn}.yaml
7
+ * - Agent YAML: agent.yaml
8
+ * - Library YAML: {library_idn}.yaml
9
+ */
10
+ import fs from 'fs-extra';
11
+ import yaml from 'js-yaml';
12
+ import { patchYamlToPyyaml } from './yaml-patch.js';
13
+
14
+ // ── V2 Types ──
15
+
16
+ export interface V2InlineSkill {
17
+ title: string;
18
+ idn: string;
19
+ prompt_script: string;
20
+ runner_type: string;
21
+ model: {
22
+ model_idn: string;
23
+ provider_idn: string;
24
+ };
25
+ parameters: Array<{ name: string; default_value: string }>;
26
+ }
27
+
28
+ export interface V2FlowEvent {
29
+ idn: string;
30
+ skill_selector: string;
31
+ skill_idn: string | null;
32
+ state_idn: string | null;
33
+ integration_idn: string | null;
34
+ connector_idn: string | null;
35
+ interrupt_mode: string;
36
+ }
37
+
38
+ export interface V2StateField {
39
+ title: string;
40
+ idn: string;
41
+ default_value: string;
42
+ scope: string;
43
+ }
44
+
45
+ export interface V2FlowDefinition {
46
+ title: string;
47
+ idn: string;
48
+ description: string | null;
49
+ agent_id: string | null;
50
+ skills: V2InlineSkill[];
51
+ events: V2FlowEvent[];
52
+ state_fields: V2StateField[];
53
+ default_runner_type: string;
54
+ default_provider_idn: string;
55
+ default_model_idn: string;
56
+ publication_type: string | null;
57
+ }
58
+
59
+ export interface V2ProjectMeta {
60
+ idn: string;
61
+ name: string;
62
+ version: string;
63
+ description: string;
64
+ is_auto_update_enabled: boolean;
65
+ registry: string;
66
+ registry_item_idn: string;
67
+ }
68
+
69
+ export interface V2AgentMeta {
70
+ idn: string;
71
+ title: string | null;
72
+ description: string | null;
73
+ }
74
+
75
+ export interface V2LibraryDefinition {
76
+ title: string;
77
+ idn: string;
78
+ description: string | null;
79
+ skills: V2InlineSkill[];
80
+ }
81
+
82
+ // ── Custom YAML tag for !enum values ──
83
+
84
+ const enumTag = new yaml.Type('!enum', {
85
+ kind: 'scalar',
86
+ resolve: () => true,
87
+ construct: (data: string) => data,
88
+ represent: (data: unknown) => String(data),
89
+ });
90
+
91
+ const V2_YAML_SCHEMA = yaml.DEFAULT_SCHEMA.extend([enumTag]);
92
+
93
+ // ── Shared YAML dump options ──
94
+
95
+ const YAML_DUMP_OPTIONS: yaml.DumpOptions = {
96
+ indent: 2,
97
+ quotingType: '"',
98
+ forceQuotes: false,
99
+ lineWidth: -1,
100
+ noRefs: true,
101
+ sortKeys: false,
102
+ flowLevel: -1,
103
+ schema: V2_YAML_SCHEMA,
104
+ };
105
+
106
+ // ── Skill sorting: CamelCase first, then _prefixed, then snake_case ──
107
+
108
+ function skillSortKey(idn: string): string {
109
+ if (idn.startsWith('_')) {
110
+ return `1_${idn}`; // _prefixed second
111
+ }
112
+ if (idn[0] && idn[0] === idn[0].toUpperCase()) {
113
+ return `0_${idn}`; // CamelCase first
114
+ }
115
+ return `2_${idn}`; // snake_case last
116
+ }
117
+
118
+ /**
119
+ * Sort skills in V2 export order (case-sensitive ASCII sort within groups):
120
+ * 1. CamelCase (public) - case-sensitive alphabetically
121
+ * 2. _prefixed (private) - case-sensitive alphabetically
122
+ * 3. snake_case - case-sensitive alphabetically
123
+ */
124
+ export function sortV2Skills<T extends { idn: string }>(skills: T[]): T[] {
125
+ return [...skills].sort((a, b) => {
126
+ const ka = skillSortKey(a.idn);
127
+ const kb = skillSortKey(b.idn);
128
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Sort parameters alphabetically by name (case-sensitive, V2 export order)
134
+ */
135
+ export function sortV2Parameters<T extends { name: string }>(params: T[]): T[] {
136
+ return [...params].sort((a, b) => {
137
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
138
+ });
139
+ }
140
+
141
+ // ── Flow YAML ──
142
+
143
+ export async function parseV2FlowYaml(filePath: string): Promise<V2FlowDefinition> {
144
+ const content = await fs.readFile(filePath, 'utf8');
145
+ return yaml.load(content, { schema: V2_YAML_SCHEMA }) as V2FlowDefinition;
146
+ }
147
+
148
+ /**
149
+ * Generate V2 flow YAML content from API data
150
+ *
151
+ * Produces the exact format found in the reference V2 export:
152
+ * title, idn, description, agent_id, skills[], events[], state_fields[],
153
+ * default_runner_type, default_provider_idn, default_model_idn, publication_type
154
+ */
155
+ export function generateV2FlowYaml(
156
+ flowIdn: string,
157
+ flowTitle: string,
158
+ flowDescription: string | null,
159
+ defaultRunnerType: string,
160
+ defaultProviderIdn: string,
161
+ defaultModelIdn: string,
162
+ skills: V2InlineSkill[],
163
+ events: V2FlowEvent[],
164
+ stateFields: V2StateField[]
165
+ ): string {
166
+ // Sort skills in V2 order and sort parameters within each skill
167
+ const sortedSkills = sortV2Skills(skills).map(s => ({
168
+ ...s,
169
+ parameters: sortV2Parameters(s.parameters),
170
+ }));
171
+
172
+ // Sort events by idn, then skill_idn, then integration_idn, then connector_idn
173
+ const sortedEvents = [...events].sort((a, b) => {
174
+ if (a.idn !== b.idn) return a.idn < b.idn ? -1 : 1;
175
+ const as = a.skill_idn || '';
176
+ const bs = b.skill_idn || '';
177
+ if (as !== bs) return as < bs ? -1 : 1;
178
+ const ai = a.integration_idn || '';
179
+ const bi = b.integration_idn || '';
180
+ if (ai !== bi) return ai < bi ? -1 : 1;
181
+ const ac = a.connector_idn || '';
182
+ const bc = b.connector_idn || '';
183
+ return ac < bc ? -1 : ac > bc ? 1 : 0;
184
+ });
185
+ // Sort state_fields alphabetically by idn
186
+ const sortedStates = [...stateFields].sort((a, b) => a.idn < b.idn ? -1 : a.idn > b.idn ? 1 : 0);
187
+
188
+ const flowDef: V2FlowDefinition = {
189
+ title: flowTitle,
190
+ idn: flowIdn,
191
+ description: flowDescription ?? null,
192
+ agent_id: null,
193
+ skills: sortedSkills,
194
+ events: sortedEvents,
195
+ state_fields: sortedStates,
196
+ default_runner_type: defaultRunnerType,
197
+ default_provider_idn: defaultProviderIdn,
198
+ default_model_idn: defaultModelIdn,
199
+ publication_type: null,
200
+ };
201
+
202
+ // Flow YAML uses lineWidth: -1 (no wrapping) to keep prompt_script paths on one line
203
+ // Then patch to convert double-quoted JSON values to single-quoted
204
+ return patchYamlToPyyaml(yaml.dump(flowDef, { ...YAML_DUMP_OPTIONS, lineWidth: -1 }));
205
+ }
206
+
207
+ // ── Project YAML ──
208
+
209
+ export async function parseV2ProjectYaml(filePath: string): Promise<V2ProjectMeta> {
210
+ const content = await fs.readFile(filePath, 'utf8');
211
+ const parsed = yaml.load(content, { schema: V2_YAML_SCHEMA }) as { project: V2ProjectMeta };
212
+ return parsed.project;
213
+ }
214
+
215
+ /**
216
+ * Generate V2 project YAML
217
+ *
218
+ * Format:
219
+ * project:
220
+ * idn: naf
221
+ * name: naf
222
+ * version: 4.1.0
223
+ * description: ""
224
+ * is_auto_update_enabled: true
225
+ * registry: production
226
+ * registry_item_idn: naf
227
+ */
228
+ export function generateV2ProjectYaml(meta: V2ProjectMeta): string {
229
+ return yaml.dump({ project: meta }, YAML_DUMP_OPTIONS);
230
+ }
231
+
232
+ // ── Agent YAML ──
233
+
234
+ export async function parseV2AgentYaml(filePath: string): Promise<V2AgentMeta> {
235
+ const content = await fs.readFile(filePath, 'utf8');
236
+ const parsed = yaml.load(content, { schema: V2_YAML_SCHEMA }) as { agent: V2AgentMeta };
237
+ return parsed.agent;
238
+ }
239
+
240
+ /**
241
+ * Generate V2 agent YAML
242
+ *
243
+ * Format:
244
+ * agent:
245
+ * idn: TaskManager
246
+ * title: TaskManager
247
+ * description: null
248
+ *
249
+ * V2 export preserves description exactly as provided (null stays null, "" stays "")
250
+ */
251
+ export function generateV2AgentYaml(meta: V2AgentMeta): string {
252
+ return patchYamlToPyyaml(yaml.dump({ agent: meta }, YAML_DUMP_OPTIONS));
253
+ }
254
+
255
+ // ── Library YAML ──
256
+
257
+ export async function parseV2LibraryYaml(filePath: string): Promise<V2LibraryDefinition> {
258
+ const content = await fs.readFile(filePath, 'utf8');
259
+ return yaml.load(content, { schema: V2_YAML_SCHEMA }) as V2LibraryDefinition;
260
+ }
261
+
262
+ /**
263
+ * Generate V2 library YAML
264
+ *
265
+ * Format:
266
+ * title: Test Library
267
+ * idn: testLib
268
+ * description: Shared utility library
269
+ * skills:
270
+ * - idn: utilSkill
271
+ * ...
272
+ */
273
+ export function generateV2LibraryYaml(lib: V2LibraryDefinition): string {
274
+ return yaml.dump(lib, YAML_DUMP_OPTIONS);
275
+ }
276
+
277
+ // ── Conversion helpers ──
278
+
279
+ /**
280
+ * Build a V2InlineSkill entry from API skill data
281
+ */
282
+ export function buildV2InlineSkill(
283
+ skillIdn: string,
284
+ skillTitle: string,
285
+ runnerType: string,
286
+ modelIdn: string,
287
+ providerIdn: string,
288
+ parameters: Array<{ name: string; default_value: string }>,
289
+ promptScriptRelPath: string
290
+ ): V2InlineSkill {
291
+ return {
292
+ title: skillTitle || '',
293
+ idn: skillIdn,
294
+ prompt_script: promptScriptRelPath,
295
+ runner_type: runnerType,
296
+ model: {
297
+ model_idn: modelIdn,
298
+ provider_idn: providerIdn,
299
+ },
300
+ parameters: parameters.map(p => ({
301
+ name: p.name,
302
+ default_value: p.default_value ?? '',
303
+ })),
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Build a V2FlowEvent entry from API event data
309
+ */
310
+ export function buildV2FlowEvent(
311
+ eventIdn: string,
312
+ skillSelector: string,
313
+ skillIdn: string | null,
314
+ stateIdn: string | null,
315
+ integrationIdn: string | null,
316
+ connectorIdn: string | null,
317
+ interruptMode: string
318
+ ): V2FlowEvent {
319
+ return {
320
+ idn: eventIdn,
321
+ skill_selector: skillSelector,
322
+ skill_idn: skillIdn || null,
323
+ state_idn: stateIdn || null,
324
+ integration_idn: integrationIdn || null,
325
+ connector_idn: connectorIdn || null,
326
+ interrupt_mode: interruptMode,
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Build a V2StateField entry from API state data
332
+ */
333
+ export function buildV2StateField(
334
+ stateIdn: string,
335
+ stateTitle: string,
336
+ defaultValue: string,
337
+ scope: string
338
+ ): V2StateField {
339
+ return {
340
+ title: stateTitle || '',
341
+ idn: stateIdn,
342
+ default_value: defaultValue ?? '',
343
+ scope,
344
+ };
345
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * YAML Post-Processor - Patches js-yaml output to match pyyaml's formatting
3
+ *
4
+ * Replicates pyyaml's Emitter wrapping behavior:
5
+ * - Double-quoted: breaks with `\` when column + pending > best_width (80) at space
6
+ * - Plain scalar: breaks at space when column > best_width
7
+ * - Continuation indent: parent indent + best_indent (usually +2)
8
+ * - Single-quote preference for strings with brackets
9
+ */
10
+
11
+ const BEST_WIDTH = 80;
12
+
13
+ /**
14
+ * Patch full YAML document output to match pyyaml formatting
15
+ */
16
+ export function patchYamlToPyyaml(yamlText: string): string {
17
+ const lines = yamlText.split('\n');
18
+ const result: string[] = [];
19
+ let inBlockScalar = false;
20
+ let blockIndent = 0;
21
+
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const line = lines[i]!;
24
+
25
+ // Track block scalar context (|- or >- or | or >)
26
+ if (inBlockScalar) {
27
+ const currentIndent = line.length - line.trimStart().length;
28
+ if (line.trim() === '' || currentIndent > blockIndent) {
29
+ result.push(line);
30
+ continue;
31
+ }
32
+ inBlockScalar = false;
33
+ }
34
+
35
+ // Detect start of block scalar
36
+ if (/^\s*[\w-]+:\s+[|>]-?\s*$/.test(line)) {
37
+ const keyIndent = line.length - line.trimStart().length;
38
+ inBlockScalar = true;
39
+ blockIndent = keyIndent;
40
+ result.push(line);
41
+ continue;
42
+ }
43
+
44
+ result.push(...patchLine(line));
45
+ }
46
+
47
+ return result.join('\n');
48
+ }
49
+
50
+ // Keys that must NEVER be wrapped
51
+ const NO_WRAP_KEYS = new Set([
52
+ 'prompt_script', 'idn', 'runner_type',
53
+ 'model_idn', 'provider_idn', 'skill_idn', 'state_idn',
54
+ 'integration_idn', 'connector_idn', 'interrupt_mode',
55
+ 'skill_selector', 'name', 'scope', 'agent_id',
56
+ 'default_runner_type', 'default_provider_idn', 'default_model_idn',
57
+ 'publication_type', 'is_hidden', 'is_auto_update_enabled',
58
+ 'group', 'registry', 'registry_item_idn', 'version',
59
+ ]);
60
+
61
+ function patchLine(line: string): string[] {
62
+ if (line.trim() === '' || line.trim().startsWith('#')) {
63
+ return [line];
64
+ }
65
+
66
+ const kvMatch = line.match(/^(\s*(?:-\s+)?)([\w-]+):\s+(.+)$/);
67
+ if (!kvMatch) {
68
+ return [line];
69
+ }
70
+
71
+ const prefix = kvMatch[1]!;
72
+ const key = kvMatch[2]!;
73
+ const value = kvMatch[3]!;
74
+
75
+ // Single-quote fix for JSON-like values (before anything else)
76
+ const sqFix = tryConvertToSingleQuote(value);
77
+ const effectiveValue = sqFix ?? value;
78
+ const effectiveLine = sqFix !== null ? `${prefix}${key}: ${sqFix}` : line;
79
+
80
+ if (NO_WRAP_KEYS.has(key)) {
81
+ return [effectiveLine];
82
+ }
83
+
84
+ // Only wrap if line exceeds BEST_WIDTH
85
+ if (effectiveLine.length <= BEST_WIDTH) {
86
+ return [effectiveLine];
87
+ }
88
+
89
+ const keyPart = `${prefix}${key}: `;
90
+ // pyyaml continuation indent = current mapping indent + best_indent
91
+ // For " description: ..." indent is 2, continuation = 2 + 2 = 4 spaces
92
+ const keyIndent = prefix.replace(/-\s+$/, '').length;
93
+ const contIndent = ' '.repeat(keyIndent + 2);
94
+
95
+ if (effectiveValue.startsWith('"') && effectiveValue.endsWith('"')) {
96
+ return wrapDoubleQuoted(keyPart, effectiveValue, contIndent);
97
+ }
98
+
99
+ if (effectiveValue.startsWith("'") && effectiveValue.endsWith("'")) {
100
+ return [effectiveLine]; // Single-quoted: don't wrap
101
+ }
102
+
103
+ return wrapPlainScalar(keyPart, effectiveValue, contIndent);
104
+ }
105
+
106
+ /**
107
+ * Try to convert double-quoted string with escaped chars to single-quoted
108
+ */
109
+ function tryConvertToSingleQuote(value: string): string | null {
110
+ if (!value.startsWith('"') || !value.endsWith('"')) return null;
111
+
112
+ const inner = value.slice(1, -1);
113
+ if (!inner.includes('\\"')) return null;
114
+ if (!inner.includes('[') && !inner.includes('{')) return null;
115
+
116
+ const unescaped = inner.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
117
+
118
+ if (unescaped.includes("'")) {
119
+ return `'${unescaped.replace(/'/g, "''")}'`;
120
+ }
121
+ return `'${unescaped}'`;
122
+ }
123
+
124
+ /**
125
+ * Wrap double-quoted scalar matching pyyaml's write_double_quoted algorithm:
126
+ * - Track column from 0
127
+ * - At each space, check if column + pending > best_width
128
+ * - If yes, emit text + `\`, newline, indent, `\ ` (escaped space for continuation)
129
+ */
130
+ function wrapDoubleQuoted(keyPart: string, quotedValue: string, contIndent: string): string[] {
131
+ const inner = quotedValue.slice(1, -1);
132
+ const result: string[] = [];
133
+
134
+ let column = keyPart.length + 1; // keyPart + opening "
135
+ let lineStart = 0;
136
+ let lastSpace = -1;
137
+
138
+ for (let i = 0; i < inner.length; i++) {
139
+ column++;
140
+ if (inner[i] === ' ') {
141
+ lastSpace = i;
142
+ }
143
+
144
+ // pyyaml condition: column + remaining_in_word > best_width, at a space or start >= end
145
+ if (column > BEST_WIDTH && lastSpace > lineStart) {
146
+ // Break at lastSpace
147
+ const chunk = inner.slice(lineStart, lastSpace);
148
+ if (result.length === 0) {
149
+ result.push(`${keyPart}"${chunk}\\`);
150
+ } else {
151
+ result.push(`${contIndent}\\ ${chunk}\\`);
152
+ }
153
+ lineStart = lastSpace + 1; // skip the space
154
+ column = contIndent.length + 2 + (i - lastSpace); // contIndent + "\ " + chars after space
155
+ lastSpace = -1;
156
+ }
157
+ }
158
+
159
+ // Remaining text
160
+ const remaining = inner.slice(lineStart);
161
+ if (result.length === 0) {
162
+ result.push(`${keyPart}"${remaining}"`);
163
+ } else {
164
+ result.push(`${contIndent}\\ ${remaining}"`);
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Wrap plain scalar matching pyyaml's write_plain algorithm:
172
+ * - At each space, if column > best_width, break
173
+ * - Continuation is just indented text (no backslash)
174
+ */
175
+ function wrapPlainScalar(keyPart: string, value: string, contIndent: string): string[] {
176
+ const result: string[] = [];
177
+ let column = keyPart.length;
178
+ let lineStart = 0;
179
+ let lastSpace = -1;
180
+
181
+ for (let i = 0; i < value.length; i++) {
182
+ column++;
183
+ if (value[i] === ' ') {
184
+ // pyyaml breaks at single space when column > best_width
185
+ if (column > BEST_WIDTH && lastSpace >= lineStart) {
186
+ const chunk = value.slice(lineStart, i);
187
+ if (result.length === 0) {
188
+ result.push(`${keyPart}${chunk}`);
189
+ } else {
190
+ result.push(`${contIndent}${chunk}`);
191
+ }
192
+ lineStart = i + 1; // skip the space
193
+ column = contIndent.length;
194
+ }
195
+ lastSpace = i;
196
+ }
197
+ }
198
+
199
+ // Remaining
200
+ const remaining = value.slice(lineStart);
201
+ if (result.length === 0) {
202
+ result.push(`${keyPart}${remaining}`);
203
+ } else {
204
+ result.push(`${contIndent}${remaining}`);
205
+ }
206
+
207
+ return result;
208
+ }
package/src/fsutil.ts CHANGED
@@ -31,6 +31,15 @@ export async function ensureState(customerIdn: string): Promise<void> {
31
31
  await fs.ensureDir(customerProjectsDir(customerIdn));
32
32
  }
33
33
 
34
+ /**
35
+ * Ensure only the .newo/ state directory exists (no V1 projects/ dir)
36
+ * Used by newo_v2 format to avoid creating V1 artifacts
37
+ */
38
+ export async function ensureStateOnly(customerIdn: string): Promise<void> {
39
+ await fs.ensureDir(STATE_DIR);
40
+ await fs.ensureDir(customerStateDir(customerIdn));
41
+ }
42
+
34
43
  export function projectDir(customerIdn: string, projectIdn: string): string {
35
44
  return path.posix.join(customerProjectsDir(customerIdn), projectIdn);
36
45
  }
@@ -110,6 +119,34 @@ export function skillMetadataPath(
110
119
  return path.posix.join(skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn), 'metadata.yaml');
111
120
  }
112
121
 
122
+ // Library paths (cli_v1 format)
123
+ export function libraryDir(customerIdn: string, projectIdn: string, libraryIdn: string): string {
124
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'libraries', libraryIdn);
125
+ }
126
+
127
+ export function libraryMetadataPath(customerIdn: string, projectIdn: string, libraryIdn: string): string {
128
+ return path.posix.join(libraryDir(customerIdn, projectIdn, libraryIdn), 'metadata.yaml');
129
+ }
130
+
131
+ export function librarySkillFolderPath(
132
+ customerIdn: string, projectIdn: string, libraryIdn: string, skillIdn: string
133
+ ): string {
134
+ return path.posix.join(libraryDir(customerIdn, projectIdn, libraryIdn), skillIdn);
135
+ }
136
+
137
+ export function librarySkillScriptPath(
138
+ customerIdn: string, projectIdn: string, libraryIdn: string, skillIdn: string, runnerType: RunnerType = 'guidance'
139
+ ): string {
140
+ const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
141
+ return path.posix.join(librarySkillFolderPath(customerIdn, projectIdn, libraryIdn, skillIdn), `${skillIdn}${extension}`);
142
+ }
143
+
144
+ export function librarySkillMetadataPath(
145
+ customerIdn: string, projectIdn: string, libraryIdn: string, skillIdn: string
146
+ ): string {
147
+ return path.posix.join(librarySkillFolderPath(customerIdn, projectIdn, libraryIdn, skillIdn), 'metadata.yaml');
148
+ }
149
+
113
150
  // Legacy metadata path - keep for backwards compatibility
114
151
  export function metadataPath(customerIdn: string, projectIdn: string): string {
115
152
  return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.json');
@@ -298,9 +298,9 @@ export async function pushProjectAttributes(
298
298
  // Value type is already parsed (we removed !enum tags above)
299
299
  const valueType = localAttr.value_type;
300
300
 
301
- // Check if value changed
302
- const localValue = String(localAttr.value || '');
303
- const remoteValue = String(remoteAttr.value || '');
301
+ // Check if value changed (use ?? to preserve 0, false, empty string)
302
+ const localValue = String(localAttr.value ?? '');
303
+ const remoteValue = String(remoteAttr.value ?? '');
304
304
 
305
305
  if (localValue !== remoteValue) {
306
306
  if (verbose) console.log(` 🔄 Updating project attribute: ${localAttr.idn}`);
@@ -69,8 +69,8 @@ export async function findSkillScriptFiles(skillFolderPath: string): Promise<Ski
69
69
  if (stats.isFile()) {
70
70
  const ext = path.extname(fileName).toLowerCase();
71
71
 
72
- // Check for script file extensions
73
- if (['.jinja', '.guidance', '.nsl'].includes(ext)) {
72
+ // Check for script file extensions (all formats)
73
+ if (['.jinja', '.guidance', '.nsl', '.nslg'].includes(ext)) {
74
74
  const content = await fs.readFile(filePath, 'utf8');
75
75
  scriptFiles.push({
76
76
  filePath,
package/src/types.ts CHANGED
@@ -182,10 +182,16 @@ export interface AgentData {
182
182
  flows: Record<string, FlowData>;
183
183
  }
184
184
 
185
+ export interface LibraryData {
186
+ id: string;
187
+ skills: Record<string, SkillMetadata>;
188
+ }
189
+
185
190
  export interface ProjectData {
186
191
  projectId: string;
187
192
  projectIdn: string;
188
193
  agents: Record<string, AgentData>;
194
+ libraries?: Record<string, LibraryData> | undefined;
189
195
  }
190
196
 
191
197
  export interface ProjectMap {