premanmcp 0.10.5 → 0.10.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.
- package/README.md +24 -2
- package/bin/api_tools.js +1 -1
- package/bin/cli.js +14 -3
- package/bin/connect.js +46 -0
- package/bin/hook.js +116 -1
- package/bin/runner.js +22 -6
- package/dist/server.js +25 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ npm exec -y premanmcp@latest -- connect --project
|
|
|
133
133
|
## What It Does
|
|
134
134
|
|
|
135
135
|
- Converts API endpoints into agent-callable MCP tools.
|
|
136
|
+
- 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
137
|
- Creates/connects a PreMan account from the terminal or IDE agent.
|
|
137
138
|
- Lets agents test real backend endpoints from the IDE.
|
|
138
139
|
- Syncs endpoint inventory across backend and frontend workflows.
|
|
@@ -166,6 +167,10 @@ Convert these endpoints into an MCP.
|
|
|
166
167
|
Show me the audit log for this hosted MCP.
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
```text
|
|
171
|
+
Move my Postman collection over to PreMan.
|
|
172
|
+
```
|
|
173
|
+
|
|
169
174
|
```text
|
|
170
175
|
Pull my pending PreMan fix tasks and fix the failing endpoint.
|
|
171
176
|
```
|
|
@@ -230,6 +235,7 @@ npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
|
|
|
230
235
|
```bash
|
|
231
236
|
preman hook install # Write the pre-push hook (connect does this too)
|
|
232
237
|
preman hook status # Installed? And does the command in it still answer?
|
|
238
|
+
preman hook repair # Rewrite it if it stopped working (usually automatic)
|
|
233
239
|
preman hook uninstall # Remove it, restoring any hook it replaced
|
|
234
240
|
```
|
|
235
241
|
|
|
@@ -242,17 +248,33 @@ than `@latest`, so upgrading us never changes what your pushes run; re-run `hook
|
|
|
242
248
|
to move it. Set `PREMAN_HOOK_INVOCATION` to write a command of your own instead, and
|
|
243
249
|
`PREMAN_SKIP_HOOK=1` to silence the hook for a push.
|
|
244
250
|
|
|
251
|
+
You should not have to run `repair`. A hook of ours that stops answering is rewritten in the
|
|
252
|
+
background by anything that proves PreMan runs here — any command, or your agent starting the
|
|
253
|
+
MCP server — because nothing re-runs `connect` after an upgrade to notice, and the broken
|
|
254
|
+
state is silent. It is narrow on purpose: a hook that still works keeps the version it pins, a
|
|
255
|
+
hook we did not write is never touched, and no hook is installed where you never asked for
|
|
256
|
+
one. The answer is remembered for an hour per directory, and `PREMAN_NO_HOOK_REPAIR=1` turns
|
|
257
|
+
it off.
|
|
258
|
+
|
|
245
259
|
### Runner
|
|
246
260
|
|
|
247
|
-
`connect --guide`
|
|
261
|
+
`preman onboard` and `connect --guide` set this up for you; these are for managing it
|
|
262
|
+
afterwards.
|
|
248
263
|
|
|
249
264
|
```bash
|
|
250
265
|
preman runner status # Paired? Running?
|
|
251
266
|
preman runner start --background # Hold the job stream, log to ~/.preman/runner.log
|
|
252
267
|
preman runner stop # Stop it and report offline
|
|
253
|
-
preman runner register --agent claude-code #
|
|
268
|
+
preman runner register --agent claude-code # Pair without starting
|
|
254
269
|
```
|
|
255
270
|
|
|
271
|
+
`start` pairs this machine itself when it is not paired yet, and re-pairs it when the
|
|
272
|
+
backend has revoked the token it held, so neither is a command you have to be told to run.
|
|
273
|
+
It works out which agent to pair as from the session it is running in, then from the agent
|
|
274
|
+
PreMan is already configured in, then from the only one installed — and asks only when
|
|
275
|
+
those disagree. Pass `--agent cursor|claude-code|codex` to settle it yourself, which is
|
|
276
|
+
also what a machine with no terminal to ask in needs.
|
|
277
|
+
|
|
256
278
|
The runner holds one outbound connection to PreMan and runs the work PreMan queues for
|
|
257
279
|
this machine — a failing endpoint becomes an agent run in your own repo instead of a
|
|
258
280
|
prompt you have to paste. It is bound to the agent and directory it was registered with
|
package/bin/api_tools.js
CHANGED
package/bin/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
connectCommand,
|
|
24
24
|
discoverEndpoints,
|
|
25
25
|
dispatchCommand,
|
|
26
|
+
resolveAgentForPairing,
|
|
26
27
|
writeCursorConfig,
|
|
27
28
|
} from "./connect.js";
|
|
28
29
|
import {
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
} from "./integrations.js";
|
|
35
36
|
import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
|
|
36
37
|
import { STATUS_HELP, statusCommand } from "./status.js";
|
|
37
|
-
import { HOOK_HELP, hookCommand } from "./hook.js";
|
|
38
|
+
import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
|
|
38
39
|
import { RUNNER_HELP, runnerCommand } from "./runner.js";
|
|
39
40
|
import { VERIFY_HELP, verifyCommand } from "./verify.js";
|
|
40
41
|
import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
|
|
@@ -135,6 +136,7 @@ async function loginCommand() {
|
|
|
135
136
|
await loginBrowser(cliArgs);
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
139
|
+
const cli = cliInvocation();
|
|
138
140
|
const creds = await authenticateTerminal(cliArgs);
|
|
139
141
|
process.stdout.write(`PreMan account ready.
|
|
140
142
|
|
|
@@ -153,6 +155,7 @@ You can now run:
|
|
|
153
155
|
* the same write plus agent choice, pairing, and dispatch setup.
|
|
154
156
|
*/
|
|
155
157
|
async function installCursorMcp() {
|
|
158
|
+
const cli = cliInvocation();
|
|
156
159
|
const serverName = argValue("--name", "preman");
|
|
157
160
|
const projectInstall = hasFlag("--project");
|
|
158
161
|
|
|
@@ -179,7 +182,7 @@ Server name: ${serverName}
|
|
|
179
182
|
Backend: ${serverConfig.env.PREMAN_BACKEND}
|
|
180
183
|
|
|
181
184
|
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.` :
|
|
185
|
+
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
186
|
2. Restart Cursor or toggle the PreMan MCP server off/on.
|
|
184
187
|
3. In your API repo, ask your coding agent:
|
|
185
188
|
"Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
|
|
@@ -216,6 +219,11 @@ function startServer() {
|
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
async function main() {
|
|
222
|
+
// Running at all is the signal. `hook` and `verify` own the hook themselves,
|
|
223
|
+
// and connect reinstalls it in the foreground, so a background rewrite there
|
|
224
|
+
// would only race with the one the user is watching.
|
|
225
|
+
if (!["hook", "verify", "connect"].includes(command)) scheduleHookRepair();
|
|
226
|
+
|
|
219
227
|
if (command === "login") {
|
|
220
228
|
await loginCommand();
|
|
221
229
|
} else if (command === "status") {
|
|
@@ -225,7 +233,10 @@ async function main() {
|
|
|
225
233
|
} else if (command === "hook") {
|
|
226
234
|
await hookCommand(commandArgs);
|
|
227
235
|
} else if (command === "runner") {
|
|
228
|
-
|
|
236
|
+
// Injected rather than imported inside runner.js, which connect.js already
|
|
237
|
+
// depends on: the agent picker lives there, and importing it back would
|
|
238
|
+
// close the cycle.
|
|
239
|
+
await runnerCommand(commandArgs, { resolveAgent: resolveAgentForPairing });
|
|
229
240
|
} else if (command === "logout") {
|
|
230
241
|
await logoutCommand();
|
|
231
242
|
} else if (command === "doctor") {
|
package/bin/connect.js
CHANGED
|
@@ -157,6 +157,52 @@ export function runningInside(env = process.env) {
|
|
|
157
157
|
return "";
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/** Is PreMan's MCP server already written into this agent's config? */
|
|
161
|
+
function agentIsLinked(agentId, serverName = "preman") {
|
|
162
|
+
try {
|
|
163
|
+
const home = os.homedir();
|
|
164
|
+
if (agentId === "cursor") {
|
|
165
|
+
return Boolean(readJsonFile(path.join(home, ".cursor", "mcp.json")).mcpServers?.[serverName]);
|
|
166
|
+
}
|
|
167
|
+
if (agentId === "claude_code") {
|
|
168
|
+
return Boolean(readJsonFile(path.join(home, ".claude.json")).mcpServers?.[serverName]);
|
|
169
|
+
}
|
|
170
|
+
if (agentId === "codex") {
|
|
171
|
+
const file = path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
|
|
172
|
+
return existsSync(file) && readFileSync(file, "utf8").includes(`[mcp_servers.${serverName}]`);
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// An unreadable config is not an answer; fall through to the other signals.
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Which agent this machine should pair as, or "" when only a person can say.
|
|
182
|
+
*
|
|
183
|
+
* Pairing needs an agent id, and demanding `--agent` for it turned "start the
|
|
184
|
+
* runner" into a command someone had to be told twice. The signals are ordered
|
|
185
|
+
* by how much they actually know: the session we are inside, then the agent
|
|
186
|
+
* PreMan is already configured in, then the only one installed. Asking is the
|
|
187
|
+
* last resort rather than the first, and a machine with no terminal to ask in
|
|
188
|
+
* gets "" so the caller can fail with a sentence instead of hanging on a prompt.
|
|
189
|
+
*/
|
|
190
|
+
export async function resolveAgentForPairing({ interactive = Boolean(process.stdin.isTTY) } = {}) {
|
|
191
|
+
const inside = runningInside();
|
|
192
|
+
if (inside) return inside;
|
|
193
|
+
|
|
194
|
+
const linked = AGENTS.filter((agent) => agentIsLinked(agent.id));
|
|
195
|
+
if (linked.length === 1) return linked[0].id;
|
|
196
|
+
|
|
197
|
+
const detected = detectAgents();
|
|
198
|
+
const present = AGENTS.filter((agent) => detected[agent.id]);
|
|
199
|
+
if (present.length === 1) return present[0].id;
|
|
200
|
+
|
|
201
|
+
if (!interactive) return "";
|
|
202
|
+
const picked = await promptAgentChoice(detected);
|
|
203
|
+
return picked?.id || "";
|
|
204
|
+
}
|
|
205
|
+
|
|
160
206
|
async function promptAgentChoice(detected, insideId = runningInside()) {
|
|
161
207
|
process.stdout.write("Which coding agent?\n");
|
|
162
208
|
AGENTS.forEach((agent, index) => {
|
package/bin/hook.js
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* by construction rather than by configuration.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
13
14
|
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
14
16
|
|
|
15
17
|
import { cliInvocation, makeArgs, packageVersion } from "./shared.js";
|
|
16
18
|
import { BLOCK_EXIT_CODE } from "./verify.js";
|
|
@@ -20,14 +22,23 @@ Hook options:
|
|
|
20
22
|
install Write the pre-push hook into this repository
|
|
21
23
|
uninstall Remove PreMan's pre-push hook
|
|
22
24
|
status Report whether the hook is installed, and still works
|
|
25
|
+
repair Rewrite a hook of ours whose command no longer answers
|
|
23
26
|
--force Overwrite a foreign pre-push hook (a backup is kept)
|
|
24
27
|
PREMAN_HOOK_INVOCATION=<cmd> Use this command line instead of probing for one
|
|
28
|
+
PREMAN_NO_HOOK_REPAIR=1 Never repair a hook in the background
|
|
25
29
|
`;
|
|
26
30
|
|
|
27
31
|
const MARKER = "# >>> preman pre-push >>>";
|
|
28
32
|
const END_MARKER = "# <<< preman pre-push <<<";
|
|
29
33
|
const HOOK_TIMEOUT_SECONDS = 120;
|
|
30
34
|
const PROBE_TIMEOUT_MS = 45000;
|
|
35
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const HEALTH_FILE = path.join(os.homedir(), ".preman", "hook-health.json");
|
|
37
|
+
// How long a healthy answer is believed for. The repair costs a probe, and a
|
|
38
|
+
// hook that worked an hour ago almost always still works, so this is the knob
|
|
39
|
+
// that keeps a background check off the critical path of every command.
|
|
40
|
+
const RECHECK_MS = 60 * 60 * 1000;
|
|
41
|
+
const HEALTH_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
31
42
|
|
|
32
43
|
function gitDir() {
|
|
33
44
|
const result = spawnSync("git", ["rev-parse", "--git-dir"], { encoding: "utf8" });
|
|
@@ -259,6 +270,94 @@ export function hookStatus({ probe = false } = {}) {
|
|
|
259
270
|
};
|
|
260
271
|
}
|
|
261
272
|
|
|
273
|
+
function readHookChecks() {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(readFileSync(HEALTH_FILE, "utf8"));
|
|
276
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
277
|
+
} catch {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Remember what a check found, so the next command does not pay for it again. */
|
|
283
|
+
function rememberHookCheck(cwd, now, fields) {
|
|
284
|
+
const checks = readHookChecks();
|
|
285
|
+
for (const [key, value] of Object.entries(checks)) {
|
|
286
|
+
if (now - Number(value?.at || 0) > HEALTH_TTL_MS) delete checks[key];
|
|
287
|
+
}
|
|
288
|
+
checks[cwd] = { at: now, ...fields };
|
|
289
|
+
try {
|
|
290
|
+
mkdirSync(path.dirname(HEALTH_FILE), { recursive: true });
|
|
291
|
+
writeFileSync(HEALTH_FILE, JSON.stringify(checks, null, 2) + "\n", { mode: 0o600 });
|
|
292
|
+
} catch {
|
|
293
|
+
// A cache we cannot write costs a probe next time and nothing else.
|
|
294
|
+
}
|
|
295
|
+
return { cwd, ...fields };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Put a hook of ours back into working order, without being asked.
|
|
300
|
+
*
|
|
301
|
+
* The failure this exists for is invisible: a hook written by a version that
|
|
302
|
+
* embedded the wrong command keeps printing one skipped line per push, and
|
|
303
|
+
* nothing re-runs `connect` after an upgrade to notice. So anything that proves
|
|
304
|
+
* PreMan is running here -- any command, the MCP server starting -- is taken as
|
|
305
|
+
* the moment to check.
|
|
306
|
+
*
|
|
307
|
+
* Deliberately narrow. It only ever rewrites a hook we wrote that no longer
|
|
308
|
+
* answers: an absent hook is not installed behind the user, a foreign hook is
|
|
309
|
+
* not touched, and a hook that works is left on whatever version it pins.
|
|
310
|
+
*/
|
|
311
|
+
export function repairDeadHook({ cwd = process.cwd(), now = Date.now(), force = false } = {}) {
|
|
312
|
+
const remember = (fields) => rememberHookCheck(cwd, now, fields);
|
|
313
|
+
if (!force) {
|
|
314
|
+
const last = readHookChecks()[cwd];
|
|
315
|
+
if (last && now - Number(last.at || 0) < RECHECK_MS) return { cwd, action: "recent" };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let status;
|
|
319
|
+
try {
|
|
320
|
+
status = hookStatus({ probe: true });
|
|
321
|
+
} catch {
|
|
322
|
+
return remember({ action: "not-a-repository" });
|
|
323
|
+
}
|
|
324
|
+
if (status.state !== "installed") return remember({ action: status.state });
|
|
325
|
+
if (status.works) return remember({ action: "healthy", invocation: status.invocation });
|
|
326
|
+
|
|
327
|
+
const result = installHook(makeArgs([]));
|
|
328
|
+
return remember({
|
|
329
|
+
action: result.action === "updated" ? "repaired" : result.action,
|
|
330
|
+
invocation: result.invocation || "",
|
|
331
|
+
replaced: status.invocation,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Run the repair in a detached child, because every step of it is synchronous
|
|
337
|
+
* and one of them shells out to npm. Nothing that merely wants a healthy hook
|
|
338
|
+
* should wait on a probe, least of all the MCP server, whose stdout is a
|
|
339
|
+
* protocol -- hence stdio ignored rather than inherited.
|
|
340
|
+
*/
|
|
341
|
+
export function scheduleHookRepair({ cwd = process.cwd(), now = Date.now() } = {}) {
|
|
342
|
+
if (process.env.PREMAN_HOOK) return { action: "skipped", reason: "running inside the hook" };
|
|
343
|
+
if (process.env.PREMAN_NO_HOOK_REPAIR) return { action: "skipped", reason: "disabled" };
|
|
344
|
+
const last = readHookChecks()[cwd];
|
|
345
|
+
if (last && now - Number(last.at || 0) < RECHECK_MS) {
|
|
346
|
+
return { action: "skipped", reason: "checked recently" };
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const child = spawn(process.execPath, [path.join(__dirname, "cli.js"), "hook", "repair"], {
|
|
350
|
+
cwd,
|
|
351
|
+
detached: true,
|
|
352
|
+
stdio: "ignore",
|
|
353
|
+
});
|
|
354
|
+
child.unref();
|
|
355
|
+
return { action: "spawned", pid: child.pid };
|
|
356
|
+
} catch {
|
|
357
|
+
return { action: "skipped", reason: "could not spawn" };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
262
361
|
export async function hookCommand(commandArgs = []) {
|
|
263
362
|
const sub = commandArgs.find((value) => !value.startsWith("-")) || "status";
|
|
264
363
|
const args = makeArgs(commandArgs);
|
|
@@ -279,6 +378,22 @@ export async function hookCommand(commandArgs = []) {
|
|
|
279
378
|
return result;
|
|
280
379
|
}
|
|
281
380
|
|
|
381
|
+
if (sub === "repair") {
|
|
382
|
+
// Asked for by hand, "repair" means now: the hourly gate exists to keep this
|
|
383
|
+
// off the critical path of unrelated commands, not to refuse a person.
|
|
384
|
+
const result = repairDeadHook({ force: !args.has("--if-stale") });
|
|
385
|
+
const said = {
|
|
386
|
+
repaired: () => `Pre-push hook repaired: it now runs ${result.invocation}\n`,
|
|
387
|
+
healthy: () => "Pre-push hook already works here.\n",
|
|
388
|
+
recent: () => "Pre-push hook checked recently; nothing to do.\n",
|
|
389
|
+
absent: () => `No PreMan hook here. Install one: ${cliInvocation()} hook install\n`,
|
|
390
|
+
foreign: () => "The pre-push hook here is not ours; left alone.\n",
|
|
391
|
+
unproven: () => "Cannot repair: no way to run the PreMan CLI was found here.\n",
|
|
392
|
+
}[result.action];
|
|
393
|
+
process.stdout.write(said ? said() : `Pre-push hook: ${result.action}\n`);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
282
397
|
if (sub === "uninstall") {
|
|
283
398
|
const result = uninstallHook();
|
|
284
399
|
process.stdout.write(
|
package/bin/runner.js
CHANGED
|
@@ -765,8 +765,9 @@ async function goOffline(args, state, log = defaultLog) {
|
|
|
765
765
|
|
|
766
766
|
export const RUNNER_HELP = `
|
|
767
767
|
Runner options:
|
|
768
|
-
runner start [--background] Hold the job stream and run queued work here
|
|
769
|
-
|
|
768
|
+
runner start [--background] Hold the job stream and run queued work here,
|
|
769
|
+
pairing this machine first if it is not paired
|
|
770
|
+
runner register [--agent a] Pair without starting (start does this for you)
|
|
770
771
|
runner status Report whether the local runner is running
|
|
771
772
|
runner stop Stop the background runner and go offline
|
|
772
773
|
--agent <name> claude-code | cursor | codex
|
|
@@ -858,7 +859,9 @@ async function startForeground(args, commandArgs) {
|
|
|
858
859
|
const agent = normalizeAgentId(args.value("--agent", "")) || "";
|
|
859
860
|
if (!agent) {
|
|
860
861
|
throw new RunnerError(
|
|
861
|
-
|
|
862
|
+
"This machine is not paired as a runner yet, and which agent to pair as could not be" +
|
|
863
|
+
` worked out here. Say so directly: '${cliInvocation()} runner start --background` +
|
|
864
|
+
" --agent cursor|claude-code|codex'.",
|
|
862
865
|
2
|
|
863
866
|
);
|
|
864
867
|
}
|
|
@@ -915,9 +918,22 @@ async function startForeground(args, commandArgs) {
|
|
|
915
918
|
}
|
|
916
919
|
}
|
|
917
920
|
|
|
918
|
-
export async function runnerCommand(commandArgs = []) {
|
|
921
|
+
export async function runnerCommand(commandArgs = [], { resolveAgent = null } = {}) {
|
|
919
922
|
const sub = commandArgs.find((value) => !value.startsWith("-")) || "status";
|
|
920
|
-
|
|
923
|
+
let args = makeArgs(commandArgs);
|
|
924
|
+
|
|
925
|
+
// Pairing needs an agent id, and the only place that can be asked for one is
|
|
926
|
+
// here: `--background` hands the work to a detached child with no terminal, so
|
|
927
|
+
// a question deferred to it is a question nobody ever sees.
|
|
928
|
+
if ((sub === "register" || sub === "start") && resolveAgent && !args.value("--agent", "")) {
|
|
929
|
+
if (sub === "register" || !readRunnerState()) {
|
|
930
|
+
const resolved = await resolveAgent();
|
|
931
|
+
if (resolved) {
|
|
932
|
+
commandArgs = [...commandArgs, "--agent", resolved];
|
|
933
|
+
args = makeArgs(commandArgs);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
921
937
|
|
|
922
938
|
if (sub === "register") {
|
|
923
939
|
const agent = normalizeAgentId(args.value("--agent", ""));
|
|
@@ -966,7 +982,7 @@ export async function runnerCommand(commandArgs = []) {
|
|
|
966
982
|
const alive = runnerIsAlive(pid);
|
|
967
983
|
if (!state) {
|
|
968
984
|
process.stdout.write(
|
|
969
|
-
`Local runner: not paired\n
|
|
985
|
+
`Local runner: not paired\n Start it: ${cliInvocation()} runner start --background\n`
|
|
970
986
|
);
|
|
971
987
|
return { state: "unpaired" };
|
|
972
988
|
}
|
package/dist/server.js
CHANGED
|
@@ -3,10 +3,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerUserAuthFlowTools } from "./user_auth_flow.js";
|
|
5
5
|
import { shareAuthFlowToUi } from "./auth_flow_ui.js";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import fs from "node:fs/promises";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import os from "node:os";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
10
12
|
import { randomUUID } from "node:crypto";
|
|
11
13
|
import { MCP_PREVIEW_RESOURCE_URI, RESOURCE_URI_META_KEY, buildConversionPanelHtml, writeMcpPreviewFile, } from "./mcp-preview-panel.js";
|
|
12
14
|
let REPO_CONFIG = null;
|
|
@@ -1444,11 +1446,34 @@ export function createServer() {
|
|
|
1444
1446
|
return server;
|
|
1445
1447
|
}
|
|
1446
1448
|
// ── Start ─────────────────────────────────────────────────────────────
|
|
1449
|
+
/**
|
|
1450
|
+
* A pre-push hook PreMan wrote can stop working — the machine gained a `preman`
|
|
1451
|
+
* belonging to another package, or lost the one it pinned — and the only symptom
|
|
1452
|
+
* is one skipped line per push. This process starting is proof PreMan runs here,
|
|
1453
|
+
* so it is a good moment to check. Detached with stdio ignored: the check shells
|
|
1454
|
+
* out to npm, and this process's stdout is the MCP protocol.
|
|
1455
|
+
*/
|
|
1456
|
+
function repairPushHookInBackground() {
|
|
1457
|
+
try {
|
|
1458
|
+
const cli = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "cli.js");
|
|
1459
|
+
if (!existsSync(cli))
|
|
1460
|
+
return;
|
|
1461
|
+
const child = spawn(process.execPath, [cli, "hook", "repair", "--if-stale"], {
|
|
1462
|
+
detached: true,
|
|
1463
|
+
stdio: "ignore",
|
|
1464
|
+
});
|
|
1465
|
+
child.unref();
|
|
1466
|
+
}
|
|
1467
|
+
catch {
|
|
1468
|
+
// Never worth failing a server start over.
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1447
1471
|
async function main() {
|
|
1448
1472
|
await initAuth();
|
|
1449
1473
|
const server = createServer();
|
|
1450
1474
|
const transport = new StdioServerTransport();
|
|
1451
1475
|
await server.connect(transport);
|
|
1476
|
+
repairPushHookInBackground();
|
|
1452
1477
|
// Starting the configured MCP server is itself proof that the coding agent
|
|
1453
1478
|
// is live. Previously the workbench remained in `connecting` until someone
|
|
1454
1479
|
// explicitly asked the agent to call `preman_status`, even when this process
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.6",
|
|
4
4
|
"description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
|
|
16
16
|
"test": "npm run build && npm run test:connect && npm run test:node",
|
|
17
17
|
"test:connect": "node scripts/smoke-connect.mjs",
|
|
18
|
-
"test:node": "node --test --test-timeout=
|
|
18
|
+
"test:node": "node --test --test-timeout=90000 scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-api-tools.mjs scripts/smoke-bin-scope.mjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@modelcontextprotocol/ext-apps": "^0.1.0",
|