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,222 @@
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
+ // ── Custom YAML tag for !enum values ──
14
+ const enumTag = new yaml.Type('!enum', {
15
+ kind: 'scalar',
16
+ resolve: () => true,
17
+ construct: (data) => data,
18
+ represent: (data) => String(data),
19
+ });
20
+ const V2_YAML_SCHEMA = yaml.DEFAULT_SCHEMA.extend([enumTag]);
21
+ // ── Shared YAML dump options ──
22
+ const YAML_DUMP_OPTIONS = {
23
+ indent: 2,
24
+ quotingType: '"',
25
+ forceQuotes: false,
26
+ lineWidth: -1,
27
+ noRefs: true,
28
+ sortKeys: false,
29
+ flowLevel: -1,
30
+ schema: V2_YAML_SCHEMA,
31
+ };
32
+ // ── Skill sorting: CamelCase first, then _prefixed, then snake_case ──
33
+ function skillSortKey(idn) {
34
+ if (idn.startsWith('_')) {
35
+ return `1_${idn}`; // _prefixed second
36
+ }
37
+ if (idn[0] && idn[0] === idn[0].toUpperCase()) {
38
+ return `0_${idn}`; // CamelCase first
39
+ }
40
+ return `2_${idn}`; // snake_case last
41
+ }
42
+ /**
43
+ * Sort skills in V2 export order (case-sensitive ASCII sort within groups):
44
+ * 1. CamelCase (public) - case-sensitive alphabetically
45
+ * 2. _prefixed (private) - case-sensitive alphabetically
46
+ * 3. snake_case - case-sensitive alphabetically
47
+ */
48
+ export function sortV2Skills(skills) {
49
+ return [...skills].sort((a, b) => {
50
+ const ka = skillSortKey(a.idn);
51
+ const kb = skillSortKey(b.idn);
52
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
53
+ });
54
+ }
55
+ /**
56
+ * Sort parameters alphabetically by name (case-sensitive, V2 export order)
57
+ */
58
+ export function sortV2Parameters(params) {
59
+ return [...params].sort((a, b) => {
60
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
61
+ });
62
+ }
63
+ // ── Flow YAML ──
64
+ export async function parseV2FlowYaml(filePath) {
65
+ const content = await fs.readFile(filePath, 'utf8');
66
+ return yaml.load(content, { schema: V2_YAML_SCHEMA });
67
+ }
68
+ /**
69
+ * Generate V2 flow YAML content from API data
70
+ *
71
+ * Produces the exact format found in the reference V2 export:
72
+ * title, idn, description, agent_id, skills[], events[], state_fields[],
73
+ * default_runner_type, default_provider_idn, default_model_idn, publication_type
74
+ */
75
+ export function generateV2FlowYaml(flowIdn, flowTitle, flowDescription, defaultRunnerType, defaultProviderIdn, defaultModelIdn, skills, events, stateFields) {
76
+ // Sort skills in V2 order and sort parameters within each skill
77
+ const sortedSkills = sortV2Skills(skills).map(s => ({
78
+ ...s,
79
+ parameters: sortV2Parameters(s.parameters),
80
+ }));
81
+ // Sort events by idn, then skill_idn, then integration_idn, then connector_idn
82
+ const sortedEvents = [...events].sort((a, b) => {
83
+ if (a.idn !== b.idn)
84
+ return a.idn < b.idn ? -1 : 1;
85
+ const as = a.skill_idn || '';
86
+ const bs = b.skill_idn || '';
87
+ if (as !== bs)
88
+ return as < bs ? -1 : 1;
89
+ const ai = a.integration_idn || '';
90
+ const bi = b.integration_idn || '';
91
+ if (ai !== bi)
92
+ return ai < bi ? -1 : 1;
93
+ const ac = a.connector_idn || '';
94
+ const bc = b.connector_idn || '';
95
+ return ac < bc ? -1 : ac > bc ? 1 : 0;
96
+ });
97
+ // Sort state_fields alphabetically by idn
98
+ const sortedStates = [...stateFields].sort((a, b) => a.idn < b.idn ? -1 : a.idn > b.idn ? 1 : 0);
99
+ const flowDef = {
100
+ title: flowTitle,
101
+ idn: flowIdn,
102
+ description: flowDescription ?? null,
103
+ agent_id: null,
104
+ skills: sortedSkills,
105
+ events: sortedEvents,
106
+ state_fields: sortedStates,
107
+ default_runner_type: defaultRunnerType,
108
+ default_provider_idn: defaultProviderIdn,
109
+ default_model_idn: defaultModelIdn,
110
+ publication_type: null,
111
+ };
112
+ // Flow YAML uses lineWidth: -1 (no wrapping) to keep prompt_script paths on one line
113
+ // Then patch to convert double-quoted JSON values to single-quoted
114
+ return patchYamlToPyyaml(yaml.dump(flowDef, { ...YAML_DUMP_OPTIONS, lineWidth: -1 }));
115
+ }
116
+ // ── Project YAML ──
117
+ export async function parseV2ProjectYaml(filePath) {
118
+ const content = await fs.readFile(filePath, 'utf8');
119
+ const parsed = yaml.load(content, { schema: V2_YAML_SCHEMA });
120
+ return parsed.project;
121
+ }
122
+ /**
123
+ * Generate V2 project YAML
124
+ *
125
+ * Format:
126
+ * project:
127
+ * idn: naf
128
+ * name: naf
129
+ * version: 4.1.0
130
+ * description: ""
131
+ * is_auto_update_enabled: true
132
+ * registry: production
133
+ * registry_item_idn: naf
134
+ */
135
+ export function generateV2ProjectYaml(meta) {
136
+ return yaml.dump({ project: meta }, YAML_DUMP_OPTIONS);
137
+ }
138
+ // ── Agent YAML ──
139
+ export async function parseV2AgentYaml(filePath) {
140
+ const content = await fs.readFile(filePath, 'utf8');
141
+ const parsed = yaml.load(content, { schema: V2_YAML_SCHEMA });
142
+ return parsed.agent;
143
+ }
144
+ /**
145
+ * Generate V2 agent YAML
146
+ *
147
+ * Format:
148
+ * agent:
149
+ * idn: TaskManager
150
+ * title: TaskManager
151
+ * description: null
152
+ *
153
+ * V2 export preserves description exactly as provided (null stays null, "" stays "")
154
+ */
155
+ export function generateV2AgentYaml(meta) {
156
+ return patchYamlToPyyaml(yaml.dump({ agent: meta }, YAML_DUMP_OPTIONS));
157
+ }
158
+ // ── Library YAML ──
159
+ export async function parseV2LibraryYaml(filePath) {
160
+ const content = await fs.readFile(filePath, 'utf8');
161
+ return yaml.load(content, { schema: V2_YAML_SCHEMA });
162
+ }
163
+ /**
164
+ * Generate V2 library YAML
165
+ *
166
+ * Format:
167
+ * title: Test Library
168
+ * idn: testLib
169
+ * description: Shared utility library
170
+ * skills:
171
+ * - idn: utilSkill
172
+ * ...
173
+ */
174
+ export function generateV2LibraryYaml(lib) {
175
+ return yaml.dump(lib, YAML_DUMP_OPTIONS);
176
+ }
177
+ // ── Conversion helpers ──
178
+ /**
179
+ * Build a V2InlineSkill entry from API skill data
180
+ */
181
+ export function buildV2InlineSkill(skillIdn, skillTitle, runnerType, modelIdn, providerIdn, parameters, promptScriptRelPath) {
182
+ return {
183
+ title: skillTitle || '',
184
+ idn: skillIdn,
185
+ prompt_script: promptScriptRelPath,
186
+ runner_type: runnerType,
187
+ model: {
188
+ model_idn: modelIdn,
189
+ provider_idn: providerIdn,
190
+ },
191
+ parameters: parameters.map(p => ({
192
+ name: p.name,
193
+ default_value: p.default_value ?? '',
194
+ })),
195
+ };
196
+ }
197
+ /**
198
+ * Build a V2FlowEvent entry from API event data
199
+ */
200
+ export function buildV2FlowEvent(eventIdn, skillSelector, skillIdn, stateIdn, integrationIdn, connectorIdn, interruptMode) {
201
+ return {
202
+ idn: eventIdn,
203
+ skill_selector: skillSelector,
204
+ skill_idn: skillIdn || null,
205
+ state_idn: stateIdn || null,
206
+ integration_idn: integrationIdn || null,
207
+ connector_idn: connectorIdn || null,
208
+ interrupt_mode: interruptMode,
209
+ };
210
+ }
211
+ /**
212
+ * Build a V2StateField entry from API state data
213
+ */
214
+ export function buildV2StateField(stateIdn, stateTitle, defaultValue, scope) {
215
+ return {
216
+ title: stateTitle || '',
217
+ idn: stateIdn,
218
+ default_value: defaultValue ?? '',
219
+ scope,
220
+ };
221
+ }
222
+ //# sourceMappingURL=v2-yaml.js.map
@@ -0,0 +1,14 @@
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
+ * Patch full YAML document output to match pyyaml formatting
12
+ */
13
+ export declare function patchYamlToPyyaml(yamlText: string): string;
14
+ //# sourceMappingURL=yaml-patch.d.ts.map
@@ -0,0 +1,184 @@
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
+ const BEST_WIDTH = 80;
11
+ /**
12
+ * Patch full YAML document output to match pyyaml formatting
13
+ */
14
+ export function patchYamlToPyyaml(yamlText) {
15
+ const lines = yamlText.split('\n');
16
+ const result = [];
17
+ let inBlockScalar = false;
18
+ let blockIndent = 0;
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ // Track block scalar context (|- or >- or | or >)
22
+ if (inBlockScalar) {
23
+ const currentIndent = line.length - line.trimStart().length;
24
+ if (line.trim() === '' || currentIndent > blockIndent) {
25
+ result.push(line);
26
+ continue;
27
+ }
28
+ inBlockScalar = false;
29
+ }
30
+ // Detect start of block scalar
31
+ if (/^\s*[\w-]+:\s+[|>]-?\s*$/.test(line)) {
32
+ const keyIndent = line.length - line.trimStart().length;
33
+ inBlockScalar = true;
34
+ blockIndent = keyIndent;
35
+ result.push(line);
36
+ continue;
37
+ }
38
+ result.push(...patchLine(line));
39
+ }
40
+ return result.join('\n');
41
+ }
42
+ // Keys that must NEVER be wrapped
43
+ const NO_WRAP_KEYS = new Set([
44
+ 'prompt_script', 'idn', 'runner_type',
45
+ 'model_idn', 'provider_idn', 'skill_idn', 'state_idn',
46
+ 'integration_idn', 'connector_idn', 'interrupt_mode',
47
+ 'skill_selector', 'name', 'scope', 'agent_id',
48
+ 'default_runner_type', 'default_provider_idn', 'default_model_idn',
49
+ 'publication_type', 'is_hidden', 'is_auto_update_enabled',
50
+ 'group', 'registry', 'registry_item_idn', 'version',
51
+ ]);
52
+ function patchLine(line) {
53
+ if (line.trim() === '' || line.trim().startsWith('#')) {
54
+ return [line];
55
+ }
56
+ const kvMatch = line.match(/^(\s*(?:-\s+)?)([\w-]+):\s+(.+)$/);
57
+ if (!kvMatch) {
58
+ return [line];
59
+ }
60
+ const prefix = kvMatch[1];
61
+ const key = kvMatch[2];
62
+ const value = kvMatch[3];
63
+ // Single-quote fix for JSON-like values (before anything else)
64
+ const sqFix = tryConvertToSingleQuote(value);
65
+ const effectiveValue = sqFix ?? value;
66
+ const effectiveLine = sqFix !== null ? `${prefix}${key}: ${sqFix}` : line;
67
+ if (NO_WRAP_KEYS.has(key)) {
68
+ return [effectiveLine];
69
+ }
70
+ // Only wrap if line exceeds BEST_WIDTH
71
+ if (effectiveLine.length <= BEST_WIDTH) {
72
+ return [effectiveLine];
73
+ }
74
+ const keyPart = `${prefix}${key}: `;
75
+ // pyyaml continuation indent = current mapping indent + best_indent
76
+ // For " description: ..." indent is 2, continuation = 2 + 2 = 4 spaces
77
+ const keyIndent = prefix.replace(/-\s+$/, '').length;
78
+ const contIndent = ' '.repeat(keyIndent + 2);
79
+ if (effectiveValue.startsWith('"') && effectiveValue.endsWith('"')) {
80
+ return wrapDoubleQuoted(keyPart, effectiveValue, contIndent);
81
+ }
82
+ if (effectiveValue.startsWith("'") && effectiveValue.endsWith("'")) {
83
+ return [effectiveLine]; // Single-quoted: don't wrap
84
+ }
85
+ return wrapPlainScalar(keyPart, effectiveValue, contIndent);
86
+ }
87
+ /**
88
+ * Try to convert double-quoted string with escaped chars to single-quoted
89
+ */
90
+ function tryConvertToSingleQuote(value) {
91
+ if (!value.startsWith('"') || !value.endsWith('"'))
92
+ return null;
93
+ const inner = value.slice(1, -1);
94
+ if (!inner.includes('\\"'))
95
+ return null;
96
+ if (!inner.includes('[') && !inner.includes('{'))
97
+ return null;
98
+ const unescaped = inner.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
99
+ if (unescaped.includes("'")) {
100
+ return `'${unescaped.replace(/'/g, "''")}'`;
101
+ }
102
+ return `'${unescaped}'`;
103
+ }
104
+ /**
105
+ * Wrap double-quoted scalar matching pyyaml's write_double_quoted algorithm:
106
+ * - Track column from 0
107
+ * - At each space, check if column + pending > best_width
108
+ * - If yes, emit text + `\`, newline, indent, `\ ` (escaped space for continuation)
109
+ */
110
+ function wrapDoubleQuoted(keyPart, quotedValue, contIndent) {
111
+ const inner = quotedValue.slice(1, -1);
112
+ const result = [];
113
+ let column = keyPart.length + 1; // keyPart + opening "
114
+ let lineStart = 0;
115
+ let lastSpace = -1;
116
+ for (let i = 0; i < inner.length; i++) {
117
+ column++;
118
+ if (inner[i] === ' ') {
119
+ lastSpace = i;
120
+ }
121
+ // pyyaml condition: column + remaining_in_word > best_width, at a space or start >= end
122
+ if (column > BEST_WIDTH && lastSpace > lineStart) {
123
+ // Break at lastSpace
124
+ const chunk = inner.slice(lineStart, lastSpace);
125
+ if (result.length === 0) {
126
+ result.push(`${keyPart}"${chunk}\\`);
127
+ }
128
+ else {
129
+ result.push(`${contIndent}\\ ${chunk}\\`);
130
+ }
131
+ lineStart = lastSpace + 1; // skip the space
132
+ column = contIndent.length + 2 + (i - lastSpace); // contIndent + "\ " + chars after space
133
+ lastSpace = -1;
134
+ }
135
+ }
136
+ // Remaining text
137
+ const remaining = inner.slice(lineStart);
138
+ if (result.length === 0) {
139
+ result.push(`${keyPart}"${remaining}"`);
140
+ }
141
+ else {
142
+ result.push(`${contIndent}\\ ${remaining}"`);
143
+ }
144
+ return result;
145
+ }
146
+ /**
147
+ * Wrap plain scalar matching pyyaml's write_plain algorithm:
148
+ * - At each space, if column > best_width, break
149
+ * - Continuation is just indented text (no backslash)
150
+ */
151
+ function wrapPlainScalar(keyPart, value, contIndent) {
152
+ const result = [];
153
+ let column = keyPart.length;
154
+ let lineStart = 0;
155
+ let lastSpace = -1;
156
+ for (let i = 0; i < value.length; i++) {
157
+ column++;
158
+ if (value[i] === ' ') {
159
+ // pyyaml breaks at single space when column > best_width
160
+ if (column > BEST_WIDTH && lastSpace >= lineStart) {
161
+ const chunk = value.slice(lineStart, i);
162
+ if (result.length === 0) {
163
+ result.push(`${keyPart}${chunk}`);
164
+ }
165
+ else {
166
+ result.push(`${contIndent}${chunk}`);
167
+ }
168
+ lineStart = i + 1; // skip the space
169
+ column = contIndent.length;
170
+ }
171
+ lastSpace = i;
172
+ }
173
+ }
174
+ // Remaining
175
+ const remaining = value.slice(lineStart);
176
+ if (result.length === 0) {
177
+ result.push(`${keyPart}${remaining}`);
178
+ }
179
+ else {
180
+ result.push(`${contIndent}${remaining}`);
181
+ }
182
+ return result;
183
+ }
184
+ //# sourceMappingURL=yaml-patch.js.map
package/dist/fsutil.d.ts CHANGED
@@ -7,6 +7,11 @@ export declare function customerStateDir(customerIdn: string): string;
7
7
  export declare function mapPath(customerIdn: string): string;
