@guildai/cli 0.7.0 → 0.7.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.
Files changed (41) 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 +10 -3
  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 +59 -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/workspace/get.js +1 -1
  27. package/dist/commands/workspace/list.js +28 -6
  28. package/dist/commands/workspace/select.js +40 -9
  29. package/dist/components/TaskView.js +2 -2
  30. package/dist/lib/agent-helpers.d.ts +59 -2
  31. package/dist/lib/agent-helpers.js +153 -8
  32. package/dist/lib/alternate-screen.js +2 -0
  33. package/dist/lib/api-client.js +2 -1
  34. package/dist/lib/config.d.ts +3 -0
  35. package/dist/lib/config.js +33 -0
  36. package/dist/lib/session-events.d.ts +1 -1
  37. package/dist/lib/session-events.js +5 -3
  38. package/dist/lib/session-polling.d.ts +8 -0
  39. package/dist/lib/session-polling.js +49 -0
  40. package/dist/lib/spinners.js +4 -1
  41. package/package.json +1 -1
@@ -13,7 +13,7 @@ import chalk from 'chalk';
13
13
  import { readFileSync } from 'fs';
14
14
  import path from 'path';
15
15
  import { fileURLToPath } from 'url';
16
- import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName, matchesAgent, getAgentName, } from '../lib/session-events.js';
16
+ import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName, getAgentName, } from '../lib/session-events.js';
17
17
  import { printResumeHint, fetchSession, fetchSessionEvents, eventsToDisplayMessages, } from '../lib/session-resume.js';
18
18
  import { DEFAULT_EVENT_TYPES, parseEventFilter, shouldShowEvent, } from '../lib/event-filter.js';
19
19
  import { fetchEvents, fetchTasks } from '../lib/session-events-fetch.js';
@@ -1087,24 +1087,37 @@ export function createChatCommand() {
1087
1087
  else {
1088
1088
  inactivityCounter++;
1089
1089
  }
1090
- // Check if we got a completion response
1091
- // Look for runtime_done events from the ROOT task (the agent we're chatting with)
1092
- // For orchestrating agents like agent-builder that spawn child tasks, we need to
1093
- // wait for the root agent's completion, not just any runtime_done event.
1094
- const targetAgent = options.agent || 'assistant';
1095
- const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' &&
1096
- matchesAgent(e.task?.agent, targetAgent) &&
1097
- !e.task?.parent_task_id // Root task has no parent
1098
- );
1099
- // Also check for agent_notification_message events from the root agent
1100
- const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
1101
- matchesAgent(e.task?.agent, targetAgent) &&
1102
- !e.task?.parent_task_id);
1090
+ const isRootTask = (e) => !e.task?.parent_task;
1091
+ const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' && isRootTask(e));
1092
+ const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' && isRootTask(e));
1093
+ const hasRootTaskError = allEvents.some((e) => e.type === 'runtime_error' && isRootTask(e));
1103
1094
  // Check for a ui_prompt request... that ends the game.
1104
1095
  const hasUIPromptMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
1105
1096
  e.task?.tool_name === 'ui_prompt');
1097
+ if (hasRootTaskError) {
1098
+ debug('Found error event from root agent, exiting --once mode');
1099
+ const errorEvents = allEvents.filter((e) => e.type === 'runtime_error' || e.type === 'agent_notification_error');
1100
+ if (errorEvents.length > 0 && !options.mode) {
1101
+ const lastError = errorEvents[errorEvents.length - 1];
1102
+ const content = lastError.content;
1103
+ if (content?.data) {
1104
+ console.error(chalk.red(`Error: ${content.data}`));
1105
+ }
1106
+ else {
1107
+ console.error(chalk.red('Agent failed to start'));
1108
+ }
1109
+ }
1110
+ else if (options.mode === 'json') {
1111
+ console.log(JSON.stringify({
1112
+ session_id: session.id,
1113
+ events: allEvents,
1114
+ error: true,
1115
+ }, null, 2));
1116
+ }
1117
+ process.exit(1);
1118
+ }
1106
1119
  if (hasRootTaskDone || hasAgentMessage || hasUIPromptMessage) {
1107
- debug(`Found completion event from root agent (${targetAgent}), exiting --once mode`);
1120
+ debug('Found completion event from root agent, exiting --once mode');
1108
1121
  await outputOnceResult(session.id, allEvents, options.mode);
1109
1122
  process.exit(0);
1110
1123
  }
