@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.
@@ -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
- function writeProjectMcpJson(entry) {
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
- writeFileSync(path, JSON.stringify(doc, null, 2) + "\n", "utf8");
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
- mkdirSync(DIR, { recursive: true });
40
- writeFileSync(FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
41
- // Tokens live herekeep it user-only readable.
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
  }
@@ -23,11 +23,13 @@ export function loadKeystore() {
23
23
  }
24
24
  }
25
25
  export function saveKeystore(ks) {
26
- mkdirSync(keystoreDir(), { recursive: true });
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) */
@@ -57,10 +57,33 @@ export function heartbeatRequest(rpcId, issueId, claimToken) {
57
57
  params: { name: "heartbeat", arguments: { identifier: issueId, claimToken } },
58
58
  };
59
59
  }
60
- /** A heartbeat result of CLAIM_LOST means the lease is gone — stop tracking it. */
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 text = JSON.stringify(result);
65
- return /CLAIM_LOST/.test(text);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Retasc CLI — sign in with GitHub, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
5
5
  "type": "module",
6
6
  "bin": {