@sechroom/cli 2026.6.29-rc.692f8a8f → 2026.6.30-rc.3b1b16ff

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.
Files changed (2) hide show
  1. package/dist/index.js +194 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
16
16
  var CONFIG_DIR = join(homedir(), ".config", "sechroom");
17
17
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
18
18
  var TOKEN_FILE = join(CONFIG_DIR, "token.json");
19
+ var ACCOUNTS_FILE = join(CONFIG_DIR, "accounts.json");
20
+ var DEFAULT_ACCOUNT = "default";
19
21
  var STATE_DIR_NAME = ".sechroom";
20
22
  var BASELINE_CONFIG_NAME = ".sechroom.json";
21
23
  var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
@@ -44,23 +46,87 @@ function writeDcrClientId(baseUrl, clientId) {
44
46
  const p = readPersisted();
45
47
  writePersisted({ clientIds: { ...p.clientIds ?? {}, [baseUrl]: clientId } });
46
48
  }
47
- function readToken() {
48
- const envTok = process.env.SECHROOM_TOKEN;
49
- if (envTok) return { accessToken: envTok };
49
+ function readAccountsFile() {
50
50
  try {
51
- return JSON.parse(readFileSync(TOKEN_FILE, "utf8"));
51
+ const parsed = JSON.parse(readFileSync(ACCOUNTS_FILE, "utf8"));
52
+ if (parsed && typeof parsed === "object" && parsed.accounts) return parsed;
53
+ } catch {
54
+ }
55
+ try {
56
+ const legacy = JSON.parse(readFileSync(TOKEN_FILE, "utf8"));
57
+ if (legacy?.accessToken) {
58
+ const migrated = {
59
+ accounts: {
60
+ [DEFAULT_ACCOUNT]: { ...legacy, baseUrl: readPersisted().baseUrl ?? DEFAULT_BASE_URL }
61
+ },
62
+ default: DEFAULT_ACCOUNT
63
+ };
64
+ writeAccountsFile(migrated);
65
+ rmSync(TOKEN_FILE, { force: true });
66
+ return migrated;
67
+ }
52
68
  } catch {
53
- return null;
54
69
  }
70
+ return { accounts: {} };
55
71
  }
56
- function writeToken(tok) {
72
+ function writeAccountsFile(file) {
57
73
  ensureDir();
58
- writeFileSync(TOKEN_FILE, JSON.stringify(tok, null, 2), { mode: 384 });
74
+ writeFileSync(ACCOUNTS_FILE, JSON.stringify(file, null, 2), { mode: 384 });
75
+ }
76
+ function readAccount(alias) {
77
+ return readAccountsFile().accounts[alias] ?? null;
78
+ }
79
+ function writeAccount(alias, entry) {
80
+ const file = readAccountsFile();
81
+ file.accounts[alias] = entry;
82
+ if (!file.default) file.default = alias;
83
+ writeAccountsFile(file);
84
+ }
85
+ function removeAccount(alias) {
86
+ const file = readAccountsFile();
87
+ if (!(alias in file.accounts)) return false;
88
+ delete file.accounts[alias];
89
+ if (file.default === alias) file.default = Object.keys(file.accounts)[0];
90
+ writeAccountsFile(file);
91
+ return true;
92
+ }
93
+ function listAccounts() {
94
+ const file = readAccountsFile();
95
+ return Object.entries(file.accounts).map(([alias, entry]) => ({
96
+ alias,
97
+ entry,
98
+ isDefault: file.default === alias
99
+ }));
100
+ }
101
+ function setDefaultAccount(alias) {
102
+ const file = readAccountsFile();
103
+ if (!(alias in file.accounts)) throw new Error(`No account "${alias}". Run \`sechroom login --account ${alias}\` first.`);
104
+ file.default = alias;
105
+ writeAccountsFile(file);
106
+ }
107
+ function resolveAccountAlias(flagAccount) {
108
+ return flagAccount ?? process.env.SECHROOM_ACCOUNT ?? readLocalConfig().account ?? readAccountsFile().default ?? DEFAULT_ACCOUNT;
109
+ }
110
+ function writeLocalAccount(alias) {
111
+ const home = findConfigHome() ?? process.cwd();
112
+ const overridePath = join(home, OVERRIDE_CONFIG_NAME);
113
+ mkdirSync(dirname(overridePath), { recursive: true });
114
+ const current = readJsonConfig(overridePath) ?? {};
115
+ writeFileSync(
116
+ overridePath,
117
+ JSON.stringify({ ...current, account: alias }, null, 2),
118
+ { mode: 384 }
119
+ );
120
+ return overridePath;
121
+ }
122
+ function readToken() {
123
+ const envTok = process.env.SECHROOM_TOKEN;
124
+ if (envTok) return { accessToken: envTok };
125
+ return readAccount(resolveAccountAlias());
59
126
  }
60
127
  function clearToken() {
61
- if (!existsSync(TOKEN_FILE)) return void 0;
62
- rmSync(TOKEN_FILE);
63
- return TOKEN_FILE;
128
+ const alias = resolveAccountAlias();
129
+ return removeAccount(alias) ? `${ACCOUNTS_FILE} (account "${alias}")` : void 0;
64
130
  }
65
131
  function clearPersisted() {
66
132
  if (!existsSync(CONFIG_FILE)) return void 0;
@@ -95,6 +161,7 @@ function readLocalConfig() {
95
161
  tenant: merged.tenant,
96
162
  workspaceId: merged.workspaceId,
97
163
  defaultProjectId: merged.defaultProjectId,
164
+ account: merged.account,
98
165
  path: existsSync(baselinePath) ? baselinePath : overridePath
99
166
  };
100
167
  }
@@ -129,7 +196,8 @@ function resolveConfig(flags) {
129
196
  }
130
197
  const workspaceId = process.env.SECHROOM_WORKSPACE ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
131
198
  const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
132
- return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
199
+ const account = resolveAccountAlias(flags.account);
200
+ return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, account, workspaceId, defaultProjectId, clientId: persisted.clientId };
133
201
  }
