recappi 0.1.0 → 0.1.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/dist/index.js CHANGED
@@ -692,6 +692,7 @@ var init_tui = __esm({
692
692
 
693
693
  // src/cli.ts
694
694
  import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
695
+ import os3 from "os";
695
696
 
696
697
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
697
698
  var external_exports = {};
@@ -15208,7 +15209,7 @@ function date4(params) {
15208
15209
  config(en_default());
15209
15210
 
15210
15211
  // ../../packages/contracts/src/index.ts
15211
- var CLI_SCHEMA_VERSION = "2026-06-24";
15212
+ var CLI_SCHEMA_VERSION = "2026-06-25";
15212
15213
  function toJsonSchema(schema) {
15213
15214
  return external_exports.toJSONSchema(schema, { target: "draft-2020-12" });
15214
15215
  }
@@ -15262,6 +15263,22 @@ var authStatusDataSchema = external_exports.object({
15262
15263
  email: external_exports.string().optional(),
15263
15264
  userId: external_exports.string().optional()
15264
15265
  });
15266
+ var authLoginDataSchema = external_exports.object({
15267
+ loggedIn: external_exports.literal(true),
15268
+ origin: external_exports.string(),
15269
+ email: external_exports.string().optional(),
15270
+ userId: external_exports.string().optional()
15271
+ });
15272
+ var authLogoutDataSchema = external_exports.object({
15273
+ loggedIn: external_exports.literal(false),
15274
+ origin: external_exports.string(),
15275
+ cleared: external_exports.boolean()
15276
+ });
15277
+ var authImportDataSchema = external_exports.object({
15278
+ imported: external_exports.boolean(),
15279
+ origin: external_exports.string(),
15280
+ source: external_exports.literal("macos-keychain")
15281
+ });
15265
15282
  var uploadSuccessSchema = external_exports.object({
15266
15283
  filePath: external_exports.string(),
15267
15284
  recordingId: external_exports.string(),
@@ -15543,17 +15560,19 @@ async function resolveAuthContext(opts = {}) {
15543
15560
  token: config2.token,
15544
15561
  source: "config"
15545
15562
  };
15546
- const platform = opts.platform ?? process.platform;
15547
- if (platform === "darwin" && env.RECAPPI_DISABLE_KEYCHAIN_AUTH !== "1") {
15548
- const token = await readMacOSAppToken();
15549
- if (token) return { origin, token, source: "macos-keychain" };
15563
+ if (opts.includeMacOSKeychain === true) {
15564
+ const keychain = await inspectMacOSAppKeychain({
15565
+ env,
15566
+ platform: opts.platform
15567
+ });
15568
+ if (keychain.token) return { origin, token: keychain.token, source: "macos-keychain" };
15550
15569
  }
15551
15570
  return { origin, token: null, source: "none" };
15552
15571
  }
15553
15572
  function requireToken(ctx) {
15554
15573
  if (!ctx.token) {
15555
15574
  throw cliError("auth.not_logged_in", "Not logged in to Recappi.", {
15556
- hint: "Set RECAPPI_AUTH_TOKEN, or open Recappi Mini and sign in before retrying."
15575
+ hint: "Run recappi auth login, or set RECAPPI_AUTH_TOKEN for automation."
15557
15576
  });
15558
15577
  }
15559
15578
  return ctx.token;
@@ -15579,14 +15598,61 @@ function validateOrigin(value) {
15579
15598
  }
15580
15599
  return url2.origin;
15581
15600
  }
15601
+ async function saveAuthConfig(homeDir, config2) {
15602
+ const target = primaryConfigPath(homeDir);
15603
+ await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
15604
+ const existing = await readConfigObject(target);
15605
+ const next = {
15606
+ ...existing,
15607
+ origin: validateOrigin(config2.origin),
15608
+ authToken: config2.token
15609
+ };
15610
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
15611
+ await fs.writeFile(tmp, `${JSON.stringify(next, null, 2)}
15612
+ `, { mode: 384 });
15613
+ await fs.rename(tmp, target);
15614
+ await fs.chmod(target, 384).catch(() => {
15615
+ });
15616
+ }
15617
+ async function clearAuthConfig(homeDir) {
15618
+ const target = primaryConfigPath(homeDir);
15619
+ const existing = await readConfigObject(target);
15620
+ if (!("authToken" in existing)) return false;
15621
+ delete existing.authToken;
15622
+ await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
15623
+ await fs.writeFile(target, `${JSON.stringify(existing, null, 2)}
15624
+ `, { mode: 384 });
15625
+ await fs.chmod(target, 384).catch(() => {
15626
+ });
15627
+ return true;
15628
+ }
15629
+ async function inspectMacOSAppKeychain(opts = {}) {
15630
+ const env = opts.env ?? process.env;
15631
+ if (env.RECAPPI_DISABLE_KEYCHAIN_AUTH === "1") {
15632
+ return {
15633
+ status: "disabled",
15634
+ token: null,
15635
+ message: "macOS app keychain lookup is disabled by RECAPPI_DISABLE_KEYCHAIN_AUTH=1."
15636
+ };
15637
+ }
15638
+ const platform = opts.platform ?? process.platform;
15639
+ if (platform !== "darwin") {
15640
+ return {
15641
+ status: "unsupported",
15642
+ token: null,
15643
+ message: "macOS app keychain lookup is only available on macOS."
15644
+ };
15645
+ }
15646
+ return readMacOSAppToken();
15647
+ }
15648
+ function primaryConfigPath(homeDir) {
15649
+ return path.join(homeDir, ".config", "recappi", "config.json");
15650
+ }
15582
15651
  async function readConfig(homeDir) {
15583
- const candidates = [
15584
- path.join(homeDir, ".config", "recappi", "config.json"),
15585
- path.join(homeDir, ".recappi", "config.json")
15586
- ];
15652
+ const candidates = [primaryConfigPath(homeDir), path.join(homeDir, ".recappi", "config.json")];
15587
15653
  for (const candidate of candidates) {
15588
15654
  try {
15589
- const parsed = JSON.parse(await fs.readFile(candidate, "utf8"));
15655
+ const parsed = await readConfigObject(candidate);
15590
15656
  const token = typeof parsed.authToken === "string" ? parsed.authToken.trim() : void 0;
15591
15657
  const origin = typeof parsed.origin === "string" ? parsed.origin.trim() : void 0;
15592
15658
  if (token || origin) return { ...token ? { token } : {}, ...origin ? { origin } : {} };
@@ -15601,6 +15667,14 @@ async function readConfig(homeDir) {
15601
15667
  }
15602
15668
  return {};
15603
15669
  }
15670
+ async function readConfigObject(candidate) {
15671
+ try {
15672
+ return JSON.parse(await fs.readFile(candidate, "utf8"));
15673
+ } catch (error51) {
15674
+ if (error51.code === "ENOENT") return {};
15675
+ throw error51;
15676
+ }
15677
+ }
15604
15678
  async function readMacOSAppToken() {
15605
15679
  try {
15606
15680
  const { stdout } = await execFileAsync(
@@ -15609,10 +15683,165 @@ async function readMacOSAppToken() {
15609
15683
  { timeout: 2e3, maxBuffer: 1024 * 1024 }
15610
15684
  );
15611
15685
  const token = stdout.trim();
15612
- return token.length > 0 ? token : null;
15613
- } catch {
15614
- return null;
15686
+ if (token.length > 0) {
15687
+ return {
15688
+ status: "ok",
15689
+ token,
15690
+ message: "Found a Recappi Mini app token in the macOS keychain."
15691
+ };
15692
+ }
15693
+ return {
15694
+ status: "missing",
15695
+ token: null,
15696
+ message: "Recappi Mini app keychain item was present but empty."
15697
+ };
15698
+ } catch (error51) {
15699
+ const err = error51;
15700
+ const timedOut = err.killed || err.signal === "SIGTERM";
15701
+ const stderr = typeof err.message === "string" ? err.message : "";
15702
+ if (!timedOut && /could not be found|The specified item could not be found/i.test(stderr)) {
15703
+ return {
15704
+ status: "missing",
15705
+ token: null,
15706
+ message: "No Recappi Mini app token was found in the macOS keychain."
15707
+ };
15708
+ }
15709
+ return {
15710
+ status: "error",
15711
+ token: null,
15712
+ message: timedOut ? "Timed out while reading the Recappi Mini app keychain token." : "Could not read the Recappi Mini app keychain token.",
15713
+ hint: "Use recappi auth login for the CLI, or run recappi auth import-macos explicitly if you want to import the app session."
15714
+ };
15715
+ }
15716
+ }
15717
+
15718
+ // src/auth-login.ts
15719
+ import { execFile as execFile2 } from "child_process";
15720
+ import os2 from "os";
15721
+ import { promisify as promisify2 } from "util";
15722
+ var execFileAsync2 = promisify2(execFile2);
15723
+ async function loginWithDeviceCode(opts) {
15724
+ const origin = validateOrigin(opts.origin);
15725
+ const fetchImpl = opts.deps?.fetchImpl ?? fetch;
15726
+ const sleep = opts.deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
15727
+ const start = await startDeviceAuth(origin, fetchImpl);
15728
+ opts.onPrompt?.(
15729
+ [
15730
+ "To sign in to Recappi CLI:",
15731
+ ` 1. Open ${start.verificationUri}`,
15732
+ ` 2. Enter code: ${start.userCode}`,
15733
+ ""
15734
+ ].join("\n")
15735
+ );
15736
+ if (!opts.noOpen) {
15737
+ const openUrl2 = opts.deps?.openUrl ?? openUrlWithSystemBrowser;
15738
+ await openUrl2(start.verificationUriComplete).catch((error51) => {
15739
+ opts.onPrompt?.(
15740
+ `Could not open the browser automatically: ${error51 instanceof Error ? error51.message : String(error51)}
15741
+ `
15742
+ );
15743
+ });
15744
+ }
15745
+ let intervalMs = start.interval * 1e3;
15746
+ const expiresAt = Date.now() + start.expiresIn * 1e3;
15747
+ while (Date.now() < expiresAt) {
15748
+ await sleep(intervalMs);
15749
+ const poll = await pollDeviceAuth(origin, start.deviceCode, fetchImpl);
15750
+ if (poll.status === "pending") {
15751
+ if (typeof poll.interval === "number") intervalMs = poll.interval * 1e3;
15752
+ continue;
15753
+ }
15754
+ if (poll.status === "slow_down") {
15755
+ intervalMs = (typeof poll.interval === "number" ? poll.interval : intervalMs / 1e3 + 5) * 1e3;
15756
+ continue;
15757
+ }
15758
+ if (poll.status === "denied") {
15759
+ throw cliError("auth.unauthorized", "Recappi CLI sign-in was denied.");
15760
+ }
15761
+ if (poll.status === "expired") {
15762
+ throw cliError("auth.unauthorized", "Recappi CLI sign-in code expired.", {
15763
+ hint: "Run recappi auth login again."
15764
+ });
15765
+ }
15766
+ if (poll.status === "authorized") {
15767
+ const token = typeof poll.token === "string" ? poll.token.trim() : "";
15768
+ if (!token) {
15769
+ throw cliError("cloud.invalid_response", "Recappi device auth returned no token.");
15770
+ }
15771
+ const user = isRecord(poll.user) ? poll.user : {};
15772
+ await saveAuthConfig(opts.homeDir ?? os2.homedir(), { origin, token });
15773
+ return {
15774
+ loggedIn: true,
15775
+ origin,
15776
+ ...typeof user.email === "string" ? { email: user.email } : {},
15777
+ ...typeof user.id === "string" ? { userId: user.id } : {}
15778
+ };
15779
+ }
15780
+ throw cliError("cloud.invalid_response", "Recappi device auth returned an unknown status.");
15781
+ }
15782
+ throw cliError("auth.unauthorized", "Recappi CLI sign-in timed out.", {
15783
+ hint: "Run recappi auth login again."
15784
+ });
15785
+ }
15786
+ async function startDeviceAuth(origin, fetchImpl) {
15787
+ const response = await fetchImpl(new URL("/api/device-auth/start", origin), {
15788
+ method: "POST"
15789
+ });
15790
+ if (!response.ok) {
15791
+ throw cliError(
15792
+ "cloud.http_error",
15793
+ `Could not start Recappi device sign-in (${response.status}).`,
15794
+ {
15795
+ retryable: response.status >= 500
15796
+ }
15797
+ );
15798
+ }
15799
+ const body = await response.json();
15800
+ const deviceCode = stringField(body.device_code, "device_code");
15801
+ const userCode = stringField(body.user_code, "user_code");
15802
+ const verificationUri = stringField(body.verification_uri, "verification_uri");
15803
+ const verificationUriComplete = stringField(
15804
+ body.verification_uri_complete,
15805
+ "verification_uri_complete"
15806
+ );
15807
+ const expiresIn = numberField(body.expires_in, "expires_in");
15808
+ const interval = numberField(body.interval, "interval");
15809
+ return { deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn, interval };
15810
+ }
15811
+ async function pollDeviceAuth(origin, deviceCode, fetchImpl) {
15812
+ const response = await fetchImpl(new URL("/api/device-auth/poll", origin), {
15813
+ method: "POST",
15814
+ headers: { "content-type": "application/json" },
15815
+ body: JSON.stringify({ device_code: deviceCode })
15816
+ });
15817
+ if (!response.ok) {
15818
+ throw cliError("cloud.http_error", `Recappi device sign-in poll failed (${response.status}).`, {
15819
+ retryable: response.status >= 500
15820
+ });
15615
15821
  }
15822
+ return await response.json();
15823
+ }
15824
+ async function openUrlWithSystemBrowser(url2) {
15825
+ if (process.platform === "darwin") {
15826
+ await execFileAsync2("/usr/bin/open", [url2]);
15827
+ return;
15828
+ }
15829
+ if (process.platform === "win32") {
15830
+ await execFileAsync2("cmd", ["/c", "start", "", url2]);
15831
+ return;
15832
+ }
15833
+ await execFileAsync2("xdg-open", [url2]);
15834
+ }
15835
+ function stringField(value, name) {
15836
+ if (typeof value === "string" && value.length > 0) return value;
15837
+ throw cliError("cloud.invalid_response", `Recappi device auth response is missing ${name}.`);
15838
+ }
15839
+ function numberField(value, name) {
15840
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
15841
+ throw cliError("cloud.invalid_response", `Recappi device auth response is missing ${name}.`);
15842
+ }
15843
+ function isRecord(value) {
15844
+ return typeof value === "object" && value !== null;
15616
15845
  }
15617
15846
 
15618
15847
  // src/api.ts
@@ -15779,9 +16008,11 @@ var RecappiApiClient = class {
15779
16008
  this.auth = auth;
15780
16009
  this.fetchImpl = opts.fetchImpl ?? fetch;
15781
16010
  this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16011
+ this.env = opts.env ?? process.env;
15782
16012
  }
15783
16013
  fetchImpl;
15784
16014
  sleep;
16015
+ env;
15785
16016
  async authStatus() {
15786
16017
  if (!this.auth.token) {
15787
16018
  return { loggedIn: false, origin: this.auth.origin };
@@ -15793,7 +16024,7 @@ var RecappiApiClient = class {
15793
16024
  return { loggedIn: false, origin: this.auth.origin };
15794
16025
  }
15795
16026
  const body = await parseJson(response);
15796
- const user = isRecord(body) && isRecord(body.user) ? body.user : void 0;
16027
+ const user = isRecord2(body) && isRecord2(body.user) ? body.user : void 0;
15797
16028
  return {
15798
16029
  loggedIn: Boolean(user),
15799
16030
  origin: this.auth.origin,
@@ -15820,7 +16051,7 @@ var RecappiApiClient = class {
15820
16051
  status: this.auth.token ? "ok" : "error",
15821
16052
  message: this.auth.token ? `Found auth token from ${this.auth.source}.` : "No Recappi auth token found.",
15822
16053
  ...this.auth.token ? {} : {
15823
- hint: "Set RECAPPI_AUTH_TOKEN, configure ~/.config/recappi/config.json, or sign in with Recappi Mini on macOS."
16054
+ hint: "Run recappi auth login, or set RECAPPI_AUTH_TOKEN for automation."
15824
16055
  }
15825
16056
  });
15826
16057
  if (this.auth.token) {
@@ -15830,7 +16061,9 @@ var RecappiApiClient = class {
15830
16061
  name: "auth.session",
15831
16062
  status: status2.loggedIn ? "ok" : "error",
15832
16063
  message: status2.loggedIn ? `Cloud session is valid${status2.email ? ` for ${status2.email}` : ""}.` : "Cloud did not accept the configured auth token.",
15833
- ...status2.loggedIn ? {} : { hint: "Refresh the token or sign in again before retrying cloud commands." }
16064
+ ...status2.loggedIn ? {} : {
16065
+ hint: `Run recappi auth login again. If you use --origin, make sure the token belongs to ${this.auth.origin}.`
16066
+ }
15834
16067
  });
15835
16068
  } catch (error51) {
15836
16069
  const cli = toCliError(error51);
@@ -15842,6 +16075,13 @@ var RecappiApiClient = class {
15842
16075
  });
15843
16076
  }
15844
16077
  }
16078
+ const keychain = await inspectMacOSAppKeychain({ env: this.env });
16079
+ checks.push({
16080
+ name: "auth.macos_keychain",
16081
+ status: keychain.status === "error" ? "warn" : "ok",
16082
+ message: keychain.message,
16083
+ ...keychain.status === "ok" ? { hint: "Run recappi auth import-macos to copy this app session into CLI config." } : keychain.hint ? { hint: keychain.hint } : {}
16084
+ });
15845
16085
  checks.push({
15846
16086
  name: "audio.wav",
15847
16087
  status: "ok",
@@ -15882,7 +16122,7 @@ var RecappiApiClient = class {
15882
16122
  limit: String(opts.limit)
15883
16123
  });
15884
16124
  const parsed = await this.getJson(`/api/jobs?${params}`);
15885
- const items = Array.isArray(parsed.items) ? parsed.items.filter(isRecord).map(mapJobListItem) : [];
16125
+ const items = Array.isArray(parsed.items) ? parsed.items.filter(isRecord2).map(mapJobListItem) : [];
15886
16126
  return jobListDataSchema.parse({
15887
16127
  items,
15888
16128
  status: typeof parsed.status === "string" ? parsed.status : opts.status,
@@ -16035,7 +16275,7 @@ var RecappiApiClient = class {
16035
16275
  }
16036
16276
  );
16037
16277
  const parsed = await parseJson(response);
16038
- if (!isRecord(parsed) || typeof parsed.etag !== "string") {
16278
+ if (!isRecord2(parsed) || typeof parsed.etag !== "string") {
16039
16279
  throw cliError("cloud.invalid_response", "Upload part response was missing etag.");
16040
16280
  }
16041
16281
  parts.push({ partNumber, etag: parsed.etag });
@@ -16107,8 +16347,8 @@ async function parseJson(response) {
16107
16347
  async function responseMessage(response) {
16108
16348
  try {
16109
16349
  const parsed = await response.clone().json();
16110
- if (isRecord(parsed) && typeof parsed.message === "string") return parsed.message;
16111
- if (isRecord(parsed) && typeof parsed.error === "string") return parsed.error;
16350
+ if (isRecord2(parsed) && typeof parsed.message === "string") return parsed.message;
16351
+ if (isRecord2(parsed) && typeof parsed.error === "string") return parsed.error;
16112
16352
  } catch {
16113
16353
  }
16114
16354
  try {
@@ -16118,7 +16358,7 @@ async function responseMessage(response) {
16118
16358
  return response.statusText;
16119
16359
  }
16120
16360
  }
16121
- function isRecord(value) {
16361
+ function isRecord2(value) {
16122
16362
  return typeof value === "object" && value !== null && !Array.isArray(value);
16123
16363
  }
16124
16364
  function mapTranscript(row) {
@@ -16162,7 +16402,7 @@ function requiredNumber(row, key) {
16162
16402
  }
16163
16403
  function parseSegments(value, text, durationMs) {
16164
16404
  const decoded = decodeJsonArray(value);
16165
- const rawSegments = decoded.filter(isRecord).map((segment) => {
16405
+ const rawSegments = decoded.filter(isRecord2).map((segment) => {
16166
16406
  const segmentText = typeof segment.text === "string" ? segment.text.trim() : "";
16167
16407
  if (!segmentText) return null;
16168
16408
  const start = typeof segment.start === "number" && segment.start >= 0 ? segment.start : 0;
@@ -16213,12 +16453,12 @@ function parseSummary(row) {
16213
16453
  if (strings.length > 0) summary[key] = strings;
16214
16454
  }
16215
16455
  }
16216
- const actionItems = Array.isArray(payload?.actionItems) ? payload.actionItems.filter(isRecord).flatMap((item) => {
16456
+ const actionItems = Array.isArray(payload?.actionItems) ? payload.actionItems.filter(isRecord2).flatMap((item) => {
16217
16457
  if (typeof item.what !== "string" || !item.what.trim()) return [];
16218
16458
  return [{ what: item.what, ...typeof item.who === "string" ? { who: item.who } : {} }];
16219
16459
  }) : [];
16220
16460
  if (actionItems.length > 0) summary.actionItems = actionItems;
16221
- const quotes = Array.isArray(payload?.quotes) ? payload.quotes.filter(isRecord).flatMap((item) => {
16461
+ const quotes = Array.isArray(payload?.quotes) ? payload.quotes.filter(isRecord2).flatMap((item) => {
16222
16462
  if (typeof item.text !== "string" || !item.text.trim()) return [];
16223
16463
  return [
16224
16464
  {
@@ -16228,7 +16468,7 @@ function parseSummary(row) {
16228
16468
  ];
16229
16469
  }) : [];
16230
16470
  if (quotes.length > 0) summary.quotes = quotes;
16231
- const timeline = Array.isArray(payload?.timeline) ? payload.timeline.filter(isRecord).flatMap((item) => {
16471
+ const timeline = Array.isArray(payload?.timeline) ? payload.timeline.filter(isRecord2).flatMap((item) => {
16232
16472
  if (typeof item.startMs !== "number" || typeof item.endMs !== "number" || typeof item.title !== "string" || typeof item.summary !== "string") {
16233
16473
  return [];
16234
16474
  }
@@ -16248,7 +16488,7 @@ function parseSummary(row) {
16248
16488
  return summary;
16249
16489
  }
16250
16490
  function mapJobListItem(row) {
16251
- const recording = isRecord(row.recording) ? row.recording : {};
16491
+ const recording = isRecord2(row.recording) ? row.recording : {};
16252
16492
  return {
16253
16493
  jobId: stringValue(row.jobId) ?? stringValue(row.id) ?? "",
16254
16494
  recordingId: stringValue(row.recordingId) ?? "",
@@ -16295,11 +16535,11 @@ function decodeJsonArray(value) {
16295
16535
  }
16296
16536
  }
16297
16537
  function decodeJsonRecord(value) {
16298
- if (isRecord(value)) return value;
16538
+ if (isRecord2(value)) return value;
16299
16539
  if (typeof value !== "string" || !value.trim()) return null;
16300
16540
  try {
16301
16541
  const decoded = JSON.parse(value);
16302
- return isRecord(decoded) ? decoded : null;
16542
+ return isRecord2(decoded) ? decoded : null;
16303
16543
  } catch {
16304
16544
  return null;
16305
16545
  }
@@ -16385,7 +16625,20 @@ function renderEnvelope(envelope, opts) {
16385
16625
  `);
16386
16626
  }
16387
16627
  function renderHumanSuccess(command, data, opts) {
16388
- if (command === "auth status" && isRecord2(data)) {
16628
+ if (command === "auth login" && isRecord3(data)) {
16629
+ opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
16630
+ `);
16631
+ return;
16632
+ }
16633
+ if (command === "auth logout" && isRecord3(data)) {
16634
+ opts.stdout(data.cleared ? "Signed out of Recappi CLI\n" : "No Recappi CLI session to clear\n");
16635
+ return;
16636
+ }
16637
+ if (command === "auth import-macos" && isRecord3(data)) {
16638
+ opts.stdout("Imported the Recappi Mini app session into Recappi CLI\n");
16639
+ return;
16640
+ }
16641
+ if (command === "auth status" && isRecord3(data)) {
16389
16642
  if (data.loggedIn) {
16390
16643
  opts.stdout(`Signed in${typeof data.email === "string" ? ` as ${data.email}` : ""}
16391
16644
  `);
@@ -16394,17 +16647,17 @@ function renderHumanSuccess(command, data, opts) {
16394
16647
  opts.stdout("Not logged in\n");
16395
16648
  return;
16396
16649
  }
16397
- if (command === "version" && isRecord2(data) && typeof data.version === "string") {
16650
+ if (command === "version" && isRecord3(data) && typeof data.version === "string") {
16398
16651
  opts.stdout(`${data.version}
16399
16652
  `);
16400
16653
  return;
16401
16654
  }
16402
- if (command === "doctor" && isRecord2(data) && Array.isArray(data.checks)) {
16655
+ if (command === "doctor" && isRecord3(data) && Array.isArray(data.checks)) {
16403
16656
  const status = typeof data.status === "string" ? data.status : "unknown";
16404
16657
  opts.stdout(`Doctor: ${status}
16405
16658
  `);
16406
16659
  for (const check2 of data.checks) {
16407
- if (!isRecord2(check2)) continue;
16660
+ if (!isRecord3(check2)) continue;
16408
16661
  const checkStatus = typeof check2.status === "string" ? check2.status : "unknown";
16409
16662
  const name = typeof check2.name === "string" ? check2.name : "check";
16410
16663
  const message = typeof check2.message === "string" ? ` \u2014 ${check2.message}` : "";
@@ -16415,7 +16668,7 @@ function renderHumanSuccess(command, data, opts) {
16415
16668
  }
16416
16669
  return;
16417
16670
  }
16418
- if (command === "transcript get" && isRecord2(data)) {
16671
+ if (command === "transcript get" && isRecord3(data)) {
16419
16672
  renderTranscriptHuman(data, opts);
16420
16673
  return;
16421
16674
  }
@@ -16448,7 +16701,7 @@ Next:
16448
16701
  }
16449
16702
  return;
16450
16703
  }
16451
- if ((command === "jobs wait" || command === "upload") && isRecord2(data)) {
16704
+ if ((command === "jobs wait" || command === "upload") && isRecord3(data)) {
16452
16705
  if (typeof data.transcriptId === "string") {
16453
16706
  opts.stdout("Transcription ready\n");
16454
16707
  opts.stdout(` transcriptId: ${data.transcriptId}
@@ -16469,10 +16722,10 @@ Next:
16469
16722
  }
16470
16723
  return;
16471
16724
  }
16472
- if (command === "schema" && isRecord2(data) && Array.isArray(data.commands)) {
16725
+ if (command === "schema" && isRecord3(data) && Array.isArray(data.commands)) {
16473
16726
  opts.stdout("Commands:\n");
16474
16727
  for (const entry of data.commands) {
16475
- if (!isRecord2(entry) || typeof entry.name !== "string") continue;
16728
+ if (!isRecord3(entry) || typeof entry.name !== "string") continue;
16476
16729
  const summary = typeof entry.summary === "string" ? ` \u2014 ${entry.summary}` : "";
16477
16730
  opts.stdout(` ${entry.name}${summary}
16478
16731
  `);
@@ -16562,7 +16815,7 @@ function renderTranscriptHuman(data, opts) {
16562
16815
  const segments = Array.isArray(data.segments) ? data.segments : [];
16563
16816
  let printedBody = false;
16564
16817
  for (const segment of segments) {
16565
- if (!isRecord2(segment) || typeof segment.text !== "string") continue;
16818
+ if (!isRecord3(segment) || typeof segment.text !== "string") continue;
16566
16819
  const clock = typeof segment.startMs === "number" ? `[${formatClock(segment.startMs / 1e3)}] ` : "";
16567
16820
  const speaker = typeof segment.speaker === "string" ? `${segment.speaker}: ` : "";
16568
16821
  opts.stdout(`${clock}${speaker}${segment.text}
@@ -16574,7 +16827,7 @@ function renderTranscriptHuman(data, opts) {
16574
16827
  `);
16575
16828
  printedBody = true;
16576
16829
  }
16577
- const summary = isRecord2(data.summary) ? data.summary : void 0;
16830
+ const summary = isRecord3(data.summary) ? data.summary : void 0;
16578
16831
  if (!summary || summary.status !== "succeeded") return;
16579
16832
  if (typeof summary.tldr === "string" && summary.tldr.length > 0) {
16580
16833
  opts.stdout(`
@@ -16592,7 +16845,7 @@ Summary:
16592
16845
  if (Array.isArray(summary.actionItems) && summary.actionItems.length > 0) {
16593
16846
  opts.stdout("\nAction items:\n");
16594
16847
  for (const item of summary.actionItems) {
16595
- if (!isRecord2(item) || typeof item.what !== "string") continue;
16848
+ if (!isRecord3(item) || typeof item.what !== "string") continue;
16596
16849
  const who = typeof item.who === "string" ? `${item.who}: ` : "";
16597
16850
  opts.stdout(` - ${who}${item.what}
16598
16851
  `);
@@ -16627,7 +16880,7 @@ function applyFields(command, data, fields, compact) {
16627
16880
  };
16628
16881
  return compact ? compactData(filtered2) : filtered2;
16629
16882
  }
16630
- if (!isRecord2(data)) return data;
16883
+ if (!isRecord3(data)) return data;
16631
16884
  const allowed = new Set(Object.keys(data));
16632
16885
  assertKnownFields(fields, allowed);
16633
16886
  const filtered = pickFields(data, fields);
@@ -16652,7 +16905,7 @@ function compactData(value) {
16652
16905
  if (Array.isArray(value)) {
16653
16906
  return value.map(compactData).filter((item) => item !== void 0);
16654
16907
  }
16655
- if (isRecord2(value)) {
16908
+ if (isRecord3(value)) {
16656
16909
  const out = {};
16657
16910
  for (const [key, child] of Object.entries(value)) {
16658
16911
  const compacted = compactData(child);
@@ -16670,20 +16923,23 @@ function stableStringify(value, compact) {
16670
16923
  }
16671
16924
  function sortKeys(value) {
16672
16925
  if (Array.isArray(value)) return value.map(sortKeys);
16673
- if (!isRecord2(value)) return value;
16926
+ if (!isRecord3(value)) return value;
16674
16927
  return Object.fromEntries(
16675
16928
  Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])
16676
16929
  );
16677
16930
  }
16678
- function isRecord2(value) {
16931
+ function isRecord3(value) {
16679
16932
  return typeof value === "object" && value !== null && !Array.isArray(value);
16680
16933
  }
16681
16934
  function isUploadBatch(value) {
16682
- return isRecord2(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
16935
+ return isRecord3(value) && Array.isArray(value.successes) && Array.isArray(value.failures);
16683
16936
  }
16684
16937
 
16685
16938
  // src/schema.ts
16686
16939
  var COMMAND_DATA_SCHEMAS = {
16940
+ "auth login": authLoginDataSchema,
16941
+ "auth logout": authLogoutDataSchema,
16942
+ "auth import-macos": authImportDataSchema,
16687
16943
  "auth status": authStatusDataSchema,
16688
16944
  doctor: doctorDataSchema,
16689
16945
  upload: uploadBatchDataSchema,
@@ -16814,8 +17070,16 @@ async function runCli(deps = {}) {
16814
17070
  renderSuccess("version", { version: CLI_VERSION }, render2);
16815
17071
  return 0;
16816
17072
  }
16817
- const auth = await resolveAuthContext({ origin: parsed.options.origin, env: deps.env });
16818
- const client = new RecappiApiClient(auth, { fetchImpl: deps.fetchImpl, sleep: deps.sleep });
17073
+ const auth = await resolveAuthContext({
17074
+ origin: parsed.options.origin,
17075
+ env: deps.env,
17076
+ homeDir: deps.homeDir
17077
+ });
17078
+ const client = new RecappiApiClient(auth, {
17079
+ fetchImpl: deps.fetchImpl,
17080
+ sleep: deps.sleep,
17081
+ env: deps.env
17082
+ });
16819
17083
  if (parsed.kind === "dashboard") {
16820
17084
  const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
16821
17085
  await runDashboard2({
@@ -16830,6 +17094,44 @@ async function runCli(deps = {}) {
16830
17094
  renderSuccess("auth status", data, render2);
16831
17095
  return data.loggedIn ? 0 : 3;
16832
17096
  }
17097
+ if (parsed.kind === "auth-login") {
17098
+ const data = await loginWithDeviceCode({
17099
+ origin: auth.origin,
17100
+ homeDir: deps.homeDir,
17101
+ noOpen: parsed.noOpen,
17102
+ onPrompt: (message) => stderr(message),
17103
+ deps: {
17104
+ fetchImpl: deps.fetchImpl,
17105
+ openUrl: deps.openUrl,
17106
+ sleep: deps.sleep
17107
+ }
17108
+ });
17109
+ renderSuccess("auth login", data, render2);
17110
+ return 0;
17111
+ }
17112
+ if (parsed.kind === "auth-logout") {
17113
+ const cleared = await clearAuthConfig(deps.homeDir ?? os3.homedir());
17114
+ renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render2);
17115
+ return 0;
17116
+ }
17117
+ if (parsed.kind === "auth-import-macos") {
17118
+ const keychain = await inspectMacOSAppKeychain({ env: deps.env });
17119
+ if (!keychain.token) {
17120
+ throw cliError("auth.not_logged_in", keychain.message, {
17121
+ hint: keychain.hint ?? "Run recappi auth login instead."
17122
+ });
17123
+ }
17124
+ await saveAuthConfig(deps.homeDir ?? os3.homedir(), {
17125
+ origin: auth.origin,
17126
+ token: keychain.token
17127
+ });
17128
+ renderSuccess(
17129
+ "auth import-macos",
17130
+ { imported: true, origin: auth.origin, source: "macos-keychain" },
17131
+ render2
17132
+ );
17133
+ return 0;
17134
+ }
16833
17135
  if (parsed.kind === "doctor") {
16834
17136
  const data = await client.doctor();
16835
17137
  renderSuccess("doctor", data, render2);
@@ -17027,6 +17329,34 @@ Agent mode:
17027
17329
  addCommonOptions(program);
17028
17330
  const auth = program.command("auth").description("Authentication commands");
17029
17331
  addCommonOptions(auth);
17332
+ const authLogin = auth.command("login").description("Sign in to Recappi Cloud with a device code").option("--no-open", "print the device URL without opening a browser");
17333
+ addCommonOptions(authLogin);
17334
+ authLogin.action((opts, command) => {
17335
+ onSelect({
17336
+ kind: "auth-login",
17337
+ options: collectGlobalOptions(command),
17338
+ commandName: "auth login",
17339
+ ...opts.noOpen === true ? { noOpen: true } : {}
17340
+ });
17341
+ });
17342
+ const authLogout = auth.command("logout").description("Remove the Recappi CLI sign-in token");
17343
+ addCommonOptions(authLogout);
17344
+ authLogout.action((_options, command) => {
17345
+ onSelect({
17346
+ kind: "auth-logout",
17347
+ options: collectGlobalOptions(command),
17348
+ commandName: "auth logout"
17349
+ });
17350
+ });
17351
+ const authImportMacOS = auth.command("import-macos").description("Copy the Recappi Mini macOS app session into CLI config");
17352
+ addCommonOptions(authImportMacOS);
17353
+ authImportMacOS.action((_options, command) => {
17354
+ onSelect({
17355
+ kind: "auth-import-macos",
17356
+ options: collectGlobalOptions(command),
17357
+ commandName: "auth import-macos"
17358
+ });
17359
+ });
17030
17360
  const authStatus = auth.command("status").description("Show Recappi Cloud sign-in status");
17031
17361
  addCommonOptions(authStatus);
17032
17362
  authStatus.action((_options, command) => {