@guildai/cli 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent/chat.js +29 -62
- 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 +59 -92
- 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.js +28 -15
- 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/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/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/session-events.d.ts +1 -1
- 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/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -13,7 +13,7 @@ import chalk from 'chalk';
|
|
|
13
13
|
import { readFileSync } from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName,
|
|
16
|
+
import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName, getAgentName, } from '../lib/session-events.js';
|
|
17
17
|
import { printResumeHint, fetchSession, fetchSessionEvents, eventsToDisplayMessages, } from '../lib/session-resume.js';
|
|
18
18
|
import { DEFAULT_EVENT_TYPES, parseEventFilter, shouldShowEvent, } from '../lib/event-filter.js';
|
|
19
19
|
import { fetchEvents, fetchTasks } from '../lib/session-events-fetch.js';
|
|
@@ -1087,24 +1087,37 @@ export function createChatCommand() {
|
|
|
1087
1087
|
else {
|
|
1088
1088
|
inactivityCounter++;
|
|
1089
1089
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const targetAgent = options.agent || 'assistant';
|
|
1095
|
-
const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' &&
|
|
1096
|
-
matchesAgent(e.task?.agent, targetAgent) &&
|
|
1097
|
-
!e.task?.parent_task_id // Root task has no parent
|
|
1098
|
-
);
|
|
1099
|
-
// Also check for agent_notification_message events from the root agent
|
|
1100
|
-
const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
|
|
1101
|
-
matchesAgent(e.task?.agent, targetAgent) &&
|
|
1102
|
-
!e.task?.parent_task_id);
|
|
1090
|
+
const isRootTask = (e) => !e.task?.parent_task;
|
|
1091
|
+
const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' && isRootTask(e));
|
|
1092
|
+
const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' && isRootTask(e));
|
|
1093
|
+
const hasRootTaskError = allEvents.some((e) => e.type === 'runtime_error' && isRootTask(e));
|
|
1103
1094
|
// Check for a ui_prompt request... that ends the game.
|
|
1104
1095
|
const hasUIPromptMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
|
|
1105
1096
|
e.task?.tool_name === 'ui_prompt');
|
|
1097
|
+
if (hasRootTaskError) {
|
|
1098
|
+
debug('Found error event from root agent, exiting --once mode');
|
|
1099
|
+
const errorEvents = allEvents.filter((e) => e.type === 'runtime_error' || e.type === 'agent_notification_error');
|
|
1100
|
+
if (errorEvents.length > 0 && !options.mode) {
|
|
1101
|
+
const lastError = errorEvents[errorEvents.length - 1];
|
|
1102
|
+
const content = lastError.content;
|
|
1103
|
+
if (content?.data) {
|
|
1104
|
+
console.error(chalk.red(`Error: ${content.data}`));
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
console.error(chalk.red('Agent failed to start'));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
else if (options.mode === 'json') {
|
|
1111
|
+
console.log(JSON.stringify({
|
|
1112
|
+
session_id: session.id,
|
|
1113
|
+
events: allEvents,
|
|
1114
|
+
error: true,
|
|
1115
|
+
}, null, 2));
|
|
1116
|
+
}
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
}
|
|
1106
1119
|
if (hasRootTaskDone || hasAgentMessage || hasUIPromptMessage) {
|
|
1107
|
-
debug(
|
|
1120
|
+
debug('Found completion event from root agent, exiting --once mode');
|
|
1108
1121
|
await outputOnceResult(session.id, allEvents, options.mode);
|
|
1109
1122
|
process.exit(0);
|
|
1110
1123
|
}
|
|
@@ -27,13 +27,13 @@ export function createConfigListCommand() {
|
|
|
27
27
|
output.progress(chalk.dim(' guild workspace select'));
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
if (hasGlobal) {
|
|
30
|
+
if (config.global && hasGlobal) {
|
|
31
31
|
output.progress(chalk.bold('Global config') + chalk.dim(' (~/.guild/config.json):'));
|
|
32
32
|
for (const [key, value] of Object.entries(config.global)) {
|
|
33
33
|
output.progress(` ${key}: ${chalk.cyan(String(value))}`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
if (hasLocal) {
|
|
36
|
+
if (config.local && hasLocal) {
|
|
37
37
|
if (hasGlobal)
|
|
38
38
|
output.progress('');
|
|
39
39
|
output.progress(chalk.bold('Local config') + chalk.dim(' (guild.json):'));
|
|
@@ -17,7 +17,7 @@ export function createIntegrationOperationCreateCommand() {
|
|
|
17
17
|
cmd
|
|
18
18
|
.description('Create operation(s) for an integration version')
|
|
19
19
|
.argument('<id_or_name>', 'Integration ID or name (owner~name)')
|
|
20
|
-
.option('--version <semver>', 'Specific version, e.g. 1.0.0')
|
|
20
|
+
.option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
|
|
21
21
|
.option('--operation <name>', 'Operation identifier, e.g. list_users')
|
|
22
22
|
.option('--method <method>', 'HTTP method: GET, POST, PUT, PATCH, DELETE')
|
|
23
23
|
.option('--path <path>', 'REST path, e.g. /{owner}/{repo}/issues')
|
|
@@ -35,7 +35,7 @@ export function createIntegrationOperationCreateCommand() {
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
const client = new GuildAPIClient();
|
|
38
|
-
const versionId = await resolveVersionId(client, identifier, options.
|
|
38
|
+
const versionId = await resolveVersionId(client, identifier, options.versionNumber);
|
|
39
39
|
if (options.openapi) {
|
|
40
40
|
// OpenAPI mode
|
|
41
41
|
if (!existsSync(options.openapi)) {
|
|
@@ -14,7 +14,7 @@ export function createIntegrationOperationListCommand() {
|
|
|
14
14
|
cmd
|
|
15
15
|
.description('List operations for an integration version')
|
|
16
16
|
.argument('<id_or_name>', 'Integration ID or name (owner~name)')
|
|
17
|
-
.option('--version <semver>', 'Specific version, e.g. 1.0.0')
|
|
17
|
+
.option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
|
|
18
18
|
.option('--limit <number>', 'Number of results to return', '100')
|
|
19
19
|
.option('--offset <number>', 'Offset for pagination', '0')
|
|
20
20
|
.action(async (identifier, options) => {
|
|
@@ -26,7 +26,7 @@ export function createIntegrationOperationListCommand() {
|
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
28
|
const client = new GuildAPIClient();
|
|
29
|
-
const versionId = await resolveVersionId(client, identifier, options.
|
|
29
|
+
const versionId = await resolveVersionId(client, identifier, options.versionNumber);
|
|
30
30
|
const params = new URLSearchParams();
|
|
31
31
|
params.append('limit', options.limit);
|
|
32
32
|
params.append('offset', options.offset);
|
|
@@ -63,7 +63,7 @@ export function createIntegrationUpdateCommand() {
|
|
|
63
63
|
hasUpdates = true;
|
|
64
64
|
}
|
|
65
65
|
if (options.baseUrl !== undefined) {
|
|
66
|
-
body.protocol_config = { base_url: options.baseUrl };
|
|
66
|
+
body.protocol_config = { protocol: 'REST', base_url: options.baseUrl };
|
|
67
67
|
hasUpdates = true;
|
|
68
68
|
}
|
|
69
69
|
// Auth config updates need the discriminator
|
|
@@ -34,7 +34,7 @@ export function createIntegrationVersionGetCommand() {
|
|
|
34
34
|
cmd
|
|
35
35
|
.description('Get version details')
|
|
36
36
|
.argument('<id_or_name>', 'Integration ID or name (owner~name)')
|
|
37
|
-
.option('--version <semver>', 'Specific version, e.g. 1.0.0')
|
|
37
|
+
.option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
|
|
38
38
|
.action(async (identifier, options) => {
|
|
39
39
|
const output = createOutputWriter();
|
|
40
40
|
try {
|
|
@@ -44,7 +44,7 @@ export function createIntegrationVersionGetCommand() {
|
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
46
|
const client = new GuildAPIClient();
|
|
47
|
-
const versionId = await resolveVersionId(client, identifier, options.
|
|
47
|
+
const versionId = await resolveVersionId(client, identifier, options.versionNumber);
|
|
48
48
|
const response = await client.get(`/integration_versions/${versionId}`);
|
|
49
49
|
if (getOutputMode() === 'json') {
|
|
50
50
|
output.data(response);
|
|
@@ -23,7 +23,7 @@ export function createIntegrationVersionPublishCommand() {
|
|
|
23
23
|
cmd
|
|
24
24
|
.description('Publish a built version')
|
|
25
25
|
.argument('<id_or_name>', 'Integration ID or name (owner~name)')
|
|
26
|
-
.option('--version <semver>', 'Specific version to publish, e.g. 1.0.0')
|
|
26
|
+
.option('--version-number <semver>', 'Specific version to publish, e.g. 1.0.0')
|
|
27
27
|
.action(async (identifier, options) => {
|
|
28
28
|
const output = createOutputWriter();
|
|
29
29
|
try {
|
|
@@ -33,7 +33,7 @@ export function createIntegrationVersionPublishCommand() {
|
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
35
|
const client = new GuildAPIClient();
|
|
36
|
-
const versionId = await resolveVersionId(client, identifier, options.
|
|
36
|
+
const versionId = await resolveVersionId(client, identifier, options.versionNumber);
|
|
37
37
|
// Get current version info for display
|
|
38
38
|
const currentVersion = await client.get(`/integration_versions/${versionId}`);
|
|
39
39
|
const versionDisplay = currentVersion.version_number || versionId;
|
|
@@ -22,7 +22,7 @@ export function createIntegrationVersionTestCommand() {
|
|
|
22
22
|
cmd
|
|
23
23
|
.description('Test an endpoint invocation')
|
|
24
24
|
.argument('<id_or_name>', 'Integration ID or name (owner~name)')
|
|
25
|
-
.option('--version <semver>', 'Specific version, e.g. 1.0.0')
|
|
25
|
+
.option('--version-number <semver>', 'Specific version, e.g. 1.0.0')
|
|
26
26
|
.requiredOption('--operation <name>', 'Operation to test, e.g. list_users')
|
|
27
27
|
.option('--account <name>', 'Account name to resolve credential from')
|
|
28
28
|
.option('--input-path <json>', 'JSON object of path parameters')
|
|
@@ -50,7 +50,7 @@ export function createIntegrationVersionTestCommand() {
|
|
|
50
50
|
body.input_body = parseJsonFlag(options.inputBody, '--input-body');
|
|
51
51
|
}
|
|
52
52
|
const client = new GuildAPIClient();
|
|
53
|
-
const versionId = await resolveVersionId(client, identifier, options.
|
|
53
|
+
const versionId = await resolveVersionId(client, identifier, options.versionNumber);
|
|
54
54
|
if (options.account) {
|
|
55
55
|
const creds = await client.get(`/accounts/${options.account}/credentials?integration=${encodeURIComponent(identifier)}`);
|
|
56
56
|
if (creds.items.length === 0) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { GuildAPIClient } from '../../lib/api-client.js';
|
|
5
5
|
import { getAuthToken } from '../../lib/auth.js';
|
|
6
|
+
import { parseEventFilter } from '../../lib/event-filter.js';
|
|
6
7
|
import { handleAxiosError } from '../../lib/errors.js';
|
|
7
8
|
import { createOutputWriter } from '../../lib/output.js';
|
|
8
9
|
export function createSessionEventsCommand() {
|
|
@@ -10,7 +11,7 @@ export function createSessionEventsCommand() {
|
|
|
10
11
|
cmd
|
|
11
12
|
.description('List events in a session')
|
|
12
13
|
.argument('<session-id>', 'Session ID')
|
|
13
|
-
.option('--
|
|
14
|
+
.option('--events <types>', 'Event types to show. Shorthands: none, user, system, all, or comma-separated type names')
|
|
14
15
|
.option('--limit <number>', 'Number of results to return', '100')
|
|
15
16
|
.option('--offset <number>', 'Offset for pagination', '0')
|
|
16
17
|
.action(async (sessionId, options) => {
|
|
@@ -25,8 +26,11 @@ export function createSessionEventsCommand() {
|
|
|
25
26
|
const params = new URLSearchParams();
|
|
26
27
|
params.append('limit', options.limit);
|
|
27
28
|
params.append('offset', options.offset);
|
|
28
|
-
if (options.
|
|
29
|
-
|
|
29
|
+
if (options.events) {
|
|
30
|
+
const filter = parseEventFilter(options.events);
|
|
31
|
+
if (filter.size > 0) {
|
|
32
|
+
params.append('types', [...filter].join(','));
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
const response = await client.get(`/sessions/${sessionId}/events?${params.toString()}`);
|
|
32
36
|
output.data(response);
|
|
@@ -8,7 +8,7 @@ export function createWorkspaceGetCommand() {
|
|
|
8
8
|
const cmd = new Command('get');
|
|
9
9
|
cmd
|
|
10
10
|
.description('Get workspace details')
|
|
11
|
-
.argument('<identifier>', 'Workspace ID or full name (e.g., owner
|
|
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();
|
|
@@ -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
|