@retasc/cli 1.2.2 → 1.2.4
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 +17 -4
- package/dist/config.js +7 -3
- package/dist/lib/keystore.js +5 -3
- package/dist/lib/watchdog.js +26 -3
- package/package.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
export const SERVER_NAME = "retasc";
|
|
5
5
|
/** Normalize a user-supplied scope string. `user` (global) is refused and
|
|
@@ -128,8 +128,12 @@ export function fallbackNote(res) {
|
|
|
128
128
|
? "(Claude Code CLI not detected — wrote .mcp.json instead.)"
|
|
129
129
|
: `(claude mcp add failed${res.msg ? `: ${res.msg}` : ""} — wrote .mcp.json instead.)`;
|
|
130
130
|
}
|
|
131
|
-
/** Merge a given server entry into ./.mcp.json (project scope on disk).
|
|
132
|
-
|
|
131
|
+
/** Merge a given server entry into ./.mcp.json (project scope on disk).
|
|
132
|
+
* The inline-key fallback forms (mcpServerEntry / mcpProxyEntry) embed the API
|
|
133
|
+
* key, so lock the file to 0600. The secret-free marker (mcpMarkerEntry) doesn't
|
|
134
|
+
* need it, but chmod'ing unconditionally is harmless and keeps this the single
|
|
135
|
+
* correct path. Exported for the permission test. */
|
|
136
|
+
export function writeProjectMcpJson(entry) {
|
|
133
137
|
const path = join(process.cwd(), ".mcp.json");
|
|
134
138
|
let doc = {};
|
|
135
139
|
if (existsSync(path)) {
|
|
@@ -142,7 +146,16 @@ function writeProjectMcpJson(entry) {
|
|
|
142
146
|
}
|
|
143
147
|
doc.mcpServers = doc.mcpServers ?? {};
|
|
144
148
|
doc.mcpServers[SERVER_NAME] = entry;
|
|
145
|
-
|
|
149
|
+
// May carry an inline API key — create it 0600 so it's never world-readable,
|
|
150
|
+
// even for the sub-ms before the chmod (which only closes an existing 0644 file,
|
|
151
|
+
// since the write mode above doesn't apply when overwriting).
|
|
152
|
+
writeFileSync(path, JSON.stringify(doc, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
153
|
+
try {
|
|
154
|
+
chmodSync(path, 0o600);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* best effort (e.g. Windows) */
|
|
158
|
+
}
|
|
146
159
|
return path;
|
|
147
160
|
}
|
|
148
161
|
/**
|
package/dist/config.js
CHANGED
|
@@ -36,9 +36,13 @@ export function loadConfig() {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
export function saveConfig(cfg) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
39
|
+
// Create the dir user-only and the file 0600 on the initial write, so tokens
|
|
40
|
+
// never exist world-readable even briefly (the chmod below only closes a
|
|
41
|
+
// pre-existing 0644 file — it can't undo a world-readable creation window).
|
|
42
|
+
mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
|
43
|
+
writeFileSync(FILE, JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
44
|
+
// Tokens live here — keep it user-only readable (covers overwriting an
|
|
45
|
+
// existing 0644 file, where the write mode above doesn't apply).
|
|
42
46
|
try {
|
|
43
47
|
chmodSync(FILE, 0o600);
|
|
44
48
|
}
|
package/dist/lib/keystore.js
CHANGED
|
@@ -23,11 +23,13 @@ export function loadKeystore() {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
export function saveKeystore(ks) {
|
|
26
|
-
|
|
26
|
+
// Dir user-only, file 0600 on the initial write — the keystore holds API keys,
|
|
27
|
+
// so it must never exist world-readable even for the sub-ms before the chmod.
|
|
28
|
+
mkdirSync(keystoreDir(), { recursive: true, mode: 0o700 });
|
|
27
29
|
const p = keystorePath();
|
|
28
|
-
writeFileSync(p, JSON.stringify(ks, null, 2) + "\n", "utf8");
|
|
30
|
+
writeFileSync(p, JSON.stringify(ks, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
29
31
|
try {
|
|
30
|
-
chmodSync(p, 0o600); // secrets — user-only
|
|
32
|
+
chmodSync(p, 0o600); // secrets — user-only (covers overwriting an existing 0644 file)
|
|
31
33
|
}
|
|
32
34
|
catch {
|
|
33
35
|
/* best effort (e.g. Windows) */
|
package/dist/lib/watchdog.js
CHANGED
|
@@ -57,10 +57,33 @@ export function heartbeatRequest(rpcId, issueId, claimToken) {
|
|
|
57
57
|
params: { name: "heartbeat", arguments: { identifier: issueId, claimToken } },
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* A heartbeat CLAIM_LOST means the lease is gone — stop tracking it.
|
|
62
|
+
*
|
|
63
|
+
* The server throws `Error("CLAIM_LOST: …")` (convex/lib/claims.ts), and http.ts
|
|
64
|
+
* surfaces that as an MCP tool ERROR: `{ isError: true, content:[{text:"CLAIM_LOST: …"}] }`.
|
|
65
|
+
* Match ONLY that signal (or a top-level `error`/`code` field, for a future
|
|
66
|
+
* structured shape) — NEVER `JSON.stringify` the whole result and regex it, or a
|
|
67
|
+
* benign payload field that merely CONTAINS "CLAIM_LOST" (an issue title, a
|
|
68
|
+
* checkpoint note quoting an error, a comment body) would silently drop a live
|
|
69
|
+
* lease and let the reclaimer hand our work to someone else (RTSC-150).
|
|
70
|
+
*/
|
|
61
71
|
export function isClaimLost(result) {
|
|
62
72
|
if (!result || typeof result !== "object")
|
|
63
73
|
return false;
|
|
64
|
-
const
|
|
65
|
-
|
|
74
|
+
const r = result;
|
|
75
|
+
// A structured error signal on a top-level field (belt-and-suspenders; also the
|
|
76
|
+
// shape the older unit test asserts). Bounded to top-level scalars, not nested.
|
|
77
|
+
if (r.code === "CLAIM_LOST")
|
|
78
|
+
return true;
|
|
79
|
+
if (typeof r.error === "string" && /CLAIM_LOST/.test(r.error))
|
|
80
|
+
return true;
|
|
81
|
+
// The live shape: only an MCP error envelope, and only its text blocks — success
|
|
82
|
+
// payloads (isError absent/false) are never treated as a lost claim.
|
|
83
|
+
if (r.isError !== true || !Array.isArray(r.content))
|
|
84
|
+
return false;
|
|
85
|
+
return r.content.some((c) => c !== null &&
|
|
86
|
+
typeof c === "object" &&
|
|
87
|
+
typeof c.text === "string" &&
|
|
88
|
+
/CLAIM_LOST/.test(c.text));
|
|
66
89
|
}
|
package/package.json
CHANGED