134
202
  function describeConfig(flags) {
135
203
  const local = readLocalConfig();
@@ -245,14 +313,25 @@ async function exchange(meta, clientId, code, verifier, redirectUri) {
245
313
  })
246
314
  });
247
315
  if (!res.ok) throw new Error(`Token exchange failed (${res.status}): ${await res.text()}`);
248
- persistTokenResponse(await res.json());
316
+ return await res.json();
317
+ }
318
+ function decodeEmail(accessToken) {
319
+ try {
320
+ const payload = accessToken.split(".")[1];
321
+ if (!payload) return void 0;
322
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
323
+ return typeof claims.email === "string" ? claims.email : void 0;
324
+ } catch {
325
+ return void 0;
326
+ }
249
327
  }
250
- function persistTokenResponse(json) {
251
- const tok = json;
252
- writeToken({
328
+ function persistTokenResponse(account, baseUrl, tok) {
329
+ writeAccount(account, {
330
+ baseUrl,
253
331
  accessToken: tok.access_token,
254
332
  refreshToken: tok.refresh_token,
255
- expiresAt: tok.expires_in ? Date.now() + tok.expires_in * 1e3 : void 0
333
+ expiresAt: tok.expires_in ? Date.now() + tok.expires_in * 1e3 : void 0,
334
+ email: decodeEmail(tok.access_token)
256
335
  });
257
336
  }
258
337
  async function login(cfg) {
@@ -276,14 +355,25 @@ ${authUrl}
276
355
  `);
277
356
  await open(authUrl.toString());
278
357
  const code = await loopback.code;
279
- await exchange(meta, clientId, code, verifier, loopback.redirectUri);
280
- process.stderr.write("Signed in. Token cached.\n");
358
+ const tok = await exchange(meta, clientId, code, verifier, loopback.redirectUri);
359
+ persistTokenResponse(cfg.account, cfg.baseUrl, tok);
360
+ const email = decodeEmail(tok.access_token);
361
+ process.stderr.write(`Signed in${email ? ` as ${email}` : ""} (account "${cfg.account}"). Token cached.
362
+ `);
281
363
  }
282
364
  async function requireToken(cfg) {
283
365
  if (process.env.SECHROOM_TOKEN) return process.env.SECHROOM_TOKEN;
284
- const cached = readToken();
366
+ const cached = readAccount(cfg.account);
285
367
  if (!cached?.accessToken) {
286
- throw new Error("Not signed in. Run `sechroom login` (or set SECHROOM_TOKEN for headless use).");
368
+ throw new Error(
369
+ `Not signed in (account "${cfg.account}"). Run \`sechroom login --account ${cfg.account}\` (or set SECHROOM_TOKEN for headless use).`
370
+ );
371
+ }
372
+ if (cached.baseUrl !== cfg.baseUrl) {
373
+ const matching = listAccounts().filter((a) => a.entry.baseUrl === cfg.baseUrl).map((a) => a.alias);
374
+ throw new Error(
375
+ `Account "${cfg.account}" is signed in to ${cached.baseUrl}, but this call targets ${cfg.baseUrl}. ` + (matching.length > 0 ? `Use --account ${matching[0]} (signed in there)` + (matching.length > 1 ? ` \u2014 or one of: ${matching.join(", ")}` : "") + ", or " : "") + `run \`sechroom login --account ${cfg.account}\` against it.`
376
+ );
287
377
  }
288
378
  const nearExpiry = cached.expiresAt !== void 0 && Date.now() > cached.expiresAt - 6e4;
289
379
  if (nearExpiry && cached.refreshToken) {
@@ -297,10 +387,10 @@ async function requireToken(cfg) {
297
387
  })
298
388
  });
