newo 3.0.0 → 3.1.0

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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Sandbox Chat Utility Module
3
+ * Handles chat session management, message sending, and polling for responses
4
+ */
5
+ import { randomBytes } from 'crypto';
6
+ import { listIntegrations, listConnectors, createSandboxPersona, createActor, sendChatMessage, getChatHistory } from '../api.js';
7
+ const SANDBOX_INTEGRATION_IDN = 'sandbox';
8
+ const DEFAULT_TIMEZONE = 'America/Los_Angeles';
9
+ const POLL_INTERVAL_MS = 1000; // 1 second
10
+ const MAX_POLL_ATTEMPTS = 60; // Max 60 seconds wait
11
+ /**
12
+ * Generate a random external ID for chat session
13
+ */
14
+ function generateExternalId() {
15
+ return randomBytes(3).toString('hex');
16
+ }
17
+ /**
18
+ * Generate a unique persona name with NEWO CLI prefix
19
+ */
20
+ function generatePersonaName() {
21
+ const guid = randomBytes(8).toString('hex');
22
+ return `newo-cli-${guid}`;
23
+ }
24
+ /**
25
+ * Find a sandbox connector from the customer's connectors list
26
+ */
27
+ export async function findSandboxConnector(client, verbose = false) {
28
+ if (verbose)
29
+ console.log('🔍 Searching for sandbox integration...');
30
+ // First, get all integrations to find the sandbox integration
31
+ const integrations = await listIntegrations(client);
32
+ const sandboxIntegration = integrations.find(i => i.idn === SANDBOX_INTEGRATION_IDN);
33
+ if (!sandboxIntegration) {
34
+ if (verbose)
35
+ console.log('❌ Sandbox integration not found');
36
+ return null;
37
+ }
38
+ if (verbose)
39
+ console.log(`✓ Found sandbox integration: ${sandboxIntegration.id}`);
40
+ // Now get connectors for the sandbox integration
41
+ if (verbose)
42
+ console.log('🔍 Searching for sandbox connectors...');
43
+ const connectors = await listConnectors(client, sandboxIntegration.id);
44
+ const sandboxConnectors = connectors.filter(c => c.status === 'running');
45
+ if (sandboxConnectors.length === 0) {
46
+ if (verbose)
47
+ console.log('❌ No running sandbox connectors found');
48
+ return null;
49
+ }
50
+ if (verbose) {
51
+ console.log(`✓ Found ${sandboxConnectors.length} running sandbox connector(s)`);
52
+ const firstConnector = sandboxConnectors[0];
53
+ if (firstConnector) {
54
+ console.log(` Using: ${firstConnector.connector_idn}`);
55
+ }
56
+ }
57
+ return sandboxConnectors[0] || null;
58
+ }
59
+ /**
60
+ * Create a new sandbox chat session
61
+ */
62
+ export async function createChatSession(client, connector, verbose = false) {
63
+ const personaName = generatePersonaName();
64
+ const externalId = generateExternalId();
65
+ if (verbose)
66
+ console.log(`📝 Creating persona: ${personaName}`);
67
+ // Create user persona
68
+ const personaResponse = await createSandboxPersona(client, {
69
+ name: personaName,
70
+ title: personaName
71
+ });
72
+ if (verbose)
73
+ console.log(`✓ Persona created: ${personaResponse.id}`);
74
+ // Create actor (ties persona to sandbox connector)
75
+ if (verbose)
76
+ console.log(`🔗 Creating actor for ${connector.connector_idn}...`);
77
+ const actorResponse = await createActor(client, personaResponse.id, {
78
+ name: personaName,
79
+ external_id: externalId,
80
+ integration_idn: SANDBOX_INTEGRATION_IDN,
81
+ connector_idn: connector.connector_idn,
82
+ time_zone_identifier: DEFAULT_TIMEZONE
83
+ });
84
+ if (verbose)
85
+ console.log(`✓ Actor created: ${actorResponse.id} (Chat ID)`);
86
+ return {
87
+ user_persona_id: personaResponse.id,
88
+ user_actor_id: actorResponse.id,
89
+ agent_persona_id: null, // Will be populated from first response
90
+ connector_idn: connector.connector_idn,
91
+ session_id: null,
92
+ external_id: externalId
93
+ };
94
+ }
95
+ /**
96
+ * Send a message in the chat session
97
+ * Returns the timestamp when message was sent (for filtering responses)
98
+ */
99
+ export async function sendMessage(client, session, text, verbose = false) {
100
+ if (verbose)
101
+ console.log(`💬 Sending message: "${text}"`);
102
+ const sentAt = new Date();
103
+ await sendChatMessage(client, session.user_actor_id, {
104
+ text,
105
+ arguments: []
106
+ });
107
+ if (verbose)
108
+ console.log('✓ Message sent');
109
+ return sentAt;
110
+ }
111
+ /**
112
+ * Poll for new conversation acts (messages and debug info)
113
+ * Continues polling until we get an agent response, not just any new message
114
+ */
115
+ export async function pollForResponse(client, session, messageSentAt = null, verbose = false) {
116
+ let attempts = 0;
117
+ let agentPersonaId = session.agent_persona_id;
118
+ if (verbose)
119
+ console.log('⏳ Waiting for agent response...');
120
+ // Add small delay before first poll to allow message to be processed
121
+ await new Promise(resolve => setTimeout(resolve, 500));
122
+ while (attempts < MAX_POLL_ATTEMPTS) {
123
+ try {
124
+ if (verbose && attempts % 5 === 0) {
125
+ console.log(` [Poll attempt ${attempts + 1}/${MAX_POLL_ATTEMPTS}] Checking for messages...`);
126
+ }
127
+ // Use Chat History API instead of acts API (doesn't require account_id)
128
+ const response = await getChatHistory(client, {
129
+ user_actor_id: session.user_actor_id,
130
+ page: 1,
131
+ per: 100
132
+ });
133
+ if (verbose && attempts === 0) {
134
+ console.log(` Initial poll returned ${response.items.length} message(s)`);
135
+ }
136
+ if (response.items && response.items.length > 0) {
137
+ // Convert chat history format to acts format
138
+ const convertedActs = response.items.map((item) => ({
139
+ id: item.id || `chat_${Math.random()}`,
140
+ command_act_id: null,
141
+ external_event_id: item.external_event_id || 'chat_history',
142
+ arguments: item.arguments || [],
143
+ reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
144
+ runtime_context_id: item.runtime_context_id || 'chat_history',
145
+ source_text: item.payload?.text || item.message || item.content || item.text || '',
146
+ original_text: item.payload?.text || item.message || item.content || item.text || '',
147
+ datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
148
+ user_actor_id: session.user_actor_id,
149
+ agent_actor_id: item.agent_actor_id || null,
150
+ user_persona_id: session.user_persona_id,
151
+ user_persona_name: 'User',
152
+ agent_persona_id: item.agent_persona_id || agentPersonaId || 'unknown',
153
+ external_id: item.external_id || null,
154
+ integration_idn: 'sandbox',
155
+ connector_idn: session.connector_idn,
156
+ to_integration_idn: null,
157
+ to_connector_idn: null,
158
+ is_agent: Boolean(item.is_agent === true),
159
+ project_idn: item.project_idn || null,
160
+ flow_idn: item.flow_idn || 'unknown',
161
+ skill_idn: item.skill_idn || 'unknown',
162
+ session_id: item.session_id || session.session_id || 'unknown',
163
+ recordings: item.recordings || [],
164
+ contact_information: item.contact_information || null
165
+ }));
166
+ // Extract agent_persona_id from the first act if we don't have it yet
167
+ if (!agentPersonaId && convertedActs.length > 0) {
168
+ const firstItem = convertedActs[0];
169
+ if (firstItem && firstItem.agent_persona_id !== 'unknown') {
170
+ agentPersonaId = firstItem.agent_persona_id;
171
+ if (verbose)
172
+ console.log(`✓ Extracted agent_persona_id: ${agentPersonaId}`);
173
+ }
174
+ }
175
+ // Filter for agent messages that came AFTER our message was sent
176
+ const agentMessages = convertedActs.filter(act => {
177
+ if (!act.is_agent)
178
+ return false;
179
+ // If we have a messageSentAt timestamp, ONLY include messages with datetime after it
180
+ if (messageSentAt) {
181
+ // Parse the act datetime - it may not have timezone, assume UTC
182
+ let actDatetime = act.datetime;
183
+ if (!actDatetime.endsWith('Z') && !actDatetime.includes('+') && !actDatetime.includes('-', 10)) {
184
+ actDatetime = actDatetime + 'Z'; // Assume UTC if no timezone
185
+ }
186
+ const actTime = new Date(actDatetime);
187
+ const sentTime = messageSentAt.getTime();
188
+ const actTimeMs = actTime.getTime();
189
+ const timeDiff = actTimeMs - sentTime;
190
+ if (verbose && attempts === 0) {
191
+ console.log(` Checking agent message:`);
192
+ console.log(` Original datetime: ${act.datetime}`);
193
+ console.log(` Parsed datetime: ${actDatetime}`);
194
+ console.log(` Act timestamp: ${actTimeMs} (${new Date(actTimeMs).toISOString()})`);
195
+ console.log(` Sent timestamp: ${sentTime} (${messageSentAt.toISOString()})`);
196
+ console.log(` Difference: ${timeDiff}ms (${(timeDiff / 1000).toFixed(1)}s)`);
197
+ console.log(` Include: ${timeDiff > -100 ? 'YES' : 'NO'}`);
198
+ }
199
+ // Only include messages sent AFTER our message (allow small negative buffer for processing time)
200
+ return timeDiff > -100;
201
+ }
202
+ // For first message (no messageSentAt), include all agent messages
203
+ return true;
204
+ });
205
+ if (agentMessages.length > 0) {
206
+ if (verbose)
207
+ console.log(`✓ Received ${agentMessages.length} agent message(s) after our message (${messageSentAt?.toISOString()})`);
208
+ // Return ONLY the single newest agent message (first one, since API returns newest first)
209
+ const latestAgentMessage = agentMessages[0];
210
+ if (latestAgentMessage) {
211
+ return { acts: [latestAgentMessage], agentPersonaId };
212
+ }
213
+ }
214
+ else if (verbose && attempts % 10 === 0) {
215
+ console.log(` No new agent messages yet (checked ${response.items.length} total messages, sentAt: ${messageSentAt?.toISOString()}), continuing...`);
216
+ }
217
+ }
218
+ }
219
+ catch (error) {
220
+ if (verbose && attempts < 3) {
221
+ console.log(`⚠️ Error polling (attempt ${attempts + 1}): ${error.message}`);
222
+ }
223
+ // Continue polling despite errors
224
+ }
225
+ attempts++;
226
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
227
+ }
228
+ if (verbose)
229
+ console.log('⏱️ Timeout waiting for response');
230
+ return { acts: [], agentPersonaId };
231
+ }
232
+ /**
233
+ * Extract agent messages from acts
234
+ */
235
+ export function extractAgentMessages(acts) {
236
+ return acts.filter(act => act.is_agent && act.reference_idn === 'agent_message');
237
+ }
238
+ /**
239
+ * Extract debug information from acts
240
+ */
241
+ export function extractDebugInfo(acts) {
242
+ return acts.map(act => ({
243
+ flow_idn: act.flow_idn,
244
+ skill_idn: act.skill_idn,
245
+ session_id: act.session_id,
246
+ runtime_context_id: act.runtime_context_id,
247
+ reference_idn: act.reference_idn,
248
+ arguments: act.arguments
249
+ }));
250
+ }
251
+ /**
252
+ * Format debug info for display
253
+ */
254
+ export function formatDebugInfo(acts) {
255
+ const lines = [];
256
+ for (const act of acts) {
257
+ if (act.is_agent) {
258
+ lines.push(`\n[Agent Act] ${act.reference_idn}`);
259
+ }
260
+ else {
261
+ lines.push(`\n[User Act] ${act.reference_idn}`);
262
+ }
263
+ lines.push(` Flow: ${act.flow_idn || 'N/A'}`);
264
+ lines.push(` Skill: ${act.skill_idn || 'N/A'}`);
265
+ lines.push(` Session: ${act.session_id}`);
266
+ if (act.runtime_context_id) {
267
+ lines.push(` Context: ${act.runtime_context_id}`);
268
+ }
269
+ if (act.arguments && act.arguments.length > 0) {
270
+ lines.push(` Arguments:`);
271
+ for (const arg of act.arguments) {
272
+ if (typeof arg === 'object' && arg !== null && 'name' in arg) {
273
+ lines.push(` ${arg.name}: ${JSON.stringify(arg.value).substring(0, 100)}`);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ return lines.join('\n');
279
+ }
280
+ //# sourceMappingURL=chat.js.map
package/dist/types.d.ts CHANGED
@@ -480,4 +480,85 @@ export interface PublishFlowRequest {
480
480
  export interface PublishFlowResponse {
481
481
  success: boolean;
482
482
  }
483
+ export interface Integration {
484
+ readonly id: string;
485
+ readonly title: string;
486
+ readonly idn: string;
487
+ readonly description: string;
488
+ readonly is_disabled: boolean;
489
+ readonly channel: string;
490
+ }
491
+ export interface IntegrationSetting {
492
+ readonly title: string;
493
+ readonly idn: string;
494
+ readonly control_type: string;
495
+ readonly value_type: string;
496
+ readonly is_required: boolean;
497
+ readonly default_value?: string | null;
498
+ }
499
+ export interface Connector {
500
+ readonly id: string;
501
+ readonly title: string;
502
+ readonly connector_idn: string;
503
+ readonly integration_idn: string;
504
+ readonly status: string;
505
+ readonly api_key?: string;
506
+ readonly settings: readonly ConnectorSetting[];
507
+ }
508
+ export interface ConnectorSetting {
509
+ readonly idn: string;
510
+ readonly value: string;
511
+ }
512
+ export interface CreateSandboxPersonaRequest {
513
+ name: string;
514
+ title: string;
515
+ }
516
+ export interface CreateSandboxPersonaResponse {
517
+ id: string;
518
+ }
519
+ export interface CreateActorRequest {
520
+ name: string;
521
+ external_id: string;
522
+ integration_idn: string;
523
+ connector_idn: string;
524
+ time_zone_identifier?: string;
525
+ }
526
+ export interface CreateActorResponse {
527
+ id: string;
528
+ }
529
+ export interface SendChatMessageRequest {
530
+ text: string;
531
+ arguments?: readonly any[];
532
+ }
533
+ export interface ConversationActsParams {
534
+ user_persona_id: string;
535
+ user_actor_id: string;
536
+ agent_persona_id?: string;
537
+ per?: number;
538
+ page?: number;
539
+ }
540
+ export interface ConversationActsResponse {
541
+ readonly items: readonly ConversationAct[];
542
+ readonly metadata?: {
543
+ readonly page: number;
544
+ readonly per: number;
545
+ readonly total: number;
546
+ };
547
+ }
548
+ export interface SandboxChatSession {
549
+ user_persona_id: string;
550
+ user_actor_id: string;
551
+ agent_persona_id: string | null;
552
+ connector_idn: string;
553
+ session_id: string | null;
554
+ external_id: string;
555
+ }
556
+ export interface ChatDebugInfo {
557
+ flow_idn: string | null;
558
+ skill_idn: string | null;
559
+ session_id: string;
560
+ runtime_context_id: string | null;
561
+ reference_idn: string;
562
+ arguments: readonly any[];
563
+ }
483
564
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "newo",
3
- "version": "3.0.0",
4
- "description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
3
+ "version": "3.1.0",
4
+ "description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features sandbox testing, IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "newo": "dist/cli.js"
@@ -29,7 +29,10 @@
29
29
  "workspace",
30
30
  "conversations",
31
31
  "chat-history",
32
- "personas"
32
+ "personas",
33
+ "sandbox",
34
+ "testing",
35
+ "agent-testing"
33
36
  ],
34
37
  "author": "sabbah13",
35
38
  "license": "MIT",
package/src/api.ts CHANGED
@@ -34,7 +34,16 @@ import type {
34
34
  CreateProjectRequest,
35
35
  CreateProjectResponse,
36
36
  PublishFlowRequest,
37
- PublishFlowResponse
37
+ PublishFlowResponse,
38
+ Integration,
39
+ Connector,
40
+ CreateSandboxPersonaRequest,
41
+ CreateSandboxPersonaResponse,
42
+ CreateActorRequest,
43
+ CreateActorResponse,
44
+ SendChatMessageRequest,
45
+ ConversationActsParams,
46
+ ConversationActsResponse
38
47
  } from './types.js';
39
48
 
40
49
  // Per-request retry tracking to avoid shared state issues
@@ -311,4 +320,49 @@ export async function createPersona(client: AxiosInstance, personaData: CreatePe
311
320
  export async function publishFlow(client: AxiosInstance, flowId: string, publishData: PublishFlowRequest): Promise<PublishFlowResponse> {
312
321
  const response = await client.post<PublishFlowResponse>(`/api/v1/designer/flows/${flowId}/publish`, publishData);
313
322
  return response.data;
323
+ }
324
+
325
+ // Sandbox Chat API Functions
326
+
327
+ export async function listIntegrations(client: AxiosInstance): Promise<Integration[]> {
328
+ const response = await client.get<Integration[]>('/api/v1/integrations');
329
+ return response.data;
330
+ }
331
+
332
+ export async function listConnectors(client: AxiosInstance, integrationId: string): Promise<Connector[]> {
333
+ const response = await client.get<Connector[]>(`/api/v1/integrations/${integrationId}/connectors`);
334
+ return response.data;
335
+ }
336
+
337
+ export async function createSandboxPersona(client: AxiosInstance, personaData: CreateSandboxPersonaRequest): Promise<CreateSandboxPersonaResponse> {
338
+ const response = await client.post<CreateSandboxPersonaResponse>('/api/v1/customer/personas', personaData);
339
+ return response.data;
340
+ }
341
+
342
+ export async function createActor(client: AxiosInstance, personaId: string, actorData: CreateActorRequest): Promise<CreateActorResponse> {
343
+ const response = await client.post<CreateActorResponse>(`/api/v1/customer/personas/${personaId}/actors`, actorData);
344
+ return response.data;
345
+ }
346
+
347
+ export async function sendChatMessage(client: AxiosInstance, actorId: string, messageData: SendChatMessageRequest): Promise<void> {
348
+ await client.post(`/api/v1/chat/user/${actorId}`, messageData);
349
+ }
350
+
351
+ export async function getConversationActs(client: AxiosInstance, params: ConversationActsParams): Promise<ConversationActsResponse> {
352
+ const queryParams: Record<string, any> = {
353
+ user_persona_id: params.user_persona_id,
354
+ user_actor_id: params.user_actor_id,
355
+ per: params.per || 100,
356
+ page: params.page || 1
357
+ };
358
+
359
+ // Only add agent_persona_id if provided
360
+ if (params.agent_persona_id) {
361
+ queryParams.agent_persona_id = params.agent_persona_id;
362
+ }
363
+
364
+ const response = await client.get<ConversationActsResponse>('/api/v1/bff/conversations/acts', {
365
+ params: queryParams
366
+ });
367
+ return response.data;
314
368
  }
package/src/auth.ts CHANGED
@@ -54,6 +54,11 @@ function validateUrl(url: string, name: string): void {
54
54
 
55
55
  // Enhanced logging function
56
56
  function logAuthEvent(level: 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>): void {
57
+ // Skip all logging if in quiet mode
58
+ if (process.env.NEWO_QUIET_MODE === 'true') {
59
+ return;
60
+ }
61
+
57
62
  const timestamp = new Date().toISOString();
58
63
  const logEntry = {
59
64
  timestamp,
@@ -62,7 +67,7 @@ function logAuthEvent(level: 'info' | 'warn' | 'error', message: string, meta?:
62
67
  message,
63
68
  ...meta
64
69
  };
65
-
70
+
66
71
  // Sanitize sensitive data
67
72
  const sanitized = JSON.parse(JSON.stringify(logEntry, (key, value) => {
68
73
  if (typeof key === 'string' && (key.toLowerCase().includes('key') || key.toLowerCase().includes('token') || key.toLowerCase().includes('secret'))) {
@@ -70,7 +75,7 @@ function logAuthEvent(level: 'info' | 'warn' | 'error', message: string, meta?:
70
75
  }
71
76
  return value;
72
77
  }));
73
-
78
+
74
79
  if (level === 'error') {
75
80
  console.error(JSON.stringify(sanitized));
76
81
  } else if (level === 'warn') {
@@ -11,6 +11,8 @@ Core Commands:
11
11
  newo push [--customer <idn>] [--no-publish] # upload modified *.guidance/*.jinja + attributes back to NEWO, publish flows by default
12
12
  newo status [--customer <idn>] # show modified files that would be pushed
13
13
  newo conversations [--customer <idn>] [--all] # download user conversations -> ./newo_customers/<idn>/conversations.yaml
14
+ newo sandbox "<message>" [--customer <idn>] # test agent in sandbox - single message mode (NEW v3.1.0)
15
+ newo sandbox --actor <id> "message" # continue existing sandbox conversation with chat ID
14
16
  newo pull-attributes [--customer <idn>] # download customer attributes -> ./newo_customers/<idn>/attributes.yaml
15
17
  newo list-customers # list available customers and their configuration
16
18
  newo meta [--customer <idn>] # get project metadata (debug command)
@@ -46,6 +48,8 @@ Flags:
46
48
  --all # include all available data (for conversations: all personas and acts)
47
49
  --force, -f # force overwrite without prompting (for pull command)
48
50
  --verbose, -v # enable detailed logging and progress information
51
+ --quiet, -q # minimal output for automation (sandbox only)
52
+ --actor <id> # continue existing sandbox chat with actor/chat ID
49
53
  --confirm # confirm destructive operations without prompting
50
54
  --no-publish # skip automatic flow publishing during push operations
51
55
 
@@ -108,6 +112,12 @@ Usage Examples:
108
112
  # Import AKB articles:
109
113
  newo import-akb articles.txt da4550db-2b95-4500-91ff-fb4b60fe7be9
110
114
 
115
+ # Sandbox testing (NEW v3.1.0):
116
+ newo sandbox "Hello, I want to order pizza" # Start new conversation
117
+ newo sandbox --actor abc123... "I want 2 large pizzas" # Continue conversation
118
+ newo sandbox "Test query" --verbose # With debug info
119
+ newo sandbox "Test query" --quiet # For automation/scripts
120
+
111
121
  File Structure:
112
122
  newo_customers/
113
123
  ├── <customer-idn>/