@retasc/cli 1.47.0 → 1.48.1
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/CHANGELOG.md +43 -0
- package/dist/commands/bind.js +3 -0
- package/dist/commands/doctor.js +82 -10
- package/dist/commands/mcp.js +9 -0
- package/dist/index.js +20 -0
- package/dist/lib/binding.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,49 @@ release commits and the issues they reference.
|
|
|
6
6
|
|
|
7
7
|
Dates are the npm publish date. Each entry names the RTSC issue behind it.
|
|
8
8
|
|
|
9
|
+
## 1.48.1 (2026-09-08)
|
|
10
|
+
|
|
11
|
+
- **RTSC-878** — `retasc doctor` no longer condemns the entry `retasc setup` writes. Its
|
|
12
|
+
machine-global check dated from RTSC-91, when every top-level `mcpServers.retasc` carried
|
|
13
|
+
a key or a workspace id and so routed every unbound folder to one org; presence alone was
|
|
14
|
+
a correct test for that. RTSC-780 then made one global entry mandatory, the keyless `auto`
|
|
15
|
+
marker, which carries no identity at all, and the check was never taught the difference.
|
|
16
|
+
Doctor therefore printed `✗ GLOBAL Retasc server registered` at the marker and offered
|
|
17
|
+
`claude mcp remove -s user retasc` as the fix, which strips Retasc from every folder
|
|
18
|
+
without a binding of its own. It cost a customer a day.
|
|
19
|
+
- The check now reads the entry's identity: `RETASC_WORKSPACE=auto` with no key resolved is
|
|
20
|
+
reported as correct and named, so someone who just ran `setup` can see that what it wrote
|
|
21
|
+
is what belongs there. Every other shape stays illegal, including a keyless entry that
|
|
22
|
+
names a real `ws_` workspace, which still routes every unbound folder to one binding.
|
|
23
|
+
- The exemption is not granted on the sentinel alone. `parseServerEntry` reads
|
|
24
|
+
`RETASC_WORKSPACE` before it looks for a key in an `Authorization` header or in
|
|
25
|
+
`RETASC_MCP_KEY`, so an entry carrying both the `auto` sentinel and a real credential
|
|
26
|
+
parses as an innocent keyless marker; judging the parse alone would have exempted a live
|
|
27
|
+
machine-global registration pointing every unbound folder at that key's org. The raw
|
|
28
|
+
entry is examined too, and anything carrying credential material or a `url` of its own
|
|
29
|
+
stays illegal whatever its `env` claims.
|
|
30
|
+
|
|
31
|
+
## 1.48.0 (2026-09-08)
|
|
32
|
+
|
|
33
|
+
- **RTSC-859** — `retasc key mint --hosted`: a key now says at mint what it is for.
|
|
34
|
+
`workspace` (the default, and what every bind door says explicitly) expects the local
|
|
35
|
+
watchdog; `hosted` is for a cloud agent or CI that will never have one.
|
|
36
|
+
|
|
37
|
+
Why: a workspace key sent raw to the server with no proxy is two things at once and the
|
|
38
|
+
server cannot tell which, a hosted agent working as documented or a laptop whose config
|
|
39
|
+
went stale. Both got the same advice on every claim, run `bind`, which a hosted agent
|
|
40
|
+
cannot do. With `kind` on the row, a hosted key is told the one true thing (renew your
|
|
41
|
+
own leases) and shows "no folder (hosted)" on the Agents page as a fact rather than a
|
|
42
|
+
gap; a workspace key with no runner gets the firm remedy, because for that row it is
|
|
43
|
+
true. A key that said nothing, which includes every key minted before this and any bare
|
|
44
|
+
`key mint` since, keeps the old hedged text, since nothing on the row can say which it
|
|
45
|
+
is. `--hosted` and `--install` are refused together: one wires a local watchdog, the
|
|
46
|
+
other says there will never be one.
|
|
47
|
+
|
|
48
|
+
`retasc mcp install --no-watchdog` is not deprecated (it writes exactly a hosted
|
|
49
|
+
deployment's config) but now says, once, that a key used that way should be minted
|
|
50
|
+
`--hosted`.
|
|
51
|
+
|
|
9
52
|
## 1.47.0 (2026-09-08)
|
|
10
53
|
|
|
11
54
|
- **RTSC-864** — `retasc key mint` now prints every remote-key config block a hosted
|
package/dist/commands/bind.js
CHANGED
|
@@ -515,6 +515,9 @@ export async function completeWorkspaceSetup(args) {
|
|
|
515
515
|
projectId: projectId,
|
|
516
516
|
agentName: opts.agent,
|
|
517
517
|
runtime: opts.runtime ?? "claude-code",
|
|
518
|
+
// RTSC-859 — bind is the folder door, so this is a workspace key, and the row
|
|
519
|
+
// says so rather than leaving it to the server's default.
|
|
520
|
+
kind: "workspace",
|
|
518
521
|
// RTSC-810 — named after the FOLDER, the leaf only, exactly as the setup-token
|
|
519
522
|
// door has done since RTSC-532: the Keys list is the folder map (`client-a →
|
|
520
523
|
// ENG`), and the old prefix-plus-"key" name told nobody which folder held it.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { claudeConfigPath, isNetworkError, readGlobalBinding, readLocalBinding, readShadowedBinding, resolveBinding, sameIdentity, } from "../lib/binding.js";
|
|
3
|
-
import { getBinding } from "../lib/keystore.js";
|
|
3
|
+
import { AUTO_WORKSPACE, getBinding } from "../lib/keystore.js";
|
|
4
4
|
import { gitCommonDir } from "../lib/gitRepo.js";
|
|
5
5
|
import { detectHarnesses } from "../lib/harness.js";
|
|
6
6
|
import { runsOk } from "../lib/launcher.js";
|
|
@@ -16,8 +16,9 @@ import { clean } from "../lib/text.js";
|
|
|
16
16
|
// binding, which teaches people to distrust everything else it says. When both
|
|
17
17
|
// exist, we report the claude-local one (what Claude Code actually runs;
|
|
18
18
|
// local > project) and warn if a shadowed folder marker disagrees. The
|
|
19
|
-
// illegal-global check
|
|
20
|
-
// can't prove (unreadable config
|
|
19
|
+
// illegal-global check reads the entry's IDENTITY, not merely its presence
|
|
20
|
+
// (RTSC-878), and never prints an all-clear it can't prove (unreadable config
|
|
21
|
+
// ≠ no global server).
|
|
21
22
|
const ok = (m) => console.log(` ✓ ${m}`);
|
|
22
23
|
const warn = (m) => console.log(` ! ${m}`);
|
|
23
24
|
const bad = (m) => console.log(` ✗ ${m}`);
|
|
@@ -48,6 +49,65 @@ export function launcherVerdict(local, probe = runsOk) {
|
|
|
48
49
|
shown: clean([local.command, ...probeArgs].join(" ")),
|
|
49
50
|
};
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* RTSC-878: is the machine-global entry a real registration, or the marker `setup` writes?
|
|
54
|
+
*
|
|
55
|
+
* The old rule was presence alone, and it was correct when it was written (RTSC-91): every
|
|
56
|
+
* top-level `mcpServers.retasc` then carried a key or a workspace id, so any of them routed
|
|
57
|
+
* every unbound folder on the disk to one org. RTSC-780 changed that world by making ONE
|
|
58
|
+
* global entry mandatory, the keyless `auto` marker, which carries no identity at all: the
|
|
59
|
+
* proxy resolves the folder it was spawned in and the org still comes from the folder.
|
|
60
|
+
*
|
|
61
|
+
* Doctor kept the old rule and so told people to delete what `retasc setup` had just
|
|
62
|
+
* installed. Following that advice strips Retasc from every folder without a binding of its
|
|
63
|
+
* own, and it happened to a real customer before this was fixed.
|
|
64
|
+
*
|
|
65
|
+
* The test is the marker's OWN sentinel rather than a shape heuristic: `RETASC_WORKSPACE`
|
|
66
|
+
* is `auto` and no key resolved from it. Workspace ids are `ws_`-prefixed uuids
|
|
67
|
+
* (`keystore.ts`), so `auto` can never be one, and an entry naming a real workspace stays
|
|
68
|
+
* illegal exactly as before.
|
|
69
|
+
*
|
|
70
|
+
* The sentinel alone is NOT enough, and this is the trap the exemption has to survive.
|
|
71
|
+
* `parseServerEntry` reads `RETASC_WORKSPACE` BEFORE it looks for a key in an
|
|
72
|
+
* `Authorization` header or in `RETASC_MCP_KEY`, so an entry that carries BOTH the `auto`
|
|
73
|
+
* sentinel and a real credential parses as an innocent keyless marker. That shape is a
|
|
74
|
+
* live machine-global registration pointing every unbound folder at whatever org its key
|
|
75
|
+
* names, which is the exact fault this check exists to catch, and it is not theoretical:
|
|
76
|
+
* `lib/harness.ts` records an agent hand-writing a machine-global entry with an inline key.
|
|
77
|
+
* So the raw entry is examined too, and anything carrying credential material or its own
|
|
78
|
+
* endpoint is illegal no matter what its `env` claims.
|
|
79
|
+
*/
|
|
80
|
+
export function globalVerdict(global) {
|
|
81
|
+
if (global.status === "none")
|
|
82
|
+
return "clear";
|
|
83
|
+
if (global.status === "unreadable")
|
|
84
|
+
return "unreadable";
|
|
85
|
+
const b = global.binding;
|
|
86
|
+
if (b && b.workspaceId === AUTO_WORKSPACE && !b.key && isBareMarker(global.raw))
|
|
87
|
+
return "auto";
|
|
88
|
+
return "illegal";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Does the entry as written carry nothing but the `auto` sentinel?
|
|
92
|
+
*
|
|
93
|
+
* Deliberately a DENYLIST of credential-bearing and endpoint-bearing fields rather than an
|
|
94
|
+
* exact-shape match: `setup` may add a field to the marker later (it already grew `env`
|
|
95
|
+
* once), and an exemption that broke on every such addition would be reverted rather than
|
|
96
|
+
* updated. What must never be exempt is an entry that can reach a server on its own terms:
|
|
97
|
+
* a key in `env`, a key in a header, or a `url` of its own.
|
|
98
|
+
*/
|
|
99
|
+
function isBareMarker(raw) {
|
|
100
|
+
if (!raw || typeof raw !== "object")
|
|
101
|
+
return false;
|
|
102
|
+
const e = raw;
|
|
103
|
+
if (e.url !== undefined)
|
|
104
|
+
return false;
|
|
105
|
+
if (e.headers !== undefined)
|
|
106
|
+
return false;
|
|
107
|
+
if (e.env && typeof e.env === "object" && e.env.RETASC_MCP_KEY !== undefined)
|
|
108
|
+
return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
51
111
|
/**
|
|
52
112
|
* RTSC-498: macOS is the only platform this CLI is tested on. That was true and written
|
|
53
113
|
* down nowhere, which left someone on Linux or Windows unable to tell their own mistake
|
|
@@ -192,22 +252,34 @@ export async function doctorAction() {
|
|
|
192
252
|
}
|
|
193
253
|
// 2) The safety check: any illegal machine-global Retasc server?
|
|
194
254
|
const global = readGlobalBinding();
|
|
195
|
-
|
|
255
|
+
// Pulled out once: the verdict below is computed from the same entry, and reading it
|
|
256
|
+
// through the union again inside a branch loses the `present` narrowing.
|
|
257
|
+
const entry = global.status === "present" ? global.binding : undefined;
|
|
258
|
+
const verdict = globalVerdict(global);
|
|
259
|
+
if (verdict === "clear") {
|
|
196
260
|
ok("no illegal global Retasc server.");
|
|
197
261
|
}
|
|
198
|
-
else if (
|
|
262
|
+
else if (verdict === "auto") {
|
|
263
|
+
// RTSC-878: the entry `retasc setup` writes, and the one state doctor used to
|
|
264
|
+
// condemn. Named rather than passed over in silence: someone who just ran `setup`
|
|
265
|
+
// should see that the thing it wrote is the thing that is supposed to be there.
|
|
266
|
+
ok("machine-wide Retasc entry is the keyless `auto` marker `retasc setup` writes;\n" +
|
|
267
|
+
" it names no org, so every folder still resolves its own binding.");
|
|
268
|
+
}
|
|
269
|
+
else if (verdict === "unreadable") {
|
|
199
270
|
warn("could not parse Claude Code's config — cannot verify there is no global Retasc server\n" +
|
|
200
271
|
" (a local-scope binding for this folder would be invisible too).");
|
|
201
272
|
}
|
|
202
273
|
else {
|
|
203
274
|
// Name the org it points at when we can: "leaks across orgs" is abstract,
|
|
204
|
-
// "every unbound folder gets org X" is what makes someone act.
|
|
205
|
-
//
|
|
206
|
-
//
|
|
275
|
+
// "every unbound folder gets org X" is what makes someone act. A keyless entry
|
|
276
|
+
// that names a WORKSPACE is still illegal (it routes every unbound folder to that
|
|
277
|
+
// one binding); only the identity-free `auto` marker above is not. If the network
|
|
278
|
+
// already failed above, don't burn another timeout.
|
|
207
279
|
let points = "";
|
|
208
|
-
if (
|
|
280
|
+
if (entry?.key && !networkDown) {
|
|
209
281
|
try {
|
|
210
|
-
const b = await resolveBinding(
|
|
282
|
+
const b = await resolveBinding(entry.url || cfg.mcpUrl, entry.key);
|
|
211
283
|
points = ` It points at org "${clean(b.org.name)}".`;
|
|
212
284
|
}
|
|
213
285
|
catch {
|
package/dist/commands/mcp.js
CHANGED
|
@@ -267,6 +267,15 @@ export function installMcp(opts) {
|
|
|
267
267
|
console.log("Two things it does not cover, both safe (they fall back to the normal 30-minute lease timeout): if the proxy stops, renewal stops; and a claim made elsewhere — another terminal, direct HTTP MCP, or a previous proxy before a restart — is renewed by nobody. Heartbeat or checkpoint those yourself.");
|
|
268
268
|
return;
|
|
269
269
|
}
|
|
270
|
+
// RTSC-859 — this is the direct HTTP form, no watchdog, which is exactly a HOSTED
|
|
271
|
+
// deployment's shape. It is the correct config for one (RTSC-864 documents it), and it
|
|
272
|
+
// is NOT deprecated. But the key it wires does not know what it is for unless it was
|
|
273
|
+
// minted `--hosted`, and a workspace key used this way is told on every claim to go
|
|
274
|
+
// and wire the watchdog it is deliberately running without. Say so once, here, where
|
|
275
|
+
// the choice is being made, rather than warning on every claim afterwards.
|
|
276
|
+
console.log("\nNo watchdog: nothing on a machine will renew this key's claims. If this key runs " +
|
|
277
|
+
"from a cloud agent or CI, mint it with `retasc key mint --hosted` so Retasc knows " +
|
|
278
|
+
"and stops advising it to run `bind`; a hosted key is expected to heartbeat itself.");
|
|
270
279
|
const res = tryClaudeCli(opts.url, opts.key, scope);
|
|
271
280
|
if (res.ok) {
|
|
272
281
|
console.log(`✓ Registered MCP server "${SERVER_NAME}" with Claude Code (scope: ${scope}).`);
|
package/dist/index.js
CHANGED
|
@@ -365,10 +365,22 @@ key
|
|
|
365
365
|
.option("--agent <name>", "Agent member name (default: auto, \"{you}'s {runtime}\")")
|
|
366
366
|
.option("--runtime <runtime>", "Label for this agent in the Dash (claude-code | codex | grok | …). Does NOT choose where MCP config is written — `retasc setup` wires every harness on the machine.", "claude-code")
|
|
367
367
|
.option("--name <label>", "Key label")
|
|
368
|
+
// RTSC-859 — say at mint that this key will never have a local runner. Without it
|
|
369
|
+
// the server cannot tell a cloud agent working as documented from a laptop whose
|
|
370
|
+
// config went stale, and hands both the same advice: run `bind`, which a hosted
|
|
371
|
+
// agent cannot.
|
|
372
|
+
.option("--hosted", "This key runs somewhere with no local install (a cloud agent, CI): never told to run bind, never mistaken for a stale setup")
|
|
368
373
|
.option("--install", "Also wire the key into your agent via MCP")
|
|
369
374
|
.option("--scope <scope>", "MCP install scope if --install: local | project", "local")
|
|
370
375
|
.action(async (opts) => {
|
|
371
376
|
requireLogin();
|
|
377
|
+
// RTSC-859 — the two flags contradict each other. `--install` wires a local watchdog;
|
|
378
|
+
// `--hosted` declares there will never be one. Accepting both produced a row the Dash
|
|
379
|
+
// labelled "no local watchdog" for a laptop that was running one, silenced its
|
|
380
|
+
// stale-CLI nudge, and had the proxy's `name_workspace` refused on every start.
|
|
381
|
+
if (opts.hosted && opts.install) {
|
|
382
|
+
fail(new Error("--hosted and --install contradict each other: --install wires a local watchdog, --hosted says this key will never have one. Pick one."));
|
|
383
|
+
}
|
|
372
384
|
try {
|
|
373
385
|
const res = (await api.mintKey({
|
|
374
386
|
orgId: opts.orgId,
|
|
@@ -376,6 +388,14 @@ key
|
|
|
376
388
|
agentName: opts.agent,
|
|
377
389
|
runtime: opts.runtime,
|
|
378
390
|
keyName: opts.name,
|
|
391
|
+
// RTSC-859 — said only when the caller actually said something. `--hosted` is
|
|
392
|
+
// the word for a cloud agent or CI. `--install` wires the watchdog right here, so
|
|
393
|
+
// that key is a workspace key by construction. A BARE `key mint` says nothing:
|
|
394
|
+
// it is the documented hosted path (RTSC-864, llms.txt) AND the manual-wiring
|
|
395
|
+
// path, and stamping `workspace` on it sent every cloud agent minted that way a
|
|
396
|
+
// firm "re-run bind in that folder" for a folder that never existed. Absent keeps
|
|
397
|
+
// the honest hedge, which is exactly right for a key we know nothing about.
|
|
398
|
+
...(opts.hosted ? { kind: "hosted" } : opts.install ? { kind: "workspace" } : {}),
|
|
379
399
|
}));
|
|
380
400
|
console.log(`✓ Minted key: ${res.key}`);
|
|
381
401
|
console.log(" (Shown once — store it now.)");
|
package/dist/lib/binding.js
CHANGED
|
@@ -213,7 +213,7 @@ export function readGlobalBinding() {
|
|
|
213
213
|
const raw = doc?.mcpServers?.retasc;
|
|
214
214
|
if (!raw)
|
|
215
215
|
return { status: "none" };
|
|
216
|
-
return { status: "present", binding: parseServerEntry(raw, "global") };
|
|
216
|
+
return { status: "present", binding: parseServerEntry(raw, "global"), raw };
|
|
217
217
|
}
|
|
218
218
|
/** Two per-folder bindings are the SAME binding if they agree on workspace id
|
|
219
219
|
* (canonical form) or on the literal key (legacy forms). Anything else — or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.1",
|
|
4
4
|
"description": "Retasc CLI — the issue tracker AI agents pull work from. Sign in with GitHub or Google, 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": {
|