@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.
Files changed (47) hide show
  1. package/README.md +2 -1
  2. package/docs/CLI.md +65 -1
  3. package/docs/DEMO.md +453 -0
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/README.md +1 -0
  6. package/package.json +7 -6
  7. package/src/agent/{local.mjs → fixture.mjs} +1 -1
  8. package/src/cli/execute.mjs +6 -4
  9. package/src/cli/interactive-core.mjs +5 -279
  10. package/src/cli/interactive-io.mjs +101 -0
  11. package/src/cli/interactive-slash.mjs +184 -0
  12. package/src/cli/repl.mjs +1 -2
  13. package/src/cli/slash-commands.mjs +20 -2
  14. package/src/cli/tui-actions.mjs +72 -0
  15. package/src/cli/tui-blessed.mjs +198 -0
  16. package/src/cli/tui-keys.mjs +80 -0
  17. package/src/cli/tui-lane.mjs +73 -0
  18. package/src/cli/tui-render.mjs +169 -0
  19. package/src/cli/tui-submit.mjs +82 -0
  20. package/src/cli/tui.mjs +30 -613
  21. package/src/commands/permissions-eval.mjs +122 -0
  22. package/src/commands/permissions-rules.mjs +53 -0
  23. package/src/commands/permissions-text.mjs +112 -0
  24. package/src/commands/permissions.mjs +15 -281
  25. package/src/commands/usage.mjs +1 -1
  26. package/src/constants.mjs +7 -1
  27. package/src/mcp/local.mjs +55 -0
  28. package/src/mcp/parsers.mjs +46 -0
  29. package/src/mcp/probe.mjs +12 -351
  30. package/src/mcp/remote-oauth.mjs +55 -0
  31. package/src/mcp/remote-session.mjs +54 -0
  32. package/src/mcp/remote-sse.mjs +82 -0
  33. package/src/mcp/remote.mjs +74 -0
  34. package/src/plugins/api.mjs +187 -0
  35. package/src/plugins/configuration.mjs +124 -0
  36. package/src/plugins/discover.mjs +8 -804
  37. package/src/plugins/helpers.mjs +187 -0
  38. package/src/plugins/subsystems.mjs +198 -0
  39. package/src/plugins/validators.mjs +142 -0
  40. package/src/sdk-execute.mjs +82 -0
  41. package/src/sdk-settings.mjs +88 -0
  42. package/src/sdk.mjs +13 -164
  43. package/src/tools/builtin/oracle.mjs +2 -2
  44. package/src/tools/builtin/runtime-content.mjs +31 -0
  45. package/src/tools/builtin/runtime-decisions.mjs +115 -0
  46. package/src/tools/builtin/runtime.mjs +18 -148
  47. 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 { parseJsonc } from './settings/load.mjs';
11
-
12
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
- const covenCodeBin = path.join(repoRoot, 'bin', 'coven-code.mjs');
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 { localAgentResponse } from '../../agent/local.mjs';
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: ${localAgentResponse(request.flags.prompt, '')}`;
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 { localAgentResponse } from '../../agent/local.mjs';
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 = localAgentResponse(request.flags.prompt, '');
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),