@kosuke-ai/cli 1.0.1-next.5 → 1.0.1-next.6

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 (82) hide show
  1. package/README.md +104 -0
  2. package/dist/commands/mcp.js +85 -0
  3. package/dist/generated/tools/deploy/cancel.js +20 -0
  4. package/dist/generated/tools/deploy/messages.js +20 -0
  5. package/dist/generated/tools/deploy/run.js +52 -0
  6. package/dist/generated/tools/index.js +138 -0
  7. package/dist/generated/tools/preview/get.js +24 -0
  8. package/dist/generated/tools/preview/health.js +24 -0
  9. package/dist/generated/tools/preview/reset.js +23 -0
  10. package/dist/generated/tools/preview/share-create.js +24 -0
  11. package/dist/generated/tools/preview/share-delete.js +24 -0
  12. package/dist/generated/tools/preview/share-get.js +24 -0
  13. package/dist/generated/tools/preview/shared-artifact.js +25 -0
  14. package/dist/generated/tools/preview/shared.js +20 -0
  15. package/dist/generated/tools/projects/create.js +26 -0
  16. package/dist/generated/tools/projects/delete.js +20 -0
  17. package/dist/generated/tools/projects/env-vars-add.js +50 -0
  18. package/dist/generated/tools/projects/env-vars-remove.js +31 -0
  19. package/dist/generated/tools/projects/get.js +20 -0
  20. package/dist/generated/tools/projects/list.js +12 -0
  21. package/dist/generated/tools/projects/services-add.js +111 -0
  22. package/dist/generated/tools/projects/services-list.js +20 -0
  23. package/dist/generated/tools/projects/services-remove.js +25 -0
  24. package/dist/generated/tools/projects/services-update.js +104 -0
  25. package/dist/generated/tools/projects/settings-get.js +20 -0
  26. package/dist/generated/tools/projects/settings-import.js +208 -0
  27. package/dist/generated/tools/projects/settings-update.js +72 -0
  28. package/dist/generated/tools/projects/skills.js +32 -0
  29. package/dist/generated/tools/projects/update.js +24 -0
  30. package/dist/generated/tools/sessions/activate.js +25 -0
  31. package/dist/generated/tools/sessions/artifacts-get.js +30 -0
  32. package/dist/generated/tools/sessions/artifacts-list.js +25 -0
  33. package/dist/generated/tools/sessions/checklist-generate.js +25 -0
  34. package/dist/generated/tools/sessions/checklist-get.js +25 -0
  35. package/dist/generated/tools/sessions/comments-add.js +53 -0
  36. package/dist/generated/tools/sessions/comments-delete.js +30 -0
  37. package/dist/generated/tools/sessions/comments-list.js +25 -0
  38. package/dist/generated/tools/sessions/comments-update.js +62 -0
  39. package/dist/generated/tools/sessions/create.js +69 -0
  40. package/dist/generated/tools/sessions/delete.js +25 -0
  41. package/dist/generated/tools/sessions/diff-files.js +25 -0
  42. package/dist/generated/tools/sessions/diff-patch.js +64 -0
  43. package/dist/generated/tools/sessions/edit-prompt.js +69 -0
  44. package/dist/generated/tools/sessions/files-get.js +30 -0
  45. package/dist/generated/tools/sessions/files-list.js +25 -0
  46. package/dist/generated/tools/sessions/files-write.js +37 -0
  47. package/dist/generated/tools/sessions/get.js +25 -0
  48. package/dist/generated/tools/sessions/git-commit-message.js +25 -0
  49. package/dist/generated/tools/sessions/git-commit.js +29 -0
  50. package/dist/generated/tools/sessions/git-push.js +25 -0
  51. package/dist/generated/tools/sessions/git-state.js +25 -0
  52. package/dist/generated/tools/sessions/jobs-cancel.js +30 -0
  53. package/dist/generated/tools/sessions/jobs-get.js +30 -0
  54. package/dist/generated/tools/sessions/list.js +56 -0
  55. package/dist/generated/tools/sessions/messages.js +25 -0
  56. package/dist/generated/tools/sessions/search.js +17 -0
  57. package/dist/generated/tools/sessions/send.js +137 -0
  58. package/dist/generated/tools/sessions/update.js +63 -0
  59. package/dist/generated/tools/workspaces/access-status.js +28 -0
  60. package/dist/generated/tools/workspaces/ai-provider-get.js +20 -0
  61. package/dist/generated/tools/workspaces/ai-provider-remove.js +39 -0
  62. package/dist/generated/tools/workspaces/ai-provider-set.js +106 -0
  63. package/dist/generated/tools/workspaces/credits.js +20 -0
  64. package/dist/generated/tools/workspaces/get.js +20 -0
  65. package/dist/generated/tools/workspaces/getting-started-get.js +20 -0
  66. package/dist/generated/tools/workspaces/getting-started-update.js +27 -0
  67. package/dist/generated/tools/workspaces/list.js +12 -0
  68. package/dist/generated/tools/workspaces/members.js +24 -0
  69. package/dist/generated/tools/workspaces/sandbox-limit.js +20 -0
  70. package/dist/generated/tools/workspaces/stop-sandboxes.js +31 -0
  71. package/dist/generated/tools/workspaces/usage.js +28 -0
  72. package/dist/program.js +4 -2
  73. package/dist/runtime/mcp-attachments.js +110 -0
  74. package/dist/runtime/mcp-composite-support.js +49 -0
  75. package/dist/runtime/mcp-composite.js +309 -0
  76. package/dist/runtime/mcp-guard.js +74 -0
  77. package/dist/runtime/mcp-invoke.js +123 -0
  78. package/dist/runtime/mcp-open-pr.js +83 -0
  79. package/dist/runtime/mcp-redact.js +31 -0
  80. package/dist/runtime/mcp-server.js +219 -0
  81. package/dist/runtime/mcp-tool.js +6 -0
  82. package/package.json +2 -1
