premanmcp 0.10.6 → 0.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/bin/account.js +19 -4
- package/bin/cli.js +8 -2
- package/bin/connect/agents.js +198 -0
- package/bin/connect/checkin.js +503 -0
- package/bin/connect/configs.js +276 -0
- package/bin/connect/dispatch.js +131 -0
- package/bin/connect/errors.js +16 -0
- package/bin/connect/guide.js +523 -0
- package/bin/connect/pairing.js +94 -0
- package/bin/connect.js +101 -1554
- package/bin/desktop.js +1 -1
- package/bin/detect.js +42 -3
- package/bin/hook.js +10 -1
- package/bin/hosted.js +4 -4
- package/bin/progress.js +2 -5
- package/bin/runner.js +3 -30
- package/bin/shared.js +26 -1
- package/bin/status.js +1 -6
- package/bin/tests.js +341 -0
- package/bin/verify.js +82 -54
- package/dist/server.js +53 -0
- package/package.json +2 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing PreMan into each agent's own config, and proving it landed.
|
|
3
|
+
*
|
|
4
|
+
* Every agent stores MCP servers somewhere different and one of them owns its
|
|
5
|
+
* file through a CLI, so each writer is separate and each is followed by a read
|
|
6
|
+
* back: a silently failed write must not be reported as a success. The
|
|
7
|
+
* copy-paste renderers exist for the machines where we cannot write at all.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
13
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { LAUNCHER_ARGS, LAUNCHER_COMMAND, backendUrl, frontendUrl, onPath, readJsonFile, writeJsonFile } from "../shared.js";
|
|
15
|
+
import { AGENTS } from "./agents.js";
|
|
16
|
+
|
|
17
|
+
// ── Config writers ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function cursorConfigPath(projectInstall) {
|
|
20
|
+
return projectInstall
|
|
21
|
+
? path.join(process.cwd(), ".cursor", "mcp.json")
|
|
22
|
+
: path.join(os.homedir(), ".cursor", "mcp.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The `cursor-agent mcp enable` invocation, shared by the writer and tests. */
|
|
26
|
+
export function cursorMcpEnableArgs(serverName) {
|
|
27
|
+
return ["mcp", "enable", serverName];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function writeCursorConfig({ serverName, serverConfig, projectInstall }) {
|
|
31
|
+
const configPath = cursorConfigPath(projectInstall);
|
|
32
|
+
const config = { ...readJsonFile(configPath) };
|
|
33
|
+
config.mcpServers = { ...(config.mcpServers || {}), [serverName]: serverConfig };
|
|
34
|
+
writeJsonFile(configPath, config);
|
|
35
|
+
|
|
36
|
+
// Writing mcp.json is not enough for cursor-agent. Newly added servers stay
|
|
37
|
+
// off the CLI's approved list until `mcp enable`, so a first-time connect
|
|
38
|
+
// that then launches the agent finds only built-in tools and reports PreMan
|
|
39
|
+
// as missing. Desktop still reads the file after a restart.
|
|
40
|
+
if (onPath("cursor-agent")) {
|
|
41
|
+
try {
|
|
42
|
+
execFileSync("cursor-agent", cursorMcpEnableArgs(serverName), {
|
|
43
|
+
stdio: "pipe",
|
|
44
|
+
timeout: 15000,
|
|
45
|
+
});
|
|
46
|
+
return { path: configPath, how: "wrote and enabled" };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const detail = error?.stderr?.toString().trim() || error.message;
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
`Note: cursor-agent mcp enable failed (${detail}); the server is written to ${configPath}.\n`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { path: configPath, how: "wrote" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The `claude mcp add` invocation, shared by the writer and the printed fallback. */
|
|
58
|
+
export function claudeMcpAddArgs(serverName, serverConfig, projectInstall) {
|
|
59
|
+
const envArgs = Object.entries(serverConfig.env).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
60
|
+
return [
|
|
61
|
+
"mcp",
|
|
62
|
+
"add",
|
|
63
|
+
serverName,
|
|
64
|
+
"--scope",
|
|
65
|
+
projectInstall ? "project" : "user",
|
|
66
|
+
...envArgs,
|
|
67
|
+
"--",
|
|
68
|
+
serverConfig.command,
|
|
69
|
+
...serverConfig.args,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Claude Code owns ~/.claude.json, so prefer its own CLI. Fall back to writing
|
|
75
|
+
* the config directly when `claude` is not installed — a user can connect before
|
|
76
|
+
* installing the agent.
|
|
77
|
+
*/
|
|
78
|
+
export function writeClaudeConfig({ serverName, serverConfig, projectInstall }) {
|
|
79
|
+
if (onPath("claude")) {
|
|
80
|
+
const args = claudeMcpAddArgs(serverName, serverConfig, projectInstall);
|
|
81
|
+
try {
|
|
82
|
+
execFileSync("claude", args, { stdio: "pipe" });
|
|
83
|
+
return { path: projectInstall ? ".mcp.json" : "Claude Code user config", how: "registered via claude mcp add" };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const detail = error?.stderr?.toString().trim() || error.message;
|
|
86
|
+
process.stdout.write(`Note: claude mcp add failed (${detail}); writing the config directly.\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (projectInstall) {
|
|
91
|
+
const configPath = path.join(process.cwd(), ".mcp.json");
|
|
92
|
+
const config = { ...readJsonFile(configPath) };
|
|
93
|
+
config.mcpServers = { ...(config.mcpServers || {}), [serverName]: serverConfig };
|
|
94
|
+
writeJsonFile(configPath, config);
|
|
95
|
+
return { path: configPath, how: "wrote" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Merge only mcpServers: the user config holds a lot of unrelated state.
|
|
99
|
+
// CLAUDE_CONFIG_DIR relocates that file, and writing to ~ when it is set
|
|
100
|
+
// produces a config Claude Code never reads.
|
|
101
|
+
const configPath = path.join(process.env.CLAUDE_CONFIG_DIR || os.homedir(), ".claude.json");
|
|
102
|
+
const config = { ...readJsonFile(configPath) };
|
|
103
|
+
config.mcpServers = { ...(config.mcpServers || {}), [serverName]: serverConfig };
|
|
104
|
+
writeJsonFile(configPath, config);
|
|
105
|
+
return { path: configPath, how: "wrote" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Render the `[mcp_servers.<name>]` block for Codex's TOML config. Only strings
|
|
110
|
+
* and string arrays appear here, so a hand-rolled writer is enough — no TOML
|
|
111
|
+
* dependency for one fixed shape.
|
|
112
|
+
*/
|
|
113
|
+
export function renderCodexToml(serverName, serverConfig) {
|
|
114
|
+
const str = (value) => JSON.stringify(String(value));
|
|
115
|
+
const lines = [
|
|
116
|
+
`[mcp_servers.${serverName}]`,
|
|
117
|
+
`command = ${str(serverConfig.command)}`,
|
|
118
|
+
`args = [${serverConfig.args.map(str).join(", ")}]`,
|
|
119
|
+
];
|
|
120
|
+
const env = Object.entries(serverConfig.env);
|
|
121
|
+
if (env.length) {
|
|
122
|
+
lines.push("", `[mcp_servers.${serverName}.env]`);
|
|
123
|
+
for (const [key, value] of env) lines.push(`${key} = ${str(value)}`);
|
|
124
|
+
}
|
|
125
|
+
return `${lines.join("\n")}\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Replace an existing preman block, or append a new one, leaving the rest alone. */
|
|
129
|
+
export function upsertCodexToml(existing, serverName, block) {
|
|
130
|
+
const header = new RegExp(`^\\[mcp_servers\\.${serverName}(\\.|\\])`, "m");
|
|
131
|
+
if (!header.test(existing)) {
|
|
132
|
+
const separator = existing.trim() ? "\n" : "";
|
|
133
|
+
return `${existing.trimEnd()}${separator}\n${block}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lines = existing.split("\n");
|
|
137
|
+
const out = [];
|
|
138
|
+
let skipping = false;
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
const isOurHeader = new RegExp(`^\\[mcp_servers\\.${serverName}(\\.|\\])`).test(line);
|
|
141
|
+
if (isOurHeader) {
|
|
142
|
+
skipping = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (skipping && /^\[/.test(line) && !isOurHeader) skipping = false;
|
|
146
|
+
if (!skipping) out.push(line);
|
|
147
|
+
}
|
|
148
|
+
return `${out.join("\n").trimEnd()}\n\n${block}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function writeCodexConfig({ serverName, serverConfig }) {
|
|
152
|
+
const configPath = path.join(
|
|
153
|
+
process.env.CODEX_HOME || path.join(os.homedir(), ".codex"),
|
|
154
|
+
"config.toml"
|
|
155
|
+
);
|
|
156
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
157
|
+
const next = upsertCodexToml(existing, serverName, renderCodexToml(serverName, serverConfig));
|
|
158
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
159
|
+
writeFileSync(configPath, next, { mode: 0o600 });
|
|
160
|
+
chmodSync(configPath, 0o600); // mode above is ignored for an existing file
|
|
161
|
+
return { path: configPath, how: "wrote" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const WRITERS = {
|
|
165
|
+
cursor: writeCursorConfig,
|
|
166
|
+
claude_code: writeClaudeConfig,
|
|
167
|
+
codex: writeCodexConfig,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// ── Copy-paste fallbacks ────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/** Quote only what a shell would otherwise mangle, so the line stays readable. */
|
|
173
|
+
export function shellQuote(value) {
|
|
174
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** What a user would paste by hand to get exactly what the writer would have written. */
|
|
178
|
+
export function renderAgentSnippet(agentId, serverName, serverConfig, { projectInstall = false } = {}) {
|
|
179
|
+
if (agentId === "codex") return renderCodexToml(serverName, serverConfig);
|
|
180
|
+
if (agentId === "claude_code") {
|
|
181
|
+
const args = claudeMcpAddArgs(serverName, serverConfig, projectInstall);
|
|
182
|
+
return `claude ${args.map(shellQuote).join(" ")}\n`;
|
|
183
|
+
}
|
|
184
|
+
return `${JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2)}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Every agent's snippet, for when we could not pick one for the user. */
|
|
188
|
+
export function renderAllAgentSnippets(args, serverName, { projectInstall = false } = {}) {
|
|
189
|
+
// Build config WITHOUT resolved API key to avoid leaking secrets in CI logs.
|
|
190
|
+
// Users must supply --api-key or set PREMAN_API_KEY separately.
|
|
191
|
+
const env = {
|
|
192
|
+
PREMAN_BACKEND: backendUrl(args),
|
|
193
|
+
PREMAN_FRONTEND: frontendUrl(args),
|
|
194
|
+
};
|
|
195
|
+
const serverConfig = {
|
|
196
|
+
command: LAUNCHER_COMMAND,
|
|
197
|
+
args: [...LAUNCHER_ARGS],
|
|
198
|
+
env,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return AGENTS.map((agent) => {
|
|
202
|
+
// Adjust hint based on projectInstall, matching verifyWrittenConfig logic
|
|
203
|
+
let hint = agent.snippetHint;
|
|
204
|
+
if (agent.id === "cursor" && projectInstall) {
|
|
205
|
+
hint = "merge into .cursor/mcp.json, then run: cursor-agent mcp enable " + serverName;
|
|
206
|
+
} else if (agent.id === "claude_code") {
|
|
207
|
+
hint = projectInstall ? "run (with --scope project):" : "run:";
|
|
208
|
+
}
|
|
209
|
+
return `# ${agent.label} — ${hint}\n` + renderAgentSnippet(agent.id, serverName, serverConfig, { projectInstall });
|
|
210
|
+
}).join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Post-write validation ───────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/** The lines belonging to our `[mcp_servers.<name>]` block, or null if absent. */
|
|
216
|
+
function codexBlockLines(text, serverName) {
|
|
217
|
+
const lines = text.split("\n");
|
|
218
|
+
const start = lines.findIndex((line) => line.trim() === `[mcp_servers.${serverName}]`);
|
|
219
|
+
if (start === -1) return null;
|
|
220
|
+
const block = [];
|
|
221
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
222
|
+
const line = lines[i];
|
|
223
|
+
if (/^\[/.test(line) && !line.startsWith(`[mcp_servers.${serverName}.`)) break;
|
|
224
|
+
block.push(line);
|
|
225
|
+
}
|
|
226
|
+
return block;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Read back what we just wrote, so a silently-failed write is not reported as a
|
|
231
|
+
* success. Proves the entry is on disk (or that Claude Code knows about it) —
|
|
232
|
+
* whether the agent has actually loaded it is only ever proven by a check-in.
|
|
233
|
+
*
|
|
234
|
+
* Never throws: a verification that cannot run must not fail the connect. An
|
|
235
|
+
* unreadable state reports "unknown" and stays quiet, because a wrong warning
|
|
236
|
+
* costs more trust than a missing one.
|
|
237
|
+
*/
|
|
238
|
+
export function verifyWrittenConfig(agent, { serverName, written }) {
|
|
239
|
+
try {
|
|
240
|
+
if (agent.id === "codex") {
|
|
241
|
+
const text = written.path && existsSync(written.path) ? readFileSync(written.path, "utf8") : "";
|
|
242
|
+
const block = codexBlockLines(text, serverName);
|
|
243
|
+
if (!block) {
|
|
244
|
+
return { status: "mismatch", detail: `no [mcp_servers.${serverName}] block in ${written.path}` };
|
|
245
|
+
}
|
|
246
|
+
if (!block.some((line) => line.trim() === `command = "${LAUNCHER_COMMAND}"`)) {
|
|
247
|
+
return {
|
|
248
|
+
status: "mismatch",
|
|
249
|
+
detail: `${written.path} does not launch ${LAUNCHER_COMMAND}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { status: "verified" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Claude Code registered the server itself — ask its CLI what it ended up with.
|
|
256
|
+
if (agent.id === "claude_code" && written.how !== "wrote") {
|
|
257
|
+
const probe = spawnSync("claude", ["mcp", "get", serverName], { stdio: "ignore" });
|
|
258
|
+
if (probe.error) return { status: "unknown", detail: "could not run claude" };
|
|
259
|
+
return probe.status === 0
|
|
260
|
+
? { status: "verified" }
|
|
261
|
+
: { status: "mismatch", detail: `claude mcp get ${serverName} did not find the server` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const entry = readJsonFile(written.path).mcpServers?.[serverName];
|
|
265
|
+
if (!entry) return { status: "mismatch", detail: `${serverName} is missing from ${written.path}` };
|
|
266
|
+
if (entry.command !== LAUNCHER_COMMAND) {
|
|
267
|
+
return {
|
|
268
|
+
status: "mismatch",
|
|
269
|
+
detail: `${written.path} does not launch ${LAUNCHER_COMMAND}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return { status: "verified" };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return { status: "unknown", detail: error.message };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloud-dispatch credential (SCRUM-124).
|
|
3
|
+
*
|
|
4
|
+
* With it PreMan can start an agent run; without it PreMan can only describe the
|
|
5
|
+
* fix. Always skippable, never able to fail a connect -- which is why it lives
|
|
6
|
+
* apart from the connect flow rather than inside it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { callBackendJson, cliInvocation, makeArgs, promptSecret, promptText, resolveApiKey } from "../shared.js";
|
|
10
|
+
import { ConnectError, EXIT_USAGE } from "./errors.js";
|
|
11
|
+
import { findAgent } from "./agents.js";
|
|
12
|
+
|
|
13
|
+
// ── Dispatch credential (SCRUM-124) ─────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export async function captureDispatchCredential(args, agent, apiKey, { prompt = true } = {}) {
|
|
16
|
+
if (!agent.dispatch) return;
|
|
17
|
+
if (args.has("--skip-dispatch-credential")) return;
|
|
18
|
+
|
|
19
|
+
let secret = args.value("--dispatch-credential", "");
|
|
20
|
+
let routineId = args.value("--routine-id", "");
|
|
21
|
+
|
|
22
|
+
if (!secret) {
|
|
23
|
+
if (!prompt || !process.stdin.isTTY) return;
|
|
24
|
+
// Framed as the expected step rather than an optional aside. Without it
|
|
25
|
+
// PreMan can only suggest fixes; with it, it can run them. Presenting it as
|
|
26
|
+
// "optional, press Enter to skip" meant almost everyone skipped the thing
|
|
27
|
+
// that makes the product act rather than advise.
|
|
28
|
+
process.stdout.write(
|
|
29
|
+
`\nOptional — let PreMan start ${agent.label} runs for you, so it can apply\n` +
|
|
30
|
+
`fixes and run checks on a schedule instead of only telling you what to do.\n` +
|
|
31
|
+
`Get the token from ${agent.dispatch.source}.\n`
|
|
32
|
+
);
|
|
33
|
+
secret = await promptSecret(`Paste your ${agent.dispatch.credential} (Enter to skip): `);
|
|
34
|
+
if (!secret) {
|
|
35
|
+
process.stdout.write(
|
|
36
|
+
`Skipped. Run '${cliInvocation()} dispatch --agent ${agent.id.replace("_", "-")}' when you have the token.\n`
|
|
37
|
+
);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (agent.dispatch.needsRoutine && !routineId) {
|
|
41
|
+
routineId = await promptText("Routine id or URL: ");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (agent.dispatch.needsRoutine && !routineId) {
|
|
46
|
+
process.stdout.write("Skipped: a routine id is required alongside the token.\n");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const body = agent.dispatch.needsRoutine
|
|
51
|
+
? { provider: agent.id, secret, routine_id: extractRoutineId(routineId) }
|
|
52
|
+
: { provider: agent.id, secret };
|
|
53
|
+
|
|
54
|
+
const result = await callBackendJson(args, "PUT", "/workbench/coding-agent/dispatch", {
|
|
55
|
+
token: apiKey,
|
|
56
|
+
json: body,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.ok) {
|
|
60
|
+
process.stdout.write("Cloud dispatch enabled — PreMan can now start runs for you.\n");
|
|
61
|
+
} else if (result.status_code === 404 || result.status_code === 405) {
|
|
62
|
+
process.stdout.write("Note: this PreMan backend does not support cloud dispatch yet; skipped.\n");
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(`Note: could not save the credential (${result.status_code}); skipped.\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const DISPATCH_HELP = `
|
|
69
|
+
Dispatch options:
|
|
70
|
+
--agent <name> cursor | claude-code (defaults to the connected agent)
|
|
71
|
+
--dispatch-credential <t> Cloud-dispatch token (non-interactive)
|
|
72
|
+
--routine-id <id> Claude Code routine id, with --dispatch-credential
|
|
73
|
+
--api-key <key> PreMan API key. If omitted, stored credentials are used
|
|
74
|
+
--backend <url> PreMan backend URL
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `preman dispatch` — store the cloud-dispatch credential on its own.
|
|
79
|
+
*
|
|
80
|
+
* Exists so connect never has to ask for a token mid-onboarding. Someone who
|
|
81
|
+
* skipped it (or did not have it yet) comes back here instead of re-running the
|
|
82
|
+
* whole connect.
|
|
83
|
+
*/
|
|
84
|
+
export async function dispatchCommand(commandArgs) {
|
|
85
|
+
const args = makeArgs(commandArgs);
|
|
86
|
+
const apiKey = resolveApiKey(args);
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
throw new ConnectError(
|
|
89
|
+
`No PreMan credentials. Run '${cliInvocation()} connect' first, or pass --api-key pm_live_….`,
|
|
90
|
+
EXIT_USAGE
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let agent = findAgent(args.value("--agent", ""));
|
|
95
|
+
if (!agent && args.value("--agent", "")) {
|
|
96
|
+
throw new ConnectError(
|
|
97
|
+
`Unknown agent: ${args.value("--agent", "")}. Use cursor or claude-code.`,
|
|
98
|
+
EXIT_USAGE
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!agent) {
|
|
103
|
+
const current = await callBackendJson(args, "GET", "/workbench/coding-agent", { token: apiKey });
|
|
104
|
+
agent = findAgent(current.ok ? current.agent : "");
|
|
105
|
+
if (!agent) {
|
|
106
|
+
throw new ConnectError(
|
|
107
|
+
"Could not tell which agent to set up. Pass --agent cursor or --agent claude-code.",
|
|
108
|
+
EXIT_USAGE
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!agent.dispatch) {
|
|
114
|
+
throw new ConnectError(`${agent.label} has no cloud-dispatch API yet.`, EXIT_USAGE);
|
|
115
|
+
}
|
|
116
|
+
if (!args.value("--dispatch-credential", "") && !process.stdin.isTTY) {
|
|
117
|
+
throw new ConnectError(
|
|
118
|
+
"preman dispatch needs a terminal, or --dispatch-credential <token>.",
|
|
119
|
+
EXIT_USAGE
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await captureDispatchCredential(args, agent, apiKey);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Accept either a bare trig_… id or the routine URL it appears in. */
|
|
127
|
+
export function extractRoutineId(value) {
|
|
128
|
+
const raw = String(value || "").trim();
|
|
129
|
+
const match = raw.match(/trig_[A-Za-z0-9]+/);
|
|
130
|
+
return match ? match[0] : raw;
|
|
131
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error type the connect modules share.
|
|
3
|
+
*
|
|
4
|
+
* Its own file because every other module needs it and none of them should have
|
|
5
|
+
* to import a sibling to raise. `exitCode` is what the CLI exits with, so a
|
|
6
|
+
* usage mistake (2) stays distinguishable from a run that genuinely failed (1).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const EXIT_USAGE = 2;
|
|
10
|
+
|
|
11
|
+
export class ConnectError extends Error {
|
|
12
|
+
constructor(message, exitCode = 1) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
}
|
|
16
|
+
}
|