299
389
  if (res.ok) {
300
- persistTokenResponse(await res.json());
301
- return readToken().accessToken;
390
+ persistTokenResponse(cfg.account, cfg.baseUrl, await res.json());
391
+ return readAccount(cfg.account).accessToken;
302
392
  }
303
- throw new Error("Session expired and refresh failed. Run `sechroom login` again.");
393
+ throw new Error(`Session expired and refresh failed. Run \`sechroom login --account ${cfg.account}\` again.`);
304
394
  }
305
395
  return cached.accessToken;
306
396
  }
@@ -545,6 +635,62 @@ function fail(error) {
545
635
  process.exit(1);
546
636
  }
547
637
 
638
+ // src/commands/accounts.ts
639
+ function registerAccounts(program2) {
640
+ const accounts = program2.command("accounts").description("Manage signed-in identities (multi-account)");
641
+ accounts.addHelpText(
642
+ "after",
643
+ `
644
+ Examples:
645
+ $ sechroom login --account work sign in a second identity under the "work" alias
646
+ $ sechroom accounts list see every signed-in identity + which is active here
647
+ $ sechroom accounts use work make "work" the machine default
648
+ $ sechroom accounts use work --local pin "work" for THIS directory only (gitignored)
649
+ $ sechroom logout --account work sign the "work" identity out`
650
+ );
651
+ accounts.command("list").description("List signed-in accounts (alias, email, server, expiry)").action((_opts, cmd) => {
652
+ const active2 = resolveAccountAlias(cmd.optsWithGlobals().account);
653
+ const rows = listAccounts().map(({ alias, entry, isDefault }) => ({
654
+ alias,
655
+ email: entry.email,
656
+ baseUrl: entry.baseUrl,
657
+ expiresAt: entry.expiresAt ? new Date(entry.expiresAt).toISOString() : void 0,
658
+ isDefault,
659
+ isActiveHere: alias === active2
660
+ }));
661
+ if (cmd.optsWithGlobals().json) return emit({ accounts: rows, active: active2 }, true);
662
+ if (rows.length === 0) {
663
+ console.log(style.dim("No accounts signed in. Run `sechroom login` (optionally --account <alias>)."));
664
+ return;
665
+ }
666
+ for (const r of rows) {
667
+ const markers = [r.isActiveHere ? style.green("active") : "", r.isDefault ? style.dim("default") : ""].filter(Boolean).join(" ");
668
+ console.log(
669
+ `${r.alias.padEnd(16)} ${(r.email ?? "\u2014").padEnd(28)} ${r.baseUrl} ${markers}`
670
+ );
671
+ }
672
+ });
673
+ accounts.command("use <alias>").description("Select the account used for calls (machine default, or --local for this directory)").option("--local", "pin for this directory only (gitignored .sechroom/config.json)").action((alias, opts, cmd) => {
674
+ const json = cmd.optsWithGlobals().json;
675
+ if (opts.local) {
676
+ const path = writeLocalAccount(alias);
677
+ if (json) return emit({ account: alias, scope: "local", path }, true);
678
+ console.log(style.green(`Account "${alias}" pinned for this directory (${path}).`));
679
+ return;
680
+ }
681
+ setDefaultAccount(alias);
682
+ if (json) return emit({ account: alias, scope: "global" }, true);
683
+ console.log(style.green(`Account "${alias}" is now the machine default.`));
684
+ });
685
+ accounts.command("remove <alias>").description("Forget an account's cached token (local removal \u2014 does not revoke at the server)").action((alias, _opts, cmd) => {
686
+ const removed = removeAccount(alias);
687
+ if (cmd.optsWithGlobals().json) return emit({ removed: removed ? [alias] : [] }, true);
688
+ console.log(
689
+ removed ? style.green(`Account "${alias}" removed.`) : style.dim(`No account "${alias}".`)
690
+ );
691
+ });
692
+ }
693
+
548
694
  // src/commands/memory.ts