@@ -0,0 +1,28 @@
1
+ import { operation } from '../../commands/workspaces/access-status.js';
2
+ export const tool = {
3
+ "name": "workspaces_access_status",
4
+ "toolset": "workspaces",
5
+ "description": "Read which providers you, or the workspace, can run.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "scope": {
14
+ "type": "string",
15
+ "description": "Whose provider access to report. Anything other than 'workspace' reads as 'member'.",
16
+ "enum": [
17
+ "member",
18
+ "workspace"
19
+ ]
20
+ }
21
+ },
22
+ "required": [
23
+ "workspaceId"
24
+ ],
25
+ "additionalProperties": false
26
+ },
27
+ operation,
28
+ };
@@ -0,0 +1,20 @@
1
+ import { operation } from '../../commands/workspaces/ai-provider-get.js';
2
+ export const tool = {
3
+ "name": "workspaces_ai_provider_get",
4
+ "toolset": "workspaces",
5
+ "description": "Read the workspace credentials and active auth method per provider. Secrets are masked.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ }
13
+ },
14
+ "required": [
15
+ "workspaceId"
16
+ ],
17
+ "additionalProperties": false
18
+ },
19
+ operation,
20
+ };
@@ -0,0 +1,39 @@
1
+ import { operation } from '../../commands/workspaces/ai-provider-remove.js';
2
+ export const tool = {
3
+ "name": "workspaces_ai_provider_remove",
4
+ "toolset": "workspaces",
5
+ "description": "Remove the credentials of one auth method. Which methods a provider takes is not in the schema. Admins only.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "provider": {
14
+ "type": "string",
15
+ "enum": [
16
+ "anthropic",
17
+ "openai"
18
+ ]
19
+ },
20
+ "authMethod": {
21
+ "type": "string",
22
+ "description": "Must belong to the provider: anthropic takes the first three, openai api_key or codex.",
23
+ "enum": [
24
+ "api_key",
25
+ "aws_bedrock",
26
+ "claude_code",
27
+ "codex"
28
+ ]
29
+ }
30
+ },
31
+ "required": [
32
+ "workspaceId",
33
+ "provider",
34
+ "authMethod"
35
+ ],
36
+ "additionalProperties": false
37
+ },
38
+ operation,
39
+ };
@@ -0,0 +1,106 @@
1
+ import { operation } from '../../commands/workspaces/ai-provider-set.js';
2
+ export const tool = {
3
+ "name": "workspaces_ai_provider_set",
4
+ "toolset": "workspaces",
5
+ "description": "Store or switch workspace credentials. The body is one of the operation variants. Admins only.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "body": {
14
+ "description": "The whole request body.",
15
+ "oneOf": [
16
+ {
17
+ "type": "object",
18
+ "properties": {
19
+ "operation": {
20
+ "type": "string",
21
+ "const": "anthropic_api_key"
22
+ },
23
+ "anthropicApiKey": {
24
+ "type": "string",
25
+ "minLength": 1
26
+ }
27
+ },
28
+ "required": [
29
+ "operation"
30
+ ]
31
+ },
32
+ {
33
+ "type": "object",
34
+ "properties": {
35
+ "operation": {
36
+ "type": "string",
37
+ "const": "anthropic_aws_bedrock"
38
+ },
39
+ "region": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ },
43
+ "bedrockApiKey": {
44
+ "type": "string",
45
+ "minLength": 1
46
+ },
47
+ "validationModel": {
48
+ "type": "string"
49
+ }
50
+ },
51
+ "required": [
52
+ "operation",
53
+ "region"
54
+ ]
55
+ },
56
+ {
57
+ "type": "object",
58
+ "properties": {
59
+ "operation": {
60
+ "type": "string",
61
+ "const": "anthropic_claude_code"
62
+ }
63
+ },
64
+ "required": [
65
+ "operation"
66
+ ]
67
+ },
68
+ {
69
+ "type": "object",
70
+ "properties": {
71
+ "operation": {
72
+ "type": "string",
73
+ "const": "openai_api_key"
74
+ },
75
+ "openaiApiKey": {
76
+ "type": "string",
77
+ "minLength": 1
78
+ }
79
+ },
80
+ "required": [
81
+ "operation"
82
+ ]
83
+ },
84
+ {
85
+ "type": "object",
86
+ "properties": {
87
+ "operation": {
88
+ "type": "string",
89
+ "const": "openai_codex"
90
+ }
91
+ },
92
+ "required": [
93
+ "operation"
94
+ ]
95
+ }
96
+ ]
97
+ }
98
+ },
99
+ "required": [
100
+ "workspaceId",
101
+ "body"
102
+ ],
103
+ "additionalProperties": false
104
+ },
105
+ operation,
106
+ };
@@ -0,0 +1,20 @@
1
+ import { operation } from '../../commands/workspaces/credits.js';
2
+ export const tool = {
3
+ "name": "workspaces_credits",
4
+ "toolset": "workspaces",
5
+ "description": "Read the credit balance and limit of a workspace.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ }
13
+ },
14
+ "required": [
15
+ "workspaceId"
16
+ ],
17
+ "additionalProperties": false
18
+ },
19
+ operation,
20
+ };
@@ -0,0 +1,20 @@
1
+ import { operation } from '../../commands/workspaces/get.js';
2
+ export const tool = {
3
+ "name": "workspaces_get",
4
+ "toolset": "workspaces",
5
+ "description": "Read one workspace and your role in it.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ }
13
+ },
14
+ "required": [
15
+ "workspaceId"
16
+ ],
17
+ "additionalProperties": false
18
+ },
19
+ operation,
20
+ };
@@ -0,0 +1,20 @@
1
+ import { operation } from '../../commands/workspaces/getting-started-get.js';
2
+ export const tool = {
3
+ "name": "workspaces_getting_started_get",
4
+ "toolset": "workspaces",
5
+ "description": "Read the getting-started guide and which milestones are done.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ }
13
+ },
14
+ "required": [
15
+ "workspaceId"
16
+ ],
17
+ "additionalProperties": false
18
+ },
19
+ operation,
20
+ };
@@ -0,0 +1,27 @@
1
+ import { operation } from '../../commands/workspaces/getting-started-update.js';
2
+ export const tool = {
3
+ "name": "workspaces_getting_started_update",
4
+ "toolset": "workspaces",
5
+ "description": "Dismiss the guide (empty body or action=dismiss) or record the preview milestone.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "action": {
14
+ "type": "string",
15
+ "enum": [
16
+ "dismiss",
17
+ "preview_opened"
18
+ ]
19
+ }
20
+ },
21
+ "required": [
22
+ "workspaceId"
23
+ ],
24
+ "additionalProperties": false
25
+ },
26
+ operation,
27
+ };
@@ -0,0 +1,12 @@
1
+ import { operation } from '../../commands/workspaces/list.js';
2
+ export const tool = {
3
+ "name": "workspaces_list",
4
+ "toolset": "workspaces",
5
+ "description": "List the workspaces you belong to.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {},
9
+ "additionalProperties": false
10
+ },
11
+ operation,
12
+ };
@@ -0,0 +1,24 @@
1
+ import { operation } from '../../commands/workspaces/members.js';
2
+ export const tool = {
3
+ "name": "workspaces_members",
4
+ "toolset": "workspaces",
5
+ "description": "List the members of a workspace, and its pending invitations unless excluded.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "includeInvitations": {
14
+ "type": "boolean",
15
+ "description": "Include pending invitations. Defaults to true; only the literal 'false' turns it off."
16
+ }
17
+ },
18
+ "required": [
19
+ "workspaceId"
20
+ ],
21
+ "additionalProperties": false
22
+ },
23
+ operation,
24
+ };
@@ -0,0 +1,20 @@
1
+ import { operation } from '../../commands/workspaces/sandbox-limit.js';
2
+ export const tool = {
3
+ "name": "workspaces_sandbox_limit",
4
+ "toolset": "workspaces",
5
+ "description": "Read how many sandboxes may run at once and which sessions are running now.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ }
13
+ },
14
+ "required": [
15
+ "workspaceId"
16
+ ],
17
+ "additionalProperties": false
18
+ },
19
+ operation,
20
+ };
@@ -0,0 +1,31 @@
1
+ import { operation } from '../../commands/workspaces/stop-sandboxes.js';
2
+ export const tool = {
3
+ "name": "workspaces_stop_sandboxes",
4
+ "toolset": "workspaces",
5
+ "description": "Pause the sandboxes of the given chat sessions.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "sessionIds": {
14
+ "minItems": 1,
15
+ "maxItems": 50,
16
+ "type": "array",
17
+ "items": {
18
+ "type": "string",
19
+ "format": "uuid",
20
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
21
+ }
22
+ }
23
+ },
24
+ "required": [
25
+ "workspaceId",
26
+ "sessionIds"
27
+ ],
28
+ "additionalProperties": false
29
+ },
30
+ operation,
31
+ };
@@ -0,0 +1,28 @@
1
+ import { operation } from '../../commands/workspaces/usage.js';
2
+ export const tool = {
3
+ "name": "workspaces_usage",
4
+ "toolset": "workspaces",
5
+ "description": "Read sandbox usage by day and by project. start must precede end and the range is capped at 366 days.",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "workspaceId": {
10
+ "type": "string",
11
+ "description": "Workspace id."
12
+ },
13
+ "start": {
14
+ "type": "string",
15
+ "description": "Range start, any Date-parsable string. Default: end minus 30 days."
16
+ },
17
+ "end": {
18
+ "type": "string",
19
+ "description": "Range end, any Date-parsable string. Default: now."
20
+ }
21
+ },
22
+ "required": [
23
+ "workspaceId"
24
+ ],
25
+ "additionalProperties": false
26
+ },
27
+ operation,
28
+ };
package/dist/program.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { Command } from 'commander';
2
2
  import { attachAuthCommands } from './commands/auth.js';
