newo 3.7.4 → 3.7.5
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 +15 -2
- package/README.md +70 -2
- package/dist/cli/commands/get-skill.d.ts +3 -0
- package/dist/cli/commands/get-skill.js +72 -0
- package/dist/cli/commands/help.js +29 -0
- package/dist/cli/commands/logs.d.ts +6 -1
- package/dist/cli/commands/logs.js +62 -14
- package/dist/cli/commands/sandbox.d.ts +10 -4
- package/dist/cli/commands/sandbox.js +182 -51
- package/dist/cli/commands/update-skill.d.ts +4 -0
- package/dist/cli/commands/update-skill.js +119 -0
- package/dist/cli.js +8 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +3 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +67 -19
- package/dist/sandbox/chat.d.ts +23 -3
- package/dist/sandbox/chat.js +83 -30
- package/dist/sync/remote-skill.d.ts +33 -0
- package/dist/sync/remote-skill.js +52 -0
- package/package.json +1 -1
- package/src/cli/commands/get-skill.ts +84 -0
- package/src/cli/commands/help.ts +29 -0
- package/src/cli/commands/logs.ts +83 -15
- package/src/cli/commands/sandbox.ts +238 -60
- package/src/cli/commands/update-skill.ts +139 -0
- package/src/cli.ts +10 -0
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +101 -20
- package/src/sandbox/chat.ts +106 -29
- package/src/sync/remote-skill.ts +92 -0
|
@@ -1,21 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sandbox Chat Command Handler
|
|
3
3
|
* Supports both single-command and interactive modes
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx newo sandbox "Hello" --customer <idn> # Single message mode
|
|
7
|
+
* npx newo sandbox --actor <actor_id> "Follow up" # Continue existing chat
|
|
8
|
+
* npx newo sandbox "ping" --connector vibe_agent # Select specific connector (v3.7.5)
|
|
9
|
+
* npx newo sandbox --list-connectors # Show running sandbox connectors (v3.7.5)
|
|
10
|
+
* npx newo sandbox --file ./msg.txt --actor <id> # Message from file (v3.7.5)
|
|
11
|
+
* cat msg.txt | npx newo sandbox --stdin # Message from stdin (v3.7.5)
|
|
12
|
+
* npx newo sandbox "ping" --timeout 420 # Custom response timeout in seconds (v3.7.5)
|
|
13
|
+
* npx newo sandbox "ping" --json # Machine-readable output (v3.7.5)
|
|
4
14
|
*/
|
|
15
|
+
import fs from 'fs-extra';
|
|
5
16
|
import { makeClient } from '../../api.js';
|
|
6
17
|
import { getValidAccessToken } from '../../auth.js';
|
|
7
18
|
import { selectSingleCustomer } from '../customer-selection.js';
|
|
8
19
|
import { getChatHistory } from '../../api.js';
|
|
9
|
-
import { findSandboxConnector, createChatSession, sendMessage, pollForResponse, extractAgentMessages, formatDebugInfo } from '../../sandbox/chat.js';
|
|
20
|
+
import { findSandboxConnector, listRunningSandboxConnectors, createChatSession, sendMessage, pollForResponse, extractAgentMessages, formatDebugInfo } from '../../sandbox/chat.js';
|
|
21
|
+
const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
22
|
+
/**
|
|
23
|
+
* Normalize an act's external_event_id: the chat-history converter falls back
|
|
24
|
+
* to the placeholder 'chat_history' when the API omits the field.
|
|
25
|
+
*/
|
|
26
|
+
function actEventId(act) {
|
|
27
|
+
if (!act)
|
|
28
|
+
return null;
|
|
29
|
+
const id = act.external_event_id;
|
|
30
|
+
return id && id !== 'chat_history' ? id : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read message text from --file, --stdin, or positional argument
|
|
34
|
+
*/
|
|
35
|
+
async function resolveMessage(args) {
|
|
36
|
+
if (args.file) {
|
|
37
|
+
const filePath = String(args.file);
|
|
38
|
+
if (!(await fs.pathExists(filePath))) {
|
|
39
|
+
throw new Error(`Message file not found: ${filePath}`);
|
|
40
|
+
}
|
|
41
|
+
return await fs.readFile(filePath, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
if (args.stdin) {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for await (const chunk of process.stdin) {
|
|
46
|
+
chunks.push(Buffer.from(chunk));
|
|
47
|
+
}
|
|
48
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
49
|
+
}
|
|
50
|
+
const messageArg = args._[1];
|
|
51
|
+
return messageArg === undefined ? null : String(messageArg);
|
|
52
|
+
}
|
|
10
53
|
/**
|
|
11
54
|
* Handle sandbox command
|
|
12
|
-
* Usage:
|
|
13
|
-
* npx newo sandbox "Hello" --customer <idn> # Single message mode
|
|
14
|
-
* npx newo sandbox --actor <actor_id> "Follow up" # Continue existing chat
|
|
15
|
-
* npx newo sandbox --interactive # Interactive mode (TBD)
|
|
16
55
|
*/
|
|
17
56
|
export async function handleSandboxCommand(customerConfig, args, verbose) {
|
|
18
|
-
const
|
|
57
|
+
const json = Boolean(args.json);
|
|
58
|
+
// --json implies quiet logging: stdout must stay machine-readable
|
|
59
|
+
const quiet = Boolean(args.quiet || args.q) || json;
|
|
19
60
|
// Save original console functions
|
|
20
61
|
const originalConsoleLog = console.log;
|
|
21
62
|
const originalConsoleError = console.error;
|
|
@@ -47,6 +88,13 @@ export async function handleSandboxCommand(customerConfig, args, verbose) {
|
|
|
47
88
|
console.error = originalConsoleError;
|
|
48
89
|
console.warn = originalConsoleWarn;
|
|
49
90
|
}
|
|
91
|
+
const integrationIdn = args.integration ? String(args.integration) : undefined;
|
|
92
|
+
const connectorIdn = args.connector ? String(args.connector) : undefined;
|
|
93
|
+
// List running connectors and exit
|
|
94
|
+
if (args['list-connectors']) {
|
|
95
|
+
await listConnectorsCommand(client, integrationIdn, json);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
50
98
|
// Check for interactive mode
|
|
51
99
|
const interactive = args.interactive || args.i;
|
|
52
100
|
if (interactive) {
|
|
@@ -56,32 +104,42 @@ export async function handleSandboxCommand(customerConfig, args, verbose) {
|
|
|
56
104
|
}
|
|
57
105
|
process.exit(1);
|
|
58
106
|
}
|
|
107
|
+
const timeoutSeconds = args.timeout ? parseFloat(String(args.timeout)) : DEFAULT_TIMEOUT_SECONDS;
|
|
108
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
109
|
+
if (!quiet)
|
|
110
|
+
console.error(`❌ Invalid --timeout value: ${args.timeout} (expected positive number of seconds)`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const options = {
|
|
114
|
+
quiet,
|
|
115
|
+
json,
|
|
116
|
+
verbose: quiet ? false : verbose,
|
|
117
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
118
|
+
integrationIdn,
|
|
119
|
+
connectorIdn
|
|
120
|
+
};
|
|
59
121
|
// Check if continuing existing chat
|
|
60
122
|
const actorId = args.actor;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!messageArg) {
|
|
123
|
+
const message = await resolveMessage(args);
|
|
124
|
+
if (message === null) {
|
|
64
125
|
if (!quiet) {
|
|
65
126
|
console.log('❌ Message is required');
|
|
66
|
-
console.log('Usage: npx newo sandbox "your message" [--actor <id>]');
|
|
67
|
-
console.log(' or: npx newo sandbox --actor <id>
|
|
127
|
+
console.log('Usage: npx newo sandbox "your message" [--actor <id>] [--connector <idn>]');
|
|
128
|
+
console.log(' or: npx newo sandbox --file <path> [--actor <id>]');
|
|
129
|
+
console.log(' or: cat msg.txt | npx newo sandbox --stdin [--actor <id>]');
|
|
68
130
|
}
|
|
69
131
|
process.exit(1);
|
|
70
132
|
}
|
|
71
|
-
// Convert to string (minimist may parse numbers)
|
|
72
|
-
const message = String(messageArg);
|
|
73
133
|
if (message.trim() === '') {
|
|
74
134
|
if (!quiet)
|
|
75
135
|
console.log('❌ Message cannot be empty');
|
|
76
136
|
process.exit(1);
|
|
77
137
|
}
|
|
78
138
|
if (actorId) {
|
|
79
|
-
|
|
80
|
-
await continueExistingChat(client, actorId, message, verbose, quiet, originalConsoleLog, originalConsoleError, originalConsoleWarn);
|
|
139
|
+
await continueExistingChat(client, actorId, message, options, originalConsoleLog);
|
|
81
140
|
}
|
|
82
141
|
else {
|
|
83
|
-
|
|
84
|
-
await startNewChat(client, message, verbose, quiet, originalConsoleLog, originalConsoleError, originalConsoleWarn);
|
|
142
|
+
await startNewChat(client, message, options, originalConsoleLog);
|
|
85
143
|
}
|
|
86
144
|
}
|
|
87
145
|
catch (error) {
|
|
@@ -107,23 +165,78 @@ export async function handleSandboxCommand(customerConfig, args, verbose) {
|
|
|
107
165
|
}
|
|
108
166
|
}
|
|
109
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Print running connectors of the (sandbox) integration
|
|
170
|
+
*/
|
|
171
|
+
async function listConnectorsCommand(client, integrationIdn, asJson) {
|
|
172
|
+
const connectors = await listRunningSandboxConnectors(client, integrationIdn);
|
|
173
|
+
if (asJson) {
|
|
174
|
+
console.log(JSON.stringify(connectors.map(c => ({
|
|
175
|
+
connector_idn: c.connector_idn,
|
|
176
|
+
integration_idn: c.integration_idn,
|
|
177
|
+
title: c.title,
|
|
178
|
+
status: c.status
|
|
179
|
+
})), null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (connectors.length === 0) {
|
|
183
|
+
console.log(`No running connectors found in integration '${integrationIdn || 'sandbox'}'`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log(`🔌 Running connectors in integration '${integrationIdn || 'sandbox'}':\n`);
|
|
187
|
+
for (const c of connectors) {
|
|
188
|
+
console.log(` ${c.connector_idn}${c.title ? ` (${c.title})` : ''}`);
|
|
189
|
+
}
|
|
190
|
+
console.log(`\n💡 Use: npx newo sandbox "your message" --connector <connector_idn>`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Build and print the --json result object
|
|
194
|
+
*/
|
|
195
|
+
function printJsonResult(session, acts, userAct, elapsedMs, print) {
|
|
196
|
+
const agentAct = acts.find(a => a.is_agent) || null;
|
|
197
|
+
const jsonResult = {
|
|
198
|
+
actor_id: session.user_actor_id,
|
|
199
|
+
persona_id: session.user_persona_id !== 'unknown' ? session.user_persona_id : null,
|
|
200
|
+
connector_idn: session.connector_idn,
|
|
201
|
+
// external_event_id of the user turn is the correlation key for `newo logs --event-id`
|
|
202
|
+
external_event_id: actEventId(userAct),
|
|
203
|
+
user_external_event_id: actEventId(userAct),
|
|
204
|
+
agent_external_event_id: actEventId(agentAct),
|
|
205
|
+
response: agentAct ? (agentAct.source_text || agentAct.original_text || null) : null,
|
|
206
|
+
elapsed_ms: elapsedMs,
|
|
207
|
+
timed_out: agentAct === null,
|
|
208
|
+
flow_idn: agentAct && agentAct.flow_idn !== 'unknown' ? agentAct.flow_idn : null,
|
|
209
|
+
skill_idn: agentAct && agentAct.skill_idn !== 'unknown' ? agentAct.skill_idn : null,
|
|
210
|
+
session_id: agentAct && agentAct.session_id !== 'unknown' ? agentAct.session_id : null
|
|
211
|
+
};
|
|
212
|
+
print(JSON.stringify(jsonResult, null, 2));
|
|
213
|
+
}
|
|
110
214
|
/**
|
|
111
215
|
* Start a new sandbox chat and send a message
|
|
112
216
|
*/
|
|
113
|
-
async function startNewChat(client, message,
|
|
217
|
+
async function startNewChat(client, message, options, originalConsoleLog) {
|
|
218
|
+
const { quiet, json, verbose, timeoutMs } = options;
|
|
114
219
|
if (!quiet)
|
|
115
220
|
console.log('🔧 Starting new sandbox chat...\n');
|
|
116
|
-
// Find sandbox connector
|
|
117
|
-
const
|
|
221
|
+
// Find sandbox connector (throws with available list when --connector not found)
|
|
222
|
+
const selection = {};
|
|
223
|
+
if (options.integrationIdn)
|
|
224
|
+
selection.integrationIdn = options.integrationIdn;
|
|
225
|
+
if (options.connectorIdn)
|
|
226
|
+
selection.connectorIdn = options.connectorIdn;
|
|
227
|
+
const connector = await findSandboxConnector(client, verbose, selection);
|
|
118
228
|
if (!connector) {
|
|
119
229
|
if (!quiet) {
|
|
120
230
|
console.error('❌ No running sandbox connector found');
|
|
121
231
|
console.error(' Please ensure you have a sandbox connector configured in your NEWO project');
|
|
122
232
|
}
|
|
233
|
+
else if (json) {
|
|
234
|
+
originalConsoleLog(JSON.stringify({ error: 'No running sandbox connector found' }));
|
|
235
|
+
}
|
|
123
236
|
process.exit(1);
|
|
124
237
|
}
|
|
125
238
|
// Create chat session
|
|
126
|
-
const session = await createChatSession(client, connector,
|
|
239
|
+
const session = await createChatSession(client, connector, verbose);
|
|
127
240
|
if (!quiet) {
|
|
128
241
|
console.log(`\n📋 Chat Session Created:`);
|
|
129
242
|
console.log(` Chat ID (actor_id): ${session.user_actor_id}`);
|
|
@@ -132,14 +245,20 @@ async function startNewChat(client, message, verbose, quiet = false, originalCon
|
|
|
132
245
|
console.log(` External ID: ${session.external_id}\n`);
|
|
133
246
|
console.log(`📤 You: ${message}\n`);
|
|
134
247
|
}
|
|
135
|
-
else {
|
|
248
|
+
else if (!json) {
|
|
136
249
|
// In quiet mode, output Chat ID FIRST to stdout
|
|
137
250
|
originalConsoleLog(`CHAT_ID:${session.user_actor_id}`);
|
|
138
251
|
originalConsoleLog(`You: ${message}`);
|
|
139
252
|
}
|
|
140
|
-
const
|
|
253
|
+
const startedAt = Date.now();
|
|
254
|
+
const sentAt = await sendMessage(client, session, message, verbose);
|
|
141
255
|
// Poll for response
|
|
142
|
-
const { acts, agentPersonaId } = await pollForResponse(client, session, sentAt,
|
|
256
|
+
const { acts, agentPersonaId, userAct } = await pollForResponse(client, session, sentAt, verbose, timeoutMs);
|
|
257
|
+
const elapsedMs = Date.now() - startedAt;
|
|
258
|
+
if (json) {
|
|
259
|
+
printJsonResult(session, acts, userAct, elapsedMs, originalConsoleLog);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
143
262
|
if (acts.length === 0) {
|
|
144
263
|
if (!quiet) {
|
|
145
264
|
console.log('⏱️ No response received within timeout period');
|
|
@@ -174,38 +293,40 @@ async function startNewChat(client, message, verbose, quiet = false, originalCon
|
|
|
174
293
|
if (quiet) {
|
|
175
294
|
return; // Exit early, showing only messages
|
|
176
295
|
}
|
|
177
|
-
// Display debug information
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
296
|
+
// Display debug information
|
|
297
|
+
if (verbose) {
|
|
298
|
+
console.log('\n📊 Debug Information:');
|
|
299
|
+
console.log(formatDebugInfo(acts));
|
|
300
|
+
console.log('');
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Show condensed debug info for single-command mode
|
|
304
|
+
console.log('📊 Debug Summary:');
|
|
305
|
+
const agentActs = acts.filter(a => a.is_agent);
|
|
306
|
+
if (agentActs.length > 0) {
|
|
307
|
+
const lastAct = agentActs[agentActs.length - 1];
|
|
308
|
+
if (lastAct) {
|
|
309
|
+
console.log(` Flow: ${lastAct.flow_idn || 'N/A'}`);
|
|
310
|
+
console.log(` Skill: ${lastAct.skill_idn || 'N/A'}`);
|
|
311
|
+
console.log(` Session: ${lastAct.session_id}`);
|
|
312
|
+
if (actEventId(userAct)) {
|
|
313
|
+
console.log(` Event ID (user turn): ${actEventId(userAct)}`);
|
|
194
314
|
}
|
|
195
|
-
console.log(` Acts Processed: ${acts.length} (${agentActs.length} agent, ${acts.length - agentActs.length} system)`);
|
|
196
315
|
}
|
|
197
|
-
console.log(
|
|
316
|
+
console.log(` Acts Processed: ${acts.length} (${agentActs.length} agent, ${acts.length - agentActs.length} system)`);
|
|
198
317
|
}
|
|
199
|
-
// Show continuation info
|
|
200
|
-
console.log(`💡 To continue this conversation:`);
|
|
201
|
-
console.log(` npx newo sandbox --actor ${session.user_actor_id} "your next message"`);
|
|
202
318
|
console.log('');
|
|
203
319
|
}
|
|
320
|
+
// Show continuation info
|
|
321
|
+
console.log(`💡 To continue this conversation:`);
|
|
322
|
+
console.log(` npx newo sandbox --actor ${session.user_actor_id} "your next message"`);
|
|
323
|
+
console.log('');
|
|
204
324
|
}
|
|
205
325
|
/**
|
|
206
326
|
* Continue an existing sandbox chat
|
|
207
327
|
*/
|
|
208
|
-
async function continueExistingChat(client, actorId, message,
|
|
328
|
+
async function continueExistingChat(client, actorId, message, options, originalConsoleLog) {
|
|
329
|
+
const { quiet, json, verbose, timeoutMs } = options;
|
|
209
330
|
if (!quiet) {
|
|
210
331
|
console.log(`💬 Continuing chat...`);
|
|
211
332
|
console.log(` Chat ID: ${actorId}\n`);
|
|
@@ -232,20 +353,27 @@ async function continueExistingChat(client, actorId, message, verbose, quiet = f
|
|
|
232
353
|
user_actor_id: actorId,
|
|
233
354
|
user_persona_id: 'unknown', // Not needed for continuation
|
|
234
355
|
agent_persona_id: null,
|
|
235
|
-
connector_idn: 'sandbox',
|
|
356
|
+
connector_idn: options.connectorIdn || 'sandbox',
|
|
236
357
|
session_id: null,
|
|
237
358
|
external_id: 'continuation'
|
|
238
359
|
};
|
|
239
360
|
// Send message (use original console in quiet mode)
|
|
240
361
|
if (quiet) {
|
|
241
|
-
|
|
362
|
+
if (!json)
|
|
363
|
+
originalConsoleLog(`You: ${message}`);
|
|
242
364
|
}
|
|
243
365
|
else {
|
|
244
366
|
console.log(`📤 You: ${message}\n`);
|
|
245
367
|
}
|
|
246
|
-
const
|
|
368
|
+
const startedAt = Date.now();
|
|
369
|
+
const sentAt = await sendMessage(client, session, message, verbose);
|
|
247
370
|
// Poll for response using timestamp-based filtering
|
|
248
|
-
const { acts } = await pollForResponse(client, session, sentAt,
|
|
371
|
+
const { acts, userAct } = await pollForResponse(client, session, sentAt, verbose, timeoutMs);
|
|
372
|
+
const elapsedMs = Date.now() - startedAt;
|
|
373
|
+
if (json) {
|
|
374
|
+
printJsonResult(session, acts, userAct, elapsedMs, originalConsoleLog);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
249
377
|
if (acts.length === 0) {
|
|
250
378
|
if (!quiet) {
|
|
251
379
|
console.log('⏱️ No response received within timeout period');
|
|
@@ -292,6 +420,9 @@ async function continueExistingChat(client, actorId, message, verbose, quiet = f
|
|
|
292
420
|
console.log(` Flow: ${lastAct.flow_idn || 'N/A'}`);
|
|
293
421
|
console.log(` Skill: ${lastAct.skill_idn || 'N/A'}`);
|
|
294
422
|
console.log(` Session: ${lastAct.session_id}`);
|
|
423
|
+
if (actEventId(userAct)) {
|
|
424
|
+
console.log(` Event ID (user turn): ${actEventId(userAct)}`);
|
|
425
|
+
}
|
|
295
426
|
}
|
|
296
427
|
console.log(` Acts Processed: ${acts.length} (${agentActs.length} agent, ${acts.length - agentActs.length} user)`);
|
|
297
428
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MultiCustomerConfig, CliArgs } from '../../types.js';
|
|
2
|
+
export declare function findLocalProjectWorkspace(customerIdn: string, projectIdn: string): Promise<string | null>;
|
|
3
|
+
export declare function handleUpdateSkillCommand(customerConfig: MultiCustomerConfig, args: CliArgs, verbose?: boolean): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=update-skill.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Skill Command Handler - Point edit of a skill directly on the platform
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* newo update-skill <skill-idn> --project <idn> --agent <idn> --flow <idn> \
|
|
6
|
+
* [--model <provider_idn>/<model_idn>] \
|
|
7
|
+
* [--script <file.nsl|file.guidance>] \
|
|
8
|
+
* [--publish] [--publish-description "<text>"]
|
|
9
|
+
*
|
|
10
|
+
* Unlike `newo push`, this changes exactly one skill without requiring a
|
|
11
|
+
* pulled workspace and without touching any other modified files. Typical
|
|
12
|
+
* use: temporarily switching a skill's model for an A/B test run.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import { requireSingleCustomer } from '../customer-selection.js';
|
|
16
|
+
import { makeClient, updateSkill, publishFlow } from '../../api.js';
|
|
17
|
+
import { getValidAccessToken } from '../../auth.js';
|
|
18
|
+
import { resolveRemoteSkill, parseModelFlag } from '../../sync/remote-skill.js';
|
|
19
|
+
import { projectDir } from '../../fsutil.js';
|
|
20
|
+
import { v2ProjectDir } from '../../format/paths-v2.js';
|
|
21
|
+
const USAGE = 'Usage: newo update-skill <skill-idn> --project <project-idn> --agent <agent-idn> --flow <flow-idn> [--model <provider>/<model>] [--script <file>] [--publish] [--publish-description "<text>"] [--customer <idn>]';
|
|
22
|
+
export async function findLocalProjectWorkspace(customerIdn, projectIdn) {
|
|
23
|
+
const candidates = [
|
|
24
|
+
projectDir(customerIdn, projectIdn),
|
|
25
|
+
v2ProjectDir(customerIdn, projectIdn)
|
|
26
|
+
];
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
if (await fs.pathExists(candidate)) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export async function handleUpdateSkillCommand(customerConfig, args, verbose = false) {
|
|
35
|
+
const skillIdn = args._[1];
|
|
36
|
+
const projectIdn = args.project;
|
|
37
|
+
const agentIdn = args.agent;
|
|
38
|
+
const flowIdn = args.flow;
|
|
39
|
+
const modelFlag = args.model;
|
|
40
|
+
const scriptFlag = args.script;
|
|
41
|
+
const shouldPublish = Boolean(args.publish);
|
|
42
|
+
const publishDescription = args['publish-description'];
|
|
43
|
+
if (!skillIdn || !projectIdn || !agentIdn || !flowIdn) {
|
|
44
|
+
console.error('Error: skill IDN, --project, --agent and --flow are required');
|
|
45
|
+
console.error(USAGE);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
if (!modelFlag && !scriptFlag) {
|
|
49
|
+
console.error('Error: nothing to update — pass --model and/or --script');
|
|
50
|
+
console.error(USAGE);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const newModel = modelFlag ? parseModelFlag(String(modelFlag)) : null;
|
|
54
|
+
let newScript = null;
|
|
55
|
+
if (scriptFlag) {
|
|
56
|
+
const scriptPath = String(scriptFlag);
|
|
57
|
+
if (!(await fs.pathExists(scriptPath))) {
|
|
58
|
+
console.error(`Error: script file not found: ${scriptPath}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
newScript = await fs.readFile(scriptPath, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
const selectedCustomer = requireSingleCustomer(customerConfig, args.customer);
|
|
64
|
+
const token = await getValidAccessToken(selectedCustomer);
|
|
65
|
+
const client = await makeClient(verbose, token);
|
|
66
|
+
if (verbose)
|
|
67
|
+
console.log(`🔍 Resolving skill ${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}...`);
|
|
68
|
+
const { project, agent, flow, skill } = await resolveRemoteSkill(client, {
|
|
69
|
+
projectIdn,
|
|
70
|
+
agentIdn,
|
|
71
|
+
flowIdn,
|
|
72
|
+
skillIdn
|
|
73
|
+
});
|
|
74
|
+
// Build updated skill object, preserving everything we don't change
|
|
75
|
+
const updatedSkill = {
|
|
76
|
+
...skill,
|
|
77
|
+
...(newModel ? { model: newModel } : {}),
|
|
78
|
+
...(newScript !== null ? { prompt_script: newScript } : {})
|
|
79
|
+
};
|
|
80
|
+
console.log(`✏️ Updating skill: ${project.idn}/${agent.idn}/${flow.idn}/${skill.idn} (${skill.id})`);
|
|
81
|
+
if (newModel) {
|
|
82
|
+
console.log(` Model: ${skill.model.provider_idn}/${skill.model.model_idn} → ${newModel.provider_idn}/${newModel.model_idn}`);
|
|
83
|
+
}
|
|
84
|
+
if (newScript !== null) {
|
|
85
|
+
console.log(` Script: ${(skill.prompt_script || '').length} chars → ${newScript.length} chars (from ${scriptFlag})`);
|
|
86
|
+
}
|
|
87
|
+
await updateSkill(client, updatedSkill);
|
|
88
|
+
console.log('✅ Skill updated (draft)');
|
|
89
|
+
// Warn when a pulled local workspace exists: it now diverges from the platform
|
|
90
|
+
const localProjectDir = await findLocalProjectWorkspace(selectedCustomer.idn, project.idn);
|
|
91
|
+
if (localProjectDir) {
|
|
92
|
+
console.warn(`⚠️ Local workspace exists at ${localProjectDir} and now differs from the platform.`);
|
|
93
|
+
console.warn(` Run 'newo pull' to sync it, or remember to revert this change.`);
|
|
94
|
+
}
|
|
95
|
+
if (shouldPublish) {
|
|
96
|
+
const publishData = {
|
|
97
|
+
version: '1.0',
|
|
98
|
+
description: publishDescription || 'Published via NEWO CLI (update-skill)',
|
|
99
|
+
type: 'public'
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
await publishFlow(client, flow.id, publishData);
|
|
103
|
+
console.log(`🚀 Flow published: ${flow.idn}`);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
|
|
107
|
+
console.error(`❌ Failed to publish flow '${flow.idn}': ${errorMessage}`);
|
|
108
|
+
const errorDetails = error.response?.data?.reasons || error.response?.data?.errors || error.response?.data?.detail;
|
|
109
|
+
if (errorDetails) {
|
|
110
|
+
console.error(` Details: ${JSON.stringify(errorDetails)}`);
|
|
111
|
+
}
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(`💡 Changes are draft-only. Add --publish to publish flow '${flow.idn}'.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=update-skill.js.map
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,8 @@ import { handleDeleteAgentCommand } from './cli/commands/delete-agent.js';
|
|
|
21
21
|
import { handleCreateFlowCommand } from './cli/commands/create-flow.js';
|
|
22
22
|
import { handleDeleteFlowCommand } from './cli/commands/delete-flow.js';
|
|
23
23
|
import { handleCreateSkillCommand } from './cli/commands/create-skill.js';
|
|
24
|
+
import { handleGetSkillCommand } from './cli/commands/get-skill.js';
|
|
25
|
+
import { handleUpdateSkillCommand } from './cli/commands/update-skill.js';
|
|
24
26
|
import { handleDeleteSkillCommand } from './cli/commands/delete-skill.js';
|
|
25
27
|
import { handleCreateProjectCommand } from './cli/commands/create-project.js';
|
|
26
28
|
import { handleCreateCustomerCommand } from './cli/commands/create-customer.js';
|
|
@@ -161,6 +163,12 @@ async function main() {
|
|
|
161
163
|
case 'create-skill':
|
|
162
164
|
await handleCreateSkillCommand(customerConfig, args, verbose);
|
|
163
165
|
break;
|
|
166
|
+
case 'get-skill':
|
|
167
|
+
await handleGetSkillCommand(customerConfig, args, verbose);
|
|
168
|
+
break;
|
|
169
|
+
case 'update-skill':
|
|
170
|
+
await handleUpdateSkillCommand(customerConfig, args, verbose);
|
|
171
|
+
break;
|
|
164
172
|
case 'delete-skill':
|
|
165
173
|
await handleDeleteSkillCommand(customerConfig, args, verbose);
|
|
166
174
|
break;
|
|
@@ -104,6 +104,9 @@ export declare class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta,
|
|
|
104
104
|
* V2 path: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skill}.nsl
|
|
105
105
|
*/
|
|
106
106
|
private pushV2SkillUpdate;
|
|
107
|
+
private normalizePathForComparison;
|
|
108
|
+
private resolveV2SkillTargetForScriptPath;
|
|
109
|
+
private resolveV2SkillTargetFromCanonicalPath;
|
|
107
110
|
/**
|
|
108
111
|
* Push a V2 library skill update
|
|
109
112
|
* Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* skills/{SkillIdn}.nsl|.nslg
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
|
+
import path from 'path';
|
|
17
18
|
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, createSkill, createSkillParameter, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, getFlow, } from '../../../api.js';
|
|
18
19
|
import { syncFlowMetadata, emptyFlowSyncCounts, totalFlowSyncOps, describeFlowSyncCounts } from '../../../sync/flow-metadata.js';
|
|
19
20
|
import { ensureStateOnly, writeFileSafe, mapPath, } from '../../../fsutil.js';
|
|
@@ -460,7 +461,7 @@ export class V2ProjectSyncStrategy {
|
|
|
460
461
|
const isLibrary = change.path.includes('/libraries/');
|
|
461
462
|
const count = isLibrary
|
|
462
463
|
? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
|
|
463
|
-
: await this.pushV2SkillUpdate(client, change, mapData, newHashes);
|
|
464
|
+
: await this.pushV2SkillUpdate(client, change, mapData, newHashes, customer.idn);
|
|
464
465
|
result.updated += count;
|
|
465
466
|
}
|
|
466
467
|
}
|
|
@@ -818,23 +819,12 @@ export class V2ProjectSyncStrategy {
|
|
|
818
819
|
*
|
|
819
820
|
* V2 path: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skill}.nsl
|
|
820
821
|
*/
|
|
821
|
-
async pushV2SkillUpdate(client, change, mapData, newHashes) {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
828
|
-
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
829
|
-
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
830
|
-
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
831
|
-
// Look up skill in map
|
|
832
|
-
const projectData = mapData.projects[projectIdn];
|
|
833
|
-
const agentData = projectData?.agents[agentIdn];
|
|
834
|
-
const flowData = agentData?.flows[flowIdn];
|
|
835
|
-
const skillData = flowData?.skills[skillIdn];
|
|
836
|
-
if (!skillData) {
|
|
837
|
-
throw new Error(`Skill ${skillIdn} not found in project map (path: ${change.path})`);
|
|
822
|
+
async pushV2SkillUpdate(client, change, mapData, newHashes, customerIdn) {
|
|
823
|
+
const target = await this.resolveV2SkillTargetForScriptPath(customerIdn, change.path, mapData) ||
|
|
824
|
+
this.resolveV2SkillTargetFromCanonicalPath(change.path, mapData);
|
|
825
|
+
const skillData = target?.skillData;
|
|
826
|
+
if (!target || !skillData) {
|
|
827
|
+
throw new Error(`Skill not found in project map (path: ${change.path})`);
|
|
838
828
|
}
|
|
839
829
|
// Read updated script content
|
|
840
830
|
const content = await fs.readFile(change.path, 'utf8');
|
|
@@ -850,9 +840,67 @@ export class V2ProjectSyncStrategy {
|
|
|
850
840
|
path: skillData.path
|
|
851
841
|
});
|
|
852
842
|
newHashes[change.path] = sha256(content);
|
|
853
|
-
this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
|
|
843
|
+
this.logger.info(`[newo_v2] Pushed: ${target.skillIdn}`);
|
|
854
844
|
return 1;
|
|
855
845
|
}
|
|
846
|
+
normalizePathForComparison(filePath) {
|
|
847
|
+
return path.resolve(filePath).replace(/\\/g, '/');
|
|
848
|
+
}
|
|
849
|
+
async resolveV2SkillTargetForScriptPath(customerIdn, scriptPath, mapData) {
|
|
850
|
+
const normalizedScriptPath = this.normalizePathForComparison(scriptPath);
|
|
851
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
852
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
853
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
854
|
+
const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
|
|
855
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
let flowDef;
|
|
859
|
+
try {
|
|
860
|
+
flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
for (const skill of flowDef.skills || []) {
|
|
866
|
+
const runnerType = this.normalizeRunnerType(skill.runner_type || flowData.skills[skill.idn]?.runner_type);
|
|
867
|
+
const resolvedScriptPath = await this.resolveV2FlowSkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skill.idn, runnerType, skill.prompt_script);
|
|
868
|
+
if (this.normalizePathForComparison(resolvedScriptPath) === normalizedScriptPath) {
|
|
869
|
+
return {
|
|
870
|
+
projectIdn,
|
|
871
|
+
agentIdn,
|
|
872
|
+
flowIdn,
|
|
873
|
+
skillIdn: skill.idn,
|
|
874
|
+
skillData: flowData.skills[skill.idn]
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
resolveV2SkillTargetFromCanonicalPath(scriptPath, mapData) {
|
|
884
|
+
// Parse canonical V2 path:
|
|
885
|
+
// .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
|
|
886
|
+
const pathParts = scriptPath.split('/');
|
|
887
|
+
const skillFileName = pathParts[pathParts.length - 1] || '';
|
|
888
|
+
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
889
|
+
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
890
|
+
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
891
|
+
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
892
|
+
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
893
|
+
const projectData = mapData.projects[projectIdn];
|
|
894
|
+
const agentData = projectData?.agents[agentIdn];
|
|
895
|
+
const flowData = agentData?.flows[flowIdn];
|
|
896
|
+
return {
|
|
897
|
+
projectIdn,
|
|
898
|
+
agentIdn,
|
|
899
|
+
flowIdn,
|
|
900
|
+
skillIdn,
|
|
901
|
+
skillData: flowData?.skills[skillIdn]
|
|
902
|
+
};
|
|
903
|
+
}
|
|
856
904
|
/**
|
|
857
905
|
* Push a V2 library skill update
|
|
858
906
|
* Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
|