8
8
  export declare function hashesPath(customerIdn: string): string;
9
9
  export declare function ensureState(customerIdn: string): Promise<void>;
10
+ /**
11
+ * Ensure only the .newo/ state directory exists (no V1 projects/ dir)
12
+ * Used by newo_v2 format to avoid creating V1 artifacts
13
+ */
14
+ export declare function ensureStateOnly(customerIdn: string): Promise<void>;
10
15
  export declare function projectDir(customerIdn: string, projectIdn: string): string;
11
16
  export declare function flowsYamlPath(customerIdn: string): string;
12
17
  export declare function customerAttributesPath(customerIdn: string): string;
@@ -19,6 +24,11 @@ export declare function projectMetadataPath(customerIdn: string, projectIdn: str
19
24
  export declare function agentMetadataPath(customerIdn: string, projectIdn: string, agentIdn: string): string;
20
25
  export declare function flowMetadataPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string): string;
21
26
  export declare function skillMetadataPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string): string;
27
+ export declare function libraryDir(customerIdn: string, projectIdn: string, libraryIdn: string): string;
28
+ export declare function libraryMetadataPath(customerIdn: string, projectIdn: string, libraryIdn: string): string;
29
+ export declare function librarySkillFolderPath(customerIdn: string, projectIdn: string, libraryIdn: string, skillIdn: string): string;
30
+ export declare function librarySkillScriptPath(customerIdn: string, projectIdn: string, libraryIdn: string, skillIdn: string, runnerType?: RunnerType): string;
31
+ export declare function librarySkillMetadataPath(customerIdn: string, projectIdn: string, libraryIdn: string, skillIdn: string): string;
22
32
  export declare function metadataPath(customerIdn: string, projectIdn: string): string;
