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.
@@ -7,7 +7,7 @@ import { listIntegrations, listConnectors, createSandboxPersona, createActor, se
7
7
  const SANDBOX_INTEGRATION_IDN = 'sandbox';
8
8
  const DEFAULT_TIMEZONE = 'America/Los_Angeles';
9
9
  const POLL_INTERVAL_MS = 1000; // 1 second
10
- const MAX_POLL_ATTEMPTS = 60; // Max 60 seconds wait
10
+ const DEFAULT_TIMEOUT_MS = 60_000; // Default max wait for agent response
11
11
  /**
12
12
  * Generate a random external ID for chat session
13
13
  */
@@ -22,39 +22,67 @@ function generatePersonaName() {
22
22
  return `newo-cli-${guid}`;
23
23
  }
24
24
  /**
25
- * Find a sandbox connector from the customer's connectors list
25
+ * List running connectors of an integration (default: sandbox).
26
+ * Used by `newo sandbox --list-connectors` and connector selection.
26
27
  */
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
28
+ export async function listRunningSandboxConnectors(client, integrationIdn = SANDBOX_INTEGRATION_IDN) {
31
29
  const integrations = await listIntegrations(client);
32
- const sandboxIntegration = integrations.find(i => i.idn === SANDBOX_INTEGRATION_IDN);
33
- if (!sandboxIntegration) {
30
+ const integration = integrations.find(i => i.idn === integrationIdn);
31
+ if (!integration) {
32
+ throw new Error(`Integration '${integrationIdn}' not found. Available integrations: ${integrations.map(i => i.idn).join(', ') || '(none)'}`);
33
+ }
34
+ const connectors = await listConnectors(client, integration.id);
35
+ return connectors.filter(c => c.status === 'running');
36
+ }
37
+ /**
38
+ * Find a sandbox connector from the customer's connectors list.
39
+ *
40
+ * Without options, preserves legacy behavior: first running connector of the
41
+ * 'sandbox' integration. With options.connectorIdn, selects that exact
42
+ * connector and throws a descriptive error (listing available connectors)
43
+ * when it is not found or not running.
44
+ */
45
+ export async function findSandboxConnector(client, verbose = false, options = {}) {
46
+ const integrationIdn = options.integrationIdn || SANDBOX_INTEGRATION_IDN;
47
+ if (verbose)
48
+ console.log(`šŸ” Searching for ${integrationIdn} integration...`);
49
+ let runningConnectors;
50
+ try {
51
+ runningConnectors = await listRunningSandboxConnectors(client, integrationIdn);
52
+ }
53
+ catch (error) {
54
+ if (options.connectorIdn)
55
+ throw error;
34
56
  if (verbose)
35
- console.log('āŒ Sandbox integration not found');
57
+ console.log(`āŒ ${error instanceof Error ? error.message : String(error)}`);
36
58
  return null;
37
59
  }
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) {
60
+ if (runningConnectors.length === 0) {
61
+ if (options.connectorIdn) {
62
+ throw new Error(`No running connectors found in integration '${integrationIdn}'`);
63
+ }
46
64
  if (verbose)
47
- console.log('āŒ No running sandbox connectors found');
65
+ console.log(`āŒ No running ${integrationIdn} connectors found`);
48
66
  return null;
49
67
  }
68
+ if (options.connectorIdn) {
69
+ const match = runningConnectors.find(c => c.connector_idn === options.connectorIdn);
70
+ if (!match) {
71
+ const available = runningConnectors.map(c => c.connector_idn).join(', ');
72
+ throw new Error(`Connector '${options.connectorIdn}' not found among running connectors of integration '${integrationIdn}'. Available: ${available}`);
73
+ }
74
+ if (verbose)
75
+ console.log(`āœ“ Using connector: ${match.connector_idn}`);
76
+ return match;
77
+ }
50
78
  if (verbose) {
51
- console.log(`āœ“ Found ${sandboxConnectors.length} running sandbox connector(s)`);
52
- const firstConnector = sandboxConnectors[0];
79
+ console.log(`āœ“ Found ${runningConnectors.length} running ${integrationIdn} connector(s)`);
80
+ const firstConnector = runningConnectors[0];
53
81
  if (firstConnector) {
54
82
  console.log(` Using: ${firstConnector.connector_idn}`);
55
83
  }
56
84
  }
57
- return sandboxConnectors[0] || null;
85
+ return runningConnectors[0] || null;
58
86
  }
59
87
  /**
60
88
  * Create a new sandbox chat session
@@ -77,7 +105,7 @@ export async function createChatSession(client, connector, verbose = false) {
77
105
  const actorResponse = await createActor(client, personaResponse.id, {
78
106
  name: personaName,
79
107
  external_id: externalId,
80
- integration_idn: SANDBOX_INTEGRATION_IDN,
108
+ integration_idn: connector.integration_idn || SANDBOX_INTEGRATION_IDN,
81
109
  connector_idn: connector.connector_idn,
82
110
  time_zone_identifier: DEFAULT_TIMEZONE
83
111
  });
@@ -97,8 +125,10 @@ export async function createChatSession(client, connector, verbose = false) {
97
125
  * Returns the timestamp when message was sent (for filtering responses)
98
126
  */
99
127
  export async function sendMessage(client, session, text, verbose = false) {
100
- if (verbose)
101
- console.log(`šŸ’¬ Sending message: "${text}"`);
128
+ if (verbose) {
129
+ const preview = text.length > 200 ? `${text.slice(0, 200)}… (${text.length} chars)` : text;
130
+ console.log(`šŸ’¬ Sending message: "${preview}"`);
131
+ }
102
132
  const sentAt = new Date();
103
133
  await sendChatMessage(client, session.user_actor_id, {
104
134
  text,
@@ -108,21 +138,33 @@ export async function sendMessage(client, session, text, verbose = false) {
108
138
  console.log('āœ“ Message sent');
109
139
  return sentAt;
110
140
  }
141
+ /**
142
+ * Parse an act datetime that may lack timezone info (assume UTC)
143
+ */
144
+ function parseActDatetimeMs(datetime) {
145
+ let d = datetime;
146
+ if (!d.endsWith('Z') && !d.includes('+') && !d.includes('-', 10)) {
147
+ d = d + 'Z';
148
+ }
149
+ return new Date(d).getTime();
150
+ }
111
151
  /**
112
152
  * Poll for new conversation acts (messages and debug info)
113
153
  * Continues polling until we get an agent response, not just any new message
114
154
  */
115
- export async function pollForResponse(client, session, messageSentAt = null, verbose = false) {
155
+ export async function pollForResponse(client, session, messageSentAt = null, verbose = false, timeoutMs = DEFAULT_TIMEOUT_MS) {
116
156
  let attempts = 0;
117
157
  let agentPersonaId = session.agent_persona_id;
158
+ let userAct = null;
159
+ const maxPollAttempts = Math.max(1, Math.ceil(timeoutMs / POLL_INTERVAL_MS));
118
160
  if (verbose)
119
- console.log('ā³ Waiting for agent response...');
161
+ console.log(`ā³ Waiting for agent response (timeout: ${Math.round(timeoutMs / 1000)}s)...`);
120
162
  // Add small delay before first poll to allow message to be processed
121
163
  await new Promise(resolve => setTimeout(resolve, 500));
122
- while (attempts < MAX_POLL_ATTEMPTS) {
164
+ while (attempts < maxPollAttempts) {
123
165
  try {
124
166
  if (verbose && attempts % 5 === 0) {
125
- console.log(` [Poll attempt ${attempts + 1}/${MAX_POLL_ATTEMPTS}] Checking for messages...`);
167
+ console.log(` [Poll attempt ${attempts + 1}/${maxPollAttempts}] Checking for messages...`);
126
168
  }
127
169
  // Use Chat History API instead of acts API (doesn't require account_id)
128
170
  const response = await getChatHistory(client, {
@@ -172,6 +214,17 @@ export async function pollForResponse(client, session, messageSentAt = null, ver
172
214
  console.log(`āœ“ Extracted agent_persona_id: ${agentPersonaId}`);
173
215
  }
174
216
  }
217
+ // Track the user act of our sent message (newest non-agent act at/after sentAt).
218
+ // Its external_event_id is the correlation key for `newo logs --event-id`.
219
+ for (const act of convertedActs) {
220
+ if (act.is_agent)
221
+ continue;
222
+ if (messageSentAt && parseActDatetimeMs(act.datetime) - messageSentAt.getTime() <= -100)
223
+ continue;
224
+ if (!userAct || parseActDatetimeMs(act.datetime) >= parseActDatetimeMs(userAct.datetime)) {
225
+ userAct = act;
226
+ }
227
+ }
175
228
  // Filter for agent messages that came AFTER our message was sent
176
229
  const agentMessages = convertedActs.filter(act => {
177
230
  if (!act.is_agent)
@@ -208,7 +261,7 @@ export async function pollForResponse(client, session, messageSentAt = null, ver
208
261
  // Return ONLY the single newest agent message (first one, since API returns newest first)
209
262
  const latestAgentMessage = agentMessages[0];
210
263
  if (latestAgentMessage) {
211
- return { acts: [latestAgentMessage], agentPersonaId };
264
+ return { acts: [latestAgentMessage], agentPersonaId, userAct };
212
265
  }
213
266
  }
214
267
  else if (verbose && attempts % 10 === 0) {
@@ -227,7 +280,7 @@ export async function pollForResponse(client, session, messageSentAt = null, ver
227
280
  }
228
281
  if (verbose)
229
282
  console.log('ā±ļø Timeout waiting for response');
230
- return { acts: [], agentPersonaId };
283
+ return { acts: [], agentPersonaId, userAct };
231
284
  }
232
285
  /**
233
286
  * Extract agent messages from acts
@@ -0,0 +1,33 @@
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 type { ProjectMeta, Agent, Flow, Skill } from '../types.js';
9
+ export interface RemoteSkillPath {
10
+ projectIdn: string;
11
+ agentIdn: string;
12
+ flowIdn: string;
13
+ skillIdn: string;
14
+ }
15
+ export interface ResolvedRemoteSkill {
16
+ project: ProjectMeta;
17
+ agent: Agent;
18
+ flow: Flow;
19
+ skill: Skill;
20
+ }
21
+ /**
22
+ * Resolve a skill on the platform by its IDN path.
23
+ * Throws descriptive errors listing available IDNs at the failing level.
24
+ */
25
+ export declare function resolveRemoteSkill(client: AxiosInstance, path: RemoteSkillPath): Promise<ResolvedRemoteSkill>;
26
+ /**
27
+ * Parse a `--model provider_idn/model_idn` flag value
28
+ */
29
+ export declare function parseModelFlag(value: string): {
30
+ provider_idn: string;
31
+ model_idn: string;
32
+ };
33
+ //# sourceMappingURL=remote-skill.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { listProjects, listAgents, listFlowSkills, getSkill } from '../api.js';
2
+ /**
3
+ * Resolve a skill on the platform by its IDN path.
4
+ * Throws descriptive errors listing available IDNs at the failing level.
5
+ */
6
+ export async function resolveRemoteSkill(client, path) {
7
+ const projects = await listProjects(client);
8
+ const project = projects.find(p => p.idn === path.projectIdn);
9
+ if (!project) {
10
+ throw new Error(`Project '${path.projectIdn}' not found. Available projects: ${projects.map(p => p.idn).join(', ') || '(none)'}`);
11
+ }
12
+ const agents = await listAgents(client, project.id);
13
+ const agent = agents.find(a => a.idn === path.agentIdn);
14
+ if (!agent) {
15
+ throw new Error(`Agent '${path.agentIdn}' not found in project '${path.projectIdn}'. Available agents: ${agents.map(a => a.idn).join(', ') || '(none)'}`);
16
+ }
17
+ const flows = agent.flows || [];
18
+ const flow = flows.find(f => f.idn === path.flowIdn);
19
+ if (!flow) {
20
+ throw new Error(`Flow '${path.flowIdn}' not found in agent '${path.agentIdn}'. Available flows: ${flows.map(f => f.idn).join(', ') || '(none)'}`);
21
+ }
22
+ const skills = await listFlowSkills(client, flow.id);
23
+ const skillStub = skills.find(s => s.idn === path.skillIdn);
24
+ if (!skillStub) {
25
+ throw new Error(`Skill '${path.skillIdn}' not found in flow '${path.flowIdn}'. Available skills: ${skills.map(s => s.idn).join(', ') || '(none)'}`);
26
+ }
27
+ // The flow-skills list endpoint already returns full skills including
28
+ // prompt_script (pull relies on this). Only fall back to the by-id
29
+ // endpoint when prompt_script is absent — and tolerate a 404 there,
30
+ // since /api/v1/designer/skills/{id} is not available on all accounts.
31
+ let skill = skillStub;
32
+ if (skill.prompt_script === undefined || skill.prompt_script === null) {
33
+ try {
34
+ skill = await getSkill(client, skillStub.id);
35
+ }
36
+ catch {
37
+ skill = skillStub;
38
+ }
39
+ }
40
+ return { project, agent, flow, skill };
41
+ }
42
+ /**
43
+ * Parse a `--model provider_idn/model_idn` flag value
44
+ */
45
+ export function parseModelFlag(value) {
46
+ const parts = value.split('/');
47
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
48
+ throw new Error(`Invalid --model value '${value}'. Expected format: <provider_idn>/<model_idn> (e.g. openai/gpt4o)`);
49
+ }
50
+ return { provider_idn: parts[0], model_idn: parts[1] };
51
+ }
52
+ //# sourceMappingURL=remote-skill.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newo",
3
- "version": "3.7.3",
3
+ "version": "3.7.5",
4
4
  "description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, 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": {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Get Skill Command Handler - Inspect a skill's live state on the platform
3
+ *
4
+ * Usage:
5
+ * newo get-skill <skill-idn> --project <idn> --agent <idn> --flow <idn> [--json]
6
+ *
7
+ * Shows the skill as it currently lives on the platform (model, runner_type,
8
+ * parameters, prompt_script) without requiring a pulled local workspace.
9
+ */
10
+ import { requireSingleCustomer } from '../customer-selection.js';
11
+ import { makeClient } from '../../api.js';
12
+ import { getValidAccessToken } from '../../auth.js';
13
+ import { resolveRemoteSkill } from '../../sync/remote-skill.js';
14
+ import type { MultiCustomerConfig, CliArgs } from '../../types.js';
15
+
16
+ const USAGE = 'Usage: newo get-skill <skill-idn> --project <project-idn> --agent <agent-idn> --flow <flow-idn> [--json] [--customer <idn>]';
17
+
18
+ export async function handleGetSkillCommand(
19
+ customerConfig: MultiCustomerConfig,
20
+ args: CliArgs,
21
+ verbose: boolean = false
22
+ ): Promise<void> {
23
+ const skillIdn = args._[1] as string | undefined;
24
+ const projectIdn = args.project as string | undefined;
25
+ const agentIdn = args.agent as string | undefined;
26
+ const flowIdn = args.flow as string | undefined;
27
+ const asJson = Boolean(args.json);
28
+
29
+ if (!skillIdn || !projectIdn || !agentIdn || !flowIdn) {
30
+ console.error('Error: skill IDN, --project, --agent and --flow are required');
31
+ console.error(USAGE);
32
+ process.exit(1);
33
+ }
34
+
35
+ const selectedCustomer = requireSingleCustomer(customerConfig, args.customer as string | undefined);
36
+
37
+ if (asJson) {
38
+ process.env.NEWO_QUIET_MODE = 'true'; // keep stdout machine-readable for piping
39
+ }
40
+
41
+ const token = await getValidAccessToken(selectedCustomer);
42
+ const client = await makeClient(verbose, token);
43
+
44
+ if (verbose) console.log(`šŸ” Resolving skill ${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}...`);
45
+
46
+ const { project, agent, flow, skill } = await resolveRemoteSkill(client, {
47
+ projectIdn,
48
+ agentIdn,
49
+ flowIdn,
50
+ skillIdn
51
+ });
52
+
53
+ if (asJson) {
54
+ console.log(JSON.stringify({
55
+ project_idn: project.idn,
56
+ agent_idn: agent.idn,
57
+ flow_idn: flow.idn,
58
+ id: skill.id,
59
+ idn: skill.idn,
60
+ title: skill.title,
61
+ runner_type: skill.runner_type,
62
+ model: skill.model,
63
+ parameters: skill.parameters,
64
+ prompt_script: skill.prompt_script ?? null
65
+ }, null, 2));
66
+ return;
67
+ }
68
+
69
+ console.log(`šŸ“œ Skill: ${project.idn}/${agent.idn}/${flow.idn}/${skill.idn}`);
70
+ console.log(` ID: ${skill.id}`);
71
+ console.log(` Title: ${skill.title}`);
72
+ console.log(` Runner type: ${skill.runner_type}`);
73
+ console.log(` Model: ${skill.model.provider_idn}/${skill.model.model_idn}`);
74
+ if (skill.parameters.length > 0) {
75
+ console.log(` Parameters:`);
76
+ for (const param of skill.parameters) {
77
+ console.log(` ${param.name}${param.default_value !== undefined ? ` = ${JSON.stringify(param.default_value)}` : ''}`);
78
+ }
79
+ } else {
80
+ console.log(` Parameters: (none)`);
81
+ }
82
+ console.log(`\n--- prompt_script (${(skill.prompt_script || '').length} chars) ---`);
83
+ console.log(skill.prompt_script || '(empty)');
84
+ }
@@ -17,6 +17,11 @@ Core Commands:
17
17
  newo conversations [--customer <idn>] [--all] # download user conversations -> conversations.yaml
18
18
  newo sandbox "<message>" [--customer <idn>] # test agent in sandbox - single message mode
19
19
  newo sandbox --actor <id> "message" # continue existing sandbox conversation
20
+ newo sandbox --list-connectors # list running sandbox connectors (NEW v3.7.5)
21
+ newo sandbox "<msg>" --connector <idn> # chat through a specific connector (NEW v3.7.5)
22
+ newo sandbox --file <path> | --stdin # send long messages from file/stdin (NEW v3.7.5)
23
+ newo get-skill <idn> --project <p> --agent <a> --flow <f> [--json] # inspect live skill on platform (NEW v3.7.5)
24
+ newo update-skill <idn> --project <p> --agent <a> --flow <f> [--model <prov>/<model>] [--script <file>] [--publish] # point-edit skill (NEW v3.7.5)
20
25
  newo pull-attributes [--customer <idn>] # download customer + project attributes
21
26
  newo list-customers # list available customers
22
27
  newo meta [--customer <idn>] # get project metadata (debug)
@@ -77,6 +82,8 @@ Analytics & Monitoring (NEW):
77
82
  newo logs --type <types> # filter by type: system, operation, call (comma-separated)
78
83
  newo logs --flow <idn> --skill <idn> # filter by flow and/or skill
79
84
  newo logs --message <text> # search in log messages
85
+ newo logs --name <ActionName> # filter by action name in data.name, e.g. Gen, GetMemory (NEW v3.7.5)
86
+ newo logs --event-id <uuid> # filter by external event ID (correlate with sandbox --json)
80
87
  newo logs --follow, -f # tail mode - continuously poll for new logs
81
88
  newo logs --json # output logs as JSON array
82
89
  newo logs --raw # output each log as single JSON line (for piping)
@@ -95,6 +102,12 @@ Flags:
95
102
  --verbose, -v # enable detailed logging and progress information
96
103
  --quiet, -q # minimal output for automation (sandbox only)
97
104
  --actor <id> # continue existing sandbox chat with actor/chat ID
105
+ --connector <idn> # sandbox: select connector by connector_idn (default: first running)
106
+ --integration <idn> # sandbox: integration to search connectors in (default: sandbox)
107
+ --file <path> # sandbox: read message text from file
108
+ --stdin # sandbox: read message text from stdin
109
+ --timeout <seconds> # sandbox: max wait for agent response (default: 60)
110
+ --json # sandbox: machine-readable output incl. external_event_id (user+agent turns)
98
111
  --confirm # confirm destructive operations without prompting
99
112
  --no-publish # skip automatic flow publishing during push operations
100
113
  --output, -o <file> # output file path (for export command)
@@ -202,6 +215,22 @@ Usage Examples:
202
215
  newo sandbox "Test query" --verbose # With debug info
203
216
  newo sandbox "Test query" --quiet # For automation/scripts
204
217
 
218
+ # Sandbox connector selection + automation (NEW v3.7.5):
219
+ newo sandbox --list-connectors # Show running sandbox connectors
220
+ newo sandbox "ping" --connector vibe_agent # Chat through specific connector
221
+ newo sandbox --file ./chunk1.txt --actor abc123... --json # Long message from file, JSON output
222
+ cat msg.txt | newo sandbox --stdin --timeout 420 --json # From stdin with 7-minute timeout
223
+ # --json output: {actor_id, persona_id, external_event_id, user_external_event_id,
224
+ # agent_external_event_id, response, elapsed_ms, timed_out, ...}
225
+ # Correlate a turn with its logs: newo logs --event-id <external_event_id> --json
226
+
227
+ # Live skill inspection / point edits (NEW v3.7.5):
228
+ newo get-skill structured_generation --project vibe --agent VibeAgent --flow VibeFlow # View live state
229
+ newo get-skill structured_generation --project vibe --agent VibeAgent --flow VibeFlow --json # As JSON
230
+ newo update-skill structured_generation --project vibe --agent VibeAgent --flow VibeFlow \\
231
+ --model openai/gpt54 --publish # Switch model and publish
232
+ newo update-skill my_skill --project p --agent a --flow f --script ./patched.nsl --publish # Replace script
233
+
205
234
  # Analytics logs (NEW v3.5.0):
206
235
  newo logs # Last 1 hour of logs
207
236
  newo logs --hours 24 # Last 24 hours
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { AxiosInstance } from 'axios';
17
- import type { MultiCustomerConfig, LogEntry, LogLevel, LogType, LogsQueryParams, CliArgs } from '../../types.js';
17
+ import type { MultiCustomerConfig, LogEntry, LogLevel, LogType, LogsQueryParams, LogsResponse, CliArgs } from '../../types.js';
18
18
  import { makeClient, getLogs } from '../../api.js';
19
19
  import { getValidAccessToken } from '../../auth.js';
20
20
  import { requireSingleCustomer } from '../customer-selection.js';
@@ -33,6 +33,8 @@ const colors = {
33
33
  gray: '\x1b[90m'
34
34
  };
35
35
 
36
+ type GetLogsFn = (client: AxiosInstance, params: LogsQueryParams) => Promise<LogsResponse>;
37
+
36
38
  function getLevelColor(level: LogLevel): string {
37
39
  switch (level) {
38
40
  case 'error': return colors.red;
@@ -106,7 +108,13 @@ export async function handleLogsCommand(
106
108
  // Select customer
107
109
  const selectedCustomer = requireSingleCustomer(customerConfig, args.customer as string | undefined);
108
110
 
109
- console.log(`šŸ“Š Fetching logs for ${selectedCustomer.idn}...`);
111
+ // Keep stdout machine-readable when JSON output is requested (for piping to jq etc.)
112
+ const machineOutput = Boolean(args.json || args.raw);
113
+ if (machineOutput) {
114
+ process.env.NEWO_QUIET_MODE = 'true'; // suppress auth logging on stdout
115
+ } else {
116
+ console.log(`šŸ“Š Fetching logs for ${selectedCustomer.idn}...`);
117
+ }
110
118
 
111
119
  // Get access token and create client
112
120
  const token = await getValidAccessToken(selectedCustomer);
@@ -164,29 +172,84 @@ export async function handleLogsCommand(
164
172
  const follow = Boolean(args.follow || args.f);
165
173
  const asJson = Boolean(args.json);
166
174
  const raw = Boolean(args.raw);
175
+ // --name filters by data.name (e.g. action name like Gen or GetMemory).
176
+ // The API has no such query param, so it is applied client-side.
177
+ const nameFilter = args.name ? String(args.name) : null;
167
178
 
168
179
  if (follow) {
169
- await tailLogs(client, params, asJson);
180
+ await tailLogs(client, params, asJson, nameFilter);
170
181
  } else {
171
- await fetchAndDisplayLogs(client, params, asJson, raw);
182
+ await fetchAndDisplayLogs(client, params, asJson, raw, nameFilter);
172
183
  }
173
184
  }
174
185
 
175
- async function fetchAndDisplayLogs(
186
+ function filterByName(logs: readonly LogEntry[], nameFilter: string | null): LogEntry[] {
187
+ if (!nameFilter) return [...logs];
188
+ return logs.filter(log => log.data['name'] === nameFilter);
189
+ }
190
+
191
+ export async function collectLogsForDisplay(
192
+ client: AxiosInstance,
193
+ params: LogsQueryParams,
194
+ nameFilter: string | null = null,
195
+ getLogsFn: GetLogsFn = getLogs
196
+ ): Promise<LogEntry[]> {
197
+ if (!nameFilter) {
198
+ const response = await getLogsFn(client, params);
199
+ return [...response.items];
200
+ }
201
+
202
+ const pageSize = Number.isFinite(params.per) && params.per && params.per > 0 ? params.per : 50;
203
+ let page = Number.isFinite(params.page) && params.page && params.page > 0 ? params.page : 1;
204
+ const logs: LogEntry[] = [];
205
+
206
+ while (true) {
207
+ const response = await getLogsFn(client, {
208
+ ...params,
209
+ page,
210
+ per: pageSize
211
+ });
212
+
213
+ logs.push(...filterByName(response.items, nameFilter));
214
+
215
+ if (response.items.length < pageSize) {
216
+ break;
217
+ }
218
+
219
+ page++;
220
+ }
221
+
222
+ return logs;
223
+ }
224
+
225
+ export async function fetchAndDisplayLogs(
176
226
  client: AxiosInstance,
177
227
  params: LogsQueryParams,
178
228
  asJson: boolean,
179
- raw: boolean
229
+ raw: boolean,
230
+ nameFilter: string | null = null,
231
+ getLogsFn: GetLogsFn = getLogs
180
232
  ): Promise<void> {
181
233
  try {
182
- const response = await getLogs(client, params);
183
- const logs = response.items;
234
+ const logs = await collectLogsForDisplay(client, params, nameFilter, getLogsFn);
184
235
 
185
236
  if (asJson) {
186
237
  console.log(JSON.stringify(logs, null, 2));
187
238
  return;
188
239
  }
189
240
 
241
+ // --raw is a machine-readable JSONL contract: stdout must contain only
242
+ // one JSON object per log line, with no banners or empty-result text.
243
+ if (raw) {
244
+ const sortedLogs = [...logs].sort((a, b) =>
245
+ new Date(a.datetime).getTime() - new Date(b.datetime).getTime()
246
+ );
247
+ for (const log of sortedLogs) {
248
+ console.log(JSON.stringify(log));
249
+ }
250
+ return;
251
+ }
252
+
190
253
  if (logs.length === 0) {
191
254
  console.log('\nNo logs found for the specified criteria.');
192
255
  return;
@@ -203,11 +266,7 @@ async function fetchAndDisplayLogs(
203
266
  const useColors = process.stdout.isTTY !== false;
204
267
 
205
268
  for (const log of sortedLogs) {
206
- if (raw) {
207
- console.log(JSON.stringify(log));
208
- } else {
209
- console.log(formatLogEntry(log, useColors));
210
- }
269
+ console.log(formatLogEntry(log, useColors));
211
270
  }
212
271
 
213
272
  console.log(`\nāœ… Displayed ${logs.length} log entries`);
@@ -220,7 +279,8 @@ async function fetchAndDisplayLogs(
220
279
  async function tailLogs(
221
280
  client: AxiosInstance,
222
281
  params: LogsQueryParams,
223
- asJson: boolean
282
+ asJson: boolean,
283
+ nameFilter: string | null = null
224
284
  ): Promise<void> {
225
285
  console.log('šŸ”„ Watching for new logs (Ctrl+C to stop)...\n');
226
286
 
@@ -241,7 +301,7 @@ async function tailLogs(
241
301
  };
242
302
 
243
303
  const response = await getLogs(client, pollParams);
244
- const logs = response.items;
304
+ const logs = filterByName(response.items, nameFilter);
245
305
 
246
306
  // Filter out already seen logs and sort by time
247
307
  const newLogs = logs
@@ -304,6 +364,7 @@ Filter Options:
304
364
  --flow <idn> Filter by flow IDN
305
365
  --skill <idn> Filter by skill IDN
306
366
  --message <text> Search in log messages
367
+ --name <ActionName> Filter by action name in data.name, e.g. Gen, GetMemory (client-side)
307
368
  --event-id <uuid> Filter by external event ID
308
369
  --runtime-id <uuid> Filter by runtime context ID
309
370
  --actor-id <uuid> Filter by user actor ID
@@ -325,5 +386,12 @@ Examples:
325
386
  newo logs --type call --skill CreateActor # Skill calls for CreateActor
326
387
  newo logs --flow CACreatorFlow --follow # Tail logs for specific flow
327
388
  newo logs --json --per 100 # Get 100 logs as JSON
389
+ newo logs --type call --name Gen --json # Only Gen action calls
390
+
391
+ Notes:
392
+ The model used for a turn is in data.source.model of the --json output —
393
+ do NOT infer it from actor/agent names.
394
+ external_event_id (newo sandbox --json) correlates a chat turn with its
395
+ logs: newo logs --event-id <id>
328
396
  `);
329
397
  }