@guildai/cli 0.3.17 → 0.4.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.
@@ -19,7 +19,7 @@ import { runGit } from '../../lib/git.js';
19
19
  import { suppressScrollbackClear } from '../../lib/alternate-screen.js';
20
20
  import * as readline from 'readline';
21
21
  import { pollForResponse } from '../../lib/session-polling.js';
22
- import { readStdinAsJSON } from '../../lib/stdin.js';
22
+ import { readStdinAsJSON, ensureInteractiveStdin } from '../../lib/stdin.js';
23
23
  import { fetchSession, fetchSessionEvents } from '../../lib/session-resume.js';
24
24
  // ESM equivalent of __dirname
25
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -266,6 +266,7 @@ export function createAgentChatCommand() {
266
266
  else {
267
267
  // Interactive mode: use shared ChatApp with full features
268
268
  // (splash animation, task panel, status line, etc.)
269
+ ensureInteractiveStdin('guild agent chat');
269
270
  const resumeCommand = 'guild agent chat';
270
271
  // Handle --resume: fetch existing session + events
271
272
  let resumeSession;
@@ -4,25 +4,9 @@ import { GuildAPIClient } from '../../lib/api-client.js';
4
4
  import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
5
5
  import * as fs from 'fs/promises';
6
6
  import * as path from 'path';
7
- import * as readline from 'readline';
8
7
  import { getAuthenticatedUrl } from '../../lib/auth.js';
9
8
  import { runGit, GitError, formatGitError } from '../../lib/git.js';
10
9
  import { createOutputWriter } from '../../lib/output.js';
11
- async function promptForDirectory(suggestedDir) {
12
- const rl = readline.createInterface({
13
- input: process.stdin,
14
- output: process.stdout,
15
- });
16
- return new Promise((resolve) => {
17
- rl.question(`Target directory (default: ${suggestedDir}): `, (answer) => {
18
- rl.close();
19
- resolve(answer.trim() || suggestedDir);
20
- });
21
- });
22
- }
23
- function isInteractive() {
24
- return process.stdin.isTTY === true;
25
- }
26
10
  async function isDirectoryEmpty(dirPath) {
27
11
  try {
28
12
  const files = await fs.readdir(dirPath);
@@ -51,18 +35,8 @@ export function createAgentCloneCommand() {
51
35
  process.exit(1);
52
36
  }
53
37
  output.progress(`✓ Fetched agent '${agent.name}' (${agent.id})`);
54
- // Determine target directory
55
- let targetDir = options.directory;
56
- if (!targetDir) {
57
- if (isInteractive()) {
58
- const suggestedDir = `./${agent.name}`;
59
- targetDir = await promptForDirectory(suggestedDir);
60
- }
61
- else {
62
- output.error('Error: Target directory required in non-interactive mode', `Provide a directory:\n guild agent clone ${agentId} --directory ./my-agent\n\nOr run interactively to be prompted.`);
63
- process.exit(1);
64
- }
65
- }
38
+ // Determine target directory (default to agent name, like git clone)
39
+ const targetDir = options.directory || agent.name;
66
40
  // Check if directory exists and is not empty
67
41
  const dirExists = await fs
68
42
  .access(targetDir)
@@ -64,15 +64,23 @@ function isInteractive() {
64
64
  export function createAgentInitCommand() {
65
65
  const cmd = new Command('init');
66
66
  cmd
67
- .description('Initialize current directory as a Guild agent')
67
+ .description('Initialize a directory as a Guild agent')
68
68
  .option('--name <name>', 'Agent name')
69
69
  .option('--template <template>', 'Agent template (LLM, AUTO_MANAGED_STATE, BLANK)')
70
70
  .option('--fork <agent-id>', 'Fork from existing agent')
71
71
  .option('--owner <owner-id>', 'Owner account (user or organization ID)')
72
+ .option('--directory <path>', 'Directory to initialize (created if needed)')
72
73
  .option('--force', 'Overwrite existing guild.json', false)
73
74
  .action(async (options) => {
74
- const cwd = process.cwd();
75
- const guildJsonPath = path.join(cwd, 'guild.json');
75
+ // Resolve target directory
76
+ const targetDir = options.directory
77
+ ? path.resolve(process.cwd(), options.directory)
78
+ : process.cwd();
79
+ // Create directory if it doesn't exist
80
+ if (options.directory) {
81
+ await fs.mkdir(targetDir, { recursive: true });
82
+ }
83
+ const guildJsonPath = path.join(targetDir, 'guild.json');
76
84
  try {
77
85
  // Check if already initialized
78
86
  const guildJsonExists = await fs
@@ -93,7 +101,7 @@ export function createAgentInitCommand() {
93
101
  if (!agentName) {
94
102
  if (isInteractive()) {
95
103
  // Use slugified directory name as default
96
- const dirName = path.basename(cwd);
104
+ const dirName = path.basename(targetDir);
97
105
  const defaultName = slugify(dirName);
98
106
  agentName = await promptForName(defaultName);
99
107
  if (!agentName) {
@@ -176,13 +184,13 @@ export function createAgentInitCommand() {
176
184
  format.detail(`Agent: ${agent.name} (${agent.id})`);
177
185
  format.detail(`Owner: ${owner.name}`);
178
186
  // Step 2: Initialize git repository if needed
179
- const gitDir = path.join(cwd, '.git');
187
+ const gitDir = path.join(targetDir, '.git');
180
188
  const gitExists = await fs
181
189
  .access(gitDir)
182
190
  .then(() => true)
183
191
  .catch(() => false);
184
192
  if (!gitExists) {
185
- await runGit(['init'], { cwd });
193
+ await runGit(['init'], { cwd: targetDir });
186
194
  steps.succeed('Initialize git repository');
187
195
  }
188
196
  else {
@@ -192,13 +200,17 @@ export function createAgentInitCommand() {
192
200
  // Step 3: Configure git remote
193
201
  try {
194
202
  // Check if remote 'origin' exists
195
- await runGit(['remote', 'get-url', 'origin'], { cwd });
203
+ await runGit(['remote', 'get-url', 'origin'], { cwd: targetDir });
196
204
  // Remote exists, update it
197
- await runGit(['remote', 'set-url', 'origin', agent.git_url], { cwd });
205
+ await runGit(['remote', 'set-url', 'origin', agent.git_url], {
206
+ cwd: targetDir,
207
+ });
198
208
  }
199
209
  catch {
200
210
  // Remote doesn't exist, add it
201
- await runGit(['remote', 'add', 'origin', agent.git_url], { cwd });
211
+ await runGit(['remote', 'add', 'origin', agent.git_url], {
212
+ cwd: targetDir,
213
+ });
202
214
  }
203
215
  steps.succeed('Configure git remote');
204
216
  format.detail(`Remote: ${agent.git_url}`);
@@ -242,7 +254,7 @@ export function createAgentInitCommand() {
242
254
  console.error(` guild agent get ${agent.id}`);
243
255
  console.error('');
244
256
  console.error('Or wait and try pulling the scaffolding manually:');
245
- console.error(` cd ${cwd}`);
257
+ console.error(` cd ${targetDir}`);
246
258
  console.error(' git pull origin main');
247
259
  process.exit(1);
248
260
  }
@@ -258,7 +270,7 @@ export function createAgentInitCommand() {
258
270
  }
259
271
  for (let attempt = 1; attempt <= pullMaxAttempts; attempt++) {
260
272
  try {
261
- await runGit(['pull', authenticatedGitUrl, 'main'], { cwd });
273
+ await runGit(['pull', authenticatedGitUrl, 'main'], { cwd: targetDir });
262
274
  steps.succeed('Pull scaffolding from remote');
263
275
  break;
264
276
  }
@@ -288,7 +300,7 @@ export function createAgentInitCommand() {
288
300
  await fs.writeFile(guildJsonPath, JSON.stringify(guildConfig, null, 2) + '\n', 'utf-8');
289
301
  steps.succeed('Create guild.json');
290
302
  // Add guild.json to .gitignore if not already present
291
- const gitignorePath = path.join(cwd, '.gitignore');
303
+ const gitignorePath = path.join(targetDir, '.gitignore');
292
304
  try {
293
305
  const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
294
306
  if (!gitignoreContent.includes('guild.json')) {
@@ -303,9 +315,17 @@ export function createAgentInitCommand() {
303
315
  steps.complete('Agent initialized successfully');
304
316
  // Display next steps
305
317
  format.section('Next steps:');
306
- format.detail('1. Edit agent.ts to implement your agent logic');
307
- format.detail(`2. Run 'guild agent save --message "Initial implementation"'`);
308
- format.detail(`3. Run 'guild agent test' to test your agent`);
318
+ if (options.directory) {
319
+ format.detail(`1. cd ${targetDir}`);
320
+ format.detail('2. Edit agent.ts to implement your agent logic');
321
+ format.detail(`3. Run 'guild agent save --message "Initial implementation"'`);
322
+ format.detail(`4. Run 'guild agent test' to test your agent`);
323
+ }
324
+ else {
325
+ format.detail('1. Edit agent.ts to implement your agent logic');
326
+ format.detail(`2. Run 'guild agent save --message "Initial implementation"'`);
327
+ format.detail(`3. Run 'guild agent test' to test your agent`);
328
+ }
309
329
  }
310
330
  catch (error) {
311
331
  if (error instanceof GitError) {
@@ -3,7 +3,8 @@ import { Command } from 'commander';
3
3
  import { GuildAPIClient } from '../../lib/api-client.js';
4
4
  import { getAuthToken } from '../../lib/auth.js';
5
5
  import { handleAxiosError } from '../../lib/errors.js';
6
- import { createOutputWriter } from '../../lib/output.js';
6
+ import { getOutputMode } from '../../lib/output-mode.js';
7
+ import { createOutputWriter, formatAgentTable } from '../../lib/output.js';
7
8
  const SORT_MAP = {
8
9
  updated: 'updated_at',
9
10
  newest: 'created_at',
@@ -42,7 +43,12 @@ export function createAgentListCommand() {
42
43
  params.append('sort_by', sortField);
43
44
  }
44
45
  const response = await client.get(`/agents?${params.toString()}`);
45
- output.data(response);
46
+ if (getOutputMode() === 'json') {
47
+ output.data(response);
48
+ }
49
+ else {
50
+ formatAgentTable(response.items, response.pagination);
51
+ }
46
52
  }
47
53
  catch (error) {
48
54
  const formattedError = handleAxiosError(error);
@@ -2,6 +2,7 @@
2
2
  import { Command } from 'commander';
3
3
  import { GuildAPIClient } from '../../lib/api-client.js';
4
4
  import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
5
+ import { getOutputMode } from '../../lib/output-mode.js';
5
6
  import { createOutputWriter } from '../../lib/output.js';
6
7
  import * as fs from 'fs/promises';
7
8
  import * as path from 'path';
@@ -254,8 +255,12 @@ export function createAgentSaveCommand() {
254
255
  output.progress('To publish this version:');
255
256
  output.progress(' guild agent save --message "..." --publish');
256
257
  }
257
- // Output JSON to stdout for programmatic use
258
- output.data({ version });
258
+ // Output JSON to stdout only in --json mode.
259
+ // In interactive mode the progress messages above already
260
+ // show version details; dumping raw JSON is noise.
261
+ if (getOutputMode() === 'json') {
262
+ output.data({ version });
263
+ }
259
264
  }
260
265
  catch (error) {
261
266
  if (error instanceof GitError) {
@@ -3,7 +3,8 @@ import { Command } from 'commander';
3
3
  import { GuildAPIClient } from '../../lib/api-client.js';
4
4
  import { getAuthToken } from '../../lib/auth.js';
5
5
  import { handleAxiosError } from '../../lib/errors.js';
6
- import { createOutputWriter } from '../../lib/output.js';
6
+ import { getOutputMode } from '../../lib/output-mode.js';
7
+ import { createOutputWriter, formatAgentTable } from '../../lib/output.js';
7
8
  const SORT_MAP = {
8
9
  updated: 'updated_at',
9
10
  newest: 'created_at',
@@ -40,7 +41,12 @@ export function createAgentSearchCommand() {
40
41
  params.append('sort_by', sortField);
41
42
  }
42
43
  const response = await client.get(`/agents?${params.toString()}`);
43
- output.data(response);
44
+ if (getOutputMode() === 'json') {
45
+ output.data(response);
46
+ }
47
+ else {
48
+ formatAgentTable(response.items, response.pagination);
49
+ }
44
50
  }
45
51
  catch (error) {
46
52
  const formattedError = handleAxiosError(error);
@@ -14,7 +14,7 @@ import { showBetaGuidance } from '../../lib/auth.js';
14
14
  import * as readline from 'readline';
15
15
  import { isQuietMode, getOutputMode } from '../../lib/output-mode.js';
16
16
  import { pollForResponse } from '../../lib/session-polling.js';
17
- import { readStdinAsJSON } from '../../lib/stdin.js';
17
+ import { readStdinAsJSON, ensureInteractiveStdin } from '../../lib/stdin.js';
18
18
  import { loadLocalConfig, getWorkspaceId } from '../../lib/guild-config.js';
19
19
  import { runGit, GitError, formatGitError } from '../../lib/git.js';
20
20
  import { pollUntilComplete } from '../../lib/polling.js';
@@ -415,6 +415,7 @@ export function createAgentTestCommand() {
415
415
  process.exit(0);
416
416
  }
417
417
  // Interactive mode: use shared ChatApp (spinner, progress, prompt)
418
+ ensureInteractiveStdin('guild agent test');
418
419
  const resumeCommand = 'guild agent test';
419
420
  const isInteractive = getOutputMode() === 'interactive' && !isQuietMode();
420
421
  if (isInteractive) {
@@ -4,7 +4,8 @@ import { GuildAPIClient } from '../../lib/api-client.js';
4
4
  import { getGuildcoreUrl } from '../../lib/config.js';
5
5
  import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
6
6
  import { getAgentId } from '../../lib/agent-helpers.js';
7
- import { createOutputWriter } from '../../lib/output.js';
7
+ import { getOutputMode } from '../../lib/output-mode.js';
8
+ import { createOutputWriter, formatVersionTable } from '../../lib/output.js';
8
9
  export function createAgentVersionsCommand() {
9
10
  const cmd = new Command('versions');
10
11
  cmd
@@ -22,7 +23,12 @@ export function createAgentVersionsCommand() {
22
23
  const offset = parseInt(options.offset, 10);
23
24
  try {
24
25
  const result = await client.get(`/agents/${agentId}/versions?limit=${limit}&offset=${offset}`);
25
- output.data(result);
26
+ if (getOutputMode() === 'json') {
27
+ output.data(result);
28
+ }
29
+ else {
30
+ formatVersionTable(result.items, result.pagination);
31
+ }
26
32
  }
27
33
  catch (error) {
28
34
  const formattedError = handleAxiosError(error);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createAgentWorkspacesCommand(): Command;
3
+ //# sourceMappingURL=workspaces.d.ts.map
@@ -0,0 +1,51 @@
1
+ // Copyright (c) 2026 Guild.ai All Rights Reserved
2
+ import { Command } from 'commander';
3
+ import { GuildAPIClient } from '../../lib/api-client.js';
4
+ import { getGuildcoreUrl } from '../../lib/config.js';
5
+ import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
6
+ import { getAgentId } from '../../lib/agent-helpers.js';
7
+ import { createOutputWriter, formatWorkspaceTable } from '../../lib/output.js';
8
+ import { getOutputMode } from '../../lib/output-mode.js';
9
+ export function createAgentWorkspacesCommand() {
10
+ const cmd = new Command('workspaces');
11
+ cmd
12
+ .description('List workspaces that use an agent')
13
+ .argument('[identifier]', 'Agent ID or full name (e.g., owner/agent-name)')
14
+ .option('--limit <number>', 'Maximum number of workspaces to return', '20')
15
+ .option('--offset <number>', 'Number of workspaces to skip', '0')
16
+ .action(async (agentIdArg, options) => {
17
+ const output = createOutputWriter();
18
+ const { agentId } = await getAgentId(agentIdArg);
19
+ const baseUrl = getGuildcoreUrl();
20
+ const client = new GuildAPIClient({ baseUrl });
21
+ const limit = parseInt(options.limit, 10);
22
+ const offset = parseInt(options.offset, 10);
23
+ try {
24
+ const result = await client.get(`/agents/${agentId}/workspaces?limit=${limit}&offset=${offset}`);
25
+ if (getOutputMode() === 'json') {
26
+ output.data(result);
27
+ }
28
+ else {
29
+ formatWorkspaceTable(result.items, result.pagination);
30
+ }
31
+ }
32
+ catch (error) {
33
+ const formattedError = handleAxiosError(error);
34
+ if (formattedError.code === ErrorCodes.AUTH_REQUIRED) {
35
+ output.error('Not logged in.', 'Please authenticate first:\n guild auth login');
36
+ }
37
+ else if (formattedError.code === ErrorCodes.CONN_REFUSED) {
38
+ output.error('Cannot connect to Guild servers');
39
+ }
40
+ else if (formattedError.code === ErrorCodes.NOT_FOUND) {
41
+ output.error(`Agent not found: ${agentId}`, 'Check the agent ID:\n guild agent list');
42
+ }
43
+ else {
44
+ output.error(`Failed to list workspaces: ${formattedError.details}`);
45
+ }
46
+ process.exit(1);
47
+ }
48
+ });
49
+ return cmd;
50
+ }
51
+ //# sourceMappingURL=workspaces.js.map
@@ -16,6 +16,7 @@ import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayNam
16
16
  import { printResumeHint, fetchSession, fetchSessionEvents, eventsToDisplayMessages, } from '../lib/session-resume.js';
17
17
  import { AgentInstallPrompt } from '../components/AgentInstallPrompt.js';
18
18
  import { getWorkspaceId } from '../lib/guild-config.js';
19
+ import { ensureInteractiveStdin } from '../lib/stdin.js';
19
20
  import { brand, BRAND_COLOR, code as codeColor } from '../lib/colors.js';
20
21
  import { SplashAnimation } from '../components/SplashAnimation.js';
21
22
  import { LOADING_TIMINGS } from '../lib/loading-messages.js';
@@ -1062,6 +1063,7 @@ export function createChatCommand() {
1062
1063
  }
1063
1064
  else {
1064
1065
  // Interactive mode - check auth first, then render UI with splash animation
1066
+ ensureInteractiveStdin('guild chat');
1065
1067
  await ensureAuthenticated();
1066
1068
  // Build the resume command string for exit hints
1067
1069
  let resumeCommand = 'guild chat';
@@ -4,7 +4,8 @@ import { GuildAPIClient } from '../../../lib/api-client.js';
4
4
  import { getAuthToken } from '../../../lib/auth.js';
5
5
  import { getWorkspaceId } from '../../../lib/guild-config.js';
6
6
  import { handleAxiosError, ErrorCodes } from '../../../lib/errors.js';
7
- import { createOutputWriter } from '../../../lib/output.js';
7
+ import { createOutputWriter, formatWorkspaceAgentTable } from '../../../lib/output.js';
8
+ import { getOutputMode } from '../../../lib/output-mode.js';
8
9
  export function createWorkspaceAgentListCommand() {
9
10
  const cmd = new Command('list');
10
11
  cmd
@@ -36,12 +37,12 @@ export function createWorkspaceAgentListCommand() {
36
37
  params.append('limit', options.limit);
37
38
  params.append('offset', options.offset);
38
39
  const response = await client.get(`/workspaces/${workspaceId}/workspace_agents?${params.toString()}`);
39
- const agents = response.items;
40
- if (agents.length === 0) {
41
- output.data({ items: [], message: 'No agents installed in this workspace.' });
42
- return;
40
+ if (getOutputMode() === 'json') {
41
+ output.data(response);
42
+ }
43
+ else {
44
+ formatWorkspaceAgentTable(response.items);
43
45
  }
44
- output.data(response);
45
46
  }
46
47
  catch (error) {
47
48
  const formattedError = handleAxiosError(error);
@@ -2,7 +2,8 @@
2
2
  import { Command } from 'commander';
3
3
  import { GuildAPIClient } from '../../../lib/api-client.js';
4
4
  import { handleAxiosError, ErrorCodes } from '../../../lib/errors.js';
5
- import { createOutputWriter } from '../../../lib/output.js';
5
+ import { getOutputMode } from '../../../lib/output-mode.js';
6
+ import { createOutputWriter, formatContextTable } from '../../../lib/output.js';
6
7
  export function createWorkspaceContextListCommand() {
7
8
  const cmd = new Command('list');
8
9
  cmd
@@ -18,7 +19,12 @@ export function createWorkspaceContextListCommand() {
18
19
  params.append('limit', options.limit);
19
20
  params.append('offset', options.offset);
20
21
  const response = await client.get(`/workspaces/${workspaceId}/contexts?${params.toString()}`);
21
- output.data(response);
22
+ if (getOutputMode() === 'json') {
23
+ output.data(response);
24
+ }
25
+ else {
26
+ formatContextTable(response.items, response.pagination);
27
+ }
22
28
  }
23
29
  catch (error) {
24
30
  const formattedError = handleAxiosError(error);
@@ -13,7 +13,7 @@ export function createWorkspaceContextPublishCommand() {
13
13
  const output = createOutputWriter();
14
14
  try {
15
15
  const client = new GuildAPIClient();
16
- await client.patch(`/workspaces/${workspaceId}/contexts/${contextId}`, {
16
+ await client.patch(`/contexts/${contextId}`, {
17
17
  status: 'PUBLISHED',
18
18
  });
19
19
  output.success(`Context ${contextId} published successfully`);
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import { createAgentUnpublishCommand } from './commands/agent/unpublish.js';
28
28
  import { createAgentRevalidateCommand } from './commands/agent/revalidate.js';
29
29
  import { createAgentSearchCommand } from './commands/agent/search.js';
30
30
  import { createAgentOwnersCommand } from './commands/agent/owners.js';
31
+ import { createAgentWorkspacesCommand } from './commands/agent/workspaces.js';
31
32
  import { createAgentTagsListCommand } from './commands/agent/tags/list.js';
32
33
  import { createAgentTagsAddCommand } from './commands/agent/tags/add.js';
33
34
  import { createAgentTagsRemoveCommand } from './commands/agent/tags/remove.js';
@@ -66,6 +67,7 @@ import { createDoctorCommand } from './commands/doctor.js';
66
67
  import { createSetupCommand } from './commands/setup.js';
67
68
  import { showSplashScreen, shouldShowSplash } from './lib/splash.js';
68
69
  import { checkForUpdate } from './lib/update-check.js';
70
+ import { setupUnknownCommandSuggestions } from './lib/did-you-mean.js';
69
71
  import chalk from 'chalk';
70
72
  // ESM equivalent of __dirname
71
73
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -131,6 +133,7 @@ agentCmd.addCommand(createAgentUnpublishCommand());
131
133
  agentCmd.addCommand(createAgentRevalidateCommand());
132
134
  agentCmd.addCommand(createAgentSearchCommand());
133
135
  agentCmd.addCommand(createAgentOwnersCommand());
136
+ agentCmd.addCommand(createAgentWorkspacesCommand());
134
137
  // Agent tags subcommand group
135
138
  const tagsCmd = agentCmd.command('tags').description('Manage agent tags');
136
139
  tagsCmd.addCommand(createAgentTagsListCommand());
@@ -214,6 +217,8 @@ configCmd.addCommand(createConfigListCommand());
214
217
  configCmd.addCommand(createConfigGetCommand());
215
218
  configCmd.addCommand(createConfigSetCommand());
216
219
  configCmd.addCommand(createConfigPathCommand());
220
+ // Set up cross-level "Did you mean?" suggestions for unknown commands
221
+ setupUnknownCommandSuggestions(program);
217
222
  // Parse arguments
218
223
  program.parse(process.argv);
219
224
  //# sourceMappingURL=index.js.map
@@ -66,8 +66,8 @@ export async function readAgentFiles(cwd) {
66
66
  const content = await fs.readFile(fullPath, 'utf-8');
67
67
  files.push({ path: filePath, content });
68
68
  }
69
- const fileNames = files.map((f) => f.path);
70
- const missing = REQUIRED_AGENT_FILES.filter((f) => !fileNames.includes(f));
69
+ const filePaths = files.map((f) => f.path);
70
+ const missing = REQUIRED_AGENT_FILES.filter((req) => !filePaths.some((fp) => fp === req || fp.endsWith(`/${req}`)));
71
71
  if (missing.length > 0) {
72
72
  throw new Error(`Missing required files: ${missing.join(', ')}`);
73
73
  }
@@ -0,0 +1,25 @@
1
+ import { Command } from 'commander';
2
+ interface CommandEntry {
3
+ path: string;
4
+ description: string;
5
+ leafName: string;
6
+ }
7
+ /**
8
+ * Recursively collects all commands from the command tree.
9
+ * Includes both leaf commands and command groups (parents with subcommands).
10
+ * Aliases are added as separate entries.
11
+ */
12
+ export declare function getAllCommands(cmd: Command, prefix?: string): CommandEntry[];
13
+ /**
14
+ * Standard Levenshtein distance between two strings.
15
+ */
16
+ export declare function levenshtein(a: string, b: string): number;
17
+ /**
18
+ * Sets up cross-level "Did you mean?" suggestions for unknown commands.
19
+ *
20
+ * Disables Commander's built-in same-level suggestions and intercepts
21
+ * "unknown command" errors to search the entire command tree.
22
+ */
23
+ export declare function setupUnknownCommandSuggestions(program: Command): void;
24
+ export {};
25
+ //# sourceMappingURL=did-you-mean.d.ts.map
@@ -0,0 +1,143 @@
1
+ // Copyright (c) 2026 Guild.ai All Rights Reserved
2
+ /**
3
+ * Recursively collects all commands from the command tree.
4
+ * Includes both leaf commands and command groups (parents with subcommands).
5
+ * Aliases are added as separate entries.
6
+ */
7
+ export function getAllCommands(cmd, prefix = '') {
8
+ const results = [];
9
+ for (const sub of cmd.commands) {
10
+ const name = sub.name();
11
+ const cmdPath = prefix ? `${prefix} ${name}` : name;
12
+ const description = sub.description();
13
+ const hasChildren = sub.commands.length > 0;
14
+ // Always include this command (even if it has subcommands)
15
+ results.push({ path: cmdPath, description, leafName: name });
16
+ // If this command has subcommands, also recurse into them
17
+ if (hasChildren) {
18
+ results.push(...getAllCommands(sub, cmdPath));
19
+ }
20
+ // Add aliases as separate entries
21
+ for (const alias of sub.aliases()) {
22
+ const aliasPath = prefix ? `${prefix} ${alias}` : alias;
23
+ results.push({ path: aliasPath, description, leafName: alias });
24
+ if (hasChildren) {
25
+ results.push(...getAllCommands(sub, aliasPath));
26
+ }
27
+ }
28
+ }
29
+ return results;
30
+ }
31
+ /**
32
+ * Standard Levenshtein distance between two strings.
33
+ */
34
+ export function levenshtein(a, b) {
35
+ const m = a.length;
36
+ const n = b.length;
37
+ // Use a single-row DP approach for space efficiency
38
+ const row = Array.from({ length: n + 1 }, (_, i) => i);
39
+ for (let i = 1; i <= m; i++) {
40
+ let prev = i - 1;
41
+ row[0] = i;
42
+ for (let j = 1; j <= n; j++) {
43
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
44
+ const val = Math.min(row[j] + 1, // deletion
45
+ row[j - 1] + 1, // insertion
46
+ prev + cost // substitution
47
+ );
48
+ prev = row[j];
49
+ row[j] = val;
50
+ }
51
+ }
52
+ return row[n];
53
+ }
54
+ /**
55
+ * Recursively disables Commander's built-in suggestion on all commands.
56
+ */
57
+ function disableBuiltinSuggestions(cmd) {
58
+ cmd.showSuggestionAfterError(false);
59
+ for (const sub of cmd.commands) {
60
+ disableBuiltinSuggestions(sub);
61
+ }
62
+ }
63
+ /**
64
+ * Creates the writeErr handler that intercepts "unknown command" errors
65
+ * and prints cross-level suggestions.
66
+ */
67
+ function createWriteErrHandler(program) {
68
+ return (str) => {
69
+ // Always write the original error
70
+ process.stderr.write(str);
71
+ // Check if this is an "unknown command" error
72
+ const match = /unknown command '([^']+)'/.exec(str);
73
+ if (!match) {
74
+ return;
75
+ }
76
+ const unknown = match[1];
77
+ const allCommands = getAllCommands(program);
78
+ // Determine the typed parent context from process.argv
79
+ // e.g., "guild agent trigger" → typed args are ["agent", "trigger"]
80
+ const typedArgs = process.argv.slice(2);
81
+ const typedParent = typedArgs.slice(0, -1).join(' ');
82
+ // Score each command
83
+ const scored = allCommands.map((entry) => {
84
+ const leafName = entry.leafName;
85
+ // Exact leaf name match gets score 0
86
+ let score = leafName === unknown ? 0 : levenshtein(unknown, leafName);
87
+ // Context bonus: if command's parent matches typed parent, reduce score
88
+ const entryParts = entry.path.split(' ');
89
+ const entryParent = entryParts.slice(0, -1).join(' ');
90
+ if (entryParent === typedParent && score > 0) {
91
+ score -= 0.5;
92
+ }
93
+ return { ...entry, score };
94
+ });
95
+ // Filter poor matches
96
+ const threshold = Math.max(2, unknown.length / 2);
97
+ const filtered = scored.filter((s) => s.score <= threshold);
98
+ if (filtered.length === 0) {
99
+ return;
100
+ }
101
+ // Sort by score, deduplicate by path
102
+ filtered.sort((a, b) => a.score - b.score);
103
+ const seen = new Set();
104
+ const unique = filtered.filter((entry) => {
105
+ if (seen.has(entry.path)) {
106
+ return false;
107
+ }
108
+ seen.add(entry.path);
109
+ return true;
110
+ });
111
+ // Print top 5 suggestions
112
+ const top = unique.slice(0, 5);
113
+ process.stderr.write('\nDid you mean?\n');
114
+ for (const entry of top) {
115
+ const cmd = `guild ${entry.path}`;
116
+ const padding = ' '.repeat(Math.max(2, 30 - cmd.length));
117
+ process.stderr.write(` ${cmd}${padding}${entry.description}\n`);
118
+ }
119
+ };
120
+ }
121
+ /**
122
+ * Recursively configures writeErr on a command and all its subcommands.
123
+ */
124
+ function configureOutputRecursively(cmd, writeErr) {
125
+ cmd.configureOutput({ writeErr });
126
+ for (const sub of cmd.commands) {
127
+ configureOutputRecursively(sub, writeErr);
128
+ }
129
+ }
130
+ /**
131
+ * Sets up cross-level "Did you mean?" suggestions for unknown commands.
132
+ *
133
+ * Disables Commander's built-in same-level suggestions and intercepts
134
+ * "unknown command" errors to search the entire command tree.
135
+ */
136
+ export function setupUnknownCommandSuggestions(program) {
137
+ // Disable Commander's built-in suggestion on all commands
138
+ disableBuiltinSuggestions(program);
139
+ // Create a shared writeErr handler and apply it to all commands
140
+ const writeErr = createWriteErrHandler(program);
141
+ configureOutputRecursively(program, writeErr);
142
+ }
143
+ //# sourceMappingURL=did-you-mean.js.map