@askexenow/exe-os 0.9.30 → 0.9.32

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 (64) hide show
  1. package/dist/bin/backfill-conversations.js +135 -7
  2. package/dist/bin/backfill-responses.js +135 -7
  3. package/dist/bin/backfill-vectors.js +135 -7
  4. package/dist/bin/cleanup-stale-review-tasks.js +139 -11
  5. package/dist/bin/cli.js +812 -486
  6. package/dist/bin/exe-assign.js +135 -7
  7. package/dist/bin/exe-boot.js +422 -113
  8. package/dist/bin/exe-cloud.js +160 -9
  9. package/dist/bin/exe-dispatch.js +136 -8
  10. package/dist/bin/exe-doctor.js +255 -13
  11. package/dist/bin/exe-export-behaviors.js +136 -8
  12. package/dist/bin/exe-forget.js +136 -8
  13. package/dist/bin/exe-gateway.js +171 -24
  14. package/dist/bin/exe-heartbeat.js +141 -13
  15. package/dist/bin/exe-kill.js +140 -12
  16. package/dist/bin/exe-launch-agent.js +143 -15
  17. package/dist/bin/exe-link.js +357 -48
  18. package/dist/bin/exe-pending-messages.js +136 -8
  19. package/dist/bin/exe-pending-notifications.js +136 -8
  20. package/dist/bin/exe-pending-reviews.js +138 -10
  21. package/dist/bin/exe-review.js +136 -8
  22. package/dist/bin/exe-search.js +155 -20
  23. package/dist/bin/exe-session-cleanup.js +166 -38
  24. package/dist/bin/exe-start-codex.js +142 -14
  25. package/dist/bin/exe-start-opencode.js +140 -12
  26. package/dist/bin/exe-status.js +148 -20
  27. package/dist/bin/exe-team.js +136 -8
  28. package/dist/bin/git-sweep.js +138 -10
  29. package/dist/bin/graph-backfill.js +135 -7
  30. package/dist/bin/graph-export.js +136 -8
  31. package/dist/bin/intercom-check.js +153 -25
  32. package/dist/bin/scan-tasks.js +138 -10
  33. package/dist/bin/setup.js +447 -121
  34. package/dist/bin/shard-migrate.js +135 -7
  35. package/dist/gateway/index.js +151 -23
  36. package/dist/hooks/bug-report-worker.js +151 -23
  37. package/dist/hooks/codex-stop-task-finalizer.js +145 -17
  38. package/dist/hooks/commit-complete.js +138 -10
  39. package/dist/hooks/error-recall.js +159 -24
  40. package/dist/hooks/ingest.js +142 -14
  41. package/dist/hooks/instructions-loaded.js +136 -8
  42. package/dist/hooks/notification.js +136 -8
  43. package/dist/hooks/post-compact.js +136 -8
  44. package/dist/hooks/post-tool-combined.js +159 -24
  45. package/dist/hooks/pre-compact.js +136 -8
  46. package/dist/hooks/pre-tool-use.js +144 -16
  47. package/dist/hooks/prompt-submit.js +195 -55
  48. package/dist/hooks/session-end.js +141 -13
  49. package/dist/hooks/session-start.js +165 -30
  50. package/dist/hooks/stop.js +136 -8
  51. package/dist/hooks/subagent-stop.js +136 -8
  52. package/dist/hooks/summary-worker.js +374 -65
  53. package/dist/index.js +136 -8
  54. package/dist/lib/cloud-sync.js +355 -46
  55. package/dist/lib/consolidation.js +1 -0
  56. package/dist/lib/exe-daemon.js +469 -127
  57. package/dist/lib/hybrid-search.js +155 -20
  58. package/dist/lib/keychain.js +191 -7
  59. package/dist/lib/schedules.js +138 -10
  60. package/dist/lib/store.js +135 -7
  61. package/dist/mcp/server.js +706 -213
  62. package/dist/runtime/index.js +136 -8
  63. package/dist/tui/App.js +208 -31
  64. package/package.json +1 -1
