@askexenow/exe-os 0.8.37 → 0.8.39

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 (93) hide show
  1. package/README.md +17 -8
  2. package/dist/bin/backfill-conversations.js +112 -70
  3. package/dist/bin/backfill-responses.js +53 -18
  4. package/dist/bin/backfill-vectors.js +43 -16
  5. package/dist/bin/cleanup-stale-review-tasks.js +38 -16
  6. package/dist/bin/cli.js +790 -468
  7. package/dist/bin/exe-agent.js +19 -4
  8. package/dist/bin/exe-assign.js +46 -13
  9. package/dist/bin/exe-boot.js +288 -129
  10. package/dist/bin/exe-call.js +20 -10
  11. package/dist/bin/exe-cloud.js +135 -30
  12. package/dist/bin/exe-dispatch.js +1 -1
  13. package/dist/bin/exe-doctor.js +38 -16
  14. package/dist/bin/exe-export-behaviors.js +43 -21
  15. package/dist/bin/exe-forget.js +39 -17
  16. package/dist/bin/exe-gateway.js +159 -50
  17. package/dist/bin/exe-heartbeat.js +53 -31
  18. package/dist/bin/exe-kill.js +40 -18
  19. package/dist/bin/exe-launch-agent.js +109 -36
  20. package/dist/bin/exe-link.js +196 -87
  21. package/dist/bin/exe-new-employee.js +56 -17
  22. package/dist/bin/exe-pending-messages.js +47 -25
  23. package/dist/bin/exe-pending-notifications.js +38 -16
  24. package/dist/bin/exe-pending-reviews.js +51 -29
  25. package/dist/bin/exe-rename.js +21 -7
  26. package/dist/bin/exe-review.js +41 -13
  27. package/dist/bin/exe-search.js +57 -21
  28. package/dist/bin/exe-session-cleanup.js +67 -31
  29. package/dist/bin/exe-settings.js +63 -2
  30. package/dist/bin/exe-status.js +35 -13
  31. package/dist/bin/exe-team.js +35 -13
  32. package/dist/bin/git-sweep.js +45 -17
  33. package/dist/bin/graph-backfill.js +38 -16
  34. package/dist/bin/graph-export.js +38 -16
  35. package/dist/bin/install.js +10 -1
  36. package/dist/bin/scan-tasks.js +47 -19
  37. package/dist/bin/setup.js +444 -259
  38. package/dist/bin/shard-migrate.js +38 -16
  39. package/dist/bin/wiki-sync.js +40 -17
  40. package/dist/gateway/index.js +113 -48
  41. package/dist/hooks/bug-report-worker.js +66 -39
  42. package/dist/hooks/commit-complete.js +45 -17
  43. package/dist/hooks/error-recall.js +60 -20
  44. package/dist/hooks/exe-heartbeat-hook.js +3 -2
  45. package/dist/hooks/ingest-worker.js +174 -45
  46. package/dist/hooks/ingest.js +74 -28
  47. package/dist/hooks/instructions-loaded.js +46 -17
  48. package/dist/hooks/notification.js +44 -15
  49. package/dist/hooks/post-compact.js +44 -15
  50. package/dist/hooks/pre-compact.js +42 -14
  51. package/dist/hooks/pre-tool-use.js +59 -22
  52. package/dist/hooks/prompt-ingest-worker.js +75 -14
  53. package/dist/hooks/prompt-submit.js +75 -32
  54. package/dist/hooks/response-ingest-worker.js +76 -15
  55. package/dist/hooks/session-end.js +54 -22
  56. package/dist/hooks/session-start.js +57 -20
  57. package/dist/hooks/stop.js +44 -15
  58. package/dist/hooks/subagent-stop.js +44 -15
  59. package/dist/hooks/summary-worker.js +339 -106
  60. package/dist/index.js +94 -23
  61. package/dist/lib/cloud-sync.js +191 -80
  62. package/dist/lib/config.js +4 -1
  63. package/dist/lib/consolidation.js +5 -4
  64. package/dist/lib/database.js +1 -0
  65. package/dist/lib/device-registry.js +2 -1
  66. package/dist/lib/embedder.js +9 -1
  67. package/dist/lib/employee-templates.js +5 -0
  68. package/dist/lib/employees.js +11 -6
  69. package/dist/lib/exe-daemon-client.js +6 -1
  70. package/dist/lib/exe-daemon.js +95 -36
  71. package/dist/lib/hybrid-search.js +57 -21
  72. package/dist/lib/identity-templates.js +16 -7
  73. package/dist/lib/identity.js +1 -1
  74. package/dist/lib/keychain.js +2 -1
  75. package/dist/lib/license.js +56 -6
  76. package/dist/lib/messaging.js +1 -1
  77. package/dist/lib/reminders.js +2 -2
  78. package/dist/lib/schedules.js +38 -16
  79. package/dist/lib/skill-learning.js +1 -1
  80. package/dist/lib/store.js +44 -16
  81. package/dist/lib/tasks.js +1 -1
  82. package/dist/lib/tmux-routing.js +1 -1
  83. package/dist/mcp/server.js +280 -155
  84. package/dist/mcp/tools/complete-reminder.js +1 -1
  85. package/dist/mcp/tools/create-task.js +14 -6
  86. package/dist/mcp/tools/deactivate-behavior.js +2 -2
  87. package/dist/mcp/tools/list-reminders.js +1 -1
  88. package/dist/mcp/tools/list-tasks.js +36 -28
  89. package/dist/mcp/tools/send-message.js +1 -1
  90. package/dist/mcp/tools/update-task.js +1 -1
  91. package/dist/runtime/index.js +42 -8
  92. package/dist/tui/App.js +220 -99
  93. package/package.json +5 -3
package/dist/bin/setup.js CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
4
10
  var __esm = (fn, res) => function __init() {
5
11
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
12
  };
@@ -25,7 +31,7 @@ __export(config_exports, {
25
31
  migrateConfig: () => migrateConfig,
26
32
  saveConfig: () => saveConfig
27
33
  });
28
- import { readFile, writeFile, mkdir } from "fs/promises";
34
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
29
35
  import { readFileSync, existsSync, renameSync } from "fs";
30
36
  import path from "path";
31
37
  import os from "os";
@@ -151,6 +157,9 @@ async function saveConfig(config) {
151
157
  await mkdir(dir, { recursive: true });
152
158
  const configPath = path.join(dir, "config.json");
153
159
  await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
160
+ if (config.cloud?.apiKey) {
161
+ await chmod(configPath, 384);
162
+ }
154
163
  }
