@guildai/cli 0.7.1 → 0.8.1

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.
@@ -234,4 +234,73 @@ export async function buildEphemeralVersion(client, agentId, files, cwd, summary
234
234
  }
235
235
  return { version, cached: false };
236
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
+ }
237
306
  //# sourceMappingURL=agent-helpers.js.map
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import axios from 'axios';
4
4
  import { getAuthToken, clearAuthToken } from './auth.js';
5
- import { retry, debug, GuildCLIError, ErrorCodes } from './errors.js';
5
+ import { retry, debug, GuildCLIError, ErrorCodes, shouldClearAuthToken, } from './errors.js';
6
6
  import { getUserAgent, getGuildcoreUrl } from './config.js';
7
7
  import { getIapHeaders } from './iap.js';
8
8
  /**
@@ -69,14 +69,21 @@ export class GuildAPIClient {
69
69
  return response.data;
70
70
  }
71
71
  catch (error) {
72
- // Handle invalid/expired token - clear it and give clear error
73
72
  if (error &&
74
73
  typeof error === 'object' &&
75
74
  'response' in error &&
76
75
  error.response?.status === 401) {
77
- debug('Token rejected by server (401), clearing invalid token');
78
- await clearAuthToken();
79
- throw new GuildCLIError(ErrorCodes.AUTH_TOKEN_INVALID, 'Your authentication token is invalid or expired. Please run: guild auth login');
76
+ const resp = error.response;
77
+ const data = resp?.data;
78
+ const message = typeof data === 'object' && data && 'message' in data
79
+ ? data.message
80
+ : undefined;
81
+ if (shouldClearAuthToken(message)) {
82
+ debug('Guild auth failure (401), clearing token');
83
+ await clearAuthToken();
84
+ throw new GuildCLIError(ErrorCodes.AUTH_TOKEN_INVALID, 'Your authentication token is invalid or expired. Please run: guild auth login');
85
+ }
86
+ debug(`Non-auth 401: ${message ?? 'no message'}`);
80
87
  }
81
88
  throw error;
82
89
  }
@@ -25,6 +25,7 @@ export interface Agent {
25
25
  status: string;
26
26
  description: string | null;
27
27
  is_public?: boolean;
28
+ is_archived?: boolean;
28
29
  git_url?: string;
29
30
  cached_likes_count?: number;
30
31
  created_at?: string;
@@ -244,6 +245,22 @@ export interface TriggerWorkspaceAgent {
244
245
  version_number: string | null;
245
246
  };
246
247
  }
248
+ /**
249
+ * Workspace owner as embedded in trigger API responses.
250
+ */
251
+ export interface TriggerWorkspaceOwner {
252
+ id: string;
253
+ type: 'user' | 'organization';
254
+ name: string;
255
+ }
256
+ /**
257
+ * Workspace as embedded in trigger API responses.
258
+ */
259
+ export interface TriggerWorkspace {
260
+ id: string;
261
+ name: string;
262
+ owner: TriggerWorkspaceOwner;
263
+ }
247
264
  /**
248
265
  * Webhook trigger - fires when a third-party service sends an event.
249
266
  * Used by: trigger list, trigger get, trigger create
@@ -265,6 +282,7 @@ export interface TriggerWebhook {
265
282
  disabled_reason: TriggerDisabledReason | null;
266
283
  agent: TriggerAgent;
267
284
  workspace_agent: TriggerWorkspaceAgent | null;
285
+ workspace: TriggerWorkspace;
268
286
  }
269
287
  /**
270
288
  * Time trigger - fires on a schedule.
@@ -284,6 +302,7 @@ export interface TriggerTime {
284
302
  disabled_reason: TriggerDisabledReason | null;
285
303
  agent: TriggerAgent;
286
304
  workspace_agent: TriggerWorkspaceAgent | null;
305
+ workspace: TriggerWorkspace;
287
306
  }
288
307
  /**
289
308
  * Discriminated union of trigger types.
@@ -361,6 +380,43 @@ export interface CredentialsPolicy {
361
380
  updated_at: string;
362
381
  }
363
382
  export type CredentialsPolicyListResponse = PaginatedResponse<CredentialsPolicy>;
383
+ export type TaskStatus = 'CREATED' | 'DISPATCHED' | 'STARTED' | 'RUNNING' | 'WAITING' | 'ERROR' | 'DONE' | 'INTERRUPTED';
384
+ export interface TokenUsage {
385
+ input_tokens: number;
386
+ output_tokens: number;
387
+ cache_read_tokens: number;
388
+ cache_write_tokens: number;
389
+ total_tokens: number;
390
+ llm_call_count: number;
391
+ }
392
+ interface TaskBase {
393
+ id: string;
394
+ entity_type: string;
395
+ status: TaskStatus;
396
+ token_usage: TokenUsage | null;
397
+ created_at: string;
398
+ updated_at: string;
399
+ }
400
+ export interface TaskAgent extends TaskBase {
401
+ entity_type: 'EntTaskAgent';
402
+ agent: {
403
+ name: string;
404
+ full_name?: string;
405
+ } | null;
406
+ version: {
407
+ id: string;
408
+ version_number?: string;
409
+ } | null;
410
+ }
411
+ export interface TaskTool extends TaskBase {
412
+ entity_type: 'EntTaskTool';
413
+ tool_name: string;
414
+ tool_call_id: string;
415
+ request_bytes: number;
416
+ response_bytes: number;
417
+ http_status_code: number;
418
+ }
419
+ export type Task = TaskAgent | TaskTool;
364
420
  /**
365
421
  * Session entity from API responses.
366
422
  * Used by: session list
@@ -40,10 +40,6 @@ export declare function startDeviceFlow(authUrl?: string, returnUrl?: string, re
40
40
  * @returns Access token if successful, null if failed/expired
41
41
  */
