@guildai/cli 0.9.0 → 0.9.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.d.ts +1 -0
- package/dist/commands/agent/chat.js +24 -1
- package/dist/commands/agent/init.js +1 -1
- package/dist/commands/agent/test.d.ts +1 -0
- package/dist/commands/agent/test.js +92 -34
- package/dist/commands/chat.d.ts +1 -0
- package/dist/commands/chat.js +178 -46
- package/dist/commands/integration/connect.js +1 -1
- package/dist/commands/integration/create.js +36 -36
- package/dist/commands/integration/operation/create.js +2 -1
- package/dist/commands/integration/operation/list.js +23 -15
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/session/events.js +16 -7
- package/dist/commands/session/send.js +1 -1
- package/dist/commands/workspace/agent/add.js +16 -3
- package/dist/commands/workspace/agent/list.js +14 -1
- package/dist/commands/workspace/agent/remove.js +14 -1
- package/dist/commands/workspace/clear.d.ts +3 -0
- package/dist/commands/workspace/clear.js +45 -0
- package/dist/commands/workspace/select.js +3 -1
- package/dist/index.js +53 -6
- package/dist/lib/api-types.d.ts +1 -0
- package/dist/lib/generated-types.d.ts +1 -1
- package/dist/lib/generated-types.js +1 -0
- package/dist/lib/guild-config.d.ts +13 -0
- package/dist/lib/guild-config.js +19 -0
- package/dist/lib/npmrc.js +6 -2
- package/dist/lib/session-events.d.ts +32 -16
- package/dist/lib/session-events.js +22 -0
- package/dist/lib/session-polling.d.ts +4 -3
- package/dist/lib/session-polling.js +75 -17
- package/dist/lib/session-resume.js +4 -1
- package/dist/lib/stdin.d.ts +4 -0
- package/dist/lib/stdin.js +23 -0
- package/dist/lib/validate-input-schema.d.ts +19 -0
- package/dist/lib/validate-input-schema.js +208 -0
- package/dist/lib/workspace-helpers.d.ts +20 -0
- package/dist/lib/workspace-helpers.js +49 -0
- package/dist/mcp/tools.js +8 -52
- package/docs/skills/agent-dev.md +191 -128
- package/package.json +2 -1
package/dist/lib/npmrc.js
CHANGED
|
@@ -8,7 +8,8 @@ function getRegistryUrl() {
|
|
|
8
8
|
const url = getGuildcoreUrl();
|
|
9
9
|
return url.replace('/api', '/npm') + (url.endsWith('/') ? '' : '/');
|
|
10
10
|
}
|
|
11
|
-
const SCOPES = ['@guildai
|
|
11
|
+
const SCOPES = ['@guildai-services', '@guildai-agents'];
|
|
12
|
+
const LEGACY_SCOPES = ['@guildai'];
|
|
12
13
|
export async function configureNpmrc() {
|
|
13
14
|
const registryUrl = getRegistryUrl();
|
|
14
15
|
const scope = registryUrl.split(':').slice(1).join(':');
|
|
@@ -35,7 +36,10 @@ export async function configureNpmrc() {
|
|
|
35
36
|
export async function cleanupNpmrc() {
|
|
36
37
|
const registryUrl = getRegistryUrl();
|
|
37
38
|
const scope = registryUrl.split(':').slice(1).join(':');
|
|
38
|
-
const keys = [
|
|
39
|
+
const keys = [
|
|
40
|
+
...[...SCOPES, ...LEGACY_SCOPES].map((s) => `${s}:registry`),
|
|
41
|
+
`${scope}:_authToken`,
|
|
42
|
+
];
|
|
39
43
|
for (const key of keys) {
|
|
40
44
|
try {
|
|
41
45
|
await execa('npm', [
|
|
@@ -19,10 +19,12 @@ export interface AgentRef {
|
|
|
19
19
|
}
|
|
20
20
|
export interface SessionTaskAgentItem extends BaseTaskFields {
|
|
21
21
|
agent: AgentRef;
|
|
22
|
+
parent_task_id: string | null;
|
|
22
23
|
}
|
|
23
24
|
export interface SessionTaskToolItem extends BaseTaskFields {
|
|
24
25
|
tool_name: string;
|
|
25
26
|
tool_call_id: string;
|
|
27
|
+
parent_task: SessionTaskItem | null;
|
|
26
28
|
}
|
|
27
29
|
export type SessionTaskItem = SessionTaskAgentItem | SessionTaskToolItem;
|
|
28
30
|
export interface SessionTaskAgent extends SessionTaskAgentItem {
|
|
@@ -91,26 +93,29 @@ export interface RuntimeDoneEvent extends BaseEvent {
|
|
|
91
93
|
type: 'runtime_done';
|
|
92
94
|
content: Record<string, unknown>;
|
|
93
95
|
}
|
|
96
|
+
export interface TextContent {
|
|
97
|
+
type: 'text';
|
|
98
|
+
data: string;
|
|
99
|
+
}
|
|
100
|
+
export interface ResponseStreamContent {
|
|
101
|
+
type: 'application/guild-response-stream';
|
|
102
|
+
stream_id: string;
|
|
103
|
+
sequence: number;
|
|
104
|
+
status: 'streaming' | 'done' | 'aborted';
|
|
105
|
+
text: string;
|
|
106
|
+
}
|
|
107
|
+
export type AgentNotificationMessageContent = TextContent | ResponseStreamContent;
|
|
94
108
|
export interface AgentNotificationMessageEvent extends BaseEvent {
|
|
95
109
|
type: 'agent_notification_message';
|
|
96
|
-
content:
|
|
97
|
-
type: 'text';
|
|
98
|
-
data: string;
|
|
99
|
-
};
|
|
110
|
+
content: AgentNotificationMessageContent;
|
|
100
111
|
}
|
|
101
112
|
export interface AgentNotificationProgressEvent extends BaseEvent {
|
|
102
113
|
type: 'agent_notification_progress';
|
|
103
|
-
content:
|
|
104
|
-
type: 'text';
|
|
105
|
-
data: string;
|
|
106
|
-
};
|
|
114
|
+
content: TextContent;
|
|
107
115
|
}
|
|
108
116
|
export interface AgentNotificationErrorEvent extends BaseEvent {
|
|
109
117
|
type: 'agent_notification_error';
|
|
110
|
-
content:
|
|
111
|
-
type: 'text';
|
|
112
|
-
data: string;
|
|
113
|
-
};
|
|
118
|
+
content: TextContent;
|
|
114
119
|
}
|
|
115
120
|
export interface AgentConsoleEvent extends BaseEvent {
|
|
116
121
|
type: 'agent_console';
|
|
@@ -126,10 +131,7 @@ export interface TriggerMessageEvent extends BaseEvent {
|
|
|
126
131
|
}
|
|
127
132
|
export interface SystemErrorEvent extends BaseEvent {
|
|
128
133
|
type: 'system_error';
|
|
129
|
-
content:
|
|
130
|
-
type: 'text';
|
|
131
|
-
data: string;
|
|
132
|
-
};
|
|
134
|
+
content: TextContent;
|
|
133
135
|
details?: Record<string, unknown>;
|
|
134
136
|
}
|
|
135
137
|
export interface LlmStartEvent extends BaseEvent {
|
|
@@ -169,6 +171,20 @@ export interface CredentialsRequestEvent extends BaseEvent {
|
|
|
169
171
|
export declare function isUnfulfilledAgentInstallRequest(event: SessionEvent): event is AgentInstallRequestEvent;
|
|
170
172
|
/** Check if event is an unfulfilled credentials request */
|
|
171
173
|
export declare function isUnfulfilledCredentialsRequest(event: SessionEvent): event is CredentialsRequestEvent;
|
|
174
|
+
/** Check if an agent notification message is a transient response stream draft. */
|
|
175
|
+
export declare function isResponseStreamEvent(event: SessionEvent): event is AgentNotificationMessageEvent & {
|
|
176
|
+
content: ResponseStreamContent;
|
|
177
|
+
};
|
|
178
|
+
/** Check if a response stream event carries the final generated text. */
|
|
179
|
+
export declare function isDoneResponseStreamEvent(event: SessionEvent): event is AgentNotificationMessageEvent & {
|
|
180
|
+
content: ResponseStreamContent & {
|
|
181
|
+
status: 'done';
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
/** Extract display text from notification content. */
|
|
185
|
+
export declare function getAgentNotificationText(event: AgentNotificationMessageEvent): string;
|
|
186
|
+
/** Check if an event belongs to the root task or has no task context. */
|
|
187
|
+
export declare function isRootTaskEvent(event: SessionEvent): boolean;
|
|
172
188
|
export interface InterruptedEvent extends BaseEvent {
|
|
173
189
|
type: 'interrupted';
|
|
174
190
|
interrupted_at: string;
|
|
@@ -87,4 +87,26 @@ export function isUnfulfilledAgentInstallRequest(event) {
|
|
|
87
87
|
export function isUnfulfilledCredentialsRequest(event) {
|
|
88
88
|
return event.type === 'credentials_request' && !event.is_fulfilled;
|
|
89
89
|
}
|
|
90
|
+
/** Check if an agent notification message is a transient response stream draft. */
|
|
91
|
+
export function isResponseStreamEvent(event) {
|
|
92
|
+
return (event.type === 'agent_notification_message' &&
|
|
93
|
+
event.content.type === 'application/guild-response-stream');
|
|
94
|
+
}
|
|
95
|
+
/** Check if a response stream event carries the final generated text. */
|
|
96
|
+
export function isDoneResponseStreamEvent(event) {
|
|
97
|
+
return isResponseStreamEvent(event) && event.content.status === 'done';
|
|
98
|
+
}
|
|
99
|
+
/** Extract display text from notification content. */
|
|
100
|
+
export function getAgentNotificationText(event) {
|
|
101
|
+
return event.content.type === 'text' ? event.content.data : event.content.text;
|
|
102
|
+
}
|
|
103
|
+
/** Check if an event belongs to the root task or has no task context. */
|
|
104
|
+
export function isRootTaskEvent(event) {
|
|
105
|
+
const task = event.task;
|
|
106
|
+
if (!task)
|
|
107
|
+
return true;
|
|
108
|
+
if ('parent_task_id' in task)
|
|
109
|
+
return task.parent_task_id === null;
|
|
110
|
+
return task.parent_task === null;
|
|
111
|
+
}
|
|
90
112
|
//# sourceMappingURL=session-events.js.map
|
|
@@ -11,9 +11,10 @@ export interface PollResult {
|
|
|
11
11
|
* and one-shot agents (which may only emit runtime_done with output content).
|
|
12
12
|
*
|
|
13
13
|
* Priority:
|
|
14
|
-
* 1. agent_notification_message — preferred, immediate return
|
|
15
|
-
* 2.
|
|
16
|
-
* 3.
|
|
14
|
+
* 1. root agent_notification_message — preferred, immediate return
|
|
15
|
+
* 2. root runtime_error — fail fast
|
|
16
|
+
* 3. root runtime_done content — fallback for one-shot agents
|
|
17
|
+
* 4. root done response-stream after root runtime_done — streaming fallback
|
|
17
18
|
*/
|
|
18
19
|
export declare function pollForResponse(client: GuildAPIClient, sessionId: string, afterEventId: string | undefined, maxWaitTime?: number): Promise<PollResult>;
|
|
19
20
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { debug, isDebugMode } from './errors.js';
|
|
4
4
|
import { shouldShowEvent } from './event-filter.js';
|
|
5
5
|
import { fetchEvents } from './session-events-fetch.js';
|
|
6
|
+
import { getAgentNotificationText, isDoneResponseStreamEvent, isResponseStreamEvent, isRootTaskEvent, } from './session-events.js';
|
|
6
7
|
/**
|
|
7
8
|
* Poll for agent response using from_id cursor.
|
|
8
9
|
*
|
|
@@ -10,28 +11,51 @@ import { fetchEvents } from './session-events-fetch.js';
|
|
|
10
11
|
* and one-shot agents (which may only emit runtime_done with output content).
|
|
11
12
|
*
|
|
12
13
|
* Priority:
|
|
13
|
-
* 1. agent_notification_message — preferred, immediate return
|
|
14
|
-
* 2.
|
|
15
|
-
* 3.
|
|
14
|
+
* 1. root agent_notification_message — preferred, immediate return
|
|
15
|
+
* 2. root runtime_error — fail fast
|
|
16
|
+
* 3. root runtime_done content — fallback for one-shot agents
|
|
17
|
+
* 4. root done response-stream after root runtime_done — streaming fallback
|
|
16
18
|
*/
|
|
17
19
|
export async function pollForResponse(client, sessionId, afterEventId, maxWaitTime = 60000) {
|
|
18
20
|
const startTime = Date.now();
|
|
19
21
|
let fromId = afterEventId;
|
|
22
|
+
let responseStreamDone = null;
|
|
20
23
|
while (Date.now() - startTime < maxWaitTime) {
|
|
21
24
|
const events = await fetchEvents(client, sessionId, { fromId });
|
|
22
25
|
let lastAgentRuntimeDone = null;
|
|
26
|
+
let rootRuntimeDone = false;
|
|
23
27
|
for (const event of events) {
|
|
24
28
|
debug(`pollForResponse event: ${event.type}`);
|
|
25
29
|
if (event.type === 'agent_notification_message') {
|
|
26
|
-
|
|
30
|
+
if (isResponseStreamEvent(event)) {
|
|
31
|
+
if (isRootTaskEvent(event) && isDoneResponseStreamEvent(event)) {
|
|
32
|
+
responseStreamDone = {
|
|
33
|
+
response: getAgentNotificationText(event),
|
|
34
|
+
lastEventId: event.id,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!isRootTaskEvent(event))
|
|
40
|
+
continue;
|
|
41
|
+
return {
|
|
42
|
+
response: getAgentNotificationText(event),
|
|
43
|
+
lastEventId: event.id,
|
|
44
|
+
};
|
|
27
45
|
}
|
|
28
46
|
if (event.type === 'runtime_done' &&
|
|
29
|
-
event.content !== undefined &&
|
|
30
47
|
event.task &&
|
|
31
|
-
'agent' in event.task
|
|
32
|
-
|
|
48
|
+
'agent' in event.task &&
|
|
49
|
+
isRootTaskEvent(event)) {
|
|
50
|
+
rootRuntimeDone = true;
|
|
51
|
+
if (event.content !== undefined) {
|
|
52
|
+
lastAgentRuntimeDone = JSON.stringify(event.content);
|
|
53
|
+
}
|
|
33
54
|
}
|
|
34
|
-
if (event.type === 'runtime_error' &&
|
|
55
|
+
if (event.type === 'runtime_error' &&
|
|
56
|
+
event.task &&
|
|
57
|
+
'agent' in event.task &&
|
|
58
|
+
isRootTaskEvent(event)) {
|
|
35
59
|
return {
|
|
36
60
|
response: JSON.stringify({ error: event.content }),
|
|
37
61
|
lastEventId: event.id,
|
|
@@ -47,9 +71,14 @@ export async function pollForResponse(client, sessionId, afterEventId, maxWaitTi
|
|
|
47
71
|
if (lastAgentRuntimeDone !== null) {
|
|
48
72
|
return { response: lastAgentRuntimeDone, lastEventId: fromId };
|
|
49
73
|
}
|
|
74
|
+
if (rootRuntimeDone && responseStreamDone !== null) {
|
|
75
|
+
return { response: responseStreamDone.response, lastEventId: fromId };
|
|
76
|
+
}
|
|
50
77
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
51
78
|
}
|
|
52
|
-
return
|
|
79
|
+
return responseStreamDone
|
|
80
|
+
? { response: responseStreamDone.response, lastEventId: fromId }
|
|
81
|
+
: { response: null, lastEventId: fromId };
|
|
53
82
|
}
|
|
54
83
|
/**
|
|
55
84
|
* Poll for agent response while streaming matching events to stdout as JSONL.
|
|
@@ -61,25 +90,49 @@ export async function pollForResponse(client, sessionId, afterEventId, maxWaitTi
|
|
|
61
90
|
export async function pollForResponseWithEvents(client, sessionId, eventFilter, afterEventId, maxWaitTime = 60000) {
|
|
62
91
|
const startTime = Date.now();
|
|
63
92
|
let fromId = afterEventId;
|
|
93
|
+
let responseStreamDone = null;
|
|
64
94
|
while (Date.now() - startTime < maxWaitTime) {
|
|
65
95
|
const events = await fetchEvents(client, sessionId, { fromId });
|
|
66
96
|
let lastAgentRuntimeDone = null;
|
|
97
|
+
let rootRuntimeDone = false;
|
|
67
98
|
for (const event of events) {
|
|
68
99
|
debug(`pollForResponseWithEvents event: ${event.type}`);
|
|
69
|
-
// Stream matching events to stdout as JSONL
|
|
70
|
-
|
|
100
|
+
// Stream matching events to stdout as JSONL. Response-stream events are
|
|
101
|
+
// transient drafts, so keep automation output stable and wait for the
|
|
102
|
+
// final agent message or terminal fallback.
|
|
103
|
+
if (shouldShowEvent(event.type, eventFilter) && !isResponseStreamEvent(event)) {
|
|
71
104
|
process.stdout.write(JSON.stringify(event) + '\n');
|
|
72
105
|
}
|
|
73
106
|
if (event.type === 'agent_notification_message') {
|
|
74
|
-
|
|
107
|
+
if (isResponseStreamEvent(event)) {
|
|
108
|
+
if (isRootTaskEvent(event) && isDoneResponseStreamEvent(event)) {
|
|
109
|
+
responseStreamDone = {
|
|
110
|
+
response: getAgentNotificationText(event),
|
|
111
|
+
lastEventId: event.id,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!isRootTaskEvent(event))
|
|
117
|
+
continue;
|
|
118
|
+
return {
|
|
119
|
+
response: getAgentNotificationText(event),
|
|
120
|
+
lastEventId: event.id,
|
|
121
|
+
};
|
|
75
122
|
}
|
|
76
123
|
if (event.type === 'runtime_done' &&
|
|
77
|
-
event.content !== undefined &&
|
|
78
124
|
event.task &&
|
|
79
|
-
'agent' in event.task
|
|
80
|
-
|
|
125
|
+
'agent' in event.task &&
|
|
126
|
+
isRootTaskEvent(event)) {
|
|
127
|
+
rootRuntimeDone = true;
|
|
128
|
+
if (event.content !== undefined) {
|
|
129
|
+
lastAgentRuntimeDone = JSON.stringify(event.content);
|
|
130
|
+
}
|
|
81
131
|
}
|
|
82
|
-
if (event.type === 'runtime_error' &&
|
|
132
|
+
if (event.type === 'runtime_error' &&
|
|
133
|
+
event.task &&
|
|
134
|
+
'agent' in event.task &&
|
|
135
|
+
isRootTaskEvent(event)) {
|
|
83
136
|
return {
|
|
84
137
|
response: JSON.stringify({ error: event.content }),
|
|
85
138
|
lastEventId: event.id,
|
|
@@ -95,8 +148,13 @@ export async function pollForResponseWithEvents(client, sessionId, eventFilter,
|
|
|
95
148
|
if (lastAgentRuntimeDone !== null) {
|
|
96
149
|
return { response: lastAgentRuntimeDone, lastEventId: fromId };
|
|
97
150
|
}
|
|
151
|
+
if (rootRuntimeDone && responseStreamDone !== null) {
|
|
152
|
+
return { response: responseStreamDone.response, lastEventId: fromId };
|
|
153
|
+
}
|
|
98
154
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
99
155
|
}
|
|
100
|
-
return
|
|
156
|
+
return responseStreamDone
|
|
157
|
+
? { response: responseStreamDone.response, lastEventId: fromId }
|
|
158
|
+
: { response: null, lastEventId: fromId };
|
|
101
159
|
}
|
|
102
160
|
//# sourceMappingURL=session-polling.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { marked } from 'marked';
|
|
9
9
|
import { markedTerminal } from 'marked-terminal';
|
|
10
|
+
import { getAgentNotificationText, isResponseStreamEvent, } from './session-events.js';
|
|
10
11
|
import { fetchEvents } from './session-events-fetch.js';
|
|
11
12
|
import { brand, code as codeColor } from './colors.js';
|
|
12
13
|
// Configure marked for terminal rendering (same config as chat.tsx)
|
|
@@ -64,7 +65,9 @@ export function eventsToDisplayMessages(events) {
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
else if (event.type === 'agent_notification_message') {
|
|
67
|
-
|
|
68
|
+
if (isResponseStreamEvent(event))
|
|
69
|
+
continue;
|
|
70
|
+
const text = getAgentNotificationText(event);
|
|
68
71
|
if (text.trim()) {
|
|
69
72
|
const rendered = renderMarkdown(text);
|
|
70
73
|
const messageContent = `${chalk.green('●')} ${chalk.bold('assistant')}\n${rendered.trim()}`;
|
package/dist/lib/stdin.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export declare function isInteractive(): boolean;
|
|
|
8
8
|
* accidentally pipe input without --mode get clear guidance instead of a hang.
|
|
9
9
|
*/
|
|
10
10
|
export declare function ensureInteractiveStdin(command: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Read raw text from stdin with a timeout.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readStdinAsText(): Promise<string>;
|
|
11
15
|
/**
|
|
12
16
|
* Read JSON from stdin with a timeout.
|
|
13
17
|
*/
|
package/dist/lib/stdin.js
CHANGED
|
@@ -24,6 +24,29 @@ export function ensureInteractiveStdin(command) {
|
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Read raw text from stdin with a timeout.
|
|
29
|
+
*/
|
|
30
|
+
export function readStdinAsText() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let input = '';
|
|
33
|
+
const timeout = setTimeout(() => {
|
|
34
|
+
reject(new Error('Timeout waiting for stdin'));
|
|
35
|
+
}, 5000);
|
|
36
|
+
process.stdin.setEncoding('utf8');
|
|
37
|
+
process.stdin.on('data', (chunk) => {
|
|
38
|
+
input += chunk;
|
|
39
|
+
});
|
|
40
|
+
process.stdin.on('end', () => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
resolve(input);
|
|
43
|
+
});
|
|
44
|
+
process.stdin.on('error', (error) => {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
reject(error);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
27
50
|
/**
|
|
28
51
|
* Read JSON from stdin with a timeout.
|
|
29
52
|
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface ValidationSuccess {
|
|
2
|
+
valid: true;
|
|
3
|
+
}
|
|
4
|
+
interface ValidationFailure {
|
|
5
|
+
valid: false;
|
|
6
|
+
errors: Array<{
|
|
7
|
+
message: string;
|
|
8
|
+
path?: string[];
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
interface ValidationSkipped {
|
|
12
|
+
valid: true;
|
|
13
|
+
skipped: true;
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
export type ValidationResult = ValidationSuccess | ValidationFailure | ValidationSkipped;
|
|
17
|
+
export declare function validateInputSchema(agentDir: string, inputs: unknown[]): Promise<ValidationResult>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=validate-input-schema.d.ts.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Copyright 2026 Guild.ai
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { debug } from './errors.js';
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
function resolveSourceEntryPoint(agentDir, packageJson) {
|
|
11
|
+
const exports = packageJson.exports;
|
|
12
|
+
if (exports?.['.']) {
|
|
13
|
+
const distPath = exports['.'];
|
|
14
|
+
const sourcePath = distPath
|
|
15
|
+
.replace(/^\.\/dist\//, '')
|
|
16
|
+
.replace(/\.compiled\.js$/, '.ts')
|
|
17
|
+
.replace(/\.js$/, '.ts');
|
|
18
|
+
return path.join(agentDir, sourcePath);
|
|
19
|
+
}
|
|
20
|
+
return path.join(agentDir, 'agent.ts');
|
|
21
|
+
}
|
|
22
|
+
async function fileExists(filePath) {
|
|
23
|
+
return fs
|
|
24
|
+
.access(filePath)
|
|
25
|
+
.then(() => true)
|
|
26
|
+
.catch(() => false);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract a single `const <name> = z.<call>(...)` declaration from source,
|
|
30
|
+
* starting at the given regex match. Uses balanced parenthesis matching
|
|
31
|
+
* with string literal awareness.
|
|
32
|
+
*/
|
|
33
|
+
function extractZodDeclaration(source, match) {
|
|
34
|
+
const startIndex = match.index;
|
|
35
|
+
const afterEquals = match.index + match[0].length;
|
|
36
|
+
let depth = 0;
|
|
37
|
+
let foundOpen = false;
|
|
38
|
+
let endIndex = afterEquals;
|
|
39
|
+
let inString = null;
|
|
40
|
+
for (let charIndex = afterEquals; charIndex < source.length; charIndex++) {
|
|
41
|
+
const char = source[charIndex];
|
|
42
|
+
if (inString) {
|
|
43
|
+
if (char === '\\') {
|
|
44
|
+
charIndex++;
|
|
45
|
+
}
|
|
46
|
+
else if (char === inString) {
|
|
47
|
+
inString = null;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
52
|
+
inString = char;
|
|
53
|
+
}
|
|
54
|
+
else if (char === '(') {
|
|
55
|
+
depth++;
|
|
56
|
+
foundOpen = true;
|
|
57
|
+
}
|
|
58
|
+
else if (char === ')') {
|
|
59
|
+
depth--;
|
|
60
|
+
if (foundOpen && depth === 0) {
|
|
61
|
+
endIndex = charIndex + 1;
|
|
62
|
+
if (source[endIndex] === ';')
|
|
63
|
+
endIndex++;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!foundOpen)
|
|
69
|
+
return null;
|
|
70
|
+
return source.substring(startIndex, endIndex);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract inputSchema and any Zod schema dependencies from agent source.
|
|
74
|
+
* Finds all `const <name> = z.<call>(...)` declarations that appear before
|
|
75
|
+
* inputSchema (which may be referenced inside it), plus inputSchema itself.
|
|
76
|
+
* Returns them in source order so dependencies are defined before use.
|
|
77
|
+
*/
|
|
78
|
+
function extractInputSchema(source) {
|
|
79
|
+
const inputPattern = /const\s+inputSchema\s*=\s*/;
|
|
80
|
+
const inputMatch = inputPattern.exec(source);
|
|
81
|
+
if (!inputMatch)
|
|
82
|
+
return null;
|
|
83
|
+
const inputBlock = extractZodDeclaration(source, inputMatch);
|
|
84
|
+
if (!inputBlock)
|
|
85
|
+
return null;
|
|
86
|
+
const blocks = [];
|
|
87
|
+
const zodDeclPattern = /const\s+\w+\s*=\s*z\s*\.\s*/g;
|
|
88
|
+
let declMatch;
|
|
89
|
+
while ((declMatch = zodDeclPattern.exec(source)) !== null) {
|
|
90
|
+
if (declMatch.index >= inputMatch.index)
|
|
91
|
+
break;
|
|
92
|
+
const block = extractZodDeclaration(source, declMatch);
|
|
93
|
+
if (block)
|
|
94
|
+
blocks.push(block);
|
|
95
|
+
}
|
|
96
|
+
blocks.push(inputBlock);
|
|
97
|
+
return blocks.join('\n');
|
|
98
|
+
}
|
|
99
|
+
export async function validateInputSchema(agentDir, inputs) {
|
|
100
|
+
const nodeModulesPath = path.join(agentDir, 'node_modules');
|
|
101
|
+
if (!(await fileExists(nodeModulesPath))) {
|
|
102
|
+
return {
|
|
103
|
+
valid: true,
|
|
104
|
+
skipped: true,
|
|
105
|
+
reason: 'run npm install for pre-build input validation',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const pkgJsonPath = path.join(agentDir, 'package.json');
|
|
109
|
+
if (!(await fileExists(pkgJsonPath))) {
|
|
110
|
+
return {
|
|
111
|
+
valid: true,
|
|
112
|
+
skipped: true,
|
|
113
|
+
reason: 'no package.json found',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf-8'));
|
|
117
|
+
let entryPoint = resolveSourceEntryPoint(agentDir, pkgJson);
|
|
118
|
+
if (!(await fileExists(entryPoint))) {
|
|
119
|
+
const fallback = path.join(agentDir, 'agent.ts');
|
|
120
|
+
if (await fileExists(fallback)) {
|
|
121
|
+
entryPoint = fallback;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
return {
|
|
125
|
+
valid: true,
|
|
126
|
+
skipped: true,
|
|
127
|
+
reason: `could not find agent source file (tried ${path.basename(entryPoint)})`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const agentSource = await fs.readFile(entryPoint, 'utf-8');
|
|
132
|
+
const schemaBlock = extractInputSchema(agentSource);
|
|
133
|
+
if (!schemaBlock) {
|
|
134
|
+
return {
|
|
135
|
+
valid: true,
|
|
136
|
+
skipped: true,
|
|
137
|
+
reason: 'could not find inputSchema definition in agent source',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'guild-validate-'));
|
|
141
|
+
try {
|
|
142
|
+
const extractorSource = [
|
|
143
|
+
`import { z } from "zod";`,
|
|
144
|
+
schemaBlock,
|
|
145
|
+
`const inputs = JSON.parse(process.argv[2]);`,
|
|
146
|
+
`const errors = [];`,
|
|
147
|
+
`for (let i = 0; i < inputs.length; i++) {`,
|
|
148
|
+
` try {`,
|
|
149
|
+
` inputSchema.parse(inputs[i]);`,
|
|
150
|
+
` } catch (e) {`,
|
|
151
|
+
` const issues = e.issues || [{ message: e.message }];`,
|
|
152
|
+
` errors.push({ index: i, issues: issues.map(iss => ({ message: iss.message, path: iss.path })) });`,
|
|
153
|
+
` }`,
|
|
154
|
+
`}`,
|
|
155
|
+
`if (errors.length > 0) {`,
|
|
156
|
+
` console.log(JSON.stringify({ valid: false, errors }));`,
|
|
157
|
+
`} else {`,
|
|
158
|
+
` console.log(JSON.stringify({ valid: true }));`,
|
|
159
|
+
`}`,
|
|
160
|
+
].join('\n');
|
|
161
|
+
const extractorPath = path.join(tmpDir, 'validate.mjs');
|
|
162
|
+
await fs.writeFile(extractorPath, extractorSource);
|
|
163
|
+
const { build } = await import('esbuild');
|
|
164
|
+
const bundlePath = path.join(tmpDir, 'validate-bundle.mjs');
|
|
165
|
+
await build({
|
|
166
|
+
entryPoints: [extractorPath],
|
|
167
|
+
bundle: true,
|
|
168
|
+
format: 'esm',
|
|
169
|
+
target: 'esnext',
|
|
170
|
+
platform: 'node',
|
|
171
|
+
outfile: bundlePath,
|
|
172
|
+
logLevel: 'silent',
|
|
173
|
+
nodePaths: [path.join(agentDir, 'node_modules')],
|
|
174
|
+
});
|
|
175
|
+
const inputArg = JSON.stringify(inputs);
|
|
176
|
+
const { stdout } = await execFileAsync('node', [bundlePath, inputArg], {
|
|
177
|
+
cwd: agentDir,
|
|
178
|
+
timeout: 10000,
|
|
179
|
+
});
|
|
180
|
+
const result = JSON.parse(stdout.trim());
|
|
181
|
+
if (result.valid) {
|
|
182
|
+
return { valid: true };
|
|
183
|
+
}
|
|
184
|
+
const flatErrors = [];
|
|
185
|
+
for (const entry of result.errors) {
|
|
186
|
+
for (const issue of entry.issues) {
|
|
187
|
+
const prefix = inputs.length > 1 ? `Line ${entry.index + 1}: ` : '';
|
|
188
|
+
flatErrors.push({
|
|
189
|
+
message: `${prefix}${issue.message}`,
|
|
190
|
+
path: issue.path,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { valid: false, errors: flatErrors };
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
debug('Local input validation failed: %s', error);
|
|
198
|
+
return {
|
|
199
|
+
valid: true,
|
|
200
|
+
skipped: true,
|
|
201
|
+
reason: 'local validation encountered an error (server-side validation will still run)',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=validate-input-schema.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace resolution helper for workspace subcommands.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a workspace identifier (UUID or human-readable name) to a UUID.
|
|
5
|
+
* Used by `guild workspace agent add`, `list`, and `remove`.
|
|
6
|
+
*/
|
|
7
|
+
import { GuildAPIClient } from './api-client.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a workspace ID or name to a workspace UUID.
|
|
10
|
+
*
|
|
11
|
+
* UUID inputs pass through unchanged. Name inputs are resolved by searching
|
|
12
|
+
* the user's workspaces via `GET /me/workspaces?filter=all&search=...` first,
|
|
13
|
+
* with a `fetchAll` fallback for IDs and names not matched by the search.
|
|
14
|
+
*
|
|
15
|
+
* Throws an `Error` with message `Workspace "<idOrName>" not found` if
|
|
16
|
+
* nothing resolves — callers should catch this and call `output.error` +
|
|
17
|
+
* `process.exit(1)`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveWorkspaceId(client: GuildAPIClient, idOrName: string): Promise<string>;
|
|
20
|
+
//# sourceMappingURL=workspace-helpers.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright 2026 Guild.ai
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
/**
|
|
5
|
+
* Match a workspace against a search argument (case-insensitive name, full_name, or exact ID).
|
|
6
|
+
*/
|
|
7
|
+
function matchesWorkspaceArg(w, arg) {
|
|
8
|
+
return (w.id === arg ||
|
|
9
|
+
w.name === arg ||
|
|
10
|
+
w.name.toLowerCase() === arg.toLowerCase() ||
|
|
11
|
+
w.full_name === arg ||
|
|
12
|
+
w.full_name?.toLowerCase() === arg.toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a workspace ID or name to a workspace UUID.
|
|
16
|
+
*
|
|
17
|
+
* UUID inputs pass through unchanged. Name inputs are resolved by searching
|
|
18
|
+
* the user's workspaces via `GET /me/workspaces?filter=all&search=...` first,
|
|
19
|
+
* with a `fetchAll` fallback for IDs and names not matched by the search.
|
|
20
|
+
*
|
|
21
|
+
* Throws an `Error` with message `Workspace "<idOrName>" not found` if
|
|
22
|
+
* nothing resolves — callers should catch this and call `output.error` +
|
|
23
|
+
* `process.exit(1)`.
|
|
24
|
+
*/
|
|
25
|
+
export async function resolveWorkspaceId(client, idOrName) {
|
|
26
|
+
// Short-circuit UUID inputs — pass through unchanged
|
|
27
|
+
if (UUID_RE.test(idOrName)) {
|
|
28
|
+
return idOrName;
|
|
29
|
+
}
|
|
30
|
+
// Use server-side search first. Extract just the name part for full_name
|
|
31
|
+
// inputs (e.g. "owner/workspace-name") since the backend searches by name.
|
|
32
|
+
const searchTerm = idOrName.includes('/')
|
|
33
|
+
? (idOrName.split('/').pop() ?? idOrName)
|
|
34
|
+
: idOrName;
|
|
35
|
+
const searchResults = await client.get(`/me/workspaces?filter=all&search=${encodeURIComponent(searchTerm)}&limit=100`);
|
|
36
|
+
const directMatch = searchResults.items.find((w) => matchesWorkspaceArg(w, idOrName));
|
|
37
|
+
if (directMatch) {
|
|
38
|
+
return directMatch.id;
|
|
39
|
+
}
|
|
40
|
+
// Fall back to fetching all workspaces (handles IDs and names not returned
|
|
41
|
+
// by search, e.g. when the backend's ILIKE search doesn't match full_name).
|
|
42
|
+
const allWorkspaces = await client.fetchAll('/me/workspaces?filter=all');
|
|
43
|
+
const fallbackMatch = allWorkspaces.find((w) => matchesWorkspaceArg(w, idOrName));
|
|
44
|
+
if (fallbackMatch) {
|
|
45
|
+
return fallbackMatch.id;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Workspace "${idOrName}" not found`);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=workspace-helpers.js.map
|