@retasc/cli 1.18.0 → 1.20.0
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 +38 -0
- package/dist/api.js +19 -0
- package/dist/commands/billing.js +4 -2
- package/dist/commands/bind.js +50 -2
- package/dist/commands/join.js +3 -2
- package/dist/index.js +20 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,44 @@ 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.20.0 (2026-08-04)
|
|
10
|
+
|
|
11
|
+
- **RTSC-561** — the org gained an **admin** role, and the CLI stopped refusing it.
|
|
12
|
+
`retasc billing` gated on `role !== "owner"` client-side, so it would have refused an
|
|
13
|
+
admin locally for a call the server allows. Owner-only copy in `bind` and the command
|
|
14
|
+
help now names both roles.
|
|
15
|
+
|
|
16
|
+
## 1.19.0 (2026-08-03)
|
|
17
|
+
|
|
18
|
+
- **RTSC-495** — `retasc bind --setup <code>` sets a folder up with no sign-in and nothing
|
|
19
|
+
to answer, so an agent can do it on behalf of someone who does not use a terminal.
|
|
20
|
+
|
|
21
|
+
The Dash's connect step now hands over a block you paste to your agent, with this command
|
|
22
|
+
inside it. Everything the interactive `bind` would ask was already answered in the
|
|
23
|
+
browser, and the code carries those answers across. An agent's shell is not a TTY, so the
|
|
24
|
+
ordinary path refuses it outright at the first prompt.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
npx @retasc/cli bind --setup rtscsetup_…
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The code is single-use and lives ten minutes. The alternative — pasting a real API key
|
|
31
|
+
into the prompt — needed no backend at all, but would have left a long-lived credential in
|
|
32
|
+
an agent's conversation history forever.
|
|
33
|
+
|
|
34
|
+
A folder that is already connected is refused rather than silently replaced, and the
|
|
35
|
+
refusal happens BEFORE the code is spent, so a mix-up costs nothing.
|
|
36
|
+
|
|
37
|
+
- **RTSC-532** — the agent says which folder it is in, and the key remembers.
|
|
38
|
+
|
|
39
|
+
Binding the wrong folder used to look exactly like success: the agent still called in, so
|
|
40
|
+
the Dash went green, while the folder you actually work in had no Retasc in it. The prompt
|
|
41
|
+
now asks the agent to report its full path and wait before it runs anything, and the
|
|
42
|
+
confirmation names the real path instead of "This folder".
|
|
43
|
+
|
|
44
|
+
The key it creates is named after that folder, so the Keys page shows which folder each
|
|
45
|
+
key belongs to (`client-a`) instead of naming them all after the project (`ENG key`).
|
|
46
|
+
|
|
9
47
|
## 1.18.0 (2026-08-02)
|
|
10
48
|
|
|
11
49
|
- **RTSC-530** — setting up from scratch now asks where your work comes from, and imports it
|
package/dist/api.js
CHANGED
|
@@ -43,6 +43,10 @@ const fns = {
|
|
|
43
43
|
claimableGhosts: makeFunctionReference("ghosts:claimableGhosts"),
|
|
44
44
|
claimGhost: makeFunctionReference("ghosts:claimGhost"),
|
|
45
45
|
dismissGhostPrompt: makeFunctionReference("ghosts:dismissGhostPrompt"),
|
|
46
|
+
// RTSC-495 — the ONLY unauthenticated call the CLI makes. Redeemed by `bind --setup`
|
|
47
|
+
// on a machine that has never signed in, because the human answered everything in the
|
|
48
|
+
// Dash and it is her agent, not her, running this.
|
|
49
|
+
redeemSetupToken: makeFunctionReference("setupToken:redeemSetupToken"),
|
|
46
50
|
};
|
|
47
51
|
function client() {
|
|
48
52
|
const cfg = loadConfig();
|
|
@@ -51,6 +55,18 @@ function client() {
|
|
|
51
55
|
c.setAuth(cfg.token);
|
|
52
56
|
return c;
|
|
53
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A client that NEVER attaches a session (RTSC-495).
|
|
60
|
+
*
|
|
61
|
+
* `client()` attaches `cfg.token` whenever one exists, and a setup-token redeem must not
|
|
62
|
+
* depend on it either way: the machine usually has no session at all, and where it has a
|
|
63
|
+
* STALE one, sending it risks failing an auth check on a call that never needed identity.
|
|
64
|
+
* Separate function rather than a flag, so "this call is unauthenticated" is visible at
|
|
65
|
+
* the call site instead of buried in an argument.
|
|
66
|
+
*/
|
|
67
|
+
function anonClient() {
|
|
68
|
+
return new ConvexHttpClient(loadConfig().deploymentUrl);
|
|
69
|
+
}
|
|
54
70
|
/**
|
|
55
71
|
* Read a caught backend error into the parts a command wants to show (RTSC-261).
|
|
56
72
|
*
|
|
@@ -178,6 +194,9 @@ export const api = {
|
|
|
178
194
|
runImport: (args) => withAuth(() => client().action(fns.runImport, args)),
|
|
179
195
|
latestImport: (args) => withAuth(() => client().query(fns.latestImport, args)),
|
|
180
196
|
importHistory: (args) => withAuth(() => client().query(fns.importHistory, args)),
|
|
197
|
+
// NOT wrapped in `withAuth`: there is no session to refresh, and its retry path would
|
|
198
|
+
// start a device flow — the exact interactive prompt this whole flow exists to avoid.
|
|
199
|
+
redeemSetupToken: (args) => anonClient().action(fns.redeemSetupToken, args),
|
|
181
200
|
claimableGhosts: (args) => withAuth(() => client().query(fns.claimableGhosts, args)),
|
|
182
201
|
claimGhost: (args) => withAuth(() => client().mutation(fns.claimGhost, args)),
|
|
183
202
|
dismissGhostPrompt: (args) => withAuth(() => client().mutation(fns.dismissGhostPrompt, args)),
|
package/dist/commands/billing.js
CHANGED
|
@@ -44,8 +44,10 @@ export async function billingAction(opts) {
|
|
|
44
44
|
const org = me.orgs.find((o) => o.id === orgId);
|
|
45
45
|
orgLabel = org ? `${org.name}${org.slug ? ` (${org.slug})` : ""}` : String(orgId);
|
|
46
46
|
// Fail early with the actionable message rather than a raw FORBIDDEN from the server.
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Owner OR admin (RTSC-561) — this mirrors `requireOwnerOrAdmin`, and a stale copy of
|
|
48
|
+
// the rule here would refuse an admin locally for a call the server would have allowed.
|
|
49
|
+
if (org && org.role !== "owner" && org.role !== "admin") {
|
|
50
|
+
throw new Error(`Billing needs the owner or admin role — yours in ${orgLabel} is "${org.role}".`);
|
|
49
51
|
}
|
|
50
52
|
const [status, charges] = await Promise.all([
|
|
51
53
|
api.billingStatus({ orgId: orgId }),
|
package/dist/commands/bind.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { api, cliError } from "../api.js";
|
|
2
3
|
import { deviceLogin } from "../auth.js";
|
|
3
4
|
import { loadConfig, patchConfig } from "../config.js";
|
|
@@ -285,7 +286,7 @@ export async function completeWorkspaceSetup(args) {
|
|
|
285
286
|
let prefix;
|
|
286
287
|
if (!projectId && opts.project && opts.prefix) {
|
|
287
288
|
if (!args.canCreateProject) {
|
|
288
|
-
cliError("FORBIDDEN", "Only an owner can create a project.", `Ask
|
|
289
|
+
cliError("FORBIDDEN", "Only an owner or admin can create a project.", `Ask one of them to add one, then pass --project-id <id>.`);
|
|
289
290
|
}
|
|
290
291
|
const p = (await api.createProject({ orgId, name: opts.project, prefix: opts.prefix }));
|
|
291
292
|
projectId = p.projectId;
|
|
@@ -300,7 +301,7 @@ export async function completeWorkspaceSetup(args) {
|
|
|
300
301
|
// what to ask for, by name. A bare FORBIDDEN from `createProject` would be both
|
|
301
302
|
// wrong (they never asked to create anything) and unactionable.
|
|
302
303
|
if (!list.length) {
|
|
303
|
-
cliError("NO_PROJECTS", `Org ${org} has no projects yet, and only an owner can create one.`, `Ask
|
|
304
|
+
cliError("NO_PROJECTS", `Org ${org} has no projects yet, and only an owner or admin can create one.`, `Ask one of them to add a project, then finish this folder with: ${selfCommand(VERSION)} bind --org-id ${orgId}`);
|
|
304
305
|
}
|
|
305
306
|
// One project is not a choice. Asking anyway would be the only question in the
|
|
306
307
|
// common invite path, for an answer that was never in doubt.
|
|
@@ -416,6 +417,53 @@ export async function completeWorkspaceSetup(args) {
|
|
|
416
417
|
if (isInteractive())
|
|
417
418
|
console.log(`\n${NEXT_STEP}`);
|
|
418
419
|
}
|
|
420
|
+
export async function setupFromToken(token, opts, deps = {}) {
|
|
421
|
+
const cfg = loadConfig();
|
|
422
|
+
const cwd = process.cwd();
|
|
423
|
+
const guard = deps.guard ?? rebindGuard;
|
|
424
|
+
const redeem = deps.redeem ??
|
|
425
|
+
((t, label) => api.redeemSetupToken({ token: t, label }));
|
|
426
|
+
// BEFORE the redeem. `rebindGuard` prints its own explanation and, with no TTY,
|
|
427
|
+
// refuses with exit 1 unless `--yes` — which is exactly the required behaviour, so it
|
|
428
|
+
// is reused rather than restated. No org/project passed: its idempotent-converge
|
|
429
|
+
// shortcut needs ids we do not have yet, and cannot apply to a single-use token anyway.
|
|
430
|
+
const { proceed, existing } = await guard({ cwd, mcpUrl: cfg.mcpUrl, yes: opts.yes });
|
|
431
|
+
if (!proceed)
|
|
432
|
+
return;
|
|
433
|
+
// Resolve (and if needed install) `retasc` BEFORE the token is spent, so a machine that
|
|
434
|
+
// cannot get a working launcher fails while the code is still redeemable.
|
|
435
|
+
const install = await chooseInstall(opts.install);
|
|
436
|
+
const launcher = resolveLauncher({ version: VERSION, install });
|
|
437
|
+
const note = launcherNote(launcher);
|
|
438
|
+
if (note)
|
|
439
|
+
console.log(note);
|
|
440
|
+
// RTSC-532 — the LEAF name, never the full path. It names the key in the Dash, so it
|
|
441
|
+
// has to be recognisable ("client-a → ENG") without publishing where on her disk it
|
|
442
|
+
// sits. `basename` of the cwd is exactly that.
|
|
443
|
+
const redeemed = await redeem(token.trim(), basename(cwd));
|
|
444
|
+
const workspaceId = existing?.workspaceId ?? newWorkspaceId();
|
|
445
|
+
(deps.bind ?? setBinding)(workspaceId, {
|
|
446
|
+
orgId: redeemed.orgId,
|
|
447
|
+
projectId: redeemed.projectId,
|
|
448
|
+
key: redeemed.key,
|
|
449
|
+
url: cfg.mcpUrl,
|
|
450
|
+
prefix: redeemed.prefix,
|
|
451
|
+
orgName: redeemed.orgName,
|
|
452
|
+
boundPath: cwd,
|
|
453
|
+
createdAt: Date.now(),
|
|
454
|
+
});
|
|
455
|
+
(deps.marker ?? installMarker)({ workspaceId, scope: "local", launcher });
|
|
456
|
+
// RTSC-532 — name the PATH, not "this folder". A binding is folder → project, and
|
|
457
|
+
// until this line said which folder, a wrong one was undetectable: the agent still
|
|
458
|
+
// calls in, so the Dash shows success while the folder she actually works in has no
|
|
459
|
+
// Retasc tools. Her agent relays this sentence to her, so it is also the last chance
|
|
460
|
+
// to notice.
|
|
461
|
+
console.log(`✓ ${clean(cwd)} is connected to ${clean(redeemed.orgName)} / ${clean(redeemed.prefix)}.`);
|
|
462
|
+
// NOT gated on isInteractive(), unlike the interactive path's copy of this line. There
|
|
463
|
+
// is never a TTY here, and this is the one instruction the agent has to pass on — the
|
|
464
|
+
// Dash waits on a tool call that cannot happen until the restart does (RTSC-496).
|
|
465
|
+
console.log(`\n${NEXT_STEP}`);
|
|
466
|
+
}
|
|
419
467
|
/**
|
|
420
468
|
* What to do with a workspace that is now set up.
|
|
421
469
|
*
|
package/dist/commands/join.js
CHANGED
|
@@ -223,8 +223,9 @@ export async function joinAction(link, opts) {
|
|
|
223
223
|
orgLabel,
|
|
224
224
|
opts,
|
|
225
225
|
existing: guard.existing,
|
|
226
|
-
// `createProject` is `
|
|
227
|
-
//
|
|
226
|
+
// `createProject` is `requireOwnerOrAdmin`, and an invite only ever grants `member`
|
|
227
|
+
// (admin is granted by `setMemberRole` after joining, never by a bearer code —
|
|
228
|
+
// RTSC-561). Offering "+ create new" here would be offering a refusal as a choice.
|
|
228
229
|
canCreateProject: false,
|
|
229
230
|
});
|
|
230
231
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./c
|
|
|
5
5
|
import { installMcp, normalizeScope } from "./commands/mcp.js";
|
|
6
6
|
import { installGate } from "./commands/gate.js";
|
|
7
7
|
import { claimAction } from "./commands/claim.js";
|
|
8
|
-
import { bindAction } from "./commands/bind.js";
|
|
8
|
+
import { bindAction, setupFromToken } from "./commands/bind.js";
|
|
9
9
|
import { joinAction } from "./commands/join.js";
|
|
10
10
|
import { identityAction } from "./commands/identity.js";
|
|
11
11
|
import { importAction } from "./commands/import.js";
|
|
@@ -183,7 +183,22 @@ program
|
|
|
183
183
|
// is present; this is how a scripted run, or a developer whose own agent runs setup,
|
|
184
184
|
// says no up front instead of being installed onto.
|
|
185
185
|
.option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
|
|
186
|
+
// RTSC-495 — the agent's door. Everything this command normally asks was already
|
|
187
|
+
// answered in the Dash, so the token stands in for all of it and nothing is prompted.
|
|
188
|
+
.option("--setup <token>", "Complete setup from a Dash setup code — no sign-in, no prompts")
|
|
186
189
|
.action(async (opts) => {
|
|
190
|
+
// BEFORE requireLogin: the whole point is a machine that has never signed in. The
|
|
191
|
+
// token is the authorization, and asking for a session here would refuse every
|
|
192
|
+
// caller this flag exists for.
|
|
193
|
+
//
|
|
194
|
+
// `!== undefined`, not truthiness: `--setup ""` is a stated intent to use a setup
|
|
195
|
+
// code, and it must fail as a bad code. Falling through to the interactive path
|
|
196
|
+
// would answer it with "Not signed in", which is the one diagnosis that sends the
|
|
197
|
+
// reader looking in exactly the wrong place.
|
|
198
|
+
if (opts.setup !== undefined) {
|
|
199
|
+
await setupFromToken(opts.setup, opts).catch(fail);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
187
202
|
requireLogin();
|
|
188
203
|
await bindAction(opts).catch(fail);
|
|
189
204
|
});
|
|
@@ -196,7 +211,7 @@ program
|
|
|
196
211
|
});
|
|
197
212
|
program
|
|
198
213
|
.command("billing")
|
|
199
|
-
.description("Show the org's billing: subscription, what's owed now, and the charge + on-chain payment history across every payment link ever used (owner
|
|
214
|
+
.description("Show the org's billing: subscription, what's owed now, and the charge + on-chain payment history across every payment link ever used (owner or admin).")
|
|
200
215
|
.option("--org-id <id>", "Which org (defaults to your only one).")
|
|
201
216
|
.option("--json", "Emit the raw payload instead of the summary.")
|
|
202
217
|
.action(async (opts) => {
|
|
@@ -341,7 +356,7 @@ key
|
|
|
341
356
|
const members = program.command("members").description("Invite people to an org and manage invites.");
|
|
342
357
|
members
|
|
343
358
|
.command("invite")
|
|
344
|
-
.description("Mint a single-use invite code for an org (owner
|
|
359
|
+
.description("Mint a single-use invite code for an org (owner or admin). Shown once.")
|
|
345
360
|
.requiredOption("--org-id <id>")
|
|
346
361
|
.option("--expires-days <n>", "Days until the code expires (1–30, default 7)", (v) => parseInt(v, 10))
|
|
347
362
|
.action(async (opts) => {
|
|
@@ -362,7 +377,7 @@ members
|
|
|
362
377
|
});
|
|
363
378
|
members
|
|
364
379
|
.command("list")
|
|
365
|
-
.description("List an org's invites and their status (owner
|
|
380
|
+
.description("List an org's invites and their status (owner or admin).")
|
|
366
381
|
.requiredOption("--org-id <id>")
|
|
367
382
|
.option("--json", "Emit the raw payload instead of the table.")
|
|
368
383
|
.action(async (opts) => {
|
|
@@ -377,7 +392,7 @@ members
|
|
|
377
392
|
});
|
|
378
393
|
members
|
|
379
394
|
.command("revoke")
|
|
380
|
-
.description("Revoke an unused invite (owner
|
|
395
|
+
.description("Revoke an unused invite (owner or admin).")
|
|
381
396
|
.requiredOption("--invite-id <id>")
|
|
382
397
|
.action(async (opts) => {
|
|
383
398
|
requireLogin();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Retasc CLI \u2014 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": {
|