42
42
  export declare function pollForToken(authUrl: string, code: string, interval: number): Promise<string | null>;
43
- /**
44
- * Show beta invitation guidance for users who aren't authenticated.
45
- */
46
- export declare function showBetaGuidance(): void;
47
43
  /**
48
44
  * Perform complete login flow
49
45
  * @param returnUrl - Optional custom URL to redirect to after authentication
package/dist/lib/auth.js CHANGED
@@ -185,15 +185,6 @@ export async function pollForToken(authUrl, code, interval) {
185
185
  spinner.fail('Authorization timed out');
186
186
  return null;
187
187
  }
188
- /**
189
- * Show beta invitation guidance for users who aren't authenticated.
190
- */
191
- export function showBetaGuidance() {
192
- console.error('');
193
- console.error(chalk.dim("Don't have an account? Guild.ai is in closed beta."));
194
- console.error(chalk.dim('Request an invitation at hello@guild.ai'));
195
- console.error('');
196
- }
197
188
  /**
198
189
  * Perform complete login flow
199
190
  * @param returnUrl - Optional custom URL to redirect to after authentication
@@ -242,7 +233,6 @@ export async function login(returnUrl, returnLabel, nonInteractive) {
242
233
  await saveAuthToken(token);
243
234
  return true;
244
235
  }
245
- showBetaGuidance();
246
236
  return false;
247
237
  }
248
238
  catch (error) {
@@ -254,7 +244,6 @@ export async function login(returnUrl, returnLabel, nonInteractive) {
254
244
  if (formattedError.code) {
255
245
  console.error(chalk.dim(`\nError code: ${formattedError.code}`));
256
246
  }
257
- showBetaGuidance();
258
247
  return false;
259
248
  }
260
249
  }
@@ -50,10 +50,6 @@ export declare function debug(message: string, ...args: unknown[]): void;
50
50
  * Format error for output
51
51
  */
52
52
  export declare function formatError(error: string, details: string, code?: string, stack?: string, suggestions?: string[]): CLIError;