@@ -1083,8 +1083,8 @@ function findPackageRoot() {
1083
1083
  function getAvailableMemoryGB() {
1084
1084
  if (process.platform === "darwin") {
1085
1085
  try {
1086
- const { execSync: execSync4 } = __require("child_process");
1087
- const vmstat = execSync4("vm_stat", { encoding: "utf8" });
1086
+ const { execSync: execSync5 } = __require("child_process");
1087
+ const vmstat = execSync5("vm_stat", { encoding: "utf8" });
1088
1088
  const pageSize = 16384;
1089
1089
  const pageSizeMatch = vmstat.match(/page size of (\d+) bytes/);
1090
1090
  const actualPageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : pageSize;
@@ -2768,6 +2768,7 @@ var init_database = __esm({
2768
2768
  // src/lib/keychain.ts
2769
2769
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
2770
2770
  import { existsSync as existsSync6 } from "fs";
2771
+ import { execSync as execSync2 } from "child_process";
2771
2772
  import path6 from "path";
2772
2773
  import os5 from "os";
2773
2774
  function getKeyDir() {
@@ -2776,6 +2777,59 @@ function getKeyDir() {
2776
2777
  function getKeyPath() {
2777
2778
  return path6.join(getKeyDir(), "master.key");
2778
2779
  }
2780
+ function macKeychainGet() {
2781
+ if (process.platform !== "darwin") return null;
2782
+ try {
2783
+ return execSync2(
2784
+ `security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
2785
+ { encoding: "utf-8", timeout: 5e3 }
2786
+ ).trim();
2787
+ } catch {
2788
+ return null;
2789
+ }
2790
+ }
2791
+ function macKeychainSet(value) {
2792
+ if (process.platform !== "darwin") return false;
2793
+ try {
2794
+ try {
2795
+ execSync2(
2796
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
2797
+ { timeout: 5e3 }
2798
+ );
2799
+ } catch {
2800
+ }
2801
+ execSync2(
2802
+ `security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
2803
+ { timeout: 5e3 }
2804
+ );
2805
+ return true;
2806
+ } catch {
2807
+ return false;
2808
+ }
2809
+ }
2810
+ function linuxSecretGet() {
2811
+ if (process.platform !== "linux") return null;
2812
+ try {
2813
+ return execSync2(
2814
+ `secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
2815
+ { encoding: "utf-8", timeout: 5e3 }
2816
+ ).trim();
2817
+ } catch {
2818
+ return null;
2819
+ }
2820
+ }
2821
+ function linuxSecretSet(value) {
2822
+ if (process.platform !== "linux") return false;
2823
+ try {
2824
+ execSync2(
2825
+ `echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
2826
+ { timeout: 5e3 }
2827
+ );
2828
+ return true;
2829
+ } catch {
2830
+ return false;
2831
+ }
2832
+ }
2779
2833
  async function tryKeytar() {
2780
2834
  try {
2781
2835
  return await import("keytar");
@@ -2783,13 +2837,63 @@ async function tryKeytar() {
2783
2837
  return null;
2784
2838
  }
2785
2839
  }
2840
+ function deriveMachineKey() {
2841
+ try {
2842
+ const crypto3 = __require("crypto");
2843
+ const material = [
2844
+ os5.hostname(),
2845
+ os5.userInfo().username,
2846
+ os5.arch(),
2847
+ os5.platform(),
2848
+ // Machine ID on Linux (stable across reboots)
2849
+ process.platform === "linux" ? readMachineId() : ""
2850
+ ].join("|");
2851
+ return crypto3.createHash("sha256").update(material).digest();
2852
+ } catch {
2853
+ return null;
2854
+ }
2855
+ }
2856
+ function readMachineId() {
2857
+ try {
2858
+ const { readFileSync: readFileSync6 } = __require("fs");
2859
+ return readFileSync6("/etc/machine-id", "utf-8").trim();
2860
+ } catch {
2861
+ return "";
2862
+ }
2863
+ }
2864
+ function decryptWithMachineKey(encrypted, machineKey) {
2865
+ if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
2866
+ try {
2867
+ const crypto3 = __require("crypto");
2868
+ const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
2869
+ if (parts.length !== 3) return null;
2870
+ const [ivB64, tagB64, cipherB64] = parts;
2871
+ const iv = Buffer.from(ivB64, "base64");
2872
+ const authTag = Buffer.from(tagB64, "base64");
2873
+ const decipher = crypto3.createDecipheriv("aes-256-gcm", machineKey, iv);
2874
+ decipher.setAuthTag(authTag);
2875
+ let decrypted = decipher.update(cipherB64, "base64", "utf-8");
2876
+ decrypted += decipher.final("utf-8");
2877
+ return decrypted;
2878
+ } catch {
2879
+ return null;
2880
+ }
2881
+ }
2786
2882
  async function getMasterKey() {
2883
+ const nativeValue = macKeychainGet() ?? linuxSecretGet();
2884
+ if (nativeValue) {
2885
+ return Buffer.from(nativeValue, "base64");
2886
+ }
2787
2887
  const keytar = await tryKeytar();
2788
2888
  if (keytar) {
2789
2889
  try {
2790
- const stored = await keytar.getPassword(SERVICE, ACCOUNT);
2791
- if (stored) {
2792
- return Buffer.from(stored, "base64");
2890
+ const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
2891
+ if (keytarValue) {
2892
+ const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
2893
+ if (migrated) {
2894
+ process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
2895
+ }
2896
+ return Buffer.from(keytarValue, "base64");
2793
2897
  }
2794
2898
  } catch {
2795
2899
  }
@@ -2803,8 +2907,31 @@ async function getMasterKey() {
2803
2907
  return null;
2804
2908
  }
2805
2909
  try {
2806
- const content = await readFile3(keyPath, "utf-8");
2807
- return Buffer.from(content.trim(), "base64");
2910
+ const content = (await readFile3(keyPath, "utf-8")).trim();
2911
+ let b64Value;
2912
+ if (content.startsWith(ENCRYPTED_PREFIX)) {
2913
+ const machineKey = deriveMachineKey();
2914
+ if (!machineKey) {
2915
+ process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
2916
+ return null;
2917
+ }
2918
+ const decrypted = decryptWithMachineKey(content, machineKey);
2919
+ if (!decrypted) {
2920
+ process.stderr.write(
2921
+ "[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
2922
+ );
2923
+ return null;
2924
+ }
2925
+ b64Value = decrypted;
2926
+ } else {
2927
+ b64Value = content;
2928
+ }
2929
+ const key = Buffer.from(b64Value, "base64");
2930
+ const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
2931
+ if (migrated) {
2932
+ process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
2933
+ }
2934
+ return key;
2808
2935
  } catch (err) {
2809
2936
  process.stderr.write(
2810
2937
  `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
@@ -2813,12 +2940,13 @@ async function getMasterKey() {
2813
2940
  return null;
2814
2941
  }
2815
2942
  }
2816
- var SERVICE, ACCOUNT;
2943
+ var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
2817
2944
  var init_keychain = __esm({
2818
2945
  "src/lib/keychain.ts"() {
2819
2946
  "use strict";
2820
2947
  SERVICE = "exe-mem";
2821
2948
  ACCOUNT = "master-key";
2949
+ ENCRYPTED_PREFIX = "enc:";
2822
2950
  }
2823
2951
  });
2824
2952
 
@@ -4341,7 +4469,7 @@ __export(project_name_exports, {
4341
4469
  _resetCache: () => _resetCache,
4342
4470
  getProjectName: () => getProjectName
4343
4471
  });
4344
- import { execSync as execSync2 } from "child_process";
4472
+ import { execSync as execSync3 } from "child_process";
4345
4473
  import path9 from "path";
4346
4474
  function getProjectName(cwd) {
4347
4475
  const dir = cwd ?? process.cwd();
@@ -4349,7 +4477,7 @@ function getProjectName(cwd) {
4349
4477
  try {
4350
4478
  let repoRoot;
4351
4479
  try {
4352
- const gitCommonDir = execSync2("git rev-parse --path-format=absolute --git-common-dir", {
4480
+ const gitCommonDir = execSync3("git rev-parse --path-format=absolute --git-common-dir", {
4353
4481
  cwd: dir,
4354
4482
  encoding: "utf8",
4355
4483
  timeout: 2e3,
@@ -4357,7 +4485,7 @@ function getProjectName(cwd) {
4357
4485
  }).trim();
4358
4486
  repoRoot = path9.dirname(gitCommonDir);
4359
4487
  } catch {
4360
- repoRoot = execSync2("git rev-parse --show-toplevel", {
4488
+ repoRoot = execSync3("git rev-parse --show-toplevel", {
4361
4489
  cwd: dir,
4362
4490
  encoding: "utf8",
4363
4491
  timeout: 2e3,
@@ -4391,14 +4519,14 @@ var file_grep_exports = {};
4391
4519
  __export(file_grep_exports, {
4392
4520
  grepProjectFiles: () => grepProjectFiles
4393
4521
  });
4394
- import { execSync as execSync3 } from "child_process";
4522
+ import { execSync as execSync4 } from "child_process";
4395
4523
  import { readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync9 } from "fs";
4396
4524
  import path10 from "path";
4397
4525
  import crypto2 from "crypto";
4398
4526
  function hasRipgrep() {
4399
4527
  if (_hasRg === null) {
4400
4528
  try {
4401
- execSync3("rg --version", { stdio: "ignore", timeout: 2e3 });
4529
+ execSync4("rg --version", { stdio: "ignore", timeout: 2e3 });
4402
4530
  _hasRg = true;
4403
4531
  } catch {
4404
4532
  _hasRg = false;
@@ -4464,7 +4592,7 @@ function grepWithRipgrep(pattern, projectRoot, patterns) {
4464
4592
  const globs = (patterns ?? DEFAULT_PATTERNS).map((p) => `--glob '${p}'`).join(" ");
4465
4593
  const excludes = EXCLUDE_DIRS.map((d) => `--glob '!${d}'`).join(" ");
4466
4594
  const cmd = `rg -i -c --hidden --no-config --no-ignore '${pattern.replace(/'/g, "\\'")}' . ${globs} ${excludes} --max-filesize ${MAX_FILE_SIZE} 2>/dev/null || true`;
4467
- const output = execSync3(cmd, {
4595
+ const output = execSync4(cmd, {
4468
4596
  cwd: projectRoot,
4469
4597
  encoding: "utf8",
4470
4598
  timeout: 3e3,
@@ -4479,12 +4607,12 @@ function grepWithRipgrep(pattern, projectRoot, patterns) {
4479
4607
  const matchCount = parseInt(line.slice(colonIdx + 1));
4480
4608
  if (isNaN(matchCount) || matchCount === 0) continue;
4481
4609
  try {
4482
- const firstMatch = execSync3(
4610
+ const firstMatch = execSync4(
4483
4611
  `rg -i -n --hidden '${pattern.replace(/'/g, "\\'")}' '${filePath}' --max-count 1 2>/dev/null | head -1`,
4484
4612
  { cwd: projectRoot, encoding: "utf8", timeout: 1e3 }
4485
4613
  ).trim();
4486
4614
  const lineNum = parseInt(firstMatch.split(":")[0] ?? "1");
4487
- const totalLines = execSync3(`wc -l < '${filePath}'`, {
4615
+ const totalLines = execSync4(`wc -l < '${filePath}'`, {
4488
4616
  cwd: projectRoot,
4489
4617
  encoding: "utf8",
4490
4618
  timeout: 1e3
@@ -5113,10 +5241,17 @@ async function applyEntityBoost(results, query, client) {
5113
5241
  if (ENTITY_BOOST_WEIGHT === 0 || results.length === 0) {
5114
5242
  return emptyResult;
5115
5243
  }
5116
- console.time("entity-boost");
5244
+ const debugStart = process.env.EXE_DEBUG_HOOKS ? performance.now() : 0;
5245
+ const debugEnd = () => {
5246
+ if (!process.env.EXE_DEBUG_HOOKS) return;
5247
+ process.stderr.write(
5248
+ `[entity-boost] ${(performance.now() - debugStart).toFixed(3)}ms
5249
+ `
5250
+ );
5251
+ };
5117
5252
  const entities = await matchEntities(query, client);
5118
5253
  if (entities.length === 0) {
5119
- console.timeEnd("entity-boost");
5254
+ debugEnd();
5120
5255
  return emptyResult;
5121
5256
  }
5122
5257
  const boostMap = /* @__PURE__ */ new Map();
@@ -5138,7 +5273,7 @@ async function applyEntityBoost(results, query, client) {
5138
5273
  await traverseAndScore(entities, client, boostMap, resultIds, graphContextMap);
5139
5274
  await applyHyperedgeBoost(entities, client, boostMap, resultIds);
5140
5275
  if (boostMap.size === 0) {
5141
- console.timeEnd("entity-boost");
5276
+ debugEnd();
5142
5277
  return emptyResult;
5143
5278
  }
5144
5279
  const scored = results.map((r, i) => ({
@@ -5149,7 +5284,7 @@ async function applyEntityBoost(results, query, client) {
5149
5284
  scored.sort(
5150
5285
  (a, b) => b.baseScore + b.entityBoost - (a.baseScore + a.entityBoost)
5151
5286
  );
5152
- console.timeEnd("entity-boost");
5287
+ debugEnd();
5153
5288
  return {
5154
5289
  results: scored.map((s) => s.record),
5155
5290
  graphContext: graphContextMap
@@ -1,6 +1,14 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/lib/keychain.ts
2
9
  import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
3
10
  import { existsSync } from "fs";
11
+ import { execSync } from "child_process";
4
12
  import path from "path";
5
13
  import os from "os";
6
14
  var SERVICE = "exe-mem";
@@ -11,6 +19,83 @@ function getKeyDir() {
11
19
  function getKeyPath() {
12
20
  return path.join(getKeyDir(), "master.key");
13
21
  }
22
+ function macKeychainGet() {
23
+ if (process.platform !== "darwin") return null;
24
+ try {
25
+ return execSync(
26
+ `security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
27
+ { encoding: "utf-8", timeout: 5e3 }
28
+ ).trim();
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function macKeychainSet(value) {
34
+ if (process.platform !== "darwin") return false;
35
+ try {
36
+ try {
37
+ execSync(
38
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
39
+ { timeout: 5e3 }
40
+ );
41
+ } catch {
42
+ }
43
+ execSync(
44
+ `security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
45
+ { timeout: 5e3 }
46
+ );
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+ function macKeychainDelete() {
53
+ if (process.platform !== "darwin") return false;
54
+ try {
55
+ execSync(
56
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
57
+ { timeout: 5e3 }
58
+ );
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function linuxSecretGet() {
65
+ if (process.platform !== "linux") return null;
66
+ try {
67
+ return execSync(
68
+ `secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
69
+ { encoding: "utf-8", timeout: 5e3 }
70
+ ).trim();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function linuxSecretSet(value) {
76
+ if (process.platform !== "linux") return false;
77
+ try {
78
+ execSync(
79
+ `echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
80
+ { timeout: 5e3 }
81
+ );
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ function linuxSecretDelete() {
88
+ if (process.platform !== "linux") return false;
89
+ try {
90
+ execSync(
91
+ `secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
92
+ { timeout: 5e3 }
93
+ );
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
14
99
  async function tryKeytar() {
15
100
  try {
16
101
  return await import("keytar");
@@ -18,13 +103,73 @@ async function tryKeytar() {
18
103
  return null;
19
104
  }
20
105
  }
106
+ var ENCRYPTED_PREFIX = "enc:";
107
+ function deriveMachineKey() {
108
+ try {
109
+ const crypto = __require("crypto");
110
+ const material = [
111
+ os.hostname(),
112
+ os.userInfo().username,
113
+ os.arch(),
114
+ os.platform(),
115
+ // Machine ID on Linux (stable across reboots)
116
+ process.platform === "linux" ? readMachineId() : ""
117
+ ].join("|");
118
+ return crypto.createHash("sha256").update(material).digest();
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ function readMachineId() {
124
+ try {
125
+ const { readFileSync } = __require("fs");
126
+ return readFileSync("/etc/machine-id", "utf-8").trim();
127
+ } catch {
128
+ return "";
129
+ }
130
+ }
131
+ function encryptWithMachineKey(plaintext, machineKey) {
132
+ const crypto = __require("crypto");
133
+ const iv = crypto.randomBytes(12);
134
+ const cipher = crypto.createCipheriv("aes-256-gcm", machineKey, iv);
135
+ let encrypted = cipher.update(plaintext, "utf-8", "base64");
136
+ encrypted += cipher.final("base64");
137
+ const authTag = cipher.getAuthTag().toString("base64");
138
+ return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${authTag}:${encrypted}`;
139
+ }
140
+ function decryptWithMachineKey(encrypted, machineKey) {
141
+ if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
142
+ try {
143
+ const crypto = __require("crypto");
144
+ const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
145
+ if (parts.length !== 3) return null;
146
+ const [ivB64, tagB64, cipherB64] = parts;
147
+ const iv = Buffer.from(ivB64, "base64");
148
+ const authTag = Buffer.from(tagB64, "base64");
149
+ const decipher = crypto.createDecipheriv("aes-256-gcm", machineKey, iv);
150
+ decipher.setAuthTag(authTag);
151
+ let decrypted = decipher.update(cipherB64, "base64", "utf-8");
152
+ decrypted += decipher.final("utf-8");
153
+ return decrypted;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
21
158
  async function getMasterKey() {
159
+ const nativeValue = macKeychainGet() ?? linuxSecretGet();
160
+ if (nativeValue) {
161
+ return Buffer.from(nativeValue, "base64");
162
+ }
22
163
  const keytar = await tryKeytar();
23
164
  if (keytar) {
24
165
  try {
25
- const stored = await keytar.getPassword(SERVICE, ACCOUNT);
26
- if (stored) {
27
- return Buffer.from(stored, "base64");
166
+ const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
167
+ if (keytarValue) {
168
+ const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
169
+ if (migrated) {
170
+ process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
171
+ }
172
+ return Buffer.from(keytarValue, "base64");
28
173
  }
29
174
  } catch {
30
175
  }
@@ -38,8 +183,31 @@ async function getMasterKey() {
38
183
  return null;
39
184
  }
40
185
  try {
41
- const content = await readFile(keyPath, "utf-8");
42
- return Buffer.from(content.trim(), "base64");
186
+ const content = (await readFile(keyPath, "utf-8")).trim();
187
+ let b64Value;
188
+ if (content.startsWith(ENCRYPTED_PREFIX)) {
189
+ const machineKey = deriveMachineKey();
190
+ if (!machineKey) {
191
+ process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
192
+ return null;
193
+ }
194
+ const decrypted = decryptWithMachineKey(content, machineKey);
195
+ if (!decrypted) {
196
+ process.stderr.write(
197
+ "[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
198
+ );
199
+ return null;
200
+ }
201
+ b64Value = decrypted;
202
+ } else {
203
+ b64Value = content;
204
+ }
205
+ const key = Buffer.from(b64Value, "base64");
206
+ const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
207
+ if (migrated) {
208
+ process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
209
+ }
210
+ return key;
43
211
  } catch (err) {
44
212
  process.stderr.write(
45
213
  `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
@@ -50,6 +218,9 @@ async function getMasterKey() {
50
218
  }
51
219
  async function setMasterKey(key) {
52
220
  const b64 = key.toString("base64");
221
+ if (macKeychainSet(b64) || linuxSecretSet(b64)) {
222
+ return;
223
+ }
53
224
  const keytar = await tryKeytar();
54
225
  if (keytar) {
55
226
  try {
@@ -61,10 +232,23 @@ async function setMasterKey(key) {
61
232
  const dir = getKeyDir();
62
233
  await mkdir(dir, { recursive: true });
63
234
  const keyPath = getKeyPath();
64
- await writeFile(keyPath, b64 + "\n", "utf-8");
65
- await chmod(keyPath, 384);
235
+ const machineKey = deriveMachineKey();
236
+ if (machineKey) {
237
+ const encrypted = encryptWithMachineKey(b64, machineKey);
238
+ await writeFile(keyPath, encrypted + "\n", "utf-8");
239
+ await chmod(keyPath, 384);
240
+ process.stderr.write("[keychain] Key stored encrypted (machine-bound).\n");
241
+ } else {
242
+ await writeFile(keyPath, b64 + "\n", "utf-8");
243
+ await chmod(keyPath, 384);
244
+ process.stderr.write(
245
+ "[keychain] WARNING: Key stored in plaintext file \u2014 no OS keychain available.\n"
246
+ );
247
+ }
66
248
  }
67
249
  async function deleteMasterKey() {
250
+ macKeychainDelete();
251
+ linuxSecretDelete();
68
252
  const keytar = await tryKeytar();
69
253
  if (keytar) {
70
254
  try {