newo 3.7.3 → 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.
@@ -23,7 +23,17 @@ import type {
23
23
  const SANDBOX_INTEGRATION_IDN = 'sandbox';
24
24
  const DEFAULT_TIMEZONE = 'America/Los_Angeles';
25
25
  const POLL_INTERVAL_MS = 1000; // 1 second
26
- const MAX_POLL_ATTEMPTS = 60; // Max 60 seconds wait
26
+ const DEFAULT_TIMEOUT_MS = 60_000; // Default max wait for agent response
27
+
28
+ /**
29
+ * Options for selecting which connector to chat through
30
+ */
31
+ export interface ConnectorSelectionOptions {
32
+ /** Integration IDN to search connectors in (default: 'sandbox') */
33
+ integrationIdn?: string;
34
+ /** Exact connector_idn to use; when omitted, the first running connector is used */
35
+ connectorIdn?: string;
36
+ }
27
37
 
28
38
  /**
29
39
  * Generate a random external ID for chat session
@@ -41,41 +51,81 @@ function generatePersonaName(): string {
41
51
  }
42
52
 
43
53
  /**
44
- * Find a sandbox connector from the customer's connectors list
54
+ * List running connectors of an integration (default: sandbox).
55
+ * Used by `newo sandbox --list-connectors` and connector selection.
45
56
  */
46
- export async function findSandboxConnector(client: AxiosInstance, verbose: boolean = false): Promise<Connector | null> {
47
- if (verbose) console.log('🔍 Searching for sandbox integration...');
48
-
49
- // First, get all integrations to find the sandbox integration
57
+ export async function listRunningSandboxConnectors(
58
+ client: AxiosInstance,
59
+ integrationIdn: string = SANDBOX_INTEGRATION_IDN
60
+ ): Promise<Connector[]> {
50
61
  const integrations = await listIntegrations(client);
51
- const sandboxIntegration = integrations.find(i => i.idn === SANDBOX_INTEGRATION_IDN);
62
+ const integration = integrations.find(i => i.idn === integrationIdn);
52
63
 
53
- if (!sandboxIntegration) {
54
- if (verbose) console.log('❌ Sandbox integration not found');
55
- return null;
64
+ if (!integration) {
65
+ throw new Error(
66
+ `Integration '${integrationIdn}' not found. Available integrations: ${integrations.map(i => i.idn).join(', ') || '(none)'}`
67
+ );
56
68
  }
57
69
 
58
- if (verbose) console.log(`✓ Found sandbox integration: ${sandboxIntegration.id}`);
70
+ const connectors = await listConnectors(client, integration.id);
71
+ return connectors.filter(c => c.status === 'running');
72
+ }
59
73
 
60
- // Now get connectors for the sandbox integration
61
- if (verbose) console.log('🔍 Searching for sandbox connectors...');
62
- const connectors = await listConnectors(client, sandboxIntegration.id);
63
- const sandboxConnectors = connectors.filter(c => c.status === 'running');
74
+ /**
75
+ * Find a sandbox connector from the customer's connectors list.
76
+ *
77
+ * Without options, preserves legacy behavior: first running connector of the
78
+ * 'sandbox' integration. With options.connectorIdn, selects that exact
79
+ * connector and throws a descriptive error (listing available connectors)
80
+ * when it is not found or not running.
81
+ */
82
+ export async function findSandboxConnector(
83
+ client: AxiosInstance,
84
+ verbose: boolean = false,
85
+ options: ConnectorSelectionOptions = {}
86
+ ): Promise<Connector | null> {
87
+ const integrationIdn = options.integrationIdn || SANDBOX_INTEGRATION_IDN;
88
+
89
+ if (verbose) console.log(`🔍 Searching for ${integrationIdn} integration...`);
90
+
91
+ let runningConnectors: Connector[];
92
+ try {
93
+ runningConnectors = await listRunningSandboxConnectors(client, integrationIdn);
94
+ } catch (error) {
95
+ if (options.connectorIdn) throw error;
96
+ if (verbose) console.log(`❌ ${error instanceof Error ? error.message : String(error)}`);
97
+ return null;
98
+ }
64
99
 
65
- if (sandboxConnectors.length === 0) {
66
- if (verbose) console.log('❌ No running sandbox connectors found');
100
+ if (runningConnectors.length === 0) {
101
+ if (options.connectorIdn) {
102
+ throw new Error(`No running connectors found in integration '${integrationIdn}'`);
103
+ }
104
+ if (verbose) console.log(`❌ No running ${integrationIdn} connectors found`);
67
105
  return null;
68
106
  }
69
107
 
108
+ if (options.connectorIdn) {
109
+ const match = runningConnectors.find(c => c.connector_idn === options.connectorIdn);
110
+ if (!match) {
111
+ const available = runningConnectors.map(c => c.connector_idn).join(', ');
112
+ throw new Error(
113
+ `Connector '${options.connectorIdn}' not found among running connectors of integration '${integrationIdn}'. Available: ${available}`
114
+ );
115
+ }
116
+ if (verbose) console.log(`✓ Using connector: ${match.connector_idn}`);
117
+ return match;
118
+ }
119
+
70
120
  if (verbose) {
71
- console.log(`✓ Found ${sandboxConnectors.length} running sandbox connector(s)`);
72
- const firstConnector = sandboxConnectors[0];
121
+ console.log(`✓ Found ${runningConnectors.length} running ${integrationIdn} connector(s)`);
122
+ const firstConnector = runningConnectors[0];
73
123
  if (firstConnector) {
74
124
  console.log(` Using: ${firstConnector.connector_idn}`);
75
125
  }
76
126
  }
77
127
 
78
- return sandboxConnectors[0] || null;
128
+ return runningConnectors[0] || null;
79
129
  }
80
130
 
81
131
  /**
@@ -105,7 +155,7 @@ export async function createChatSession(
105
155
  const actorResponse = await createActor(client, personaResponse.id, {
106
156
  name: personaName,
107
157
  external_id: externalId,
108
- integration_idn: SANDBOX_INTEGRATION_IDN,
158
+ integration_idn: connector.integration_idn || SANDBOX_INTEGRATION_IDN,
109
159
  connector_idn: connector.connector_idn,
110
160
  time_zone_identifier: DEFAULT_TIMEZONE
111
161
  });
@@ -132,7 +182,10 @@ export async function sendMessage(
132
182
  text: string,
133
183
  verbose: boolean = false
134
184
  ): Promise<Date> {
135
- if (verbose) console.log(`💬 Sending message: "${text}"`);
185
+ if (verbose) {
186
+ const preview = text.length > 200 ? `${text.slice(0, 200)}… (${text.length} chars)` : text;
187
+ console.log(`💬 Sending message: "${preview}"`);
188
+ }
136
189
 
137
190
  const sentAt = new Date();
138
191
 
@@ -146,6 +199,17 @@ export async function sendMessage(
146
199
  return sentAt;
147
200
  }
148
201
 
202
+ /**
203
+ * Parse an act datetime that may lack timezone info (assume UTC)
204
+ */
205
+ function parseActDatetimeMs(datetime: string): number {
206
+ let d = datetime;
207
+ if (!d.endsWith('Z') && !d.includes('+') && !d.includes('-', 10)) {
208
+ d = d + 'Z';
209
+ }
210
+ return new Date(d).getTime();
211
+ }
212
+
149
213
  /**
150
214
  * Poll for new conversation acts (messages and debug info)
151
215
  * Continues polling until we get an agent response, not just any new message
@@ -154,20 +218,23 @@ export async function pollForResponse(
154
218
  client: AxiosInstance,
155
219
  session: SandboxChatSession,
156
220
  messageSentAt: Date | null = null,
157
- verbose: boolean = false
158
- ): Promise<{ acts: ConversationAct[]; agentPersonaId: string | null }> {
221
+ verbose: boolean = false,
222
+ timeoutMs: number = DEFAULT_TIMEOUT_MS
223
+ ): Promise<{ acts: ConversationAct[]; agentPersonaId: string | null; userAct: ConversationAct | null }> {
159
224
  let attempts = 0;
160
225
  let agentPersonaId = session.agent_persona_id;
226
+ let userAct: ConversationAct | null = null;
227
+ const maxPollAttempts = Math.max(1, Math.ceil(timeoutMs / POLL_INTERVAL_MS));
161
228
 
162
- if (verbose) console.log('⏳ Waiting for agent response...');
229
+ if (verbose) console.log(`⏳ Waiting for agent response (timeout: ${Math.round(timeoutMs / 1000)}s)...`);
163
230
 
164
231
  // Add small delay before first poll to allow message to be processed
165
232
  await new Promise(resolve => setTimeout(resolve, 500));
166
233
 
167
- while (attempts < MAX_POLL_ATTEMPTS) {
234
+ while (attempts < maxPollAttempts) {
168
235
  try {
169
236
  if (verbose && attempts % 5 === 0) {
170
- console.log(` [Poll attempt ${attempts + 1}/${MAX_POLL_ATTEMPTS}] Checking for messages...`);
237
+ console.log(` [Poll attempt ${attempts + 1}/${maxPollAttempts}] Checking for messages...`);
171
238
  }
172
239
 
173
240
  // Use Chat History API instead of acts API (doesn't require account_id)
@@ -221,6 +288,16 @@ export async function pollForResponse(
221
288
  }
222
289
  }
223
290
 
291
+ // Track the user act of our sent message (newest non-agent act at/after sentAt).
292
+ // Its external_event_id is the correlation key for `newo logs --event-id`.
293
+ for (const act of convertedActs) {
294
+ if (act.is_agent) continue;
295
+ if (messageSentAt && parseActDatetimeMs(act.datetime) - messageSentAt.getTime() <= -100) continue;
296
+ if (!userAct || parseActDatetimeMs(act.datetime) >= parseActDatetimeMs(userAct.datetime)) {
297
+ userAct = act;
298
+ }
299
+ }
300
+
224
301
  // Filter for agent messages that came AFTER our message was sent
225
302
  const agentMessages = convertedActs.filter(act => {
226
303
  if (!act.is_agent) return false;
@@ -262,7 +339,7 @@ export async function pollForResponse(
262
339
  // Return ONLY the single newest agent message (first one, since API returns newest first)
263
340
  const latestAgentMessage = agentMessages[0];
264
341
  if (latestAgentMessage) {
265
- return { acts: [latestAgentMessage], agentPersonaId };
342
+ return { acts: [latestAgentMessage], agentPersonaId, userAct };
266
343
  }
267
344
  } else if (verbose && attempts % 10 === 0) {
268
345
  console.log(` No new agent messages yet (checked ${response.items.length} total messages, sentAt: ${messageSentAt?.toISOString()}), continuing...`);
@@ -280,7 +357,7 @@ export async function pollForResponse(
280
357
  }
281
358
 
282
359
  if (verbose) console.log('⏱️ Timeout waiting for response');
283
- return { acts: [], agentPersonaId };
360
+ return { acts: [], agentPersonaId, userAct };
284
361
  }
285
362
 
286
363
  /**
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Remote skill resolution by IDN path (project/agent/flow/skill).
3
+ *
4
+ * Used by `newo get-skill` and `newo update-skill` to address a single skill
5
+ * on the platform without requiring a pulled local workspace.
6
+ */
7
+ import type { AxiosInstance } from 'axios';
8
+ import { listProjects, listAgents, listFlowSkills, getSkill } from '../api.js';
9
+ import type { ProjectMeta, Agent, Flow, Skill } from '../types.js';
10
+
11
+ export interface RemoteSkillPath {
12
+ projectIdn: string;
13
+ agentIdn: string;
14
+ flowIdn: string;
15
+ skillIdn: string;
16
+ }
17
+
18
+ export interface ResolvedRemoteSkill {
19
+ project: ProjectMeta;
20
+ agent: Agent;
21
+ flow: Flow;
22
+ skill: Skill;
23
+ }
24
+
25
+ /**
26
+ * Resolve a skill on the platform by its IDN path.
27
+ * Throws descriptive errors listing available IDNs at the failing level.
28
+ */
29
+ export async function resolveRemoteSkill(
30
+ client: AxiosInstance,
31
+ path: RemoteSkillPath
32
+ ): Promise<ResolvedRemoteSkill> {
33
+ const projects = await listProjects(client);
34
+ const project = projects.find(p => p.idn === path.projectIdn);
35
+ if (!project) {
36
+ throw new Error(
37
+ `Project '${path.projectIdn}' not found. Available projects: ${projects.map(p => p.idn).join(', ') || '(none)'}`
38
+ );
39
+ }
40
+
41
+ const agents = await listAgents(client, project.id);
42
+ const agent = agents.find(a => a.idn === path.agentIdn);
43
+ if (!agent) {
44
+ throw new Error(
45
+ `Agent '${path.agentIdn}' not found in project '${path.projectIdn}'. Available agents: ${agents.map(a => a.idn).join(', ') || '(none)'}`
46
+ );
47
+ }
48
+
49
+ const flows = agent.flows || [];
50
+ const flow = flows.find(f => f.idn === path.flowIdn);
51
+ if (!flow) {
52
+ throw new Error(
53
+ `Flow '${path.flowIdn}' not found in agent '${path.agentIdn}'. Available flows: ${flows.map(f => f.idn).join(', ') || '(none)'}`
54
+ );
55
+ }
56
+
57
+ const skills = await listFlowSkills(client, flow.id);
58
+ const skillStub = skills.find(s => s.idn === path.skillIdn);
59
+ if (!skillStub) {
60
+ throw new Error(
61
+ `Skill '${path.skillIdn}' not found in flow '${path.flowIdn}'. Available skills: ${skills.map(s => s.idn).join(', ') || '(none)'}`
62
+ );
63
+ }
64
+
65
+ // The flow-skills list endpoint already returns full skills including
66
+ // prompt_script (pull relies on this). Only fall back to the by-id
67
+ // endpoint when prompt_script is absent — and tolerate a 404 there,
68
+ // since /api/v1/designer/skills/{id} is not available on all accounts.
69
+ let skill = skillStub;
70
+ if (skill.prompt_script === undefined || skill.prompt_script === null) {
71
+ try {
72
+ skill = await getSkill(client, skillStub.id);
73
+ } catch {
74
+ skill = skillStub;
75
+ }
76
+ }
77
+
78
+ return { project, agent, flow, skill };
79
+ }
80
+
81
+ /**
82
+ * Parse a `--model provider_idn/model_idn` flag value
83
+ */
84
+ export function parseModelFlag(value: string): { provider_idn: string; model_idn: string } {
85
+ const parts = value.split('/');
86
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
87
+ throw new Error(
88
+ `Invalid --model value '${value}'. Expected format: <provider_idn>/<model_idn> (e.g. openai/gpt4o)`
89
+ );
90
+ }
91
+ return { provider_idn: parts[0], model_idn: parts[1] };
92
+ }