23
33
  export declare const ROOT_DIR: string;
24
34
  export declare const MAP_PATH: string;
package/dist/fsutil.js CHANGED
@@ -22,6 +22,14 @@ export async function ensureState(customerIdn) {
22
22
  await fs.ensureDir(customerStateDir(customerIdn));
23
23
  await fs.ensureDir(customerProjectsDir(customerIdn));
24
24
  }
25
+ /**
26
+ * Ensure only the .newo/ state directory exists (no V1 projects/ dir)
27
+ * Used by newo_v2 format to avoid creating V1 artifacts
28
+ */
29
+ export async function ensureStateOnly(customerIdn) {
30
+ await fs.ensureDir(STATE_DIR);
31
+ await fs.ensureDir(customerStateDir(customerIdn));
32
+ }
25
33
  export function projectDir(customerIdn, projectIdn) {
26
34
  return path.posix.join(customerProjectsDir(customerIdn), projectIdn);
27
35
  }
@@ -63,6 +71,23 @@ export function flowMetadataPath(customerIdn, projectIdn, agentIdn, flowIdn) {
63
71
  export function skillMetadataPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn) {
64
72
  return path.posix.join(skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn), 'metadata.yaml');
65
73
  }
74
+ // Library paths (cli_v1 format)
75
+ export function libraryDir(customerIdn, projectIdn, libraryIdn) {
76
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'libraries', libraryIdn);
77
+ }
78
+ export function libraryMetadataPath(customerIdn, projectIdn, libraryIdn) {
79
+ return path.posix.join(libraryDir(customerIdn, projectIdn, libraryIdn), 'metadata.yaml');
80
+ }
81
+ export function librarySkillFolderPath(customerIdn, projectIdn, libraryIdn, skillIdn) {
82
+ return path.posix.join(libraryDir(customerIdn, projectIdn, libraryIdn), skillIdn);
83
+ }
84
+ export function librarySkillScriptPath(customerIdn, projectIdn, libraryIdn, skillIdn, runnerType = 'guidance') {
85
+ const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
86
+ return path.posix.join(librarySkillFolderPath(customerIdn, projectIdn, libraryIdn, skillIdn), `${skillIdn}${extension}`);
87
+ }
88
+ export function librarySkillMetadataPath(customerIdn, projectIdn, libraryIdn, skillIdn) {
89
+ return path.posix.join(librarySkillFolderPath(customerIdn, projectIdn, libraryIdn, skillIdn), 'metadata.yaml');
90
+ }
66
91
  // Legacy metadata path - keep for backwards compatibility
