autho 3.0.4 → 3.0.6

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/autho.js +775 -146
  2. package/package.json +1 -1
package/dist/autho.js CHANGED
@@ -590,7 +590,7 @@ var package_default;
590
590
  var init_package = __esm(() => {
591
591
  package_default = {
592
592
  name: "autho",
593
- version: "3.0.4",
593
+ version: "3.0.6",
594
594
  description: "Local-first secret manager for humans and coding agents, rebuilt on Bun",
595
595
  type: "module",
596
596
  bin: {
@@ -4105,7 +4105,7 @@ var init_palette = __esm(() => {
4105
4105
  // apps/cli/src/tui/settings.tsx
4106
4106
  import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
4107
4107
  import { homedir as homedir2 } from "os";
4108
- import { dirname as dirname3, join as join3, resolve as resolve4 } from "path";
4108
+ import { dirname as dirname3, join as join3, resolve as resolve5 } from "path";
4109
4109
  import { useKeyboard as useKeyboard4, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
4110
4110
  import { useCallback, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef5, useState as useState4 } from "react";
4111
4111
  import { jsx as jsx4, jsxs as jsxs4, Fragment } from "@opentui/react/jsx-runtime";
@@ -4372,7 +4372,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
4372
4372
  return;
4373
4373
  case "vaultDir": {
4374
4374
  const config = { ...loadConfig() };
4375
- const target = first.trim() ? resolve4(first.trim()).replace(/\\/g, "/") : null;
4375
+ const target = first.trim() ? resolve5(first.trim()).replace(/\\/g, "/") : null;
4376
4376
  if (target)
4377
4377
  config.vaultDir = target;
4378
4378
  else
@@ -4440,7 +4440,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
4440
4440
  registerPasskey(first.trim());
4441
4441
  return;
4442
4442
  case "recoveryPath": {
4443
- const target = resolve4(first.trim() || join3(homedir2(), "autho-recovery.txt"));
4443
+ const target = resolve5(first.trim() || join3(homedir2(), "autho-recovery.txt"));
4444
4444
  if (existsSync5(target)) {
4445
4445
  setError("That file already exists \u2014 pick another name");
4446
4446
  return;
@@ -5305,24 +5305,203 @@ var init_unlock = __esm(() => {
5305
5305
  init_theme();
5306
5306
  });
5307
5307
 
5308
- // apps/cli/src/tui/vault.tsx
5308
+ // apps/cli/src/tui/links.tsx
5309
5309
  import { useKeyboard as useKeyboard6, useTerminalDimensions as useTerminalDimensions4 } from "@opentui/react";
5310
- import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo5, useState as useState6 } from "react";
5311
- import { jsx as jsx6, jsxs as jsxs6, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
5310
+ import { useMemo as useMemo5, useRef as useRef7, useState as useState6 } from "react";
5311
+ import { jsx as jsx6, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
5312
+ function LinkPicker({ primary, entries, linkedIds, onToggle, onClose }) {
5313
+ const { width, height } = useTerminalDimensions4();
5314
+ const [query, setQuery] = useState6("");
5315
+ const [index, setIndex] = useState6(0);
5316
+ const latestQuery = useRef7("");
5317
+ const candidates = useMemo5(() => entries.filter((e) => e.id !== primary.id), [entries, primary.id]);
5318
+ const search = (q) => fuzzyFilter(candidates, q, (e) => [e.name, e.username, e.url, typeLabel(e.type)]);
5319
+ const results = useMemo5(() => search(query), [candidates, query]);
5320
+ const current = Math.min(index, Math.max(0, results.length - 1));
5321
+ useKeyboard6((key) => {
5322
+ if (key.name === "escape") {
5323
+ key.preventDefault();
5324
+ onClose();
5325
+ return;
5326
+ }
5327
+ if (key.name === "up") {
5328
+ key.preventDefault();
5329
+ setIndex(Math.max(0, current - 1));
5330
+ return;
5331
+ }
5332
+ if (key.name === "down") {
5333
+ key.preventDefault();
5334
+ setIndex(Math.min(results.length - 1, current + 1));
5335
+ return;
5336
+ }
5337
+ if (key.name === "pageup") {
5338
+ key.preventDefault();
5339
+ setIndex(Math.max(0, current - 10));
5340
+ return;
5341
+ }
5342
+ if (key.name === "pagedown") {
5343
+ key.preventDefault();
5344
+ setIndex(Math.min(results.length - 1, current + 10));
5345
+ return;
5346
+ }
5347
+ if (isEnter(key)) {
5348
+ key.preventDefault();
5349
+ const fresh = latestQuery.current === query ? results : search(latestQuery.current);
5350
+ const entry = fresh[latestQuery.current === query ? current : 0];
5351
+ if (entry)
5352
+ onToggle(entry);
5353
+ }
5354
+ });
5355
+ const boxWidth = Math.min(72, Math.max(30, width - 4));
5356
+ const rows = Math.max(3, Math.min(14, height - 8));
5357
+ const offset = Math.max(0, Math.min(current - Math.floor(rows / 2), results.length - rows));
5358
+ const visible = results.slice(offset, offset + rows);
5359
+ const inner = boxWidth - 4;
5360
+ return /* @__PURE__ */ jsx6("box", {
5361
+ position: "absolute",
5362
+ top: 2,
5363
+ left: Math.max(0, Math.floor((width - boxWidth) / 2)),
5364
+ width: boxWidth,
5365
+ zIndex: 100,
5366
+ children: /* @__PURE__ */ jsxs6("box", {
5367
+ flexDirection: "column",
5368
+ border: true,
5369
+ borderStyle: "rounded",
5370
+ borderColor: theme.borderFocus,
5371
+ backgroundColor: theme.panel,
5372
+ paddingX: 1,
5373
+ title: ` Link to ${truncate(primary.name, 40)} `,
5374
+ titleColor: theme.accent,
5375
+ titleAlignment: "center",
5376
+ children: [
5377
+ /* @__PURE__ */ jsxs6("box", {
5378
+ flexDirection: "row",
5379
+ height: 1,
5380
+ gap: 1,
5381
+ children: [
5382
+ /* @__PURE__ */ jsx6("text", {
5383
+ fg: theme.accent,
5384
+ children: /* @__PURE__ */ jsx6("strong", {
5385
+ children: "\u203A"
5386
+ })
5387
+ }),
5388
+ /* @__PURE__ */ jsx6(TextField, {
5389
+ value: query,
5390
+ onChange: (v) => {
5391
+ latestQuery.current = v;
5392
+ setQuery(v);
5393
+ setIndex(0);
5394
+ },
5395
+ focused: true,
5396
+ placeholder: "search secrets to link"
5397
+ })
5398
+ ]
5399
+ }),
5400
+ /* @__PURE__ */ jsxs6("box", {
5401
+ flexDirection: "column",
5402
+ marginTop: 1,
5403
+ children: [
5404
+ visible.length === 0 ? /* @__PURE__ */ jsx6("text", {
5405
+ fg: theme.dim,
5406
+ children: "No matching secrets"
5407
+ }) : null,
5408
+ visible.map((entry, i) => {
5409
+ const on = offset + i === current;
5410
+ const linked = linkedIds.has(entry.id);
5411
+ const badge = typeBadge(entry.type);
5412
+ const name = truncate(entry.name, Math.max(4, inner - 6));
5413
+ return /* @__PURE__ */ jsx6("box", {
5414
+ flexDirection: "row",
5415
+ height: 1,
5416
+ backgroundColor: on ? theme.selection : undefined,
5417
+ onMouseDown: (event) => {
5418
+ event.preventDefault();
5419
+ onToggle(entry);
5420
+ },
5421
+ onMouseOver: () => setIndex(offset + i),
5422
+ children: /* @__PURE__ */ jsxs6("text", {
5423
+ children: [
5424
+ /* @__PURE__ */ jsx6("span", {
5425
+ fg: linked ? theme.success : theme.dim,
5426
+ children: linked ? " \u2713 " : " "
5427
+ }),
5428
+ /* @__PURE__ */ jsxs6("span", {
5429
+ fg: badge.color,
5430
+ children: [
5431
+ badge.glyph,
5432
+ " "
5433
+ ]
5434
+ }),
5435
+ on ? /* @__PURE__ */ jsx6("strong", {
5436
+ fg: theme.accent,
5437
+ children: name
5438
+ }) : /* @__PURE__ */ jsx6("span", {
5439
+ fg: theme.text,
5440
+ children: name
5441
+ })
5442
+ ]
5443
+ })
5444
+ }, entry.id);
5445
+ })
5446
+ ]
5447
+ }),
5448
+ /* @__PURE__ */ jsxs6("text", {
5449
+ marginTop: 1,
5450
+ children: [
5451
+ /* @__PURE__ */ jsx6(Chip, {
5452
+ label: "\u2191\u2193"
5453
+ }),
5454
+ /* @__PURE__ */ jsx6("span", {
5455
+ fg: theme.muted,
5456
+ children: " move "
5457
+ }),
5458
+ /* @__PURE__ */ jsx6(Chip, {
5459
+ label: "enter"
5460
+ }),
5461
+ /* @__PURE__ */ jsx6("span", {
5462
+ fg: theme.muted,
5463
+ children: " link/unlink "
5464
+ }),
5465
+ /* @__PURE__ */ jsx6(Chip, {
5466
+ label: "esc"
5467
+ }),
5468
+ /* @__PURE__ */ jsx6("span", {
5469
+ fg: theme.muted,
5470
+ children: " done"
5471
+ })
5472
+ ]
5473
+ })
5474
+ ]
5475
+ })
5476
+ });
5477
+ }
5478
+ var init_links = __esm(() => {
5479
+ init_components();
5480
+ init_theme();
5481
+ init_util();
5482
+ });
5483
+
5484
+ // apps/cli/src/tui/vault.tsx
5485
+ import { useKeyboard as useKeyboard7, useTerminalDimensions as useTerminalDimensions5 } from "@opentui/react";
5486
+ import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo6, useState as useState7 } from "react";
5487
+ import { jsx as jsx7, jsxs as jsxs7, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
5312
5488
  function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings, setHints, revealMs }) {
5313
5489
  const app = useApp();
5314
- const { width, height } = useTerminalDimensions4();
5490
+ const { width, height } = useTerminalDimensions5();
5315
5491
  const wide = width >= WIDE_MIN_COLUMNS;
5316
- const [entries, setEntries] = useState6([]);
5317
- const [query, setQuery] = useState6("");
5318
- const [pane, setPane] = useState6("list");
5319
- const [actionIndex, setActionIndex] = useState6(0);
5492
+ const [entries, setEntries] = useState7([]);
5493
+ const [query, setQuery] = useState7("");
5494
+ const [pane, setPane] = useState7("list");
5495
+ const [actionIndex, setActionIndex] = useState7(0);
5320
5496
  const searchFocused = pane === "search";
5321
- const [selectedId, setSelectedId] = useState6(initialSelectedId ?? null);
5322
- const [detailOpen, setDetailOpen] = useState6(false);
5323
- const [revealed, setRevealed] = useState6(null);
5324
- const [otp, setOtp] = useState6(null);
5325
- const [confirmDelete, setConfirmDelete] = useState6(false);
5497
+ const [selectedId, setSelectedId] = useState7(initialSelectedId ?? null);
5498
+ const [detailOpen, setDetailOpen] = useState7(false);
5499
+ const [revealed, setRevealed] = useState7(null);
5500
+ const [otp, setOtp] = useState7(null);
5501
+ const [confirmDelete, setConfirmDelete] = useState7(false);
5502
+ const [links, setLinks] = useState7(null);
5503
+ const [linkPickerOpen, setLinkPickerOpen] = useState7(false);
5504
+ const [linkedOtp, setLinkedOtp] = useState7(null);
5326
5505
  const reload = useCallback3(() => {
5327
5506
  try {
5328
5507
  setEntries(session.listSecretIndex());
@@ -5332,7 +5511,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5332
5511
  }
5333
5512
  }, [session, app]);
5334
5513
  useEffect6(reload, [reload]);
5335
- const filtered = useMemo5(() => fuzzyFilter(entries, query, (e) => [e.name, e.username, e.url, e.description, typeLabel(e.type)]), [entries, query]);
5514
+ const filtered = useMemo6(() => fuzzyFilter(entries, query, (e) => [e.name, e.username, e.url, e.description, typeLabel(e.type)]), [entries, query]);
5336
5515
  const selectedIndex = Math.max(0, filtered.findIndex((e) => e.id === selectedId));
5337
5516
  const selected = filtered[selectedIndex] ?? null;
5338
5517
  useEffect6(() => {
@@ -5343,10 +5522,25 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5343
5522
  useEffect6(() => {
5344
5523
  setRevealed((r) => r && r.id !== selected?.id ? null : r);
5345
5524
  setOtp((o) => o && o.id !== selected?.id ? null : o);
5525
+ setLinkedOtp(null);
5346
5526
  setConfirmDelete(false);
5347
5527
  setActionIndex(0);
5348
5528
  }, [selected?.id]);
5349
- const now = useNow(1000, Boolean(revealed || otp));
5529
+ const loadLinks = useCallback3((id) => {
5530
+ if (!id) {
5531
+ setLinks(null);
5532
+ return;
5533
+ }
5534
+ try {
5535
+ setLinks(session.getLinks(id));
5536
+ } catch (e) {
5537
+ app.toast(e instanceof Error ? e.message : String(e), "error");
5538
+ setLinks(null);
5539
+ }
5540
+ }, [session, app]);
5541
+ useEffect6(() => loadLinks(selected?.id), [loadLinks, selected?.id, selected?.linkCount]);
5542
+ const linkedOtpSecret = links?.links.find((l) => l.type === "otp") ?? null;
5543
+ const now = useNow(1000, Boolean(revealed || otp || linkedOtp));
5350
5544
  useEffect6(() => {
5351
5545
  if (revealed && now >= revealed.until) {
5352
5546
  setRevealed(null);
@@ -5360,22 +5554,56 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5360
5554
  return null;
5361
5555
  }
5362
5556
  }, [session, app]);
5363
- const generateOtp = useCallback3((entry, options) => {
5557
+ const makeOtp = useCallback3((id, options) => {
5364
5558
  try {
5365
- const result = session.generateOtp(entry.id, options);
5366
- const next = { id: entry.id, code: result.code, expiresAt: Date.parse(result.expiresAt) };
5367
- setOtp(next);
5368
- return next;
5559
+ const result = session.generateOtp(id, options);
5560
+ return { id, code: result.code, expiresAt: Date.parse(result.expiresAt) };
5369
5561
  } catch (e) {
5370
5562
  app.toast(e instanceof Error ? e.message : String(e), "error");
5371
5563
  return null;
5372
5564
  }
5373
5565
  }, [session, app]);
5566
+ const generateOtp = useCallback3((entry, options) => {
5567
+ const next = makeOtp(entry.id, options);
5568
+ if (next)
5569
+ setOtp(next);
5570
+ return next;
5571
+ }, [makeOtp]);
5374
5572
  useEffect6(() => {
5375
5573
  if (otp && selected && otp.id === selected.id && now >= otp.expiresAt) {
5376
5574
  generateOtp(selected, { audit: false });
5377
5575
  }
5378
5576
  }, [now, otp, selected, generateOtp]);
5577
+ useEffect6(() => {
5578
+ if (linkedOtp && linkedOtp.id === linkedOtpSecret?.id && now >= linkedOtp.expiresAt) {
5579
+ setLinkedOtp(makeOtp(linkedOtp.id, { audit: false }));
5580
+ }
5581
+ }, [now, linkedOtp, linkedOtpSecret?.id, makeOtp]);
5582
+ const copyLinkedOtp = useCallback3(() => {
5583
+ if (!linkedOtpSecret) {
5584
+ app.toast("No OTP linked to this secret", "info");
5585
+ return;
5586
+ }
5587
+ const code = makeOtp(linkedOtpSecret.id);
5588
+ if (!code)
5589
+ return;
5590
+ setLinkedOtp(code);
5591
+ app.copy("OTP code", code.code, { secret: true });
5592
+ }, [app, linkedOtpSecret, makeOtp]);
5593
+ const toggleLink = useCallback3((primary, target) => {
5594
+ const linked = links?.links.some((l) => l.id === target.id) ?? false;
5595
+ try {
5596
+ if (linked)
5597
+ session.unlinkSecrets(primary.id, target.id);
5598
+ else
5599
+ session.linkSecrets(primary.id, target.id);
5600
+ app.toast(linked ? `Unlinked ${target.name}` : `Linked ${target.name}`, "info");
5601
+ } catch (e) {
5602
+ app.toast(e instanceof Error ? e.message : String(e), "error");
5603
+ }
5604
+ reload();
5605
+ loadLinks(primary.id);
5606
+ }, [links, session, app, reload, loadLinks]);
5379
5607
  const copyPrimary = useCallback3((entry) => {
5380
5608
  if (!entry)
5381
5609
  return;
@@ -5418,10 +5646,12 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5418
5646
  setRevealed({ id: entry.id, value, until: Date.now() + revealMs });
5419
5647
  }
5420
5648
  }, [revealed, readValue, revealMs]);
5421
- const remove = useCallback3((entry) => {
5649
+ const remove = useCallback3((entry, withLinked = false) => {
5422
5650
  try {
5423
- session.removeSecret(entry.id);
5424
- app.toast(`Deleted "${entry.name}"`);
5651
+ const { removedLinked, keptLinked } = session.removeSecret(entry.id, { withLinked });
5652
+ const also = removedLinked.length ? ` and ${removedLinked.map((l) => l.name).join(", ")}` : "";
5653
+ const kept = keptLinked.length ? ` \xB7 kept ${keptLinked.map((l) => l.name).join(", ")} (linked elsewhere)` : "";
5654
+ app.toast(`Deleted "${entry.name}"${also}${kept}`);
5425
5655
  setConfirmDelete(false);
5426
5656
  setDetailOpen(false);
5427
5657
  setPane("list");
@@ -5451,6 +5681,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5451
5681
  ...selected.type !== "otp" ? [{ id: "reveal", hotkey: "s", label: revealed?.id === selected.id ? "hide" : "reveal", color: theme.revealed, run: () => toggleReveal(selected) }] : [],
5452
5682
  ...selected.url && selected.type !== "otp" ? [{ id: "open", hotkey: "o", label: "open", color: theme.info, run: () => open(selected) }] : [],
5453
5683
  { id: "edit", hotkey: "e", label: "edit", color: theme.accent, run: () => onEdit(selected.id) },
5684
+ { id: "link", hotkey: "L", label: "link", color: theme.info, run: () => setLinkPickerOpen(true) },
5454
5685
  { id: "delete", hotkey: "d", label: "delete", color: theme.danger, run: () => setConfirmDelete(true) }
5455
5686
  ] : [];
5456
5687
  const currentAction = Math.min(actionIndex, Math.max(0, actions.length - 1));
@@ -5472,13 +5703,15 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5472
5703
  while (PANES[index] === "actions" && !selected);
5473
5704
  focusPane(PANES[index]);
5474
5705
  };
5475
- useKeyboard6((key) => {
5476
- if (!app.active || key.ctrl || key.meta)
5706
+ useKeyboard7((key) => {
5707
+ if (!app.active || key.ctrl || key.meta || linkPickerOpen)
5477
5708
  return;
5478
5709
  if (confirmDelete) {
5479
5710
  key.preventDefault();
5480
5711
  if ((key.name === "y" || isEnter(key)) && selected)
5481
5712
  remove(selected);
5713
+ else if (key.name === "a" && selected && links?.links.length)
5714
+ remove(selected, true);
5482
5715
  else if (key.name === "n" || key.name === "escape")
5483
5716
  setConfirmDelete(false);
5484
5717
  return;
@@ -5498,6 +5731,10 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5498
5731
  }
5499
5732
  return;
5500
5733
  }
5734
+ if (key.name === "l" && key.shift && selected) {
5735
+ setLinkPickerOpen(true);
5736
+ return;
5737
+ }
5501
5738
  switch (key.name) {
5502
5739
  case "up":
5503
5740
  case "k":
@@ -5611,13 +5848,16 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5611
5848
  case "e":
5612
5849
  onEdit(selected.id);
5613
5850
  return;
5851
+ case "t":
5852
+ copyLinkedOtp();
5853
+ return;
5614
5854
  case "d":
5615
5855
  case "delete":
5616
5856
  setConfirmDelete(true);
5617
5857
  return;
5618
5858
  }
5619
5859
  });
5620
- const commands = useMemo5(() => {
5860
+ const commands = useMemo6(() => {
5621
5861
  const list = [
5622
5862
  { id: "new", title: "New secret", hint: "n", group: "Vault", run: onCreate },
5623
5863
  { id: "search", title: "Search secrets", hint: "/", group: "Vault", run: () => setPane("search") },
@@ -5625,11 +5865,26 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5625
5865
  ];
5626
5866
  if (selected) {
5627
5867
  const name = selected.name;
5628
- list.unshift({ id: "copy", title: `Copy ${selected.type === "otp" ? "OTP code" : selected.type === "note" ? "note" : "password"} \u2014 ${name}`, hint: "c / enter", group: "Selected", run: () => copyPrimary(selected) }, { id: "copy-user", title: `Copy username \u2014 ${name}`, hint: "u", group: "Selected", run: () => copyUsername(selected) }, { id: "reveal", title: `${revealed ? "Hide" : "Reveal"} value \u2014 ${name}`, hint: "s", group: "Selected", run: () => toggleReveal(selected) }, { id: "edit", title: `Edit \u2014 ${name}`, hint: "e", group: "Selected", run: () => onEdit(selected.id) }, { id: "delete", title: `Delete \u2014 ${name}`, hint: "d", group: "Selected", run: () => setConfirmDelete(true) });
5868
+ list.unshift({ id: "copy", title: `Copy ${selected.type === "otp" ? "OTP code" : selected.type === "note" ? "note" : "password"} \u2014 ${name}`, hint: "c / enter", group: "Selected", run: () => copyPrimary(selected) }, { id: "copy-user", title: `Copy username \u2014 ${name}`, hint: "u", group: "Selected", run: () => copyUsername(selected) }, { id: "reveal", title: `${revealed ? "Hide" : "Reveal"} value \u2014 ${name}`, hint: "s", group: "Selected", run: () => toggleReveal(selected) }, { id: "edit", title: `Edit \u2014 ${name}`, hint: "e", group: "Selected", run: () => onEdit(selected.id) }, { id: "link", title: `Link secrets \u2014 ${name}`, hint: "L", group: "Selected", run: () => setLinkPickerOpen(true) }, { id: "delete", title: `Delete \u2014 ${name}`, hint: "d", group: "Selected", run: () => setConfirmDelete(true) });
5869
+ if (linkedOtpSecret) {
5870
+ list.splice(1, 0, { id: "copy-linked-otp", title: `Copy ${linkedOtpSecret.name} code \u2014 ${name}`, hint: "t", group: "Selected", run: copyLinkedOtp });
5871
+ }
5629
5872
  if (selected.url) {
5630
5873
  list.splice(2, 0, { id: "copy-url", title: `Copy URL \u2014 ${name}`, hint: "w", group: "Selected", run: () => copyUrl(selected) }, { id: "open-url", title: `Open ${hostOf(selected.url)} in browser`, hint: selected.type === "otp" ? undefined : "o", group: "Selected", run: () => open(selected) });
5631
5874
  }
5632
5875
  }
5876
+ for (const link of [...links?.links ?? [], ...links?.linkedFrom ?? []]) {
5877
+ list.push({
5878
+ id: `goto-linked:${link.id}`,
5879
+ title: `Go to ${link.name}`,
5880
+ hint: typeLabel(link.type),
5881
+ group: "Linked",
5882
+ run: () => {
5883
+ setQuery("");
5884
+ setSelectedId(link.id);
5885
+ }
5886
+ });
5887
+ }
5633
5888
  for (const entry of entries) {
5634
5889
  list.push({
5635
5890
  id: `goto:${entry.id}`,
@@ -5645,7 +5900,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5645
5900
  });
5646
5901
  }
5647
5902
  return list;
5648
- }, [entries, selected, revealed, onCreate, onEdit, reload, copyPrimary, copyUsername, copyUrl, toggleReveal]);
5903
+ }, [entries, selected, revealed, links, linkedOtpSecret, onCreate, onEdit, reload, copyPrimary, copyUsername, copyUrl, toggleReveal, copyLinkedOtp]);
5649
5904
  useScreenContext(commands, selected ? {
5650
5905
  label: selected.type === "otp" ? "OTP code" : selected.type === "note" ? "Note" : "Password",
5651
5906
  read: () => selected.type === "otp" ? generateOtp(selected)?.code ?? null : readValue(selected),
@@ -5654,7 +5909,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5654
5909
  useEffect6(() => {
5655
5910
  let hints;
5656
5911
  if (confirmDelete)
5657
- hints = [["y", "delete"], ["n/esc", "cancel"]];
5912
+ hints = links?.links.length ? [["y", "delete"], ["a", "with linked"], ["n/esc", "cancel"]] : [["y", "delete"], ["n/esc", "cancel"]];
5658
5913
  else if (searchFocused)
5659
5914
  hints = [["type", "filter"], ["enter/\u2193", "results"], ["esc", "clear"]];
5660
5915
  else if (pane === "actions")
@@ -5664,11 +5919,11 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5664
5919
  else
5665
5920
  hints = [["\u2191\u2193", "move"], ["enter", "copy"], ["\u2192/tab", "actions"], ["/", "search"], ["n", "new"], ["q", "quit"]];
5666
5921
  setHints(hints);
5667
- }, [confirmDelete, searchFocused, pane, showDetail, setHints]);
5922
+ }, [confirmDelete, links, searchFocused, pane, showDetail, setHints]);
5668
5923
  const listWidth = wide ? Math.max(32, Math.floor(width * 0.38)) : width;
5669
5924
  const offset = Math.max(0, Math.min(selectedIndex - Math.floor(listHeight / 2), filtered.length - listHeight));
5670
5925
  const visible = filtered.slice(offset, offset + listHeight);
5671
- const list = /* @__PURE__ */ jsx6("box", {
5926
+ const list = /* @__PURE__ */ jsx7("box", {
5672
5927
  flexDirection: "column",
5673
5928
  width: wide ? listWidth : "100%",
5674
5929
  flexShrink: 0,
@@ -5678,9 +5933,9 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5678
5933
  title: ` ${filtered.length}/${entries.length} secrets `,
5679
5934
  titleColor: theme.muted,
5680
5935
  onMouseScroll: (event) => move(event.scroll?.direction === "up" ? -3 : 3),
5681
- children: filtered.length === 0 ? /* @__PURE__ */ jsx6("box", {
5936
+ children: filtered.length === 0 ? /* @__PURE__ */ jsx7("box", {
5682
5937
  padding: 1,
5683
- children: /* @__PURE__ */ jsx6("text", {
5938
+ children: /* @__PURE__ */ jsx7("text", {
5684
5939
  fg: theme.dim,
5685
5940
  children: entries.length === 0 ? "No secrets yet \u2014 press n to add one." : `No match for "${query}".`
5686
5941
  })
@@ -5690,7 +5945,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5690
5945
  const sub = entry.username ?? (entry.url ? hostOf(entry.url) : "");
5691
5946
  const room = listWidth - 6;
5692
5947
  const name = truncate(entry.name, Math.max(8, Math.min(room, Math.ceil(room * 0.55))));
5693
- return /* @__PURE__ */ jsx6("box", {
5948
+ return /* @__PURE__ */ jsx7("box", {
5694
5949
  flexDirection: "row",
5695
5950
  height: 1,
5696
5951
  paddingX: 1,
@@ -5704,66 +5959,75 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5704
5959
  setPane("actions");
5705
5960
  }
5706
5961
  },
5707
- children: /* @__PURE__ */ jsxs6("text", {
5962
+ children: /* @__PURE__ */ jsxs7("text", {
5708
5963
  children: [
5709
- /* @__PURE__ */ jsxs6("span", {
5964
+ /* @__PURE__ */ jsxs7("span", {
5710
5965
  fg: badge.color,
5711
5966
  children: [
5712
5967
  badge.glyph,
5713
5968
  " "
5714
5969
  ]
5715
5970
  }),
5716
- isSelected ? /* @__PURE__ */ jsx6("strong", {
5971
+ isSelected ? /* @__PURE__ */ jsx7("strong", {
5717
5972
  fg: theme.accent,
5718
5973
  children: name
5719
- }) : /* @__PURE__ */ jsx6("span", {
5974
+ }) : /* @__PURE__ */ jsx7("span", {
5720
5975
  fg: theme.text,
5721
5976
  children: name
5722
5977
  }),
5723
- /* @__PURE__ */ jsx6("span", {
5978
+ entry.linkCount ? /* @__PURE__ */ jsxs7("span", {
5979
+ fg: theme.muted,
5980
+ children: [
5981
+ " \xB7",
5982
+ entry.linkCount
5983
+ ]
5984
+ }) : null,
5985
+ /* @__PURE__ */ jsx7("span", {
5724
5986
  fg: theme.dim,
5725
- children: sub ? ` ${truncate(sub, Math.max(0, room - name.length - 2))}` : ""
5987
+ children: sub ? ` ${truncate(sub, Math.max(0, room - name.length - 2 - (entry.linkCount ? 3 : 0)))}` : ""
5726
5988
  })
5727
5989
  ]
5728
5990
  })
5729
5991
  }, entry.id);
5730
5992
  })
5731
5993
  });
5732
- const detail = selected ? /* @__PURE__ */ jsx6(Detail, {
5994
+ const detail = selected ? /* @__PURE__ */ jsx7(Detail, {
5733
5995
  entry: selected,
5734
5996
  revealed: revealed?.id === selected.id ? revealed : null,
5735
5997
  otp: otp?.id === selected.id ? otp : null,
5736
5998
  now,
5737
5999
  confirmDelete,
6000
+ links,
6001
+ linkedOtp,
5738
6002
  actions,
5739
6003
  focusedAction: pane === "actions" ? currentAction : null
5740
- }) : /* @__PURE__ */ jsx6("box", {
6004
+ }) : /* @__PURE__ */ jsx7("box", {
5741
6005
  flexGrow: 1,
5742
6006
  border: true,
5743
6007
  borderStyle: "rounded",
5744
6008
  borderColor: theme.border,
5745
6009
  padding: 1,
5746
- children: /* @__PURE__ */ jsx6("text", {
6010
+ children: /* @__PURE__ */ jsx7("text", {
5747
6011
  fg: theme.dim,
5748
6012
  children: "Nothing selected."
5749
6013
  })
5750
6014
  });
5751
- return /* @__PURE__ */ jsxs6("box", {
6015
+ return /* @__PURE__ */ jsxs7("box", {
5752
6016
  flexDirection: "column",
5753
6017
  flexGrow: 1,
5754
6018
  width: "100%",
5755
6019
  children: [
5756
- /* @__PURE__ */ jsxs6("box", {
6020
+ /* @__PURE__ */ jsxs7("box", {
5757
6021
  flexDirection: "row",
5758
6022
  paddingX: 1,
5759
6023
  height: 1,
5760
6024
  children: [
5761
- /* @__PURE__ */ jsx6("text", {
6025
+ /* @__PURE__ */ jsx7("text", {
5762
6026
  bg: searchFocused ? theme.accent : theme.chip,
5763
6027
  fg: searchFocused ? theme.onAccent : theme.muted,
5764
6028
  children: " / "
5765
6029
  }),
5766
- /* @__PURE__ */ jsx6(TextField, {
6030
+ /* @__PURE__ */ jsx7(TextField, {
5767
6031
  value: query,
5768
6032
  onChange: setQuery,
5769
6033
  focused: searchFocused && app.active,
@@ -5771,7 +6035,7 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5771
6035
  })
5772
6036
  ]
5773
6037
  }),
5774
- /* @__PURE__ */ jsxs6("box", {
6038
+ /* @__PURE__ */ jsxs7("box", {
5775
6039
  flexDirection: "row",
5776
6040
  flexGrow: 1,
5777
6041
  gap: 1,
@@ -5779,21 +6043,69 @@ function VaultScreen({ session, initialSelectedId, onCreate, onEdit, onSettings,
5779
6043
  wide || !detailOpen ? list : null,
5780
6044
  showDetail ? detail : null
5781
6045
  ]
5782
- })
6046
+ }),
6047
+ linkPickerOpen && selected ? /* @__PURE__ */ jsx7(LinkPicker, {
6048
+ primary: selected,
6049
+ entries,
6050
+ linkedIds: new Set(links?.links.map((l) => l.id) ?? []),
6051
+ onToggle: (target) => toggleLink(selected, target),
6052
+ onClose: () => setLinkPickerOpen(false)
6053
+ }) : null
6054
+ ]
6055
+ });
6056
+ }
6057
+ function LinkedRow({ link, otp, now }) {
6058
+ const badge = typeBadge(link.type);
6059
+ const seconds = otp ? Math.max(0, Math.ceil((otp.expiresAt - now) / 1000)) : 0;
6060
+ return /* @__PURE__ */ jsxs7("text", {
6061
+ children: [
6062
+ /* @__PURE__ */ jsxs7("span", {
6063
+ fg: badge.color,
6064
+ children: [
6065
+ " ",
6066
+ badge.glyph,
6067
+ " "
6068
+ ]
6069
+ }),
6070
+ /* @__PURE__ */ jsx7("span", {
6071
+ fg: theme.text,
6072
+ children: link.name
6073
+ }),
6074
+ link.type === "otp" ? otp ? /* @__PURE__ */ jsxs7(Fragment2, {
6075
+ children: [
6076
+ /* @__PURE__ */ jsx7("span", {
6077
+ fg: theme.success,
6078
+ children: /* @__PURE__ */ jsx7("strong", {
6079
+ children: ` ${otp.code.slice(0, 3)} ${otp.code.slice(3)}`
6080
+ })
6081
+ }),
6082
+ /* @__PURE__ */ jsxs7("span", {
6083
+ fg: seconds <= 5 ? theme.danger : theme.muted,
6084
+ children: [
6085
+ " ",
6086
+ seconds,
6087
+ "s"
6088
+ ]
6089
+ })
6090
+ ]
6091
+ }) : /* @__PURE__ */ jsx7("span", {
6092
+ fg: theme.dim,
6093
+ children: " press t for the code"
6094
+ }) : null
5783
6095
  ]
5784
6096
  });
5785
6097
  }
5786
6098
  function valueName(type) {
5787
6099
  return type === "note" ? "Note" : type === "otp" ? "Secret key" : "Password";
5788
6100
  }
5789
- function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedAction }) {
6101
+ function Detail({ entry, revealed, otp, now, confirmDelete, links, linkedOtp, actions, focusedAction }) {
5790
6102
  const badge = typeBadge(entry.type);
5791
6103
  const valueLabel = valueName(entry.type);
5792
6104
  const revealSeconds = revealed ? Math.max(0, Math.ceil((revealed.until - now) / 1000)) : 0;
5793
6105
  const otpSeconds = otp ? Math.max(0, Math.ceil((otp.expiresAt - now) / 1000)) : 0;
5794
6106
  const otpBar = 10;
5795
6107
  const otpFilled = Math.round(otpSeconds / 30 * otpBar);
5796
- return /* @__PURE__ */ jsxs6("box", {
6108
+ return /* @__PURE__ */ jsxs7("box", {
5797
6109
  flexDirection: "column",
5798
6110
  flexGrow: 1,
5799
6111
  border: true,
@@ -5804,51 +6116,51 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
5804
6116
  title: ` ${typeLabel(entry.type)} `,
5805
6117
  titleColor: badge.color,
5806
6118
  children: [
5807
- /* @__PURE__ */ jsxs6("text", {
6119
+ /* @__PURE__ */ jsxs7("text", {
5808
6120
  children: [
5809
- /* @__PURE__ */ jsxs6("span", {
6121
+ /* @__PURE__ */ jsxs7("span", {
5810
6122
  fg: badge.color,
5811
6123
  children: [
5812
6124
  badge.glyph,
5813
6125
  " "
5814
6126
  ]
5815
6127
  }),
5816
- /* @__PURE__ */ jsx6("strong", {
6128
+ /* @__PURE__ */ jsx7("strong", {
5817
6129
  fg: theme.accent,
5818
6130
  children: entry.name
5819
6131
  })
5820
6132
  ]
5821
6133
  }),
5822
- /* @__PURE__ */ jsxs6("box", {
6134
+ /* @__PURE__ */ jsxs7("box", {
5823
6135
  flexDirection: "column",
5824
6136
  children: [
5825
- entry.type !== "note" ? /* @__PURE__ */ jsx6(FieldRow, {
6137
+ entry.type !== "note" ? /* @__PURE__ */ jsx7(FieldRow, {
5826
6138
  label: "Username",
5827
6139
  value: entry.username ?? "\u2014",
5828
6140
  color: entry.username ? theme.text : theme.dim
5829
6141
  }) : null,
5830
- /* @__PURE__ */ jsx6(FieldRow, {
6142
+ /* @__PURE__ */ jsx7(FieldRow, {
5831
6143
  label: valueLabel,
5832
6144
  value: revealed ? revealed.value : MASK,
5833
6145
  color: revealed ? theme.revealed : theme.dim
5834
6146
  }),
5835
- entry.url ? /* @__PURE__ */ jsx6(FieldRow, {
6147
+ entry.url ? /* @__PURE__ */ jsx7(FieldRow, {
5836
6148
  label: "URL",
5837
6149
  value: entry.url,
5838
6150
  color: theme.info
5839
6151
  }) : null,
5840
- entry.description ? /* @__PURE__ */ jsx6(FieldRow, {
6152
+ entry.description ? /* @__PURE__ */ jsx7(FieldRow, {
5841
6153
  label: "Notes",
5842
6154
  value: entry.description
5843
6155
  }) : null,
5844
- /* @__PURE__ */ jsx6(FieldRow, {
6156
+ /* @__PURE__ */ jsx7(FieldRow, {
5845
6157
  label: "Updated",
5846
6158
  value: entry.updatedAt.slice(0, 16).replace("T", " "),
5847
6159
  color: theme.dim
5848
6160
  })
5849
6161
  ]
5850
6162
  }),
5851
- entry.type === "otp" ? /* @__PURE__ */ jsx6("box", {
6163
+ entry.type === "otp" ? /* @__PURE__ */ jsx7("box", {
5852
6164
  flexDirection: "row",
5853
6165
  border: true,
5854
6166
  borderStyle: "rounded",
@@ -5856,26 +6168,26 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
5856
6168
  paddingX: 1,
5857
6169
  gap: 2,
5858
6170
  alignSelf: "flex-start",
5859
- children: otp ? /* @__PURE__ */ jsxs6(Fragment2, {
6171
+ children: otp ? /* @__PURE__ */ jsxs7(Fragment2, {
5860
6172
  children: [
5861
- /* @__PURE__ */ jsx6("text", {
6173
+ /* @__PURE__ */ jsx7("text", {
5862
6174
  fg: theme.success,
5863
6175
  selectable: true,
5864
- children: /* @__PURE__ */ jsx6("strong", {
6176
+ children: /* @__PURE__ */ jsx7("strong", {
5865
6177
  children: `${otp.code.slice(0, 3)} ${otp.code.slice(3)}`
5866
6178
  })
5867
6179
  }),
5868
- /* @__PURE__ */ jsxs6("text", {
6180
+ /* @__PURE__ */ jsxs7("text", {
5869
6181
  children: [
5870
- /* @__PURE__ */ jsx6("span", {
6182
+ /* @__PURE__ */ jsx7("span", {
5871
6183
  fg: otpSeconds <= 5 ? theme.danger : theme.success,
5872
6184
  children: "\u2588".repeat(otpFilled)
5873
6185
  }),
5874
- /* @__PURE__ */ jsx6("span", {
6186
+ /* @__PURE__ */ jsx7("span", {
5875
6187
  fg: theme.chip,
5876
6188
  children: "\u2588".repeat(otpBar - otpFilled)
5877
6189
  }),
5878
- /* @__PURE__ */ jsxs6("span", {
6190
+ /* @__PURE__ */ jsxs7("span", {
5879
6191
  fg: otpSeconds <= 5 ? theme.danger : theme.muted,
5880
6192
  children: [
5881
6193
  " ",
@@ -5886,12 +6198,38 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
5886
6198
  ]
5887
6199
  })
5888
6200
  ]
5889
- }) : /* @__PURE__ */ jsx6("text", {
6201
+ }) : /* @__PURE__ */ jsx7("text", {
5890
6202
  fg: theme.dim,
5891
6203
  children: "press o to show the code"
5892
6204
  })
5893
6205
  }) : null,
5894
- revealed ? /* @__PURE__ */ jsxs6("text", {
6206
+ links && (links.links.length > 0 || links.linkedFrom.length > 0) ? /* @__PURE__ */ jsxs7("box", {
6207
+ flexDirection: "column",
6208
+ children: [
6209
+ links.links.length > 0 ? /* @__PURE__ */ jsx7("text", {
6210
+ fg: theme.muted,
6211
+ children: "Linked"
6212
+ }) : null,
6213
+ links.links.map((link) => /* @__PURE__ */ jsx7(LinkedRow, {
6214
+ link,
6215
+ otp: linkedOtp?.id === link.id ? linkedOtp : null,
6216
+ now
6217
+ }, link.id)),
6218
+ links.linkedFrom.length > 0 ? /* @__PURE__ */ jsxs7("text", {
6219
+ children: [
6220
+ /* @__PURE__ */ jsx7("span", {
6221
+ fg: theme.muted,
6222
+ children: "Linked from "
6223
+ }),
6224
+ /* @__PURE__ */ jsx7("span", {
6225
+ fg: theme.text,
6226
+ children: links.linkedFrom.map((l) => l.name).join(", ")
6227
+ })
6228
+ ]
6229
+ }) : null
6230
+ ]
6231
+ }) : null,
6232
+ revealed ? /* @__PURE__ */ jsxs7("text", {
5895
6233
  fg: theme.dim,
5896
6234
  children: [
5897
6235
  "Visible for ",
@@ -5899,30 +6237,30 @@ function Detail({ entry, revealed, otp, now, confirmDelete, actions, focusedActi
5899
6237
  "s \u2014 drag to select, ctrl+c to copy the selection."
5900
6238
  ]
5901
6239
  }) : null,
5902
- confirmDelete ? /* @__PURE__ */ jsx6("box", {
6240
+ confirmDelete ? /* @__PURE__ */ jsx7("box", {
5903
6241
  backgroundColor: theme.dangerBg,
5904
6242
  paddingX: 1,
5905
- children: /* @__PURE__ */ jsxs6("text", {
6243
+ children: /* @__PURE__ */ jsxs7("text", {
5906
6244
  fg: theme.danger,
5907
6245
  children: [
5908
- /* @__PURE__ */ jsxs6("strong", {
6246
+ /* @__PURE__ */ jsxs7("strong", {
5909
6247
  children: [
5910
6248
  'Delete "',
5911
6249
  entry.name,
5912
6250
  '" permanently?'
5913
6251
  ]
5914
6252
  }),
5915
- /* @__PURE__ */ jsx6("span", {
6253
+ /* @__PURE__ */ jsx7("span", {
5916
6254
  fg: theme.muted,
5917
- children: " y = delete \xB7 n = cancel"
6255
+ children: links?.links.length ? ` y = only this \xB7 a = also its ${links.links.length} linked \xB7 n = cancel` : " y = delete \xB7 n = cancel"
5918
6256
  })
5919
6257
  ]
5920
6258
  })
5921
- }) : /* @__PURE__ */ jsx6("box", {
6259
+ }) : /* @__PURE__ */ jsx7("box", {
5922
6260
  flexDirection: "row",
5923
6261
  gap: 1,
5924
6262
  flexWrap: "wrap",
5925
- children: actions.map((action, i) => /* @__PURE__ */ jsx6(Button, {
6263
+ children: actions.map((action, i) => /* @__PURE__ */ jsx7(Button, {
5926
6264
  hotkey: action.hotkey,
5927
6265
  label: action.label,
5928
6266
  onPress: action.run,
@@ -5937,6 +6275,7 @@ var PANES, WIDE_MIN_COLUMNS = 96;
5937
6275
  var init_vault = __esm(() => {
5938
6276
  init_components();
5939
6277
  init_context();
6278
+ init_links();
5940
6279
  init_theme();
5941
6280
  init_util();
5942
6281
  PANES = ["search", "list", "actions"];
@@ -5945,29 +6284,29 @@ var init_vault = __esm(() => {
5945
6284
  // apps/cli/src/tui.tsx
5946
6285
  import { basename as basename3 } from "path";
5947
6286
  import { createCliRenderer } from "@opentui/core";
5948
- import { createRoot, useKeyboard as useKeyboard7, useRenderer, useSelectionHandler } from "@opentui/react";
5949
- import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo6, useRef as useRef7, useState as useState7 } from "react";
5950
- import { jsx as jsx7, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
6287
+ import { createRoot, useKeyboard as useKeyboard8, useRenderer, useSelectionHandler } from "@opentui/react";
6288
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo7, useRef as useRef8, useState as useState8 } from "react";
6289
+ import { jsx as jsx8, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
5951
6290
  function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs: initialClipboardClearMs, idleLockMs: initialIdleLockMs, onQuit, sessionSink, passkeyRunner }) {
5952
6291
  const renderer = useRenderer();
5953
- const [timings, setTimingsState] = useState7({ revealMs: initialRevealMs, clipboardClearMs: initialClipboardClearMs, idleLockMs: initialIdleLockMs });
6292
+ const [timings, setTimingsState] = useState8({ revealMs: initialRevealMs, clipboardClearMs: initialClipboardClearMs, idleLockMs: initialIdleLockMs });
5954
6293
  const { revealMs, clipboardClearMs, idleLockMs } = timings;
5955
6294
  const setTimings = useCallback4((next) => setTimingsState((prev) => ({ ...prev, ...next })), []);
5956
- const [route, setRoute] = useState7({ name: "unlock" });
5957
- const [session, setSession] = useState7(null);
5958
- const [toastState, setToastState] = useState7(null);
5959
- const [hints, setHints] = useState7([]);
5960
- const [paletteOpen, setPaletteOpen] = useState7(false);
5961
- const commandsRef = useRef7([]);
5962
- const copyTargetRef = useRef7(null);
5963
- const sessionRef = useRef7(null);
6295
+ const [route, setRoute] = useState8({ name: "unlock" });
6296
+ const [session, setSession] = useState8(null);
6297
+ const [toastState, setToastState] = useState8(null);
6298
+ const [hints, setHints] = useState8([]);
6299
+ const [paletteOpen, setPaletteOpen] = useState8(false);
6300
+ const commandsRef = useRef8([]);
6301
+ const copyTargetRef = useRef8(null);
6302
+ const sessionRef = useRef8(null);
5964
6303
  sessionRef.current = session;
5965
6304
  if (sessionSink)
5966
6305
  sessionSink.current = session;
5967
- const lastCtrlC = useRef7(0);
5968
- const lastActivity = useRef7(Date.now());
5969
- const toastTimer = useRef7(null);
5970
- const quitting = useRef7(false);
6306
+ const lastCtrlC = useRef8(0);
6307
+ const lastActivity = useRef8(Date.now());
6308
+ const toastTimer = useRef8(null);
6309
+ const quitting = useRef8(false);
5971
6310
  const toast = useCallback4((message, kind = "success") => {
5972
6311
  if (toastTimer.current)
5973
6312
  clearTimeout(toastTimer.current);
@@ -6001,7 +6340,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6001
6340
  onQuit();
6002
6341
  }, [onQuit]);
6003
6342
  const openPalette = useCallback4(() => setPaletteOpen(true), []);
6004
- const api = useMemo6(() => ({
6343
+ const api = useMemo7(() => ({
6005
6344
  active: !paletteOpen,
6006
6345
  toast,
6007
6346
  copy,
@@ -6039,7 +6378,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6039
6378
  }
6040
6379
  toast("Press ctrl+c again to quit", "info");
6041
6380
  }, [renderer, clipboard, clipboardClearMs, quit, toast]);
6042
- useKeyboard7((key) => {
6381
+ useKeyboard8((key) => {
6043
6382
  lastActivity.current = Date.now();
6044
6383
  if (!key.ctrl || key.meta)
6045
6384
  return;
@@ -6105,7 +6444,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6105
6444
  const vaultName = basename3(vaultPath);
6106
6445
  let screen;
6107
6446
  if (route.name === "unlock" || !session) {
6108
- screen = /* @__PURE__ */ jsx7(UnlockScreen, {
6447
+ screen = /* @__PURE__ */ jsx8(UnlockScreen, {
6109
6448
  vaultPath,
6110
6449
  onUnlock,
6111
6450
  setHints,
@@ -6113,7 +6452,7 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6113
6452
  passkeyRunner
6114
6453
  }, `unlock:${route.name === "unlock" ? route.notice ?? "" : ""}`);
6115
6454
  } else if (route.name === "form") {
6116
- screen = /* @__PURE__ */ jsx7(SecretForm, {
6455
+ screen = /* @__PURE__ */ jsx8(SecretForm, {
6117
6456
  session,
6118
6457
  editing: route.editing,
6119
6458
  setHints,
@@ -6125,14 +6464,14 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6125
6464
  }
6126
6465
  });
6127
6466
  } else if (route.name === "settings") {
6128
- screen = /* @__PURE__ */ jsx7(SettingsScreen, {
6467
+ screen = /* @__PURE__ */ jsx8(SettingsScreen, {
6129
6468
  vaultPath,
6130
6469
  setHints,
6131
6470
  onBack: goVault,
6132
6471
  passkeyRunner
6133
6472
  });
6134
6473
  } else {
6135
- screen = /* @__PURE__ */ jsx7(VaultScreen, {
6474
+ screen = /* @__PURE__ */ jsx8(VaultScreen, {
6136
6475
  session,
6137
6476
  initialSelectedId: route.focusId,
6138
6477
  onCreate: () => setRoute({ name: "form", editing: null }),
@@ -6142,9 +6481,9 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6142
6481
  revealMs
6143
6482
  }, route.focusId ?? "vault");
6144
6483
  }
6145
- return /* @__PURE__ */ jsx7(AppContext.Provider, {
6484
+ return /* @__PURE__ */ jsx8(AppContext.Provider, {
6146
6485
  value: api,
6147
- children: /* @__PURE__ */ jsxs7("box", {
6486
+ children: /* @__PURE__ */ jsxs8("box", {
6148
6487
  flexDirection: "column",
6149
6488
  width: "100%",
6150
6489
  height: "100%",
@@ -6155,15 +6494,15 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6155
6494
  lastActivity.current = Date.now();
6156
6495
  },
6157
6496
  children: [
6158
- /* @__PURE__ */ jsx7(Header, {
6497
+ /* @__PURE__ */ jsx8(Header, {
6159
6498
  title,
6160
- right: /* @__PURE__ */ jsxs7("text", {
6499
+ right: /* @__PURE__ */ jsxs8("text", {
6161
6500
  fg: theme.dim,
6162
6501
  children: [
6163
- session ? /* @__PURE__ */ jsx7("span", {
6502
+ session ? /* @__PURE__ */ jsx8("span", {
6164
6503
  fg: theme.success,
6165
6504
  children: "\u25CF unlocked"
6166
- }) : /* @__PURE__ */ jsx7("span", {
6505
+ }) : /* @__PURE__ */ jsx8("span", {
6167
6506
  fg: theme.muted,
6168
6507
  children: "\u25CB locked"
6169
6508
  }),
@@ -6172,17 +6511,17 @@ function App({ vaultPath, clipboard, revealMs: initialRevealMs, clipboardClearMs
6172
6511
  ]
6173
6512
  })
6174
6513
  }),
6175
- /* @__PURE__ */ jsx7("box", {
6514
+ /* @__PURE__ */ jsx8("box", {
6176
6515
  flexDirection: "column",
6177
6516
  flexGrow: 1,
6178
6517
  children: screen
6179
6518
  }),
6180
- /* @__PURE__ */ jsx7(StatusBar, {
6519
+ /* @__PURE__ */ jsx8(StatusBar, {
6181
6520
  hints,
6182
6521
  pinned: globalHints,
6183
6522
  toast: toastState
6184
6523
  }),
6185
- paletteOpen ? /* @__PURE__ */ jsx7(CommandPalette, {
6524
+ paletteOpen ? /* @__PURE__ */ jsx8(CommandPalette, {
6186
6525
  commands: [...commandsRef.current, ...globalCommands],
6187
6526
  onClose: () => setPaletteOpen(false)
6188
6527
  }) : null
@@ -6210,7 +6549,7 @@ async function runTui(vaultPath) {
6210
6549
  closeSessionAndRecordSeal(vault, sessionSink.current).catch(() => {});
6211
6550
  sessionSink.current = null;
6212
6551
  });
6213
- createRoot(renderer).render(/* @__PURE__ */ jsx7(App, {
6552
+ createRoot(renderer).render(/* @__PURE__ */ jsx8(App, {
6214
6553
  vaultPath: vault,
6215
6554
  clipboard,
6216
6555
  revealMs: numberPreference("revealSeconds") * 1000,
@@ -6238,10 +6577,9 @@ var init_tui = __esm(() => {
6238
6577
  init_clipboard();
6239
6578
  init_passkey();
6240
6579
  init_package();
6241
- import { spawn as spawn3 } from "child_process";
6242
6580
  import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
6243
6581
  import { createInterface as createInterface2 } from "readline/promises";
6244
- import { basename as basename4, isAbsolute, resolve as resolve5 } from "path";
6582
+ import { basename as basename4, isAbsolute, resolve as resolve6 } from "path";
6245
6583
 
6246
6584
  // apps/cli/src/password.ts
6247
6585
  import { createInterface } from "readline/promises";
@@ -6683,6 +7021,321 @@ async function daemonStop(options) {
6683
7021
  // apps/cli/src/index.ts
6684
7022
  init_src3();
6685
7023
  init_os_secrets();
7024
+
7025
+ // apps/web/src/server.ts
7026
+ init_src();
7027
+ init_src3();
7028
+ init_os_secrets();
7029
+ import { randomBytes as randomBytes6 } from "crypto";
7030
+ import { resolve as resolve4 } from "path";
7031
+ function json2(data, status = 200, headers) {
7032
+ return new Response(JSON.stringify(data, null, 2), {
7033
+ headers: {
7034
+ "content-type": "application/json; charset=utf-8",
7035
+ ...headers ?? {}
7036
+ },
7037
+ status
7038
+ });
7039
+ }
7040
+ function html(body) {
7041
+ return new Response(body, {
7042
+ headers: {
7043
+ "content-type": "text/html; charset=utf-8"
7044
+ }
7045
+ });
7046
+ }
7047
+ function parseCookies(request) {
7048
+ const cookieHeader = request.headers.get("cookie") ?? "";
7049
+ return Object.fromEntries(cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
7050
+ const index = part.indexOf("=");
7051
+ return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))];
7052
+ }));
7053
+ }
7054
+ async function openSession(vaultPath, state) {
7055
+ return new VaultSession(new AuthoDatabase(vaultPath), state.rootKey, {
7056
+ sealBound: state.sealBound,
7057
+ sealRecord: await loadSealRecord(vaultPath)
7058
+ });
7059
+ }
7060
+ function requireSession(request, sessions) {
7061
+ const cookies = parseCookies(request);
7062
+ const sessionId = cookies.autho_session;
7063
+ if (!sessionId) {
7064
+ throw new Error("Missing web session");
7065
+ }
7066
+ const session = sessions.get(sessionId);
7067
+ if (!session) {
7068
+ throw new Error("Unknown web session");
7069
+ }
7070
+ if (Date.parse(session.expiresAt) <= Date.now()) {
7071
+ sessions.delete(sessionId);
7072
+ throw new Error("Expired web session");
7073
+ }
7074
+ return { ...session, id: sessionId };
7075
+ }
7076
+ async function readJson2(request) {
7077
+ return await request.json();
7078
+ }
7079
+ function page2() {
7080
+ return `<!doctype html>
7081
+ <html lang="en">
7082
+ <head>
7083
+ <meta charset="utf-8" />
7084
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7085
+ <title>Autho</title>
7086
+ <style>
7087
+ :root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; }
7088
+ body { margin: 0; background: #f5f1e8; color: #1f2937; }
7089
+ main { max-width: 760px; margin: 0 auto; padding: 32px 20px 64px; }
7090
+ section { background: #fffdf8; border: 1px solid #d6cbb8; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 8px 30px rgba(83,58,18,0.08); }
7091
+ h1 { margin-top: 0; font-size: 2rem; }
7092
+ input, select, button { font: inherit; padding: 10px 12px; border-radius: 10px; border: 1px solid #bca889; }
7093
+ button { background: #1f4d3a; color: white; border: none; cursor: pointer; }
7094
+ form { display: flex; gap: 8px; flex-wrap: wrap; }
7095
+ pre { background: #1f2937; color: #f9fafb; padding: 12px; border-radius: 12px; overflow: auto; }
7096
+ ul { padding-left: 18px; }
7097
+ </style>
7098
+ </head>
7099
+ <body>
7100
+ <main>
7101
+ <section>
7102
+ <h1>Autho Local Web</h1>
7103
+ <p>Local-only Bun web UI for vault unlock and secret browsing.</p>
7104
+ <form id="unlock-form">
7105
+ <input id="password" type="password" placeholder="Master password" required />
7106
+ <input id="totp" inputmode="numeric" autocomplete="one-time-code" placeholder="Authenticator code (if enabled)" />
7107
+ <button type="submit">Unlock</button>
7108
+ </form>
7109
+ </section>
7110
+ <section>
7111
+ <h2>Secrets</h2>
7112
+ <button id="refresh">Refresh</button>
7113
+ <ul id="secret-list"></ul>
7114
+ <pre id="output"></pre>
7115
+ </section>
7116
+ <section id="linked" hidden>
7117
+ <h2>Linked to <span id="linked-name"></span></h2>
7118
+ <ul id="linked-list"></ul>
7119
+ <p id="linked-from"></p>
7120
+ <form id="link-form">
7121
+ <input id="link-ref" placeholder="Secret name or id to link" required />
7122
+ <button type="submit">Link</button>
7123
+ </form>
7124
+ </section>
7125
+ </main>
7126
+ <script>
7127
+ const output = document.getElementById('output');
7128
+ const secretList = document.getElementById('secret-list');
7129
+ async function refresh() {
7130
+ const response = await fetch('/api/secrets');
7131
+ if (!response.ok) {
7132
+ output.textContent = await response.text();
7133
+ return;
7134
+ }
7135
+ const data = await response.json();
7136
+ secretList.innerHTML = '';
7137
+ data.data.forEach((secret) => {
7138
+ const item = document.createElement('li');
7139
+ const open = document.createElement('a');
7140
+ open.href = '#';
7141
+ open.textContent = secret.name + " (" + secret.type + ")" + (secret.linkCount ? " \xB7 " + secret.linkCount + " linked" : "");
7142
+ open.addEventListener('click', (event) => { event.preventDefault(); showSecret(secret.id); });
7143
+ item.appendChild(open);
7144
+ secretList.appendChild(item);
7145
+ });
7146
+ output.textContent = JSON.stringify(data, null, 2);
7147
+ }
7148
+ let current = null;
7149
+ async function showSecret(ref) {
7150
+ const response = await fetch('/api/secrets/' + encodeURIComponent(ref));
7151
+ if (!response.ok) {
7152
+ output.textContent = await response.text();
7153
+ return;
7154
+ }
7155
+ const secret = (await response.json()).data;
7156
+ current = secret;
7157
+ const { links, linkedFrom, ...rest } = secret;
7158
+ output.textContent = JSON.stringify(rest, null, 2);
7159
+ document.getElementById('linked').hidden = false;
7160
+ document.getElementById('linked-name').textContent = secret.name;
7161
+ const list = document.getElementById('linked-list');
7162
+ list.innerHTML = '';
7163
+ if (!links.length) {
7164
+ list.innerHTML = '<li>No linked secrets</li>';
7165
+ }
7166
+ links.forEach((linked) => {
7167
+ const item = document.createElement('li');
7168
+ const open = document.createElement('a');
7169
+ open.href = '#';
7170
+ open.textContent = linked.name + " (" + linked.type + ") ";
7171
+ open.addEventListener('click', (event) => { event.preventDefault(); showSecret(linked.id); });
7172
+ const unlink = document.createElement('button');
7173
+ unlink.textContent = 'Unlink';
7174
+ unlink.addEventListener('click', () => changeLink('DELETE', linked.id));
7175
+ item.append(open, unlink);
7176
+ list.appendChild(item);
7177
+ });
7178
+ document.getElementById('linked-from').textContent = linkedFrom.length
7179
+ ? 'Linked from: ' + linkedFrom.map((primary) => primary.name).join(', ')
7180
+ : '';
7181
+ }
7182
+ async function changeLink(method, secret) {
7183
+ const response = await fetch('/api/links', {
7184
+ method,
7185
+ headers: { 'content-type': 'application/json' },
7186
+ body: JSON.stringify({ primary: current.id, secret })
7187
+ });
7188
+ if (!response.ok) {
7189
+ output.textContent = await response.text();
7190
+ return;
7191
+ }
7192
+ await showSecret(current.id);
7193
+ await refresh();
7194
+ }
7195
+ document.getElementById('link-form').addEventListener('submit', async (event) => {
7196
+ event.preventDefault();
7197
+ const input = document.getElementById('link-ref');
7198
+ await changeLink('POST', input.value);
7199
+ input.value = '';
7200
+ });
7201
+ document.getElementById('unlock-form').addEventListener('submit', async (event) => {
7202
+ event.preventDefault();
7203
+ const response = await fetch('/api/session/unlock', {
7204
+ method: 'POST',
7205
+ headers: { 'content-type': 'application/json' },
7206
+ body: JSON.stringify({
7207
+ password: document.getElementById('password').value,
7208
+ totp: document.getElementById('totp').value || undefined
7209
+ })
7210
+ });
7211
+ output.textContent = await response.text();
7212
+ if (response.ok) {
7213
+ await refresh();
7214
+ }
7215
+ });
7216
+ document.getElementById('refresh').addEventListener('click', refresh);
7217
+ </script>
7218
+ </body>
7219
+ </html>`;
7220
+ }
7221
+ async function startWebServer(options = {}) {
7222
+ const vaultPath = resolve4(options.vaultPath ?? defaultVaultPath());
7223
+ const host = options.host ?? "127.0.0.1";
7224
+ const port = options.port ?? 8061;
7225
+ const sessions = new Map;
7226
+ const cleanup = () => {
7227
+ for (const [id, session] of sessions.entries()) {
7228
+ if (Date.parse(session.expiresAt) <= Date.now()) {
7229
+ sessions.delete(id);
7230
+ }
7231
+ }
7232
+ };
7233
+ const server = Bun.serve({
7234
+ fetch: async (request) => {
7235
+ cleanup();
7236
+ const url = new URL(request.url);
7237
+ try {
7238
+ if (request.method === "GET" && url.pathname === "/health") {
7239
+ return json2({ ok: true, running: true, vaultPath });
7240
+ }
7241
+ if (request.method === "GET" && url.pathname === "/") {
7242
+ return html(page2());
7243
+ }
7244
+ if (request.method === "POST" && url.pathname === "/api/session/unlock") {
7245
+ const body = await readJson2(request);
7246
+ const ttlSeconds = body.ttlSeconds ?? 3600;
7247
+ if (ttlSeconds <= 0 || ttlSeconds > 86400) {
7248
+ return json2({ error: "Session TTL must be between 1 and 86400 seconds" }, 400);
7249
+ }
7250
+ const { rootKey, sealBound } = VaultService.unlockRootKey(vaultPath, {
7251
+ password: body.password,
7252
+ sealRecord: await loadSealRecord(vaultPath),
7253
+ totp: body.totp
7254
+ });
7255
+ const sessionId = randomBytes6(16).toString("hex");
7256
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
7257
+ sessions.set(sessionId, { expiresAt, rootKey, sealBound });
7258
+ return json2({ expiresAt, sessionId }, 200, { "set-cookie": `autho_session=${sessionId}; HttpOnly; SameSite=Strict; Path=/; Secure` });
7259
+ }
7260
+ if (request.method === "POST" && url.pathname === "/api/session/lock") {
7261
+ const cookies = parseCookies(request);
7262
+ if (cookies.autho_session) {
7263
+ sessions.delete(cookies.autho_session);
7264
+ }
7265
+ return json2({ locked: true }, 200, { "set-cookie": "autho_session=; Max-Age=0; Path=/; Secure" });
7266
+ }
7267
+ if (url.pathname.startsWith("/api/")) {
7268
+ const webSession = requireSession(request, sessions);
7269
+ const session = await openSession(vaultPath, webSession);
7270
+ try {
7271
+ if (request.method === "GET" && url.pathname === "/api/status") {
7272
+ return json2({
7273
+ initialized: true,
7274
+ secretCount: session.listSecrets().length,
7275
+ unlocked: true,
7276
+ vaultPath
7277
+ });
7278
+ }
7279
+ if (request.method === "GET" && url.pathname === "/api/secrets") {
7280
+ return json2({ data: session.listSecretIndex() });
7281
+ }
7282
+ if (request.method === "GET" && url.pathname.startsWith("/api/secrets/")) {
7283
+ const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
7284
+ const { linkedFrom, links } = session.getLinks(ref);
7285
+ return json2({ data: { ...session.getSecret(ref), linkedFrom, links } });
7286
+ }
7287
+ if ((request.method === "POST" || request.method === "DELETE") && url.pathname === "/api/links") {
7288
+ const body = await readJson2(request);
7289
+ return json2({
7290
+ data: request.method === "POST" ? session.linkSecrets(body.primary, body.secret) : session.unlinkSecrets(body.primary, body.secret)
7291
+ });
7292
+ }
7293
+ if (request.method === "POST" && url.pathname === "/api/secrets") {
7294
+ const body = await readJson2(request);
7295
+ return json2({
7296
+ data: session.addSecret({
7297
+ metadata: Object.fromEntries(Object.entries({
7298
+ algorithm: body.algorithm,
7299
+ description: body.description,
7300
+ digits: body.digits,
7301
+ url: body.url
7302
+ }).filter(([, value]) => value !== undefined)),
7303
+ name: body.name,
7304
+ type: body.type,
7305
+ username: body.username,
7306
+ value: body.value
7307
+ })
7308
+ }, 201);
7309
+ }
7310
+ if (request.method === "DELETE" && url.pathname.startsWith("/api/secrets/")) {
7311
+ const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
7312
+ return json2({ data: session.removeSecret(ref, { withLinked: url.searchParams.get("withLinked") === "1" }) });
7313
+ }
7314
+ if (request.method === "GET" && url.pathname.startsWith("/api/otp/")) {
7315
+ const ref = decodeURIComponent(url.pathname.slice("/api/otp/".length));
7316
+ return json2({ data: session.generateOtp(ref) });
7317
+ }
7318
+ } finally {
7319
+ await closeSessionAndRecordSeal(vaultPath, session);
7320
+ }
7321
+ }
7322
+ } catch (error) {
7323
+ const message = error instanceof Error ? error.message : String(error);
7324
+ const status = /session/i.test(message) ? 401 : 400;
7325
+ return json2({ error: message }, status);
7326
+ }
7327
+ return json2({ error: "Not found" }, 404);
7328
+ },
7329
+ hostname: host,
7330
+ port
7331
+ });
7332
+ process.on("SIGINT", () => process.exit(0));
7333
+ process.on("SIGTERM", () => process.exit(0));
7334
+ console.log(`Autho web server listening on http://${host}:${server.port}`);
7335
+ await new Promise(() => {});
7336
+ }
7337
+
7338
+ // apps/cli/src/index.ts
6686
7339
  function parseArgs(argv) {
6687
7340
  const dashDashIndex = argv.indexOf("--");
6688
7341
  const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
@@ -6776,7 +7429,7 @@ function printSealChanges(changes, diffTrusted) {
6776
7429
  }
6777
7430
  }
6778
7431
  function absolutePath(path) {
6779
- return resolve5(path);
7432
+ return resolve6(path);
6780
7433
  }
6781
7434
  function buildSecretMetadata(args) {
6782
7435
  return Object.fromEntries(Object.entries({
@@ -7031,34 +7684,10 @@ async function runInitWizard(vaultPath, password, existingCreds) {
7031
7684
  console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
7032
7685
  }
7033
7686
  async function runWebServer(vaultPath, args) {
7034
- const commandArgs = [
7035
- "run",
7036
- absolutePath("./apps/web/src/index.ts"),
7037
- "serve",
7038
- "--vault",
7039
- absolutePath(vaultPath)
7040
- ];
7041
- const host = getString(args, "host");
7042
- const port = getString(args, "port") ?? String(numberPreference("webPort"));
7043
- if (host) {
7044
- commandArgs.push("--host", host);
7045
- }
7046
- if (port) {
7047
- commandArgs.push("--port", port);
7048
- }
7049
- await new Promise((resolvePromise, rejectPromise) => {
7050
- const child = spawn3(process.execPath, commandArgs, {
7051
- cwd: process.cwd(),
7052
- stdio: "inherit"
7053
- });
7054
- child.on("exit", (code) => {
7055
- if (code === 0) {
7056
- resolvePromise();
7057
- return;
7058
- }
7059
- rejectPromise(new Error(`Web server exited with code ${code ?? 1}`));
7060
- });
7061
- child.on("error", rejectPromise);
7687
+ await startWebServer({
7688
+ host: getString(args, "host"),
7689
+ port: Number(getString(args, "port") ?? numberPreference("webPort")),
7690
+ vaultPath: absolutePath(vaultPath)
7062
7691
  });
7063
7692
  }
7064
7693
  function help() {
@@ -7235,7 +7864,7 @@ async function main() {
7235
7864
  console.log(" Tip: Use a cloud-synced folder (e.g. Google Drive) to share the vault across machines.");
7236
7865
  const customDir = (await readLine(` Path (Enter to keep default): `)).trim();
7237
7866
  if (customDir && customDir !== currentDir) {
7238
- const resolved = resolve5(customDir).replace(/\\/g, "/");
7867
+ const resolved = resolve6(customDir).replace(/\\/g, "/");
7239
7868
  saveConfig({ ...config, vaultDir: resolved });
7240
7869
  effectiveVaultPath = resolved + "/vault.db";
7241
7870
  console.log(` \x1B[32m\u2713\x1B[0m Vault directory set to ${resolved}`);
@@ -7303,7 +7932,7 @@ async function main() {
7303
7932
  if (key === "autoLock") {
7304
7933
  updated.autoLock = value === "true";
7305
7934
  } else if (key === "vaultDir") {
7306
- updated.vaultDir = isAbsolute(value) ? value : resolve5(value);
7935
+ updated.vaultDir = isAbsolute(value) ? value : resolve6(value);
7307
7936
  } else if (key === "editor") {
7308
7937
  updated.editor = value;
7309
7938
  } else if (key === "autoLockTimeout") {