@opencoven/coven-code 0.0.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.
Files changed (86) hide show
  1. package/README.md +145 -0
  2. package/bin/coven-code-sdk.mjs +12 -0
  3. package/bin/coven-code.mjs +19 -0
  4. package/docs/CLI.md +192 -0
  5. package/docs/CONFIGURATION.md +107 -0
  6. package/docs/DEVELOPMENT.md +104 -0
  7. package/docs/DOGFOOD-PROTOCOL.md +263 -0
  8. package/docs/MCP-SKILLS-PLUGINS.md +127 -0
  9. package/docs/README.md +38 -0
  10. package/docs/RELEASE.md +33 -0
  11. package/docs/SDK.md +107 -0
  12. package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +904 -0
  13. package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +670 -0
  14. package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +235 -0
  15. package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +63 -0
  16. package/package.json +36 -0
  17. package/src/agent/lane.mjs +136 -0
  18. package/src/agent/local.mjs +95 -0
  19. package/src/cli/dispatch.mjs +66 -0
  20. package/src/cli/execute.mjs +588 -0
  21. package/src/cli/help.mjs +58 -0
  22. package/src/cli/interactive-core.mjs +302 -0
  23. package/src/cli/notifications.mjs +13 -0
  24. package/src/cli/parse.mjs +83 -0
  25. package/src/cli/reasoning.mjs +45 -0
  26. package/src/cli/refs.mjs +162 -0
  27. package/src/cli/repl.mjs +61 -0
  28. package/src/cli/slash-commands.mjs +357 -0
  29. package/src/cli/stream-json.mjs +116 -0
  30. package/src/cli/tui.mjs +757 -0
  31. package/src/commands/agents.mjs +53 -0
  32. package/src/commands/config.mjs +27 -0
  33. package/src/commands/ide.mjs +17 -0
  34. package/src/commands/login.mjs +84 -0
  35. package/src/commands/mcp.mjs +176 -0
  36. package/src/commands/permissions.mjs +328 -0
  37. package/src/commands/plugins.mjs +86 -0
  38. package/src/commands/review.mjs +74 -0
  39. package/src/commands/skill.mjs +23 -0
  40. package/src/commands/threads.mjs +165 -0
  41. package/src/commands/tools.mjs +77 -0
  42. package/src/commands/update.mjs +31 -0
  43. package/src/commands/usage.mjs +34 -0
  44. package/src/constants.mjs +46 -0
  45. package/src/main.mjs +87 -0
  46. package/src/mcp/discover.mjs +154 -0
  47. package/src/mcp/permissions.mjs +52 -0
  48. package/src/mcp/probe.mjs +424 -0
  49. package/src/mcp/registry.mjs +96 -0
  50. package/src/plugins/discover.mjs +880 -0
  51. package/src/sdk-install.mjs +187 -0
  52. package/src/sdk.mjs +314 -0
  53. package/src/settings/load.mjs +134 -0
  54. package/src/settings/paths.mjs +101 -0
  55. package/src/skills/builtin/building-skills/SKILL.md +20 -0
  56. package/src/skills/discover.mjs +95 -0
  57. package/src/threads/store.mjs +176 -0
  58. package/src/tools/builtin/bash.mjs +110 -0
  59. package/src/tools/builtin/create-file.mjs +66 -0
  60. package/src/tools/builtin/edit-file.mjs +76 -0
  61. package/src/tools/builtin/finder.mjs +73 -0
  62. package/src/tools/builtin/glob.mjs +74 -0
  63. package/src/tools/builtin/grep.mjs +82 -0
  64. package/src/tools/builtin/index.mjs +83 -0
  65. package/src/tools/builtin/librarian.mjs +97 -0
  66. package/src/tools/builtin/look-at.mjs +92 -0
  67. package/src/tools/builtin/mcp.mjs +51 -0
  68. package/src/tools/builtin/mermaid.mjs +59 -0
  69. package/src/tools/builtin/oracle.mjs +56 -0
  70. package/src/tools/builtin/painter.mjs +81 -0
  71. package/src/tools/builtin/plugin-tool.mjs +53 -0
  72. package/src/tools/builtin/read-mcp-resource.mjs +63 -0
  73. package/src/tools/builtin/read-web-page.mjs +72 -0
  74. package/src/tools/builtin/read.mjs +59 -0
  75. package/src/tools/builtin/runtime.mjs +215 -0
  76. package/src/tools/builtin/task.mjs +63 -0
  77. package/src/tools/builtin/toolbox-tool.mjs +57 -0
  78. package/src/tools/builtin/undo-edit.mjs +97 -0
  79. package/src/tools/builtin/web-search.mjs +128 -0
  80. package/src/tools/toolbox.mjs +273 -0
  81. package/src/util/fs.mjs +13 -0
  82. package/src/util/glob.mjs +46 -0
  83. package/src/util/html.mjs +21 -0
  84. package/src/util/media.mjs +13 -0
  85. package/src/util/shell.mjs +24 -0
  86. package/src/util/table.mjs +11 -0
