@decidio/sdk 0.6.1 → 0.6.3

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
@@ -1,8 +1,44 @@
1
1
  # Changelog — @decidio/sdk
2
2
 
3
- Release notes for the client SDK. The Python package `decidio` ships the same changes under the same
4
- version (the two are conformance-locked twins). Each entry names what changed, what a reader must do
5
- on upgrade, and what proved it.
3
+ Release notes for the client SDK. The Python package `decidio` ships under the same version; the core -
4
+ the gate, signing, receipts and the verifier - is conformance-locked between the two, while the command line's
5
+ first-run surfaces (the fork, `--demo`, the code sign-in) are TypeScript-first and named as such in the parity
6
+ allowlist. Each entry names what changed, what a reader must do on upgrade, and what proved it.
7
+
8
+ ## 0.6.3 — the two 0.6.2 reviews' small items
9
+
10
+ - **`DECIDIO_DEBUG_INPUT=1` no longer prints what you type** (review: Medium). The 0.6.2 trace hex-dumped every stdin
11
+ byte - passwords, codes and links, muted prompt or not. It now prints the SHAPE only: control bytes named (`\r`, `\n`,
12
+ ESC), every printable byte as `x`. A newline problem is visible in the shape alone.
13
+ - Four strings still described the sign-in as an emailed link to paste; they say the code now. The CHANGELOG's "twins"
14
+ line overstated CLI parity and now says exactly what is locked and what is TypeScript-first. Two README headings
15
+ still said "0.4". The offline local demo no longer prints "the terminal follows the web app or your phone" when it
16
+ has just said there are no such doors. `receipt --json` help says it prints a summary; the credential is the file.
17
+
18
+ ## 0.6.2 — the fork after its first real run
19
+
20
+ Founder UAT of 0.6.1 on a Windows console, same day.
21
+
22
+ - **Prompts pause.** On the founder's cmd.exe the agent-id prompt "took the folder's" name without waiting. The
23
+ terminal driver could not reproduce it, so this is defence plus instrumentation: one readline now lives for the whole
24
+ command (a console's `\r\n` can no longer split across two questions - the previous per-question interfaces could
25
+ hand the second byte to the next prompt as Enter), and `DECIDIO_DEBUG_INPUT=1` prints the raw bytes stdin delivers, so
26
+ a recurrence names its cause instead of a theory.
27
+ - **"What next?" has a way out:** `3 sign out of this computer (npx @decidio/sdk logout)`.
28
+ - **Option 2 in a folder that already holds an agent** says so before asking for a name (with cleanup / a new folder /
29
+ `--force` as the ways out), instead of `init` refusing afterwards with text that read like a prompt.
30
+ - **The bare command is update-gated** on a terminal, so a cached old npx copy announces the newer version instead of
31
+ quietly showing an old first screen; `npx @decidio/sdk` is enough, `@latest` is not needed.
32
+ - **Sign in by CODE, not by paste.** When the command line asks for a sign-in, the email it triggers carries an
33
+ 8-character code and nothing to click; you type the code into the terminal that asked (dashes, case and an O for a 0
34
+ are forgiven). Three misses and the code is dead. The web app's sign-in screen keeps its link email unchanged; two
35
+ flows, each with exactly one action. Why this shape and not "click the link and the terminal continues": that design
36
+ moves a session to a DIFFERENT device than the one that clicked, which is a new attack class the privacy council had
37
+ to harden (docs/Decidio_CLI_Signin_Wait_Spec.md, retired); a code travels from the inbox to the device you are
38
+ sitting at, and an attacker who triggers the email gains nothing. A pasted link is still accepted, quietly.
39
+ - **`npx decidio`.** A new npm package named `decidio` holds one file: it resolves the `@decidio/sdk` it pins (same
40
+ version, released in lockstep) and runs that CLI. `npx decidio …` is `npx @decidio/sdk …` with fewer keystrokes; the
41
+ update gate names whichever package you came through. Not something to import - the SDK stays `@decidio/sdk`.
6
42
 
7
43
  ## 0.6.1 — the first-run fork, and the local demo's loose ends
8
44
 
package/README.md CHANGED
@@ -9,6 +9,7 @@ is the two-minute tour, the sign-in and the token admin around it. Install, wrap
9
9
  ## The two-minute tour
10
10
 