3
+ import { attachMcpCommands } from './commands/mcp.js';
3
4
  import { operations } from './generated/commands/index.js';
4
5
  import { attachOperation } from './runtime/attach.js';
5
6
  import { createRuntime, ExitSignal } from './runtime/runtime.js';
6
7
  import { version } from './runtime/version.js';
7
8
  /**
8
- * The command tree: `auth` by hand, everything else from the generated
9
- * descriptors. Global flags are read before any subcommand runs, so the
9
+ * The command tree: `auth` and `mcp` by hand, everything else from the
10
+ * generated descriptors. Global flags are read before any subcommand runs, so the
10
11
  * runtime the subcommands share is built once, from them.
11
12
  */
12
13
  export function createProgram(options = {}) {
@@ -29,6 +30,7 @@ export function createProgram(options = {}) {
29
30
  output: (outcome) => runtimeFor().output(outcome),
30
31
  };
31
32
  attachAuthCommands(program, lazy, options.auth);
33
+ attachMcpCommands(program, lazy, options.mcp);
32
34
  for (const operation of operations)
33
35
  attachOperation(program, operation, lazy);
34
36
  return program;
@@ -0,0 +1,110 @@
1
+ import { closeSync, constants, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { RefusedArgument } from './mcp-guard.js';
5
+ /**
6
+ * Where an attachment may come from. On the CLI a person types the path; here
7
+ * the model chooses it, so a prompt that says "attach ~/.config/kosuke/credentials.json"
8
+ * would otherwise upload a token to the platform and into the transcript. Every
9
+ * path is resolved through its symlinks first, then held to one root, and a
10
+ * hidden segment (`.ssh`, `.aws`, `.config`, `.env`) is refused even inside it:
11
+ * those hold credentials far more often than screenshots.
12
+ *
13
+ * The bytes that leave are then read once, through a handle opened on the
14
+ * path that passed and checked to be a regular file, and staged as a private
15
+ * copy for the shared multipart code to pick up. Reading the original path
16
+ * again later would trust a name a concurrent writer could have re-pointed,
17
+ * and a FIFO or a device under the root would hang the server for good.
18
+ */
19
+ /** Mirrors the server's cap, which the shared attachment code enforces again. */
20
+ const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
21
+ export function defaultAttachmentPolicy(env = process.env) {
22
+ return { root: resolve(env.KOSUKE_MCP_ATTACHMENT_ROOT ?? process.cwd()) };
23
+ }
24
+ /** The real path of a file that may be attached, or a refusal that says why. */
25
+ export function confineAttachment(path, policy) {
26
+ const given = isAbsolute(path) ? path : resolve(policy.root, path);
27
+ let root;
28
+ let real;
29
+ try {
30
+ root = realpathSync(policy.root);
31
+ }
32
+ catch {
33
+ throw new RefusedArgument(`The attachment root ${policy.root} does not exist.`);
34
+ }
35
+ try {
36
+ real = realpathSync(given);
37
+ }
38
+ catch {
39
+ throw new RefusedArgument(`Attachment ${path} does not exist or cannot be read.`);
40
+ }
41
+ const inside = relative(root, real);
42
+ if (inside === '' || inside.startsWith('..') || isAbsolute(inside)) {
43
+ throw new RefusedArgument(`Attachment ${path} resolves to ${real}, outside the attachment root ${root}. ` +
44
+ `Only files under the root can be attached; start the server with ` +
45
+ `--attachment-root or KOSUKE_MCP_ATTACHMENT_ROOT to change it.`);
46
+ }
47
+ const hidden = inside.split(sep).find((segment) => segment.startsWith('.'));
48
+ if (hidden) {
49
+ throw new RefusedArgument(`Attachment ${path} is under a hidden path (${hidden}), which this server does not ` +
50
+ `attach: such paths hold credentials more often than screenshots.`);
51
+ }
52
+ // Checked before any open: opening a FIFO for reading blocks until a writer
53
+ // arrives, and a directory or a device is not something to upload either.
54
+ if (!statSync(real).isFile()) {
55
+ throw new RefusedArgument(`Attachment ${path} is not a regular file.`);
56
+ }
57
+ return real;
58
+ }
59
+ /**
60
+ * Read each confined file once through its own handle and stage a private
61
+ * copy under a fresh 0700 directory. The handle is what is checked (a regular
62
+ * file, within the size cap) and what is read, so a path swapped after the
63
+ * check changes nothing; the copy keeps the file's name, which is what the
64
+ * platform sees, and lives only for this one request.
65
+ */
66
+ export function stageAttachments(realPaths) {
67
+ if (!realPaths.length)
68
+ return { paths: [], cleanup: () => { } };
69
+ const directory = mkdtempSync(join(tmpdir(), 'kosuke-mcp-attachment-'));
70
+ const cleanup = () => rmSync(directory, { recursive: true, force: true });
71
+ try {
72
+ const paths = realPaths.map((real, index) => {
73
+ const bytes = readOnce(real);
74
+ const slot = join(directory, String(index));
75
+ mkdirSync(slot, { mode: 0o700 });
76
+ const copy = join(slot, basename(real));
77
+ writeFileSync(copy, bytes, { mode: 0o600 });
78
+ return copy;
79
+ });
80
+ return { paths, cleanup };
81
+ }
82
+ catch (error) {
83
+ cleanup();
84
+ throw error;
85
+ }
86
+ }
87
+ function readOnce(real) {
88
+ // O_NOFOLLOW: the leaf must still be the file realpath saw, not a symlink
89
+ // planted since. O_NONBLOCK: should it have become a FIFO, open returns
90
+ // instead of waiting, and fstat refuses it below.
91
+ let fd;
92
+ try {
93
+ fd = openSync(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
94
+ }
95
+ catch {
96
+ throw new RefusedArgument(`Attachment ${real} cannot be opened.`);
97
+ }
98
+ try {
99
+ const stat = fstatSync(fd);
100
+ if (!stat.isFile())
101
+ throw new RefusedArgument(`Attachment ${real} is not a regular file.`);
102
+ if (stat.size > MAX_ATTACHMENT_BYTES) {
103
+ throw new RefusedArgument(`Attachment ${real} is larger than the 10 MiB attachment limit.`);
104
+ }
105
+ return readFileSync(fd);
106
+ }
107
+ finally {
108
+ closeSync(fd);
109
+ }
110
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What a composite tool is made of: the contract the server gives it, and the
3
+ * small readings of an invocation the three tools share. Kept apart from the
4
+ * tools themselves so the open-pr helpers can use them without a cycle.
5
+ */
6
+ export const COMPOSITE_TOOLSET = 'composite';
7
+ export function pick(args, keys) {
8
+ return Object.fromEntries(keys.filter((key) => args[key] !== undefined).map((k) => [k, args[k]]));
9
+ }
10
+ export function data(invocation) {
11
+ const body = invocation.body;
12
+ const value = body?.data;
13
+ return typeof value === 'object' && value !== null ? value : {};
14
+ }
15
+ export function tail(messages, count = 3) {
16
+ if (!Array.isArray(messages))
17
+ return [];
18
+ return messages.slice(-count).map((m) => ({ role: m.role, content: messageText(m) }));
19
+ }
20
+ /** Blocks whose `content` is prose addressed to the reader; tool and plan blocks are not. */
21
+ const PROSE_BLOCKS = new Set(['text', 'clarification']);
22
+ /**
23
+ * The text of a message. A user message carries it in `content`; an
24
+ * assistant message carries `content: null` and its text in `blocks`, beside
25
+ * tool blocks. Prose blocks joined in order, so a clarification question is
26
+ * what comes back for a job that is awaiting one; `content` as the fallback;
27
+ * null when there is no text at all.
28
+ */
29
+ export function messageText(message) {
30
+ const blocks = Array.isArray(message.blocks) ? message.blocks : [];
31
+ const text = blocks
32
+ .filter((block) => PROSE_BLOCKS.has(String(block.type)) && typeof block.content === 'string')
33
+ .map((block) => block.content)
34
+ .join('\n\n')
35
+ .trim();
36
+ if (text)
37
+ return text;
38
+ return typeof message.content === 'string' && message.content.trim() ? message.content : null;
39
+ }
40
+ export function done(last, payload, warnings = last.warnings) {
41
+ return { ok: true, status: last.status, body: { data: payload }, warnings };
42
+ }
43
+ /** A step that failed: its error, named, so the model knows how far it got. */
44
+ export function failed(step, name) {
45
+ const body = typeof step.body === 'object' && step.body !== null
46
+ ? { step: name, ...step.body }
47
+ : { step: name, error: step.body ?? `HTTP ${step.status}` };
48
+ return { ...step, body };
49
+ }