@guildai/cli 0.6.2 → 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.
- package/README.md +3 -1
- package/dist/commands/agent/chat.js +41 -99
- package/dist/commands/agent/clone.js +1 -1
- package/dist/commands/agent/code.js +1 -1
- package/dist/commands/agent/fork.js +2 -2
- package/dist/commands/agent/get.js +1 -1
- package/dist/commands/agent/grep.js +2 -2
- package/dist/commands/agent/init.js +10 -3
- package/dist/commands/agent/list.js +9 -1
- package/dist/commands/agent/publish.js +1 -1
- package/dist/commands/agent/revalidate.js +1 -1
- package/dist/commands/agent/save.js +23 -2
- package/dist/commands/agent/test.js +70 -130
- package/dist/commands/agent/unpublish.js +1 -1
- package/dist/commands/agent/update.js +1 -1
- package/dist/commands/agent/versions.js +1 -1
- package/dist/commands/agent/workspaces.js +1 -1
- package/dist/commands/chat.d.ts +2 -1
- package/dist/commands/chat.js +189 -88
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/integration/operation/create.js +2 -2
- package/dist/commands/integration/operation/list.js +2 -2
- package/dist/commands/integration/update.js +1 -1
- package/dist/commands/integration/version/get.js +2 -2
- package/dist/commands/integration/version/publish.js +2 -2
- package/dist/commands/integration/version/test.js +2 -2
- package/dist/commands/session/events.js +7 -3
- package/dist/commands/session/interrupt.d.ts +3 -0
- package/dist/commands/session/interrupt.js +33 -0
- package/dist/commands/setup.js +70 -11
- package/dist/commands/workspace/get.js +1 -1
- package/dist/commands/workspace/list.js +28 -6
- package/dist/commands/workspace/select.js +40 -9
- package/dist/components/TaskView.js +2 -2
- package/dist/index.js +2 -0
- package/dist/lib/agent-helpers.d.ts +59 -2
- package/dist/lib/agent-helpers.js +153 -8
- package/dist/lib/alternate-screen.js +2 -0
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/config.d.ts +3 -0
- package/dist/lib/config.js +33 -0
- package/dist/lib/event-filter.d.ts +50 -0
- package/dist/lib/event-filter.js +91 -0
- package/dist/lib/generated-types.d.ts +2 -0
- package/dist/lib/generated-types.js +20 -0
- package/dist/lib/session-events.d.ts +27 -2
- package/dist/lib/session-events.js +5 -3
- package/dist/lib/session-polling.d.ts +8 -0
- package/dist/lib/session-polling.js +49 -0
- package/dist/lib/spinners.js +4 -1
- package/docs/CLI_WORKFLOW.md +7 -1
- package/docs/DESIGN.md +1 -1
- package/docs/skills/codex-agent-dev.md +155 -0
- package/docs/skills/integrations.md +338 -0
- package/package.json +1 -1
package/dist/commands/setup.js
CHANGED
|
@@ -11,7 +11,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
13
13
|
const docsDir = path.join(packageRoot, 'docs');
|
|
14
|
-
const
|
|
14
|
+
const CLAUDE_SKILL_FILES = [
|
|
15
15
|
{
|
|
16
16
|
src: path.join(docsDir, 'skills', 'agent-dev.md'),
|
|
17
17
|
dest: path.join('.claude', 'skills', 'agent-dev', 'skill.md'),
|
|
@@ -22,6 +22,18 @@ const SKILL_FILES = [
|
|
|
22
22
|
dest: path.join('.claude', 'skills', 'guild-cli-workflow', 'skill.md'),
|
|
23
23
|
label: '.claude/skills/guild-cli-workflow/skill.md',
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
src: path.join(docsDir, 'skills', 'integrations.md'),
|
|
27
|
+
dest: path.join('.claude', 'skills', 'integrations', 'skill.md'),
|
|
28
|
+
label: '.claude/skills/integrations/skill.md',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const CODEX_SKILL_FILES = [
|
|
32
|
+
{
|
|
33
|
+
src: path.join(docsDir, 'skills', 'codex-agent-dev.md'),
|
|
34
|
+
dest: path.join('.agents', 'skills', 'guild-agent-dev', 'SKILL.md'),
|
|
35
|
+
label: '.agents/skills/guild-agent-dev/SKILL.md',
|
|
36
|
+
},
|
|
25
37
|
];
|
|
26
38
|
const CLAUDE_MD_TEMPLATE = `# CLAUDE.md
|
|
27
39
|
|
|
@@ -38,6 +50,22 @@ guild agent save --message "Initial version" --wait --publish
|
|
|
38
50
|
|
|
39
51
|
See \`.claude/skills/agent-dev/skill.md\` for SDK reference, patterns, and anti-hallucination guide.
|
|
40
52
|
`;
|
|
53
|
+
const AGENTS_MD_TEMPLATE = `# AGENTS.md
|
|
54
|
+
|
|
55
|
+
## Guild Agent Development
|
|
56
|
+
|
|
57
|
+
This project uses Guild for agent development. Codex instructions are installed in \`.agents/skills/guild-agent-dev/SKILL.md\`.
|
|
58
|
+
|
|
59
|
+
### Quick Start
|
|
60
|
+
|
|
61
|
+
\`\`\`bash
|
|
62
|
+
guild agent init --name my-agent --template LLM
|
|
63
|
+
guild agent test --ephemeral
|
|
64
|
+
guild agent save -A --message "Initial version"
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
Use \`guild doctor\` to check authentication, server connectivity, workspace selection, and git setup.
|
|
68
|
+
`;
|
|
41
69
|
async function fileExists(filePath) {
|
|
42
70
|
try {
|
|
43
71
|
await fs.access(filePath);
|
|
@@ -82,21 +110,29 @@ async function setupMcp(options) {
|
|
|
82
110
|
}
|
|
83
111
|
async function setup(options) {
|
|
84
112
|
const output = createOutputWriter();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
113
|
+
if (options.agentsMd && !options.codex) {
|
|
114
|
+
output.error('--agents-md requires --codex', 'Create Codex setup files with:\n guild setup --codex --agents-md');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (options.claudeMd && options.codex) {
|
|
118
|
+
output.error('--claude-md cannot be used with --codex', 'Create Claude setup with:\n guild setup --claude-md\n\nCreate Codex setup with:\n guild setup --codex --agents-md');
|
|
89
119
|
process.exit(1);
|
|
90
120
|
}
|
|
121
|
+
const skillFiles = options.codex ? CODEX_SKILL_FILES : CLAUDE_SKILL_FILES;
|
|
122
|
+
// Verify source docs exist
|
|
123
|
+
for (const file of skillFiles) {
|
|
124
|
+
if (!(await fileExists(file.src))) {
|
|
125
|
+
output.error('Could not find Guild CLI docs. Reinstall the CLI: npm install -g @guildai/cli');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
91
129
|
output.progress('Setting up Guild CLI skills...');
|
|
92
130
|
output.progress('');
|
|
93
|
-
// Ensure .claude/skills/ directory exists
|
|
94
|
-
const skillsDir = path.join(process.cwd(), '.claude', 'skills');
|
|
95
|
-
await fs.mkdir(skillsDir, { recursive: true });
|
|
96
131
|
let filesCreated = 0;
|
|
97
132
|
let filesSkipped = 0;
|
|
133
|
+
let codexProjectFilesChanged = false;
|
|
98
134
|
// Copy skill files
|
|
99
|
-
for (const file of
|
|
135
|
+
for (const file of skillFiles) {
|
|
100
136
|
const destPath = path.join(process.cwd(), file.dest);
|
|
101
137
|
const exists = await fileExists(destPath);
|
|
102
138
|
if (exists && !options.force) {
|
|
@@ -113,6 +149,9 @@ async function setup(options) {
|
|
|
113
149
|
output.success(`Created ${file.label}`);
|
|
114
150
|
}
|
|
115
151
|
filesCreated++;
|
|
152
|
+
if (options.codex) {
|
|
153
|
+
codexProjectFilesChanged = true;
|
|
154
|
+
}
|
|
116
155
|
}
|
|
117
156
|
}
|
|
118
157
|
// Handle --mcp flag
|
|
@@ -139,6 +178,21 @@ async function setup(options) {
|
|
|
139
178
|
filesCreated++;
|
|
140
179
|
}
|
|
141
180
|
}
|
|
181
|
+
// Handle AGENTS.md template for Codex setup
|
|
182
|
+
if (options.agentsMd) {
|
|
183
|
+
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
|
184
|
+
const exists = await fileExists(agentsMdPath);
|
|
185
|
+
if (exists) {
|
|
186
|
+
output.progress('AGENTS.md already exists (not overwriting)');
|
|
187
|
+
filesSkipped++;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
await fs.writeFile(agentsMdPath, AGENTS_MD_TEMPLATE, 'utf-8');
|
|
191
|
+
output.success('Created AGENTS.md');
|
|
192
|
+
filesCreated++;
|
|
193
|
+
codexProjectFilesChanged = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
142
196
|
// Summary
|
|
143
197
|
output.progress('');
|
|
144
198
|
if (filesCreated > 0 && filesSkipped === 0) {
|
|
@@ -155,13 +209,18 @@ async function setup(options) {
|
|
|
155
209
|
else {
|
|
156
210
|
output.success('Guild CLI skills installed.');
|
|
157
211
|
}
|
|
212
|
+
if (codexProjectFilesChanged) {
|
|
213
|
+
output.progress('Restart Codex to pick up the new project instructions.');
|
|
214
|
+
}
|
|
158
215
|
}
|
|
159
216
|
export function createSetupCommand() {
|
|
160
217
|
const cmd = new Command('setup');
|
|
161
218
|
cmd
|
|
162
|
-
.description('Set up Guild CLI skills for coding assistants (Claude Code, etc.)')
|
|
163
|
-
.option('--force', 'Overwrite existing skill files', false)
|
|
219
|
+
.description('Set up Guild CLI skills for coding assistants (Claude Code, Codex, etc.)')
|
|
220
|
+
.option('--force', 'Overwrite existing skill files and Guild MCP config', false)
|
|
221
|
+
.option('--codex', 'Install Codex skill files instead of Claude Code skills', false)
|
|
164
222
|
.option('--claude-md', 'Also create a CLAUDE.md template in the project root', false)
|
|
223
|
+
.option('--agents-md', 'With --codex, also create an AGENTS.md template in the project root', false)
|
|
165
224
|
.option('--no-mcp', 'Skip MCP server configuration')
|
|
166
225
|
.action(async (options) => {
|
|
167
226
|
await setup(options);
|
|
@@ -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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
-
|
|
63
|
+
const directMatch = candidates.find((w) => matchesWorkspaceArg(w, workspaceArg));
|
|
64
|
+
if (!directMatch) {
|
|
56
65
|
const allWorkspaces = await client.fetchAll('/me/workspaces?filter=all');
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
170
|
+
tasksByParent.get(parentId)?.push(task);
|
|
171
171
|
});
|
|
172
172
|
// Process root tasks first, then children
|
|
173
173
|
const processedIds = new Set();
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,7 @@ import { createSessionEventsCommand } from './commands/session/events.js';
|
|
|
59
59
|
import { createSessionTasksCommand } from './commands/session/tasks.js';
|
|
60
60
|
import { createSessionCreateCommand } from './commands/session/create.js';
|
|
61
61
|
import { createSessionSendCommand } from './commands/session/send.js';
|
|
62
|
+
import { createSessionInterruptCommand } from './commands/session/interrupt.js';
|
|
62
63
|
import { createJobGetCommand } from './commands/job/get.js';
|
|
63
64
|
import { createJobStepGetCommand } from './commands/job/step-get.js';
|
|
64
65
|
import { createConfigListCommand } from './commands/config/list.js';
|
|
@@ -276,6 +277,7 @@ sessionCmd.addCommand(createSessionEventsCommand());
|
|
|
276
277
|
sessionCmd.addCommand(createSessionTasksCommand());
|
|
277
278
|
sessionCmd.addCommand(createSessionCreateCommand());
|
|
278
279
|
sessionCmd.addCommand(createSessionSendCommand());
|
|
280
|
+
sessionCmd.addCommand(createSessionInterruptCommand());
|
|
279
281
|
// Job command group
|
|
280
282
|
const jobCmd = program.command('job').description('Job management');
|
|
281
283
|
jobCmd.addCommand(createJobGetCommand());
|
|
@@ -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
|
|
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
|
|
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('
|
|
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}
|
|
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
|
package/dist/lib/api-client.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import { getAuthToken, clearAuthToken } from './auth.js';
|
|
5
5
|
import { retry, debug, GuildCLIError, ErrorCodes } from './errors.js';
|
|
6
|
-
import { getGuildcoreUrl } from './config.js';
|
|
6
|
+
import { getUserAgent, getGuildcoreUrl } from './config.js';
|
|
7
7
|
import { getIapHeaders } from './iap.js';
|
|
8
8
|
/**
|
|
9
9
|
* HTTP client for Guild API
|
|
@@ -24,6 +24,7 @@ export class GuildAPIClient {
|
|
|
24
24
|
headers: {
|
|
25
25
|
'Content-Type': 'application/json',
|
|
26
26
|
Accept: 'application/json',
|
|
27
|
+
'User-Agent': getUserAgent(),
|
|
27
28
|
},
|
|
28
29
|
});
|
|
29
30
|
debug(`API Client initialized: ${this.baseUrl}, retry: ${this.enableRetry}, max retries: ${this.maxRetries}`);
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare function getCliVersion(): string;
|
|
2
|
+
export declare function isDevBuild(): boolean;
|
|
3
|
+
export declare function getUserAgent(): string;
|
|
1
4
|
/**
|
|
2
5
|
* IAP (Identity-Aware Proxy) configuration for internal *.guildai.dev hosts.
|
|
3
6
|
* Users must run `gcloud auth login` with an authorized Google account to access.
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
// Copyright 2026 Guild.ai
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const FALLBACK_VERSION = 'unknown';
|
|
6
|
+
let cachedVersion;
|
|
7
|
+
export function getCliVersion() {
|
|
8
|
+
if (!cachedVersion) {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
11
|
+
cachedVersion = pkg.version ?? FALLBACK_VERSION;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
cachedVersion = FALLBACK_VERSION;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return cachedVersion;
|
|
18
|
+
}
|
|
19
|
+
export function isDevBuild() {
|
|
20
|
+
try {
|
|
21
|
+
return !__dirname.includes('node_modules');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function getUserAgent() {
|
|
28
|
+
try {
|
|
29
|
+
const base = `GuildCLI/${getCliVersion()}`;
|
|
30
|
+
return isDevBuild() ? `${base}/dev` : base;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 'GuildCLI';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
3
36
|
/**
|
|
4
37
|
* IAP configuration for shared.guildai.dev
|
|
5
38
|
*/
|