11
11
  ```bash
12
+ npx decidio # the short form: asks one question, then does the thing you picked
12
13
  npx @decidio/sdk quickstart # against your workspace (you need an invite - request one below)
13
14
  npx @decidio/sdk quickstart --demo # no invite yet? the same tour on this machine, against a local stand-in of the API
14
15
  # Not sure? The bare command asks: with no sign-in on this computer it asks whether you have an
@@ -22,7 +23,7 @@ you approve right there (or from your Decidio app), executes on approval, then d
22
23
  sealed Authority Receipt and verifies it offline in front of you, pinned to your workspace's
23
24
  issuer. It mints a **7-day** tour token (`init` mints 90-day tokens — different jobs, different
24
25
  lifetimes) and ends by offering to clean itself up (`quickstart cleanup` works later too).
25
- Sign in once — `npx @decidio/sdk login`, or when the tour asks: the CLI emails you a link, you paste it, and
26
+ Sign in once — `npx @decidio/sdk login`, or when the tour asks: the CLI emails you an 8-character code, you type it, and
26
27
  the sign-in is kept in your profile (owner-only) for 30 days, or until this terminal closes, `logout`, or
27
28
  `quickstart cleanup`. At the human step the tour offers three doors: answer here, open the decision in
28
29
  the web app (a one-time link, signed in for you), or scan the QR with your phone — whichever answers
@@ -74,8 +75,8 @@ npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: co
74
75
  Every network command talks to the hosted sandbox by default (`verify` is offline) and announces its target host on
75
76
  stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
76
77
 
77
- `init` signs you into your workspace (email + password, or paste the sign-in link from your
78
- workspace email), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
78
+ `init` signs you into your workspace (email + password, or the 8-character code the CLI emails you —
79
+ type it where you asked), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
79
80
  is sent), registers the agent, mints its **floor-limited API token** (limited to the agent runtime — request authorization, poll status, report signed confirmation; it cannot administer the workspace or approve anything — and your workspace session never touches the agent's disk), and writes `.env`.
80
81
  Every later command that talks to Decidio reads `.env` from this directory automatically (`verify` is
81
82
  offline and never does). Re-running init for an
@@ -91,7 +92,7 @@ agent name — if it's already taken in your workspace, `init` stops cleanly and
91
92
  fresh one (or pass `--unique` to auto-append a random suffix — collision-proof for shared
92
93
  workspaces and CI). Interactive terminals sign in once (`npx @decidio/sdk login` — stored owner-only in your
93
94
  profile, bound to this terminal window, 30 days). Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
94
- request a sign-in link, then exchange its `?login=` value at `POST /demo/login-consume` — the
95
+ request a sign-in link from the web app's sign-in screen, then exchange its `?login=` value at `POST /demo/login-consume` — the
95
96
  response's `token` is your session token (the CLI prints these exact instructions when it
96
97
  can't prompt). That session token is your workspace login, not the agent's credential: unset it
97
98
  before launching agent code from the same shell, because child processes inherit exported
@@ -659,7 +660,7 @@ that fits every other confirm error.
659
660
  - **The token recovery journal** (`.decidio-token-journal`, owner-only, gitignored; ids and timestamps, never a token value; append-only by the CLI's behaviour — not OS-enforced or tamper-evident). One JSON row per event: `minted` (a token was issued, written BEFORE its credential file), `retired how=persisted` (the credential file now holds it - the token is live and that file is its handle; not revoked), `retired how=revoked` (revoked server-side), `probe` (a durability check). `doctor` names rows still outstanding; `quickstart cleanup` sweeps them.
660
661
  - `quickstart --demo` — the same tour with no account: an in-process stand-in of the API on 127.0.0.1 (shipped in this package) answers every door, a key generated for the run signs a real receipt (its content says `local-demo`; pinning it to the hosted issuer fails), and the web/phone door is a hosted demo decision page reached through a one-word relay (no account, no record, ten minutes). Your folder gains exactly one file, the receipt. Offline, the tour is terminal-only and says so.
661
662
  - `quickstart --phone web` — the QR at the human step opens the phone web rendering even when the Decidio app is installed (by default it opens the app when there is one).
662
- - `login [email]` / `logout` — sign in ONCE (the CLI emails you a link; you paste it) and forget that sign-in on this computer (the session token itself stays valid until it expires (up to 30 days) — there is no server-side revocation yet). The session is stored in your user profile, owner-only, bound to the terminal window that created it, for 30 days; `DECIDIO_SESSION_TOKEN` in the environment outranks it (headless shells).
663
+ - `login [email]` / `logout` — sign in ONCE (the CLI emails you an 8-character code; you type it) and forget that sign-in on this computer (the session token itself stays valid until it expires (up to 30 days) — there is no server-side revocation yet). The session is stored in your user profile, owner-only, bound to the terminal window that created it, for 30 days; `DECIDIO_SESSION_TOKEN` in the environment outranks it (headless shells).
663
664
  - `dev [--target URL] [--port N]` — local relay for the resume webhook (operator setups)
664
665
 
665
666
  The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
@@ -702,7 +703,7 @@ Every error raised after a decision exists carries the `decisionId`, so a suspen
702
703
  timed-out action is always findable in
703
704
  your queue.
704
705
 
705
- ## What 0.4 does not yet claim
706
+ ## What the SDK does not yet claim
706
707
 
707
708
  The retest that graded 0.3.3 *A- for the core and the shipped stores* and the first-run reviews of
708
709
  0.3.3 and 0.3.5 agree on what is left, and none of it is a defect a patch release closes. Stated here so
@@ -728,7 +729,7 @@ the next reader grades a promise, not a moving target:
728
729
 
729
730
  ## Compatibility and support
730
731
 
731
- - **Stable in 0.4:** `guard.protect` / `withApproval` and their options; the durable resume
732
+ - **Stable surface:** `guard.protect` / `withApproval` and their options; the durable resume
732
733
  controller (`createDecidioResume`: `park`, `resolve`, `reconcile`, `reconfirm`, `recover`, the worker
733
734
  and both webhook handlers); the `PendingStore` contract and both shipped stores, including the on-disk
734
735
  revision-chain layout; `EvidenceOutcome` (a root export) and `ResumeStatus` / `RecoveryState` (on the
package/dist/cli.js CHANGED
@@ -467,22 +467,44 @@ export function approvalAnswer(answer) {
467
467
  return "reject";
468
468
  return "again";
469
469
  }
470
+ let sharedRl = null;
471
+ let sharedRlOpen = false;
472
+ function closeSharedRl() { if (sharedRlOpen) {
473
+ sharedRlOpen = false;
474
+ sharedRl?.close();
475
+ } }
470
476
  function ask(question, mute = false) {
471
477
  if (process.stdin.readableEnded || process.stdin.destroyed)
472
478
  return Promise.resolve("");
473
479
  return new Promise((resolve) => {
474
- const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
480
+ if (!sharedRl || !sharedRlOpen) {
481
+ sharedRl = createInterface({ input: process.stdin, output: process.stderr, terminal: true, crlfDelay: Infinity });
482
+ sharedRlOpen = true;
483
+ sharedRl.once("close", () => { sharedRlOpen = false; });
484
+ }
485
+ const rl = sharedRl;
475
486
  currentRl = rl;
487
+ let restore = null;
476
488
  if (mute) {
477
489
  const anyRl = rl;
478
490
  const write = anyRl._writeToOutput?.bind(rl);
479
491
  anyRl._writeToOutput = (s) => { if (s.includes(question))
480
492
  write?.(s); };
481
- }
482
- rl.once("close", () => { if (currentRl === rl)
483
- currentRl = null; resolve(""); });
484
- rl.question(question, (answer) => { resolve(answer.trim()); rl.close(); if (mute)
485
- process.stderr.write("\n"); });
493
+ restore = () => { anyRl._writeToOutput = write; };
494
+ }
495
+ const onClose = () => { if (currentRl === rl)
496
+ currentRl = null; restore?.(); resolve(""); };
497
+ rl.once("close", onClose);
498
+ rl.question(question, (answer) => { rl.off("close", onClose); if (currentRl === rl)
499
+ currentRl = null; restore?.(); if (mute)
500
+ process.stderr.write("\n"); resolve(answer.trim()); });
501
+ });
502
+ }
503
+ if (process.env.DECIDIO_DEBUG_INPUT && process.stdin.isTTY) {
504
+ process.stdin.on("data", (c) => {
505
+ const b = Buffer.from(c);
506
+ const shape = [...b].map((x) => x === 0x0d ? "\\r" : x === 0x0a ? "\\n" : x === 0x1b ? "ESC" : x < 0x20 ? `^${String.fromCharCode(x + 64)}` : x === 0x7f ? "DEL" : "x").join("");
507
+ process.stderr.write(`[stdin ${b.length}B ${shape}]\n`);
486
508
  });
487
509
  }
488
510
  function terminalId() {
@@ -557,7 +579,7 @@ async function loginFlow(purpose, emailArg) {
557
579
  log(" ✘ no email given - re-run when you have it.");
558
580
  return null;
559
581
  }
560
- const password = await ask(" password (Enter = emailed sign-in link): ", true);
582
+ const password = await ask(" password (Enter = emailed sign-in code): ", true);
561
583
  if (password) {
562
584
  const r = await api("/demo/login", { method: "POST", body: JSON.stringify({ email, password }) }, "");
563
585
  if (r.status >= 200 && r.status < 300 && r.body?.token) {
@@ -567,29 +589,37 @@ async function loginFlow(purpose, emailArg) {
567
589
  }
568
590
  log(` ✘ password sign-in failed (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. No problem — the emailed link works for every account:`);
569
591
  }
570
- const req = await api("/demo/login-link", { method: "POST", body: JSON.stringify({ email }) }, "");
592
+ const req = await api("/demo/login-link", { method: "POST", body: JSON.stringify({ email, code: true }) }, "");
571
593
  if (!(req.status >= 200 && req.status < 300)) {
572
594
  log(` ✘ could not request a sign-in email (${req.status})${req.body?.error ? ` — ${req.body.error}` : ""}. Check the address and re-run.`);
573
595
  return null;
574
596
  }
575
- log(` ✔ sign-in email requested for ${email} — check your inbox and spam; paste the link (or its login= code; single-use, 15 minutes)`);
576
- log(dim(` invite-only lab: a link arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
597
+ log(` ✔ sign-in email sent to ${email} — it holds an 8-character code (single-use, 15 minutes). Check spam too.`);
598
+ log(dim(` invite-only lab: an email arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
577
599
  log(dim(` or see the whole story on this machine right now, no account: npx @decidio/sdk quickstart --demo`));
578
- const pasted = await ask(" link or code: ");
579
- if (!pasted) {
580
- log(" ✘ nothing pasted — re-run when you have the email.");
581
- return null;
582
- }
583
- const m = pasted.match(/[?&]login=([^&\s]+)/);
584
- const linkToken = m ? decodeURIComponent(m[1]) : pasted;
585
- const r = await api("/demo/login-consume", { method: "POST", body: JSON.stringify({ token: linkToken }) }, "");
586
- if (r.status >= 200 && r.status < 300 && r.body?.token) {
587
- const s = saveSession(String(r.body.token), email);
588
- log(` ✔ signed in as ${email} until ${s.expiresAt.slice(0, 10)}${s.terminal ? "" : dim(" (no terminal id here: the sign-in ends on logout or cleanup, not on close)")}`);
589
- return s.token;
600
+ for (let attempt = 1; attempt <= 3; attempt++) {
601
+ const typed = await ask(" code from the email: ");
602
+ if (!typed) {
603
+ log(" ✘ nothing typed — re-run when you have the email.");
604
+ return null;
605
+ }
606
+ const m = typed.match(/[?&]login=([^&\s]+)/);
607
+ const isLink = !!m || /^[0-9a-f]{32,}$/i.test(typed);
608
+ const body = isLink ? { token: m ? decodeURIComponent(m[1]) : typed } : { token: "c." + typed.toUpperCase().replace(/[^0-9A-Z]/g, "").replace(/O/g, "0").replace(/[IL]/g, "1").slice(0, 8), email };
609
+ const r = await api("/demo/login-consume", { method: "POST", body: JSON.stringify(body) }, "");
610
+ if (r.status >= 200 && r.status < 300 && r.body?.token) {
611
+ const s = saveSession(String(r.body.token), email);
612
+ log(` ✔ signed in as ${email} until ${s.expiresAt.slice(0, 10)}${s.terminal ? "" : dim(" (no terminal id here: the sign-in ends on logout or cleanup, not on close)")}`);
613
+ return s.token;
614
+ }
615
+ if (isLink) {
616
+ log(` ✘ that link didn't work (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. Sign-in links are single-use and expire in 15 minutes — request a fresh one and re-run.`);
617
+ return null;
618
+ }
619
+ if (attempt < 3)
620
+ log(` ✘ that code didn't work — check the email and try again (${3 - attempt} ${3 - attempt === 1 ? "try" : "tries"} left)`);
590
621
  }
591
- log(` ✘ that link didn't work (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}.`);
592
- log(` Sign-in links are single-use and expire in 15 minutes — request a fresh one and re-run.`);
622
+ log(` ✘ three misses — that code is now dead. Re-run to get a fresh email.`);
593
623
  return null;
594
624
  }