53
- /**
54
- * Print error to stdout and exit
55
- */
56
- export declare function exitWithError(error: string, details: string, code?: string, exitCode?: number): never;
57
53
  /**
58
54
  * Get user-friendly error message for common error codes
59
55
  */
@@ -74,6 +70,11 @@ export declare function isIapError(error: unknown): boolean;
74
70
  * Check if an error is a Guild auth error
75
71
  */
76
72
  export declare function isAuthError(error: unknown): boolean;
73
+ /**
74
+ * Check if a 401 response message indicates the stored auth token should be cleared.
75
+ * Uses exact match — only clear for known Guild auth failures.
76
+ */
77
+ export declare function shouldClearAuthToken(responseMessage: string | undefined): boolean;
77
78
  /**
78
79
  * Check if an error is fatal (should not be retried)
79
80
  *
@@ -82,14 +82,6 @@ export function formatError(error, details, code, stack, suggestions) {
82
82
  }
83
83
  return result;
84
84
  }
85
- /**
86
- * Print error to stdout and exit
87
- */
88
- export function exitWithError(error, details, code, exitCode = 1) {
89
- const formattedError = formatError(error, details, code);
90
- console.log(JSON.stringify(formattedError, null, 2));
91
- process.exit(exitCode);
92
- }
93
85
  /**
94
86
  * Get user-friendly error message for common error codes
95
87
  */
@@ -280,9 +272,15 @@ const IAP_ERROR_PATTERNS = [
280
272
  'gcloud returned empty token',
281
273
  ];
282
274
  /**
283
- * Patterns that indicate Guild auth errors (not IAP)
275
+ * Messages from Guildcore that indicate a real Guild auth failure.
276
+ * Source: python/guildcore/core/session.py and routing.py
284
277
  */
285
- const AUTH_ERROR_PATTERNS = ['Not authenticated', 'Authentication required'];
278
+ const GUILD_AUTH_FAILURE_MESSAGES = [
279
+ 'Access token expired',
280
+ 'Access token invalid',
281
+ 'Session expired',
282
+ 'Not authenticated',
283
+ ];
286
284
  /**
287
285
  * Check if an error is an IAP (Google Identity-Aware Proxy) error
288
286
  */