549
695
  function registerMemory(program2) {
550
696
  const memory = program2.command("memory").description("Create, read, and search memories");
@@ -3179,7 +3325,7 @@ async function ensureTenant(baseUrl, g, opts) {
3179
3325
  const local = readLocalConfig();
3180
3326
  let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
3181
3327
  if (!tenant) {
3182
- const client = await makeClient({ baseUrl, tenant: "", clientId: persisted.clientId });
3328
+ const client = await makeClient({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: persisted.clientId });
3183
3329
  const { data, error } = await client.GET("/auth/me/tenants", {});
3184
3330
  if (error) {
3185
3331
  fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
@@ -3214,7 +3360,7 @@ async function ensureTenant(baseUrl, g, opts) {
3214
3360
  }
3215
3361
  }
3216
3362
  const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
3217
- const wsClient = await makeClient({ baseUrl, tenant, clientId: persisted.clientId });
3363
+ const wsClient = await makeClient({ baseUrl, tenant, account: resolveAccountAlias(), clientId: persisted.clientId });
3218
3364
  const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
3219
3365
  yes: opts.yes,
3220
3366
  json: opts.json,
@@ -3249,7 +3395,7 @@ async function ensureTenant(baseUrl, g, opts) {
3249
3395
  `);
3250
3396
  }
3251
3397
  }
3252
- return { baseUrl, tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
3398
+ return { baseUrl, tenant, account: resolveAccountAlias(), workspaceId, defaultProjectId, clientId: persisted.clientId };
3253
3399
  }
3254
3400
  async function ensureAuth(cfg, yes) {
3255
3401
  if (process.env.SECHROOM_TOKEN) return;
@@ -3432,13 +3578,13 @@ Examples:
3432
3578
  if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
3433
3579
  if (opts.recurse) {
3434
3580
  const baseUrl2 = resolveBaseUrl(g);
3435
- await ensureAuth({ baseUrl: baseUrl2, tenant: "", clientId: readPersisted().clientId }, yes);
3581
+ await ensureAuth({ baseUrl: baseUrl2, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
3436
3582
  const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
3437
3583
  await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
3438
3584
  return;
3439
3585
  }
3440
3586
  const baseUrl = resolveBaseUrl(g);
3441
- await ensureAuth({ baseUrl, tenant: "", clientId: readPersisted().clientId }, yes);
3587
+ await ensureAuth({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
3442
3588
  const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
3443
3589
  const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
3444
3590
  if (!json && tz.action !== "already-set") {
@@ -3935,9 +4081,23 @@ function removeMaterialisedSkills(dir) {
3935
4081
  return removed;
3936
4082
  }
3937
4083
  function registerReset(program2) {
3938
- program2.command("logout").description("Sign out \u2014 remove the cached (global) auth token").action((_opts, cmd) => {
4084
+ program2.command("logout").description("Sign out \u2014 remove the active account's cached token (--account <alias> for a specific one, --all for every account)").option("--account <alias>", "sign a specific account out").option("--all", "sign every account out").action((opts, cmd) => {
4085
+ const json = cmd.optsWithGlobals().json;
4086
+ if (opts.all) {
4087
+ const removed2 = listAccounts().map((a) => a.alias);
4088
+ for (const alias of removed2) removeAccount(alias);
4089
+ if (json) return emit({ removed: removed2 }, true);
4090
+ console.log(removed2.length > 0 ? style.green(`Signed out ${removed2.length} account(s): ${removed2.join(", ")}.`) : style.dim("Already signed out (no accounts)."));
4091
+ return;
4092
+ }
4093
+ if (opts.account) {
4094
+ const ok2 = removeAccount(opts.account);
4095
+ if (json) return emit({ removed: ok2 ? [opts.account] : [] }, true);
4096
+ console.log(ok2 ? style.green(`Signed out account "${opts.account}".`) : style.dim(`No account "${opts.account}".`));
4097
+ return;
4098
+ }
3939
4099
  const removed = clearToken();
3940
- if (cmd.optsWithGlobals().json) return emit({ removed: removed ? [removed] : [] }, true);
4100
+ if (json) return emit({ removed: removed ? [removed] : [] }, true);
3941
4101
  console.log(
3942
4102
  removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
3943
4103
  );
@@ -4032,11 +4192,12 @@ Examples:
4032
4192
  $ sechroom login sign in to the configured base URL + tenant
4033
4193
  $ sechroom login --base-url https://staging.app.sechroom.ai/api
4034
4194
  $ export SECHROOM_TOKEN=<bearer> headless: skip login entirely (CI / agents)`
4035
- ).action(async (_opts, cmd) => {
4195
+ ).option("--account <alias>", "sign in under a named account alias (multi-account; default: the active alias)").action(async (opts, cmd) => {
4036
4196
  const g = cmd.optsWithGlobals();
4037
4197
  const persisted = readPersisted();
4038
4198
  const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? "https://app.sechroom.ai/api";
4039
- await login({ baseUrl: baseUrl.replace(/\/$/, ""), tenant: g.tenant ?? "" });
4199
+ const account = resolveAccountAlias(opts.account);
4200
+ await login({ baseUrl: baseUrl.replace(/\/$/, ""), tenant: g.tenant ?? "", account });
4040
4201
  });
4041
4202
  var config = program.command("config").description("Manage persisted CLI config");
4042
4203
  config.addHelpText(
@@ -4093,6 +4254,7 @@ local: ${d.localPath ?? "(none)"} ${JSON.stringify(readLocalConfig())}
4093
4254
  `
4094
4255
  );
4095
4256
  });
4257
+ registerAccounts(program);
4096
4258
  registerMemory(program);
4097
4259
  registerWorklog(program);
4098
4260
  registerLookup(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.29-rc.692f8a8f",
3
+ "version": "2026.6.30-rc.3b1b16ff",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",