@guildai/cli 0.7.0 → 0.8.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.
Files changed (45) hide show
  1. package/dist/commands/agent/chat.js +29 -62
  2. package/dist/commands/agent/clone.js +1 -1
  3. package/dist/commands/agent/code.js +1 -1
  4. package/dist/commands/agent/fork.js +2 -2
  5. package/dist/commands/agent/get.js +1 -1
  6. package/dist/commands/agent/grep.js +2 -2
  7. package/dist/commands/agent/init.js +11 -4
  8. package/dist/commands/agent/list.js +9 -1
  9. package/dist/commands/agent/publish.js +1 -1
  10. package/dist/commands/agent/revalidate.js +1 -1
  11. package/dist/commands/agent/save.js +23 -2
  12. package/dist/commands/agent/test.js +91 -92
  13. package/dist/commands/agent/unpublish.js +1 -1
  14. package/dist/commands/agent/update.js +1 -1
  15. package/dist/commands/agent/versions.js +1 -1
  16. package/dist/commands/agent/workspaces.js +1 -1
  17. package/dist/commands/chat.js +28 -15
  18. package/dist/commands/config/list.js +2 -2
  19. package/dist/commands/integration/operation/create.js +2 -2
  20. package/dist/commands/integration/operation/list.js +2 -2
  21. package/dist/commands/integration/update.js +1 -1
  22. package/dist/commands/integration/version/get.js +2 -2
  23. package/dist/commands/integration/version/publish.js +2 -2
  24. package/dist/commands/integration/version/test.js +2 -2
  25. package/dist/commands/session/events.js +7 -3
  26. package/dist/commands/session/tasks.js +8 -2
  27. package/dist/commands/workspace/get.js +1 -1
  28. package/dist/commands/workspace/list.js +28 -6
  29. package/dist/commands/workspace/select.js +40 -9
  30. package/dist/components/TaskView.js +2 -2
  31. package/dist/lib/agent-helpers.d.ts +74 -2
  32. package/dist/lib/agent-helpers.js +222 -8
  33. package/dist/lib/alternate-screen.js +2 -0
  34. package/dist/lib/api-client.js +2 -1
  35. package/dist/lib/api-types.d.ts +37 -0
  36. package/dist/lib/config.d.ts +3 -0
  37. package/dist/lib/config.js +33 -0
  38. package/dist/lib/output.d.ts +6 -1
  39. package/dist/lib/output.js +50 -0
  40. package/dist/lib/session-events.d.ts +1 -1
  41. package/dist/lib/session-events.js +5 -3
  42. package/dist/lib/session-polling.d.ts +8 -0
  43. package/dist/lib/session-polling.js +49 -0
  44. package/dist/lib/spinners.js +4 -1
  45. package/package.json +1 -1
@@ -1,23 +1,52 @@
1
1
  // Copyright 2026 Guild.ai
2
2
  // SPDX-License-Identifier: Apache-2.0
3
+ import { createHash } from 'crypto';
3
4
  import * as fs from 'fs/promises';
4
5
  import * as path from 'path';
5
6
  import { loadLocalConfig } from './guild-config.js';
6
7
  import { runGit } from './git.js';
7
8
  import { resolveOwnerId } from './owner-helpers.js';
9
+ import { pollUntilComplete } from './polling.js';
10
+ // ---------------------------------------------------------------------------
11
+ // Build error types
12
+ // ---------------------------------------------------------------------------
13
+ /** Thrown when the ephemeral version build times out or polling fails. */
14
+ export class BuildTimeoutError extends Error {
15
+ constructor(message = 'The agent version build timed out or failed to report status.') {
16
+ super(message);
17
+ this.name = 'BuildTimeoutError';
18
+ }
19
+ }
20
+ /** Thrown when the ephemeral version build fails validation. */
21
+ export class BuildFailedError extends Error {
22
+ failedSteps;
23
+ constructor(failedSteps) {
24
+ const stepSummary = failedSteps.length > 0
25
+ ? failedSteps
26
+ .map((s) => `Step "${s.name}" failed:${s.content ? `\n${s.content}` : ''}`)
27
+ .join('\n')
28
+ : 'No failed steps found. Check validation logs for details.';
29
+ super(`Build failed\n\n${stepSummary}\n\nFix the issues and retry.`);
30
+ this.name = 'BuildFailedError';
31
+ this.failedSteps = failedSteps;
32
+ }
33
+ }
8
34
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
35
  /**
10
36
  * Resolve an agent identifier to a fully-qualified form.
11
37
  *
12
- * If the identifier already contains `/` or is a UUID, return it verbatim.
13
- * Otherwise, resolve the default owner and prepend `owner.name/`.
38
+ * If the identifier already contains `~` or is a UUID, return it verbatim.
39
+ * Otherwise, resolve the default owner and prepend `owner.name~`.
14
40
  */