595
625
  async function acquireSession(purpose) {
@@ -659,12 +689,13 @@ async function firstRunFork(signedInAs = null) {
659
689
  return { kind: "stop", code: 1 };
660
690
  }
661
691
  log("");
662
- log(` What next? ${dim("[1/2]")}`);
692
+ log(` What next? ${dim("[1/2/3]")}`);
663
693
  log(` ${bold("1")} the two-minute tour against your workspace ${dim("npx @decidio/sdk quickstart")}`);
664
694
  log(` ${bold("2")} wire up your own agent now (register, token, .env) ${dim("npx @decidio/sdk init <agentId>")}`);
665
- const b = await pick("1 or 2", /^[12]$/);
695
+ log(` ${bold("3")} sign out of this computer ${dim("npx @decidio/sdk logout")}`);
696
+ const b = await pick("1, 2 or 3", /^[123]$/);
666
697
  if (!b) {
667
- log(` No answer — you are signed in; run either command above when you are ready.`);
698
+ log(` No answer — you are signed in; run any command above when you are ready.`);
668
699
  return { kind: "stop", code: 0 };
669
700
  }
670
701
  if (b === "1") {
@@ -672,6 +703,21 @@ async function firstRunFork(signedInAs = null) {
672
703
  log("");
673
704
  return { kind: "tour" };
674
705
  }
706
+ if (b === "3") {
707
+ log(` → running: npx @decidio/sdk logout`);
708
+ log("");
709
+ return { kind: "logout" };
710
+ }
711
+ const here = readEnvFile();
712
+ if (["DECIDIO_AGENT_KEY", "DECIDIO_API_TOKEN"].some((k) => (here[k] ?? "").trim()) && here.DECIDIO_AGENT_ID) {
713
+ const tour = /^quickstart-/.test(here.DECIDIO_AGENT_ID);
714
+ log(` This folder's .env already holds an agent (${tour ? "the tour's, " : ""}"${here.DECIDIO_AGENT_ID}"). A new agent here would overwrite its key.`);
715
+ log(` Either: start in a new folder ${dim("mkdir <name> && cd <name> && npx @decidio/sdk")}`);
716
+ if (tour)
717
+ log(` or: clear the tour's agent first ${dim("npx @decidio/sdk quickstart cleanup")} then run this again`);
718
+ log(` or: replace it, backing .env up ${dim("npx @decidio/sdk init <name> --force")}`);
719
+ return { kind: "stop", code: 1 };
720
+ }
675
721
  const def = defaultAgentId();
676
722
  log(` Your agent needs a name. It is how the workspace shows the request ("Requested by ${def.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}")`);
677
723
  log(` and the key .env is written under. Any name works; Enter takes the folder's.`);
@@ -721,9 +767,8 @@ async function cmdInit(agentId, opts = {}) {
721
767
  err(`✘ this directory's .env already holds the key for a DIFFERENT agent ("${existingEnv.DECIDIO_AGENT_ID}").`);
722
768
  err(` Registering "${agentId}" here would overwrite that key — and a private key that is`);
723
769
  err(` overwritten is gone for good (it never left this machine). Nothing was written.`);
724
- err(` Pick one:`);
725
- err(` 1. New directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
726
- err(` 2. Replace the old agent here, backing its .env up first: npx @decidio/sdk init ${agentId} --force`);
770
+ err(` Either: a new directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
771
+ err(` or: replace the old agent here, backing its .env up first: npx @decidio/sdk init ${agentId} --force`);
727
772
  return 1;
728
773
  }
729
774
  const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
@@ -1513,7 +1558,8 @@ async function cmdQuickstartInner(opts = {}) {
1513
1558
  }
1514
1559
  else if (doorLink)
1515
1560
  rows.push(`${bold("your phone")} ${dim("the link above works on a phone too (no QR came back this time)")}`);
1516
- rows.push(dim("whichever answers first wins — the terminal follows the web app or your phone."));
1561
+ if (!DEMO || doorLink)
1562
+ rows.push(dim("whichever answers first wins — the terminal follows the web app or your phone."));
1517
1563
  block("③", "Your call", rows, "choice");
1518
1564
  let settledState = null;
1519
1565
  const settledP = running.then(() => { settledState = "approved"; }, (e) => { settledState = e instanceof DecidioRejectedError ? "rejected" : "other"; });
@@ -2116,7 +2162,7 @@ THE TOUR
2116
2162
  (--keep-agent is refused here: nothing of the demo's can be kept)
2117
2163
 
2118
2164
  SIGN IN (once)
2119
- npx @decidio/sdk login [email] sign in ONCE: the CLI emails you a link, you paste it; the sign-in
2165
+ npx @decidio/sdk login [email] sign in ONCE: the CLI emails you an 8-character code, you type it; the sign-in
2120
2166
  is kept on this computer 30 days, or until this terminal closes, logout, or cleanup
2121
2167
  npx @decidio/sdk logout forget the stored sign-in on this computer (the token itself stays valid
2122
2168
  until it expires — there is no server-side revocation yet)
@@ -2145,7 +2191,9 @@ APPROVALS AND RECEIPTS
2145
2191
  If it starts with a dash, put it after -- or use --reason:
2146
2192
  ... approve <id> -- "-50% cut, per the board"
2147
2193
  ... approve <id> --reason "-50% cut, per the board"
2148
- npx @decidio/sdk receipt <id> [--json] download a sealed decision's Authority Receipt (.json)
2194
+ npx @decidio/sdk receipt <id> [--json] download a sealed decision's Authority Receipt to a .json FILE; --json prints
2195
+ a summary to stdout (file name, evidence, issuer, the identities you may see) -
2196
+ the credential itself is the file
2149
2197
  npx @decidio/sdk verify --issuer <did> <receipt.json> [more...] verify receipts OFFLINE, pinned to your
2150
2198
  workspace's issuer (--allow-unpinned: structural check only)
2151
2199
 
@@ -2157,7 +2205,8 @@ NOTES
2157
2205
  Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
2158
2206
  DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
2159
2207
  fully offline — it touches no network and reads no .env.
2160
- Update check: network commands ask the public npm registry once a day whether a newer version exists. If so,
2208
+ Update check: network commands (and the bare command on a terminal) ask the public npm registry once a day
2209
+ whether a newer version exists. If so,
2161
2210
  an interactive run asks "Upgrade now? [y/n]" first (y upgrades the way you installed - global, or in the
2162
2211
  project for a local install; under npx it prints the @latest form - and re-runs your command; n exits); a headless
2163
2212
  run prints the notice and continues (DECIDIO_REQUIRE_LATEST=1 makes it exit). DECIDIO_NO_UPDATE_CHECK=1 skips
@@ -2212,7 +2261,8 @@ async function latestPublished() {
2212
2261
  }
2213
2262
  }
2214
2263
  async function updateGate(cmd, argv) {
2215
- if (!cmd || !GATED_CMDS.has(cmd) || SHELL_ENV.DECIDIO_NO_UPDATE_CHECK || SHELL_ENV.DECIDIO_UPDATE_REEXEC || VERSION === "unknown" || VERSION.includes("-"))
2264
+ const gatedHere = cmd ? GATED_CMDS.has(cmd) : !!(process.stdin.isTTY && process.stdout.isTTY);
2265
+ if (!gatedHere || SHELL_ENV.DECIDIO_NO_UPDATE_CHECK || SHELL_ENV.DECIDIO_UPDATE_REEXEC || VERSION === "unknown" || VERSION.includes("-"))
2216
2266
  return null;
2217
2267
  const latest = await latestPublished();
2218
2268
  if (!latest || !semverNewer(latest, VERSION))
@@ -2228,7 +2278,9 @@ async function updateGate(cmd, argv) {
2228
2278
  const cwd = norm(process.cwd());
2229
2279
  return cwd === norm(root) || cwd.startsWith(norm(root) + pathSep) ? root : null;
2230
2280
  })();
2231
- const upgradeCmd = underNpx ? `npx @decidio/sdk@latest ${argv.slice(2).join(" ")}` : localRoot ? "npm i @decidio/sdk@latest" : "npm i -g @decidio/sdk@latest";
2281
+ const viaAlias = /[\\/]decidio$/.test(argv[1] ?? "");
2282
+ const pkgName = viaAlias ? "decidio" : "@decidio/sdk";
2283
+ const upgradeCmd = underNpx ? `npx ${pkgName}@latest ${argv.slice(2).join(" ")}` : localRoot ? `npm i ${pkgName}@latest` : `npm i -g ${pkgName}@latest`;
2232
2284
  const notice = `${latest} is available (you have ${VERSION}).`;
2233
2285
  if (!(process.stdin.isTTY && process.stdout.isTTY)) {
2234
2286
  err(`⚠ ${notice} Upgrade: ${upgradeCmd}`);
@@ -2252,7 +2304,7 @@ async function updateGate(cmd, argv) {
2252
2304
  return null;
2253
2305
  }
2254
2306
  log(` upgrading: ${upgradeCmd}${localRoot ? ` (in ${localRoot})` : ""}`);
2255
- const up = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", localRoot ? ["i", "@decidio/sdk@latest"] : ["i", "-g", "@decidio/sdk@latest"], { stdio: "inherit", shell: process.platform === "win32", env: SHELL_ENV, ...(localRoot ? { cwd: localRoot } : {}) });
2307
+ const up = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", localRoot ? ["i", `${pkgName}@latest`] : ["i", "-g", `${pkgName}@latest`], { stdio: "inherit", shell: process.platform === "win32", env: SHELL_ENV, ...(localRoot ? { cwd: localRoot } : {}) });
2256
2308
  if (up.status !== 0) {
2257
2309
  err(`✘ the upgrade did not complete (exit ${up.status ?? "?"}) — run it by hand: ${upgradeCmd}`);
2258
2310
  return 1;
@@ -2268,11 +2320,16 @@ async function main(argv) {
2268
2320
  return 0;
2269
2321
  }
2270
2322
  if (!cmd && process.stdin.isTTY && process.stdout.isTTY) {
2323
+ const gatedBare = await updateGate(cmd, argv);
2324
+ if (gatedBare !== null)
2325
+ return gatedBare;
2271
2326
  log(brand() + " — gate an action, route it to a human, execute on approval, hold the proof. " + dim("(npx @decidio/sdk --help for every command)"));
2272
2327
  const stored = process.env.DECIDIO_SESSION_TOKEN ? "" : loadSession()?.email ?? null;
2273
2328
  const pick = await firstRunFork(process.env.DECIDIO_SESSION_TOKEN ? "" : stored);
2274
2329
  if (pick.kind === "stop")
2275
2330
  return pick.code;
2331
+ if (pick.kind === "logout")
2332
+ return cmdLogout();
2276
2333
  if (pick.kind === "init")
2277
2334
  return cmdInit(pick.agentId, {});
2278
2335
  return cmdQuickstart({ demo: pick.kind === "demo" });
@@ -2393,6 +2450,8 @@ async function main(argv) {
2393
2450
  const pick = await firstRunFork();
2394
2451
  if (pick.kind === "stop")
2395
2452
  return pick.code;
2453
+ if (pick.kind === "logout")
2454
+ return cmdLogout();
2396
2455
  if (pick.kind === "init")
2397
2456
  return cmdInit(pick.agentId, {});
2398
2457
  if (pick.kind === "demo")
@@ -2497,6 +2556,7 @@ const invokedAsProgram = (() => {
2497
2556
  })();
2498
2557
  if (invokedAsProgram)
2499
2558
  main(process.argv).then(async (code) => {
2559
+ closeSharedRl();
2500
2560
  if (code !== 0)
2501
2561
  process.exitCode = code;
2502
2562
  if (stdinReadAbandoned) {
package/openapi.yaml CHANGED
@@ -5,7 +5,7 @@
5
5
  openapi: 3.1.0
6
6
  info:
7
7
  title: Decidio Agent Gate
8
- version: 0.6.1 # tracks the SDK release - checked by packages/conformance/manifests.mjs
8
+ version: 0.6.3 # tracks the SDK release - checked by packages/conformance/manifests.mjs
9
9
  description: >
10
10
  Authorize an AI-agent action (proceed | route | block), confirm the captured outcome, and
11
11
  receive the signed resume webhook when a routed action is decided. Every outcome seals a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decidio/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -84,7 +84,7 @@
84
84
  "prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
85
85
  },
86
86
  "dependencies": {
87
- "@decidio/verify": "0.6.1"
87
+ "@decidio/verify": "0.6.3"
88
88
  },
89
89
  "author": "OmniTwin Technologies Inc.",
90
90
  "bugs": {