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.
- package/.env.example +5 -0
- package/CHANGELOG.md +21 -0
- package/dist/api.d.ts +18 -0
- package/dist/api.js +28 -0
- package/dist/cli/commands/export.d.ts +3 -0
- package/dist/cli/commands/export.js +62 -0
- package/dist/cli/commands/help.js +54 -42
- package/dist/cli/commands/pull.js +38 -14
- package/dist/cli/commands/push.js +32 -32
- package/dist/cli/commands/status.js +46 -7
- package/dist/cli-new/bootstrap.d.ts +7 -1
- package/dist/cli-new/bootstrap.js +11 -5
- package/dist/cli-new/di/tokens.d.ts +1 -0
- package/dist/cli-new/di/tokens.js +1 -0
- package/dist/cli.js +4 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +5 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.js +97 -8
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +80 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +725 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/format/detect.d.ts +14 -0
- package/dist/format/detect.js +105 -0
- package/dist/format/extensions.d.ts +26 -0
- package/dist/format/extensions.js +45 -0
- package/dist/format/index.d.ts +11 -0
- package/dist/format/index.js +11 -0
- package/dist/format/paths-v2.d.ts +31 -0
- package/dist/format/paths-v2.js +104 -0
- package/dist/format/types.d.ts +28 -0
- package/dist/format/types.js +21 -0
- package/dist/format/v2-yaml.d.ts +143 -0
- package/dist/format/v2-yaml.js +222 -0
- package/dist/format/yaml-patch.d.ts +14 -0
- package/dist/format/yaml-patch.js +184 -0
- package/dist/fsutil.d.ts +10 -0
- package/dist/fsutil.js +25 -0
- package/dist/sync/attributes.js +3 -3
- package/dist/sync/skill-files.js +2 -2
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/api.ts +64 -0
- package/src/cli/commands/export.ts +78 -0
- package/src/cli/commands/help.ts +54 -42
- package/src/cli/commands/pull.ts +46 -15
- package/src/cli/commands/push.ts +38 -31
- package/src/cli/commands/status.ts +59 -9
- package/src/cli-new/bootstrap.ts +19 -7
- package/src/cli-new/di/tokens.ts +1 -0
- package/src/cli.ts +5 -0
- package/src/domain/strategies/sync/ProjectSyncStrategy.ts +122 -8
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +1007 -0
- package/src/env.ts +2 -0
- package/src/format/detect.ts +123 -0
- package/src/format/extensions.ts +61 -0
- package/src/format/index.ts +66 -0
- package/src/format/paths-v2.ts +207 -0
- package/src/format/types.ts +40 -0
- package/src/format/v2-yaml.ts +345 -0
- package/src/format/yaml-patch.ts +208 -0
- package/src/fsutil.ts +37 -0
- package/src/sync/attributes.ts +3 -3
- package/src/sync/skill-files.ts +2 -2
- 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');
|
package/src/sync/attributes.ts
CHANGED
|
@@ -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}`);
|
package/src/sync/skill-files.ts
CHANGED
|
@@ -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 {
|