@kammcs/buildit-mcp 0.2.0 → 0.8.0
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/CHANGELOG.md +149 -0
- package/README.md +273 -32
- package/SECURITY.md +1 -1
- package/dist/api/client.js +5 -1
- package/dist/api/generated/operations.js +840 -11
- package/dist/api/generated/schemas.js +936 -3
- package/dist/api/generated/strict.js +1088 -3
- package/dist/capabilities.js +236 -0
- package/dist/channel/api.js +159 -0
- package/dist/channel/approvals.js +259 -0
- package/dist/channel/handoffs.js +190 -0
- package/dist/channel/item-events.js +164 -0
- package/dist/channel/lock.js +181 -0
- package/dist/channel/loop.js +1034 -0
- package/dist/channel/mask.js +365 -0
- package/dist/channel/threads.js +60 -0
- package/dist/channel/tool-ring.js +124 -0
- package/dist/channel/typing.js +150 -0
- package/dist/channel/waiters.js +229 -0
- package/dist/channel/workers.js +66 -0
- package/dist/cli/claude-config.js +100 -0
- package/dist/cli/commands.js +438 -0
- package/dist/config.js +356 -6
- package/dist/errors.js +49 -1
- package/dist/index.js +122 -6
- package/dist/log.js +3 -2
- package/dist/secrets/keychain.js +273 -0
- package/dist/server.js +61 -5
- package/dist/tools/approve.js +791 -0
- package/dist/tools/ask.js +898 -0
- package/dist/tools/chat.js +98 -1
- package/dist/tools/handoff.js +409 -0
- package/dist/tools/items-read.js +6 -3
- package/dist/tools/items-write.js +77 -16
- package/dist/tools/plans.js +221 -14
- package/dist/tools/reply.js +138 -0
- package/dist/tools/shared.js +24 -1
- package/dist/tools/threads.js +149 -0
- package/dist/tools/whoami.js +53 -3
- package/dist/toolsets/catalog.js +3 -1
- package/dist/toolsets/registry.js +2 -0
- package/dist/transports/stdio.js +288 -12
- package/package.json +5 -1
package/dist/config.js
CHANGED
|
@@ -5,25 +5,46 @@
|
|
|
5
5
|
* environment (a flag would be visible to other processes), and no message
|
|
6
6
|
* produced here ever contains it.
|
|
7
7
|
*/
|
|
8
|
+
import { basename, resolve } from 'node:path';
|
|
8
9
|
import { parseArgs } from 'node:util';
|
|
10
|
+
import { parseCapabilitiesOption } from './capabilities.js';
|
|
9
11
|
import { LOG_LEVELS, scrubString } from './log.js';
|
|
10
12
|
import { DEFAULT_TOOLSETS, isToolsetName, TOOLSET_NAMES, } from './toolsets/toolsets.js';
|
|
11
13
|
export const DEFAULT_API_URL = 'https://api.buildit.social/functions/v1/agent-api';
|
|
12
14
|
export const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
13
15
|
export const DEFAULT_HTTP_PORT = 8765;
|
|
16
|
+
export const DEFAULT_ASK_TIMEOUT_MINUTES = 60;
|
|
17
|
+
export const DEFAULT_APPROVAL_TIMEOUT_MINUTES = 60;
|
|
18
|
+
export const DEFAULT_MAX_PARALLEL_ITEMS = 3;
|
|
14
19
|
export class ConfigError extends Error {
|
|
15
20
|
name = 'ConfigError';
|
|
16
21
|
}
|
|
17
22
|
export const USAGE = `Usage: buildit-mcp [options]
|
|
23
|
+
buildit-mcp connect --project <KEY> [--label <name>] [--folder <path>] [--api-url <url>]
|
|
24
|
+
buildit-mcp disconnect [--folder <path>] [--api-url <url>]
|
|
25
|
+
buildit-mcp status [--folder <path>] [--api-url <url>]
|
|
18
26
|
|
|
19
27
|
An MCP server for buildIt.Social. By default it serves one person over stdio,
|
|
20
28
|
with the personal access token in the BUILDIT_TOKEN environment variable.
|
|
29
|
+
With --channel it is also a Claude Code channel: the messages its owner sends
|
|
30
|
+
to the agent in buildIt.Social arrive in the Claude Code session, which
|
|
31
|
+
answers with the reply tool. Its token is the session token that
|
|
32
|
+
"buildit-mcp connect" saved for the folder (or BUILDIT_TOKEN).
|
|
33
|
+
|
|
34
|
+
Commands (run them in a terminal, in the folder where Claude Code runs):
|
|
35
|
+
connect Connect this folder to buildIt.Social as an agent
|
|
36
|
+
session: shows a code to approve in the app, then
|
|
37
|
+
saves the session token in the OS keychain
|
|
38
|
+
disconnect Forget this folder's saved session
|
|
39
|
+
status Show this folder's session
|
|
40
|
+
Run "buildit-mcp <command> --help" for a command's options.
|
|
21
41
|
|
|
22
42
|
Options (each also has an environment variable):
|
|
23
43
|
--api-url <url> BUILDIT_API_URL Agent API base URL
|
|
24
44
|
(default ${DEFAULT_API_URL})
|
|
25
45
|
--toolsets <list> BUILDIT_TOOLSETS Comma-separated toolsets, or "all"
|
|
26
|
-
(default ${DEFAULT_TOOLSETS.join(',')}
|
|
46
|
+
(default ${DEFAULT_TOOLSETS.join(',')}, or all
|
|
47
|
+
with --channel;
|
|
27
48
|
known: ${TOOLSET_NAMES.join(', ')})
|
|
28
49
|
--read-only BUILDIT_READ_ONLY=true Only list tools that change nothing
|
|
29
50
|
--exclude-tools <list> BUILDIT_EXCLUDE_TOOLS Comma-separated tool names to hide
|
|
@@ -33,6 +54,29 @@ Options (each also has an environment variable):
|
|
|
33
54
|
--port <port> BUILDIT_PORT HTTP port (default ${DEFAULT_HTTP_PORT})
|
|
34
55
|
--allowed-hosts <list> BUILDIT_ALLOWED_HOSTS Hostnames accepted in the Host header
|
|
35
56
|
--allowed-origins <list> BUILDIT_ALLOWED_ORIGINS Hostnames accepted in the Origin header
|
|
57
|
+
--channel BUILDIT_CHANNEL=true Claude Code channel mode (stdio only)
|
|
58
|
+
--folder <path> BUILDIT_FOLDER The folder whose saved session --channel uses
|
|
59
|
+
(default: the working directory)
|
|
60
|
+
--from-plugin --channel: started by the buildit plugin, which
|
|
61
|
+
Claude Code registers as the channel (the
|
|
62
|
+
plugin passes it)
|
|
63
|
+
BUILDIT_QUESTIONS_IN_APP --channel: the plugin's question hook asks in
|
|
64
|
+
buildIt.Social (default true)
|
|
65
|
+
BUILDIT_ASK_TIMEOUT_MINUTES
|
|
66
|
+
--channel: how long a question card waits,
|
|
67
|
+
1 to 60 (default 60)
|
|
68
|
+
BUILDIT_MAX_PARALLEL_ITEMS
|
|
69
|
+
--channel with the plugin: how many work items
|
|
70
|
+
are worked on at once, 1 to 10 (default 3)
|
|
71
|
+
BUILDIT_APPROVALS_IN_APP --channel: the plugin's permission hook asks to
|
|
72
|
+
approve tool calls in buildIt.Social too
|
|
73
|
+
(default true)
|
|
74
|
+
BUILDIT_APPROVAL_TIMEOUT_MINUTES
|
|
75
|
+
--channel: how long a tool approval card waits,
|
|
76
|
+
1 to 60 (default 60)
|
|
77
|
+
BUILDIT_CAPABILITIES --channel: the capability tags the session
|
|
78
|
+
suggests when it starts (macos,ios, or none),
|
|
79
|
+
instead of detecting them
|
|
36
80
|
--allow-any-host BUILDIT_ALLOW_ANY_HOST=true
|
|
37
81
|
Off loopback, serve without checking the
|
|
38
82
|
Host header (otherwise BUILDIT_ALLOWED_HOSTS
|
|
@@ -43,6 +87,60 @@ Options (each also has an environment variable):
|
|
|
43
87
|
The token is never accepted as a flag. In stdio mode set BUILDIT_TOKEN; in HTTP
|
|
44
88
|
mode every request carries its own "Authorization: Bearer <token>" header.
|
|
45
89
|
`;
|
|
90
|
+
export const COMMAND_USAGE = {
|
|
91
|
+
connect: `Usage: buildit-mcp connect --project <KEY> [options]
|
|
92
|
+
|
|
93
|
+
Connects this folder to buildIt.Social as a session of your personal agent,
|
|
94
|
+
for Claude Code's channel mode (buildit-mcp --channel). It shows a code and a
|
|
95
|
+
link: open the link, check the code, and approve in buildIt.Social. The
|
|
96
|
+
session token is then saved in the OS keychain for this folder and API.
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
--project <KEY> A project the session may reach (repeat it, or
|
|
100
|
+
separate keys with commas; 1 to 10)
|
|
101
|
+
--label <name> The session's name in buildIt.Social, 1 to 40
|
|
102
|
+
characters (default: the folder's name)
|
|
103
|
+
--folder <path> The folder (default: the working directory)
|
|
104
|
+
--api-url <url> BUILDIT_API_URL Agent API base URL
|
|
105
|
+
(default ${DEFAULT_API_URL})
|
|
106
|
+
--capabilities <tags> The capability tags to suggest (macos,ios), or
|
|
107
|
+
none, instead of detecting them. Detection looks
|
|
108
|
+
at the operating system and for known programs
|
|
109
|
+
(flutter, docker, cargo, adb, Xcode) without
|
|
110
|
+
running them, and sends only the tag names
|
|
111
|
+
--yes Save without asking "Is this you?" once it is
|
|
112
|
+
approved. For scripts only: unsafe if anyone else
|
|
113
|
+
could have seen the code, since whoever approves
|
|
114
|
+
it gets the session
|
|
115
|
+
-h, --help Show this help
|
|
116
|
+
|
|
117
|
+
Once approved, it says who approved it (the agent, its owner, the org, the
|
|
118
|
+
projects) and asks "Is this you?"; only "y" saves the token, and any other
|
|
119
|
+
answer cancels the session.
|
|
120
|
+
`,
|
|
121
|
+
disconnect: `Usage: buildit-mcp disconnect [options]
|
|
122
|
+
|
|
123
|
+
Cancels this folder's session in buildIt.Social and forgets its saved token.
|
|
124
|
+
If buildIt.Social can't be reached, the token is forgotten anyway: revoke
|
|
125
|
+
the session in the app then.
|
|
126
|
+
|
|
127
|
+
Options:
|
|
128
|
+
--folder <path> The folder (default: the working directory)
|
|
129
|
+
--api-url <url> BUILDIT_API_URL Agent API base URL
|
|
130
|
+
-h, --help Show this help
|
|
131
|
+
`,
|
|
132
|
+
status: `Usage: buildit-mcp status [options]
|
|
133
|
+
|
|
134
|
+
Shows the session saved for this folder: its agent, name, org and reach, and
|
|
135
|
+
whether buildIt.Social still accepts it.
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
--folder <path> The folder (default: the working directory)
|
|
139
|
+
--api-url <url> BUILDIT_API_URL Agent API base URL
|
|
140
|
+
-h, --help Show this help
|
|
141
|
+
`,
|
|
142
|
+
};
|
|
143
|
+
const SESSION_COMMANDS = ['connect', 'disconnect', 'status'];
|
|
46
144
|
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
47
145
|
const FALSE_VALUES = new Set(['0', 'false', 'no', 'off', '']);
|
|
48
146
|
export function parseBoolean(name, raw, fallback) {
|
|
@@ -55,6 +153,73 @@ export function parseBoolean(name, raw, fallback) {
|
|
|
55
153
|
return false;
|
|
56
154
|
throw new ConfigError(`${name} must be true or false (got "${raw}").`);
|
|
57
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The plugin's settings arrive as environment variables it fills in, so a
|
|
158
|
+
* value that is empty or doesn't parse keeps the default, with a warning,
|
|
159
|
+
* instead of stopping the server.
|
|
160
|
+
*/
|
|
161
|
+
function parseAsk(env, warnings) {
|
|
162
|
+
let questionsInApp = true;
|
|
163
|
+
const rawIn = env.BUILDIT_QUESTIONS_IN_APP?.trim() ?? '';
|
|
164
|
+
if (rawIn !== '') {
|
|
165
|
+
const value = rawIn.toLowerCase();
|
|
166
|
+
if (TRUE_VALUES.has(value))
|
|
167
|
+
questionsInApp = true;
|
|
168
|
+
else if (FALSE_VALUES.has(value))
|
|
169
|
+
questionsInApp = false;
|
|
170
|
+
else
|
|
171
|
+
warnings.push('BUILDIT_QUESTIONS_IN_APP is not true or false; questions go to buildIt.Social.');
|
|
172
|
+
}
|
|
173
|
+
let timeoutMinutes = DEFAULT_ASK_TIMEOUT_MINUTES;
|
|
174
|
+
const rawMinutes = env.BUILDIT_ASK_TIMEOUT_MINUTES?.trim() ?? '';
|
|
175
|
+
if (rawMinutes !== '') {
|
|
176
|
+
const n = Number(rawMinutes);
|
|
177
|
+
if (Number.isFinite(n) && n >= 1 && n <= 60) {
|
|
178
|
+
timeoutMinutes = Math.floor(n);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
warnings.push(`BUILDIT_ASK_TIMEOUT_MINUTES must be 1 to 60; using ${DEFAULT_ASK_TIMEOUT_MINUTES}.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let maxParallelItems = DEFAULT_MAX_PARALLEL_ITEMS;
|
|
185
|
+
const rawParallel = env.BUILDIT_MAX_PARALLEL_ITEMS?.trim() ?? '';
|
|
186
|
+
if (rawParallel !== '') {
|
|
187
|
+
const n = Number(rawParallel);
|
|
188
|
+
if (Number.isFinite(n) && n >= 1 && n <= 10) {
|
|
189
|
+
maxParallelItems = Math.floor(n);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
warnings.push(`BUILDIT_MAX_PARALLEL_ITEMS must be 1 to 10; using ${DEFAULT_MAX_PARALLEL_ITEMS}.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { questionsInApp, timeoutMinutes, maxParallelItems };
|
|
196
|
+
}
|
|
197
|
+
/** The tool approval settings, read as the question settings are (a bad value keeps the default). */
|
|
198
|
+
function parseApprovals(env, warnings) {
|
|
199
|
+
let inApp = true;
|
|
200
|
+
const rawIn = env.BUILDIT_APPROVALS_IN_APP?.trim() ?? '';
|
|
201
|
+
if (rawIn !== '') {
|
|
202
|
+
const value = rawIn.toLowerCase();
|
|
203
|
+
if (TRUE_VALUES.has(value))
|
|
204
|
+
inApp = true;
|
|
205
|
+
else if (FALSE_VALUES.has(value))
|
|
206
|
+
inApp = false;
|
|
207
|
+
else
|
|
208
|
+
warnings.push('BUILDIT_APPROVALS_IN_APP is not true or false; tool approvals go to buildIt.Social.');
|
|
209
|
+
}
|
|
210
|
+
let timeoutMinutes = DEFAULT_APPROVAL_TIMEOUT_MINUTES;
|
|
211
|
+
const rawMinutes = env.BUILDIT_APPROVAL_TIMEOUT_MINUTES?.trim() ?? '';
|
|
212
|
+
if (rawMinutes !== '') {
|
|
213
|
+
const n = Number(rawMinutes);
|
|
214
|
+
if (Number.isFinite(n) && n >= 1 && n <= 60) {
|
|
215
|
+
timeoutMinutes = Math.floor(n);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
warnings.push(`BUILDIT_APPROVAL_TIMEOUT_MINUTES must be 1 to 60; using ${DEFAULT_APPROVAL_TIMEOUT_MINUTES}.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { inApp, timeoutMinutes };
|
|
222
|
+
}
|
|
58
223
|
function splitList(raw) {
|
|
59
224
|
return [
|
|
60
225
|
...new Set(raw
|
|
@@ -90,6 +255,24 @@ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
|
90
255
|
export function isLoopbackHost(host) {
|
|
91
256
|
return LOOPBACK_HOSTS.has(host.toLowerCase());
|
|
92
257
|
}
|
|
258
|
+
/** The agent API's path on a buildIt.Social host. */
|
|
259
|
+
export const AGENT_API_PATH = '/functions/v1/agent-api';
|
|
260
|
+
/**
|
|
261
|
+
* The note shown once when an API address is a bare host (no path), which
|
|
262
|
+
* parseApiUrl completes with the agent API's path; undefined otherwise.
|
|
263
|
+
*/
|
|
264
|
+
export function apiUrlNote(raw) {
|
|
265
|
+
if (raw === undefined)
|
|
266
|
+
return undefined;
|
|
267
|
+
try {
|
|
268
|
+
if (new URL(raw.trim()).pathname !== '/')
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
return `The API address ${raw.trim()} has no path, so buildit-mcp uses ${parseApiUrl(raw)}, the agent API on that host.`;
|
|
275
|
+
}
|
|
93
276
|
export function parseApiUrl(raw) {
|
|
94
277
|
let url;
|
|
95
278
|
try {
|
|
@@ -108,6 +291,9 @@ export function parseApiUrl(raw) {
|
|
|
108
291
|
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
|
|
109
292
|
throw new ConfigError('BUILDIT_API_URL must use https:// (plain http:// is only allowed for localhost).');
|
|
110
293
|
}
|
|
294
|
+
// A bare host means its agent API.
|
|
295
|
+
if (url.pathname === '/')
|
|
296
|
+
url.pathname = AGENT_API_PATH;
|
|
111
297
|
return url.toString().replace(/\/+$/, '');
|
|
112
298
|
}
|
|
113
299
|
const HOSTNAME = /^(\[[0-9a-f:.]+\]|[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*)$/;
|
|
@@ -166,15 +352,134 @@ function parseLogLevel(raw) {
|
|
|
166
352
|
function nonEmpty(value) {
|
|
167
353
|
return value === undefined || value.trim() === '' ? undefined : value;
|
|
168
354
|
}
|
|
355
|
+
const PROJECT_KEY = /^[A-Za-z][A-Za-z0-9]{1,5}$/;
|
|
356
|
+
export const LABEL_MAX_CHARS = 40;
|
|
357
|
+
/** Control, direction and zero-width characters (a RegExp character class), which a session label may not hold. */
|
|
358
|
+
export const FORBIDDEN_CHARS = '[\\u0000-\\u001F\\u007F-\\u009F\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2069\\uFEFF]';
|
|
359
|
+
/** Project keys from --project flags (each may list several, comma-separated). */
|
|
360
|
+
export function parseProjects(values) {
|
|
361
|
+
const keys = [...new Set(values.flatMap((v) => splitList(v)).map((k) => k.toUpperCase()))];
|
|
362
|
+
if (keys.length === 0) {
|
|
363
|
+
throw new ConfigError('Name the project the session may reach with --project <KEY>, for example --project DEMO.');
|
|
364
|
+
}
|
|
365
|
+
const bad = keys.filter((k) => !PROJECT_KEY.test(k));
|
|
366
|
+
if (bad.length > 0) {
|
|
367
|
+
throw new ConfigError(`Not a project key: ${bad.join(', ')}. Keys are 2 to 6 letters and digits, starting with a letter (for example DEMO).`);
|
|
368
|
+
}
|
|
369
|
+
if (keys.length > 10)
|
|
370
|
+
throw new ConfigError('A session may reach at most 10 projects.');
|
|
371
|
+
return keys;
|
|
372
|
+
}
|
|
373
|
+
/** --capabilities or BUILDIT_CAPABILITIES, checked (ConfigError names what is wrong). */
|
|
374
|
+
function capabilitiesOption(raw, what) {
|
|
375
|
+
try {
|
|
376
|
+
return parseCapabilitiesOption(raw);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
throw new ConfigError(`${what}: ${err instanceof Error ? err.message : String(err)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** The --label given, checked; or the folder's name, made to fit. */
|
|
383
|
+
export function sessionLabel(raw, folder) {
|
|
384
|
+
if (raw !== undefined) {
|
|
385
|
+
const label = raw.trim();
|
|
386
|
+
if (label.length === 0 || Array.from(label).length > LABEL_MAX_CHARS) {
|
|
387
|
+
throw new ConfigError(`The label must be 1 to ${LABEL_MAX_CHARS} characters.`);
|
|
388
|
+
}
|
|
389
|
+
if (new RegExp(FORBIDDEN_CHARS, 'u').test(label)) {
|
|
390
|
+
throw new ConfigError('The label may not hold control, direction or invisible characters.');
|
|
391
|
+
}
|
|
392
|
+
return label;
|
|
393
|
+
}
|
|
394
|
+
const name = Array.from(basename(folder).replace(new RegExp(FORBIDDEN_CHARS, 'gu'), '').trim())
|
|
395
|
+
.slice(0, LABEL_MAX_CHARS)
|
|
396
|
+
.join('')
|
|
397
|
+
.trim();
|
|
398
|
+
return name.length > 0 ? name : 'session';
|
|
399
|
+
}
|
|
400
|
+
/** The folder a session belongs to: --folder, else BUILDIT_FOLDER, else the working directory. */
|
|
401
|
+
function folderOf(flag, env, cwd) {
|
|
402
|
+
const raw = typeof flag === 'string' ? flag : nonEmpty(env.BUILDIT_FOLDER);
|
|
403
|
+
if (raw !== undefined && raw.trim() === '')
|
|
404
|
+
throw new ConfigError('--folder must not be empty.');
|
|
405
|
+
return resolve(cwd, raw?.trim() ?? '.');
|
|
406
|
+
}
|
|
407
|
+
function parseSessionCommand(command, argv, env, cwd, secrets) {
|
|
408
|
+
let values;
|
|
409
|
+
try {
|
|
410
|
+
({ values } = parseArgs({
|
|
411
|
+
args: [...argv],
|
|
412
|
+
strict: true,
|
|
413
|
+
allowPositionals: false,
|
|
414
|
+
options: {
|
|
415
|
+
'api-url': { type: 'string' },
|
|
416
|
+
folder: { type: 'string' },
|
|
417
|
+
help: { type: 'boolean', short: 'h' },
|
|
418
|
+
...(command === 'connect'
|
|
419
|
+
? {
|
|
420
|
+
project: { type: 'string', multiple: true },
|
|
421
|
+
label: { type: 'string' },
|
|
422
|
+
capabilities: { type: 'string' },
|
|
423
|
+
yes: { type: 'boolean' },
|
|
424
|
+
}
|
|
425
|
+
: {}),
|
|
426
|
+
},
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
431
|
+
throw new ConfigError(`${scrubString(message, secrets)} Run "buildit-mcp ${command} --help" for the options.`);
|
|
432
|
+
}
|
|
433
|
+
if (values.help === true)
|
|
434
|
+
return { kind: 'help', command };
|
|
435
|
+
const apiFlag = values['api-url'];
|
|
436
|
+
const apiRaw = (typeof apiFlag === 'string' ? apiFlag : nonEmpty(env.BUILDIT_API_URL)) ?? DEFAULT_API_URL;
|
|
437
|
+
const apiUrl = parseApiUrl(apiRaw);
|
|
438
|
+
const note = apiUrlNote(apiRaw);
|
|
439
|
+
const folder = folderOf(values.folder, env, cwd);
|
|
440
|
+
if (command !== 'connect') {
|
|
441
|
+
return {
|
|
442
|
+
kind: command,
|
|
443
|
+
options: {
|
|
444
|
+
apiUrl,
|
|
445
|
+
folder,
|
|
446
|
+
apiUrlFlag: typeof apiFlag === 'string',
|
|
447
|
+
...(note === undefined ? {} : { apiUrlNote: note }),
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const projectFlags = values.project;
|
|
452
|
+
const projects = parseProjects(Array.isArray(projectFlags) ? projectFlags : []);
|
|
453
|
+
const label = sessionLabel(typeof values.label === 'string' ? values.label : undefined, folder);
|
|
454
|
+
const capabilities = typeof values.capabilities === 'string'
|
|
455
|
+
? capabilitiesOption(values.capabilities, '--capabilities')
|
|
456
|
+
: undefined;
|
|
457
|
+
return {
|
|
458
|
+
kind: 'connect',
|
|
459
|
+
options: {
|
|
460
|
+
apiUrl,
|
|
461
|
+
folder,
|
|
462
|
+
projects,
|
|
463
|
+
label,
|
|
464
|
+
yes: values.yes === true,
|
|
465
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
466
|
+
...(note === undefined ? {} : { apiUrlNote: note }),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
169
470
|
/**
|
|
170
471
|
* Parses flags and environment variables. Throws ConfigError with a message
|
|
171
472
|
* that is safe to print (it never contains the token).
|
|
172
473
|
*/
|
|
173
|
-
export function parseCli(argv, env) {
|
|
474
|
+
export function parseCli(argv, env, cwd = process.cwd()) {
|
|
174
475
|
const secrets = env.BUILDIT_TOKEN ? [env.BUILDIT_TOKEN] : [];
|
|
175
476
|
if (argv.some((a) => a === '--token' || a.startsWith('--token='))) {
|
|
176
477
|
throw new ConfigError('The token is never accepted as a flag (other processes can read flags). Set the BUILDIT_TOKEN environment variable instead.');
|
|
177
478
|
}
|
|
479
|
+
const command = SESSION_COMMANDS.find((c) => c === argv[0]);
|
|
480
|
+
if (command !== undefined) {
|
|
481
|
+
return parseSessionCommand(command, argv.slice(1), env, cwd, secrets);
|
|
482
|
+
}
|
|
178
483
|
let values;
|
|
179
484
|
try {
|
|
180
485
|
({ values } = parseArgs({
|
|
@@ -193,6 +498,9 @@ export function parseCli(argv, env) {
|
|
|
193
498
|
'allowed-hosts': { type: 'string' },
|
|
194
499
|
'allowed-origins': { type: 'string' },
|
|
195
500
|
'allow-any-host': { type: 'boolean' },
|
|
501
|
+
channel: { type: 'boolean' },
|
|
502
|
+
folder: { type: 'string' },
|
|
503
|
+
'from-plugin': { type: 'boolean' },
|
|
196
504
|
help: { type: 'boolean', short: 'h' },
|
|
197
505
|
version: { type: 'boolean', short: 'v' },
|
|
198
506
|
},
|
|
@@ -213,9 +521,23 @@ export function parseCli(argv, env) {
|
|
|
213
521
|
const warnings = [];
|
|
214
522
|
const http = values.http === true || parseBoolean('BUILDIT_HTTP', env.BUILDIT_HTTP, false);
|
|
215
523
|
const mode = http ? 'http' : 'stdio';
|
|
216
|
-
const
|
|
524
|
+
const channelMode = values.channel === true || parseBoolean('BUILDIT_CHANNEL', env.BUILDIT_CHANNEL, false);
|
|
525
|
+
if (channelMode && mode === 'http') {
|
|
526
|
+
throw new ConfigError('Channel mode (--channel) runs over stdio only: Claude Code starts it as a local server.');
|
|
527
|
+
}
|
|
528
|
+
const apiRaw = str('api-url', 'BUILDIT_API_URL') ?? DEFAULT_API_URL;
|
|
529
|
+
const apiUrl = parseApiUrl(apiRaw);
|
|
530
|
+
const apiNote = apiUrlNote(apiRaw);
|
|
531
|
+
if (apiNote !== undefined)
|
|
532
|
+
warnings.push(apiNote);
|
|
217
533
|
const toolsetsRaw = str('toolsets', 'BUILDIT_TOOLSETS');
|
|
218
|
-
|
|
534
|
+
// Channel mode turns every toolset on by default: approvals need destructive and admin (the
|
|
535
|
+
// session's own scopes still limit what works).
|
|
536
|
+
const toolsets = toolsetsRaw === undefined
|
|
537
|
+
? channelMode
|
|
538
|
+
? [...TOOLSET_NAMES]
|
|
539
|
+
: [...DEFAULT_TOOLSETS]
|
|
540
|
+
: parseToolsets(toolsetsRaw);
|
|
219
541
|
const readOnly = values['read-only'] === true || parseBoolean('BUILDIT_READ_ONLY', env.BUILDIT_READ_ONLY, false);
|
|
220
542
|
const excludeRaw = str('exclude-tools', 'BUILDIT_EXCLUDE_TOOLS');
|
|
221
543
|
const excludeTools = excludeRaw === undefined ? [] : parseToolNames(excludeRaw, 'BUILDIT_EXCLUDE_TOOLS');
|
|
@@ -236,8 +558,33 @@ export function parseCli(argv, env) {
|
|
|
236
558
|
httpConfig.allowedOrigins = parseHostList(originsRaw, 'BUILDIT_ALLOWED_ORIGINS', true);
|
|
237
559
|
}
|
|
238
560
|
let token;
|
|
561
|
+
let channel;
|
|
239
562
|
const rawToken = env.BUILDIT_TOKEN;
|
|
240
|
-
if (
|
|
563
|
+
if (channelMode) {
|
|
564
|
+
channel = {
|
|
565
|
+
folder: folderOf(values.folder, env, cwd),
|
|
566
|
+
tokenSource: 'store',
|
|
567
|
+
apiUrlFlag: typeof values['api-url'] === 'string',
|
|
568
|
+
fromPlugin: values['from-plugin'] === true || nonEmpty(env.CLAUDE_PLUGIN_ROOT) !== undefined,
|
|
569
|
+
};
|
|
570
|
+
const rawCapabilities = nonEmpty(env.BUILDIT_CAPABILITIES);
|
|
571
|
+
if (rawCapabilities !== undefined) {
|
|
572
|
+
channel.capabilities = capabilitiesOption(rawCapabilities, 'BUILDIT_CAPABILITIES');
|
|
573
|
+
}
|
|
574
|
+
const envToken = rawToken?.trim() ?? '';
|
|
575
|
+
if (envToken.startsWith('buildit_ags_')) {
|
|
576
|
+
if (/\s/.test(envToken)) {
|
|
577
|
+
throw new ConfigError('BUILDIT_TOKEN contains whitespace; check that it was copied whole.');
|
|
578
|
+
}
|
|
579
|
+
token = envToken;
|
|
580
|
+
channel.tokenSource = 'env';
|
|
581
|
+
}
|
|
582
|
+
else if (envToken !== '') {
|
|
583
|
+
// A user-level personal access token from the usual setup: the folder's session wins.
|
|
584
|
+
warnings.push('BUILDIT_TOKEN is not a session token, so channel mode ignores it and uses the session that "buildit-mcp connect" saved for the folder.');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (mode === 'stdio') {
|
|
241
588
|
if (rawToken === undefined || rawToken.trim() === '') {
|
|
242
589
|
throw new ConfigError('BUILDIT_TOKEN is not set. Create a personal access token in buildIt.Social and put it in the BUILDIT_TOKEN environment variable (never in a file you commit).');
|
|
243
590
|
}
|
|
@@ -245,7 +592,7 @@ export function parseCli(argv, env) {
|
|
|
245
592
|
if (/\s/.test(token)) {
|
|
246
593
|
throw new ConfigError('BUILDIT_TOKEN contains whitespace; check that it was copied whole.');
|
|
247
594
|
}
|
|
248
|
-
if (!token.startsWith('buildit_pat_')) {
|
|
595
|
+
if (!token.startsWith('buildit_pat_') && !token.startsWith('buildit_ags_')) {
|
|
249
596
|
warnings.push('BUILDIT_TOKEN does not start with "buildit_pat_"; buildIt.Social personal access tokens do.');
|
|
250
597
|
}
|
|
251
598
|
}
|
|
@@ -269,6 +616,9 @@ export function parseCli(argv, env) {
|
|
|
269
616
|
mode,
|
|
270
617
|
apiUrl,
|
|
271
618
|
...(token === undefined ? {} : { token }),
|
|
619
|
+
...(channel === undefined ? {} : { channel }),
|
|
620
|
+
ask: parseAsk(env, warnings),
|
|
621
|
+
approvals: parseApprovals(env, warnings),
|
|
272
622
|
toolsets,
|
|
273
623
|
readOnly,
|
|
274
624
|
excludeTools,
|
package/dist/errors.js
CHANGED
|
@@ -73,6 +73,15 @@ const NOT_FOUND_HINTS = {
|
|
|
73
73
|
plan: 'Use a handle a propose_* tool returned in this conversation, or propose the change again.',
|
|
74
74
|
route: 'The API does not serve this call: it may be older than this buildit-mcp. Check BUILDIT_API_URL, or use another tool.',
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* The next step for codes whose contract hint names an API operation this
|
|
78
|
+
* server has no tool for (get_session_handoff): said in this server's terms
|
|
79
|
+
* instead of the API's hint.
|
|
80
|
+
*/
|
|
81
|
+
const SERVER_HINTS = {
|
|
82
|
+
handoff_exists: "Don't propose it again: the subtask already has a live handoff (details.handoff_id), whose outcome arrives as event=handoff_resolved.",
|
|
83
|
+
handoff_closed: 'Nothing more to do with this handoff: its card has already ended.',
|
|
84
|
+
};
|
|
76
85
|
/**
|
|
77
86
|
* Each of the contract's rate-limit buckets, in plain words. A bucket the
|
|
78
87
|
* contract adds later is shown by retry_after alone, as the contract asks.
|
|
@@ -83,6 +92,13 @@ const RATE_BUCKETS = {
|
|
|
83
92
|
token_writes_per_day: 'writes per day for this token',
|
|
84
93
|
org_requests_per_minute: 'requests per minute for the whole org, across all its tokens',
|
|
85
94
|
user_requests_per_minute: 'requests per minute for your account, across all your tokens',
|
|
95
|
+
session_polls_per_minute: 'polls per minute for this session',
|
|
96
|
+
session_typing_per_minute: 'typing notices per minute for this session',
|
|
97
|
+
session_approvals_per_minute: 'tool approval calls per minute for this session',
|
|
98
|
+
session_handoffs_per_minute: 'handoff calls per minute for this session',
|
|
99
|
+
connect_starts_per_hour: 'connections started per hour from this address',
|
|
100
|
+
connect_pending_grants: 'connections waiting for approval on this server',
|
|
101
|
+
connect_pending_per_address: 'connections waiting for approval from this network',
|
|
86
102
|
};
|
|
87
103
|
/** The release operations, as plan_release's actions. */
|
|
88
104
|
const RELEASE_ACTIONS = {
|
|
@@ -284,7 +300,7 @@ export function describeApiError(err) {
|
|
|
284
300
|
const kindHint = err.code === 'not_found' && typeof details.kind === 'string'
|
|
285
301
|
? NOT_FOUND_HINTS[details.kind]
|
|
286
302
|
: undefined;
|
|
287
|
-
const hint = kindHint ?? hintFor(err.code, err.hint);
|
|
303
|
+
const hint = kindHint ?? SERVER_HINTS[err.code] ?? hintFor(err.code, err.hint);
|
|
288
304
|
if (hint && !(waitShown && RETRY_ADVICE.test(hint))) {
|
|
289
305
|
const text = clean(inServerTerms(hint));
|
|
290
306
|
if (!alreadySaid(text, said)) {
|
|
@@ -316,6 +332,38 @@ export function describeApiError(err) {
|
|
|
316
332
|
return lines.join('\n');
|
|
317
333
|
}
|
|
318
334
|
/** A tool result for any failure inside a tool handler. */
|
|
335
|
+
/** The codes that mean an agent session is over: its token is no longer accepted. */
|
|
336
|
+
export const SESSION_OVER_CODES = new Set([
|
|
337
|
+
'token_revoked',
|
|
338
|
+
'token_expired',
|
|
339
|
+
'token_invalid',
|
|
340
|
+
]);
|
|
341
|
+
const SESSION_OVER_WHY = {
|
|
342
|
+
token_revoked: 'it was revoked',
|
|
343
|
+
token_expired: 'it expired after 30 days unused',
|
|
344
|
+
token_invalid: 'buildIt.Social no longer recognises its token',
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* What to tell the agent once a session token is refused for good: a
|
|
348
|
+
* session is reconnected with `connect`, never with a new personal access
|
|
349
|
+
* token.
|
|
350
|
+
*/
|
|
351
|
+
export function sessionEndedText(code) {
|
|
352
|
+
const why = SESSION_OVER_WHY[code] ?? code;
|
|
353
|
+
return `This buildIt.Social session has ended (${why}). In this folder run \`buildit-mcp connect --project <KEY>\`, approve it, then restart Claude Code. Tell the person; retrying won't help.`;
|
|
354
|
+
}
|
|
355
|
+
/** A tool result for a call made after (or refused because) the session ended. */
|
|
356
|
+
export function sessionEndedResult(code) {
|
|
357
|
+
return {
|
|
358
|
+
isError: true,
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: 'text',
|
|
362
|
+
text: `buildit-mcp error: session_ended (${code})\n${sessionEndedText(code)}`,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
319
367
|
export function toolErrorResult(err, requestId) {
|
|
320
368
|
if (err instanceof ApiError) {
|
|
321
369
|
return { isError: true, content: [{ type: 'text', text: describeApiError(err) }] };
|