premanmcp 0.10.5 → 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 +35 -7
- package/bin/account.js +19 -4
- package/bin/api_tools.js +1 -1
- package/bin/cli.js +22 -5
- 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 -1508
- package/bin/desktop.js +1 -1
- package/bin/detect.js +42 -3
- package/bin/hook.js +126 -2
- package/bin/hosted.js +4 -4
- package/bin/progress.js +2 -5
- package/bin/runner.js +25 -36
- 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 +78 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,11 +48,13 @@ to go restart anything, in cheapest-first order:
|
|
|
48
48
|
1. **Self-test.** It starts the MCP server exactly as your agent will and calls
|
|
49
49
|
`preman_status` over stdio. That both completes the link and proves the whole chain —
|
|
50
50
|
launcher, package, key, backend. `--no-self-test` turns it off.
|
|
51
|
-
2. **Agent run**, which also proves your agent can load what was written.
|
|
52
|
-
agent
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
2. **Agent run**, which also proves your agent can load what was written. For Cursor it
|
|
52
|
+
runs `cursor-agent mcp enable` after writing `mcp.json` (new servers stay off the CLI
|
|
53
|
+
approved list otherwise) and launches the agent with `--approve-mcps` so the check-in
|
|
54
|
+
can actually call `preman_status`. It opens your agent interactively in a new terminal
|
|
55
|
+
window — the session you go on to use — and falls back to a headless run (`claude -p`,
|
|
56
|
+
`cursor-agent --approve-mcps -p`, `codex exec`) where no window can be opened, such as
|
|
57
|
+
CI or SSH. `--no-auto-checkin` turns it off, `PREMAN_NO_TERMINAL=1` keeps it headless.
|
|
56
58
|
3. **Wait**, if neither is possible: restart your agent and it links on its first call.
|
|
57
59
|
|
|
58
60
|
A self-test that answers from an unexpected backend is reported with the file that
|
|
@@ -71,6 +73,10 @@ up a link that already worked:
|
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
75
|
preman endpoints discover # map this repo's endpoints
|
|
76
|
+
preman tests generate # heuristic suites on saved Collections requests
|
|
77
|
+
preman tests review # list flagged suites; --approve <id> enables schedule
|
|
78
|
+
preman tests setup # harvest path-param fixture IDs into a .env snippet
|
|
79
|
+
preman tests enrich # optional LLM cases on remaining heuristic suites
|
|
74
80
|
preman runner start --background # let PreMan apply fixes on this machine
|
|
75
81
|
preman github # or connect it in the dashboard
|
|
76
82
|
preman status # which of those are done
|
|
@@ -133,6 +139,7 @@ npm exec -y premanmcp@latest -- connect --project
|
|
|
133
139
|
## What It Does
|
|
134
140
|
|
|
135
141
|
- Converts API endpoints into agent-callable MCP tools.
|
|
142
|
+
- Migrates a Postman collection in one call (`migrate_from_postman`): keeps the assertions your `pm.test` blocks declared, splits the environment into shared variables and encrypted secrets, and schedules a monitored test suite per request.
|
|
136
143
|
- Creates/connects a PreMan account from the terminal or IDE agent.
|
|
137
144
|
- Lets agents test real backend endpoints from the IDE.
|
|
138
145
|
- Syncs endpoint inventory across backend and frontend workflows.
|
|
@@ -166,6 +173,10 @@ Convert these endpoints into an MCP.
|
|
|
166
173
|
Show me the audit log for this hosted MCP.
|
|
167
174
|
```
|
|
168
175
|
|
|
176
|
+
```text
|
|
177
|
+
Move my Postman collection over to PreMan.
|
|
178
|
+
```
|
|
179
|
+
|
|
169
180
|
```text
|
|
170
181
|
Pull my pending PreMan fix tasks and fix the failing endpoint.
|
|
171
182
|
```
|
|
@@ -230,6 +241,7 @@ npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
|
|
|
230
241
|
```bash
|
|
231
242
|
preman hook install # Write the pre-push hook (connect does this too)
|
|
232
243
|
preman hook status # Installed? And does the command in it still answer?
|
|
244
|
+
preman hook repair # Rewrite it if it stopped working (usually automatic)
|
|
233
245
|
preman hook uninstall # Remove it, restoring any hook it replaced
|
|
234
246
|
```
|
|
235
247
|
|
|
@@ -242,17 +254,33 @@ than `@latest`, so upgrading us never changes what your pushes run; re-run `hook
|
|
|
242
254
|
to move it. Set `PREMAN_HOOK_INVOCATION` to write a command of your own instead, and
|
|
243
255
|
`PREMAN_SKIP_HOOK=1` to silence the hook for a push.
|
|
244
256
|
|
|
257
|
+
You should not have to run `repair`. A hook of ours that stops answering is rewritten in the
|
|
258
|
+
background by anything that proves PreMan runs here — any command, or your agent starting the
|
|
259
|
+
MCP server — because nothing re-runs `connect` after an upgrade to notice, and the broken
|
|
260
|
+
state is silent. It is narrow on purpose: a hook that still works keeps the version it pins, a
|
|
261
|
+
hook we did not write is never touched, and no hook is installed where you never asked for
|
|
262
|
+
one. The answer is remembered for an hour per directory, and `PREMAN_NO_HOOK_REPAIR=1` turns
|
|
263
|
+
it off.
|
|
264
|
+
|
|
245
265
|
### Runner
|
|
246
266
|
|
|
247
|
-
`connect --guide`
|
|
267
|
+
`preman onboard` and `connect --guide` set this up for you; these are for managing it
|
|
268
|
+
afterwards.
|
|
248
269
|
|
|
249
270
|
```bash
|
|
250
271
|
preman runner status # Paired? Running?
|
|
251
272
|
preman runner start --background # Hold the job stream, log to ~/.preman/runner.log
|
|
252
273
|
preman runner stop # Stop it and report offline
|
|
253
|
-
preman runner register --agent claude-code #
|
|
274
|
+
preman runner register --agent claude-code # Pair without starting
|
|
254
275
|
```
|
|
255
276
|
|
|
277
|
+
`start` pairs this machine itself when it is not paired yet, and re-pairs it when the
|
|
278
|
+
backend has revoked the token it held, so neither is a command you have to be told to run.
|
|
279
|
+
It works out which agent to pair as from the session it is running in, then from the agent
|
|
280
|
+
PreMan is already configured in, then from the only one installed — and asks only when
|
|
281
|
+
those disagree. Pass `--agent cursor|claude-code|codex` to settle it yourself, which is
|
|
282
|
+
also what a machine with no terminal to ask in needs.
|
|
283
|
+
|
|
256
284
|
The runner holds one outbound connection to PreMan and runs the work PreMan queues for
|
|
257
285
|
this machine — a failing endpoint becomes an agent run in your own repo instead of a
|
|
258
286
|
prompt you have to paste. It is bound to the agent and directory it was registered with
|
package/bin/account.js
CHANGED
|
@@ -29,7 +29,7 @@ export const ACCOUNT_HELP = `
|
|
|
29
29
|
Account options:
|
|
30
30
|
login --browser Approve sign-in in the browser instead of typing a password
|
|
31
31
|
logout Delete stored CLI credentials
|
|
32
|
-
doctor
|
|
32
|
+
doctor [--port n] Check backend, credentials, target, and integrations
|
|
33
33
|
watch <run-id> Follow a push simulation as it runs
|
|
34
34
|
`;
|
|
35
35
|
|
|
@@ -140,7 +140,10 @@ export async function doctorCommand(commandArgs = []) {
|
|
|
140
140
|
if (!status.ok) failures += 1;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
const
|
|
143
|
+
const explicitPort = args.value("--port", "");
|
|
144
|
+
const candidates = detectCandidates(process.cwd(), {
|
|
145
|
+
extraPorts: explicitPort ? [explicitPort] : [],
|
|
146
|
+
});
|
|
144
147
|
results.push(
|
|
145
148
|
line(
|
|
146
149
|
"local target",
|
|
@@ -150,6 +153,7 @@ export async function doctorCommand(commandArgs = []) {
|
|
|
150
153
|
: "no signals in this directory"
|
|
151
154
|
)
|
|
152
155
|
);
|
|
156
|
+
if (!candidates.length) failures += 1;
|
|
153
157
|
|
|
154
158
|
// An installed hook whose command no longer answers is the one failure here
|
|
155
159
|
// that reports itself as success everywhere else: `git push` prints a single
|
|
@@ -200,7 +204,18 @@ export async function doctorCommand(commandArgs = []) {
|
|
|
200
204
|
return { failures };
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
const TERMINAL_STATUSES = new Set([
|
|
207
|
+
const TERMINAL_STATUSES = new Set([
|
|
208
|
+
"complete",
|
|
209
|
+
"completed",
|
|
210
|
+
"succeeded",
|
|
211
|
+
"failed",
|
|
212
|
+
"error",
|
|
213
|
+
"cancelled",
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
export function isTerminalSimulationStatus(value) {
|
|
217
|
+
return TERMINAL_STATUSES.has(String(value || "").toLowerCase());
|
|
218
|
+
}
|
|
204
219
|
|
|
205
220
|
export async function watchCommand(commandArgs = []) {
|
|
206
221
|
const args = makeArgs(commandArgs);
|
|
@@ -235,7 +250,7 @@ export async function watchCommand(commandArgs = []) {
|
|
|
235
250
|
process.stdout.write(` ${mark} ${String(step.key || "").padEnd(14)} ${step.message || ""}\n`);
|
|
236
251
|
}
|
|
237
252
|
|
|
238
|
-
if (
|
|
253
|
+
if (isTerminalSimulationStatus(result.status)) {
|
|
239
254
|
const verdict = result.summary?.verdict || result.status;
|
|
240
255
|
process.stdout.write(`\nRun ${result.status}: ${verdict}\n`);
|
|
241
256
|
return result;
|
package/bin/api_tools.js
CHANGED
package/bin/cli.js
CHANGED
|
@@ -17,12 +17,14 @@ import path from "node:path";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
19
|
import { ENDPOINTS_HELP, TEST_HELP, endpointsCommand, testCommand } from "./api_tools.js";
|
|
20
|
+
import { TESTS_HELP, testsCommand } from "./tests.js";
|
|
20
21
|
import {
|
|
21
22
|
CONNECT_HELP,
|
|
22
23
|
DISPATCH_HELP,
|
|
23
24
|
connectCommand,
|
|
24
25
|
discoverEndpoints,
|
|
25
26
|
dispatchCommand,
|
|
27
|
+
resolveAgentForPairing,
|
|
26
28
|
writeCursorConfig,
|
|
27
29
|
} from "./connect.js";
|
|
28
30
|
import {
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
} from "./integrations.js";
|
|
35
37
|
import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
|
|
36
38
|
import { STATUS_HELP, statusCommand } from "./status.js";
|
|
37
|
-
import { HOOK_HELP, hookCommand } from "./hook.js";
|
|
39
|
+
import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
|
|
38
40
|
import { RUNNER_HELP, runnerCommand } from "./runner.js";
|
|
39
41
|
import { VERIFY_HELP, verifyCommand } from "./verify.js";
|
|
40
42
|
import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
|
|
@@ -55,7 +57,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
55
57
|
const ROOT = path.join(__dirname, "..");
|
|
56
58
|
|
|
57
59
|
const args = process.argv.slice(2);
|
|
58
|
-
const command = args
|
|
60
|
+
const command = args.length === 0 && process.stdin.isTTY
|
|
61
|
+
? "connect"
|
|
62
|
+
: args[0] === "--help" || args[0] === "-h"
|
|
59
63
|
? "help"
|
|
60
64
|
: args[0] && !args[0].startsWith("-")
|
|
61
65
|
? args[0]
|
|
@@ -97,6 +101,7 @@ function printHelp() {
|
|
|
97
101
|
["link|tools|run ...", "Drive a published hosted MCP"],
|
|
98
102
|
["endpoints list|discover|setup ...", "Discover, list, and set up API endpoints"],
|
|
99
103
|
["test <id> [--scenario ...] [--stress]", "Generate + run tests for an endpoint"],
|
|
104
|
+
["tests generate|review|setup|enrich", "Collections: generate, review, fixtures, enrich"],
|
|
100
105
|
];
|
|
101
106
|
const width = Math.max(...usage.map(([command]) => `${cli} ${command}`.trimEnd().length));
|
|
102
107
|
const usageLines = usage
|
|
@@ -107,7 +112,7 @@ function printHelp() {
|
|
|
107
112
|
|
|
108
113
|
Usage:
|
|
109
114
|
${usageLines}
|
|
110
|
-
${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${HOOK_HELP}${RUNNER_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${ENDPOINTS_HELP}${TEST_HELP}
|
|
115
|
+
${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${HOOK_HELP}${RUNNER_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${ENDPOINTS_HELP}${TEST_HELP}${TESTS_HELP}
|
|
111
116
|
Login options:
|
|
112
117
|
--email <email> Pre-fill the email prompt
|
|
113
118
|
--backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
@@ -135,6 +140,7 @@ async function loginCommand() {
|
|
|
135
140
|
await loginBrowser(cliArgs);
|
|
136
141
|
return;
|
|
137
142
|
}
|
|
143
|
+
const cli = cliInvocation();
|
|
138
144
|
const creds = await authenticateTerminal(cliArgs);
|
|
139
145
|
process.stdout.write(`PreMan account ready.
|
|
140
146
|
|
|
@@ -153,6 +159,7 @@ You can now run:
|
|
|
153
159
|
* the same write plus agent choice, pairing, and dispatch setup.
|
|
154
160
|
*/
|
|
155
161
|
async function installCursorMcp() {
|
|
162
|
+
const cli = cliInvocation();
|
|
156
163
|
const serverName = argValue("--name", "preman");
|
|
157
164
|
const projectInstall = hasFlag("--project");
|
|
158
165
|
|
|
@@ -179,7 +186,7 @@ Server name: ${serverName}
|
|
|
179
186
|
Backend: ${serverConfig.env.PREMAN_BACKEND}
|
|
180
187
|
|
|
181
188
|
Next steps:
|
|
182
|
-
1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` :
|
|
189
|
+
1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : `Run ${cli} login to create/connect your account and generate an API key.`}
|
|
183
190
|
2. Restart Cursor or toggle the PreMan MCP server off/on.
|
|
184
191
|
3. In your API repo, ask your coding agent:
|
|
185
192
|
"Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
|
|
@@ -216,6 +223,11 @@ function startServer() {
|
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
async function main() {
|
|
226
|
+
// Running at all is the signal. `hook` and `verify` own the hook themselves,
|
|
227
|
+
// and connect reinstalls it in the foreground, so a background rewrite there
|
|
228
|
+
// would only race with the one the user is watching.
|
|
229
|
+
if (!["hook", "verify", "connect"].includes(command)) scheduleHookRepair();
|
|
230
|
+
|
|
219
231
|
if (command === "login") {
|
|
220
232
|
await loginCommand();
|
|
221
233
|
} else if (command === "status") {
|
|
@@ -225,7 +237,10 @@ async function main() {
|
|
|
225
237
|
} else if (command === "hook") {
|
|
226
238
|
await hookCommand(commandArgs);
|
|
227
239
|
} else if (command === "runner") {
|
|
228
|
-
|
|
240
|
+
// Injected rather than imported inside runner.js, which connect.js already
|
|
241
|
+
// depends on: the agent picker lives there, and importing it back would
|
|
242
|
+
// close the cycle.
|
|
243
|
+
await runnerCommand(commandArgs, { resolveAgent: resolveAgentForPairing });
|
|
229
244
|
} else if (command === "logout") {
|
|
230
245
|
await logoutCommand();
|
|
231
246
|
} else if (command === "doctor") {
|
|
@@ -266,6 +281,8 @@ async function main() {
|
|
|
266
281
|
await endpointsCommand(commandArgs);
|
|
267
282
|
} else if (command === "test") {
|
|
268
283
|
await testCommand(commandArgs);
|
|
284
|
+
} else if (command === "tests") {
|
|
285
|
+
await testsCommand(commandArgs);
|
|
269
286
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
270
287
|
printHelp();
|
|
271
288
|
} else if (command === "start") {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which coding agent this machine should use, and how it is asked.
|
|
3
|
+
*
|
|
4
|
+
* The registry is the shared vocabulary: `id` matches the backend's
|
|
5
|
+
* normalize_agent(), so a link record and this CLI cannot disagree about who is
|
|
6
|
+
* connected. Resolution is ordered by how much each signal actually knows --
|
|
7
|
+
* the session we are inside, the agent PreMan is already written into, the only
|
|
8
|
+
* one installed -- because asking is the step people resent most.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { onPath, promptText, readJsonFile } from "../shared.js";
|
|
16
|
+
import { ConnectError, EXIT_USAGE } from "./errors.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supported agents. `id` matches the backend's normalize_agent() vocabulary so
|
|
20
|
+
* the link record and this CLI cannot disagree about who is connected.
|
|
21
|
+
*/
|
|
22
|
+
export const AGENTS = [
|
|
23
|
+
{
|
|
24
|
+
id: "cursor",
|
|
25
|
+
label: "Cursor",
|
|
26
|
+
aliases: ["cursor"],
|
|
27
|
+
dispatch: {
|
|
28
|
+
credential: "Cursor API key",
|
|
29
|
+
needsRoutine: false,
|
|
30
|
+
source: "cursor.com/dashboard → Integrations → API Keys",
|
|
31
|
+
},
|
|
32
|
+
snippetHint: "merge into ~/.cursor/mcp.json, then run: cursor-agent mcp enable preman",
|
|
33
|
+
restartHint:
|
|
34
|
+
'Fully quit and reopen Cursor, then Settings → MCP → toggle "preman" off and on. For the CLI, run `cursor-agent mcp enable preman`.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "claude_code",
|
|
38
|
+
label: "Claude Code",
|
|
39
|
+
aliases: ["claude", "claude-code", "claude_code", "claudecode"],
|
|
40
|
+
dispatch: {
|
|
41
|
+
credential: "Claude Code routine token",
|
|
42
|
+
needsRoutine: true,
|
|
43
|
+
// Named explicitly because "Claude Code routine token" reads like an
|
|
44
|
+
// Anthropic API key, which is a different credential from a different page.
|
|
45
|
+
source: "claude.ai/code/routines → your routine → Add API trigger",
|
|
46
|
+
},
|
|
47
|
+
snippetHint: "run:",
|
|
48
|
+
restartHint: 'Start a new Claude Code session and run `claude mcp list` — "preman" should be listed.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "codex",
|
|
52
|
+
label: "Codex",
|
|
53
|
+
aliases: ["codex", "openai-codex", "openai_codex"],
|
|
54
|
+
dispatch: null, // No public fire API; stays on the copy-paste path.
|
|
55
|
+
snippetHint: "append to ~/.codex/config.toml",
|
|
56
|
+
restartHint: "Restart Codex so it re-reads its config.toml.",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function findAgent(value) {
|
|
61
|
+
const raw = String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
62
|
+
return AGENTS.find((a) => a.id === raw || a.aliases.includes(raw)) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Why this agent's CLI cannot run unattended right now, or "".
|
|
68
|
+
*
|
|
69
|
+
* Installed is not the same as usable. `cursor-agent` sits on PATH and exits 1
|
|
70
|
+
* on every `-p` run until someone signs in, which surfaced as "Cursor did not
|
|
71
|
+
* register any endpoints" and sent people looking at PreMan for an hour. Only
|
|
72
|
+
* Cursor is probed because only its CLI has a cheap non-interactive status
|
|
73
|
+
* subcommand; the others are diagnosed from their own output when they fail.
|
|
74
|
+
*/
|
|
75
|
+
export function agentBlocker(agentId) {
|
|
76
|
+
if (agentId !== "cursor") return "";
|
|
77
|
+
const probe = spawnSync("cursor-agent", ["status"], {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
timeout: 15000,
|
|
80
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
81
|
+
});
|
|
82
|
+
const text = `${probe.stdout || ""}${probe.stderr || ""}`;
|
|
83
|
+
if (/not logged in|not authenticated|no active session/i.test(text)) {
|
|
84
|
+
return "cursor-agent is installed but not signed in — run `cursor-agent login`";
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Best guess at which agent this machine actually uses, for the default pick. */
|
|
90
|
+
export function detectAgents() {
|
|
91
|
+
const home = os.homedir();
|
|
92
|
+
return {
|
|
93
|
+
cursor: existsSync(path.join(home, ".cursor")) || Boolean(process.env.CURSOR_TRACE_ID),
|
|
94
|
+
claude_code: onPath("claude") || existsSync(path.join(home, ".claude.json")),
|
|
95
|
+
codex: onPath("codex") || existsSync(process.env.CODEX_HOME || path.join(home, ".codex")),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The agent whose session this command is running inside, if any.
|
|
101
|
+
*
|
|
102
|
+
* A better default than "first one installed": someone who types this into an
|
|
103
|
+
* agent's terminal almost always means that agent, and on a machine with all
|
|
104
|
+
* three installed the detected-order default is usually wrong.
|
|
105
|
+
*/
|
|
106
|
+
export function runningInside(env = process.env) {
|
|
107
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude_code";
|
|
108
|
+
if (env.CURSOR_TRACE_ID) return "cursor";
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Is PreMan's MCP server already written into this agent's config? */
|
|
113
|
+
function agentIsLinked(agentId, serverName = "preman") {
|
|
114
|
+
try {
|
|
115
|
+
const home = os.homedir();
|
|
116
|
+
if (agentId === "cursor") {
|
|
117
|
+
return Boolean(readJsonFile(path.join(home, ".cursor", "mcp.json")).mcpServers?.[serverName]);
|
|
118
|
+
}
|
|
119
|
+
if (agentId === "claude_code") {
|
|
120
|
+
return Boolean(readJsonFile(path.join(home, ".claude.json")).mcpServers?.[serverName]);
|
|
121
|
+
}
|
|
122
|
+
if (agentId === "codex") {
|
|
123
|
+
const file = path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
|
|
124
|
+
return existsSync(file) && readFileSync(file, "utf8").includes(`[mcp_servers.${serverName}]`);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// An unreadable config is not an answer; fall through to the other signals.
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Which agent this machine should pair as, or "" when only a person can say.
|
|
134
|
+
*
|
|
135
|
+
* Pairing needs an agent id, and demanding `--agent` for it turned "start the
|
|
136
|
+
* runner" into a command someone had to be told twice. The signals are ordered
|
|
137
|
+
* by how much they actually know: the session we are inside, then the agent
|
|
138
|
+
* PreMan is already configured in, then the only one installed. Asking is the
|
|
139
|
+
* last resort rather than the first, and a machine with no terminal to ask in
|
|
140
|
+
* gets "" so the caller can fail with a sentence instead of hanging on a prompt.
|
|
141
|
+
*/
|
|
142
|
+
export async function resolveAgentForPairing({ interactive = Boolean(process.stdin.isTTY) } = {}) {
|
|
143
|
+
const inside = runningInside();
|
|
144
|
+
if (inside) return inside;
|
|
145
|
+
|
|
146
|
+
const linked = AGENTS.filter((agent) => agentIsLinked(agent.id));
|
|
147
|
+
if (linked.length === 1) return linked[0].id;
|
|
148
|
+
|
|
149
|
+
const detected = detectAgents();
|
|
150
|
+
const present = AGENTS.filter((agent) => detected[agent.id]);
|
|
151
|
+
if (present.length === 1) return present[0].id;
|
|
152
|
+
|
|
153
|
+
if (!interactive) return "";
|
|
154
|
+
const picked = await promptAgentChoice(detected);
|
|
155
|
+
return picked?.id || "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The agent a directly-invoked command should drive, as an object, or null.
|
|
160
|
+
*
|
|
161
|
+
* Same signals as pairing, resolved to the record `discoverEndpoints` needs,
|
|
162
|
+
* with an explicit `--agent` taking precedence so the command stays scriptable
|
|
163
|
+
* on a machine that has all three installed. Commands the user typed on purpose
|
|
164
|
+
* get to run the agent; the caller falls back to printing the brief on null,
|
|
165
|
+
* which is also what a non-interactive run with no clear answer gets.
|
|
166
|
+
*/
|
|
167
|
+
export async function resolveAgentToDrive({ agent = "", ...options } = {}) {
|
|
168
|
+
return findAgent(agent) || findAgent(await resolveAgentForPairing(options));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function promptAgentChoice(detected, insideId = runningInside()) {
|
|
172
|
+
process.stdout.write("Which coding agent?\n");
|
|
173
|
+
AGENTS.forEach((agent, index) => {
|
|
174
|
+
let mark = "";
|
|
175
|
+
if (agent.id === insideId) mark = " (this session)";
|
|
176
|
+
else if (detected[agent.id]) mark = " (detected)";
|
|
177
|
+
process.stdout.write(` ${index + 1}. ${agent.label}${mark}\n`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const inside = AGENTS.findIndex((a) => a.id === insideId);
|
|
181
|
+
const defaultIndex =
|
|
182
|
+
inside >= 0
|
|
183
|
+
? inside
|
|
184
|
+
: Math.max(
|
|
185
|
+
0,
|
|
186
|
+
AGENTS.findIndex((a) => detected[a.id])
|
|
187
|
+
);
|
|
188
|
+
const answer = await promptText(`Pick [${defaultIndex + 1}]: `);
|
|
189
|
+
if (!answer) return AGENTS[defaultIndex];
|
|
190
|
+
|
|
191
|
+
const byNumber = Number.parseInt(answer, 10);
|
|
192
|
+
if (Number.isInteger(byNumber) && byNumber >= 1 && byNumber <= AGENTS.length) {
|
|
193
|
+
return AGENTS[byNumber - 1];
|
|
194
|
+
}
|
|
195
|
+
const byName = findAgent(answer);
|
|
196
|
+
if (byName) return byName;
|
|
197
|
+
throw new ConnectError(`Not a valid choice: ${answer}`, EXIT_USAGE);
|
|
198
|
+
}
|