15
41
  export async function resolveAgentRef(client, identifier) {
16
- if (identifier.includes('/') || UUID_RE.test(identifier)) {
42
+ if (identifier.includes('~') || UUID_RE.test(identifier)) {
17
43
  return identifier;
18
44
  }
45
+ if (identifier.includes('/')) {
46
+ return identifier.replace('/', '~');
47
+ }
19
48
  const owner = await resolveOwnerId({ client, interactive: false });
20
- return `${owner.name}/${identifier}`;
49
+ return `${owner.name}~${identifier}`;
21
50
  }
22
51
  /**
23
52
  * Get agent ID from argument or guild.json in current directory
@@ -72,10 +101,7 @@ const REQUIRED_AGENT_FILES = ['agent.ts', 'package.json'];
72
101
  export async function readAgentFiles(cwd) {
73
102
  const { stdout } = await runGit(['ls-files', '--cached', '--others', '--exclude-standard'], { cwd });
74
103
  const gitFiles = stdout.split('\n').filter((f) => f.trim().length > 0);
75
- const relevantFiles = gitFiles.filter((f) => {
76
- const ext = path.extname(f);
77
- return ['.ts', '.json', '.md'].includes(ext);
78
- });
104
+ const relevantFiles = gitFiles.filter((f) => !f.startsWith('.guild/'));
79
105
  const files = [];
80
106
  for (const filePath of relevantFiles) {
81
107
  const fullPath = path.join(cwd, filePath);
@@ -89,4 +115,192 @@ export async function readAgentFiles(cwd) {
89
115
  }
90
116
  return files;
91
117
  }
118
+ const CACHE_DIR = path.join('.guild', 'cache');
119
+ const CACHE_FILE = 'last-ephemeral.json';
120
+ /**
121
+ * Compute a deterministic hash of agent files.
122
+ * Sorts by path to ensure consistent ordering.
123
+ */
124
+ export function hashAgentFiles(files) {
125
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
126
+ const hash = createHash('sha256');
127
+ for (const file of sorted) {
128
+ hash.update(file.path);
129
+ hash.update('\0');
130
+ hash.update(file.content);
131
+ hash.update('\0');
132
+ }
133
+ return hash.digest('hex');
134
+ }
135
+ async function readEphemeralCache(cwd) {
136
+ try {
137
+ const cachePath = path.join(cwd, CACHE_DIR, CACHE_FILE);
138
+ const raw = await fs.readFile(cachePath, 'utf-8');
139
+ const parsed = JSON.parse(raw);
140
+ if (typeof parsed.hash === 'string' && typeof parsed.version_id === 'string') {
141
+ return { hash: parsed.hash, version_id: parsed.version_id };
142
+ }
143
+ return null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ async function writeEphemeralCache(cwd, cache) {
150
+ const dir = path.join(cwd, CACHE_DIR);
151
+ await fs.mkdir(dir, { recursive: true });
152
+ await fs.writeFile(path.join(dir, CACHE_FILE), JSON.stringify(cache, null, 2) + '\n');
153
+ }
154
+ /**
155
+ * Get or create an ephemeral version, skipping the build if files haven't
156
+ * changed since the last successful ephemeral build.
157
+ *
158
+ * Returns the version (cached or newly created), a cache hit flag, and the
159
+ * computed hash (used by buildEphemeralVersion to write the cache on success).
160
+ */
161
+ export async function getOrCreateEphemeral(client, agentId, files, cwd, summary, options) {
162
+ const hash = hashAgentFiles(files);
163
+ if (options?.noCache) {
164
+ const version = (await client.post(`/agents/${agentId}/versions`, {
165
+ files,
166
+ summary,
167
+ version_type: 'EPHEMERAL',
168
+ }));
169
+ return { version, cached: false, hash };
170
+ }
171
+ const cache = await readEphemeralCache(cwd);
172
+ if (cache && cache.hash === hash) {
173
+ // Verify the cached version still exists on the server
174
+ try {
175
+ const version = (await client.get(`/versions/${cache.version_id}`));
176
+ if (version.validation_status === 'PASSED') {
177
+ return { version, cached: true, hash };
178
+ }
179
+ }
180
+ catch {
181
+ // Version gone or inaccessible — fall through to create new one
182
+ }
183
+ }
184
+ const version = (await client.post(`/agents/${agentId}/versions`, {
185
+ files,
186
+ summary,
187
+ version_type: 'EPHEMERAL',
188
+ }));
189
+ return { version, cached: false, hash };
190
+ }
191
+ /**
192
+ * Build an ephemeral version end-to-end: get-or-create, poll for validation,
193
+ * cache on success, and report build failures.
194
+ *
195
+ * Consolidates the build/poll/cache pattern used by both `guild agent test`
196
+ * and `guild agent chat`.
197
+ */
198
+ export async function buildEphemeralVersion(client, agentId, files, cwd, summary, options) {
199
+ const { version: initial, cached, hash, } = await getOrCreateEphemeral(client, agentId, files, cwd, summary, options);
200
+ if (cached) {
201
+ return { version: initial, cached: true };
202
+ }
203
+ // Poll for validation to complete
204
+ const pollResult = await pollUntilComplete({
205
+ resourceId: initial.id,
206
+ endpoint: `/versions/${initial.id}`,
207
+ isComplete: (r) => r.validation_status !== 'PENDING' && r.validation_status !== 'RUNNING',
208
+ message: 'Building...',
209
+ successMessage: 'Build finished',
210
+ timeoutMessage: 'Build timed out',
211
+ maxAttempts: 120,
212
+ delayMs: 1000,
213
+ });
214
+ if (!pollResult.success || !pollResult.response) {
215
+ throw new BuildTimeoutError();
216
+ }
217
+ const version = pollResult.response;
218
+ // Cache the successful build so we can skip it next time
219
+ if (version.validation_status === 'PASSED') {
220
+ await writeEphemeralCache(cwd, { hash, version_id: version.id });
221
+ }
222
+ if (version.validation_status === 'FAILED') {
223
+ let failedSteps = [];
224
+ try {
225
+ const stepsResponse = await client.get(`/versions/${version.id}/validation/steps`);
226
+ failedSteps = stepsResponse.steps
227
+ .filter((step) => step.status === 'FAILED')
228
+ .map((step) => ({ name: step.name, content: step.content ?? undefined }));
229
+ }
230
+ catch {
231
+ // Could not fetch validation details — throw with empty steps
232
+ }
233
+ throw new BuildFailedError(failedSteps);
234
+ }
235
+ return { version, cached: false };
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // Bundle version
239
+ // ---------------------------------------------------------------------------
240
+ /** Thrown when the specified bundle file cannot be found on disk. */
241
+ export class BundleNotFoundError extends Error {
242
+ constructor(filePath) {
243
+ super(`Bundle file not found: ${filePath}`);
244
+ this.name = 'BundleNotFoundError';
245
+ }
246
+ }
247
+ /**
248
+ * Upload a pre-built bundle as an ephemeral version.
249
+ *
250
+ * The bundle file must be gzip+base64 encoded (the output of
251
+ * `esbuild ... | gzip | base64`). Source files are included for
252
+ * dashboard viewing, but the server skips its own build step because
253
+ * the ready-to-run artifact is already provided.
254
+ */
255
+ export async function buildBundledVersion(client, agentId, bundlePath, cwd, summary) {
256
+ try {
257
+ await fs.access(bundlePath);
258
+ }
259
+ catch {
260
+ throw new BundleNotFoundError(bundlePath);
261
+ }
262
+ const bundle = (await fs.readFile(bundlePath, 'utf-8')).trim();
263
+ let files = [];
264
+ try {
265
+ files = await readAgentFiles(cwd);
266
+ }
267
+ catch {
268
+ // No git or missing required files — proceed with bundle-only upload
269
+ }
270
+ const initial = (await client.post(`/agents/${agentId}/versions`, {
271
+ version_type: 'EPHEMERAL',
272
+ bundle,
273
+ encoding: 'gzip;base64',
274
+ files,
275
+ summary,
276
+ }));
277
+ const pollResult = await pollUntilComplete({
278
+ resourceId: initial.id,
279
+ endpoint: `/versions/${initial.id}`,
280
+ isComplete: (r) => r.validation_status !== 'PENDING' && r.validation_status !== 'RUNNING',
281
+ message: 'Validating bundle...',
282
+ successMessage: 'Bundle validated',
283
+ timeoutMessage: 'Bundle validation timed out',
284
+ maxAttempts: 120,
285
+ delayMs: 1000,
286
+ });
287
+ if (!pollResult.success || !pollResult.response) {
288
+ throw new BuildTimeoutError('Bundle validation timed out.');
289
+ }
290
+ const version = pollResult.response;
291
+ if (version.validation_status === 'FAILED') {
292
+ let failedSteps = [];
293
+ try {
294
+ const stepsResponse = await client.get(`/versions/${version.id}/validation/steps`);
295
+ failedSteps = stepsResponse.steps
296
+ .filter((step) => step.status === 'FAILED')
297
+ .map((step) => ({ name: step.name, content: step.content ?? undefined }));
298
+ }
299
+ catch {
300
+ // Could not fetch validation details — throw with empty steps
301
+ }
302
+ throw new BuildFailedError(failedSteps);
303
+ }
304
+ return { version };
305
+ }
92
306
  //# sourceMappingURL=agent-helpers.js.map
@@ -50,6 +50,8 @@ export function suppressScrollbackClear() {
50
50
  chunk = chunk.replace(CLEAR_TERMINAL, CLEAR_SUBSEQUENT);
51
51
  }
52
52
  }
53
+ if (!originalWrite)
54
+ return false;
53
55
  return originalWrite.apply(this, [chunk, ...args]);
54
56
  };
55
57
  process.stdout.write = patched;
@@ -3,7 +3,7 @@
3
3
  import axios from 'axios';
4
4
  import { getAuthToken, clearAuthToken } from './auth.js';
5
5
  import { retry, debug, GuildCLIError, ErrorCodes } from './errors.js';
6
- import { getGuildcoreUrl } from './config.js';
6
+ import { getUserAgent, getGuildcoreUrl } from './config.js';
7
7
  import { getIapHeaders } from './iap.js';
8
8
  /**
9
9
  * HTTP client for Guild API
@@ -24,6 +24,7 @@ export class GuildAPIClient {
24
24
  headers: {
25
25
  'Content-Type': 'application/json',
26
26
  Accept: 'application/json',
27
+ 'User-Agent': getUserAgent(),
27
28
  },
28
29
  });
29
30
  debug(`API Client initialized: ${this.baseUrl}, retry: ${this.enableRetry}, max retries: ${this.maxRetries}`);
@@ -361,6 +361,43 @@ export interface CredentialsPolicy {
361
361
  updated_at: string;
362
362
  }
363
363
  export type CredentialsPolicyListResponse = PaginatedResponse<CredentialsPolicy>;
364
+ export type TaskStatus = 'CREATED' | 'DISPATCHED' | 'STARTED' | 'RUNNING' | 'WAITING' | 'ERROR' | 'DONE' | 'INTERRUPTED';
365
+ export interface TokenUsage {
366
+ input_tokens: number;
367
+ output_tokens: number;
368
+ cache_read_tokens: number;
369
+ cache_write_tokens: number;
370
+ total_tokens: number;
371
+ llm_call_count: number;
372
+ }
373
+ interface TaskBase {
374
+ id: string;
375
+ entity_type: string;
376
+ status: TaskStatus;
377
+ token_usage: TokenUsage | null;
378
+ created_at: string;
379
+ updated_at: string;
380
+ }
381
+ export interface TaskAgent extends TaskBase {
382
+ entity_type: 'EntTaskAgent';
383
+ agent: {
384
+ name: string;
385
+ full_name?: string;
386
+ } | null;
387
+ version: {
388
+ id: string;
389
+ version_number?: string;
390
+ } | null;
391
+ }
392
+ export interface TaskTool extends TaskBase {
393
+ entity_type: 'EntTaskTool';
394
+ tool_name: string;
395
+ tool_call_id: string;
396
+ request_bytes: number;
397
+ response_bytes: number;
398
+ http_status_code: number;
399
+ }
400
+ export type Task = TaskAgent | TaskTool;
364
401
  /**
365
402
  * Session entity from API responses.
366
403
  * Used by: session list
@@ -1,3 +1,6 @@
1
+ export declare function getCliVersion(): string;
2
+ export declare function isDevBuild(): boolean;
3
+ export declare function getUserAgent(): string;
1
4
  /**
2
5
  * IAP (Identity-Aware Proxy) configuration for internal *.guildai.dev hosts.
3
6
  * Users must run `gcloud auth login` with an authorized Google account to access.
@@ -1,5 +1,38 @@
1
1
  // Copyright 2026 Guild.ai
2
2
  // SPDX-License-Identifier: Apache-2.0
3
+ import { readFileSync } from 'fs';
4
+ import path from 'path';
5
+ const FALLBACK_VERSION = 'unknown';
6
+ let cachedVersion;
7
+ export function getCliVersion() {
8
+ if (!cachedVersion) {
9
+ try {
10
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
11
+ cachedVersion = pkg.version ?? FALLBACK_VERSION;
12
+ }
13
+ catch {
14
+ cachedVersion = FALLBACK_VERSION;
15
+ }
16
+ }
17
+ return cachedVersion;
18
+ }
19
+ export function isDevBuild() {
20
+ try {
21
+ return !__dirname.includes('node_modules');
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export function getUserAgent() {
28
+ try {
29
+ const base = `GuildCLI/${getCliVersion()}`;
30
+ return isDevBuild() ? `${base}/dev` : base;
31
+ }
32
+ catch {
33
+ return 'GuildCLI';
34
+ }
35
+ }
3
36
  /**
4
37
  * IAP configuration for shared.guildai.dev
5
38
  */
@@ -1,5 +1,5 @@
1
1
  import { type Spinner } from './progress.js';
2
- import type { Agent, AgentVersion, Credentials, CredentialsPolicy, Context, Integration, IntegrationVersion, JobStep, Pagination, Session, Workspace, WorkspaceAgent, Trigger } from './api-types.js';
2
+ import type { Agent, AgentVersion, Credentials, CredentialsPolicy, Context, Integration, IntegrationVersion, JobStep, Pagination, Session, Task, Workspace, WorkspaceAgent, Trigger } from './api-types.js';
3
3
  /**
4
4
  * Format an agent list as a human-readable table.
5
5
  * Shared by agent list and agent search commands.
@@ -40,6 +40,11 @@ export declare function formatWorkspaceTable(workspaces: Workspace[], pagination
40
40
  * Used by session list command.
41
41
  */
42
42
  export declare function formatSessionTable(sessions: Session[], pagination: Pagination): void;
43
+ /**
44
+ * Format a task list as a human-readable table.
45
+ * Used by session tasks command.
46
+ */
47
+ export declare function formatTaskTable(tasks: Task[], pagination: Pagination): void;
43
48
  /**
44
49
  * Format a trigger list as a human-readable table.
45
50
  * Used by trigger list command.
@@ -359,6 +359,56 @@ export function formatSessionTable(sessions, pagination) {
359
359
  console.log(chalk.dim(`\nShowing ${showing} of ${pagination.total_count} sessions`));
360
360
  }
361
361
  }
362
+ /**
363
+ * Format a task list as a human-readable table.
364
+ * Used by session tasks command.
365
+ */
366
+ export function formatTaskTable(tasks, pagination) {
367
+ if (tasks.length === 0) {
368
+ console.log(chalk.dim('No tasks found'));
369
+ return;
370
+ }
371
+ const table = new Table({
372
+ columns: [
373
+ { name: 'id', title: 'ID', alignment: 'left' },
374
+ { name: 'name', title: 'NAME', alignment: 'left' },
375
+ { name: 'status', title: 'STATUS', alignment: 'left' },
376
+ { name: 'tokens', title: 'TOKENS', alignment: 'left' },
377
+ { name: 'created', title: 'CREATED', alignment: 'left' },
378
+ ],
379
+ });
380
+ tasks.forEach((task) => {
381
+ const name = task.entity_type === 'EntTaskAgent'
382
+ ? task.agent?.full_name || task.agent?.name || '-'
383
+ : task.tool_name;
384
+ const statusColor = task.status === 'DONE'
385
+ ? chalk.green
386
+ : task.status === 'ERROR'
387
+ ? chalk.red
388
+ : task.status === 'RUNNING' ||
389
+ task.status === 'STARTED' ||
390
+ task.status === 'WAITING'
391
+ ? chalk.yellow
392
+ : chalk.dim;
393
+ table.addRow({
394
+ id: truncate(task.id, 13),
395
+ name,
396
+ status: statusColor(task.status),
397
+ tokens: task.token_usage ? task.token_usage.total_tokens.toLocaleString() : '-',
398
+ created: task.created_at ? formatRelativeTime(task.created_at) : '',
399
+ });
400
+ });
401
+ table.printTable();
402
+ const showing = Math.min(pagination.limit, tasks.length);
403
+ if (pagination.has_more) {
404
+ const nextOffset = pagination.offset + pagination.limit;
405
+ console.log(`\nShowing ${showing} of ${pagination.total_count} tasks. ` +
406
+ chalk.dim(`Use --offset ${nextOffset} to see more.`));
407
+ }
408
+ else if (pagination.total_count > showing) {
409
+ console.log(chalk.dim(`\nShowing ${showing} of ${pagination.total_count} tasks`));
410
+ }
411
+ }
362
412
  /**
363
413
  * Format a trigger list as a human-readable table.
364
414
  * Used by trigger list command.
@@ -54,7 +54,7 @@ export declare function getAgentName(agent: AgentRef | null | undefined): string
54
54
  * Check if a task's agent matches a target agent identifier.
55
55
  *
56
56
  * Target format: "@scope/owner~name" (e.g. "@guildai/guildai~agent-builder") or simple name ("assistant")
57
- * AgentRef format: { name: "agent-builder", full_name: "guildai/agent-builder", ... }
57
+ * AgentRef format: { name: "agent-builder", full_name: "guildai~agent-builder", ... }
58
58
  */
59
59
  export declare function matchesAgent(taskAgent: AgentRef | null | undefined, targetAgent: string): boolean;
60
60
  /** Get display name for a task - agent name or tool name */
@@ -41,7 +41,7 @@ export function getAgentName(agent) {
41
41
  * Check if a task's agent matches a target agent identifier.
42
42
  *
43
43
  * Target format: "@scope/owner~name" (e.g. "@guildai/guildai~agent-builder") or simple name ("assistant")
44
- * AgentRef format: { name: "agent-builder", full_name: "guildai/agent-builder", ... }
44
+ * AgentRef format: { name: "agent-builder", full_name: "guildai~agent-builder", ... }
45
45
  */
46
46
  export function matchesAgent(taskAgent, targetAgent) {
47
47
  if (!taskAgent)
@@ -49,9 +49,11 @@ export function matchesAgent(taskAgent, targetAgent) {
49
49
  // Direct name match for simple identifiers like "assistant"
50
50
  if (taskAgent.name === targetAgent)
51
51
  return true;
52
- // Normalize "@scope/owner~name" -> "owner/name" for full_name comparison
52
+ // Normalize target to match full_name format:
53
+ // 1. Strip npm scope prefix: "@scope/owner~name" -> "owner~name"
54
+ // 2. Convert slash separator to tilde: "owner/name" -> "owner~name"
53
55
  if (taskAgent.full_name) {
54
- const normalized = targetAgent.replace(/^@[^/]+\//, '').replace('~', '/');
56
+ const normalized = targetAgent.replace(/^@[^/]+\//, '').replace('/', '~');
55
57
  if (taskAgent.full_name === normalized)
56
58
  return true;
57
59
  }
@@ -16,4 +16,12 @@ export interface PollResult {
16
16
  * 3. runtime_error from agent tasks — fail fast
17
17
  */
18
18
  export declare function pollForResponse(client: GuildAPIClient, sessionId: string, afterEventId: string | undefined, maxWaitTime?: number): Promise<PollResult>;
19
+ /**
20
+ * Poll for agent response while streaming matching events to stdout as JSONL.
21
+ *
22
+ * Same completion logic as pollForResponse(), but writes each event that
23
+ * passes the filter to stdout so callers get intermediate output (console.log,
24
+ * progress, etc.) in JSON/JSONL automation modes.
25
+ */
26
+ export declare function pollForResponseWithEvents(client: GuildAPIClient, sessionId: string, eventFilter: Set<string>, afterEventId: string | undefined, maxWaitTime?: number): Promise<PollResult>;
19
27
  //# sourceMappingURL=session-polling.d.ts.map
@@ -1,6 +1,7 @@
1
1
  // Copyright 2026 Guild.ai
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { debug, isDebugMode } from './errors.js';
4
+ import { shouldShowEvent } from './event-filter.js';
4
5
  import { fetchEvents } from './session-events-fetch.js';
5
6
  /**
6
7
  * Poll for agent response using from_id cursor.
@@ -50,4 +51,52 @@ export async function pollForResponse(client, sessionId, afterEventId, maxWaitTi
50
51
  }
51
52
  return { response: null, lastEventId: fromId };
52
53
  }
54
+ /**
55
+ * Poll for agent response while streaming matching events to stdout as JSONL.
56
+ *
57
+ * Same completion logic as pollForResponse(), but writes each event that
58
+ * passes the filter to stdout so callers get intermediate output (console.log,
59
+ * progress, etc.) in JSON/JSONL automation modes.
60
+ */
61
+ export async function pollForResponseWithEvents(client, sessionId, eventFilter, afterEventId, maxWaitTime = 60000) {
62
+ const startTime = Date.now();
63
+ let fromId = afterEventId;
64
+ while (Date.now() - startTime < maxWaitTime) {
65
+ const events = await fetchEvents(client, sessionId, { fromId });
66
+ let lastAgentRuntimeDone = null;
67
+ for (const event of events) {
68
+ debug(`pollForResponseWithEvents event: ${event.type}`);
69
+ // Stream matching events to stdout as JSONL
70
+ if (shouldShowEvent(event.type, eventFilter)) {
71
+ process.stdout.write(JSON.stringify(event) + '\n');
72
+ }
73
+ if (event.type === 'agent_notification_message') {
74
+ return { response: event.content.data, lastEventId: event.id };
75
+ }
76
+ if (event.type === 'runtime_done' &&
77
+ event.content !== undefined &&
78
+ event.task &&
79
+ 'agent' in event.task) {
80
+ lastAgentRuntimeDone = JSON.stringify(event.content);
81
+ }
82
+ if (event.type === 'runtime_error' && event.task && 'agent' in event.task) {
83
+ return {
84
+ response: JSON.stringify({ error: event.content }),
85
+ lastEventId: event.id,
86
+ };
87
+ }
88
+ if (event.type === 'agent_console' && isDebugMode()) {
89
+ process.stderr.write(`[console.${event.level}] ${event.content}\n`);
90
+ }
91
+ }
92
+ if (events.length > 0) {
93
+ fromId = events[events.length - 1].id;
94
+ }
95
+ if (lastAgentRuntimeDone !== null) {
96
+ return { response: lastAgentRuntimeDone, lastEventId: fromId };
97
+ }
98
+ await new Promise((resolve) => setTimeout(resolve, 2000));
99
+ }
100
+ return { response: null, lastEventId: fromId };
101
+ }
53
102
  //# sourceMappingURL=session-polling.js.map
@@ -751,7 +751,10 @@ export function getSpinnerThemes() {
751
751
  * @deprecated Use createSpinner() instead
752
752
  */
753
753
  export function getActiveTheme() {
754
- return getSpinnerThemes().get('classic');
754
+ const theme = getSpinnerThemes().get('classic');
755
+ if (!theme)
756
+ throw new Error('Missing classic spinner theme');
757
+ return theme;
755
758
  }
756
759
  /**
757
760
  * @deprecated Use createSpinner() instead
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guildai/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Guild.ai CLI - Build, test, and deploy AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",