newo 1.9.2 ā 2.0.1
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/CHANGELOG.md +116 -0
- package/README.md +68 -20
- package/dist/cli/commands/conversations.d.ts +3 -0
- package/dist/cli/commands/conversations.js +38 -0
- package/dist/cli/commands/help.d.ts +5 -0
- package/dist/cli/commands/help.js +50 -0
- package/dist/cli/commands/import-akb.d.ts +3 -0
- package/dist/cli/commands/import-akb.js +62 -0
- package/dist/cli/commands/list-customers.d.ts +3 -0
- package/dist/cli/commands/list-customers.js +13 -0
- package/dist/cli/commands/meta.d.ts +3 -0
- package/dist/cli/commands/meta.js +19 -0
- package/dist/cli/commands/pull-attributes.d.ts +3 -0
- package/dist/cli/commands/pull-attributes.js +16 -0
- package/dist/cli/commands/pull.d.ts +3 -0
- package/dist/cli/commands/pull.js +34 -0
- package/dist/cli/commands/push.d.ts +3 -0
- package/dist/cli/commands/push.js +39 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.js +22 -0
- package/dist/cli/customer-selection.d.ts +23 -0
- package/dist/cli/customer-selection.js +110 -0
- package/dist/cli/errors.d.ts +9 -0
- package/dist/cli/errors.js +111 -0
- package/dist/cli.js +66 -463
- package/dist/fsutil.js +1 -1
- package/dist/sync/attributes.d.ts +7 -0
- package/dist/sync/attributes.js +90 -0
- package/dist/sync/conversations.d.ts +7 -0
- package/dist/sync/conversations.js +218 -0
- package/dist/sync/metadata.d.ts +8 -0
- package/dist/sync/metadata.js +124 -0
- package/dist/sync/projects.d.ts +13 -0
- package/dist/sync/projects.js +298 -0
- package/dist/sync/push.d.ts +7 -0
- package/dist/sync/push.js +171 -0
- package/dist/sync/skill-files.d.ts +43 -0
- package/dist/sync/skill-files.js +123 -0
- package/dist/sync/status.d.ts +6 -0
- package/dist/sync/status.js +247 -0
- package/dist/sync.d.ts +10 -8
- package/dist/sync.js +12 -1226
- package/dist/types.d.ts +0 -1
- package/package.json +2 -2
- package/src/cli/commands/conversations.ts +47 -0
- package/src/cli/commands/help.ts +50 -0
- package/src/cli/commands/import-akb.ts +71 -0
- package/src/cli/commands/list-customers.ts +14 -0
- package/src/cli/commands/meta.ts +26 -0
- package/src/cli/commands/pull-attributes.ts +23 -0
- package/src/cli/commands/pull.ts +43 -0
- package/src/cli/commands/push.ts +47 -0
- package/src/cli/commands/status.ts +30 -0
- package/src/cli/customer-selection.ts +135 -0
- package/src/cli/errors.ts +111 -0
- package/src/cli.ts +77 -471
- package/src/fsutil.ts +1 -1
- package/src/sync/attributes.ts +110 -0
- package/src/sync/conversations.ts +257 -0
- package/src/sync/metadata.ts +153 -0
- package/src/sync/projects.ts +372 -0
- package/src/sync/push.ts +200 -0
- package/src/sync/skill-files.ts +181 -0
- package/src/sync/status.ts +277 -0
- package/src/sync.ts +14 -1418
- package/src/types.ts +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project synchronization operations
|
|
3
|
+
*/
|
|
4
|
+
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates } from '../api.js';
|
|
5
|
+
import { ensureState, writeFileSafe, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, skillScriptPath, skillFolderPath, flowsYamlPath, customerAttributesPath } from '../fsutil.js';
|
|
6
|
+
import { findSkillScriptFiles, isContentDifferent, askForOverwrite, getExtensionForRunner } from './skill-files.js';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import { sha256, saveHashes } from '../hash.js';
|
|
9
|
+
import yaml from 'js-yaml';
|
|
10
|
+
import { generateFlowsYaml } from './metadata.js';
|
|
11
|
+
import { saveCustomerAttributes } from './attributes.js';
|
|
12
|
+
// Type guards for project map formats
|
|
13
|
+
export function isProjectMap(x) {
|
|
14
|
+
return typeof x === 'object' && x !== null && 'projects' in x;
|
|
15
|
+
}
|
|
16
|
+
export function isLegacyProjectMap(x) {
|
|
17
|
+
return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pull a single project and all its data
|
|
21
|
+
*/
|
|
22
|
+
export async function pullSingleProject(client, customer, projectId, verbose = false, silentOverwrite = false) {
|
|
23
|
+
if (verbose)
|
|
24
|
+
console.log(`š Loading project list for customer ${customer.idn}...`);
|
|
25
|
+
const projects = projectId ?
|
|
26
|
+
[{ id: projectId, idn: 'unknown', title: 'Project' }] :
|
|
27
|
+
await listProjects(client);
|
|
28
|
+
if (projects.length === 0) {
|
|
29
|
+
console.log(`No projects found for customer ${customer.idn}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await ensureState(customer.idn);
|
|
33
|
+
// Load existing mappings if they exist
|
|
34
|
+
let existingMap = { projects: {} };
|
|
35
|
+
const mapFile = mapPath(customer.idn);
|
|
36
|
+
if (await fs.pathExists(mapFile)) {
|
|
37
|
+
const mapData = await fs.readJson(mapFile);
|
|
38
|
+
if (isProjectMap(mapData)) {
|
|
39
|
+
existingMap = mapData;
|
|
40
|
+
}
|
|
41
|
+
else if (isLegacyProjectMap(mapData)) {
|
|
42
|
+
// Convert legacy format to new format
|
|
43
|
+
existingMap = {
|
|
44
|
+
projects: {
|
|
45
|
+
[mapData.projectIdn || '']: mapData
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const newHashes = {};
|
|
51
|
+
// Progress tracking and overwrite control
|
|
52
|
+
let totalSkills = 0;
|
|
53
|
+
let processedSkills = 0;
|
|
54
|
+
let globalOverwriteAll = silentOverwrite;
|
|
55
|
+
// Count total skills for progress tracking
|
|
56
|
+
for (const project of projects) {
|
|
57
|
+
const agents = await listAgents(client, project.id);
|
|
58
|
+
for (const agent of agents) {
|
|
59
|
+
const flows = agent.flows || [];
|
|
60
|
+
for (const flow of flows) {
|
|
61
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
62
|
+
totalSkills += skills.length;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (verbose)
|
|
67
|
+
console.log(`š Total skills to process: ${totalSkills}`);
|
|
68
|
+
for (const project of projects) {
|
|
69
|
+
if (verbose)
|
|
70
|
+
console.log(`š Processing project: ${project.title} (${project.idn})`);
|
|
71
|
+
// Create project metadata
|
|
72
|
+
const projectMeta = {
|
|
73
|
+
id: project.id,
|
|
74
|
+
idn: project.idn,
|
|
75
|
+
title: project.title,
|
|
76
|
+
description: project.description || '',
|
|
77
|
+
created_at: project.created_at || '',
|
|
78
|
+
updated_at: project.updated_at || ''
|
|
79
|
+
};
|
|
80
|
+
// Save project metadata
|
|
81
|
+
const projectMetaPath = projectMetadataPath(customer.idn, project.idn);
|
|
82
|
+
const projectMetaYaml = yaml.dump(projectMeta, { indent: 2, quotingType: '"', forceQuotes: false });
|
|
83
|
+
await writeFileSafe(projectMetaPath, projectMetaYaml);
|
|
84
|
+
newHashes[projectMetaPath] = sha256(projectMetaYaml);
|
|
85
|
+
const agents = await listAgents(client, project.id);
|
|
86
|
+
if (verbose)
|
|
87
|
+
console.log(` š Found ${agents.length} agents in project ${project.title}`);
|
|
88
|
+
const projectData = {
|
|
89
|
+
projectId: project.id,
|
|
90
|
+
projectIdn: project.idn,
|
|
91
|
+
agents: {}
|
|
92
|
+
};
|
|
93
|
+
for (const agent of agents) {
|
|
94
|
+
if (verbose)
|
|
95
|
+
console.log(` š Processing agent: ${agent.title} (${agent.idn})`);
|
|
96
|
+
// Create agent metadata
|
|
97
|
+
const agentMeta = {
|
|
98
|
+
id: agent.id,
|
|
99
|
+
idn: agent.idn,
|
|
100
|
+
title: agent.title || '',
|
|
101
|
+
description: agent.description || ''
|
|
102
|
+
};
|
|
103
|
+
// Save agent metadata
|
|
104
|
+
const agentMetaPath = agentMetadataPath(customer.idn, project.idn, agent.idn);
|
|
105
|
+
const agentMetaYaml = yaml.dump(agentMeta, { indent: 2, quotingType: '"', forceQuotes: false });
|
|
106
|
+
await writeFileSafe(agentMetaPath, agentMetaYaml);
|
|
107
|
+
newHashes[agentMetaPath] = sha256(agentMetaYaml);
|
|
108
|
+
projectData.agents[agent.idn] = {
|
|
109
|
+
id: agent.id,
|
|
110
|
+
flows: {}
|
|
111
|
+
};
|
|
112
|
+
const flows = agent.flows || [];
|
|
113
|
+
if (verbose && flows.length > 0) {
|
|
114
|
+
console.log(` š Found ${flows.length} flows in agent ${agent.title}`);
|
|
115
|
+
}
|
|
116
|
+
for (const flow of flows) {
|
|
117
|
+
if (verbose)
|
|
118
|
+
console.log(` š Processing flow: ${flow.title} (${flow.idn})`);
|
|
119
|
+
// Get flow events and states for metadata
|
|
120
|
+
const [events, states] = await Promise.all([
|
|
121
|
+
listFlowEvents(client, flow.id).catch(() => []),
|
|
122
|
+
listFlowStates(client, flow.id).catch(() => [])
|
|
123
|
+
]);
|
|
124
|
+
// Create flow metadata
|
|
125
|
+
const flowMeta = {
|
|
126
|
+
id: flow.id,
|
|
127
|
+
idn: flow.idn,
|
|
128
|
+
title: flow.title,
|
|
129
|
+
description: flow.description || '',
|
|
130
|
+
default_runner_type: flow.default_runner_type,
|
|
131
|
+
default_model: flow.default_model,
|
|
132
|
+
events,
|
|
133
|
+
state_fields: states
|
|
134
|
+
};
|
|
135
|
+
// Save flow metadata
|
|
136
|
+
const flowMetaPath = flowMetadataPath(customer.idn, project.idn, agent.idn, flow.idn);
|
|
137
|
+
const flowMetaYaml = yaml.dump(flowMeta, { indent: 2, quotingType: '"', forceQuotes: false });
|
|
138
|
+
await writeFileSafe(flowMetaPath, flowMetaYaml);
|
|
139
|
+
newHashes[flowMetaPath] = sha256(flowMetaYaml);
|
|
140
|
+
projectData.agents[agent.idn].flows[flow.idn] = {
|
|
141
|
+
id: flow.id,
|
|
142
|
+
skills: {}
|
|
143
|
+
};
|
|
144
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
145
|
+
if (verbose)
|
|
146
|
+
console.log(` š Found ${skills.length} skills in flow ${flow.title}`);
|
|
147
|
+
for (const skill of skills) {
|
|
148
|
+
processedSkills++;
|
|
149
|
+
const progress = `[${processedSkills}/${totalSkills}]`;
|
|
150
|
+
if (verbose) {
|
|
151
|
+
console.log(` š ${progress} Processing skill: ${skill.title} (${skill.idn})`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Show progress for non-verbose mode
|
|
155
|
+
if (processedSkills % 10 === 0 || processedSkills === totalSkills) {
|
|
156
|
+
process.stdout.write(`\rš Processing skills: ${processedSkills}/${totalSkills} (${Math.round(processedSkills / totalSkills * 100)}%)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Create skill metadata
|
|
160
|
+
const skillMeta = {
|
|
161
|
+
id: skill.id,
|
|
162
|
+
idn: skill.idn,
|
|
163
|
+
title: skill.title,
|
|
164
|
+
runner_type: skill.runner_type,
|
|
165
|
+
model: skill.model,
|
|
166
|
+
parameters: [...skill.parameters],
|
|
167
|
+
path: skill.path
|
|
168
|
+
};
|
|
169
|
+
// Save skill metadata
|
|
170
|
+
const skillMetaPath = skillMetadataPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn);
|
|
171
|
+
const skillMetaYaml = yaml.dump(skillMeta, { indent: 2, quotingType: '"', forceQuotes: false });
|
|
172
|
+
await writeFileSafe(skillMetaPath, skillMetaYaml);
|
|
173
|
+
newHashes[skillMetaPath] = sha256(skillMetaYaml);
|
|
174
|
+
// Handle skill script with IDN-based naming and overwrite detection
|
|
175
|
+
const scriptContent = skill.prompt_script || '';
|
|
176
|
+
const targetScriptPath = skillScriptPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn, skill.runner_type);
|
|
177
|
+
const folderPath = skillFolderPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn);
|
|
178
|
+
// Check for existing script files in the skill folder
|
|
179
|
+
const existingFiles = await findSkillScriptFiles(folderPath);
|
|
180
|
+
let shouldWrite = true;
|
|
181
|
+
let hasContentMatch = false;
|
|
182
|
+
if (existingFiles.length > 0) {
|
|
183
|
+
// Check if any existing file has the same content
|
|
184
|
+
hasContentMatch = existingFiles.some(file => !isContentDifferent(file.content, scriptContent));
|
|
185
|
+
if (hasContentMatch) {
|
|
186
|
+
// Content is the same - handle file naming
|
|
187
|
+
const matchingFile = existingFiles.find(file => !isContentDifferent(file.content, scriptContent));
|
|
188
|
+
const correctName = `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`;
|
|
189
|
+
if (matchingFile && matchingFile.fileName !== correctName) {
|
|
190
|
+
// Remove old file and write with correct IDN-based name
|
|
191
|
+
await fs.remove(matchingFile.filePath);
|
|
192
|
+
if (verbose)
|
|
193
|
+
console.log(` š Renamed ${matchingFile.fileName} ā ${correctName}`);
|
|
194
|
+
}
|
|
195
|
+
else if (matchingFile && matchingFile.fileName === correctName) {
|
|
196
|
+
// Already has correct name and content - skip completely
|
|
197
|
+
shouldWrite = false;
|
|
198
|
+
newHashes[matchingFile.filePath] = sha256(scriptContent);
|
|
199
|
+
if (verbose)
|
|
200
|
+
console.log(` ā Content unchanged for ${skill.idn}, keeping existing file`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (!globalOverwriteAll) {
|
|
204
|
+
// Content is different, ask for overwrite unless global override is set
|
|
205
|
+
const existingFile = existingFiles[0];
|
|
206
|
+
const overwriteChoice = await askForOverwrite(skill.idn, existingFile.fileName, `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`);
|
|
207
|
+
if (overwriteChoice === 'quit') {
|
|
208
|
+
console.log('ā Pull operation cancelled by user');
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
else if (overwriteChoice === 'all') {
|
|
212
|
+
globalOverwriteAll = true;
|
|
213
|
+
// Continue with overwrite
|
|
214
|
+
for (const file of existingFiles) {
|
|
215
|
+
await fs.remove(file.filePath);
|
|
216
|
+
if (verbose)
|
|
217
|
+
console.log(` šļø Removed ${file.fileName}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (overwriteChoice === 'yes') {
|
|
221
|
+
// Single overwrite
|
|
222
|
+
for (const file of existingFiles) {
|
|
223
|
+
await fs.remove(file.filePath);
|
|
224
|
+
if (verbose)
|
|
225
|
+
console.log(` šļø Removed ${file.fileName}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// User said no
|
|
230
|
+
shouldWrite = false;
|
|
231
|
+
if (verbose)
|
|
232
|
+
console.log(` ā ļø Skipped overwrite for ${skill.idn}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Silent overwrite mode - remove existing files
|
|
237
|
+
for (const file of existingFiles) {
|
|
238
|
+
await fs.remove(file.filePath);
|
|
239
|
+
if (verbose)
|
|
240
|
+
console.log(` š Silent overwrite: removed ${file.fileName}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (shouldWrite) {
|
|
245
|
+
await writeFileSafe(targetScriptPath, scriptContent);
|
|
246
|
+
newHashes[targetScriptPath] = sha256(scriptContent);
|
|
247
|
+
const fileName = `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`;
|
|
248
|
+
if (verbose)
|
|
249
|
+
console.log(` ā Saved ${fileName}`);
|
|
250
|
+
}
|
|
251
|
+
projectData.agents[agent.idn].flows[flow.idn].skills[skill.idn] = skillMeta;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Store project data in map
|
|
256
|
+
existingMap.projects[project.idn] = projectData;
|
|
257
|
+
}
|
|
258
|
+
// Clear progress line for non-verbose mode
|
|
259
|
+
if (!verbose && totalSkills > 0) {
|
|
260
|
+
console.log(`\nā
Processed ${totalSkills} skills`);
|
|
261
|
+
}
|
|
262
|
+
// Save updated project map
|
|
263
|
+
await writeFileSafe(mapFile, JSON.stringify(existingMap, null, 2));
|
|
264
|
+
// Pull customer attributes as part of the project pull
|
|
265
|
+
try {
|
|
266
|
+
if (verbose)
|
|
267
|
+
console.log(`š Fetching customer attributes for ${customer.idn}...`);
|
|
268
|
+
const attributesContent = await saveCustomerAttributes(client, customer, verbose);
|
|
269
|
+
// Add attributes.yaml hash to the hash store
|
|
270
|
+
const attributesPath = customerAttributesPath(customer.idn);
|
|
271
|
+
newHashes[attributesPath] = sha256(attributesContent);
|
|
272
|
+
if (verbose)
|
|
273
|
+
console.log(`ā
Customer attributes saved to newo_customers/${customer.idn}/attributes.yaml`);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.warn(`ā ļø Failed to fetch customer attributes for ${customer.idn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
277
|
+
if (verbose)
|
|
278
|
+
console.warn('You can manually pull attributes using: newo pull-attributes');
|
|
279
|
+
}
|
|
280
|
+
// Generate flows.yaml and get its content for hashing
|
|
281
|
+
const flowsYamlContent = await generateFlowsYaml(existingMap, customer.idn, verbose);
|
|
282
|
+
// Add flows.yaml hash to the hash store
|
|
283
|
+
const flowsYamlFilePath = flowsYamlPath(customer.idn);
|
|
284
|
+
newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
|
|
285
|
+
// Save hashes (now including flows.yaml and attributes.yaml)
|
|
286
|
+
await saveHashes(newHashes, customer.idn);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Pull all projects for a customer
|
|
290
|
+
*/
|
|
291
|
+
export async function pullAll(client, customer, projectId = null, verbose = false, silentOverwrite = false) {
|
|
292
|
+
if (verbose)
|
|
293
|
+
console.log(`š Starting pull operation for customer ${customer.idn}...`);
|
|
294
|
+
await pullSingleProject(client, customer, projectId, verbose, silentOverwrite);
|
|
295
|
+
if (verbose)
|
|
296
|
+
console.log(`ā
Pull completed for customer ${customer.idn}`);
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import type { CustomerConfig } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Push changed files to NEWO platform
|
|
5
|
+
*/
|
|
6
|
+
export declare function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose?: boolean): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push operations for changed files
|
|
3
|
+
*/
|
|
4
|
+
import { updateSkill } from '../api.js';
|
|
5
|
+
import { ensureState, mapPath, skillMetadataPath } from '../fsutil.js';
|
|
6
|
+
import { validateSkillFolder, getSingleSkillFile } from './skill-files.js';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import { sha256, loadHashes, saveHashes } from '../hash.js';
|
|
9
|
+
import yaml from 'js-yaml';
|
|
10
|
+
import { generateFlowsYaml } from './metadata.js';
|
|
11
|
+
import { isProjectMap, isLegacyProjectMap } from './projects.js';
|
|
12
|
+
import { flowsYamlPath } from '../fsutil.js';
|
|
13
|
+
/**
|
|
14
|
+
* Push changed files to NEWO platform
|
|
15
|
+
*/
|
|
16
|
+
export async function pushChanged(client, customer, verbose = false) {
|
|
17
|
+
await ensureState(customer.idn);
|
|
18
|
+
if (!(await fs.pathExists(mapPath(customer.idn)))) {
|
|
19
|
+
console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (verbose)
|
|
23
|
+
console.log(`š Loading project mapping and hashes for customer ${customer.idn}...`);
|
|
24
|
+
const idMapData = await fs.readJson(mapPath(customer.idn));
|
|
25
|
+
const hashes = await loadHashes(customer.idn);
|
|
26
|
+
const newHashes = { ...hashes };
|
|
27
|
+
let pushed = 0;
|
|
28
|
+
let scanned = 0;
|
|
29
|
+
let metadataChanged = false;
|
|
30
|
+
// Handle both old single-project format and new multi-project format with type guards
|
|
31
|
+
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
32
|
+
? idMapData.projects
|
|
33
|
+
: isLegacyProjectMap(idMapData)
|
|
34
|
+
? { '': idMapData }
|
|
35
|
+
: (() => { throw new Error('Invalid project map format'); })();
|
|
36
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
37
|
+
if (verbose && projectIdn)
|
|
38
|
+
console.log(`š Checking project: ${projectIdn}`);
|
|
39
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
40
|
+
if (verbose)
|
|
41
|
+
console.log(` š Checking agent: ${agentIdn}`);
|
|
42
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
43
|
+
if (verbose)
|
|
44
|
+
console.log(` š Checking flow: ${flowIdn}`);
|
|
45
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
46
|
+
scanned++;
|
|
47
|
+
// Validate skill folder has exactly one script file
|
|
48
|
+
const validation = await validateSkillFolder(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
49
|
+
if (!validation.isValid) {
|
|
50
|
+
// Show warnings and errors
|
|
51
|
+
validation.errors.forEach(error => {
|
|
52
|
+
console.error(`ā ${error}`);
|
|
53
|
+
});
|
|
54
|
+
validation.warnings.forEach(warning => {
|
|
55
|
+
console.warn(`ā ļø ${warning}`);
|
|
56
|
+
});
|
|
57
|
+
if (validation.files.length > 1) {
|
|
58
|
+
console.warn(`ā ļø Skipping push for skill ${skillIdn} - multiple script files found:`);
|
|
59
|
+
validation.files.forEach(file => {
|
|
60
|
+
console.warn(` ⢠${file.fileName}`);
|
|
61
|
+
});
|
|
62
|
+
console.warn(` Please keep only one script file and try again.`);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Get the single valid script file
|
|
67
|
+
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
68
|
+
if (!skillFile) {
|
|
69
|
+
if (verbose)
|
|
70
|
+
console.log(` ā No valid script file found for: ${skillIdn}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const content = skillFile.content;
|
|
74
|
+
const currentPath = skillFile.filePath;
|
|
75
|
+
const h = sha256(content);
|
|
76
|
+
const oldHash = hashes[currentPath];
|
|
77
|
+
if (oldHash !== h) {
|
|
78
|
+
if (verbose)
|
|
79
|
+
console.log(`š Script changed, updating: ${skillIdn} (${skillFile.fileName})`);
|
|
80
|
+
try {
|
|
81
|
+
// Create skill object for update
|
|
82
|
+
const skillObject = {
|
|
83
|
+
id: skillMeta.id,
|
|
84
|
+
title: skillMeta.title,
|
|
85
|
+
idn: skillMeta.idn,
|
|
86
|
+
prompt_script: content,
|
|
87
|
+
runner_type: skillMeta.runner_type,
|
|
88
|
+
model: skillMeta.model,
|
|
89
|
+
parameters: skillMeta.parameters,
|
|
90
|
+
path: skillMeta.path || undefined
|
|
91
|
+
};
|
|
92
|
+
await updateSkill(client, skillObject);
|
|
93
|
+
console.log(`ā Pushed: ${skillIdn} (${skillMeta.title}) from ${skillFile.fileName}`);
|
|
94
|
+
newHashes[currentPath] = h;
|
|
95
|
+
pushed++;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error(`ā Failed to push ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (verbose) {
|
|
102
|
+
console.log(` ā No changes: ${skillIdn} (${skillFile.fileName})`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Check for metadata-only changes and push them separately
|
|
106
|
+
for (const [skillIdn] of Object.entries(flowObj.skills)) {
|
|
107
|
+
const metadataPath = projectIdn ?
|
|
108
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
109
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
110
|
+
if (await fs.pathExists(metadataPath)) {
|
|
111
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
112
|
+
const h = sha256(metadataContent);
|
|
113
|
+
const oldHash = hashes[metadataPath];
|
|
114
|
+
if (oldHash !== h) {
|
|
115
|
+
if (verbose)
|
|
116
|
+
console.log(`š Metadata-only change detected for ${skillIdn}, updating skill...`);
|
|
117
|
+
try {
|
|
118
|
+
// Load updated metadata
|
|
119
|
+
const updatedMetadata = yaml.load(metadataContent);
|
|
120
|
+
// Get current script content using file validation
|
|
121
|
+
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
122
|
+
let scriptContent = '';
|
|
123
|
+
if (skillFile) {
|
|
124
|
+
scriptContent = skillFile.content;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.warn(`ā ļø No valid script file found for metadata update: ${skillIdn}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Create skill object with updated metadata
|
|
131
|
+
const skillObject = {
|
|
132
|
+
id: updatedMetadata.id,
|
|
133
|
+
title: updatedMetadata.title,
|
|
134
|
+
idn: updatedMetadata.idn,
|
|
135
|
+
prompt_script: scriptContent,
|
|
136
|
+
runner_type: updatedMetadata.runner_type,
|
|
137
|
+
model: updatedMetadata.model,
|
|
138
|
+
parameters: updatedMetadata.parameters,
|
|
139
|
+
path: updatedMetadata.path || undefined
|
|
140
|
+
};
|
|
141
|
+
await updateSkill(client, skillObject);
|
|
142
|
+
console.log(`ā Pushed metadata update for skill: ${skillIdn} (${updatedMetadata.title})`);
|
|
143
|
+
newHashes[metadataPath] = h;
|
|
144
|
+
pushed++;
|
|
145
|
+
metadataChanged = true;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error(`ā Failed to push metadata for ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (verbose)
|
|
157
|
+
console.log(`š Scanned ${scanned} files, found ${pushed} changes`);
|
|
158
|
+
// Regenerate flows.yaml if metadata was changed
|
|
159
|
+
if (metadataChanged) {
|
|
160
|
+
if (verbose)
|
|
161
|
+
console.log(`š Regenerating flows.yaml due to metadata changes...`);
|
|
162
|
+
const flowsYamlContent = await generateFlowsYaml({ projects }, customer.idn, verbose);
|
|
163
|
+
// Update hash for flows.yaml
|
|
164
|
+
const flowsYamlFilePath = flowsYamlPath(customer.idn);
|
|
165
|
+
newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
|
|
166
|
+
}
|
|
167
|
+
// Save updated hashes
|
|
168
|
+
await saveHashes(newHashes, customer.idn);
|
|
169
|
+
console.log(pushed ? `${pushed} file(s) pushed.` : 'No changes to push.');
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RunnerType } from '../types.js';
|
|
2
|
+
export interface SkillFile {
|
|
3
|
+
filePath: string;
|
|
4
|
+
fileName: string;
|
|
5
|
+
extension: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SkillFileValidation {
|
|
9
|
+
isValid: boolean;
|
|
10
|
+
files: SkillFile[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
errors: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the correct file extension for a runner type
|
|
16
|
+
*/
|
|
17
|
+
export declare function getExtensionForRunner(runnerType: RunnerType): string;
|
|
18
|
+
/**
|
|
19
|
+
* Generate IDN-based script file path
|
|
20
|
+
*/
|
|
21
|
+
export declare function getIdnBasedScriptPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string, runnerType: RunnerType): string;
|
|
22
|
+
/**
|
|
23
|
+
* Find all script files in a skill folder
|
|
24
|
+
*/
|
|
25
|
+
export declare function findSkillScriptFiles(skillFolderPath: string): Promise<SkillFile[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Validate skill folder has exactly one script file
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateSkillFolder(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string): Promise<SkillFileValidation>;
|
|
30
|
+
/**
|
|
31
|
+
* Get the single skill script file (if valid)
|
|
32
|
+
*/
|
|
33
|
+
export declare function getSingleSkillFile(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string): Promise<SkillFile | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Check if skill script content is different from target content
|
|
36
|
+
*/
|
|
37
|
+
export declare function isContentDifferent(existingContent: string, newContent: string): boolean;
|
|
38
|
+
export type OverwriteChoice = 'yes' | 'no' | 'all' | 'quit';
|
|
39
|
+
/**
|
|
40
|
+
* Interactive overwrite confirmation
|
|
41
|
+
*/
|
|
42
|
+
export declare function askForOverwrite(skillIdn: string, existingFile: string, newFile: string): Promise<OverwriteChoice>;
|
|
43
|
+
//# sourceMappingURL=skill-files.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Get the correct file extension for a runner type
|
|
10
|
+
*/
|
|
11
|
+
export function getExtensionForRunner(runnerType) {
|
|
12
|
+
switch (runnerType) {
|
|
13
|
+
case 'guidance':
|
|
14
|
+
return 'guidance';
|
|
15
|
+
case 'nsl':
|
|
16
|
+
return 'jinja';
|
|
17
|
+
default:
|
|
18
|
+
return 'guidance';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate IDN-based script file path
|
|
23
|
+
*/
|
|
24
|
+
export function getIdnBasedScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType) {
|
|
25
|
+
const extension = getExtensionForRunner(runnerType);
|
|
26
|
+
const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
27
|
+
return path.join(folderPath, `${skillIdn}.${extension}`);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Find all script files in a skill folder
|
|
31
|
+
*/
|
|
32
|
+
export async function findSkillScriptFiles(skillFolderPath) {
|
|
33
|
+
if (!(await fs.pathExists(skillFolderPath))) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const files = await fs.readdir(skillFolderPath);
|
|
37
|
+
const scriptFiles = [];
|
|
38
|
+
for (const fileName of files) {
|
|
39
|
+
const filePath = path.join(skillFolderPath, fileName);
|
|
40
|
+
const stats = await fs.stat(filePath);
|
|
41
|
+
if (stats.isFile()) {
|
|
42
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
43
|
+
// Check for script file extensions
|
|
44
|
+
if (['.jinja', '.guidance', '.nsl'].includes(ext)) {
|
|
45
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
46
|
+
scriptFiles.push({
|
|
47
|
+
filePath,
|
|
48
|
+
fileName,
|
|
49
|
+
extension: ext.slice(1), // Remove the dot
|
|
50
|
+
content
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return scriptFiles;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate skill folder has exactly one script file
|
|
59
|
+
*/
|
|
60
|
+
export async function validateSkillFolder(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn) {
|
|
61
|
+
const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
62
|
+
const files = await findSkillScriptFiles(folderPath);
|
|
63
|
+
const warnings = [];
|
|
64
|
+
const errors = [];
|
|
65
|
+
if (files.length === 0) {
|
|
66
|
+
errors.push(`No script files found in skill folder: ${skillIdn}`);
|
|
67
|
+
}
|
|
68
|
+
else if (files.length > 1) {
|
|
69
|
+
errors.push(`Multiple script files found in skill ${skillIdn}: ${files.map(f => f.fileName).join(', ')}`);
|
|
70
|
+
warnings.push(`Only one script file allowed per skill. Remove extra files and keep one.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
isValid: files.length === 1,
|
|
74
|
+
files,
|
|
75
|
+
warnings,
|
|
76
|
+
errors
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get the single skill script file (if valid)
|
|
81
|
+
*/
|
|
82
|
+
export async function getSingleSkillFile(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn) {
|
|
83
|
+
const validation = await validateSkillFolder(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
84
|
+
if (validation.isValid && validation.files.length === 1) {
|
|
85
|
+
return validation.files[0];
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if skill script content is different from target content
|
|
91
|
+
*/
|
|
92
|
+
export function isContentDifferent(existingContent, newContent) {
|
|
93
|
+
return sha256(existingContent.trim()) !== sha256(newContent.trim());
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Interactive overwrite confirmation
|
|
97
|
+
*/
|
|
98
|
+
export async function askForOverwrite(skillIdn, existingFile, newFile) {
|
|
99
|
+
const readline = await import('readline');
|
|
100
|
+
const rl = readline.createInterface({
|
|
101
|
+
input: process.stdin,
|
|
102
|
+
output: process.stdout
|
|
103
|
+
});
|
|
104
|
+
console.log(`\nā ļø File exists for skill ${skillIdn}:`);
|
|
105
|
+
console.log(` Existing: ${existingFile}`);
|
|
106
|
+
console.log(` New: ${newFile}`);
|
|
107
|
+
const answer = await new Promise((resolve) => {
|
|
108
|
+
rl.question('Overwrite? (y)es/(n)o/(a)ll/(q)uit: ', resolve);
|
|
109
|
+
});
|
|
110
|
+
rl.close();
|
|
111
|
+
const choice = answer.toLowerCase().trim();
|
|
112
|
+
if (choice === 'q' || choice === 'quit') {
|
|
113
|
+
return 'quit';
|
|
114
|
+
}
|
|
115
|
+
if (choice === 'a' || choice === 'all') {
|
|
116
|
+
return 'all';
|
|
117
|
+
}
|
|
118
|
+
if (choice === 'y' || choice === 'yes') {
|
|
119
|
+
return 'yes';
|
|
120
|
+
}
|
|
121
|
+
return 'no';
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=skill-files.js.map
|