@retasc/cli 1.18.0 → 1.19.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 CHANGED
@@ -6,6 +6,37 @@ 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.19.0 (2026-08-03)
10
+
11
+ - **RTSC-495** — `retasc bind --setup <code>` sets a folder up with no sign-in and nothing
12
+ to answer, so an agent can do it on behalf of someone who does not use a terminal.
13
+
14
+ The Dash's connect step now hands over a block you paste to your agent, with this command
15
+ inside it. Everything the interactive `bind` would ask was already answered in the
16
+ browser, and the code carries those answers across. An agent's shell is not a TTY, so the
17
+ ordinary path refuses it outright at the first prompt.
18
+
19
+ ```
20
+ npx @retasc/cli bind --setup rtscsetup_…
21
+ ```
22
+
23
+ The code is single-use and lives ten minutes. The alternative — pasting a real API key
24
+ into the prompt — needed no backend at all, but would have left a long-lived credential in
25
+ an agent's conversation history forever.
26
+
27
+ A folder that is already connected is refused rather than silently replaced, and the
28
+ refusal happens BEFORE the code is spent, so a mix-up costs nothing.
29
+
30
+ - **RTSC-532** — the agent says which folder it is in, and the key remembers.
31
+
32
+ Binding the wrong folder used to look exactly like success: the agent still called in, so
33
+ the Dash went green, while the folder you actually work in had no Retasc in it. The prompt
34
+ now asks the agent to report its full path and wait before it runs anything, and the
35
+ confirmation names the real path instead of "This folder".
36
+
37
+ The key it creates is named after that folder, so the Keys page shows which folder each
38
+ key belongs to (`client-a`) instead of naming them all after the project (`ENG key`).
39
+
9
40
  ## 1.18.0 (2026-08-02)
10
41
 
11
42
  - **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)),
@@ -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";
@@ -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/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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.18.0",
3
+ "version": "1.19.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": {