newo 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +68 -20
  3. package/dist/cli/commands/conversations.d.ts +3 -0
  4. package/dist/cli/commands/conversations.js +38 -0
  5. package/dist/cli/commands/help.d.ts +5 -0
  6. package/dist/cli/commands/help.js +50 -0
  7. package/dist/cli/commands/import-akb.d.ts +3 -0
  8. package/dist/cli/commands/import-akb.js +62 -0
  9. package/dist/cli/commands/list-customers.d.ts +3 -0
  10. package/dist/cli/commands/list-customers.js +13 -0
  11. package/dist/cli/commands/meta.d.ts +3 -0
  12. package/dist/cli/commands/meta.js +19 -0
  13. package/dist/cli/commands/pull-attributes.d.ts +3 -0
  14. package/dist/cli/commands/pull-attributes.js +16 -0
  15. package/dist/cli/commands/pull.d.ts +3 -0
  16. package/dist/cli/commands/pull.js +34 -0
  17. package/dist/cli/commands/push.d.ts +3 -0
  18. package/dist/cli/commands/push.js +39 -0
  19. package/dist/cli/commands/status.d.ts +3 -0
  20. package/dist/cli/commands/status.js +22 -0
  21. package/dist/cli/customer-selection.d.ts +23 -0
  22. package/dist/cli/customer-selection.js +110 -0
  23. package/dist/cli/errors.d.ts +9 -0
  24. package/dist/cli/errors.js +111 -0
  25. package/dist/cli.js +66 -463
  26. package/dist/fsutil.js +1 -1
  27. package/dist/sync/attributes.d.ts +7 -0
  28. package/dist/sync/attributes.js +90 -0
  29. package/dist/sync/conversations.d.ts +7 -0
  30. package/dist/sync/conversations.js +218 -0
  31. package/dist/sync/metadata.d.ts +8 -0
  32. package/dist/sync/metadata.js +124 -0
  33. package/dist/sync/projects.d.ts +13 -0
  34. package/dist/sync/projects.js +283 -0
  35. package/dist/sync/push.d.ts +7 -0
  36. package/dist/sync/push.js +171 -0
  37. package/dist/sync/skill-files.d.ts +42 -0
  38. package/dist/sync/skill-files.js +121 -0
  39. package/dist/sync/status.d.ts +6 -0
  40. package/dist/sync/status.js +247 -0
  41. package/dist/sync.d.ts +10 -8
  42. package/dist/sync.js +12 -1197
  43. package/dist/types.d.ts +0 -1
  44. package/package.json +2 -2
  45. package/src/cli/commands/conversations.ts +47 -0
  46. package/src/cli/commands/help.ts +50 -0
  47. package/src/cli/commands/import-akb.ts +71 -0
  48. package/src/cli/commands/list-customers.ts +14 -0
  49. package/src/cli/commands/meta.ts +26 -0
  50. package/src/cli/commands/pull-attributes.ts +23 -0
  51. package/src/cli/commands/pull.ts +43 -0
  52. package/src/cli/commands/push.ts +47 -0
  53. package/src/cli/commands/status.ts +30 -0
  54. package/src/cli/customer-selection.ts +135 -0
  55. package/src/cli/errors.ts +111 -0
  56. package/src/cli.ts +77 -471
  57. package/src/fsutil.ts +1 -1
  58. package/src/sync/attributes.ts +110 -0
  59. package/src/sync/conversations.ts +257 -0
  60. package/src/sync/metadata.ts +153 -0
  61. package/src/sync/projects.ts +359 -0
  62. package/src/sync/push.ts +200 -0
  63. package/src/sync/skill-files.ts +176 -0
  64. package/src/sync/status.ts +277 -0
  65. package/src/sync.ts +14 -1389
  66. package/src/types.ts +0 -1
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Skill file management utilities
3
+ */
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import { sha256 } from '../hash.js';
7
+ import { skillFolderPath } from '../fsutil.js';
8
+ import type { RunnerType } from '../types.js';
9
+
10
+ export interface SkillFile {
11
+ filePath: string;
12
+ fileName: string;
13
+ extension: string;
14
+ content: string;
15
+ }
16
+
17
+ export interface SkillFileValidation {
18
+ isValid: boolean;
19
+ files: SkillFile[];
20
+ warnings: string[];
21
+ errors: string[];
22
+ }
23
+
24
+ /**
25
+ * Get the correct file extension for a runner type
26
+ */
27
+ export function getExtensionForRunner(runnerType: RunnerType): string {
28
+ switch (runnerType) {
29
+ case 'guidance':
30
+ return 'guidance';
31
+ case 'nsl':
32
+ return 'jinja';
33
+ default:
34
+ return 'guidance';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Generate IDN-based script file path
40
+ */
41
+ export function getIdnBasedScriptPath(
42
+ customerIdn: string,
43
+ projectIdn: string,
44
+ agentIdn: string,
45
+ flowIdn: string,
46
+ skillIdn: string,
47
+ runnerType: RunnerType
48
+ ): string {
49
+ const extension = getExtensionForRunner(runnerType);
50
+ const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
51
+ return path.join(folderPath, `${skillIdn}.${extension}`);
52
+ }
53
+
54
+ /**
55
+ * Find all script files in a skill folder
56
+ */
57
+ export async function findSkillScriptFiles(skillFolderPath: string): Promise<SkillFile[]> {
58
+ if (!(await fs.pathExists(skillFolderPath))) {
59
+ return [];
60
+ }
61
+
62
+ const files = await fs.readdir(skillFolderPath);
63
+ const scriptFiles: SkillFile[] = [];
64
+
65
+ for (const fileName of files) {
66
+ const filePath = path.join(skillFolderPath, fileName);
67
+ const stats = await fs.stat(filePath);
68
+
69
+ if (stats.isFile()) {
70
+ const ext = path.extname(fileName).toLowerCase();
71
+
72
+ // Check for script file extensions
73
+ if (['.jinja', '.guidance', '.nsl'].includes(ext)) {
74
+ const content = await fs.readFile(filePath, 'utf8');
75
+ scriptFiles.push({
76
+ filePath,
77
+ fileName,
78
+ extension: ext.slice(1), // Remove the dot
79
+ content
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ return scriptFiles;
86
+ }
87
+
88
+ /**
89
+ * Validate skill folder has exactly one script file
90
+ */
91
+ export async function validateSkillFolder(
92
+ customerIdn: string,
93
+ projectIdn: string,
94
+ agentIdn: string,
95
+ flowIdn: string,
96
+ skillIdn: string
97
+ ): Promise<SkillFileValidation> {
98
+ const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
99
+ const files = await findSkillScriptFiles(folderPath);
100
+
101
+ const warnings: string[] = [];
102
+ const errors: string[] = [];
103
+
104
+ if (files.length === 0) {
105
+ errors.push(`No script files found in skill folder: ${skillIdn}`);
106
+ } else if (files.length > 1) {
107
+ errors.push(`Multiple script files found in skill ${skillIdn}: ${files.map(f => f.fileName).join(', ')}`);
108
+ warnings.push(`Only one script file allowed per skill. Remove extra files and keep one.`);
109
+ }
110
+
111
+ return {
112
+ isValid: files.length === 1,
113
+ files,
114
+ warnings,
115
+ errors
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Get the single skill script file (if valid)
121
+ */
122
+ export async function getSingleSkillFile(
123
+ customerIdn: string,
124
+ projectIdn: string,
125
+ agentIdn: string,
126
+ flowIdn: string,
127
+ skillIdn: string
128
+ ): Promise<SkillFile | null> {
129
+ const validation = await validateSkillFolder(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
130
+
131
+ if (validation.isValid && validation.files.length === 1) {
132
+ return validation.files[0]!;
133
+ }
134
+
135
+ return null;
136
+ }
137
+
138
+ /**
139
+ * Check if skill script content is different from target content
140
+ */
141
+ export function isContentDifferent(existingContent: string, newContent: string): boolean {
142
+ return sha256(existingContent.trim()) !== sha256(newContent.trim());
143
+ }
144
+
145
+ /**
146
+ * Interactive overwrite confirmation
147
+ */
148
+ export async function askForOverwrite(skillIdn: string, existingFile: string, newFile: string): Promise<boolean> {
149
+ const readline = await import('readline');
150
+ const rl = readline.createInterface({
151
+ input: process.stdin,
152
+ output: process.stdout
153
+ });
154
+
155
+ console.log(`\n⚠️ File exists for skill ${skillIdn}:`);
156
+ console.log(` Existing: ${existingFile}`);
157
+ console.log(` New: ${newFile}`);
158
+
159
+ const answer = await new Promise<string>((resolve) => {
160
+ rl.question('Overwrite? (y)es/(n)o/(a)ll/(q)uit: ', resolve);
161
+ });
162
+ rl.close();
163
+
164
+ const choice = answer.toLowerCase().trim();
165
+
166
+ if (choice === 'q' || choice === 'quit') {
167
+ console.log('❌ Pull operation cancelled by user');
168
+ process.exit(0);
169
+ }
170
+
171
+ if (choice === 'a' || choice === 'all') {
172
+ return true; // This should be handled by caller to set global overwrite mode
173
+ }
174
+
175
+ return choice === 'y' || choice === 'yes';
176
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Status checking module
3
+ */
4
+ import fs from 'fs-extra';
5
+ import yaml from 'js-yaml';
6
+ import { sha256, loadHashes } from '../hash.js';
7
+ import {
8
+ ensureState,
9
+ mapPath,
10
+ skillMetadataPath,
11
+ customerAttributesPath,
12
+ customerAttributesBackupPath,
13
+ flowsYamlPath
14
+ } from '../fsutil.js';
15
+ import {
16
+ validateSkillFolder,
17
+ getSingleSkillFile
18
+ } from './skill-files.js';
19
+ import type {
20
+ CustomerConfig,
21
+ ProjectMap,
22
+ LegacyProjectMap,
23
+ ProjectData
24
+ } from '../types.js';
25
+
26
+ // Type guards for project map formats
27
+ function isProjectMap(x: unknown): x is ProjectMap {
28
+ return typeof x === 'object' && x !== null && 'projects' in x;
29
+ }
30
+
31
+ function isLegacyProjectMap(x: unknown): x is LegacyProjectMap {
32
+ return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
33
+ }
34
+
35
+ /**
36
+ * Check status of files for a customer
37
+ */
38
+ export async function status(customer: CustomerConfig, verbose: boolean = false): Promise<void> {
39
+ await ensureState(customer.idn);
40
+ if (!(await fs.pathExists(mapPath(customer.idn)))) {
41
+ console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
42
+ return;
43
+ }
44
+
45
+ if (verbose) console.log(`📋 Loading project mapping and hashes for customer ${customer.idn}...`);
46
+ const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
47
+ const hashes = await loadHashes(customer.idn);
48
+ let dirty = 0;
49
+
50
+ // Handle both old single-project format and new multi-project format with type guards
51
+ const projects = isProjectMap(idMapData) && idMapData.projects
52
+ ? idMapData.projects
53
+ : isLegacyProjectMap(idMapData)
54
+ ? { '': idMapData as ProjectData }
55
+ : (() => { throw new Error('Invalid project map format'); })();
56
+
57
+ for (const [projectIdn, projectData] of Object.entries(projects)) {
58
+ if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
59
+
60
+ for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
61
+ if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
62
+ for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
63
+ if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
64
+ for (const [skillIdn] of Object.entries(flowObj.skills)) {
65
+ // Validate skill folder and show warnings
66
+ const validation = await validateSkillFolder(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
67
+
68
+ if (!validation.isValid) {
69
+ // Show warnings and errors
70
+ validation.errors.forEach(error => {
71
+ console.error(`❌ ${error}`);
72
+ });
73
+ validation.warnings.forEach(warning => {
74
+ console.warn(`⚠️ ${warning}`);
75
+ });
76
+
77
+ if (validation.files.length > 1) {
78
+ console.warn(`⚠️ Multiple script files in skill ${skillIdn}:`);
79
+ validation.files.forEach(file => {
80
+ console.warn(` • ${file.fileName}`);
81
+ });
82
+ console.warn(` Status check skipped - please keep only one script file.`);
83
+ } else if (validation.files.length === 0) {
84
+ console.log(`D ${skillIdn}/ (no script files)`);
85
+ dirty++;
86
+ }
87
+ continue;
88
+ }
89
+
90
+ // Get the single valid script file
91
+ const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
92
+ if (!skillFile) {
93
+ console.log(`D ${skillIdn}/ (no valid script file)`);
94
+ dirty++;
95
+ if (verbose) console.log(` ❌ No valid script file found: ${skillIdn}`);
96
+ continue;
97
+ }
98
+
99
+ const content = skillFile.content;
100
+ const currentPath = skillFile.filePath;
101
+ const h = sha256(content);
102
+ const oldHash = hashes[currentPath];
103
+
104
+ if (verbose) {
105
+ console.log(` 📄 ${currentPath}`);
106
+ console.log(` Old hash: ${oldHash || 'none'}`);
107
+ console.log(` New hash: ${h}`);
108
+ }
109
+
110
+ if (oldHash !== h) {
111
+ console.log(`M ${currentPath}`);
112
+ dirty++;
113
+ if (verbose) console.log(` 🔄 Modified: ${skillFile.fileName}`);
114
+ } else if (verbose) {
115
+ console.log(` ✓ Unchanged: ${skillFile.fileName}`);
116
+ }
117
+ }
118
+
119
+ // Check metadata.yaml files for changes
120
+ for (const [skillIdn] of Object.entries(flowObj.skills)) {
121
+ const metadataPath = projectIdn ?
122
+ skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
123
+ skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
124
+
125
+ if (await fs.pathExists(metadataPath)) {
126
+ const metadataContent = await fs.readFile(metadataPath, 'utf8');
127
+ const h = sha256(metadataContent);
128
+ const oldHash = hashes[metadataPath];
129
+
130
+ if (verbose) {
131
+ console.log(` 📄 ${metadataPath}`);
132
+ console.log(` Old hash: ${oldHash || 'none'}`);
133
+ console.log(` New hash: ${h}`);
134
+ }
135
+
136
+ if (oldHash !== h) {
137
+ console.log(`M ${metadataPath}`);
138
+ dirty++;
139
+
140
+ // Show which metadata fields changed
141
+ try {
142
+ const newMetadata = yaml.load(metadataContent) as any;
143
+ console.log(` 📊 Metadata changed for skill: ${skillIdn}`);
144
+ if (newMetadata?.title) {
145
+ console.log(` • Title: ${newMetadata.title}`);
146
+ }
147
+ if (newMetadata?.runner_type) {
148
+ console.log(` • Runner: ${newMetadata.runner_type}`);
149
+ }
150
+ if (newMetadata?.model) {
151
+ console.log(` • Model: ${newMetadata.model.provider_idn}/${newMetadata.model.model_idn}`);
152
+ }
153
+ } catch (e) {
154
+ if (verbose) console.log(` 🔄 Modified: metadata.yaml`);
155
+ }
156
+ } else if (verbose) {
157
+ console.log(` ✓ Unchanged: ${metadataPath}`);
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // Check attributes file for changes
166
+ try {
167
+ const attributesFile = customerAttributesPath(customer.idn);
168
+ if (await fs.pathExists(attributesFile)) {
169
+ const content = await fs.readFile(attributesFile, 'utf8');
170
+ const h = sha256(content);
171
+ const oldHash = hashes[attributesFile];
172
+
173
+ if (verbose) {
174
+ console.log(`📄 ${attributesFile}`);
175
+ console.log(` Old hash: ${oldHash || 'none'}`);
176
+ console.log(` New hash: ${h}`);
177
+ }
178
+
179
+ if (oldHash !== h) {
180
+ console.log(`M ${attributesFile}`);
181
+ dirty++;
182
+
183
+ // Show which attributes changed by comparing with backup
184
+ try {
185
+ const attributesBackupFile = customerAttributesBackupPath(customer.idn);
186
+ if (await fs.pathExists(attributesBackupFile)) {
187
+ const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
188
+
189
+ const parseYaml = (content: string) => {
190
+ let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
191
+ return yaml.load(yamlContent) as { attributes: any[] };
192
+ };
193
+
194
+ const currentData = parseYaml(content);
195
+ const backupData = parseYaml(backupContent);
196
+
197
+ if (currentData?.attributes && backupData?.attributes) {
198
+ const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
199
+ const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
200
+
201
+ const changedAttributes: string[] = [];
202
+
203
+ for (const [idn, currentAttr] of currentAttrs) {
204
+ const backupAttr = backupAttrs.get(idn);
205
+ const hasChanged = !backupAttr ||
206
+ currentAttr.value !== backupAttr.value ||
207
+ currentAttr.title !== backupAttr.title ||
208
+ currentAttr.description !== backupAttr.description ||
209
+ currentAttr.group !== backupAttr.group ||
210
+ currentAttr.is_hidden !== backupAttr.is_hidden;
211
+
212
+ if (hasChanged) {
213
+ changedAttributes.push(idn);
214
+ }
215
+ }
216
+
217
+ if (changedAttributes.length > 0) {
218
+ console.log(` 📊 Changed attributes (${changedAttributes.length}):`);
219
+ changedAttributes.slice(0, 5).forEach(idn => {
220
+ const current = currentAttrs.get(idn);
221
+ console.log(` • ${idn}: ${current?.title || 'No title'}`);
222
+ });
223
+ if (changedAttributes.length > 5) {
224
+ console.log(` ... and ${changedAttributes.length - 5} more`);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ } catch (e) {
230
+ // Fallback to simple message if diff analysis fails
231
+ }
232
+
233
+ if (verbose) console.log(` 🔄 Modified: attributes.yaml`);
234
+ } else if (verbose) {
235
+ console.log(` ✓ Unchanged: attributes.yaml`);
236
+ }
237
+ }
238
+ } catch (error) {
239
+ if (verbose) console.log(`⚠️ Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
240
+ }
241
+
242
+ // Check flows.yaml file for changes
243
+ const flowsFile = flowsYamlPath(customer.idn);
244
+ if (await fs.pathExists(flowsFile)) {
245
+ try {
246
+ const flowsContent = await fs.readFile(flowsFile, 'utf8');
247
+ const h = sha256(flowsContent);
248
+ const oldHash = hashes[flowsFile];
249
+
250
+ if (verbose) {
251
+ console.log(`📄 flows.yaml`);
252
+ console.log(` Old hash: ${oldHash || 'none'}`);
253
+ console.log(` New hash: ${h}`);
254
+ }
255
+
256
+ if (oldHash !== h) {
257
+ console.log(`M ${flowsFile}`);
258
+ dirty++;
259
+ if (verbose) {
260
+ const flowsStats = await fs.stat(flowsFile);
261
+ console.log(` 🔄 Modified: flows.yaml`);
262
+ console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
263
+ console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
264
+ }
265
+ } else if (verbose) {
266
+ const flowsStats = await fs.stat(flowsFile);
267
+ console.log(` ✓ Unchanged: flows.yaml`);
268
+ console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
269
+ console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
270
+ }
271
+ } catch (error) {
272
+ if (verbose) console.log(`⚠️ Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
273
+ }
274
+ }
275
+
276
+ console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
277
+ }