@@ -0,0 +1,81 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
4
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
5
+ import { isToolDisabled } from '../toolbox.mjs';
6
+ import {
7
+ applyToolCallDecision,
8
+ createToolUseID,
9
+ permissionDeniedOutput,
10
+ pluginTextToolRunResult,
11
+ pluginToolCallEvent,
12
+ pluginToolResultEvent,
13
+ pluginToolUseBlock,
14
+ toolCallDecisionToolRun,
15
+ validateToolCallDecision,
16
+ } from './runtime.mjs';
17
+
18
+ export const TOOL_NAME = 'painter';
19
+
20
+ const PAINTER_PLACEHOLDER_PNG = Buffer.from(
21
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAFeAKU5XgS2wAAAABJRU5ErkJggg==',
22
+ 'base64',
23
+ );
24
+
25
+ export async function executePromptPainterToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
26
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
27
+ request = { ...request, flags: normalizePainterInput(request.flags) };
28
+ if (!request.flags.prompt) return { output: 'painter requires --prompt' };
29
+ const toolUseID = createToolUseID();
30
+ const callDecision = await runPluginEventHandlers(
31
+ plugins.handlers['tool.call'],
32
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
33
+ validateToolCallDecision,
34
+ );
35
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
36
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
37
+ request = { ...callResult.request, flags: normalizePainterInput(callResult.request.flags) };
38
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
39
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
40
+ return {
41
+ output: permissionDeniedOutput(TOOL_NAME, decision),
42
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
43
+ };
44
+ }
45
+ const output = writePainterArtifact(request.flags);
46
+ const resultDecision = await runPluginEventHandlers(
47
+ plugins.handlers['tool.result'],
48
+ pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
49
+ );
50
+ return {
51
+ ...pluginTextToolRunResult(resultDecision, output),
52
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
53
+ };
54
+ }
55
+
56
+ function normalizePainterInput(input = {}) {
57
+ const prompt = input.prompt ?? input.description ?? input.text ?? '';
58
+ const output = input.output ?? input.path ?? input.file ?? 'coven-code-painter-output.png';
59
+ const references = [input.reference, input.references, input.image, input.images]
60
+ .flat()
61
+ .filter(Boolean)
62
+ .map(String);
63
+ return {
64
+ ...input,
65
+ prompt: String(prompt),
66
+ output: String(output),
67
+ references,
68
+ };
69
+ }
70
+
71
+ function writePainterArtifact(input) {
72
+ const outputPath = path.resolve(process.cwd(), input.output);
73
+ mkdirSync(path.dirname(outputPath), { recursive: true });
74
+ writeFileSync(outputPath, PAINTER_PLACEHOLDER_PNG);
75
+ return [
76
+ `Generated image: ${input.output}`,
77
+ 'media_type: image/png',
78
+ `prompt: ${input.prompt}`,
79
+ ...(input.references.length ? [`references: ${input.references.join(', ')}`] : []),
80
+ ].join('\n');
81
+ }
@@ -0,0 +1,53 @@
1
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
2
+ import { createPluginToolContext, runPluginEventHandlers } from '../../plugins/discover.mjs';
3
+ import { isToolDisabled } from '../toolbox.mjs';
4
+ import {
5
+ applyToolCallDecision,
6
+ createToolUseID,
7
+ normalizePluginToolExecuteOutput,
8
+ normalizePluginToolOutput,
9
+ permissionDeniedOutput,
10
+ pluginToolCallEvent,
11
+ pluginToolResultDecisionExitCode,
12
+ pluginToolResultDecisionOutput,
13
+ pluginToolResultEvent,
14
+ pluginToolUseBlock,
15
+ toolCallDecisionToolRun,
16
+ validateToolCallDecision,
17
+ } from './runtime.mjs';
18
+
19
+ export async function executePromptPluginToolRequest(tool, request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
20
+ if (isToolDisabled(tool.name, 'plugin', parsed)) return { output: `Tool disabled: ${tool.name}` };
21
+ const toolUseID = createToolUseID();
22
+ const callDecision = await runPluginEventHandlers(
23
+ plugins.handlers['tool.call'],
24
+ pluginToolCallEvent(tool.name, request.flags, threadId, toolUseID),
25
+ validateToolCallDecision,
26
+ );
27
+ const callResult = applyToolCallDecision(tool.name, request, callDecision);
28
+ if (callResult.output) return toolCallDecisionToolRun(tool.name, request.flags, toolUseID, callResult);
29
+ request = callResult.request;
30
+ const decision = resolvePermissionDecision(tool.name, request.flags, parsed, { threadId });
31
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
32
+ return {
33
+ output: permissionDeniedOutput(tool.name, decision),
34
+ permissionDenials: [{ tool: tool.name, action: decision.action, reason: 'permission' }],
35
+ };
36
+ }
37
+ const output = typeof tool.execute === 'function' ? await tool.execute(request.flags, createPluginToolContext()) : undefined;
38
+ const normalizedOutput = normalizePluginToolExecuteOutput(output);
39
+ const resultDecision = await runPluginEventHandlers(
40
+ plugins.handlers['tool.result'],
41
+ pluginToolResultEvent(tool.name, request.flags, 'done', normalizedOutput.raw, threadId, toolUseID),
42
+ );
43
+ const finalOutput = normalizePluginToolOutput(
44
+ pluginToolResultDecisionOutput(resultDecision, normalizedOutput.raw),
45
+ );
46
+ const exitCode = pluginToolResultDecisionExitCode(resultDecision);
47
+ return {
48
+ output: finalOutput.text,
49
+ toolResultOutput: finalOutput.raw,
50
+ exitCode,
51
+ toolUse: pluginToolUseBlock(tool.name, request.flags, toolUseID),
52
+ };
53
+ }
@@ -0,0 +1,63 @@
1
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
2
+ import { listActiveMcpServerEntries } from '../../mcp/discover.mjs';
3
+ import { readMcpResource } from '../../mcp/probe.mjs';
4
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
5
+ import { isToolDisabled } from '../toolbox.mjs';
6
+ import {
7
+ applyToolCallDecision,
8
+ createToolUseID,
9
+ permissionDeniedOutput,
10
+ pluginTextToolRunResult,
11
+ pluginToolCallEvent,
12
+ pluginToolResultEvent,
13
+ pluginToolUseBlock,
14
+ toolCallDecisionToolRun,
15
+ validateToolCallDecision,
16
+ } from './runtime.mjs';
17
+
18
+ export const TOOL_NAME = 'read_mcp_resource';
19
+
20
+ export async function executePromptReadMcpResourceToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
21
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
22
+ request = { ...request, flags: normalizeReadMcpResourceInput(request.flags) };
23
+ if (!request.flags.server) return { output: 'read_mcp_resource requires --server' };
24
+ if (!request.flags.uri) return { output: 'read_mcp_resource requires --uri' };
25
+ const toolUseID = createToolUseID();
26
+ const callDecision = await runPluginEventHandlers(
27
+ plugins.handlers['tool.call'],
28
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
29
+ validateToolCallDecision,
30
+ );
31
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
32
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
33
+ request = { ...callResult.request, flags: normalizeReadMcpResourceInput(callResult.request.flags) };
34
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
35
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
36
+ return {
37
+ output: permissionDeniedOutput(TOOL_NAME, decision),
38
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
39
+ };
40
+ }
41
+ const server = listActiveMcpServerEntries(parsed, '')
42
+ .find((entry) => entry.name === request.flags.server);
43
+ if (!server) return { output: `Unknown MCP server: ${request.flags.server}` };
44
+ const output = await readMcpResource(server.config, request.flags.uri, server.name);
45
+ const resultDecision = await runPluginEventHandlers(
46
+ plugins.handlers['tool.result'],
47
+ pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
48
+ );
49
+ return {
50
+ ...pluginTextToolRunResult(resultDecision, output),
51
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
52
+ };
53
+ }
54
+
55
+ function normalizeReadMcpResourceInput(input = {}) {
56
+ const server = input.server ?? input.name ?? input.mcp_server;
57
+ const uri = input.uri ?? input.url ?? input.resource;
58
+ return {
59
+ ...input,
60
+ server: server ? String(server) : '',
61
+ uri: uri ? String(uri) : '',
62
+ };
63
+ }
@@ -0,0 +1,72 @@
1
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
2
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
3
+ import { htmlToText } from '../../util/html.mjs';
4
+ import { isToolDisabled } from '../toolbox.mjs';
5
+ import {
6
+ applyToolCallDecision,
7
+ createToolUseID,
8
+ permissionDeniedOutput,
9
+ pluginTextToolRunResult,
10
+ pluginToolCallEvent,
11
+ pluginToolResultEvent,
12
+ pluginToolUseBlock,
13
+ toolCallDecisionToolRun,
14
+ validateToolCallDecision,
15
+ } from './runtime.mjs';
16
+
17
+ export const TOOL_NAME = 'read_web_page';
18
+
19
+ export async function executePromptReadWebPageToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
20
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
21
+ request = { ...request, flags: normalizeReadWebPageInput(request.flags) };
22
+ if (!request.flags.url) return { output: 'read_web_page requires --url' };
23
+ const toolUseID = createToolUseID();
24
+ const callDecision = await runPluginEventHandlers(
25
+ plugins.handlers['tool.call'],
26
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
27
+ validateToolCallDecision,
28
+ );
29
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
30
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
31
+ request = { ...callResult.request, flags: normalizeReadWebPageInput(callResult.request.flags) };
32
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
33
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
34
+ return {
35
+ output: permissionDeniedOutput(TOOL_NAME, decision),
36
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
37
+ };
38
+ }
39
+ const output = await readWebPageBuiltin(request.flags.url);
40
+ const resultDecision = await runPluginEventHandlers(
41
+ plugins.handlers['tool.result'],
42
+ pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
43
+ );
44
+ return {
45
+ ...pluginTextToolRunResult(resultDecision, output),
46
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
47
+ };
48
+ }
49
+
50
+ function normalizeReadWebPageInput(input = {}) {
51
+ const url = input.url ?? input.uri ?? input.href;
52
+ return {
53
+ ...input,
54
+ url: url ? String(url) : '',
55
+ };
56
+ }
57
+
58
+ async function readWebPageBuiltin(url) {
59
+ try {
60
+ const response = await fetch(url, {
61
+ headers: { 'user-agent': 'coven-code/0.0.0' },
62
+ });
63
+ if (!response.ok) return `Failed to read ${url}: HTTP ${response.status}`;
64
+ const contentType = response.headers.get('content-type') ?? '';
65
+ const body = await response.text();
66
+ return contentType.includes('html') || body.includes('<')
67
+ ? htmlToText(body)
68
+ : body.trimEnd();
69
+ } catch (error) {
70
+ return `Failed to read ${url}: ${error.message}`;
71
+ }
72
+ }
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
4
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
5
+ import { isToolDisabled } from '../toolbox.mjs';
6
+ import {
7
+ applyToolCallDecision,
8
+ createToolUseID,
9
+ permissionDeniedOutput,
10
+ pluginTextToolRunResult,
11
+ pluginToolCallEvent,
12
+ pluginToolResultEvent,
13
+ pluginToolUseBlock,
14
+ toolCallDecisionToolRun,
15
+ validateToolCallDecision,
16
+ } from './runtime.mjs';
17
+
18
+ export const TOOL_NAME = 'Read';
19
+
20
+ export async function executePromptReadToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
21
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
22
+ request = { ...request, flags: normalizeReadInput(request.flags) };
23
+ if (!request.flags.path) return { output: 'Read requires --path' };
24
+ const toolUseID = createToolUseID();
25
+ const callDecision = await runPluginEventHandlers(
26
+ plugins.handlers['tool.call'],
27
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
28
+ validateToolCallDecision,
29
+ );
30
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
31
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
32
+ request = { ...callResult.request, flags: normalizeReadInput(callResult.request.flags) };
33
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
34
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
35
+ return {
36
+ output: permissionDeniedOutput(TOOL_NAME, decision),
37
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
38
+ };
39
+ }
40
+ const output = readBuiltinFile(request.flags.path).trimEnd();
41
+ const resultDecision = await runPluginEventHandlers(
42
+ plugins.handlers['tool.result'],
43
+ pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
44
+ );
45
+ return {
46
+ ...pluginTextToolRunResult(resultDecision, output),
47
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
48
+ };
49
+ }
50
+
51
+ function normalizeReadInput(input = {}) {
52
+ const filePath = input.path ?? input.file ?? input.file_path;
53
+ return { ...input, path: filePath ? String(filePath) : '' };
54
+ }
55
+
56
+ function readBuiltinFile(filePath) {
57
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
58
+ return readFileSync(absolutePath, 'utf8');
59
+ }
@@ -0,0 +1,215 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import path from 'node:path';
3
+
4
+ export function createToolUseID() {
5
+ return `toolu_${randomUUID()}`;
6
+ }
7
+
8
+ export function relativeToolPath(filePath) {
9
+ const relative = path.relative(process.cwd(), filePath);
10
+ return relative && !relative.startsWith('..') ? relative : filePath;
11
+ }
12
+
13
+ export function pluginToolCallEvent(tool, input, threadId, toolUseID = createToolUseID()) {
14
+ return {
15
+ toolUseID,
16
+ tool,
17
+ input,
18
+ thread: { id: threadId },
19
+ };
20
+ }
21
+
22
+ export function pluginToolResultEvent(tool, input, status, output, threadId, toolUseID, error) {
23
+ return {
24
+ toolUseID,
25
+ tool,
26
+ input,
27
+ status,
28
+ output,
29
+ ...(error ? { error } : {}),
30
+ thread: { id: threadId },
31
+ };
32
+ }
33
+
34
+ export function pluginToolUseBlock(tool, input, toolUseID) {
35
+ return {
36
+ type: 'tool_use',
37
+ id: toolUseID,
38
+ name: tool,
39
+ input,
40
+ };
41
+ }
42
+
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
+ export function toolResultContent(toolRun = {}) {
80
+ return Object.hasOwn(toolRun, 'toolResultOutput') ? toolRun.toolResultOutput : toolRun.output;
81
+ }
82
+
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
+ export function toolCallDecisionToolRun(toolName, input, toolUseID, callResult) {
196
+ return {
197
+ ...callResult.output,
198
+ toolUse: pluginToolUseBlock(toolName, input, toolUseID),
199
+ };
200
+ }
201
+
202
+ export function pluginResultOutput(resultDecision, output) {
203
+ return String(pluginToolResultDecisionOutput(resultDecision, output) ?? '').trimEnd();
204
+ }
205
+
206
+ export function pluginTextToolRunResult(resultDecision, output, fallbackExitCode = 0) {
207
+ return {
208
+ output: pluginResultOutput(resultDecision, output),
209
+ exitCode: pluginToolResultDecisionExitCode(resultDecision, fallbackExitCode),
210
+ };
211
+ }
212
+
213
+ export function permissionDeniedOutput(toolName, decision) {
214
+ return decision.message ? `Permission denied for ${toolName}: ${decision.message}` : `Permission denied for ${toolName}`;
215
+ }
@@ -0,0 +1,63 @@
1
+ import { localAgentResponse } from '../../agent/local.mjs';
2
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
3
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
4
+ import { isToolDisabled } from '../toolbox.mjs';
5
+ import {
6
+ applyToolCallDecision,
7
+ createToolUseID,
8
+ permissionDeniedOutput,
9
+ pluginTextToolRunResult,
10
+ pluginToolCallEvent,
11
+ pluginToolResultEvent,
12
+ pluginToolUseBlock,
13
+ toolCallDecisionToolRun,
14
+ validateToolCallDecision,
15
+ } from './runtime.mjs';
16
+
17
+ export const TOOL_NAME = 'Task';
18
+
19
+ export async function executePromptTaskToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
20
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
21
+ request = { ...request, flags: normalizeTaskInput(request.flags) };
22
+ if (!request.flags.prompt) return { output: 'Task requires --prompt' };
23
+ const toolUseID = createToolUseID();
24
+ const callDecision = await runPluginEventHandlers(
25
+ plugins.handlers['tool.call'],
26
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
27
+ validateToolCallDecision,
28
+ );
29
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
30
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
31
+ request = { ...callResult.request, flags: normalizeTaskInput(callResult.request.flags) };
32
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
33
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
34
+ return {
35
+ output: permissionDeniedOutput(TOOL_NAME, decision),
36
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
37
+ };
38
+ }
39
+ const output = localAgentResponse(request.flags.prompt, '');
40
+ const resultDecision = await runPluginEventHandlers(
41
+ plugins.handlers['tool.result'],
42
+ pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
43
+ );
44
+ const finalResult = pluginTextToolRunResult(resultDecision, output);
45
+ return {
46
+ output: finalResult.output,
47
+ exitCode: finalResult.exitCode,
48
+ subagentMessages: [{ text: finalResult.output }],
49
+ toolResultParentToolUseId: null,
50
+ finalParentToolUseId: null,
51
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
52
+ };
53
+ }
54
+
55
+ function normalizeTaskInput(input = {}) {
56
+ const prompt = input.prompt ?? input.task ?? input.instructions ?? input.message;
57
+ const description = input.description ?? input.title ?? input.name ?? 'subagent task';
58
+ return {
59
+ ...input,
60
+ description: String(description ?? 'subagent task'),
61
+ prompt: prompt ? String(prompt) : '',
62
+ };
63
+ }
@@ -0,0 +1,57 @@
1
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
2
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
3
+ import { executeToolboxTool, isToolDisabled, listToolboxTools, normalizeToolName } from '../toolbox.mjs';
4
+ import {
5
+ applyToolCallDecision,
6
+ createToolUseID,
7
+ permissionDeniedOutput,
8
+ pluginResultOutput,
9
+ pluginToolCallEvent,
10
+ pluginToolResultDecisionExitCode,
11
+ pluginToolResultEvent,
12
+ pluginToolUseBlock,
13
+ validateToolCallDecision,
14
+ } from './runtime.mjs';
15
+
16
+ export async function executePromptToolboxToolRequest(request, stdin = '', parsed = {}, plugins = { handlers: {} }, threadId = '') {
17
+ const toolboxName = normalizeToolName(request.toolName);
18
+ const tool = listToolboxTools(parsed).find((entry) => entry.name === toolboxName);
19
+ if (!tool) return { output: `Unknown tool: ${request.toolName}` };
20
+ if (isToolDisabled(tool.name, 'toolbox', parsed)) return { output: `Tool disabled: ${tool.name}` };
21
+ const toolUseID = createToolUseID();
22
+ const callDecision = await runPluginEventHandlers(
23
+ plugins.handlers['tool.call'],
24
+ pluginToolCallEvent(tool.name, request.flags, threadId, toolUseID),
25
+ validateToolCallDecision,
26
+ );
27
+ const callResult = applyToolCallDecision(tool.name, request, callDecision);
28
+ if (callResult.output) {
29
+ return {
30
+ ...callResult.output,
31
+ toolUse: pluginToolUseBlock(tool.name, request.flags, toolUseID),
32
+ };
33
+ }
34
+ request.flags = callResult.request.flags;
35
+ const decision = resolvePermissionDecision(tool.name, request.flags, parsed, { threadId });
36
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
37
+ return {
38
+ output: permissionDeniedOutput(tool.name, decision),
39
+ permissionDenials: [{ tool: tool.name, action: decision.action, reason: 'permission' }],
40
+ };
41
+ }
42
+ const result = executeToolboxTool(tool, request.flags, stdin, threadId);
43
+ if (result.stderr) process.stderr.write(result.stderr);
44
+ process.exitCode = result.status ?? 0;
45
+ const output = result.stdout.trimEnd();
46
+ const resultDecision = await runPluginEventHandlers(
47
+ plugins.handlers['tool.result'],
48
+ pluginToolResultEvent(tool.name, request.flags, (result.status ?? 0) === 0 ? 'done' : 'error', output, threadId, toolUseID),
49
+ );
50
+ const finalOutput = pluginResultOutput(resultDecision, output);
51
+ const exitCode = pluginToolResultDecisionExitCode(resultDecision, result.status ?? 0);
52
+ return {
53
+ output: finalOutput,
54
+ exitCode,
55
+ toolUse: pluginToolUseBlock(tool.name, request.flags, toolUseID),
56
+ };
57
+ }