@opencoven/coven-code 0.0.1 → 0.0.3
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 +2 -1
- package/docs/CLI.md +65 -1
- package/docs/DEMO.md +453 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/README.md +1 -0
- package/package.json +7 -6
- package/src/agent/{local.mjs → fixture.mjs} +1 -1
- package/src/cli/execute.mjs +6 -4
- package/src/cli/interactive-core.mjs +5 -279
- package/src/cli/interactive-io.mjs +101 -0
- package/src/cli/interactive-slash.mjs +184 -0
- package/src/cli/repl.mjs +1 -2
- package/src/cli/slash-commands.mjs +20 -2
- package/src/cli/tui-actions.mjs +72 -0
- package/src/cli/tui-blessed.mjs +198 -0
- package/src/cli/tui-keys.mjs +80 -0
- package/src/cli/tui-lane.mjs +73 -0
- package/src/cli/tui-render.mjs +169 -0
- package/src/cli/tui-submit.mjs +82 -0
- package/src/cli/tui.mjs +30 -613
- package/src/commands/permissions-eval.mjs +122 -0
- package/src/commands/permissions-rules.mjs +53 -0
- package/src/commands/permissions-text.mjs +112 -0
- package/src/commands/permissions.mjs +15 -281
- package/src/commands/usage.mjs +1 -1
- package/src/constants.mjs +7 -1
- package/src/mcp/local.mjs +55 -0
- package/src/mcp/parsers.mjs +46 -0
- package/src/mcp/probe.mjs +12 -351
- package/src/mcp/remote-oauth.mjs +55 -0
- package/src/mcp/remote-session.mjs +54 -0
- package/src/mcp/remote-sse.mjs +82 -0
- package/src/mcp/remote.mjs +74 -0
- package/src/plugins/api.mjs +187 -0
- package/src/plugins/configuration.mjs +124 -0
- package/src/plugins/discover.mjs +8 -804
- package/src/plugins/helpers.mjs +187 -0
- package/src/plugins/subsystems.mjs +198 -0
- package/src/plugins/validators.mjs +142 -0
- package/src/sdk-execute.mjs +82 -0
- package/src/sdk-settings.mjs +88 -0
- package/src/sdk.mjs +13 -164
- package/src/tools/builtin/oracle.mjs +2 -2
- package/src/tools/builtin/runtime-content.mjs +31 -0
- package/src/tools/builtin/runtime-decisions.mjs +115 -0
- package/src/tools/builtin/runtime.mjs +18 -148
- package/src/tools/builtin/task.mjs +2 -2
package/src/sdk.mjs
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import os from 'node:os';
|
|
6
3
|
import { createInterface } from 'node:readline';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
4
|
import { normalizeThreadVisibility, readThread, writeThread } from './threads/store.mjs';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
import {
|
|
6
|
+
closeStdin,
|
|
7
|
+
executeArgs,
|
|
8
|
+
isAsyncIterable,
|
|
9
|
+
waitForExit,
|
|
10
|
+
writeStreamInput,
|
|
11
|
+
} from './sdk-execute.mjs';
|
|
12
|
+
import {
|
|
13
|
+
prepareRunSettings,
|
|
14
|
+
resolveCovenCodeCommand,
|
|
15
|
+
withEnv,
|
|
16
|
+
writeDebugLog,
|
|
17
|
+
} from './sdk-settings.mjs';
|
|
14
18
|
|
|
15
19
|
export const threads = {
|
|
16
20
|
async new(options = {}) {
|
|
@@ -128,87 +132,11 @@ export function createPermission(tool, action, options = {}) {
|
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
function executeArgs(prompt, options, isStreamInput) {
|
|
132
|
-
const args = ['--execute'];
|
|
133
|
-
if (!isStreamInput) args.push(String(prompt));
|
|
134
|
-
args.push(options.thinking ? '--stream-json-thinking' : '--stream-json');
|
|
135
|
-
if (isStreamInput) args.push('--stream-json-input');
|
|
136
|
-
if (options.dangerouslyAllowAll) args.push('--dangerously-allow-all');
|
|
137
|
-
if (options.archive) args.push('--archive');
|
|
138
|
-
if (options.mode) args.push('--mode', options.mode);
|
|
139
|
-
if (options.reasoningEffort) args.push('--reasoning-effort', options.reasoningEffort);
|
|
140
|
-
const visibility = normalizeSdkThreadVisibility(options.visibility) ?? (options.continue ? undefined : 'workspace');
|
|
141
|
-
if (visibility) args.push('--visibility', visibility);
|
|
142
|
-
if (options.settingsFile) args.push('--settings-file', options.settingsFile);
|
|
143
|
-
if (options.continue) args.push('--continue', ...(typeof options.continue === 'string' ? [options.continue] : []));
|
|
144
|
-
if (options.toolbox) args.push('--toolbox', options.toolbox);
|
|
145
|
-
if (options.skills) args.push('--skills', options.skills);
|
|
146
|
-
if (options.mcpConfig) args.push('--mcp-config', typeof options.mcpConfig === 'string' ? options.mcpConfig : JSON.stringify(options.mcpConfig));
|
|
147
|
-
for (const label of options.labels ?? []) args.push('--label', label);
|
|
148
|
-
return args;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
135
|
function normalizeSdkThreadVisibility(visibility) {
|
|
152
136
|
if (visibility === 'team') return 'workspace';
|
|
153
137
|
return normalizeThreadVisibility(visibility);
|
|
154
138
|
}
|
|
155
139
|
|
|
156
|
-
async function writeStreamInput(child, prompt, signal) {
|
|
157
|
-
const iterator = prompt[Symbol.asyncIterator]();
|
|
158
|
-
try {
|
|
159
|
-
while (true) {
|
|
160
|
-
const { value: message, done } = await nextWithAbort(iterator, signal);
|
|
161
|
-
if (done) break;
|
|
162
|
-
if (child.stdin.destroyed) break;
|
|
163
|
-
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
164
|
-
}
|
|
165
|
-
child.stdin.end();
|
|
166
|
-
} catch (error) {
|
|
167
|
-
if (!isAbortError(error)) throw error;
|
|
168
|
-
child.stdin.destroy();
|
|
169
|
-
try {
|
|
170
|
-
iterator.return?.().catch?.(() => {});
|
|
171
|
-
} catch {
|
|
172
|
-
// Best-effort generator cleanup; abort should not wait on a slow prompt source.
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function nextWithAbort(iterator, signal) {
|
|
178
|
-
if (!signal) return iterator.next();
|
|
179
|
-
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
180
|
-
return new Promise((resolve, reject) => {
|
|
181
|
-
const onAbort = () => reject(abortReason(signal));
|
|
182
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
183
|
-
iterator.next().then(resolve, reject).finally(() => {
|
|
184
|
-
signal.removeEventListener('abort', onAbort);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function abortReason(signal) {
|
|
190
|
-
return signal.reason instanceof Error ? signal.reason : new Error('aborted');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function isAbortError(error) {
|
|
194
|
-
return error?.name === 'AbortError' || error?.code === 'ABORT_ERR' || /abort/i.test(error?.message ?? '');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function closeStdin(child) {
|
|
198
|
-
child.stdin.end();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function waitForExit(child) {
|
|
202
|
-
return new Promise((resolve, reject) => {
|
|
203
|
-
child.on('error', reject);
|
|
204
|
-
child.on('close', (code) => resolve(code ?? 0));
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function isAsyncIterable(value) {
|
|
209
|
-
return value && typeof value[Symbol.asyncIterator] === 'function';
|
|
210
|
-
}
|
|
211
|
-
|
|
212
140
|
function requireSdkThread(threadId) {
|
|
213
141
|
const thread = readThread(threadId);
|
|
214
142
|
if (!thread) throw new Error(`Unknown thread: ${threadId}`);
|
|
@@ -233,82 +161,3 @@ function threadMarkdown(thread) {
|
|
|
233
161
|
function titleCaseRole(role = '') {
|
|
234
162
|
return role ? `${role.slice(0, 1).toUpperCase()}${role.slice(1)}` : 'Message';
|
|
235
163
|
}
|
|
236
|
-
|
|
237
|
-
async function withEnv(env = {}, fn) {
|
|
238
|
-
const previous = new Map();
|
|
239
|
-
for (const [key, value] of Object.entries(env ?? {})) {
|
|
240
|
-
previous.set(key, Object.hasOwn(process.env, key) ? process.env[key] : undefined);
|
|
241
|
-
process.env[key] = value;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
return await fn();
|
|
245
|
-
} finally {
|
|
246
|
-
for (const [key, value] of previous) {
|
|
247
|
-
if (value === undefined) delete process.env[key];
|
|
248
|
-
else process.env[key] = value;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function prepareRunSettings(options = {}) {
|
|
254
|
-
if (!shouldWriteRunSettings(options)) return { settingsFile: options.settingsFile, cleanup: async () => {} };
|
|
255
|
-
const dir = await mkdtemp(path.join(os.tmpdir(), 'coven-code-sdk-settings-'));
|
|
256
|
-
const settingsFile = path.join(dir, 'settings.json');
|
|
257
|
-
const baseSettings = options.settingsFile ? await readJsonSettings(sdkOptionsPath(options.settingsFile, options.cwd)) : {};
|
|
258
|
-
const settings = { ...baseSettings };
|
|
259
|
-
if (Array.isArray(options.permissions)) settings['covenCode.permissions'] = options.permissions;
|
|
260
|
-
if (Array.isArray(options.enabledTools)) settings['covenCode.tools.enable'] = options.enabledTools;
|
|
261
|
-
if (typeof options.systemPrompt === 'string') settings['covenCode.systemPrompt'] = options.systemPrompt;
|
|
262
|
-
if (typeof options.skills === 'string') settings['covenCode.skills.path'] = options.skills;
|
|
263
|
-
await writeFile(settingsFile, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
264
|
-
return {
|
|
265
|
-
settingsFile,
|
|
266
|
-
cleanup: async () => {
|
|
267
|
-
await rm(dir, { recursive: true, force: true });
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function shouldWriteRunSettings(options = {}) {
|
|
273
|
-
return Array.isArray(options.permissions)
|
|
274
|
-
|| Array.isArray(options.enabledTools)
|
|
275
|
-
|| typeof options.systemPrompt === 'string'
|
|
276
|
-
|| typeof options.skills === 'string';
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function readJsonSettings(filePath) {
|
|
280
|
-
try {
|
|
281
|
-
return parseJsonc(await readFile(filePath, 'utf8'));
|
|
282
|
-
} catch {
|
|
283
|
-
return {};
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function writeDebugLog(options = {}, covenCodeCommand = resolveCovenCodeCommand(), args = []) {
|
|
288
|
-
if (options.logLevel !== 'debug') return;
|
|
289
|
-
const line = `level=debug cwd=${options.cwd ?? process.cwd()} argv=${[covenCodeCommand.command, ...covenCodeCommand.args, ...args].map(JSON.stringify).join(' ')}\n`;
|
|
290
|
-
process.stderr.write(line);
|
|
291
|
-
if (!options.logFile) return;
|
|
292
|
-
const logFile = sdkOptionsPath(options.logFile, options.cwd);
|
|
293
|
-
await mkdir(path.dirname(logFile), { recursive: true });
|
|
294
|
-
await appendFile(logFile, line, 'utf8');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function sdkOptionsPath(filePath, cwd = process.cwd()) {
|
|
298
|
-
if (!filePath || path.isAbsolute(filePath)) return filePath;
|
|
299
|
-
return path.resolve(cwd, filePath);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function resolveCovenCodeCommand() {
|
|
303
|
-
const cliPath = process.env.COVEN_CODE_CLI_PATH;
|
|
304
|
-
if (cliPath && existsSync(cliPath)) {
|
|
305
|
-
return isNodeScriptPath(cliPath)
|
|
306
|
-
? { command: process.execPath, args: [cliPath] }
|
|
307
|
-
: { command: cliPath, args: [] };
|
|
308
|
-
}
|
|
309
|
-
return { command: process.execPath, args: [covenCodeBin] };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function isNodeScriptPath(filePath) {
|
|
313
|
-
return filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs');
|
|
314
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fixtureAgentResponse } from '../../agent/fixture.mjs';
|
|
2
2
|
import { resolvePermissionDecision } from '../../commands/permissions.mjs';
|
|
3
3
|
import { runPluginEventHandlers } from '../../plugins/discover.mjs';
|
|
4
4
|
import { isToolDisabled } from '../toolbox.mjs';
|
|
@@ -36,7 +36,7 @@ export async function executePromptOracleToolRequest(request, parsed = {}, plugi
|
|
|
36
36
|
permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
const output = `Oracle: ${
|
|
39
|
+
const output = `Oracle: ${fixtureAgentResponse(request.flags.prompt, '')}`;
|
|
40
40
|
const resultDecision = await runPluginEventHandlers(
|
|
41
41
|
plugins.handlers['tool.result'],
|
|
42
42
|
pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function isPluginContentBlock(block) {
|
|
2
|
+
if (!block || typeof block !== 'object') return false;
|
|
3
|
+
if (block.type === 'text') return typeof block.text === 'string';
|
|
4
|
+
if (block.type === 'image') return typeof block.mimeType === 'string' && typeof block.data === 'string';
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function pluginContentBlockText(block) {
|
|
9
|
+
if (block.type === 'text') return block.text;
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizePluginToolOutput(output) {
|
|
14
|
+
if (Array.isArray(output) && output.every(isPluginContentBlock)) {
|
|
15
|
+
const text = output.map(pluginContentBlockText).filter(Boolean).join('\n').trimEnd();
|
|
16
|
+
return { raw: output, text };
|
|
17
|
+
}
|
|
18
|
+
const text = String(output ?? '').trimEnd();
|
|
19
|
+
return { raw: text, text };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizePluginToolExecuteOutput(output) {
|
|
23
|
+
if (output === undefined || typeof output === 'string') return normalizePluginToolOutput(output);
|
|
24
|
+
if (Array.isArray(output)) {
|
|
25
|
+
if (!output.every(isPluginContentBlock)) {
|
|
26
|
+
throw new Error('plugin tool result content blocks must be text or image blocks');
|
|
27
|
+
}
|
|
28
|
+
return normalizePluginToolOutput(output);
|
|
29
|
+
}
|
|
30
|
+
throw new Error('plugin tool result must be a string, content blocks, or undefined');
|
|
31
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export function validateToolCallDecision(callDecision) {
|
|
2
|
+
if (!callDecision || typeof callDecision !== 'object') {
|
|
3
|
+
throw new Error('plugin tool.call result must be an object');
|
|
4
|
+
}
|
|
5
|
+
if (
|
|
6
|
+
callDecision.action !== 'allow' &&
|
|
7
|
+
callDecision.action !== 'reject-and-continue' &&
|
|
8
|
+
callDecision.action !== 'modify' &&
|
|
9
|
+
callDecision.action !== 'synthesize' &&
|
|
10
|
+
callDecision.action !== 'error'
|
|
11
|
+
) {
|
|
12
|
+
throw new Error('plugin tool.call action must be allow, reject-and-continue, modify, synthesize, or error');
|
|
13
|
+
}
|
|
14
|
+
const allowedKeys = {
|
|
15
|
+
allow: ['action'],
|
|
16
|
+
'reject-and-continue': ['action', 'message'],
|
|
17
|
+
modify: ['action', 'input'],
|
|
18
|
+
synthesize: ['action', 'result'],
|
|
19
|
+
error: ['action', 'message'],
|
|
20
|
+
}[callDecision.action];
|
|
21
|
+
if (Object.keys(callDecision).some((key) => !allowedKeys.includes(key))) {
|
|
22
|
+
throw new Error('plugin tool.call fields must match the documented union');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyToolCallDecision(toolName, request, callDecision = { action: 'allow' }) {
|
|
27
|
+
validateToolCallDecision(callDecision);
|
|
28
|
+
if (callDecision.action === 'allow') return { request };
|
|
29
|
+
if (callDecision.action === 'reject-and-continue') {
|
|
30
|
+
if (typeof callDecision.message !== 'string') {
|
|
31
|
+
throw new Error('plugin tool.call reject-and-continue message must be a string');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
output: {
|
|
35
|
+
output: callDecision.message,
|
|
36
|
+
exitCode: 0,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (callDecision.action === 'synthesize') {
|
|
41
|
+
const result = callDecision.result && isPlainObject(callDecision.result)
|
|
42
|
+
? callDecision.result
|
|
43
|
+
: callDecision;
|
|
44
|
+
if (typeof result.output !== 'string') {
|
|
45
|
+
throw new Error('plugin tool.call synthesize result.output must be a string');
|
|
46
|
+
}
|
|
47
|
+
if (result.exitCode !== undefined && !Number.isInteger(result.exitCode)) {
|
|
48
|
+
throw new Error('plugin tool.call synthesize result.exitCode must be an integer');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
output: {
|
|
52
|
+
output: result.output.trimEnd(),
|
|
53
|
+
exitCode: result.exitCode ?? 0,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (callDecision.action === 'error') {
|
|
58
|
+
if (typeof callDecision.message !== 'string') {
|
|
59
|
+
throw new Error('plugin tool.call error message must be a string');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
output: {
|
|
63
|
+
output: callDecision.message,
|
|
64
|
+
exitCode: 1,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (callDecision.action === 'modify') {
|
|
69
|
+
if (!isPlainObject(callDecision.input)) {
|
|
70
|
+
throw new Error('plugin tool.call modify input must be an object');
|
|
71
|
+
}
|
|
72
|
+
return { request: { ...request, flags: callDecision.input } };
|
|
73
|
+
}
|
|
74
|
+
return { request };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function validateToolResultDecision(resultDecision = {}) {
|
|
78
|
+
if (!resultDecision || typeof resultDecision !== 'object' || !Object.hasOwn(resultDecision, 'status')) return;
|
|
79
|
+
if (resultDecision.status !== 'done' && resultDecision.status !== 'error' && resultDecision.status !== 'cancelled') {
|
|
80
|
+
throw new Error('plugin tool.result status must be done, error, or cancelled');
|
|
81
|
+
}
|
|
82
|
+
const allowedKeys = resultDecision.status === 'done' ? ['output', 'status'] : ['error', 'output', 'status'];
|
|
83
|
+
if (Object.keys(resultDecision).some((key) => !allowedKeys.includes(key))) {
|
|
84
|
+
throw new Error('plugin tool.result fields must match the documented union');
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
(resultDecision.status === 'error' || resultDecision.status === 'cancelled') &&
|
|
88
|
+
resultDecision.error !== undefined &&
|
|
89
|
+
typeof resultDecision.error !== 'string'
|
|
90
|
+
) {
|
|
91
|
+
throw new Error('plugin tool.result error must be a string');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function pluginToolResultDecisionOutput(resultDecision = {}, fallback) {
|
|
96
|
+
validateToolResultDecision(resultDecision);
|
|
97
|
+
if (resultDecision.status === 'error') {
|
|
98
|
+
return resultDecision.error ?? resultDecision.output ?? fallback;
|
|
99
|
+
}
|
|
100
|
+
if (resultDecision.status === 'cancelled') {
|
|
101
|
+
return resultDecision.error ?? resultDecision.output ?? 'Tool cancelled';
|
|
102
|
+
}
|
|
103
|
+
return resultDecision.output !== undefined ? resultDecision.output : fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function pluginToolResultDecisionExitCode(resultDecision = {}, fallback = 0) {
|
|
107
|
+
validateToolResultDecision(resultDecision);
|
|
108
|
+
if (resultDecision.status === 'error' || resultDecision.status === 'cancelled') return 1;
|
|
109
|
+
if (resultDecision.status === 'done') return 0;
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isPlainObject(value) {
|
|
114
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
115
|
+
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
pluginToolResultDecisionExitCode,
|
|
5
|
+
pluginToolResultDecisionOutput,
|
|
6
|
+
} from './runtime-decisions.mjs';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
applyToolCallDecision,
|
|
10
|
+
pluginToolResultDecisionExitCode,
|
|
11
|
+
pluginToolResultDecisionOutput,
|
|
12
|
+
validateToolCallDecision,
|
|
13
|
+
validateToolResultDecision,
|
|
14
|
+
} from './runtime-decisions.mjs';
|
|
15
|
+
export {
|
|
16
|
+
isPluginContentBlock,
|
|
17
|
+
normalizePluginToolExecuteOutput,
|
|
18
|
+
normalizePluginToolOutput,
|
|
19
|
+
pluginContentBlockText,
|
|
20
|
+
} from './runtime-content.mjs';
|
|
3
21
|
|
|
4
22
|
export function createToolUseID() {
|
|
5
23
|
return `toolu_${randomUUID()}`;
|
|
@@ -40,158 +58,10 @@ export function pluginToolUseBlock(tool, input, toolUseID) {
|
|
|
40
58
|
};
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
export function pluginToolResultDecisionOutput(resultDecision = {}, fallback) {
|
|
44
|
-
validateToolResultDecision(resultDecision);
|
|
45
|
-
if (resultDecision.status === 'error') {
|
|
46
|
-
return resultDecision.error ?? resultDecision.output ?? fallback;
|
|
47
|
-
}
|
|
48
|
-
if (resultDecision.status === 'cancelled') {
|
|
49
|
-
return resultDecision.error ?? resultDecision.output ?? 'Tool cancelled';
|
|
50
|
-
}
|
|
51
|
-
return resultDecision.output !== undefined ? resultDecision.output : fallback;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function pluginToolResultDecisionExitCode(resultDecision = {}, fallback = 0) {
|
|
55
|
-
validateToolResultDecision(resultDecision);
|
|
56
|
-
if (resultDecision.status === 'error' || resultDecision.status === 'cancelled') return 1;
|
|
57
|
-
if (resultDecision.status === 'done') return 0;
|
|
58
|
-
return fallback;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateToolResultDecision(resultDecision = {}) {
|
|
62
|
-
if (!resultDecision || typeof resultDecision !== 'object' || !Object.hasOwn(resultDecision, 'status')) return;
|
|
63
|
-
if (resultDecision.status !== 'done' && resultDecision.status !== 'error' && resultDecision.status !== 'cancelled') {
|
|
64
|
-
throw new Error('plugin tool.result status must be done, error, or cancelled');
|
|
65
|
-
}
|
|
66
|
-
const allowedKeys = resultDecision.status === 'done' ? ['output', 'status'] : ['error', 'output', 'status'];
|
|
67
|
-
if (Object.keys(resultDecision).some((key) => !allowedKeys.includes(key))) {
|
|
68
|
-
throw new Error('plugin tool.result fields must match the documented union');
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
(resultDecision.status === 'error' || resultDecision.status === 'cancelled') &&
|
|
72
|
-
resultDecision.error !== undefined &&
|
|
73
|
-
typeof resultDecision.error !== 'string'
|
|
74
|
-
) {
|
|
75
|
-
throw new Error('plugin tool.result error must be a string');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
61
|
export function toolResultContent(toolRun = {}) {
|
|
80
62
|
return Object.hasOwn(toolRun, 'toolResultOutput') ? toolRun.toolResultOutput : toolRun.output;
|
|
81
63
|
}
|
|
82
64
|
|
|
83
|
-
export function normalizePluginToolOutput(output) {
|
|
84
|
-
if (Array.isArray(output) && output.every(isPluginContentBlock)) {
|
|
85
|
-
const text = output.map(pluginContentBlockText).filter(Boolean).join('\n').trimEnd();
|
|
86
|
-
return { raw: output, text };
|
|
87
|
-
}
|
|
88
|
-
const text = String(output ?? '').trimEnd();
|
|
89
|
-
return { raw: text, text };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function normalizePluginToolExecuteOutput(output) {
|
|
93
|
-
if (output === undefined || typeof output === 'string') return normalizePluginToolOutput(output);
|
|
94
|
-
if (Array.isArray(output)) {
|
|
95
|
-
if (!output.every(isPluginContentBlock)) {
|
|
96
|
-
throw new Error('plugin tool result content blocks must be text or image blocks');
|
|
97
|
-
}
|
|
98
|
-
return normalizePluginToolOutput(output);
|
|
99
|
-
}
|
|
100
|
-
throw new Error('plugin tool result must be a string, content blocks, or undefined');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function isPluginContentBlock(block) {
|
|
104
|
-
if (!block || typeof block !== 'object') return false;
|
|
105
|
-
if (block.type === 'text') return typeof block.text === 'string';
|
|
106
|
-
if (block.type === 'image') return typeof block.mimeType === 'string' && typeof block.data === 'string';
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function pluginContentBlockText(block) {
|
|
111
|
-
if (block.type === 'text') return block.text;
|
|
112
|
-
return '';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function applyToolCallDecision(toolName, request, callDecision = { action: 'allow' }) {
|
|
116
|
-
validateToolCallDecision(callDecision);
|
|
117
|
-
if (callDecision.action === 'allow') return { request };
|
|
118
|
-
if (callDecision.action === 'reject-and-continue') {
|
|
119
|
-
if (typeof callDecision.message !== 'string') {
|
|
120
|
-
throw new Error('plugin tool.call reject-and-continue message must be a string');
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
output: {
|
|
124
|
-
output: callDecision.message,
|
|
125
|
-
exitCode: 0,
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
if (callDecision.action === 'synthesize') {
|
|
130
|
-
const result = callDecision.result && isPlainObject(callDecision.result)
|
|
131
|
-
? callDecision.result
|
|
132
|
-
: callDecision;
|
|
133
|
-
if (typeof result.output !== 'string') {
|
|
134
|
-
throw new Error('plugin tool.call synthesize result.output must be a string');
|
|
135
|
-
}
|
|
136
|
-
if (result.exitCode !== undefined && !Number.isInteger(result.exitCode)) {
|
|
137
|
-
throw new Error('plugin tool.call synthesize result.exitCode must be an integer');
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
output: {
|
|
141
|
-
output: result.output.trimEnd(),
|
|
142
|
-
exitCode: result.exitCode ?? 0,
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
if (callDecision.action === 'error') {
|
|
147
|
-
if (typeof callDecision.message !== 'string') {
|
|
148
|
-
throw new Error('plugin tool.call error message must be a string');
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
output: {
|
|
152
|
-
output: callDecision.message,
|
|
153
|
-
exitCode: 1,
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
if (callDecision.action === 'modify') {
|
|
158
|
-
if (!isPlainObject(callDecision.input)) {
|
|
159
|
-
throw new Error('plugin tool.call modify input must be an object');
|
|
160
|
-
}
|
|
161
|
-
return { request: { ...request, flags: callDecision.input } };
|
|
162
|
-
}
|
|
163
|
-
return { request };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function validateToolCallDecision(callDecision) {
|
|
167
|
-
if (!callDecision || typeof callDecision !== 'object') {
|
|
168
|
-
throw new Error('plugin tool.call result must be an object');
|
|
169
|
-
}
|
|
170
|
-
if (
|
|
171
|
-
callDecision.action !== 'allow' &&
|
|
172
|
-
callDecision.action !== 'reject-and-continue' &&
|
|
173
|
-
callDecision.action !== 'modify' &&
|
|
174
|
-
callDecision.action !== 'synthesize' &&
|
|
175
|
-
callDecision.action !== 'error'
|
|
176
|
-
) {
|
|
177
|
-
throw new Error('plugin tool.call action must be allow, reject-and-continue, modify, synthesize, or error');
|
|
178
|
-
}
|
|
179
|
-
const allowedKeys = {
|
|
180
|
-
allow: ['action'],
|
|
181
|
-
'reject-and-continue': ['action', 'message'],
|
|
182
|
-
modify: ['action', 'input'],
|
|
183
|
-
synthesize: ['action', 'result'],
|
|
184
|
-
error: ['action', 'message'],
|
|
185
|
-
}[callDecision.action];
|
|
186
|
-
if (Object.keys(callDecision).some((key) => !allowedKeys.includes(key))) {
|
|
187
|
-
throw new Error('plugin tool.call fields must match the documented union');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function isPlainObject(value) {
|
|
192
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
65
|
export function toolCallDecisionToolRun(toolName, input, toolUseID, callResult) {
|
|
196
66
|
return {
|
|
197
67
|
...callResult.output,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fixtureAgentResponse } from '../../agent/fixture.mjs';
|
|
2
2
|
import { resolvePermissionDecision } from '../../commands/permissions.mjs';
|
|
3
3
|
import { runPluginEventHandlers } from '../../plugins/discover.mjs';
|
|
4
4
|
import { isToolDisabled } from '../toolbox.mjs';
|
|
@@ -36,7 +36,7 @@ export async function executePromptTaskToolRequest(request, parsed = {}, plugins
|
|
|
36
36
|
permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
const output =
|
|
39
|
+
const output = fixtureAgentResponse(request.flags.prompt, '');
|
|
40
40
|
const resultDecision = await runPluginEventHandlers(
|
|
41
41
|
plugins.handlers['tool.result'],
|
|
42
42
|
pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
|