@retasc/cli 1.40.0 → 1.41.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 +21 -0
- package/dist/commands/bind.js +11 -3
- package/dist/commands/import.js +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/keystore.js +3 -1
- package/dist/lib/session.js +64 -17
- package/dist/proxy.js +17 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ 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.41.1 (2026-09-04)
|
|
10
|
+
|
|
11
|
+
- **RTSC-812** — no runtime change. Two comments in `cli/src` said project deletion did
|
|
12
|
+
not exist: `bind` called an unwanted empty project something "that cannot be deleted
|
|
13
|
+
(org-granularity delete only)", and the `import` confirm said "there is no per-project
|
|
14
|
+
delete". An owner can delete a project from the Dash now, so both were wrong, and the
|
|
15
|
+
import one sat on the last screen before an irreversible write.
|
|
16
|
+
|
|
17
|
+
## 1.41.0 (2026-09-04)
|
|
18
|
+
|
|
19
|
+
- **RTSC-825** — every workspace now carries the name of the folder it is bound to. The
|
|
20
|
+
server never receives a path, so the name can only come from the machine that has one:
|
|
21
|
+
`retasc bind` sends it when the key is minted, and the watchdog proxy re-sends it at
|
|
22
|
+
every session start, so a folder bound before this existed is named the next time an
|
|
23
|
+
agent runs there. Only the folder's own name travels, never the path above it, and it
|
|
24
|
+
is the folder the BINDING names, so a session opened in a subdirectory still reports the
|
|
25
|
+
bound folder rather than renaming the row to whatever directory it started in. The
|
|
26
|
+
Dash shows it as `workspace` on the Agents page and on the Connect page, and shows
|
|
27
|
+
nothing at all where a workspace has not reported one; a key retired before this
|
|
28
|
+
shipped can never report one, so it stays blank.
|
|
29
|
+
|
|
9
30
|
## 1.40.0 (2026-09-04)
|
|
10
31
|
|
|
11
32
|
- **RTSC-820** — every session now tells Retasc which conversation it is. `retasc setup`
|
package/dist/commands/bind.js
CHANGED
|
@@ -427,7 +427,7 @@ export async function completeWorkspaceSetup(args) {
|
|
|
427
427
|
// act: it is one of the ways a first project comes into existence. The CLI used to
|
|
428
428
|
// ask only "New project name:", so someone arriving from Jira had to invent a project
|
|
429
429
|
// they did not want, run `retasc import` afterwards, and leave an empty one behind
|
|
430
|
-
//
|
|
430
|
+
// (deletable by an owner from the Dash since RTSC-812, but still a chore).
|
|
431
431
|
const made = await firstProject(orgId, org);
|
|
432
432
|
projectId = made.projectId;
|
|
433
433
|
prefix = made.prefix;
|
|
@@ -521,6 +521,9 @@ export async function completeWorkspaceSetup(args) {
|
|
|
521
521
|
// The server cleans the name and falls back to the project's own when the
|
|
522
522
|
// leaf is empty.
|
|
523
523
|
keyName: basename(cwd),
|
|
524
|
+
// RTSC-825 — and as the WORKSPACE, which is the field the Dash reads. `keyName`
|
|
525
|
+
// stays for the legacy label; nothing reads it as a folder any more.
|
|
526
|
+
workspace: basename(cwd),
|
|
524
527
|
}));
|
|
525
528
|
// The key is named in the receipt card below, not here — one mention, in the place
|
|
526
529
|
// that says where it went (RTSC-673).
|
|
@@ -646,7 +649,7 @@ export async function setupFromToken(token, opts, deps = {}) {
|
|
|
646
649
|
const cwd = process.cwd();
|
|
647
650
|
const guard = deps.guard ?? rebindGuard;
|
|
648
651
|
const redeem = deps.redeem ??
|
|
649
|
-
((t, label) => api.redeemSetupToken({ token: t, label }));
|
|
652
|
+
((t, label, workspace) => api.redeemSetupToken({ token: t, label, workspace }));
|
|
650
653
|
// BEFORE the redeem. `rebindGuard` prints its own explanation and, with no TTY,
|
|
651
654
|
// refuses with exit 1 unless `--yes` — which is exactly the required behaviour, so it
|
|
652
655
|
// is reused rather than restated. No org/project passed: its idempotent-converge
|
|
@@ -664,7 +667,12 @@ export async function setupFromToken(token, opts, deps = {}) {
|
|
|
664
667
|
// RTSC-532 — the LEAF name, never the full path. It names the key in the Dash, so it
|
|
665
668
|
// has to be recognisable ("client-a → ENG") without publishing where on her disk it
|
|
666
669
|
// sits. `basename` of the cwd is exactly that.
|
|
667
|
-
|
|
670
|
+
//
|
|
671
|
+
// RTSC-825 — sent TWICE, on purpose: once as the legacy `label` and once as the
|
|
672
|
+
// `workspace`. They happen to be the same string here and nowhere is that a rule —
|
|
673
|
+
// the label is a human field a future caller may set to anything, so the folder
|
|
674
|
+
// travels in its own argument rather than being read back out of it.
|
|
675
|
+
const redeemed = await redeem(token.trim(), basename(cwd), basename(cwd));
|
|
668
676
|
const workspaceId = existing?.workspaceId ?? newWorkspaceId();
|
|
669
677
|
(deps.bind ?? setBinding)(workspaceId, {
|
|
670
678
|
orgId: redeemed.orgId,
|
package/dist/commands/import.js
CHANGED
|
@@ -413,8 +413,8 @@ export async function runImportFlow(opts) {
|
|
|
413
413
|
}
|
|
414
414
|
// --- confirm ---------------------------------------------------------------
|
|
415
415
|
// Named in full before anything is written. An import creates a project's worth of
|
|
416
|
-
// issues and mints a placeholder per source author
|
|
417
|
-
//
|
|
416
|
+
// issues and mints a placeholder per source author; undoing a mistake means an owner
|
|
417
|
+
// deleting the whole project from the Dash (RTSC-812), placeholders and all.
|
|
418
418
|
console.log(`\nAbout to import ${clean(target.name)} from ${clean(src.label)} into org ${orgLabel},\n` +
|
|
419
419
|
`as a new project. Issues, comments and their authors come across.`);
|
|
420
420
|
// RTSC-526 — the second run is the dangerous one, and the terminal is where a command
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { VERSION } from "./version.js";
|
|
|
6
6
|
import { selfCommand, versionStamp } from "./lib/launcher.js";
|
|
7
7
|
import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./config.js";
|
|
8
8
|
import { installMcp, noteRuntimeIsALabel, normalizeScope } from "./commands/mcp.js";
|
|
9
|
-
import { runSetup } from "./commands/setup.js";
|
|
9
|
+
import { runSetup, printSetup } from "./commands/setup.js";
|
|
10
10
|
import { installGate, resolveGatePrefix } from "./commands/gate.js";
|
|
11
11
|
import { claimAction, releaseAction } from "./commands/claim.js";
|
|
12
12
|
import { bindAction, completeWorkspaceSetup, rebindGuard, setupFromToken } from "./commands/bind.js";
|
|
@@ -576,7 +576,7 @@ program
|
|
|
576
576
|
.option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
|
|
577
577
|
.allowExcessArguments(false)
|
|
578
578
|
.action((opts) => {
|
|
579
|
-
runSetup({ install: opts.install });
|
|
579
|
+
printSetup(runSetup({ install: opts.install, quiet: true }));
|
|
580
580
|
});
|
|
581
581
|
const mcp = program.command("mcp").description("Wire the Retasc MCP server into your agent.");
|
|
582
582
|
mcp
|
package/dist/lib/keystore.js
CHANGED
|
@@ -202,6 +202,7 @@ export function resolveConn(opts) {
|
|
|
202
202
|
// came from the keystore, so it travels only to the keystore's url. That is what
|
|
203
203
|
// lets `auto` be written into a GLOBAL harness config: the entry is identical on
|
|
204
204
|
// every machine and in every folder, and carries nothing worth stealing.
|
|
205
|
+
let boundPath = "";
|
|
205
206
|
if (!key) {
|
|
206
207
|
const wsId = env.RETASC_WORKSPACE || entry?.env?.RETASC_WORKSPACE;
|
|
207
208
|
const b = wsId === AUTO_WORKSPACE
|
|
@@ -217,7 +218,8 @@ export function resolveConn(opts) {
|
|
|
217
218
|
// url" is stated here outright rather than left resting on that invariant, so
|
|
218
219
|
// restoring an unpaired url above cannot quietly resurrect the RTSC-800 redirect.
|
|
219
220
|
url = b.url;
|
|
221
|
+
boundPath = b.boundPath ?? "";
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
|
-
return { key, url: url || opts.defaultUrl || "" };
|
|
224
|
+
return { key, url: url || opts.defaultUrl || "", boundPath };
|
|
223
225
|
}
|
package/dist/lib/session.js
CHANGED
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
// sessions collapse into one server-side identity and the per-session claim fence
|
|
6
6
|
// (RTSC-49/50) is silently inert.
|
|
7
7
|
import { toolResult } from "./toolresult.js";
|
|
8
|
-
// Out-of-band ids are negative so they never collide with the harness's (the
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Out-of-band ids are negative so they never collide with the harness's (the same
|
|
9
|
+
// convention as the proxy's heartbeat ids). One id per out-of-band call, named rather
|
|
10
|
+
// than counted: the offsets these replaced had drifted into a collision, with
|
|
11
|
+
// `announceBinding` and `record_session` both sending -1001.
|
|
12
|
+
export const RPC_ID = {
|
|
13
|
+
mint: -1000,
|
|
14
|
+
announce: -1001,
|
|
15
|
+
recordSession: -1002,
|
|
16
|
+
nameWorkspace: -1003,
|
|
17
|
+
};
|
|
18
|
+
const MINT_RPC_ID = RPC_ID.mint;
|
|
11
19
|
// The mint runs BEFORE the proxy serves `initialize`, so a hung server must not
|
|
12
20
|
// stall startup past the harness's MCP timeout — that would turn "degraded
|
|
13
21
|
// identity" into "no Retasc tools at all", the exact failure fail-soft exists
|
|
@@ -92,11 +100,17 @@ export function appendFallbackNotice(toolName, resp, degraded) {
|
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
103
|
+
* One out-of-band `tools/call` (RTSC-825). The proxy makes several of these around a
|
|
104
|
+
* session's start — mint the session key, announce the binding, record the transcript,
|
|
105
|
+
* name the folder — and they had been written out longhand each time, so a change to
|
|
106
|
+
* the transport (the auth header, the timeout, how a failure is surfaced) had to be
|
|
107
|
+
* made in three places and would have been made in one.
|
|
108
|
+
*
|
|
109
|
+
* Every caller here is best-effort: the session must serve MCP traffic whatever the
|
|
110
|
+
* server says, so this NEVER throws. `expect` is the field the tool returns on success;
|
|
111
|
+
* its presence is what separates a real result from an error envelope carrying prose.
|
|
98
112
|
*/
|
|
99
|
-
|
|
113
|
+
async function callTool(opts) {
|
|
100
114
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
101
115
|
const warn = opts.warn ?? ((msg) => process.stderr.write(`[retasc] ${msg}\n`));
|
|
102
116
|
try {
|
|
@@ -109,25 +123,58 @@ export async function recordSession(opts) {
|
|
|
109
123
|
},
|
|
110
124
|
body: JSON.stringify({
|
|
111
125
|
jsonrpc: "2.0",
|
|
112
|
-
id:
|
|
126
|
+
id: opts.id,
|
|
113
127
|
method: "tools/call",
|
|
114
|
-
params: {
|
|
115
|
-
name: "record_session",
|
|
116
|
-
arguments: { transcriptId: opts.transcriptId, ...(opts.harness ? { harness: opts.harness } : {}) },
|
|
117
|
-
},
|
|
128
|
+
params: { name: opts.name, arguments: opts.args },
|
|
118
129
|
}),
|
|
119
130
|
signal: AbortSignal.timeout(MINT_TIMEOUT_MS),
|
|
120
131
|
});
|
|
121
132
|
const text = await res.text();
|
|
122
133
|
if (!res.ok) {
|
|
123
|
-
warn(
|
|
124
|
-
|
|
134
|
+
warn(`${opts.name} failed (HTTP ${res.status})`);
|
|
135
|
+
// 5xx is the server having a bad minute; anything else is it saying no.
|
|
136
|
+
return res.status >= 500 ? "unreachable" : "refused";
|
|
125
137
|
}
|
|
126
138
|
const r = toolResult(text ? JSON.parse(text) : null, warn);
|
|
127
|
-
|
|
139
|
+
if (r && typeof r === "object" && opts.expect in r)
|
|
140
|
+
return "ok";
|
|
141
|
+
// A 200 carrying an error result: an unknown tool on an older deployment, a
|
|
142
|
+
// revoked key, the wrong kind of key. All of them are answers, not outages.
|
|
143
|
+
return "refused";
|
|
128
144
|
}
|
|
129
145
|
catch (e) {
|
|
130
|
-
warn(
|
|
131
|
-
return
|
|
146
|
+
warn(`${opts.name} failed: ${String(e?.message ?? e)}`);
|
|
147
|
+
return "unreachable";
|
|
132
148
|
}
|
|
133
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Record the harness's transcript id on THIS session's key (RTSC-820). Best effort and
|
|
152
|
+
* quiet: the worst case is the Dash saying "none recorded", which is what it said
|
|
153
|
+
* before this existed. Returns true once the server has it (already-recorded counts).
|
|
154
|
+
*/
|
|
155
|
+
export async function recordSession(opts) {
|
|
156
|
+
const out = await callTool({
|
|
157
|
+
...opts,
|
|
158
|
+
id: RPC_ID.recordSession,
|
|
159
|
+
name: "record_session",
|
|
160
|
+
args: { transcriptId: opts.transcriptId, ...(opts.harness ? { harness: opts.harness } : {}) },
|
|
161
|
+
expect: "transcriptId",
|
|
162
|
+
});
|
|
163
|
+
return out === "ok";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Tell Retasc which folder a workspace key is bound to (RTSC-825). Best effort and
|
|
167
|
+
* quiet, like `recordSession`: the worst case is the Dash omitting the workspace, which
|
|
168
|
+
* is what it did before this existed. "named" once the server holds this folder
|
|
169
|
+
* (already-correct counts).
|
|
170
|
+
*/
|
|
171
|
+
export async function nameWorkspace(opts) {
|
|
172
|
+
const out = await callTool({
|
|
173
|
+
...opts,
|
|
174
|
+
id: RPC_ID.nameWorkspace,
|
|
175
|
+
name: "name_workspace",
|
|
176
|
+
args: { workspace: opts.workspace },
|
|
177
|
+
expect: "workspace",
|
|
178
|
+
});
|
|
179
|
+
return out === "ok" ? "named" : out;
|
|
180
|
+
}
|
package/dist/proxy.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { hostname } from "node:os";
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
-
import { dirname, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { applyObservation, heartbeatRequest, isClaimLost, isUnauthorized, shouldReapOnClose, untrackedLeaseWarning, } from "./lib/watchdog.js";
|
|
14
14
|
import { AUTO_WORKSPACE, resolveConn } from "./lib/keystore.js";
|
|
15
15
|
import { toolResult as parseTool } from "./lib/toolresult.js";
|
|
16
|
-
import { mintSessionKey, appendFallbackNotice, recordSession } from "./lib/session.js";
|
|
16
|
+
import { mintSessionKey, appendFallbackNotice, recordSession, nameWorkspace, RPC_ID } from "./lib/session.js";
|
|
17
17
|
import { readHookRecord, clearHookRecord } from "./lib/sessionHook.js";
|
|
18
18
|
import { attachRoot, isLocalAttachCall, mergeAttachTool, readAttachFile, resolveAttachPath, uploadFailureMessage, uploadUrlWith, } from "./lib/attachFile.js";
|
|
19
19
|
import { MAX_FETCH_BYTES, downloadFailureMessage, existingDownload, isLocalFetchCall, mergeFetchTool, resolveDownloadTarget, writeDownloadedFile, } from "./lib/fetchFile.js";
|
|
@@ -251,7 +251,7 @@ async function announceBinding() {
|
|
|
251
251
|
try {
|
|
252
252
|
const resp = await postRemote({
|
|
253
253
|
jsonrpc: "2.0",
|
|
254
|
-
id:
|
|
254
|
+
id: RPC_ID.announce,
|
|
255
255
|
method: "tools/call",
|
|
256
256
|
params: { name: "whoami", arguments: {} },
|
|
257
257
|
});
|
|
@@ -627,6 +627,20 @@ export async function runProxy() {
|
|
|
627
627
|
// attributed to this session. stdin buffers in the OS pipe meanwhile.
|
|
628
628
|
await adoptSessionKey();
|
|
629
629
|
await announceBinding();
|
|
630
|
+
// RTSC-825 — the folder this session runs in IS its workspace, and this process is
|
|
631
|
+
// the only thing that knows it. Sent on the WORKSPACE key (not the session child just
|
|
632
|
+
// adopted), so a folder bound before the CLI reported one, or renamed since, corrects
|
|
633
|
+
// itself the next time an agent starts. A no-op server-side when it already matches.
|
|
634
|
+
//
|
|
635
|
+
// The BOUND folder, not the cwd. The `auto` marker resolves this key by walking up
|
|
636
|
+
// from wherever the harness was started, so a session opened in `retasc/dash` runs on
|
|
637
|
+
// `retasc`'s key — naming it `dash` would be wrong, and the next session from the root
|
|
638
|
+
// would rename it back, flapping the row on every start. Empty for a key that came
|
|
639
|
+
// from the environment rather than the keystore: that one names no folder.
|
|
640
|
+
const BOUND_FOLDER = resolved.boundPath ? basename(resolved.boundPath) : "";
|
|
641
|
+
if (KEY && BOUND_FOLDER) {
|
|
642
|
+
nameWorkspace({ url: MCP_URL, key: KEY, workspace: BOUND_FOLDER, warn: log }).catch((e) => log(`name_workspace: ${String(e?.message ?? e)}`));
|
|
643
|
+
}
|
|
630
644
|
reportTranscript().catch((e) => log(`record_session: ${String(e?.message ?? e)}`));
|
|
631
645
|
const timer = setInterval(() => void heartbeatAll(), HEARTBEAT_MS);
|
|
632
646
|
timer.unref?.(); // the timer alone must not keep the process alive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.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": {
|