@retasc/cli 1.1.0 → 1.1.2
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/dist/commands/mcp.js +54 -31
- package/dist/index.js +1 -1
- package/dist/lib/claim.js +12 -24
- package/dist/lib/keystore.js +41 -0
- package/dist/proxy.js +5 -18
- package/package.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -58,52 +58,75 @@ export function mcpMarkerEntry(workspaceId) {
|
|
|
58
58
|
export function mcpMarkerConfigBlock(workspaceId) {
|
|
59
59
|
return JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpMarkerEntry(workspaceId) } }, null, 2);
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// --- `claude mcp add` argv builders (pure, exported for tests) -------------
|
|
62
|
+
// RTSC-99: the stdio builders put SERVER_NAME *before* the variadic `--env`
|
|
63
|
+
// flags. `--env` consumes every following non-`--` token, so a server name after
|
|
64
|
+
// it was eaten as a bogus env var ("Invalid environment variable format: retasc").
|
|
65
|
+
// Name-first + a closing `--` keeps env values bounded and the command intact.
|
|
66
|
+
/** Build `claude mcp add` argv for the HTTP transport. (`url` is a positional
|
|
67
|
+
* BEFORE the variadic `--header`, so the header flag can't swallow it.) */
|
|
68
|
+
export function buildHttpAddArgs(url, key, scope) {
|
|
69
|
+
return [
|
|
64
70
|
"mcp", "add",
|
|
65
71
|
"--transport", "http",
|
|
66
72
|
"--scope", scope,
|
|
67
73
|
SERVER_NAME, url,
|
|
68
74
|
"--header", `Authorization: Bearer ${key}`,
|
|
69
75
|
];
|
|
70
|
-
return runClaudeAdd(args);
|
|
71
76
|
}
|
|
72
|
-
/**
|
|
73
|
-
function
|
|
74
|
-
|
|
77
|
+
/** Build argv for the stdio watchdog proxy (RTSC-44). */
|
|
78
|
+
export function buildWatchdogAddArgs(url, key, scope) {
|
|
79
|
+
return [
|
|
75
80
|
"mcp", "add",
|
|
81
|
+
SERVER_NAME,
|
|
76
82
|
"--transport", "stdio",
|
|
77
83
|
"--scope", scope,
|
|
78
84
|
"--env", `RETASC_MCP_URL=${url}`,
|
|
79
85
|
"--env", `RETASC_MCP_KEY=${key}`,
|
|
80
|
-
|
|
86
|
+
"--", "retasc", "mcp-proxy",
|
|
81
87
|
];
|
|
82
|
-
return runClaudeAdd(args);
|
|
83
88
|
}
|
|
84
|
-
/**
|
|
85
|
-
function
|
|
86
|
-
|
|
89
|
+
/** Build argv for the secret-free stdio marker (RTSC-92). */
|
|
90
|
+
export function buildMarkerAddArgs(workspaceId, scope) {
|
|
91
|
+
return [
|
|
87
92
|
"mcp", "add",
|
|
93
|
+
SERVER_NAME,
|
|
88
94
|
"--transport", "stdio",
|
|
89
95
|
"--scope", scope,
|
|
90
96
|
"--env", `RETASC_WORKSPACE=${workspaceId}`,
|
|
91
|
-
|
|
97
|
+
"--", "retasc", "mcp-proxy",
|
|
92
98
|
];
|
|
93
|
-
|
|
99
|
+
}
|
|
100
|
+
function tryClaudeCli(url, key, scope) {
|
|
101
|
+
return runClaudeAdd(buildHttpAddArgs(url, key, scope));
|
|
102
|
+
}
|
|
103
|
+
function tryClaudeCliWatchdog(url, key, scope) {
|
|
104
|
+
return runClaudeAdd(buildWatchdogAddArgs(url, key, scope));
|
|
105
|
+
}
|
|
106
|
+
function tryClaudeCliMarker(workspaceId, scope) {
|
|
107
|
+
return runClaudeAdd(buildMarkerAddArgs(workspaceId, scope));
|
|
94
108
|
}
|
|
95
109
|
function runClaudeAdd(args) {
|
|
96
110
|
const r = spawnSync("claude", args, { encoding: "utf8" });
|
|
97
|
-
if (r.error)
|
|
98
|
-
|
|
111
|
+
if (r.error) {
|
|
112
|
+
// ENOENT = `claude` not on PATH (genuinely absent). Any other spawn error is
|
|
113
|
+
// a real failure of a present CLI — report it as such, not as "not detected".
|
|
114
|
+
const absent = r.error.code === "ENOENT";
|
|
115
|
+
return { ok: false, absent, msg: absent ? undefined : r.error.message };
|
|
116
|
+
}
|
|
99
117
|
if (r.status !== 0) {
|
|
100
|
-
//
|
|
101
|
-
const msg = (r.stderr || r.stdout || "").trim()
|
|
102
|
-
|
|
103
|
-
console.error(` (claude mcp add: ${msg.split("\n")[0]})`);
|
|
104
|
-
return false;
|
|
118
|
+
// CLI ran but rejected the command — present, not absent. Surface the real reason.
|
|
119
|
+
const msg = (r.stderr || r.stdout || "").trim().split("\n")[0] || `exited ${r.status}`;
|
|
120
|
+
return { ok: false, absent: false, msg };
|
|
105
121
|
}
|
|
106
|
-
return true;
|
|
122
|
+
return { ok: true };
|
|
123
|
+
}
|
|
124
|
+
/** The fallback note printed after writing .mcp.json — names the REAL reason the
|
|
125
|
+
* CLI path didn't take, never falsely claiming the CLI is missing (RTSC-99). */
|
|
126
|
+
export function fallbackNote(res) {
|
|
127
|
+
return res.absent
|
|
128
|
+
? "(Claude Code CLI not detected — wrote .mcp.json instead.)"
|
|
129
|
+
: `(claude mcp add failed${res.msg ? `: ${res.msg}` : ""} — wrote .mcp.json instead.)`;
|
|
107
130
|
}
|
|
108
131
|
/** Merge a given server entry into ./.mcp.json (project scope on disk). */
|
|
109
132
|
function writeProjectMcpJson(entry) {
|
|
@@ -131,28 +154,28 @@ export function installMcp(opts) {
|
|
|
131
154
|
const scope = opts.scope ?? "local";
|
|
132
155
|
// Watchdog mode (RTSC-44): wire the stdio proxy so claims stay alive automatically.
|
|
133
156
|
if (opts.watchdog) {
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
157
|
+
const res = tryClaudeCliWatchdog(opts.url, opts.key, scope);
|
|
158
|
+
if (res.ok) {
|
|
136
159
|
console.log(`✓ Registered Retasc with the liveness watchdog (stdio proxy, scope: ${scope}).`);
|
|
137
160
|
}
|
|
138
161
|
else {
|
|
139
162
|
const path = writeProjectMcpJson(mcpProxyEntry(opts.url, opts.key));
|
|
140
163
|
console.log(`✓ Wrote watchdog MCP config to ${path}`);
|
|
141
|
-
console.log(
|
|
164
|
+
console.log(` ${fallbackNote(res)}`);
|
|
142
165
|
}
|
|
143
166
|
console.log("\nWatchdog MCP config (Codex / OpenCode / any stdio MCP client):\n");
|
|
144
167
|
console.log(mcpProxyConfigBlock(opts.url, opts.key));
|
|
145
168
|
console.log("\nThe watchdog keeps your claims alive automatically — no per-claim heartbeats. If it ever isn't running you just fall back to the normal lease timeout (safe).");
|
|
146
169
|
return;
|
|
147
170
|
}
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
171
|
+
const res = tryClaudeCli(opts.url, opts.key, scope);
|
|
172
|
+
if (res.ok) {
|
|
150
173
|
console.log(`✓ Registered MCP server "${SERVER_NAME}" with Claude Code (scope: ${scope}).`);
|
|
151
174
|
}
|
|
152
175
|
else {
|
|
153
176
|
const path = writeProjectMcpJson(mcpServerEntry(opts.url, opts.key));
|
|
154
177
|
console.log(`✓ Wrote MCP config to ${path}`);
|
|
155
|
-
console.log(
|
|
178
|
+
console.log(` ${fallbackNote(res)}`);
|
|
156
179
|
}
|
|
157
180
|
console.log("\nMCP config block (for any other agent/client):\n");
|
|
158
181
|
console.log(mcpConfigBlock(opts.url, opts.key));
|
|
@@ -165,14 +188,14 @@ export function installMcp(opts) {
|
|
|
165
188
|
*/
|
|
166
189
|
export function installMarker(opts) {
|
|
167
190
|
const scope = opts.scope ?? "local";
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
191
|
+
const res = tryClaudeCliMarker(opts.workspaceId, scope);
|
|
192
|
+
if (res.ok) {
|
|
170
193
|
console.log(`✓ Registered Retasc watchdog (secret-free marker, scope: ${scope}).`);
|
|
171
194
|
}
|
|
172
195
|
else {
|
|
173
196
|
const path = writeProjectMcpJson(mcpMarkerEntry(opts.workspaceId));
|
|
174
197
|
console.log(`✓ Wrote secret-free watchdog marker to ${path}`);
|
|
175
|
-
console.log(
|
|
198
|
+
console.log(` ${fallbackNote(res)}`);
|
|
176
199
|
}
|
|
177
200
|
console.log("\nMarker block (any stdio MCP client) — no secret, safe to commit:\n");
|
|
178
201
|
console.log(mcpMarkerConfigBlock(opts.workspaceId));
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const program = new Command();
|
|
|
15
15
|
program
|
|
16
16
|
.name("retasc")
|
|
17
17
|
.description("Retasc — sign in, create projects, mint agent API keys, and wire your agent to the MCP server.")
|
|
18
|
-
.version("1.1.
|
|
18
|
+
.version("1.1.1");
|
|
19
19
|
function requireLogin() {
|
|
20
20
|
if (!isLoggedIn()) {
|
|
21
21
|
console.error("Not signed in. Run `retasc login` first.");
|
package/dist/lib/claim.js
CHANGED
|
@@ -4,35 +4,23 @@
|
|
|
4
4
|
// ../commands/claim.ts.
|
|
5
5
|
import { readFileSync, existsSync } from "node:fs";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
|
+
import { resolveConn } from "./keystore.js";
|
|
7
8
|
const DEFAULT_MCP_URL = "https://mcp.retasc.com/mcp";
|
|
8
9
|
/**
|
|
9
10
|
* Resolve the agent API key + MCP URL the same way the workspace's MCP wiring does,
|
|
10
|
-
* so `retasc claim` speaks as the *same agent* the harness already uses.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `key` comes back empty if nothing is found, so the caller can error clearly.
|
|
11
|
+
* so `retasc claim` speaks as the *same agent* the harness already uses. Delegates
|
|
12
|
+
* to the shared `resolveConn` (RTSC-98) so it stays in lockstep with the proxy —
|
|
13
|
+
* covering explicit env, both legacy `.mcp.json` shapes, AND the canonical
|
|
14
|
+
* secret-free marker (`RETASC_WORKSPACE` → home keystore). `key` comes back empty
|
|
15
|
+
* if nothing is found, so the caller can error clearly.
|
|
16
16
|
*/
|
|
17
17
|
export function resolveMcpConn(opts = {}) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
key = entry.env.RETASC_MCP_KEY;
|
|
25
|
-
if (!url && typeof entry.env?.RETASC_MCP_URL === "string")
|
|
26
|
-
url = entry.env.RETASC_MCP_URL;
|
|
27
|
-
if (!url && typeof entry.url === "string")
|
|
28
|
-
url = entry.url;
|
|
29
|
-
if (!key && typeof entry.headers?.Authorization === "string") {
|
|
30
|
-
const m = entry.headers.Authorization.match(/^Bearer\s+(.+)$/i);
|
|
31
|
-
if (m)
|
|
32
|
-
key = m[1].trim();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return { url: url || opts.defaultUrl || DEFAULT_MCP_URL, key };
|
|
18
|
+
const { key, url } = resolveConn({
|
|
19
|
+
env: opts.env,
|
|
20
|
+
mcpEntry: opts.mcpJson?.mcpServers?.retasc,
|
|
21
|
+
defaultUrl: opts.defaultUrl || DEFAULT_MCP_URL,
|
|
22
|
+
});
|
|
23
|
+
return { url: url || DEFAULT_MCP_URL, key };
|
|
36
24
|
}
|
|
37
25
|
/** Read & parse the workspace `.mcp.json` (or null if absent/corrupt). */
|
|
38
26
|
export function readMcpJson(dir = process.cwd()) {
|
package/dist/lib/keystore.js
CHANGED
|
@@ -50,3 +50,44 @@ export function removeBinding(workspaceId) {
|
|
|
50
50
|
export function newWorkspaceId() {
|
|
51
51
|
return `ws_${randomUUID()}`;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* RTSC-98: the ONE place that resolves a workspace's key + MCP url. Shared by the
|
|
55
|
+
* proxy (env-based) and the direct commands `claim`/`tidy`/`done` (.mcp.json-based)
|
|
56
|
+
* so the two paths can never drift again. Precedence: explicit `RETASC_MCP_KEY`
|
|
57
|
+
* env → legacy inline key in the marker (env or Authorization header) → canonical
|
|
58
|
+
* secret-free marker (`RETASC_WORKSPACE` → home keystore). `mcpEntry` is a
|
|
59
|
+
* workspace `.mcp.json`'s `mcpServers.retasc` entry, or undefined when only the
|
|
60
|
+
* environment carries the binding (the spawned proxy).
|
|
61
|
+
*/
|
|
62
|
+
export function resolveConn(opts) {
|
|
63
|
+
const env = opts.env ?? process.env;
|
|
64
|
+
const entry = opts.mcpEntry;
|
|
65
|
+
let key = env.RETASC_MCP_KEY || "";
|
|
66
|
+
let url = env.RETASC_MCP_URL || "";
|
|
67
|
+
if (entry) {
|
|
68
|
+
if (!key && typeof entry.env?.RETASC_MCP_KEY === "string")
|
|
69
|
+
key = entry.env.RETASC_MCP_KEY;
|
|
70
|
+
if (!url && typeof entry.env?.RETASC_MCP_URL === "string")
|
|
71
|
+
url = entry.env.RETASC_MCP_URL;
|
|
72
|
+
if (!url && typeof entry.url === "string")
|
|
73
|
+
url = entry.url;
|
|
74
|
+
if (!key && typeof entry.headers?.Authorization === "string") {
|
|
75
|
+
const m = String(entry.headers.Authorization).match(/^Bearer\s+(.+)$/i);
|
|
76
|
+
if (m)
|
|
77
|
+
key = m[1].trim();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Canonical (RTSC-92): secret-free marker → workspace id → home keystore.
|
|
81
|
+
if (!key) {
|
|
82
|
+
const wsId = env.RETASC_WORKSPACE || entry?.env?.RETASC_WORKSPACE;
|
|
83
|
+
if (wsId) {
|
|
84
|
+
const b = getBinding(String(wsId));
|
|
85
|
+
if (b) {
|
|
86
|
+
key = b.key;
|
|
87
|
+
if (!url)
|
|
88
|
+
url = b.url;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { key, url: url || opts.defaultUrl || "" };
|
|
93
|
+
}
|
package/dist/proxy.js
CHANGED
|
@@ -8,24 +8,11 @@
|
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { hostname } from "node:os";
|
|
10
10
|
import { applyObservation, heartbeatRequest, isClaimLost, } from "./lib/watchdog.js";
|
|
11
|
-
import {
|
|
12
|
-
// RTSC-92: resolve the workspace key
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
const envUrl = process.env.RETASC_MCP_URL || "https://mcp.retasc.com/mcp";
|
|
17
|
-
if (process.env.RETASC_MCP_KEY) {
|
|
18
|
-
return { key: process.env.RETASC_MCP_KEY, url: envUrl };
|
|
19
|
-
}
|
|
20
|
-
const wsId = process.env.RETASC_WORKSPACE;
|
|
21
|
-
if (wsId) {
|
|
22
|
-
const entry = getBinding(wsId);
|
|
23
|
-
if (entry)
|
|
24
|
-
return { key: entry.key, url: entry.url || envUrl };
|
|
25
|
-
}
|
|
26
|
-
return { key: "", url: envUrl };
|
|
27
|
-
}
|
|
28
|
-
const resolved = resolveKeyUrl();
|
|
11
|
+
import { resolveConn } from "./lib/keystore.js";
|
|
12
|
+
// RTSC-92/98: resolve the workspace key via the SHARED resolver, so the proxy and
|
|
13
|
+
// the direct commands (claim/tidy/done) can never diverge. The proxy carries its
|
|
14
|
+
// binding in its own env (RETASC_MCP_KEY legacy, or RETASC_WORKSPACE → keystore).
|
|
15
|
+
const resolved = resolveConn({ env: process.env, defaultUrl: "https://mcp.retasc.com/mcp" });
|
|
29
16
|
let MCP_URL = resolved.url;
|
|
30
17
|
const KEY = resolved.key;
|
|
31
18
|
const HEARTBEAT_MS = Number(process.env.RETASC_HEARTBEAT_MS) || 10 * 60 * 1000;
|
package/package.json
CHANGED