67
92
  export function metadataPath(customerIdn, projectIdn) {
68
93
  return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.json');
@@ -243,9 +243,9 @@ export async function pushProjectAttributes(client, customer, projectId, project
243
243
  }
244
244
  // Value type is already parsed (we removed !enum tags above)
245
245
  const valueType = localAttr.value_type;
246
- // Check if value changed
247
- const localValue = String(localAttr.value || '');
248
- const remoteValue = String(remoteAttr.value || '');
246
+ // Check if value changed (use ?? to preserve 0, false, empty string)
247
+ const localValue = String(localAttr.value ?? '');
248
+ const remoteValue = String(remoteAttr.value ?? '');
249
249
  if (localValue !== remoteValue) {
250
250
  if (verbose)
251
251
  console.log(` 🔄 Updating project attribute: ${localAttr.idn}`);
@@ -40,8 +40,8 @@ export async function findSkillScriptFiles(skillFolderPath) {
40
40
  const stats = await fs.stat(filePath);
41
41
  if (stats.isFile()) {
42
42
  const ext = path.extname(fileName).toLowerCase();
43
- // Check for script file extensions
44
- if (['.jinja', '.guidance', '.nsl'].includes(ext)) {
43
+ // Check for script file extensions (all formats)
44
+ if (['.jinja', '.guidance', '.nsl', '.nslg'].includes(ext)) {
45
45
  const content = await fs.readFile(filePath, 'utf8');
46
46
  scriptFiles.push({
47
47
  filePath,
package/dist/types.d.ts CHANGED
@@ -155,10 +155,15 @@ export interface AgentData {
155
155
  id: string;
156
156
  flows: Record<string, FlowData>;
157
157
  }
158
+ export interface LibraryData {
159
+ id: string;
160
+ skills: Record<string, SkillMetadata>;
161
+ }
158
162
  export interface ProjectData {
159
163
  projectId: string;
160
164
  projectIdn: string;
161
165
  agents: Record<string, AgentData>;
166
+ libraries?: Record<string, LibraryData> | undefined;
162
167
  }
163
168
  export interface ProjectMap {
164
169
  projects: Record<string, ProjectData>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newo",
3
- "version": "3.4.2",
3
+ "version": "3.6.0",
4
4
  "description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.ts CHANGED
@@ -627,4 +627,68 @@ export async function getLogs(
627
627
 
628
628
  const response = await client.get<LogsResponse>(`/api/v1/analytics/logs?${queryParams.toString()}`);
629
629
  return response.data;
630
+ }
631
+
632
+ // ── Libraries ──
633
+
634
+ export interface LibraryResponse {
635
+ id: string;
636
+ idn: string;
637
+ project_id: string;
638
+ skills: Skill[];
639
+ }
640
+
641
+ export async function listLibraries(client: AxiosInstance, projectId: string): Promise<LibraryResponse[]> {
642
+ const response = await client.get<LibraryResponse[]>(
643
+ `/api/v1/designer/projects/${projectId}/libraries`
644
+ );
645
+ return response.data;
646
+ }
647
+
648
+ export async function listLibrarySkills(client: AxiosInstance, libraryId: string): Promise<Skill[]> {
649
+ const response = await client.get<Skill[]>(
650
+ `/api/v1/designer/libraries/${libraryId}/skills`
651
+ );
652
+ return response.data;
653
+ }
654
+
655
+ export async function updateLibrarySkill(
656
+ client: AxiosInstance,
657
+ libraryId: string,
658
+ skillId: string,
659
+ data: { prompt_script: string; [key: string]: unknown }
660
+ ): Promise<void> {
661
+ await client.patch(
662
+ `/api/v1/designer/libraries/${libraryId}/skills/${skillId}`,
663
+ data
664
+ );
665
+ }
666
+
667
+ // ── V2 Bulk Export/Import ──
668
+
669
+ export interface V2ExportOptions {
670
+ export_akb?: boolean;
671
+ export_customer_attributes?: boolean;
672
+ exclude_projects_idn?: string[];
673
+ }
674
+
675
+ export async function exportCustomerV2(
676
+ client: AxiosInstance,
677
+ customerId: string,
678
+ options: V2ExportOptions = {}
679
+ ): Promise<Buffer> {
680
+ const params = new URLSearchParams();
681
+ params.set('customer_id', customerId);
682
+ if (options.export_akb !== undefined) params.set('export_akb', String(options.export_akb));
683
+ if (options.export_customer_attributes !== undefined) params.set('export_customer_attributes', String(options.export_customer_attributes));
684
+ if (options.exclude_projects_idn && options.exclude_projects_idn.length > 0) {
685
+ for (const idn of options.exclude_projects_idn) {
686
+ params.append('exclude_projects_idn', idn);
687
+ }
688
+ }
689
+
690
+ const response = await client.post(`/api/v2/designer/customer/export?${params.toString()}`, null, {
691
+ responseType: 'arraybuffer',
692
+ });
693
+ return Buffer.from(response.data as ArrayBuffer);
630
694
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Export command handler - downloads V2 bulk export ZIP from platform
3
+ *
4
+ * Usage:
5
+ * newo export # Export to temp/export-{customerIdn}-{timestamp}.zip
6
+ * newo export --output my-export.zip # Export to specific file
7
+ * newo export --no-akb # Exclude AKB from export
8
+ * newo export --no-attributes # Exclude customer attributes
9
+ */
10
+ import { makeClient, exportCustomerV2 } from '../../api.js';
11
+ import { getValidAccessToken } from '../../auth.js';
12
+ import { selectSingleCustomer } from '../customer-selection.js';
13
+ import fs from 'fs-extra';
14
+ import path from 'path';
15
+ import type { MultiCustomerConfig, CliArgs } from '../../types.js';
16
+
17
+ export async function handleExportCommand(
18
+ customerConfig: MultiCustomerConfig,
19
+ args: CliArgs,
20
+ verbose: boolean
21
+ ): Promise<void> {
22
+ const { selectedCustomer } = selectSingleCustomer(
23
+ customerConfig,
24
+ args.customer as string | undefined
25
+ );
26
+
27
+ if (!selectedCustomer) {
28
+ console.error('Please specify a customer with --customer');
29
+ process.exit(1);
30
+ }
31
+
32
+ const accessToken = await getValidAccessToken(selectedCustomer);
33
+ const client = await makeClient(verbose, accessToken);
34
+
35
+ // Get customer ID from token (needed for V2 export API)
36
+ const customerId = await getCustomerIdFromToken(accessToken);
37
+ if (!customerId) {
38
+ console.error('Could not determine customer ID from token');
39
+ process.exit(1);
40
+ }
41
+
42
+ const exportAkb = !args['no-akb'];
43
+ const exportAttributes = !args['no-attributes'];
44
+
45
+ console.log(`Downloading V2 export for customer ${selectedCustomer.idn}...`);
46
+
47
+ const zipBuffer = await exportCustomerV2(client, customerId, {
48
+ export_akb: exportAkb,
49
+ export_customer_attributes: exportAttributes,
50
+ });
51
+
52
+ // Determine output path
53
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
54
+ const defaultName = `export-${selectedCustomer.idn}-${timestamp}.zip`;
55
+ const outputPath = (args.output as string | undefined) || (args.o as string | undefined)
56
+ || path.join(process.cwd(), 'temp', defaultName);
57
+
58
+ await fs.ensureDir(path.dirname(outputPath));
59
+ await fs.writeFile(outputPath, zipBuffer);
60
+
61
+ console.log(`Exported ${(zipBuffer.length / 1024 / 1024).toFixed(1)}MB to ${outputPath}`);
62
+ }
63
+
64
+ /**
65
+ * Extract customer_id from JWT token payload
66
+ */
67
+ function getCustomerIdFromToken(token: string): string | null {
68
+ try {
69
+ const parts = token.split('.');
70
+ if (parts.length < 2) return null;
71
+ const payload = parts[1]!;
72
+ const padded = payload + '='.repeat(4 - (payload.length % 4));
73
+ const decoded = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as Record<string, unknown>;
74
+ return (decoded['customer_id'] as string) || (decoded['sub'] as string) || null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }