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,15 +1,27 @@
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
  */
5
15
 
6
- import type { MultiCustomerConfig, CliArgs } from '../../types.js';
16
+ import fs from 'fs-extra';
17
+ import type { MultiCustomerConfig, CliArgs, ConversationAct, SandboxChatSession } from '../../types.js';
7
18
  import { makeClient } from '../../api.js';
8
19
  import { getValidAccessToken } from '../../auth.js';
9
20
  import { selectSingleCustomer } from '../customer-selection.js';
10
21
  import { getChatHistory } from '../../api.js';
11
22
  import {
12
23
  findSandboxConnector,
24
+ listRunningSandboxConnectors,
13
25
  createChatSession,
14
26
  sendMessage,
15
27
  pollForResponse,
@@ -17,19 +29,77 @@ import {
17
29
  formatDebugInfo
18
30
  } from '../../sandbox/chat.js';
19
31
 
32
+ const DEFAULT_TIMEOUT_SECONDS = 60;
33
+
34
+ interface SandboxOptions {
35
+ quiet: boolean;
36
+ json: boolean;
37
+ verbose: boolean;
38
+ timeoutMs: number;
39
+ integrationIdn: string | undefined;
40
+ connectorIdn: string | undefined;
41
+ }
42
+
43
+ interface SandboxJsonResult {
44
+ actor_id: string;
45
+ persona_id: string | null;
46
+ connector_idn: string;
47
+ external_event_id: string | null;
48
+ user_external_event_id: string | null;
49
+ agent_external_event_id: string | null;
50
+ response: string | null;
51
+ elapsed_ms: number;
52
+ timed_out: boolean;
53
+ flow_idn: string | null;
54
+ skill_idn: string | null;
55
+ session_id: string | null;
56
+ }
57
+
58
+ /**
59
+ * Normalize an act's external_event_id: the chat-history converter falls back
60
+ * to the placeholder 'chat_history' when the API omits the field.
61
+ */
62
+ function actEventId(act: ConversationAct | null | undefined): string | null {
63
+ if (!act) return null;
64
+ const id = act.external_event_id;
65
+ return id && id !== 'chat_history' ? id : null;
66
+ }
67
+
68
+ /**
69
+ * Read message text from --file, --stdin, or positional argument
70
+ */
71
+ async function resolveMessage(args: CliArgs): Promise<string | null> {
72
+ if (args.file) {
73
+ const filePath = String(args.file);
74
+ if (!(await fs.pathExists(filePath))) {
75
+ throw new Error(`Message file not found: ${filePath}`);
76
+ }
77
+ return await fs.readFile(filePath, 'utf8');
78
+ }
79
+
80
+ if (args.stdin) {
81
+ const chunks: Buffer[] = [];
82
+ for await (const chunk of process.stdin) {
83
+ chunks.push(Buffer.from(chunk));
84
+ }
85
+ return Buffer.concat(chunks).toString('utf8');
86
+ }
87
+
88
+ const messageArg = args._[1];
89
+ return messageArg === undefined ? null : String(messageArg);
90
+ }
91
+
20
92
  /**
21
93
  * Handle sandbox command
22
- * Usage:
23
- * npx newo sandbox "Hello" --customer <idn> # Single message mode
24
- * npx newo sandbox --actor <actor_id> "Follow up" # Continue existing chat
25
- * npx newo sandbox --interactive # Interactive mode (TBD)
26
94
  */
27
95
  export async function handleSandboxCommand(
28
96
  customerConfig: MultiCustomerConfig,
29
97
  args: CliArgs,
30
98
  verbose: boolean
31
99
  ): Promise<void> {
32
- const quiet: boolean = Boolean(args.quiet || args.q);
100
+ const json: boolean = Boolean(args.json);
101
+ // --json implies quiet logging: stdout must stay machine-readable
102
+ const quiet: boolean = Boolean(args.quiet || args.q) || json;
33
103
 
34
104
  // Save original console functions
35
105
  const originalConsoleLog = console.log;
@@ -68,6 +138,15 @@ export async function handleSandboxCommand(
68
138
  console.warn = originalConsoleWarn;
69
139
  }
70
140
 
141
+ const integrationIdn = args.integration ? String(args.integration) : undefined;
142
+ const connectorIdn = args.connector ? String(args.connector) : undefined;
143
+
144
+ // List running connectors and exit
145
+ if (args['list-connectors']) {
146
+ await listConnectorsCommand(client, integrationIdn, json);
147
+ return;
148
+ }
149
+
71
150
  // Check for interactive mode
72
151
  const interactive = args.interactive || args.i;
73
152
  if (interactive) {
@@ -78,33 +157,44 @@ export async function handleSandboxCommand(
78
157
  process.exit(1);
79
158
  }
80
159
 
160
+ const timeoutSeconds = args.timeout ? parseFloat(String(args.timeout)) : DEFAULT_TIMEOUT_SECONDS;
161
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
162
+ if (!quiet) console.error(`❌ Invalid --timeout value: ${args.timeout} (expected positive number of seconds)`);
163
+ process.exit(1);
164
+ }
165
+
166
+ const options: SandboxOptions = {
167
+ quiet,
168
+ json,
169
+ verbose: quiet ? false : verbose,
170
+ timeoutMs: timeoutSeconds * 1000,
171
+ integrationIdn,
172
+ connectorIdn
173
+ };
174
+
81
175
  // Check if continuing existing chat
82
176
  const actorId = args.actor as string | undefined;
83
177
 
84
- // Extract message from arguments (position depends on whether --actor is used)
85
- const messageArg = args._[1];
86
- if (!messageArg) {
178
+ const message = await resolveMessage(args);
179
+ if (message === null) {
87
180
  if (!quiet) {
88
181
  console.log('❌ Message is required');
89
- console.log('Usage: npx newo sandbox "your message" [--actor <id>]');
90
- console.log(' or: npx newo sandbox --actor <id> "your message"');
182
+ console.log('Usage: npx newo sandbox "your message" [--actor <id>] [--connector <idn>]');
183
+ console.log(' or: npx newo sandbox --file <path> [--actor <id>]');
184
+ console.log(' or: cat msg.txt | npx newo sandbox --stdin [--actor <id>]');
91
185
  }
92
186
  process.exit(1);
93
187
  }
94
188
 
95
- // Convert to string (minimist may parse numbers)
96
- const message = String(messageArg);
97
189
  if (message.trim() === '') {
98
190
  if (!quiet) console.log('❌ Message cannot be empty');
99
191
  process.exit(1);
100
192
  }
101
193
 
102
194
  if (actorId) {
103
- // Continue existing chat
104
- await continueExistingChat(client, actorId, message, verbose, quiet, originalConsoleLog, originalConsoleError, originalConsoleWarn);
195
+ await continueExistingChat(client, actorId, message, options, originalConsoleLog);
105
196
  } else {
106
- // Start new chat
107
- await startNewChat(client, message, verbose, quiet, originalConsoleLog, originalConsoleError, originalConsoleWarn);
197
+ await startNewChat(client, message, options, originalConsoleLog);
108
198
  }
109
199
 
110
200
  } catch (error: any) {
@@ -131,32 +221,103 @@ export async function handleSandboxCommand(
131
221
  }
132
222
  }
133
223
 
224
+ /**
225
+ * Print running connectors of the (sandbox) integration
226
+ */
227
+ async function listConnectorsCommand(
228
+ client: any,
229
+ integrationIdn: string | undefined,
230
+ asJson: boolean
231
+ ): Promise<void> {
232
+ const connectors = await listRunningSandboxConnectors(client, integrationIdn);
233
+
234
+ if (asJson) {
235
+ console.log(JSON.stringify(
236
+ connectors.map(c => ({
237
+ connector_idn: c.connector_idn,
238
+ integration_idn: c.integration_idn,
239
+ title: c.title,
240
+ status: c.status
241
+ })),
242
+ null,
243
+ 2
244
+ ));
245
+ return;
246
+ }
247
+
248
+ if (connectors.length === 0) {
249
+ console.log(`No running connectors found in integration '${integrationIdn || 'sandbox'}'`);
250
+ return;
251
+ }
252
+
253
+ console.log(`🔌 Running connectors in integration '${integrationIdn || 'sandbox'}':\n`);
254
+ for (const c of connectors) {
255
+ console.log(` ${c.connector_idn}${c.title ? ` (${c.title})` : ''}`);
256
+ }
257
+ console.log(`\n💡 Use: npx newo sandbox "your message" --connector <connector_idn>`);
258
+ }
259
+
260
+ /**
261
+ * Build and print the --json result object
262
+ */
263
+ function printJsonResult(
264
+ session: SandboxChatSession,
265
+ acts: ConversationAct[],
266
+ userAct: ConversationAct | null,
267
+ elapsedMs: number,
268
+ print: typeof console.log
269
+ ): void {
270
+ const agentAct = acts.find(a => a.is_agent) || null;
271
+
272
+ const jsonResult: SandboxJsonResult = {
273
+ actor_id: session.user_actor_id,
274
+ persona_id: session.user_persona_id !== 'unknown' ? session.user_persona_id : null,
275
+ connector_idn: session.connector_idn,
276
+ // external_event_id of the user turn is the correlation key for `newo logs --event-id`
277
+ external_event_id: actEventId(userAct),
278
+ user_external_event_id: actEventId(userAct),
279
+ agent_external_event_id: actEventId(agentAct),
280
+ response: agentAct ? (agentAct.source_text || agentAct.original_text || null) : null,
281
+ elapsed_ms: elapsedMs,
282
+ timed_out: agentAct === null,
283
+ flow_idn: agentAct && agentAct.flow_idn !== 'unknown' ? agentAct.flow_idn : null,
284
+ skill_idn: agentAct && agentAct.skill_idn !== 'unknown' ? agentAct.skill_idn : null,
285
+ session_id: agentAct && agentAct.session_id !== 'unknown' ? agentAct.session_id : null
286
+ };
287
+
288
+ print(JSON.stringify(jsonResult, null, 2));
289
+ }
290
+
134
291
  /**
135
292
  * Start a new sandbox chat and send a message
136
293
  */
137
294
  async function startNewChat(
138
295
  client: any,
139
296
  message: string,
140
- verbose: boolean,
141
- quiet: boolean = false,
142
- originalConsoleLog: typeof console.log = console.log,
143
- _originalConsoleError: typeof console.error = console.error,
144
- _originalConsoleWarn: typeof console.warn = console.warn
297
+ options: SandboxOptions,
298
+ originalConsoleLog: typeof console.log
145
299
  ): Promise<void> {
300
+ const { quiet, json, verbose, timeoutMs } = options;
301
+
146
302
  if (!quiet) console.log('🔧 Starting new sandbox chat...\n');
147
303
 
148
- // Find sandbox connector
149
- const connector = await findSandboxConnector(client, quiet ? false : verbose);
304
+ // Find sandbox connector (throws with available list when --connector not found)
305
+ const selection: { integrationIdn?: string; connectorIdn?: string } = {};
306
+ if (options.integrationIdn) selection.integrationIdn = options.integrationIdn;
307
+ if (options.connectorIdn) selection.connectorIdn = options.connectorIdn;
308
+ const connector = await findSandboxConnector(client, verbose, selection);
150
309
  if (!connector) {
151
310
  if (!quiet) {
152
311
  console.error('❌ No running sandbox connector found');
153
312
  console.error(' Please ensure you have a sandbox connector configured in your NEWO project');
313
+ } else if (json) {
314
+ originalConsoleLog(JSON.stringify({ error: 'No running sandbox connector found' }));
154
315
  }
155
316
  process.exit(1);
156
317
  }
157
318
 
158
319
  // Create chat session
159
- const session = await createChatSession(client, connector, quiet ? false : verbose);
320
+ const session = await createChatSession(client, connector, verbose);
160
321
 
161
322
  if (!quiet) {
162
323
  console.log(`\n📋 Chat Session Created:`);
@@ -165,16 +326,23 @@ async function startNewChat(
165
326
  console.log(` Connector: ${session.connector_idn}`);
166
327
  console.log(` External ID: ${session.external_id}\n`);
167
328
  console.log(`📤 You: ${message}\n`);
168
- } else {
329
+ } else if (!json) {
169
330
  // In quiet mode, output Chat ID FIRST to stdout
170
331
  originalConsoleLog(`CHAT_ID:${session.user_actor_id}`);
171
332
  originalConsoleLog(`You: ${message}`);
172
333
  }
173
334
 
174
- const sentAt = await sendMessage(client, session, message, quiet ? false : verbose);
335
+ const startedAt = Date.now();
336
+ const sentAt = await sendMessage(client, session, message, verbose);
175
337
 
176
338
  // Poll for response
177
- const { acts, agentPersonaId } = await pollForResponse(client, session, sentAt, quiet ? false : verbose);
339
+ const { acts, agentPersonaId, userAct } = await pollForResponse(client, session, sentAt, verbose, timeoutMs);
340
+ const elapsedMs = Date.now() - startedAt;
341
+
342
+ if (json) {
343
+ printJsonResult(session, acts, userAct, elapsedMs, originalConsoleLog);
344
+ return;
345
+ }
178
346
 
179
347
  if (acts.length === 0) {
180
348
  if (!quiet) {
@@ -215,33 +383,34 @@ async function startNewChat(
215
383
  return; // Exit early, showing only messages
216
384
  }
217
385
 
218
- // Display debug information (skip in quiet mode)
219
- if (!quiet) {
220
- if (verbose) {
221
- console.log('\n📊 Debug Information:');
222
- console.log(formatDebugInfo(acts));
223
- console.log('');
224
- } else {
225
- // Show condensed debug info for single-command mode
226
- console.log('📊 Debug Summary:');
227
- const agentActs = acts.filter(a => a.is_agent);
228
- if (agentActs.length > 0) {
229
- const lastAct = agentActs[agentActs.length - 1];
230
- if (lastAct) {
231
- console.log(` Flow: ${lastAct.flow_idn || 'N/A'}`);
232
- console.log(` Skill: ${lastAct.skill_idn || 'N/A'}`);
233
- console.log(` Session: ${lastAct.session_id}`);
386
+ // Display debug information
387
+ if (verbose) {
388
+ console.log('\n📊 Debug Information:');
389
+ console.log(formatDebugInfo(acts));
390
+ console.log('');
391
+ } else {
392
+ // Show condensed debug info for single-command mode
393
+ console.log('📊 Debug Summary:');
394
+ const agentActs = acts.filter(a => a.is_agent);
395
+ if (agentActs.length > 0) {
396
+ const lastAct = agentActs[agentActs.length - 1];
397
+ if (lastAct) {
398
+ console.log(` Flow: ${lastAct.flow_idn || 'N/A'}`);
399
+ console.log(` Skill: ${lastAct.skill_idn || 'N/A'}`);
400
+ console.log(` Session: ${lastAct.session_id}`);
401
+ if (actEventId(userAct)) {
402
+ console.log(` Event ID (user turn): ${actEventId(userAct)}`);
234
403
  }
235
- console.log(` Acts Processed: ${acts.length} (${agentActs.length} agent, ${acts.length - agentActs.length} system)`);
236
404
  }
237
- console.log('');
405
+ console.log(` Acts Processed: ${acts.length} (${agentActs.length} agent, ${acts.length - agentActs.length} system)`);
238
406
  }
239
-
240
- // Show continuation info
241
- console.log(`💡 To continue this conversation:`);
242
- console.log(` npx newo sandbox --actor ${session.user_actor_id} "your next message"`);
243
407
  console.log('');
244
408
  }
409
+
410
+ // Show continuation info
411
+ console.log(`💡 To continue this conversation:`);
412
+ console.log(` npx newo sandbox --actor ${session.user_actor_id} "your next message"`);
413
+ console.log('');
245
414
  }
246
415
 
247
416
  /**
@@ -251,12 +420,11 @@ async function continueExistingChat(
251
420
  client: any,
252
421
  actorId: string,
253
422
  message: string,
254
- verbose: boolean,
255
- quiet: boolean = false,
256
- originalConsoleLog: typeof console.log = console.log,
257
- _originalConsoleError: typeof console.error = console.error,
258
- _originalConsoleWarn: typeof console.warn = console.warn
423
+ options: SandboxOptions,
424
+ originalConsoleLog: typeof console.log
259
425
  ): Promise<void> {
426
+ const { quiet, json, verbose, timeoutMs } = options;
427
+
260
428
  if (!quiet) {
261
429
  console.log(`💬 Continuing chat...`);
262
430
  console.log(` Chat ID: ${actorId}\n`);
@@ -283,25 +451,32 @@ async function continueExistingChat(
283
451
  }
284
452
 
285
453
  // Create a temporary session for the existing chat
286
- const session: any = {
454
+ const session: SandboxChatSession = {
287
455
  user_actor_id: actorId,
288
456
  user_persona_id: 'unknown', // Not needed for continuation
289
457
  agent_persona_id: null,
290
- connector_idn: 'sandbox',
458
+ connector_idn: options.connectorIdn || 'sandbox',
291
459
  session_id: null,
292
460
  external_id: 'continuation'
293
461
  };
294
462
 
295
463
  // Send message (use original console in quiet mode)
296
464
  if (quiet) {
297
- originalConsoleLog(`You: ${message}`);
465
+ if (!json) originalConsoleLog(`You: ${message}`);
298
466
  } else {
299
467
  console.log(`📤 You: ${message}\n`);
300
468
  }
301
- const sentAt = await sendMessage(client, session, message, quiet ? false : verbose);
469
+ const startedAt = Date.now();
470
+ const sentAt = await sendMessage(client, session, message, verbose);
302
471
 
303
472
  // Poll for response using timestamp-based filtering
304
- const { acts } = await pollForResponse(client, session, sentAt, quiet ? false : verbose);
473
+ const { acts, userAct } = await pollForResponse(client, session, sentAt, verbose, timeoutMs);
474
+ const elapsedMs = Date.now() - startedAt;
475
+
476
+ if (json) {
477
+ printJsonResult(session, acts, userAct, elapsedMs, originalConsoleLog);
478
+ return;
479
+ }
305
480
 
306
481
  if (acts.length === 0) {
307
482
  if (!quiet) {
@@ -351,6 +526,9 @@ async function continueExistingChat(
351
526
  console.log(` Flow: ${lastAct.flow_idn || 'N/A'}`);
352
527
  console.log(` Skill: ${lastAct.skill_idn || 'N/A'}`);
353
528
  console.log(` Session: ${lastAct.session_id}`);
529
+ if (actEventId(userAct)) {
530
+ console.log(` Event ID (user turn): ${actEventId(userAct)}`);
531
+ }
354
532
  }
355
533
  console.log(` Acts Processed: ${acts.length} (${agentActs.length} agent, ${acts.length - agentActs.length} user)`);
356
534
  }
@@ -0,0 +1,139 @@
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
+ import type { MultiCustomerConfig, CliArgs, Skill, PublishFlowRequest } from '../../types.js';
22
+
23
+ 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>]';
24
+
25
+ export async function findLocalProjectWorkspace(customerIdn: string, projectIdn: string): Promise<string | null> {
26
+ const candidates = [
27
+ projectDir(customerIdn, projectIdn),
28
+ v2ProjectDir(customerIdn, projectIdn)
29
+ ];
30
+
31
+ for (const candidate of candidates) {
32
+ if (await fs.pathExists(candidate)) {
33
+ return candidate;
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ export async function handleUpdateSkillCommand(
41
+ customerConfig: MultiCustomerConfig,
42
+ args: CliArgs,
43
+ verbose: boolean = false
44
+ ): Promise<void> {
45
+ const skillIdn = args._[1] as string | undefined;
46
+ const projectIdn = args.project as string | undefined;
47
+ const agentIdn = args.agent as string | undefined;
48
+ const flowIdn = args.flow as string | undefined;
49
+ const modelFlag = args.model as string | undefined;
50
+ const scriptFlag = args.script as string | undefined;
51
+ const shouldPublish = Boolean(args.publish);
52
+ const publishDescription = args['publish-description'] as string | undefined;
53
+
54
+ if (!skillIdn || !projectIdn || !agentIdn || !flowIdn) {
55
+ console.error('Error: skill IDN, --project, --agent and --flow are required');
56
+ console.error(USAGE);
57
+ process.exit(1);
58
+ }
59
+
60
+ if (!modelFlag && !scriptFlag) {
61
+ console.error('Error: nothing to update — pass --model and/or --script');
62
+ console.error(USAGE);
63
+ process.exit(1);
64
+ }
65
+
66
+ const newModel = modelFlag ? parseModelFlag(String(modelFlag)) : null;
67
+
68
+ let newScript: string | null = null;
69
+ if (scriptFlag) {
70
+ const scriptPath = String(scriptFlag);
71
+ if (!(await fs.pathExists(scriptPath))) {
72
+ console.error(`Error: script file not found: ${scriptPath}`);
73
+ process.exit(1);
74
+ }
75
+ newScript = await fs.readFile(scriptPath, 'utf8');
76
+ }
77
+
78
+ const selectedCustomer = requireSingleCustomer(customerConfig, args.customer as string | undefined);
79
+
80
+ const token = await getValidAccessToken(selectedCustomer);
81
+ const client = await makeClient(verbose, token);
82
+
83
+ if (verbose) console.log(`🔍 Resolving skill ${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}...`);
84
+
85
+ const { project, agent, flow, skill } = await resolveRemoteSkill(client, {
86
+ projectIdn,
87
+ agentIdn,
88
+ flowIdn,
89
+ skillIdn
90
+ });
91
+
92
+ // Build updated skill object, preserving everything we don't change
93
+ const updatedSkill: Skill = {
94
+ ...skill,
95
+ ...(newModel ? { model: newModel } : {}),
96
+ ...(newScript !== null ? { prompt_script: newScript } : {})
97
+ };
98
+
99
+ console.log(`✏️ Updating skill: ${project.idn}/${agent.idn}/${flow.idn}/${skill.idn} (${skill.id})`);
100
+ if (newModel) {
101
+ console.log(` Model: ${skill.model.provider_idn}/${skill.model.model_idn} → ${newModel.provider_idn}/${newModel.model_idn}`);
102
+ }
103
+ if (newScript !== null) {
104
+ console.log(` Script: ${(skill.prompt_script || '').length} chars → ${newScript.length} chars (from ${scriptFlag})`);
105
+ }
106
+
107
+ await updateSkill(client, updatedSkill);
108
+ console.log('✅ Skill updated (draft)');
109
+
110
+ // Warn when a pulled local workspace exists: it now diverges from the platform
111
+ const localProjectDir = await findLocalProjectWorkspace(selectedCustomer.idn, project.idn);
112
+ if (localProjectDir) {
113
+ console.warn(`⚠️ Local workspace exists at ${localProjectDir} and now differs from the platform.`);
114
+ console.warn(` Run 'newo pull' to sync it, or remember to revert this change.`);
115
+ }
116
+
117
+ if (shouldPublish) {
118
+ const publishData: PublishFlowRequest = {
119
+ version: '1.0',
120
+ description: publishDescription || 'Published via NEWO CLI (update-skill)',
121
+ type: 'public'
122
+ };
123
+
124
+ try {
125
+ await publishFlow(client, flow.id, publishData);
126
+ console.log(`🚀 Flow published: ${flow.idn}`);
127
+ } catch (error: any) {
128
+ const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
129
+ console.error(`❌ Failed to publish flow '${flow.idn}': ${errorMessage}`);
130
+ const errorDetails = error.response?.data?.reasons || error.response?.data?.errors || error.response?.data?.detail;
131
+ if (errorDetails) {
132
+ console.error(` Details: ${JSON.stringify(errorDetails)}`);
133
+ }
134
+ process.exit(1);
135
+ }
136
+ } else {
137
+ console.log(`💡 Changes are draft-only. Add --publish to publish flow '${flow.idn}'.`);
138
+ }
139
+ }
package/src/cli.ts 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';
@@ -185,6 +187,14 @@ async function main(): Promise<void> {
185
187
  await handleCreateSkillCommand(customerConfig, args, verbose);
186
188
  break;
187
189
 
190
+ case 'get-skill':
191
+ await handleGetSkillCommand(customerConfig, args, verbose);
192
+ break;
193
+
194
+ case 'update-skill':
195
+ await handleUpdateSkillCommand(customerConfig, args, verbose);
196
+ break;
197
+
188
198
  case 'delete-skill':
189
199
  await handleDeleteSkillCommand(customerConfig, args, verbose);
190
200
  break;