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.
@@ -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 quiet = Boolean(args.quiet || args.q);
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
- // Extract message from arguments (position depends on whether --actor is used)
62
- const messageArg = args._[1];
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> "your message"');
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
- // Continue existing chat
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
- // Start new chat
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, verbose, quiet = false, originalConsoleLog = console.log, _originalConsoleError = console.error, _originalConsoleWarn = console.warn) {
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 connector = await findSandboxConnector(client, quiet ? false : verbose);
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, quiet ? false : verbose);
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 sentAt = await sendMessage(client, session, message, quiet ? false : verbose);
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, quiet ? false : verbose);
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 (skip in quiet mode)
178
- if (!quiet) {
179
- if (verbose) {
180
- console.log('\n📊 Debug Information:');
181
- console.log(formatDebugInfo(acts));
182
- console.log('');
183
- }
184
- else {
185
- // Show condensed debug info for single-command mode
186
- console.log('📊 Debug Summary:');
187
- const agentActs = acts.filter(a => a.is_agent);
188
- if (agentActs.length > 0) {
189
- const lastAct = agentActs[agentActs.length - 1];
190
- if (lastAct) {
191
- console.log(` Flow: ${lastAct.flow_idn || 'N/A'}`);
192
- console.log(` Skill: ${lastAct.skill_idn || 'N/A'}`);
193
- console.log(` Session: ${lastAct.session_id}`);
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, verbose, quiet = false, originalConsoleLog = console.log, _originalConsoleError = console.error, _originalConsoleWarn = console.warn) {
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
- originalConsoleLog(`You: ${message}`);
362
+ if (!json)
363
+ originalConsoleLog(`You: ${message}`);
242
364
  }
243
365
  else {
244
366
  console.log(`📤 You: ${message}\n`);
245
367
  }
246
- const sentAt = await sendMessage(client, session, message, quiet ? false : verbose);
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, quiet ? false : verbose);
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
- // Parse V2 path to extract entity hierarchy
823
- // Path: .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
824
- const pathParts = change.path.split('/');
825
- const skillFileName = pathParts[pathParts.length - 1] || '';
826
- const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
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}