155
164
  async function loadConfigFrom(configPath) {
156
165
  const raw = await readFile(configPath, "utf-8");
@@ -270,6 +279,10 @@ import path4 from "path";
270
279
  import { fileURLToPath } from "url";
271
280
  function handleData(chunk) {
272
281
  _buffer += chunk.toString();
282
+ if (_buffer.length > MAX_BUFFER) {
283
+ _buffer = "";
284
+ return;
285
+ }
273
286
  let newlineIdx;
274
287
  while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
275
288
  const line = _buffer.slice(0, newlineIdx).trim();
@@ -577,7 +590,7 @@ function disconnectClient() {
577
590
  entry.resolve({ error: "Client disconnected" });
578
591
  }
579
592
  }
580
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending;
593
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending, MAX_BUFFER;
581
594
  var init_exe_daemon_client = __esm({
582
595
  "src/lib/exe-daemon-client.ts"() {
583
596
  "use strict";
@@ -594,6 +607,7 @@ var init_exe_daemon_client = __esm({
594
607
  _requestCount = 0;
595
608
  HEALTH_CHECK_INTERVAL = 100;
596
609
  _pending = /* @__PURE__ */ new Map();
610
+ MAX_BUFFER = 1e7;
597
611
  }
598
612
  });
599
613
 
@@ -678,12 +692,22 @@ __export(license_exports, {
678
692
  loadLicense: () => loadLicense,
679
693
  mirrorLicenseKey: () => mirrorLicenseKey,
680
694
  saveLicense: () => saveLicense,
695
+ startLicenseRevalidation: () => startLicenseRevalidation,
696
+ stopLicenseRevalidation: () => stopLicenseRevalidation,
681
697
  validateLicense: () => validateLicense
682
698
  });
683
699
  import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync5, mkdirSync } from "fs";
684
700
  import { randomUUID as randomUUID2 } from "crypto";
685
701
  import path5 from "path";
686
702
  import { jwtVerify, importSPKI } from "jose";
703
+ async function fetchRetry(url, init) {
704
+ try {
705
+ return await fetch(url, init);
706
+ } catch {
707
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
708
+ return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
709
+ }
710
+ }
687
711
  function loadDeviceId() {
688
712
  const deviceJsonPath = path5.join(EXE_AI_DIR, "device.json");
689
713
  try {
@@ -715,7 +739,7 @@ function loadLicense() {
715
739
  }
716
740
  function saveLicense(apiKey) {
717
741
  mkdirSync(EXE_AI_DIR, { recursive: true });
718
- writeFileSync(LICENSE_PATH, apiKey.trim(), "utf8");
742
+ writeFileSync(LICENSE_PATH, apiKey.trim(), { encoding: "utf8", mode: 384 });
719
743
  }
720
744
  async function verifyLicenseJwt(token) {
721
745
  try {
@@ -767,7 +791,7 @@ function cacheResponse(token) {
767
791
  async function validateLicense(apiKey, deviceId) {
768
792
  const did = deviceId ?? loadDeviceId();
769
793
  try {
770
- const res = await fetch(`${API_BASE}/auth/activate`, {
794
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
771
795
  method: "POST",
772
796
  headers: { "Content-Type": "application/json" },
773
797
  body: JSON.stringify({ apiKey, deviceId: did }),
@@ -802,14 +826,23 @@ async function validateLicense(apiKey, deviceId) {
802
826
  } catch {
803
827
  const cached = await getCachedLicense();
804
828
  if (cached) return cached;
805
- return FREE_LICENSE;
829
+ return { ...FREE_LICENSE, valid: false, error: "offline" };
830
+ }
831
+ }
832
+ function getCacheAgeMs() {
833
+ try {
834
+ const { statSync: statSync2 } = __require("fs");
835
+ const s = statSync2(CACHE_PATH);
836
+ return Date.now() - s.mtimeMs;
837
+ } catch {
838
+ return Infinity;
806
839
  }
807
840
  }
808
841
  async function checkLicense() {
809
842
  const key = loadLicense();
810
843
  if (!key) return FREE_LICENSE;
811
844
  const cached = await getCachedLicense();
812
- if (cached) return cached;
845
+ if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
813
846
  const deviceId = loadDeviceId();
814
847
  return validateLicense(key, deviceId);
815
848
  }
@@ -849,7 +882,7 @@ async function assertVpsLicense(opts) {
849
882
  let explicitRejection = false;
850
883
  let transientFailure = false;
851
884
  try {
852
- const res = await fetch(`${API_BASE}/auth/activate`, {
885
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
853
886
  method: "POST",
854
887
  headers: { "Content-Type": "application/json" },
855
888
  body: JSON.stringify({ apiKey, deviceId }),
@@ -930,7 +963,28 @@ async function assertVpsLicense(opts) {
930
963
  `License validation unreachable for more than ${graceDays} days. Restore network connectivity to https://askexe.com/cloud and retry. This VPS image refuses to boot after the offline grace window.`
931
964
  );
932
965
  }
933
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE;
966
+ function startLicenseRevalidation(intervalMs = 36e5) {
967
+ if (_revalTimer) return;
968
+ _revalTimer = setInterval(async () => {
969
+ try {
970
+ const license = await checkLicense();
971
+ if (!license.valid) {
972
+ process.stderr.write("[exe-os] License expired or invalid \u2014 features may be restricted\n");
973
+ }
974
+ } catch {
975
+ }
976
+ }, intervalMs);
977
+ if (_revalTimer && typeof _revalTimer === "object" && "unref" in _revalTimer) {
978
+ _revalTimer.unref();
979
+ }
980
+ }
981
+ function stopLicenseRevalidation() {
982
+ if (_revalTimer) {
983
+ clearInterval(_revalTimer);
984
+ _revalTimer = null;
985
+ }
986
+ }
987
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, RETRY_DELAY_MS, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS, _revalTimer;
934
988
  var init_license = __esm({
935
989
  "src/lib/license.ts"() {
936
990
  "use strict";
@@ -939,6 +993,7 @@ var init_license = __esm({
939
993
  CACHE_PATH = path5.join(EXE_AI_DIR, "license-cache.json");
940
994
  DEVICE_ID_PATH = path5.join(EXE_AI_DIR, "device-id");
941
995
  API_BASE = "https://askexe.com/cloud";
996
+ RETRY_DELAY_MS = 500;
942
997
  LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
943
998
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
944
999
  4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
@@ -960,6 +1015,8 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
960
1015
  employeeLimit: 1,
961
1016
  memoryLimit: 5e3
962
1017
  };
1018
+ CACHE_MAX_AGE_MS = 36e5;
1019
+ _revalTimer = null;
963
1020
  }
964
1021
  });
965
1022
 
@@ -1050,15 +1107,20 @@ function addEmployee(employees, employee) {
1050
1107
  }
1051
1108
  return [...employees, normalized];
1052
1109
  }
1110
+ function findExeBin() {
1111
+ try {
1112
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
1113
+ } catch {
1114
+ return null;
1115
+ }
1116
+ }
1053
1117
  function registerBinSymlinks(name) {
1054
1118
  const created = [];
1055
1119
  const skipped = [];
1056
1120
  const errors = [];
1057
- let exeBinPath;
1058
- try {
1059
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
1060
- } catch {
1061
- errors.push("Could not find 'exe' in PATH");
1121
+ const exeBinPath = findExeBin();
1122
+ if (!exeBinPath) {
1123
+ errors.push("Could not find 'exe-os' in PATH");
1062
1124
  return { created, skipped, errors };
1063
1125
  }
1064
1126
  const binDir = path6.dirname(exeBinPath);
@@ -1106,6 +1168,7 @@ __export(employee_templates_exports, {
1106
1168
  buildCustomEmployeePrompt: () => buildCustomEmployeePrompt,
1107
1169
  getSessionPrompt: () => getSessionPrompt,
1108
1170
  getTemplate: () => getTemplate,
1171
+ getTemplateByRole: () => getTemplateByRole,
1109
1172
  personalizePrompt: () => personalizePrompt,
1110
1173
  renderClientCOOTemplate: () => renderClientCOOTemplate
1111
1174
  });
@@ -1121,6 +1184,10 @@ function buildCustomEmployeePrompt(name, role) {
1121
1184
  function getTemplate(name) {
1122
1185
  return TEMPLATES[name];
1123
1186
  }
1187
+ function getTemplateByRole(role) {
1188
+ const lower = role.toLowerCase();
1189
+ return Object.values(TEMPLATES).find((t) => t.role.toLowerCase() === lower);
1190
+ }
1124
1191
  function personalizePrompt(prompt, templateName, actualName) {
1125
1192
  if (templateName === actualName) return prompt;
1126
1193
  const escaped = templateName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1868,6 +1935,7 @@ var init_identity = __esm({
1868
1935
  var identity_templates_exports = {};
1869
1936
  __export(identity_templates_exports, {
1870
1937
  IDENTITY_TEMPLATES: () => IDENTITY_TEMPLATES,
1938
+ PLAN_MODE_COMPAT: () => PLAN_MODE_COMPAT,
1871
1939
  POST_WORK_CHECKLIST: () => POST_WORK_CHECKLIST,
1872
1940
  getTemplate: () => getTemplate2,
1873
1941
  getTemplateForTitle: () => getTemplateForTitle
@@ -1887,10 +1955,18 @@ function getTemplateForTitle(title) {
1887
1955
  if (t.includes("review") || t.includes("audit") || t.includes("qa")) return IDENTITY_TEMPLATES["staff-code-reviewer"];
1888
1956
  return null;
1889
1957
  }
1890
- var POST_WORK_CHECKLIST, IDENTITY_TEMPLATES;
1958
+ var PLAN_MODE_COMPAT, POST_WORK_CHECKLIST, IDENTITY_TEMPLATES;
1891
1959
  var init_identity_templates = __esm({
1892
1960
  "src/lib/identity-templates.ts"() {
1893
1961
  "use strict";
1962
+ PLAN_MODE_COMPAT = `
1963
+ ## Plan Mode Compatibility
1964
+ If tool execution is unavailable (e.g., CC plan mode), switch to planning:
1965
+ - Reason about the task and create a written plan
1966
+ - Document what tools you would call and with what parameters
1967
+ - Output structured text that can be acted on when tools become available
1968
+ Do not repeatedly attempt tool calls that fail \u2014 switch to planning mode.
1969
+ `;
1894
1970
  POST_WORK_CHECKLIST = `
1895
1971
  5. Check for pending reviews (list_tasks status='needs_review' where you are reviewer) \u2014 reviews are work, process before new tasks
1896
1972
  6. Check for blocked tasks (list_tasks status='blocked') \u2014 can you unblock it? Do it now. Can't? Escalate to exe immediately.
@@ -1970,7 +2046,7 @@ Never say "I have no memories" without first searching broadly. Your memory may
1970
2046
  - **update_identity** \u2014 rewrite any agent's identity when role/responsibilities change (exe/founder only)
1971
2047
  - **get_identity** \u2014 read any agent's identity for coordination
1972
2048
  - **send_message** \u2014 direct intercom to employees
1973
-
2049
+ ${PLAN_MODE_COMPAT}
1974
2050
  ## Completion Workflow
1975
2051
 
1976
2052
  1. Read the task file and verify the deliverable matches the brief
@@ -2041,7 +2117,7 @@ You are \${agent_id}. CTO. You hold deep context on the entire codebase, archite
2041
2117
  - **store_behavior** \u2014 record corrections for engineers (p0 = always injected)
2042
2118
  - **get_identity** \u2014 read any agent's identity for review context
2043
2119
  - **query_relationships** \u2014 GraphRAG entity connections for architecture analysis
2044
-
2120
+ ${PLAN_MODE_COMPAT}
2045
2121
  ## Completion Workflow
2046
2122
 
2047
2123
  1. Read ARCHITECTURE.md before starting work on any repo
@@ -2108,7 +2184,7 @@ You are \${agent_id}. CMO. You hold deep context on design, branding, storytelli
2108
2184
  - **update_task** \u2014 mark tasks done with result summary
2109
2185
  - **store_memory** \u2014 report completions with brand alignment notes, SEO considerations
2110
2186
  - **get_identity** \u2014 read team identities for brand-consistent communication
2111
-
2187
+ ${PLAN_MODE_COMPAT}
2112
2188
  ## Completion Workflow
2113
2189
 
2114
2190
  1. Read the task file and understand the brief \u2014 tone, format, channel requirements
@@ -2175,7 +2251,7 @@ You are a principal engineer. You write production-grade code with zero shortcut
2175
2251
  - **recall_my_memory** \u2014 check past work, patterns, gotchas in this project
2176
2252
  - **store_memory** \u2014 report completions for org visibility
2177
2253
  - **ask_team_memory** \u2014 pull context from colleagues when specs reference their work
2178
-
2254
+ ${PLAN_MODE_COMPAT}
2179
2255
  ## Completion Workflow
2180
2256
 
2181
2257
  1. Read ARCHITECTURE.md if it exists \u2014 understand architecture before changing anything
@@ -2235,7 +2311,7 @@ You are the content production specialist. You turn scripts and creative briefs
2235
2311
  - **update_task** \u2014 mark tasks done with result summary
2236
2312
  - **recall_my_memory** \u2014 check past work: which models worked, which prompts produced good results
2237
2313
  - **store_memory** \u2014 report completions with production decisions for future reference
2238
-
2314
+ ${PLAN_MODE_COMPAT}
2239
2315
  ## Completion Workflow
2240
2316
 
2241
2317
  1. Read the task file \u2014 understand the brief, check budget constraints
@@ -2307,7 +2383,7 @@ You are the AI Product Lead \u2014 the competitive intelligence engine. You stud
2307
2383
  - **update_task** \u2014 mark tasks done with analysis results
2308
2384
  - **store_memory** \u2014 persist competitive analyses, evaluations, recommendations
2309
2385
  - **create_task** \u2014 when a feature is worth building, spec it for the CTO
2310
-
2386
+ ${PLAN_MODE_COMPAT}
2311
2387
  ## Completion Workflow
2312
2388
 
2313
2389
  1. Read the task \u2014 understand what capability is needed
@@ -2370,7 +2446,7 @@ You are \${agent_id}. Staff Code Reviewer and System Auditor. Last line of defen
2370
2446
  - **store_behavior** \u2014 record new patterns
2371
2447
  - **update_task** \u2014 mark reviews done with structured findings
2372
2448
  - **create_task** \u2014 assign fixes to the CTO
2373
-
2449
+ ${PLAN_MODE_COMPAT}
2374
2450
  ## Completion Workflow
2375
2451
 
2376
2452
  1. Read the task brief and understand the audit scope
@@ -2388,20 +2464,21 @@ You are \${agent_id}. Staff Code Reviewer and System Auditor. Last line of defen
2388
2464
  // src/lib/setup-wizard.ts
2389
2465
  init_config();
2390
2466
  import crypto2 from "crypto";
2391
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2392
- import os2 from "os";
2467
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
2468
+ import os3 from "os";
2393
2469
  import path8 from "path";
2394
2470
  import { createInterface } from "readline";
2395
2471
 
2396
2472
  // src/lib/keychain.ts
2397
- import { readFile as readFile2, writeFile as writeFile2, unlink, mkdir as mkdir2, chmod } from "fs/promises";
2473
+ import { readFile as readFile2, writeFile as writeFile2, unlink, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
2398
2474
  import { existsSync as existsSync2 } from "fs";
2399
2475
  import path2 from "path";
2476
+ import os2 from "os";
2400
2477
  import crypto from "crypto";
2401
2478
  var SERVICE = "exe-mem";
2402
2479
  var ACCOUNT = "master-key";
2403
2480
  function getKeyDir() {
2404
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path2.join(process.env.HOME ?? "/tmp", ".exe-os");
2481
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path2.join(os2.homedir(), ".exe-os");
2405
2482
  }
2406
2483
  function getKeyPath() {
2407
2484
  return path2.join(getKeyDir(), "master.key");
@@ -2449,7 +2526,7 @@ async function setMasterKey(key) {
2449
2526
  await mkdir2(dir, { recursive: true });
2450
2527
  const keyPath = getKeyPath();
2451
2528
  await writeFile2(keyPath, b64 + "\n", "utf-8");
2452
- await chmod(keyPath, 384);
2529
+ await chmod2(keyPath, 384);
2453
2530
  }
2454
2531
 
2455
2532
  // src/lib/model-downloader.ts
@@ -2467,48 +2544,67 @@ async function downloadModel(opts) {
2467
2544
  const tmpPath = destPath + ".tmp";
2468
2545
  await mkdir3(destDir, { recursive: true });
2469
2546
  if (existsSync3(destPath)) {
2470
- const hash2 = await fileHash(destPath);
2471
- if (hash2 === EXPECTED_SHA256) {
2547
+ const hash = await fileHash(destPath);
2548
+ if (hash === EXPECTED_SHA256) {
2472
2549
  return destPath;
2473
2550
  }
2474
2551
  }
2475
- if (existsSync3(tmpPath)) unlinkSync(tmpPath);
2476
- const response = await fetchFn(GGUF_URL, { redirect: "follow" });
2477
- if (!response.ok || !response.body) {
2478
- throw new Error(`Download failed: HTTP ${response.status}`);
2479
- }
2480
- const contentLength = Number(response.headers.get("content-length") ?? EXPECTED_SIZE);
2552
+ const MAX_RETRIES = 3;
2553
+ const DOWNLOAD_TIMEOUT_MS = 3e5;
2554
+ let lastErr;
2481
2555
  let downloaded = 0;
2482
- const hash = createHash("sha256");
2483
- const fileStream = createWriteStream(tmpPath);
2484
- const reader = response.body.getReader();
2485
- try {
2486
- while (true) {
2487
- const { done, value } = await reader.read();
2488
- if (done) break;
2489
- if (!fileStream.write(value)) {
2490
- await new Promise((resolve) => fileStream.once("drain", resolve));
2556
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
2557
+ try {
2558
+ if (existsSync3(tmpPath)) unlinkSync(tmpPath);
2559
+ const response = await fetchFn(GGUF_URL, {
2560
+ redirect: "follow",
2561
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
2562
+ });
2563
+ if (!response.ok || !response.body) {
2564
+ throw new Error(`Download failed: HTTP ${response.status}`);
2565
+ }
2566
+ const contentLength = Number(response.headers.get("content-length") ?? EXPECTED_SIZE);
2567
+ const hash = createHash("sha256");
2568
+ const fileStream = createWriteStream(tmpPath);
2569
+ const reader = response.body.getReader();
2570
+ try {
2571
+ while (true) {
2572
+ const { done, value } = await reader.read();
2573
+ if (done) break;
2574
+ if (!fileStream.write(value)) {
2575
+ await new Promise((resolve) => fileStream.once("drain", resolve));
2576
+ }
2577
+ hash.update(value);
2578
+ downloaded += value.byteLength;
2579
+ onProgress?.(downloaded, contentLength);
2580
+ }
2581
+ } finally {
2582
+ fileStream.end();
2583
+ await new Promise((resolve, reject) => {
2584
+ fileStream.on("finish", resolve);
2585
+ fileStream.on("error", reject);
2586
+ });
2587
+ }
2588
+ const actualHash = hash.digest("hex");
2589
+ if (actualHash !== EXPECTED_SHA256) {
2590
+ unlinkSync(tmpPath);
2591
+ throw new Error(
2592
+ `SHA256 mismatch: expected ${EXPECTED_SHA256}, got ${actualHash}`
2593
+ );
2594
+ }
2595
+ renameSync2(tmpPath, destPath);
2596
+ return destPath;
2597
+ } catch (err) {
2598
+ lastErr = err instanceof Error ? err : new Error(String(err));
2599
+ if (attempt < MAX_RETRIES) {
2600
+ process.stderr.write(`
2601
+ Download attempt ${attempt} failed, retrying...
2602
+ `);
2603
+ if (existsSync3(tmpPath)) unlinkSync(tmpPath);
2491
2604
  }
2492
- hash.update(value);
2493
- downloaded += value.byteLength;
2494
- onProgress?.(downloaded, contentLength);
2495
2605
  }
2496
- } finally {
2497
- fileStream.end();
2498
- await new Promise((resolve, reject) => {
2499
- fileStream.on("finish", resolve);
2500
- fileStream.on("error", reject);
2501
- });
2502
2606
  }
2503
- const actualHash = hash.digest("hex");
2504
- if (actualHash !== EXPECTED_SHA256) {
2505
- unlinkSync(tmpPath);
2506
- throw new Error(
2507
- `SHA256 mismatch: expected ${EXPECTED_SHA256}, got ${actualHash}`
2508
- );
2509
- }
2510
- renameSync2(tmpPath, destPath);
2511
- return destPath;
2607
+ throw lastErr;
2512
2608
  }
2513
2609
  async function fileHash(filePath) {
2514
2610
  return new Promise((resolve, reject) => {
@@ -2521,6 +2617,24 @@ async function fileHash(filePath) {
2521
2617
  }
2522
2618
 
2523
2619
  // src/lib/setup-wizard.ts
2620
+ var SETUP_STATE_PATH = path8.join(os3.homedir(), ".exe-os", "setup-state.json");
2621
+ function loadSetupState() {
2622
+ try {
2623
+ return JSON.parse(readFileSync6(SETUP_STATE_PATH, "utf8"));
2624
+ } catch {
2625
+ return { completedSteps: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
2626
+ }
2627
+ }
2628
+ function saveSetupState(state) {
2629
+ mkdirSync3(path8.dirname(SETUP_STATE_PATH), { recursive: true });
2630
+ writeFileSync3(SETUP_STATE_PATH, JSON.stringify(state, null, 2));
2631
+ }
2632
+ function clearSetupState() {
2633
+ try {
2634
+ unlinkSync3(SETUP_STATE_PATH);
2635
+ } catch {
2636
+ }
2637
+ }
2524
2638
  function ask(rl, prompt) {
2525
2639
  return new Promise((resolve) => {
2526
2640
  const doAsk = () => {
@@ -2560,88 +2674,133 @@ async function runSetupWizard(opts = {}) {
2560
2674
  rl.close();
2561
2675
  return;
2562
2676
  }
2677
+ const state = loadSetupState();
2678
+ if (state.completedSteps.length > 0) {
2679
+ log(`Resuming setup from step ${Math.max(...state.completedSteps) + 1}...`);
2680
+ }
2563
2681
  if (existsSync8(LEGACY_LANCE_PATH)) {
2564
2682
  log("\u26A0 Found v1.0 LanceDB at ~/.exe-os/local.lance");
2565
2683
  log(" v1.1 uses libSQL (SQLite). Your existing memories are not automatically migrated.");
2566
2684
  log(" The old directory will not be modified or deleted.");
2567
2685
  log("");
2568
2686
  }
2569
- const existingKey = await getMasterKey();
2570
- if (existingKey) {
2571
- log("Encryption key already exists \u2014 skipping generation.");
2687
+ if (!state.completedSteps.includes(1)) {
2688
+ const existingKey = await getMasterKey();
2689
+ if (existingKey) {
2690
+ log("Encryption key already exists \u2014 skipping generation.");
2691
+ } else {
2692
+ log("Generating 256-bit encryption key...");
2693
+ const key = crypto2.randomBytes(32);
2694
+ await setMasterKey(key);
2695
+ log("Encryption key generated and stored securely.");
2696
+ }
2697
+ state.completedSteps.push(1);
2698
+ saveSetupState(state);
2572
2699
  } else {
2573
- log("Generating 256-bit encryption key...");
2574
- const key = crypto2.randomBytes(32);
2575
- await setMasterKey(key);
2576
- log("Encryption key generated and stored securely.");
2700
+ log("Step 1 already complete \u2014 skipping.");
2577
2701
  }
2578
2702
  log("");
2579
- log("Exe Cloud: your memories are end-to-end encrypted, compressed, and");
2580
- log("backed up on Exe Cloud. Free for all plans. We can't read your data \u2014");
2581
- log("only your encryption key can decrypt it.");
2582
2703
  let cloudConfig;
2583
- try {
2584
- const { loadDeviceId: loadDeviceId2 } = await Promise.resolve().then(() => (init_license(), license_exports));
2585
- const deviceId = loadDeviceId2();
2586
- const res = await fetch("https://askexe.com/cloud/auth/auto-provision", {
2587
- method: "POST",
2588
- headers: { "Content-Type": "application/json" },
2589
- body: JSON.stringify({ deviceId }),
2590
- signal: AbortSignal.timeout(1e4)
2591
- });
2592
- if (res.ok) {
2593
- const data = await res.json();
2594
- if (data.apiKey) {
2595
- cloudConfig = { apiKey: data.apiKey, endpoint: "https://askexe.com/cloud" };
2596
- const { saveLicense: saveLicense3, mirrorLicenseKey: mirrorLicenseKey3 } = await Promise.resolve().then(() => (init_license(), license_exports));
2597
- saveLicense3(data.apiKey);
2598
- mirrorLicenseKey3(data.apiKey);
2599
- log("Cloud sync activated automatically.");
2704
+ if (!state.completedSteps.includes(2)) {
2705
+ log("Exe Cloud: your memories are end-to-end encrypted, compressed, and");
2706
+ log("backed up on Exe Cloud. Free for all plans. We can't read your data \u2014");
2707
+ log("only your encryption key can decrypt it.");
2708
+ try {
2709
+ const { loadDeviceId: loadDeviceId2 } = await Promise.resolve().then(() => (init_license(), license_exports));
2710
+ const deviceId = loadDeviceId2();
2711
+ let res;
2712
+ try {
2713
+ res = await fetch("https://askexe.com/cloud/auth/auto-provision", {
2714
+ method: "POST",
2715
+ headers: { "Content-Type": "application/json" },
2716
+ body: JSON.stringify({ deviceId }),
2717
+ signal: AbortSignal.timeout(1e4)
2718
+ });
2719
+ } catch {
2720
+ await new Promise((r) => setTimeout(r, 500));
2721
+ res = await fetch("https://askexe.com/cloud/auth/auto-provision", {
2722
+ method: "POST",
2723
+ headers: { "Content-Type": "application/json" },
2724
+ body: JSON.stringify({ deviceId }),
2725
+ signal: AbortSignal.timeout(1e4)
2726
+ });
2600
2727
  }
2728
+ if (res.ok) {
2729
+ const data = await res.json();
2730
+ if (data.apiKey) {
2731
+ cloudConfig = { apiKey: data.apiKey, endpoint: "https://askexe.com/cloud" };
2732
+ const { saveLicense: saveLicense3, mirrorLicenseKey: mirrorLicenseKey3 } = await Promise.resolve().then(() => (init_license(), license_exports));
2733
+ saveLicense3(data.apiKey);
2734
+ mirrorLicenseKey3(data.apiKey);
2735
+ log("Cloud sync activated automatically.");
2736
+ }
2737
+ }
2738
+ } catch {
2739
+ log("Cloud sync will activate when online.");
2601
2740
  }
2602
- } catch {
2603
- log("Cloud sync will activate when online.");
2741
+ state.completedSteps.push(2);
2742
+ saveSetupState(state);
2743
+ } else {
2744
+ log("Step 2 already complete \u2014 skipping.");
2604
2745
  }
2605
2746
  log("");
2606
- if (!skipModel2) {
2607
- log("Note: jina-embeddings-v5-text-small is licensed CC-BY-NC-4.0 (non-commercial)");
2608
- log("");
2609
- await downloadModel({
2610
- destDir: MODELS_DIR,
2611
- onProgress: (downloaded, total) => {
2612
- const pct = (downloaded / total * 100).toFixed(1);
2613
- const dlMB = (downloaded / 1e6).toFixed(0);
2614
- const totalMB = (total / 1e6).toFixed(0);
2615
- process.stderr.write(`\rDownloading model: ${pct}% (${dlMB}/${totalMB} MB)`);
2616
- }
2617
- });
2618
- process.stderr.write("\n");
2619
- log("Model downloaded and verified.");
2747
+ if (!state.completedSteps.includes(3)) {
2748
+ if (!skipModel2) {
2749
+ log("Note: jina-embeddings-v5-text-small is licensed CC-BY-NC-4.0 (non-commercial)");
2750
+ log("");
2751
+ await downloadModel({
2752
+ destDir: MODELS_DIR,
2753
+ onProgress: (downloaded, total) => {
2754
+ const pct = (downloaded / total * 100).toFixed(1);
2755
+ const dlMB = (downloaded / 1e6).toFixed(0);
2756
+ const totalMB = (total / 1e6).toFixed(0);
2757
+ process.stderr.write(`\rDownloading model: ${pct}% (${dlMB}/${totalMB} MB)`);
2758
+ }
2759
+ });
2760
+ process.stderr.write("\n");
2761
+ log("Model downloaded and verified.");
2762
+ }
2763
+ state.completedSteps.push(3);
2764
+ saveSetupState(state);
2765
+ } else {
2766
+ log("Step 3 already complete \u2014 skipping.");
2620
2767
  }
2621
- if (!skipModel2 && !skipModelValidation) {
2622
- await validateModel(log);
2768
+ if (!state.completedSteps.includes(4)) {
2769
+ if (!skipModel2 && !skipModelValidation) {
2770
+ await validateModel(log);
2771
+ }
2772
+ state.completedSteps.push(4);
2773
+ saveSetupState(state);
2774
+ } else {
2775
+ log("Step 4 already complete \u2014 skipping.");
2623
2776
  }
2624
2777
  const config = await loadConfig();
2625
- if (cloudConfig) {
2626
- config.cloud = cloudConfig;
2627
- }
2628
- await saveConfig(config);
2629
- log("");
2630
- try {
2631
- const claudeJsonPath = path8.join(os2.homedir(), ".claude.json");
2632
- let claudeJson = {};
2778
+ if (!state.completedSteps.includes(5)) {
2779
+ if (cloudConfig) {
2780
+ config.cloud = cloudConfig;
2781
+ }
2782
+ await saveConfig(config);
2783
+ log("");
2633
2784
  try {
2634
- claudeJson = JSON.parse(readFileSync6(claudeJsonPath, "utf8"));
2785
+ const claudeJsonPath = path8.join(os3.homedir(), ".claude.json");
2786
+ let claudeJson = {};
2787
+ try {
2788
+ claudeJson = JSON.parse(readFileSync6(claudeJsonPath, "utf8"));
2789
+ } catch {
2790
+ }
2791
+ if (!claudeJson.projects) claudeJson.projects = {};
2792
+ const projects = claudeJson.projects;
2793
+ for (const dir of [process.cwd(), os3.homedir()]) {
2794
+ if (!projects[dir]) projects[dir] = {};
2795
+ projects[dir].hasTrustDialogAccepted = true;
2796
+ }
2797
+ writeFileSync3(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
2635
2798
  } catch {
2636
2799
  }
2637
- if (!claudeJson.projects) claudeJson.projects = {};
2638
- const projects = claudeJson.projects;
2639
- for (const dir of [process.cwd(), os2.homedir()]) {
2640
- if (!projects[dir]) projects[dir] = {};
2641
- projects[dir].hasTrustDialogAccepted = true;
2642
- }
2643
- writeFileSync3(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
2644
- } catch {
2800
+ state.completedSteps.push(5);
2801
+ saveSetupState(state);
2802
+ } else {
2803
+ log("Step 5 already complete \u2014 skipping.");
2645
2804
  }
2646
2805
  const {
2647
2806
  loadEmployees: loadEmployees2,
@@ -2650,7 +2809,7 @@ async function runSetupWizard(opts = {}) {
2650
2809
  registerBinSymlinks: registerBinSymlinks2,
2651
2810
  EMPLOYEES_PATH: EMPLOYEES_PATH2
2652
2811
  } = await Promise.resolve().then(() => (init_employees(), employees_exports));
2653
- const { getTemplate: getEmployeeTemplate } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2812
+ const { getTemplateByRole: getTemplateByRole2 } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2654
2813
  const { identityPath: identityPath2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
2655
2814
  const { getTemplate: getIdentityTemplate } = await Promise.resolve().then(() => (init_identity_templates(), identity_templates_exports));
2656
2815
  const {
@@ -2660,152 +2819,178 @@ async function runSetupWizard(opts = {}) {
2660
2819
  validateLicense: validateLicense2
2661
2820
  } = await Promise.resolve().then(() => (init_license(), license_exports));
2662
2821
  const createdEmployees = [];
2663
- log("=== Your Team ===");
2664
- log("");
2665
- log("Every install starts with a COO \u2014 your right-hand operator.");
2666
- log("They hold the big picture: priorities, progress, and blockers.");
2667
- log("You talk to them. They coordinate everyone else.");
2668
- log("");
2669
- const cooNameInput = await ask(rl, "Name your COO (default: exe): ");
2670
- const cooName = (cooNameInput || "exe").toLowerCase();
2671
- let employees = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
2672
- if (!employees.some((e) => e.name === cooName)) {
2673
- const { DEFAULT_EXE: DEFAULT_EXE2, personalizePrompt: personalizePrompt2 } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2674
- const cooEmployee = {
2675
- name: cooName,
2676
- role: "COO",
2677
- systemPrompt: personalizePrompt2(DEFAULT_EXE2.systemPrompt, "exe", cooName),
2678
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2679
- templateName: "exe",
2680
- templateVersion: 1
2681
- };
2682
- employees = addEmployee2(employees, cooEmployee);
2683
- await saveEmployees2(employees, EMPLOYEES_PATH2);
2684
- }
2685
- const cooIdentityContent = getIdentityTemplate("coo");
2686
- if (cooIdentityContent) {
2687
- const cooIdPath = identityPath2(cooName);
2688
- mkdirSync3(path8.dirname(cooIdPath), { recursive: true });
2689
- const replaced = cooIdentityContent.replace(/agent_id:\s*exe/g, `agent_id: ${cooName}`).replace(/\$\{agent_id\}/g, cooName);
2690
- writeFileSync3(cooIdPath, replaced, "utf-8");
2691
- }
2692
- registerBinSymlinks2(cooName);
2693
- createdEmployees.push({ name: cooName, role: "COO" });
2694
- log("");
2695
- log("=== Meet Your Specialists ===");
2696
- log("");
2697
- log("Your COO coordinates specialists. Here's who you can hire:");
2698
- log("");
2699
- log(" CTO (default: yoshi)");
2700
- log(" Your head of engineering. Architecture, code reviews, tech decisions.");
2701
- log(" Manages your projects and delegates to engineers when there's parallel work.");
2702
- log("");
2703
- log(" CMO (default: mari)");
2704
- log(" Design, brand, content, SEO. Builds your visual identity, writes");
2705
- log(" your copy, and gets you found online. Delegates to content specialists.");
2706
- log("");
2707
- log("Why separate roles instead of one AI that does everything?");
2708
- log("");
2709
- log("Memory saturates. One agent juggling architecture decisions AND landing page");
2710
- log("copy AND CI/CD AND social media loses context on all of them. Competing");
2711
- log("priorities \u2014 should I fix the auth bug or write the blog post? When you split");
2712
- log("responsibilities, each specialist stays sharp because they stay focused.");
2713
- log("Your COO connects them so nothing falls through the cracks.");
2714
- log("");
2715
- log("This is how real companies scale \u2014 you're just doing it with AI");
2716
- log("instead of headcount.");
2717
- log("");
2718
- await ask(rl, "Press Enter to continue. ");
2719
- let license;
2720
- try {
2721
- license = await checkLicense2();
2722
- } catch {
2723
- license = { valid: true, plan: "free", email: "", expiresAt: null, deviceLimit: 1, employeeLimit: 2, memoryLimit: 1e3 };
2724
- }
2725
- if (license.plan === "free") {
2726
- log("Your plan: Free \u2014 1 employee (your COO)");
2822
+ let cooName = "exe";
2823
+ if (!state.completedSteps.includes(6)) {
2824
+ log("=== Your Team ===");
2727
2825
  log("");
2728
- log("The CTO and CMO are available on Solopreneur ($97/mo) \u2014 full engineering");
2729
- log("and marketing team, unlimited tasks, priority support.");
2730
- log("Get your key at https://askexe.com, then paste it here.");
2826
+ log("Every install starts with a COO \u2014 your right-hand operator.");
2827
+ log("They hold the big picture: priorities, progress, and blockers.");
2828
+ log("You talk to them. They coordinate everyone else.");
2731
2829
  log("");
2732
- const licenseInput = await ask(rl, "License key (or press Enter to skip): ");
2733
- if (licenseInput.startsWith("exe_sk_")) {
2734
- saveLicense2(licenseInput);
2735
- mirrorLicenseKey2(licenseInput);
2736
- try {
2737
- license = await validateLicense2(licenseInput);
2738
- } catch {
2739
- log("Couldn't reach the license server \u2014 your key has been saved.");
2740
- log("Run exe-os --activate <key> anytime to finish activation.");
2741
- }
2742
- } else if (!licenseInput) {
2743
- log("You can activate anytime with: exe-os --activate <key>");
2744
- } else {
2745
- log("That doesn't look like a license key (should start with exe_sk_).");
2746
- log("You can activate anytime with: exe-os --activate <key>");
2830
+ const cooNameInput = await ask(rl, "Name your COO (default: exe): ");
2831
+ cooName = (cooNameInput || "exe").toLowerCase();
2832
+ let employees = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
2833
+ if (!employees.some((e) => e.name === cooName)) {
2834
+ const { DEFAULT_EXE: DEFAULT_EXE2, personalizePrompt: personalizePrompt2 } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2835
+ const cooEmployee = {
2836
+ name: cooName,
2837
+ role: "COO",
2838
+ systemPrompt: personalizePrompt2(DEFAULT_EXE2.systemPrompt, "exe", cooName),
2839
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2840
+ templateName: "exe",
2841
+ templateVersion: 1
2842
+ };
2843
+ employees = addEmployee2(employees, cooEmployee);
2844
+ await saveEmployees2(employees, EMPLOYEES_PATH2);
2845
+ }
2846
+ const cooIdentityContent = getIdentityTemplate("coo");
2847
+ if (cooIdentityContent) {
2848
+ const cooIdPath = identityPath2(cooName);
2849
+ mkdirSync3(path8.dirname(cooIdPath), { recursive: true });
2850
+ const replaced = cooIdentityContent.replace(/agent_id:\s*exe/g, `agent_id: ${cooName}`).replace(/\$\{agent_id\}/g, cooName);
2851
+ writeFileSync3(cooIdPath, replaced, "utf-8");
2747
2852
  }
2853
+ registerBinSymlinks2(cooName);
2854
+ createdEmployees.push({ name: cooName, role: "COO" });
2855
+ state.completedSteps.push(6);
2856
+ saveSetupState(state);
2857
+ } else {
2858
+ log("Step 6 already complete \u2014 skipping.");
2859
+ const roster = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
2860
+ const existingCoo = roster.find((e) => e.role === "COO");
2861
+ if (existingCoo) cooName = existingCoo.name;
2748
2862
  }
2749
- if (license.plan !== "free") {
2750
- const planName = license.plan === "pro" ? "Solopreneur" : license.plan.charAt(0).toUpperCase() + license.plan.slice(1);
2751
- log(`Your plan: ${planName} (up to ${license.employeeLimit} employees)`);
2863
+ log("");
2864
+ if (!state.completedSteps.includes(7)) {
2865
+ log("=== Meet Your Specialists ===");
2752
2866
  log("");
2753
- const createCto = await ask(rl, "Create your CTO? (Y/n): ");
2754
- if (createCto.toLowerCase() !== "n") {
2755
- const ctoNameInput = await ask(rl, "Name your CTO (default: yoshi): ");
2756
- const ctoName = (ctoNameInput || "yoshi").toLowerCase();
2757
- if (!employees.some((e) => e.name === ctoName)) {
2758
- const ctoTemplate = getEmployeeTemplate("yoshi");
2759
- const { personalizePrompt: personalizeCto } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2760
- const ctoEmployee = {
2761
- name: ctoName,
2762
- role: "CTO",
2763
- systemPrompt: personalizeCto(ctoTemplate?.systemPrompt ?? "", "yoshi", ctoName),
2764
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2765
- };
2766
- employees = addEmployee2(employees, ctoEmployee);
2767
- await saveEmployees2(employees, EMPLOYEES_PATH2);
2768
- }
2769
- const ctoIdentityContent = getIdentityTemplate("cto");
2770
- if (ctoIdentityContent) {
2771
- const ctoIdPath = identityPath2(ctoName);
2772
- mkdirSync3(path8.dirname(ctoIdPath), { recursive: true });
2773
- const replaced = ctoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${ctoName}`).replace(/\$\{agent_id\}/g, ctoName);
2774
- writeFileSync3(ctoIdPath, replaced, "utf-8");
2867
+ log("Your COO coordinates specialists. Here's who you can hire:");
2868
+ log("");
2869
+ log(" CTO (default: yoshi)");
2870
+ log(" Your head of engineering. Architecture, code reviews, tech decisions.");
2871
+ log(" Manages your projects and delegates to engineers when there's parallel work.");
2872
+ log("");
2873
+ log(" CMO (default: mari)");
2874
+ log(" Design, brand, content, SEO. Builds your visual identity, writes");
2875
+ log(" your copy, and gets you found online. Delegates to content specialists.");
2876
+ log("");
2877
+ log("Why separate roles instead of one AI that does everything?");
2878
+ log("");
2879
+ log("Memory saturates. One agent juggling architecture decisions AND landing page");
2880
+ log("copy AND CI/CD AND social media loses context on all of them. Competing");
2881
+ log("priorities \u2014 should I fix the auth bug or write the blog post? When you split");
2882
+ log("responsibilities, each specialist stays sharp because they stay focused.");
2883
+ log("Your COO connects them so nothing falls through the cracks.");
2884
+ log("");
2885
+ log("This is how real companies scale \u2014 you're just doing it with AI");
2886
+ log("instead of headcount.");
2887
+ log("");
2888
+ await ask(rl, "Press Enter to continue. ");
2889
+ state.completedSteps.push(7);
2890
+ saveSetupState(state);
2891
+ } else {
2892
+ log("Step 7 already complete \u2014 skipping.");
2893
+ }
2894
+ if (!state.completedSteps.includes(8)) {
2895
+ let employees = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
2896
+ let license;
2897
+ try {
2898
+ license = await checkLicense2();
2899
+ } catch {
2900
+ license = { valid: true, plan: "free", email: "", expiresAt: null, deviceLimit: 1, employeeLimit: 2, memoryLimit: 1e3 };
2901
+ }
2902
+ if (license.plan === "free") {
2903
+ log("Your plan: Free \u2014 1 employee (your COO)");
2904
+ log("");
2905
+ log("The CTO and CMO are available on Solopreneur ($97/mo) \u2014 full engineering");
2906
+ log("and marketing team, unlimited tasks, priority support.");
2907
+ log("Get your key at https://askexe.com, then paste it here.");
2908
+ log("");
2909
+ const licenseInput = await ask(rl, "License key (or press Enter to skip): ");
2910
+ if (licenseInput.startsWith("exe_sk_")) {
2911
+ saveLicense2(licenseInput);
2912
+ mirrorLicenseKey2(licenseInput);
2913
+ try {
2914
+ license = await validateLicense2(licenseInput);
2915
+ } catch {
2916
+ log("Couldn't reach the license server \u2014 your key has been saved.");
2917
+ log("Run exe-os --activate <key> anytime to finish activation.");
2918
+ }
2919
+ } else if (!licenseInput) {
2920
+ log("You can activate anytime with: exe-os --activate <key>");
2921
+ } else {
2922
+ log("That doesn't look like a license key (should start with exe_sk_).");
2923
+ log("You can activate anytime with: exe-os --activate <key>");
2775
2924
  }
2776
- registerBinSymlinks2(ctoName);
2777
- createdEmployees.push({ name: ctoName, role: "CTO" });
2778
- log(`Created ${ctoName} (CTO)`);
2779
2925
  }
2780
- const createCmo = await ask(rl, "Create your CMO? (Y/n): ");
2781
- if (createCmo.toLowerCase() !== "n") {
2782
- const cmoNameInput = await ask(rl, "Name your CMO (default: mari): ");
2783
- const cmoName = (cmoNameInput || "mari").toLowerCase();
2784
- if (!employees.some((e) => e.name === cmoName)) {
2785
- const cmoTemplate = getEmployeeTemplate("mari");
2786
- const { personalizePrompt: personalizeCmo } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2787
- const cmoEmployee = {
2788
- name: cmoName,
2789
- role: "CMO",
2790
- systemPrompt: personalizeCmo(cmoTemplate?.systemPrompt ?? "", "mari", cmoName),
2791
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2792
- };
2793
- employees = addEmployee2(employees, cmoEmployee);
2794
- await saveEmployees2(employees, EMPLOYEES_PATH2);
2926
+ if (license.plan !== "free") {
2927
+ const planName = license.plan === "pro" ? "Solopreneur" : license.plan.charAt(0).toUpperCase() + license.plan.slice(1);
2928
+ log(`Your plan: ${planName} (up to ${license.employeeLimit} employees)`);
2929
+ log("");
2930
+ const createCto = await ask(rl, "Create your CTO? (Y/n): ");
2931
+ if (createCto.toLowerCase() !== "n") {
2932
+ const ctoTemplate = getTemplateByRole2("CTO");
2933
+ const ctoDefault = ctoTemplate?.name ?? "cto";
2934
+ const ctoNameInput = await ask(rl, `Name your CTO (default: ${ctoDefault}): `);
2935
+ const ctoName = (ctoNameInput || ctoDefault).toLowerCase();
2936
+ if (!employees.some((e) => e.name === ctoName)) {
2937
+ const { personalizePrompt: personalizeCto } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2938
+ const ctoEmployee = {
2939
+ name: ctoName,
2940
+ role: "CTO",
2941
+ systemPrompt: personalizeCto(ctoTemplate?.systemPrompt ?? "", ctoDefault, ctoName),
2942
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2943
+ };
2944
+ employees = addEmployee2(employees, ctoEmployee);
2945
+ await saveEmployees2(employees, EMPLOYEES_PATH2);
2946
+ }
2947
+ const ctoIdentityContent = getIdentityTemplate("cto");
2948
+ if (ctoIdentityContent) {
2949
+ const ctoIdPath = identityPath2(ctoName);
2950
+ mkdirSync3(path8.dirname(ctoIdPath), { recursive: true });
2951
+ const replaced = ctoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${ctoName}`).replace(/\$\{agent_id\}/g, ctoName);
2952
+ writeFileSync3(ctoIdPath, replaced, "utf-8");
2953
+ }
2954
+ registerBinSymlinks2(ctoName);
2955
+ createdEmployees.push({ name: ctoName, role: "CTO" });
2956
+ log(`Created ${ctoName} (CTO)`);
2795
2957
  }
2796
- const cmoIdentityContent = getIdentityTemplate("cmo");
2797
- if (cmoIdentityContent) {
2798
- const cmoIdPath = identityPath2(cmoName);
2799
- mkdirSync3(path8.dirname(cmoIdPath), { recursive: true });
2800
- const replaced = cmoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${cmoName}`).replace(/\$\{agent_id\}/g, cmoName);
2801
- writeFileSync3(cmoIdPath, replaced, "utf-8");
2958
+ const createCmo = await ask(rl, "Create your CMO? (Y/n): ");
2959
+ if (createCmo.toLowerCase() !== "n") {
2960
+ const cmoTemplate = getTemplateByRole2("CMO");
2961
+ const cmoDefault = cmoTemplate?.name ?? "cmo";
2962
+ const cmoNameInput = await ask(rl, `Name your CMO (default: ${cmoDefault}): `);
2963
+ const cmoName = (cmoNameInput || cmoDefault).toLowerCase();
2964
+ if (!employees.some((e) => e.name === cmoName)) {
2965
+ const { personalizePrompt: personalizeCmo } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
2966
+ const cmoEmployee = {
2967
+ name: cmoName,
2968
+ role: "CMO",
2969
+ systemPrompt: personalizeCmo(cmoTemplate?.systemPrompt ?? "", cmoDefault, cmoName),
2970
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2971
+ };
2972
+ employees = addEmployee2(employees, cmoEmployee);
2973
+ await saveEmployees2(employees, EMPLOYEES_PATH2);
2974
+ }
2975
+ const cmoIdentityContent = getIdentityTemplate("cmo");
2976
+ if (cmoIdentityContent) {
2977
+ const cmoIdPath = identityPath2(cmoName);
2978
+ mkdirSync3(path8.dirname(cmoIdPath), { recursive: true });
2979
+ const replaced = cmoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${cmoName}`).replace(/\$\{agent_id\}/g, cmoName);
2980
+ writeFileSync3(cmoIdPath, replaced, "utf-8");
2981
+ }
2982
+ registerBinSymlinks2(cmoName);
2983
+ createdEmployees.push({ name: cmoName, role: "CMO" });
2984
+ log(`Created ${cmoName} (CMO)`);
2802
2985
  }
2803
- registerBinSymlinks2(cmoName);
2804
- createdEmployees.push({ name: cmoName, role: "CMO" });
2805
- log(`Created ${cmoName} (CMO)`);
2986
+ log("");
2806
2987
  }
2807
- log("");
2988
+ state.completedSteps.push(8);
2989
+ saveSetupState(state);
2990
+ } else {
2991
+ log("Step 8 already complete \u2014 skipping.");
2808
2992
  }
2993
+ clearSetupState();
2809
2994
  log("=== Two Ways to Work ===");
2810
2995
  log("");
2811
2996
  log(" 1. Claude Code mode");