@@ -27,13 +27,13 @@ export function createConfigListCommand() {
27
27
  output.progress(chalk.dim(' guild workspace select'));
28
28
  return;
29
29
  }
30
- if (hasGlobal) {
30
+ if (config.global && hasGlobal) {
31
31
  output.progress(chalk.bold('Global config') + chalk.dim(' (~/.guild/config.json):'));
32
32
  for (const [key, value] of Object.entries(config.global)) {
33
33
  output.progress(` ${key}: ${chalk.cyan(String(value))}`);
34
34
  }
35
35
  }
36
- if (hasLocal) {
36
+ if (config.local && hasLocal) {
37
37
  if (hasGlobal)
38
38
  output.progress('');
39
39
  output.progress(chalk.bold('Local config') + chalk.dim(' (guild.json):'));
@@ -17,7 +17,7 @@ export function createIntegrationOperationCreateCommand() {
17
17
  cmd
18
18
  .description('Create operation(s) for an integration version')
19
19
  .argument('<id_or_name>', 'Integration ID or name (owner~name)')
20
- .option('--version <semver>', 'Specific version, e.g. 1.0.0')
20
+ .option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
21
21
  .option('--operation <name>', 'Operation identifier, e.g. list_users')
22
22
  .option('--method <method>', 'HTTP method: GET, POST, PUT, PATCH, DELETE')
23
23
  .option('--path <path>', 'REST path, e.g. /{owner}/{repo}/issues')
@@ -35,7 +35,7 @@ export function createIntegrationOperationCreateCommand() {
35
35
  process.exit(1);
36
36
  }
37
37
  const client = new GuildAPIClient();
38
- const versionId = await resolveVersionId(client, identifier, options.version);
38
+ const versionId = await resolveVersionId(client, identifier, options.versionNumber);
39
39
  if (options.openapi) {
40
40
  // OpenAPI mode
41
41
  if (!existsSync(options.openapi)) {
@@ -14,7 +14,7 @@ export function createIntegrationOperationListCommand() {
14
14
  cmd
15
15
  .description('List operations for an integration version')
16
16
  .argument('<id_or_name>', 'Integration ID or name (owner~name)')
17
- .option('--version <semver>', 'Specific version, e.g. 1.0.0')
17
+ .option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
18
18
  .option('--limit <number>', 'Number of results to return', '100')
19
19
  .option('--offset <number>', 'Offset for pagination', '0')
20
20
  .action(async (identifier, options) => {
@@ -26,7 +26,7 @@ export function createIntegrationOperationListCommand() {
26
26
  process.exit(1);
27
27
  }
28
28
  const client = new GuildAPIClient();
29
- const versionId = await resolveVersionId(client, identifier, options.version);
29
+ const versionId = await resolveVersionId(client, identifier, options.versionNumber);
30
30
  const params = new URLSearchParams();
31
31
  params.append('limit', options.limit);
32
32
  params.append('offset', options.offset);
@@ -63,7 +63,7 @@ export function createIntegrationUpdateCommand() {
63
63
  hasUpdates = true;
64
64
  }
65
65
  if (options.baseUrl !== undefined) {
66
- body.protocol_config = { base_url: options.baseUrl };
66
+ body.protocol_config = { protocol: 'REST', base_url: options.baseUrl };
67
67
  hasUpdates = true;
68
68
  }
69
69
  // Auth config updates need the discriminator
@@ -34,7 +34,7 @@ export function createIntegrationVersionGetCommand() {
34
34
  cmd
35
35
  .description('Get version details')
36
36
  .argument('<id_or_name>', 'Integration ID or name (owner~name)')
37
- .option('--version <semver>', 'Specific version, e.g. 1.0.0')
37
+ .option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
38
38
  .action(async (identifier, options) => {
39
39
  const output = createOutputWriter();
40
40
  try {
@@ -44,7 +44,7 @@ export function createIntegrationVersionGetCommand() {
44
44
  process.exit(1);
45
45
  }
46
46
  const client = new GuildAPIClient();
47
- const versionId = await resolveVersionId(client, identifier, options.version);
47
+ const versionId = await resolveVersionId(client, identifier, options.versionNumber);
48
48
  const response = await client.get(`/integration_versions/${versionId}`);
49
49
  if (getOutputMode() === 'json') {
50
50
  output.data(response);
@@ -23,7 +23,7 @@ export function createIntegrationVersionPublishCommand() {
23
23
  cmd
24
24
  .description('Publish a built version')
25
25
  .argument('<id_or_name>', 'Integration ID or name (owner~name)')
26
- .option('--version <semver>', 'Specific version to publish, e.g. 1.0.0')
26
+ .option('--version-number <semver>', 'Specific version to publish, e.g. 1.0.0')
27
27
  .action(async (identifier, options) => {
28
28
  const output = createOutputWriter();
29
29
  try {
@@ -33,7 +33,7 @@ export function createIntegrationVersionPublishCommand() {
33
33
  process.exit(1);
34
34
  }
35
35
  const client = new GuildAPIClient();
36
- const versionId = await resolveVersionId(client, identifier, options.version);
36
+ const versionId = await resolveVersionId(client, identifier, options.versionNumber);
37
37
  // Get current version info for display
38
38
  const currentVersion = await client.get(`/integration_versions/${versionId}`);
39
39
  const versionDisplay = currentVersion.version_number || versionId;
@@ -22,7 +22,7 @@ export function createIntegrationVersionTestCommand() {
22
22
  cmd
23
23
  .description('Test an endpoint invocation')
24
24
  .argument('<id_or_name>', 'Integration ID or name (owner~name)')
25
- .option('--version <semver>', 'Specific version, e.g. 1.0.0')
25
+ .option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
26
26
  .requiredOption('--operation <name>', 'Operation to test, e.g. list_users')
27
27
  .option('--account <name>', 'Account name to resolve credential from')
28
28
  .option('--input-path <json>', 'JSON object of path parameters')
@@ -50,7 +50,7 @@ export function createIntegrationVersionTestCommand() {
50
50
  body.input_body = parseJsonFlag(options.inputBody, '--input-body');
51
51
  }
52
52
  const client = new GuildAPIClient();
53
- const versionId = await resolveVersionId(client, identifier, options.version);
53
+ const versionId = await resolveVersionId(client, identifier, options.versionNumber);
54
54
  if (options.account) {
55
55
  const creds = await client.get(`/accounts/${options.account}/credentials?integration=${encodeURIComponent(identifier)}`);
56
56
  if (creds.items.length === 0) {
@@ -3,6 +3,7 @@
3
3
  import { Command } from 'commander';
4
4
  import { GuildAPIClient } from '../../lib/api-client.js';
5
5
  import { getAuthToken } from '../../lib/auth.js';
6
+ import { parseEventFilter } from '../../lib/event-filter.js';
6
7
  import { handleAxiosError } from '../../lib/errors.js';
7
8
  import { createOutputWriter } from '../../lib/output.js';
8
9
  export function createSessionEventsCommand() {
@@ -10,7 +11,7 @@ export function createSessionEventsCommand() {
10
11
  cmd
11
12
  .description('List events in a session')
12
13
  .argument('<session-id>', 'Session ID')
13
- .option('--types <types>', 'Comma-separated event types to filter (e.g., user_message,agent_notification_message)')
14
+ .option('--events <types>', 'Event types to show. Shorthands: none, user, system, all, or comma-separated type names')
14
15
  .option('--limit <number>', 'Number of results to return', '100')
15
16
  .option('--offset <number>', 'Offset for pagination', '0')
16
17
  .action(async (sessionId, options) => {
@@ -25,8 +26,11 @@ export function createSessionEventsCommand() {
25
26
  const params = new URLSearchParams();
26
27
  params.append('limit', options.limit);
27
28
  params.append('offset', options.offset);
28
- if (options.types) {
29
- params.append('types', options.types);
29
+ if (options.events) {
30
+ const filter = parseEventFilter(options.events);
31
+ if (filter.size > 0) {
32
+ params.append('types', [...filter].join(','));
33
+ }
30
34
  }
31
35
  const response = await client.get(`/sessions/${sessionId}/events?${params.toString()}`);
32
36
  output.data(response);
@@ -8,7 +8,7 @@ export function createWorkspaceGetCommand() {
8
8
  const cmd = new Command('get');
9
9
  cmd
10
10
  .description('Get workspace details')
11
- .argument('<identifier>', 'Workspace ID or full name (e.g., owner/workspace-name)')
11
+ .argument('<identifier>', 'Workspace ID or full name (e.g., owner~workspace-name)')
12
12
  .action(async (id) => {
13
13
  const output = createOutputWriter();
14
14
  try {
@@ -11,16 +11,38 @@ export function createWorkspaceListCommand() {
11
11
  .description('List workspaces')
12
12
  .option('--limit <number>', 'Number of results to return', '20')
13
13
  .option('--offset <number>', 'Offset for pagination', '0')
14
+ .option('--owner <name>', 'Filter workspaces by owner name (case-insensitive)')
14
15
  .action(async (options) => {
15
16
  const output = createOutputWriter();
16
17
  try {
17
18
  const client = new GuildAPIClient();
18
- const params = new URLSearchParams();
19
- // Use filter=all to get workspaces from all orgs user is a member of
20
- params.append('filter', 'all');
21
- params.append('limit', options.limit);
22
- params.append('offset', options.offset);
23
- const response = await client.get(`/me/workspaces?${params.toString()}`);
19
+ let response;
20
+ if (options.owner) {
21
+ // When filtering by owner, fetch ALL workspaces first so the
22
+ // client-side filter is applied to the complete dataset, not just
23
+ // one page. The backend's `filter` param only recognises "all" vs
24
+ // personal; owner-name filtering is not supported server-side.
25
+ const allWorkspaces = await client.fetchAll('/me/workspaces?filter=all');
26
+ const ownerLower = options.owner.toLowerCase();
27
+ const filtered = allWorkspaces.filter((w) => w.owner?.name?.toLowerCase() === ownerLower);
28
+ response = {
29
+ items: filtered,
30
+ pagination: {
31
+ total_count: filtered.length,
32
+ limit: filtered.length,
33
+ offset: 0,
34
+ has_more: false,
35
+ },
36
+ };
37
+ }
38
+ else {
39
+ const params = new URLSearchParams();
40
+ // Use filter=all to get workspaces from all orgs the user is a member of.
41
+ params.append('filter', 'all');
42
+ params.append('limit', options.limit);
43
+ params.append('offset', options.offset);
44
+ response = await client.get(`/me/workspaces?${params.toString()}`);
45
+ }
24
46
  if (getOutputMode() === 'json') {
25
47
  console.log(JSON.stringify(response, null, 2));
26
48
  }
@@ -35,8 +35,10 @@ export function createWorkspaceSelectCommand() {
35
35
  cmd
36
36
  .description('Select default workspace for agent testing')
37
37
  .argument('[workspace]', 'Workspace name or ID to select directly')
38
- .action(async (workspaceArg) => {
38
+ .option('--owner <name>', 'Filter workspaces by owner name (case-insensitive)')
39
+ .action(async (workspaceArg, options = {}) => {
39
40
  const output = createOutputWriter();
41
+ const ownerFilter = options.owner;
40
42
  try {
41
43
  const client = new GuildAPIClient();
42
44
  // If a workspace argument was provided, use server-side search to find it
@@ -45,19 +47,34 @@ export function createWorkspaceSelectCommand() {
45
47
  // The backend searches only the "name" column via ILIKE, so full_name (owner/name)
46
48
  // and ID lookups may not return results. Extract just the name part for search,
47
49
  // and if still no match, fall back to fetching the full list.
50
+ // Always use filter=all — the backend's `filter` param only recognises "all"
51
+ // vs personal; owner-name filtering is applied client-side below.
48
52
  const searchTerm = workspaceArg.includes('/')
49
- ? workspaceArg.split('/').pop()
53
+ ? (workspaceArg.split('/').pop() ?? workspaceArg)
50
54
  : workspaceArg;
51
55
  const searchResults = await client.get(`/me/workspaces?filter=all&search=${encodeURIComponent(searchTerm)}&limit=100`);
52
- let workspace = searchResults.items.find((w) => matchesWorkspaceArg(w, workspaceArg));
56
+ let candidates = searchResults.items;
57
+ // Apply client-side owner filter on search results
58
+ if (ownerFilter) {
59
+ candidates = candidates.filter((w) => w.owner?.name?.toLowerCase() === ownerFilter.toLowerCase());
60
+ }
53
61
  // Search didn't find it (could be an ID lookup, or search term didn't match).
54
62
  // Fall back to fetching all workspaces via pagination.
55
- if (!workspace) {
63
+ const directMatch = candidates.find((w) => matchesWorkspaceArg(w, workspaceArg));
64
+ if (!directMatch) {
56
65
  const allWorkspaces = await client.fetchAll('/me/workspaces?filter=all');
57
- workspace = allWorkspaces.find((w) => matchesWorkspaceArg(w, workspaceArg));
66
+ candidates = ownerFilter
67
+ ? allWorkspaces.filter((w) => w.owner?.name?.toLowerCase() === ownerFilter.toLowerCase())
68
+ : allWorkspaces;
58
69
  }
70
+ const workspace = candidates.find((w) => matchesWorkspaceArg(w, workspaceArg));
59
71
  if (!workspace) {
60
- output.error(`Workspace "${workspaceArg}" not found.`, 'Run: guild workspace list');
72
+ if (ownerFilter) {
73
+ output.error(`Workspace "${workspaceArg}" not found for owner "${ownerFilter}".`, 'Run: guild workspace list');
74
+ }
75
+ else {
76
+ output.error(`Workspace "${workspaceArg}" not found.`, 'Run: guild workspace list');
77
+ }
61
78
  process.exit(1);
62
79
  }
63
80
  const target = await saveWorkspaceConfig(workspace.id, workspace.name);
@@ -73,10 +90,20 @@ export function createWorkspaceSelectCommand() {
73
90
  output.error('Interactive mode requires a terminal.', 'Provide a workspace argument:\n guild workspace select <name|id>');
74
91
  process.exit(1);
75
92
  }
76
- // Interactive mode: fetch all workspaces across all pages
77
- const workspaces = await client.fetchAll('/me/workspaces?filter=all');
93
+ // Interactive mode: fetch all workspaces across all pages.
94
+ // Always use filter=all; owner-name filtering is applied client-side
95
+ // (the backend's `filter` param only recognises "all" vs personal).
96
+ let workspaces = await client.fetchAll('/me/workspaces?filter=all');
97
+ if (ownerFilter) {
98
+ workspaces = workspaces.filter((w) => w.owner?.name?.toLowerCase() === ownerFilter.toLowerCase());
99
+ }
78
100
  if (workspaces.length === 0) {
79
- output.error('No workspaces found.', 'Create a workspace first:\n guild workspace create <name>');
101
+ if (ownerFilter) {
102
+ output.error(`No workspaces found for owner "${ownerFilter}".`, 'Run: guild workspace list');
103
+ }
104
+ else {
105
+ output.error('No workspaces found.', 'Create a workspace first:\n guild workspace create <name>');
106
+ }
80
107
  process.exit(1);
81
108
  }
82
109
  // Resolve the currently selected workspace (if any)
@@ -101,6 +128,10 @@ export function createWorkspaceSelectCommand() {
101
128
  },
102
129
  });
103
130
  const selectedWorkspace = workspaces.find((w) => w.id === selectedId);
131
+ if (!selectedWorkspace) {
132
+ output.error('Selected workspace not found');
133
+ process.exit(1);
134
+ }
104
135
  const target = await saveWorkspaceConfig(selectedWorkspace.id, selectedWorkspace.name);
105
136
  if (target === 'local') {
106
137
  output.success(`Workspace set for this agent: ${formatWorkspaceDisplay(selectedWorkspace)}`);
@@ -87,7 +87,7 @@ function calculateTaskDepths(tasks) {
87
87
  const taskMap = new Map(tasks.map((t) => [t.id, t]));
88
88
  function getDepth(taskId) {
89
89
  if (depthMap.has(taskId))
90
- return depthMap.get(taskId);
90
+ return depthMap.get(taskId) ?? 0;
91
91
  const task = taskMap.get(taskId);
92
92
  const parentId = task?.parent_task?.id;
93
93
  if (!parentId) {
@@ -167,7 +167,7 @@ export function TaskView({ tasks }) {
167
167
  if (!tasksByParent.has(parentId)) {
168
168
  tasksByParent.set(parentId, []);
169
169
  }
170
- tasksByParent.get(parentId).push(task);
170
+ tasksByParent.get(parentId)?.push(task);
171
171
  });
172
172
  // Process root tasks first, then children
173
173
  const processedIds = new Set();
@@ -1,10 +1,26 @@
1
1
  import { LocalConfig } from './guild-config.js';
2
2
  import { GuildAPIClient } from './api-client.js';
3
+ import type { AgentVersion } from './api-types.js';
4
+ /** Thrown when the ephemeral version build times out or polling fails. */
5
+ export declare class BuildTimeoutError extends Error {
6
+ constructor(message?: string);
7
+ }
8
+ /** Thrown when the ephemeral version build fails validation. */
9
+ export declare class BuildFailedError extends Error {
10
+ readonly failedSteps: {
11
+ name: string;
12
+ content?: string;
13
+ }[];
14
+ constructor(failedSteps: {
15
+ name: string;
16
+ content?: string;
17
+ }[]);
18
+ }
3
19
  /**
4
20
  * Resolve an agent identifier to a fully-qualified form.
5
21
  *
6
- * If the identifier already contains `/` or is a UUID, return it verbatim.
7
- * Otherwise, resolve the default owner and prepend `owner.name/`.
22
+ * If the identifier already contains `~` or is a UUID, return it verbatim.
23
+ * Otherwise, resolve the default owner and prepend `owner.name~`.
8
24
  */
9
25
  export declare function resolveAgentRef(client: GuildAPIClient, identifier: string): Promise<string>;
10
26
  /**
@@ -27,4 +43,45 @@ export declare function readAgentFiles(cwd: string): Promise<{
27
43
  path: string;
28
44
  content: string;
29
45
  }[]>;
46
+ /**
47
+ * Compute a deterministic hash of agent files.
48
+ * Sorts by path to ensure consistent ordering.
49
+ */
50
+ export declare function hashAgentFiles(files: {
51
+ path: string;
52
+ content: string;
53
+ }[]): string;
54
+ /**
55
+ * Get or create an ephemeral version, skipping the build if files haven't
56
+ * changed since the last successful ephemeral build.
57
+ *
58
+ * Returns the version (cached or newly created), a cache hit flag, and the
59
+ * computed hash (used by buildEphemeralVersion to write the cache on success).
60
+ */
61
+ export declare function getOrCreateEphemeral(client: GuildAPIClient, agentId: string, files: {
62
+ path: string;
63
+ content: string;
64
+ }[], cwd: string, summary: string, options?: {
65
+ noCache?: boolean;
66
+ }): Promise<{
67
+ version: AgentVersion;
68
+ cached: boolean;
69
+ hash: string;
70
+ }>;
71
+ /**
72
+ * Build an ephemeral version end-to-end: get-or-create, poll for validation,
73
+ * cache on success, and report build failures.
74
+ *
75
+ * Consolidates the build/poll/cache pattern used by both `guild agent test`
76
+ * and `guild agent chat`.
77
+ */
78
+ export declare function buildEphemeralVersion(client: GuildAPIClient, agentId: string, files: {
79
+ path: string;
80
+ content: string;
81
+ }[], cwd: string, summary: string, options?: {
82
+ noCache?: boolean;
83
+ }): Promise<{
84
+ version: AgentVersion;
85
+ cached: boolean;
86
+ }>;
30
87
  //# sourceMappingURL=agent-helpers.d.ts.map
@@ -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,123 @@ 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
+ }
92
237
  //# sourceMappingURL=agent-helpers.js.map