@@ -295,7 +293,16 @@ export function isIapError(error) {
295
293
  */
296
294
  export function isAuthError(error) {
297
295
  const message = error instanceof Error ? error.message : String(error);
298
- return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
296
+ return GUILD_AUTH_FAILURE_MESSAGES.some((pattern) => message.includes(pattern));
297
+ }
298
+ /**
299
+ * Check if a 401 response message indicates the stored auth token should be cleared.
300
+ * Uses exact match — only clear for known Guild auth failures.
301
+ */
302
+ export function shouldClearAuthToken(responseMessage) {
303
+ if (!responseMessage)
304
+ return false;
305
+ return GUILD_AUTH_FAILURE_MESSAGES.some((msg) => msg === responseMessage);
299
306
  }
300
307
  /**
301
308
  * Check if an error is fatal (should not be retried)
@@ -1,10 +1,10 @@
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.
6
6
  */
7
- export declare function formatAgentTable(agents: Agent[], pagination: Pagination): void;
7
+ export declare function formatAgentTable(agents: Agent[], pagination: Pagination, showArchived?: boolean): void;
8
8
  /**
9
9
  * Format an integration list as a human-readable table.
10
10
  * Used by integration list command.
@@ -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.
@@ -42,7 +42,7 @@ function truncate(str, maxLen) {
42
42
  * Format an agent list as a human-readable table.
43
43
  * Shared by agent list and agent search commands.
44
44
  */
45
- export function formatAgentTable(agents, pagination) {
45
+ export function formatAgentTable(agents, pagination, showArchived = false) {
46
46
  if (agents.length === 0) {
47
47
  console.log(chalk.dim('No agents found'));
48
48
  return;
@@ -61,10 +61,14 @@ export function formatAgentTable(agents, pagination) {
61
61
  });
62
62
  const base = getDashboardUrl();
63
63
  agents.forEach((agent) => {
64
- const name = agent.full_name || agent.name;
64
+ const rawName = agent.full_name || agent.name;
65
65
  const agentUrl = agent.full_name ? `${base}/agents/${agent.full_name}` : '';
66
+ const linkedName = agentUrl ? hyperlink(rawName, agentUrl) : rawName;
67
+ const displayName = showArchived && agent.is_archived
68
+ ? linkedName + chalk.dim(' [archived]')
69
+ : linkedName;
66
70
  table.addRow({
67
- name: agentUrl ? hyperlink(name, agentUrl) : name,
71
+ name: displayName,
68
72
  owner: agent.owner?.name || '',
69
73
  description: truncate(agent.description || '', 40),
70
74
  updated: agent.updated_at ? formatRelativeTime(agent.updated_at) : '',
@@ -359,6 +363,56 @@ export function formatSessionTable(sessions, pagination) {
359
363
  console.log(chalk.dim(`\nShowing ${showing} of ${pagination.total_count} sessions`));
360
364
  }
361
365
  }
366
+ /**
367
+ * Format a task list as a human-readable table.
368
+ * Used by session tasks command.
369
+ */
370
+ export function formatTaskTable(tasks, pagination) {
371
+ if (tasks.length === 0) {
372
+ console.log(chalk.dim('No tasks found'));
373
+ return;
374
+ }
375
+ const table = new Table({
376
+ columns: [
377
+ { name: 'id', title: 'ID', alignment: 'left' },
378
+ { name: 'name', title: 'NAME', alignment: 'left' },
379
+ { name: 'status', title: 'STATUS', alignment: 'left' },
380
+ { name: 'tokens', title: 'TOKENS', alignment: 'left' },
381
+ { name: 'created', title: 'CREATED', alignment: 'left' },
382
+ ],
383
+ });
384
+ tasks.forEach((task) => {
385
+ const name = task.entity_type === 'EntTaskAgent'
386
+ ? task.agent?.full_name || task.agent?.name || '-'
387
+ : task.tool_name;
388
+ const statusColor = task.status === 'DONE'
389
+ ? chalk.green
390
+ : task.status === 'ERROR'
391
+ ? chalk.red
392
+ : task.status === 'RUNNING' ||
393
+ task.status === 'STARTED' ||
394
+ task.status === 'WAITING'
395
+ ? chalk.yellow
396
+ : chalk.dim;
397
+ table.addRow({
398
+ id: truncate(task.id, 13),
399
+ name,
400
+ status: statusColor(task.status),
401
+ tokens: task.token_usage ? task.token_usage.total_tokens.toLocaleString() : '-',
402
+ created: task.created_at ? formatRelativeTime(task.created_at) : '',
403
+ });
404
+ });
405
+ table.printTable();
406
+ const showing = Math.min(pagination.limit, tasks.length);
407
+ if (pagination.has_more) {
408
+ const nextOffset = pagination.offset + pagination.limit;
409
+ console.log(`\nShowing ${showing} of ${pagination.total_count} tasks. ` +
410
+ chalk.dim(`Use --offset ${nextOffset} to see more.`));
411
+ }
412
+ else if (pagination.total_count > showing) {
413
+ console.log(chalk.dim(`\nShowing ${showing} of ${pagination.total_count} tasks`));
414
+ }
415
+ }
362
416
  /**
363
417
  * Format a trigger list as a human-readable table.
364
418
  * Used by trigger list command.
@@ -388,8 +442,10 @@ export function formatTriggerTable(triggers, pagination) {
388
442
  const agentUrl = trigger.agent?.full_name
389
443
  ? `${base}/agents/${trigger.agent.full_name}`
390
444
  : '';
445
+ const ownerSegment = trigger.workspace.owner.type === 'organization' ? 'organizations' : 'users';
446
+ const triggerUrl = `${base}/${ownerSegment}/${trigger.workspace.owner.name}/workspaces/${trigger.workspace.name}/triggers`;
391
447
  table.addRow({
392
- id: hyperlink(trigger.id, `${base}/triggers/${trigger.id}/sessions`),
448
+ id: hyperlink(trigger.id, triggerUrl),
393
449
  type: trigger.type,
394
450
  agent: agentUrl ? hyperlink(agentName, agentUrl) : agentName,
395
451
  status,
@@ -71,28 +71,4 @@ export interface PollResult<T = unknown> {
71
71
  * ```
72
72
  */
73
73
  export declare function pollUntilComplete<T = unknown>(options: PollOptions<T>): Promise<PollResult<T>>;
74
- /**
75
- * Agent response with status field
76
- */
77
- interface AgentStatusResponse {
78
- status: string;
79
- [key: string]: unknown;
80
- }
81
- /**
82
- * Poll an agent until it reaches a specific status
83
- *
84
- * @param agentId - Agent ID to poll
85
- * @param targetStatus - Status to wait for (e.g., 'READY')
86
- * @param options - Additional polling options
87
- *
88
- * @example
89
- * ```typescript
90
- * const result = await pollAgentStatus(agentId, 'READY');
91
- * if (result.success) {
92
- * console.log('Agent is ready');
93
- * }
94
- * ```
95
- */
96
- export declare function pollAgentStatus(agentId: string, targetStatus: string, options?: Partial<Omit<PollOptions<AgentStatusResponse>, 'resourceId' | 'endpoint' | 'isComplete'>>): Promise<PollResult<AgentStatusResponse>>;
97
- export {};
98
74
  //# sourceMappingURL=polling.d.ts.map
@@ -64,30 +64,4 @@ export async function pollUntilComplete(options) {
64
64
  attempts,
65
65
  };
66
66
  }
67
- /**
68
- * Poll an agent until it reaches a specific status
69
- *
70
- * @param agentId - Agent ID to poll
71
- * @param targetStatus - Status to wait for (e.g., 'READY')
72
- * @param options - Additional polling options
73
- *
74
- * @example
75
- * ```typescript
76
- * const result = await pollAgentStatus(agentId, 'READY');
77
- * if (result.success) {
78
- * console.log('Agent is ready');
79
- * }
80
- * ```
81
- */
82
- export async function pollAgentStatus(agentId, targetStatus, options = {}) {
83
- return pollUntilComplete({
84
- resourceId: agentId,
85
- endpoint: `/agents/${agentId}`,
86
- isComplete: (response) => response.status === targetStatus,
87
- message: options.message || 'Waiting for agent initialization...',
88
- successMessage: options.successMessage || 'Agent initialization complete',
89
- timeoutMessage: options.timeoutMessage || 'Agent initialization timed out after 60 seconds',
90
- ...options,
91
- });
92
- }
93
67
  //# sourceMappingURL=polling.js.map
package/dist/mcp/tools.js CHANGED
@@ -387,7 +387,7 @@ export function registerTools(server, apiClient, defaultWorkspaceId, debug) {
387
387
  server.tool('guild_get_agent', 'Get details for a specific Guild agent', {
388
388
  agent_id: z
389
389
  .string()
390
- .describe('The agent ID or full name (e.g. owner/agent-name)'),
390
+ .describe('The agent ID or full name (e.g. owner~agent-name)'),
391
391
  }, async ({ agent_id }) => {
392
392
  debugLog(debug, `guild_get_agent: ${agent_id}`);
393
393
  try {
@@ -489,7 +489,7 @@ export function registerTools(server, apiClient, defaultWorkspaceId, debug) {
489
489
  agent: z
490
490
  .string()
491
491
  .optional()
492
- .describe('Agent identifier (e.g. owner/agent-name). Uses workspace default if not specified'),
492
+ .describe('Agent identifier (e.g. owner~agent-name). Uses workspace default if not specified'),
493
493
  workspace_id: workspaceIdParam,
494
494
  }, async ({ message, agent, workspace_id }) => {
495
495
  const wsId = workspace_id || defaultWorkspaceId;