@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
@@ -5,7 +5,7 @@ import { render } from 'ink';
5
5
  import React from 'react';
6
6
  import open from 'open';
7
7
  import { hyperlink } from '../../lib/colors.js';
8
- import { getAgentId, readAgentFiles } from '../../lib/agent-helpers.js';
8
+ import { getAgentId, readAgentFiles, buildEphemeralVersion, BuildTimeoutError, BuildFailedError, } from '../../lib/agent-helpers.js';
9
9
  import { ChatApp, createSession, ensureAuthenticated } from '../chat.js';
10
10
  import { readFileSync } from 'fs';
11
11
  import path from 'path';
@@ -15,7 +15,6 @@ import { isQuietMode, getOutputMode } from '../../lib/output-mode.js';
15
15
  import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
16
16
  import { format } from '../../lib/progress.js';
17
17
  import { showBetaGuidance } from '../../lib/auth.js';
18
- import { pollUntilComplete } from '../../lib/polling.js';
19
18
  import { suppressScrollbackClear } from '../../lib/alternate-screen.js';
20
19
  import * as readline from 'readline';
21
20
  import { pollForResponse } from '../../lib/session-polling.js';
@@ -31,13 +30,14 @@ export function createAgentChatCommand() {
31
30
  .description('Chat with the agent in current directory')
32
31
  .argument('[prompt...]', 'Optional initial prompt for the agent')
33
32
  .option('--path <dir>', 'Path to agent directory (defaults to current directory)')
34
- .option('--workspace <identifier>', 'Workspace ID or full name (e.g., owner/workspace-name)')
33
+ .option('--workspace <identifier>', 'Workspace ID or full name (e.g., owner~workspace-name)')
35
34
  .option('--mode <format>', 'Input mode: json (one-shot) or jsonl (line-by-line)')
36
- .option('--version <id>', 'Chat with a specific version (UUID or version number)')
35
+ .option('--agent-version <id>', 'Chat with a specific version (UUID or version number)')
37
36
  .option('--no-splash', 'Skip the splash screen animation')
38
37
  .option('--resume <session-id>', 'Resume an existing session')
39
38
  .option('--open', 'Open session in web dashboard')
40
- .addHelpText('after', '\nTo chat with a published agent by name: guild chat --agent owner/agent-name')
39
+ .option('--no-cache', 'Skip ephemeral build cache (force a fresh build)')
40
+ .addHelpText('after', '\nTo chat with a published agent by name: guild chat --agent owner~agent-name')
41
41
  .action(async (promptArgs, options) => {
42
42
  try {
43
43
  // Get agent ID from guild.json in the specified path
@@ -69,12 +69,14 @@ export function createAgentChatCommand() {
69
69
  // Default: create ephemeral version from working directory.
70
70
  // --version: use a specific existing version.
71
71
  let version;
72
- if (options.version) {
72
+ let ephemeralCached = false;
73
+ if (options.agentVersion) {
73
74
  // Explicit version: look it up by ID or version number
74
75
  const existingVersions = (await client.get(`/agents/${agentId}/versions`));
75
- const match = existingVersions.items.find((v) => v.id === options.version || v.version_number === options.version);
76
+ const match = existingVersions.items.find((v) => v.id === options.agentVersion ||
77
+ v.version_number === options.agentVersion);
76
78
  if (!match) {
77
- console.error(`Error: Version not found: ${options.version}`);
79
+ console.error(`Error: Version not found: ${options.agentVersion}`);
78
80
  console.error('');
79
81
  console.error('List available versions:');
80
82
  console.error(` guild agent versions ${agentId}`);
@@ -87,61 +89,13 @@ export function createAgentChatCommand() {
87
89
  console.error('Building agent...');
88
90
  }
89
91
  const agentFiles = await readAgentFiles(resolvedPath);
90
- version = (await client.post(`/agents/${agentId}/versions`, {
91
- files: agentFiles,
92
- summary: '[Chat] Ephemeral development version',
93
- version_type: 'EPHEMERAL',
94
- }));
95
- }
96
- // Wait for build to complete (skip if version already validated)
97
- if (version.validation_status === 'PENDING' ||
98
- version.validation_status === 'RUNNING') {
99
- const pollResult = await pollUntilComplete({
100
- resourceId: version.id,
101
- endpoint: `/versions/${version.id}`,
102
- isComplete: (response) => response.validation_status !== 'PENDING' &&
103
- response.validation_status !== 'RUNNING',
104
- message: 'Building...',
105
- successMessage: 'Build finished',
106
- timeoutMessage: 'Build timed out',
107
- maxAttempts: 120,
108
- delayMs: 1000,
109
- });
110
- if (!pollResult.success || !pollResult.response) {
111
- console.error('Error: Agent build did not complete');
112
- process.exit(1);
113
- }
114
- version = pollResult.response;
115
- }
116
- if (version.validation_status === 'FAILED') {
117
- console.error('Error: Agent build failed');
118
- console.error('');
119
- // Fetch validation steps to show the actual error
120
- try {
121
- const stepsResponse = await client.get(`/versions/${version.id}/validation/steps`);
122
- const failedSteps = stepsResponse.steps.filter((step) => step.status === 'FAILED');
123
- if (failedSteps.length > 0) {
124
- for (const step of failedSteps) {
125
- console.error(`Step "${step.name}" failed:`);
126
- if (step.content) {
127
- console.error(step.content);
128
- }
129
- console.error('');
130
- }
131
- }
132
- else {
133
- console.error('No failed steps found. Check validation logs for details.');
134
- console.error('');
135
- }
136
- }
137
- catch {
138
- // If we can't fetch steps, just show generic message
139
- console.error('Could not fetch validation details.');
140
- console.error('');
92
+ const noCache = options.cache === false;
93
+ const result = await buildEphemeralVersion(client, agentId, agentFiles, resolvedPath, '[Chat] Ephemeral development version', { noCache });
94
+ version = result.version;
95
+ ephemeralCached = result.cached;
96
+ if (ephemeralCached && !quiet) {
97
+ console.error('No changes detected, reusing cached build.');
141
98
  }
142
- console.error('Fix the issues and retry:');
143
- console.error(` guild agent chat ${agentId}`);
144
- process.exit(1);
145
99
  }
146
100
  // Branch: JSON/JSONL modes vs interactive
147
101
  if (options.mode === 'json' || options.mode === 'jsonl') {
@@ -274,6 +228,19 @@ export function createAgentChatCommand() {
274
228
  }
275
229
  }
276
230
  catch (error) {
231
+ if (error instanceof BuildTimeoutError) {
232
+ console.error('Error: Build did not complete');
233
+ console.error('');
234
+ console.error(error.message);
235
+ console.error('');
236
+ console.error('Check build status:');
237
+ console.error(' guild agent versions');
238
+ process.exit(1);
239
+ }
240
+ if (error instanceof BuildFailedError) {
241
+ console.error(`Error: ${error.message}`);
242
+ process.exit(1);
243
+ }
277
244
  const formattedError = handleAxiosError(error);
278
245
  if (formattedError.code === ErrorCodes.AUTH_REQUIRED ||
279
246
  formattedError.code === ErrorCodes.AUTH_TOKEN_INVALID) {
@@ -23,7 +23,7 @@ export function createAgentCloneCommand() {
23
23
  const cmd = new Command('clone');
24
24
  cmd
25
25
  .description('Clone an existing agent repository')
26
- .argument('<identifier>', 'Agent ID or full name to clone (e.g., owner/agent-name)')
26
+ .argument('<identifier>', 'Agent ID or full name to clone (e.g., owner~agent-name)')
27
27
  .option('--directory <path>', 'Target directory for clone')
28
28
  .option('--force', 'Clone even if directory is not empty', false)
29
29
  .action(async (agentId, options) => {
@@ -11,7 +11,7 @@ export function createAgentCodeCommand() {
11
11
  const cmd = new Command('code');
12
12
  cmd
13
13
  .description('Fetch the latest code for an agent')
14
- .argument('[identifier]', 'Agent ID or full name (e.g., owner/agent-name)')
14
+ .argument('[identifier]', 'Agent ID or full name (e.g., owner~agent-name)')
15
15
  .option('--draft', 'Include draft versions (default: only published)', false)
16
16
  .option('--output <directory>', 'Write files to directory instead of printing JSON')
17
17
  .action(async (agentIdArg, options) => {
@@ -48,7 +48,7 @@ export function createAgentForkCommand() {
48
48
  const cmd = new Command('fork');
49
49
  cmd
50
50
  .description('Fork an existing agent version to create a new agent')
51
- .argument('[identifier]', 'Agent ID, full name, or agent:version (e.g., owner/agent-name:version_xyz)')
51
+ .argument('[identifier]', 'Agent ID, full name, or agent:version (e.g., owner~agent-name:version_xyz)')
52
52
  .option('--name <name>', 'Name for the forked agent')
53
53
  .option('--description <desc>', 'Description for the forked agent')
54
54
  .option('--directory <path>', 'Target directory for clone')
@@ -65,7 +65,7 @@ export function createAgentForkCommand() {
65
65
  const agentPart = identifierArg.substring(0, colonIndex);
66
66
  sourceVersionId = identifierArg.substring(colonIndex + 1);
67
67
  if (!agentPart || !sourceVersionId) {
68
- output.error('Error: Invalid argument format', 'Expected: [identifier] or [identifier]:[version-id]\nExample: guild agent fork owner/agent-name:version_xyz\n\nTo find versions:\n guild agent versions <agent-id>');
68
+ output.error('Error: Invalid argument format', 'Expected: [identifier] or [identifier]:[version-id]\nExample: guild agent fork owner~agent-name:version_xyz\n\nTo find versions:\n guild agent versions <agent-id>');
69
69
  process.exit(1);
70
70
  }
71
71
  const resolved = await getAgentId(agentPart);
@@ -10,7 +10,7 @@ export function createAgentGetCommand() {
10
10
  const cmd = new Command('get');
11
11
  cmd
12
12
  .description('Get agent details')
13
- .argument('[identifier]', 'Agent ID or full name (e.g., owner/agent-name)')
13
+ .argument('[identifier]', 'Agent ID or full name (e.g., owner~agent-name)')
14
14
  .action(async (idArg) => {
15
15
  const output = createOutputWriter();
16
16
  try {
@@ -94,10 +94,10 @@ async function grepAllAgents(client, patternRE, publishedOnly, output) {
94
94
  }
95
95
  catch (ex) {
96
96
  const formattedError = handleAxiosError(ex);
97
- output.error(`${agent.owner?.name}/${agent.name}: ${formattedError.details}`);
97
+ output.error(`${agent.owner?.name}~${agent.name}: ${formattedError.details}`);
98
98
  return;
99
99
  }
100
- searchFiles(files, patternRE, `${agent.owner?.name}/${agent.name}/`, output);
100
+ searchFiles(files, patternRE, `${agent.owner?.name}~${agent.name}/`, output);
101
101
  }));
102
102
  offset += response.pagination.limit;
103
103
  if (!response.pagination.has_more)
@@ -210,7 +210,7 @@ export function createAgentInitCommand() {
210
210
  .then(() => true)
211
211
  .catch(() => false);
212
212
  if (!gitExists) {
213
- await runGit(['init'], { cwd: targetDir });
213
+ await runGit(['init', '-b', 'main'], { cwd: targetDir });
214
214
  steps.succeed('Initialize git repository');
215
215
  }
216
216
  else {
@@ -319,17 +319,24 @@ export function createAgentInitCommand() {
319
319
  };
320
320
  await fs.writeFile(guildJsonPath, JSON.stringify(guildConfig, null, 2) + '\n', 'utf-8');
321
321
  steps.succeed('Create guild.json');
322
- // Add guild.json to .gitignore if not already present
322
+ // Add guild.json and .guild/cache/ to .gitignore if not already present
323
323
  const gitignorePath = path.join(targetDir, '.gitignore');
324
324
  try {
325
325
  const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
326
+ let additions = '';
326
327
  if (!gitignoreContent.includes('guild.json')) {
327
- await fs.appendFile(gitignorePath, '\nguild.json\n');
328
+ additions += '\nguild.json\n';
329
+ }
330
+ if (!gitignoreContent.includes('.guild/cache/')) {
331
+ additions += '.guild/cache/\n';
332
+ }
333
+ if (additions) {
334
+ await fs.appendFile(gitignorePath, additions);
328
335
  }
329
336
  }
330
337
  catch {
331
338
  // .gitignore doesn't exist (backend should have created it), create it
332
- await fs.writeFile(gitignorePath, 'guild.json\n');
339
+ await fs.writeFile(gitignorePath, 'guild.json\n.guild/cache/\n');
333
340
  }
334
341
  // Install pre-push hook to block direct git push
335
342
  await installPrePushHook(targetDir);
@@ -3,7 +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 { handleAxiosError } from '../../lib/errors.js';
6
+ import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
7
7
  import { getOutputMode } from '../../lib/output-mode.js';
8
8
  import { createOutputWriter, formatAgentTable } from '../../lib/output.js';
9
9
  const SORT_MAP = {
@@ -19,6 +19,7 @@ export function createAgentListCommand() {
19
19
  .option('--search <query>', 'Search agents by name or description')
20
20
  .option('--sort <field>', 'Sort by: updated, newest, name, popular (default: updated)', 'updated')
21
21
  .option('--published', 'Only show published agents')
22
+ .option('--workspace <id>', 'Filter agents by workspace ID or name')
22
23
  .option('--limit <number>', 'Number of results to return', '20')
23
24
  .option('--offset <number>', 'Offset for pagination', '0')
24
25
  .action(async (options) => {
@@ -39,6 +40,9 @@ export function createAgentListCommand() {
39
40
  if (options.published) {
40
41
  params.append('published_only', 'true');
41
42
  }
43
+ if (options.workspace) {
44
+ params.append('for_workspace', options.workspace);
45
+ }
42
46
  const sortField = SORT_MAP[options.sort];
43
47
  if (sortField) {
44
48
  params.append('sort_by', sortField);
@@ -53,6 +57,10 @@ export function createAgentListCommand() {
53
57
  }
54
58
  catch (error) {
55
59
  const formattedError = handleAxiosError(error);
60
+ if (formattedError.code === ErrorCodes.NOT_FOUND) {
61
+ output.error('Workspace not found');
62
+ process.exit(1);
63
+ }
56
64
  output.error(`Failed to list agents: ${formattedError.details}`);
57
65
  process.exit(1);
58
66
  }
@@ -10,7 +10,7 @@ export function createAgentPublishCommand() {
10
10
  const cmd = new Command('publish');
11
11
  cmd
12
12
  .description('Publish the latest draft version of an agent')
13
- .argument('[identifier]', 'Agent ID or full name (e.g., owner/agent-name)')
13
+ .argument('[identifier]', 'Agent ID or full name (e.g., owner~agent-name)')
14
14
  .option('--wait', 'Wait for validation to complete before publishing')
15
15
  .action(async (agentIdArg, options) => {
16
16
  const output = createOutputWriter();
@@ -10,7 +10,7 @@ export function createAgentRevalidateCommand() {
10
10
  const cmd = new Command('revalidate');
11
11
  cmd
12
12
  .description('Revalidate an agent version')
13
- .argument('[identifier]', 'Agent ID or full name (e.g., owner/agent-name)')
13
+ .argument('[identifier]', 'Agent ID or full name (e.g., owner~agent-name)')
14
14
  .argument('[version-id]', 'ID of the version to revalidate (uses latest if omitted)')
15
15
  .action(async (agentIdArg, versionIdArg) => {
16
16
  const output = createOutputWriter();
@@ -44,6 +44,7 @@ export function createAgentSaveCommand() {
44
44
  output.error('Commit message is required with --all', 'Provide a message describing your changes:\n guild agent save -A --message "Add new feature"');
45
45
  process.exit(1);
46
46
  }
47
+ let versionNumber;
47
48
  try {
48
49
  // Check for guild.json
49
50
  const guildJsonPath = path.join(cwd, 'guild.json');
@@ -88,8 +89,22 @@ export function createAgentSaveCommand() {
88
89
  }
89
90
  packageJson.version = newVersion;
90
91
  await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
92
+ versionNumber = newVersion;
91
93
  output.progress(`✓ Bumped version: ${currentVersion} → ${newVersion}`);
92
94
  }
95
+ else {
96
+ // No bump — read version from package.json if it exists
97
+ const packageJsonPath = path.join(cwd, 'package.json');
98
+ try {
99
+ const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
100
+ if (pkg.version) {
101
+ versionNumber = pkg.version;
102
+ }
103
+ }
104
+ catch {
105
+ // No package.json or unreadable — version stays undefined
106
+ }
107
+ }
93
108
  // Check for uncommitted changes and unpushed commits
94
109
  const { stdout: statusOutput } = await runGit(['status', '--porcelain'], {
95
110
  cwd,
@@ -249,13 +264,19 @@ export function createAgentSaveCommand() {
249
264
  const commitSha = shaOutput.trim();
250
265
  // Create version in guildcore (always as DRAFT, publish happens separately)
251
266
  const client = new GuildAPIClient();
252
- let version = await client.post(`/agents/${guildConfig.agent_id}/versions`, {
267
+ const agentId = guildConfig?.agent_id;
268
+ if (!agentId) {
269
+ output.error('Not in an agent directory');
270
+ process.exit(1);
271
+ }
272
+ let version = await client.post(`/agents/${agentId}/versions`, {
253
273
  commit_sha: commitSha,
254
274
  summary: versionMessage,
255
275
  version_type: 'COMMITTED',
276
+ ...(versionNumber ? { version_number: versionNumber } : {}),
256
277
  });
257
278
  output.progress(`✓ Created version (${version.id})`);
258
- if (options.wait) {
279
+ if (options.wait || options.publish) {
259
280
  version = await waitForValidation(version.id, output);
260
281
  }
261
282
  if (options.publish) {
@@ -4,6 +4,7 @@ import { Command } from 'commander';
4
4
  import { render } from 'ink';
5
5
  import React from 'react';
6
6
  import { readFileSync } from 'fs';
7
+ import { access } from 'fs/promises';
7
8
  import path from 'path';
8
9
  import { fileURLToPath } from 'url';
9
10
  import open from 'open';
@@ -13,13 +14,13 @@ import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
13
14
  import { format } from '../../lib/progress.js';
14
15
  import { showBetaGuidance } from '../../lib/auth.js';
15
16
  import * as readline from 'readline';
17
+ import { parseEventFilter } from '../../lib/event-filter.js';
16
18
  import { isQuietMode, getOutputMode } from '../../lib/output-mode.js';
17
- import { pollForResponse } from '../../lib/session-polling.js';
19
+ import { pollForResponse, pollForResponseWithEvents, } from '../../lib/session-polling.js';
18
20
  import { readStdinAsJSON, ensureInteractiveStdin } from '../../lib/stdin.js';
19
21
  import { loadLocalConfig, getWorkspaceId } from '../../lib/guild-config.js';
20
22
  import { GitError, formatGitError } from '../../lib/git.js';
21
- import { pollUntilComplete } from '../../lib/polling.js';
22
- import { readAgentFiles } from '../../lib/agent-helpers.js';
23
+ import { readAgentFiles, buildEphemeralVersion, buildBundledVersion, BundleNotFoundError, BuildTimeoutError, BuildFailedError, } from '../../lib/agent-helpers.js';
23
24
  import { fetchSession, fetchSessionEvents } from '../../lib/session-resume.js';
24
25
  import { ChatApp, ensureAuthenticated } from '../chat.js';
25
26
  import { suppressScrollbackClear } from '../../lib/alternate-screen.js';
@@ -33,11 +34,18 @@ export function createAgentTestCommand() {
33
34
  .description('Test agent in interactive REPL session')
34
35
  .option('--workspace <identifier>', 'Workspace ID or full name (e.g., owner/workspace-name)')
35
36
  .option('--mode <format>', 'Input mode: json (one-shot) or jsonl (line-by-line)')
36
- .option('--version <id>', 'Test a specific version (UUID or version number)')
37
+ .option('--agent-version <id>', 'Test a specific version (UUID or version number)')
37
38
  .option('--resume <session-id>', 'Resume an existing test session')
38
39
  .option('--open', 'Open session in web dashboard')
40
+ .option('--events <types>', 'Event types to stream (default: user). Shorthands: none, user, system, all, or comma-separated type names')
41
+ .option('--bundle <file>', 'Path to a pre-built gzip+base64 bundle file')
42
+ .option('--no-cache', 'Skip ephemeral build cache (force a fresh build)')
39
43
  .action(async (options) => {
40
44
  const cwd = process.cwd();
45
+ // Parse --events filter once, before any branching
46
+ const eventFilter = options.events
47
+ ? parseEventFilter(options.events)
48
+ : undefined;
41
49
  try {
42
50
  // Handle --resume: skip build, fetch existing session, hand off to ChatApp
43
51
  if (options.resume) {
@@ -57,6 +65,7 @@ export function createAgentTestCommand() {
57
65
  resumeSession,
58
66
  resumeEvents,
59
67
  resumeCommand,
68
+ eventFilter,
60
69
  }), { exitOnCtrlC: false });
61
70
  await waitUntilExit();
62
71
  return;
@@ -75,25 +84,6 @@ export function createAgentTestCommand() {
75
84
  console.error(' guild agent clone <agent-id>');
76
85
  process.exit(1);
77
86
  }
78
- // Read agent files from disk for ephemeral version creation.
79
- // Skipped when --version is provided (testing a specific existing version).
80
- let agentFiles = null;
81
- if (!options.version) {
82
- try {
83
- agentFiles = await readAgentFiles(cwd);
84
- }
85
- catch (error) {
86
- const err = error;
87
- console.error('Error: Could not read agent files');
88
- console.error('');
89
- console.error(err.message);
90
- console.error('');
91
- console.error('Ensure you are in an agent directory with:');
92
- console.error(' - agent.ts (required)');
93
- console.error(' - package.json (required)');
94
- process.exit(1);
95
- }
96
- }
97
87
  // If using JSON input, read and validate it early (before auth/session creation)
98
88
  // This provides better UX - fail fast on bad JSON without needing auth
99
89
  let inputData;
@@ -111,6 +101,21 @@ export function createAgentTestCommand() {
111
101
  process.exit(1);
112
102
  }
113
103
  }
104
+ // If using bundle, verify the file exists early (before auth/session creation)
105
+ // so we fail fast without needing auth if the path is wrong.
106
+ if (options.bundle) {
107
+ try {
108
+ await access(options.bundle);
109
+ }
110
+ catch {
111
+ console.error(`Error: Bundle file not found: ${options.bundle}`);
112
+ console.error('');
113
+ console.error('Ensure the bundle file exists and the path is correct:');
114
+ console.error(' npm run build');
115
+ console.error(' guild agent test --bundle agent.js.gz');
116
+ process.exit(1);
117
+ }
118
+ }
114
119
  // Determine workspace (priority: flag > local config > global config)
115
120
  let workspaceId = options.workspace;
116
121
  if (!workspaceId) {
@@ -135,13 +140,15 @@ export function createAgentTestCommand() {
135
140
  // Resolve version for testing
136
141
  const client = new GuildAPIClient();
137
142
  let version;
143
+ let ephemeralCached = false;
138
144
  try {
139
- if (options.version) {
145
+ if (options.agentVersion) {
140
146
  // Explicit version: look it up by ID or version number
141
147
  const existingVersions = (await client.get(`/agents/${guildConfig.agent_id}/versions`));
142
- const match = existingVersions.items.find((v) => v.id === options.version || v.version_number === options.version);
148
+ const match = existingVersions.items.find((v) => v.id === options.agentVersion ||
149
+ v.version_number === options.agentVersion);
143
150
  if (!match) {
144
- console.error(`Error: Version not found: ${options.version}`);
151
+ console.error(`Error: Version not found: ${options.agentVersion}`);
145
152
  console.error('');
146
153
  console.error('List available versions:');
147
154
  console.error(` guild agent versions ${guildConfig.agent_id}`);
@@ -149,16 +156,54 @@ export function createAgentTestCommand() {
149
156
  }
150
157
  version = match;
151
158
  }
159
+ else if (options.bundle) {
160
+ // Pre-built bundle: skip server-side compilation entirely.
161
+ const result = await buildBundledVersion(client, guildConfig.agent_id, options.bundle, cwd, '[Test] Pre-built bundle');
162
+ version = result.version;
163
+ }
152
164
  else {
153
- // Default: create ephemeral version from working directory
154
- version = (await client.post(`/agents/${guildConfig.agent_id}/versions`, {
155
- files: agentFiles,
156
- summary: '[Test] Ephemeral development version',
157
- version_type: 'EPHEMERAL',
158
- }));
165
+ // Default: build ephemeral version from working directory,
166
+ // reusing the last build if files haven't changed.
167
+ const agentFiles = await readAgentFiles(cwd);
168
+ const noCache = options.cache === false;
169
+ const result = await buildEphemeralVersion(client, guildConfig.agent_id, agentFiles, cwd, '[Test] Ephemeral development version', { noCache });
170
+ version = result.version;
171
+ ephemeralCached = result.cached;
159
172
  }
160
173
  }
161
174
  catch (error) {
175
+ if (error instanceof BundleNotFoundError) {
176
+ console.error(`Error: ${error.message}`);
177
+ console.error('');
178
+ console.error('Ensure the bundle file exists and the path is correct:');
179
+ console.error(' npm run build');
180
+ console.error(' guild agent test --bundle agent.js.gz');
181
+ process.exit(1);
182
+ }
183
+ if (error instanceof BuildTimeoutError) {
184
+ console.error('Error: Build did not complete');
185
+ console.error('');
186
+ console.error(error.message);
187
+ console.error('');
188
+ console.error('Check build status:');
189
+ console.error(' guild agent versions');
190
+ process.exit(1);
191
+ }
192
+ if (error instanceof BuildFailedError) {
193
+ console.error(`Error: ${error.message}`);
194
+ process.exit(1);
195
+ }
196
+ if (error instanceof Error &&
197
+ error.message.startsWith('Missing required')) {
198
+ console.error('Error: Could not read agent files');
199
+ console.error('');
200
+ console.error(error.message);
201
+ console.error('');
202
+ console.error('Ensure you are in an agent directory with:');
203
+ console.error(' - agent.ts (required)');
204
+ console.error(' - package.json (required)');
205
+ process.exit(1);
206
+ }
162
207
  const formattedError = handleAxiosError(error);
163
208
  if (formattedError.code === ErrorCodes.NOT_FOUND) {
164
209
  console.error(`Error: Agent not found: ${guildConfig.agent_id}`);
@@ -174,61 +219,6 @@ export function createAgentTestCommand() {
174
219
  }
175
220
  throw error;
176
221
  }
177
- // Wait for validation to complete
178
- const pollResult = await pollUntilComplete({
179
- resourceId: version.id,
180
- endpoint: `/versions/${version.id}`,
181
- isComplete: (response) => response.validation_status !== 'PENDING' &&
182
- response.validation_status !== 'RUNNING',
183
- message: 'Building...',
184
- successMessage: 'Build finished',
185
- timeoutMessage: 'Build timed out',
186
- maxAttempts: 120,
187
- delayMs: 1000,
188
- });
189
- if (!pollResult.success || !pollResult.response) {
190
- console.error('Error: Build did not complete');
191
- console.error('');
192
- console.error('The agent version build timed out or failed to report status.');
193
- console.error('');
194
- console.error('Check build status:');
195
- console.error(` guild agent versions ${guildConfig.agent_id}`);
196
- console.error('');
197
- console.error('Retry the test:');
198
- console.error(' guild agent test');
199
- process.exit(1);
200
- }
201
- version = pollResult.response;
202
- if (version.validation_status === 'FAILED') {
203
- console.error('Error: Build failed');
204
- console.error('');
205
- // Fetch validation steps to show the actual error
206
- try {
207
- const stepsResponse = await client.get(`/versions/${version.id}/validation/steps`);
208
- const failedSteps = stepsResponse.steps.filter((step) => step.status === 'FAILED');
209
- if (failedSteps.length > 0) {
210
- for (const step of failedSteps) {
211
- console.error(`Step "${step.name}" failed:`);
212
- if (step.content) {
213
- console.error(step.content);
214
- }
215
- console.error('');
216
- }
217
- }
218
- else {
219
- console.error('No failed steps found. Check validation logs for details.');
220
- console.error('');
221
- }
222
- }
223
- catch {
224
- // If we can't fetch steps, just show generic message
225
- console.error('Could not fetch validation details.');
226
- console.error('');
227
- }
228
- console.error('Fix the issues and retry:');
229
- console.error(' guild agent test');
230
- process.exit(1);
231
- }
232
222
  // Create test session
233
223
  let session;
234
224
  try {
@@ -254,9 +244,13 @@ export function createAgentTestCommand() {
254
244
  const quiet = isQuietMode();
255
245
  if (!quiet) {
256
246
  console.log(`✓ Agent: ${guildConfig.name} (${guildConfig.agent_id})`);
257
- const versionDisplay = options.version
258
- ? options.version
259
- : 'ephemeral (working directory)';
247
+ const versionDisplay = options.agentVersion
248
+ ? options.agentVersion
249
+ : options.bundle
250
+ ? `bundle (${path.basename(options.bundle)})`
251
+ : ephemeralCached
252
+ ? 'ephemeral (cached, no changes)'
253
+ : 'ephemeral (working directory)';
260
254
  console.log(`✓ Version: ${versionDisplay}`);
261
255
  console.log(`✓ Workspace: ${workspaceId}`);
262
256
  const sessionLink = session.session_url
@@ -279,7 +273,9 @@ export function createAgentTestCommand() {
279
273
  });
280
274
  // Poll for response (starting from beginning)
281
275
  // 3 minutes - allow time for agents that use LLM calls for input parsing
282
- const { response } = await pollForResponse(client, session.id, undefined, 180000);
276
+ const { response } = eventFilter
277
+ ? await pollForResponseWithEvents(client, session.id, eventFilter, undefined, 180000)
278
+ : await pollForResponse(client, session.id, undefined, 180000);
283
279
  if (!response) {
284
280
  console.error('Error: No response received from agent within timeout');
285
281
  console.error('');
@@ -355,7 +351,9 @@ export function createAgentTestCommand() {
355
351
  content: jsonInput,
356
352
  });
357
353
  // Wait for response (looking for events after last seen)
358
- const result = await pollForResponse(client, session.id, lastEventId, 180000);
354
+ const result = eventFilter
355
+ ? await pollForResponseWithEvents(client, session.id, eventFilter, lastEventId, 180000)
356
+ : await pollForResponse(client, session.id, lastEventId, 180000);
359
357
  lastEventId = result.lastEventId;
360
358
  if (!result.response) {
361
359
  console.error(`Timeout: No response for line ${lineNumber}`);
@@ -404,6 +402,7 @@ export function createAgentTestCommand() {
404
402
  resumeSession: session,
405
403
  resumeEvents: [],
406
404
  resumeCommand,
405
+ eventFilter,
407
406
  }), { exitOnCtrlC: false });
408
407
  await waitUntilExit();
409
408
  }