@askexenow/exe-os 0.8.36 → 0.8.38

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 (77) hide show
  1. package/dist/bin/backfill-conversations.js +75 -61
  2. package/dist/bin/backfill-responses.js +16 -9
  3. package/dist/bin/backfill-vectors.js +1 -8
  4. package/dist/bin/cleanup-stale-review-tasks.js +1 -8
  5. package/dist/bin/cli.js +767 -348
  6. package/dist/bin/exe-assign.js +16 -9
  7. package/dist/bin/exe-boot.js +606 -42
  8. package/dist/bin/exe-call.js +9 -4
  9. package/dist/bin/exe-cloud.js +37 -3
  10. package/dist/bin/exe-dispatch.js +43 -3
  11. package/dist/bin/exe-doctor.js +1 -8
  12. package/dist/bin/exe-export-behaviors.js +11 -11
  13. package/dist/bin/exe-forget.js +1 -8
  14. package/dist/bin/exe-gateway.js +127 -37
  15. package/dist/bin/exe-heartbeat.js +6 -12
  16. package/dist/bin/exe-kill.js +8 -8
  17. package/dist/bin/exe-launch-agent.js +59 -15
  18. package/dist/bin/exe-link.js +516 -15
  19. package/dist/bin/exe-new-employee.js +35 -10
  20. package/dist/bin/exe-pending-messages.js +3 -9
  21. package/dist/bin/exe-pending-notifications.js +1 -8
  22. package/dist/bin/exe-pending-reviews.js +6 -12
  23. package/dist/bin/exe-review.js +16 -9
  24. package/dist/bin/exe-search.js +19 -12
  25. package/dist/bin/exe-session-cleanup.js +22 -14
  26. package/dist/bin/exe-status.js +1 -8
  27. package/dist/bin/exe-team.js +1 -8
  28. package/dist/bin/git-sweep.js +16 -9
  29. package/dist/bin/graph-backfill.js +8 -8
  30. package/dist/bin/graph-export.js +8 -8
  31. package/dist/bin/install.js +44 -5
  32. package/dist/bin/scan-tasks.js +16 -9
  33. package/dist/bin/setup.js +396 -245
  34. package/dist/bin/shard-migrate.js +8 -8
  35. package/dist/bin/wiki-sync.js +8 -8
  36. package/dist/gateway/index.js +85 -37
  37. package/dist/hooks/bug-report-worker.js +48 -15
  38. package/dist/hooks/commit-complete.js +16 -9
  39. package/dist/hooks/error-recall.js +21 -14
  40. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  41. package/dist/hooks/ingest-worker.js +79 -16
  42. package/dist/hooks/ingest.js +1 -1
  43. package/dist/hooks/instructions-loaded.js +17 -10
  44. package/dist/hooks/notification.js +17 -10
  45. package/dist/hooks/post-compact.js +17 -10
  46. package/dist/hooks/pre-compact.js +17 -10
  47. package/dist/hooks/pre-tool-use.js +17 -10
  48. package/dist/hooks/prompt-ingest-worker.js +28 -5
  49. package/dist/hooks/prompt-submit.js +69 -16
  50. package/dist/hooks/response-ingest-worker.js +29 -6
  51. package/dist/hooks/session-end.js +21 -14
  52. package/dist/hooks/session-start.js +21 -14
  53. package/dist/hooks/stop.js +17 -10
  54. package/dist/hooks/subagent-stop.js +17 -10
  55. package/dist/hooks/summary-worker.js +536 -22
  56. package/dist/index.js +76 -20
  57. package/dist/lib/cloud-sync.js +521 -13
  58. package/dist/lib/employee-templates.js +5 -0
  59. package/dist/lib/exe-daemon.js +97 -31
  60. package/dist/lib/hybrid-search.js +19 -12
  61. package/dist/lib/identity-templates.js +16 -7
  62. package/dist/lib/license.js +43 -2
  63. package/dist/lib/messaging.js +43 -3
  64. package/dist/lib/schedules.js +1 -8
  65. package/dist/lib/store.js +16 -9
  66. package/dist/lib/tasks.js +47 -7
  67. package/dist/lib/tmux-routing.js +45 -3
  68. package/dist/mcp/server.js +256 -128
  69. package/dist/mcp/tools/create-task.js +48 -8
  70. package/dist/mcp/tools/deactivate-behavior.js +1 -1
  71. package/dist/mcp/tools/list-tasks.js +37 -28
  72. package/dist/mcp/tools/send-message.js +46 -6
  73. package/dist/mcp/tools/update-task.js +3 -2
  74. package/dist/runtime/index.js +61 -6
  75. package/dist/tui/App.js +98 -9
  76. package/package.json +5 -3
  77. package/src/commands/exe/afk.md +116 -0
package/dist/bin/cli.js CHANGED
@@ -621,6 +621,10 @@ async function mergeHooks(packageRoot, homeDir = os3.homedir()) {
621
621
  if (!settings.hooks) {
622
622
  settings.hooks = {};
623
623
  }
624
+ if (settings.hooks && typeof settings.hooks !== "object") {
625
+ console.warn("[exe-os] Unexpected hooks schema in settings.json \u2014 skipping hook installation");
626
+ return { added: 0, skipped: 0 };
627
+ }
624
628
  const hooksToRegister = [
625
629
  {
626
630
  event: "PostToolUse",
@@ -783,6 +787,11 @@ async function mergeHooks(packageRoot, homeDir = os3.homedir()) {
783
787
  if (!settings.hooks[event]) {
784
788
  settings.hooks[event] = [];
785
789
  }
790
+ if (!Array.isArray(settings.hooks[event])) {
791
+ console.warn(`[exe-os] Hook event "${event}" has unexpected structure \u2014 skipping`);
792
+ skipped++;
793
+ continue;
794
+ }
786
795
  const existing = settings.hooks[event];
787
796
  const correctCommand = group.hooks[0]?.command ?? "";
788
797
  const alreadyCorrect = existing.some(
@@ -807,26 +816,56 @@ async function mergeHooks(packageRoot, homeDir = os3.homedir()) {
807
816
  permissions.allow = [];
808
817
  }
809
818
  const toolNames = [
819
+ // Core memory
810
820
  "store_memory",
811
821
  "recall_my_memory",
822
+ "commit_to_long_term_memory",
823
+ "consolidate_memories",
824
+ "ask_team_memory",
825
+ "get_session_context",
826
+ // Tasks
812
827
  "create_task",
813
828
  "list_tasks",
814
829
  "get_task",
815
830
  "update_task",
816
831
  "close_task",
832
+ "checkpoint_task",
833
+ // Behaviors
817
834
  "store_behavior",
818
835
  "deactivate_behavior",
819
- "send_message",
836
+ "list_behaviors",
837
+ // Identity
820
838
  "get_identity",
821
839
  "update_identity",
822
- "ask_team_memory",
823
- "get_session_context",
840
+ // Messaging
841
+ "send_message",
842
+ "acknowledge_messages",
843
+ "send_whatsapp",
844
+ "query_conversations",
845
+ // Reminders + triggers
824
846
  "create_reminder",
825
847
  "complete_reminder",
826
848
  "list_reminders",
827
- "list_behaviors",
849
+ "create_trigger",
850
+ "list_triggers",
851
+ // GraphRAG
828
852
  "query_relationships",
829
- "commit_to_long_term_memory"
853
+ "merge_entities",
854
+ // Documents + wiki
855
+ "ingest_document",
856
+ "list_documents",
857
+ "purge_document",
858
+ "rerank_documents",
859
+ "set_document_importance",
860
+ "create_wiki_page",
861
+ "update_wiki_page",
862
+ "get_wiki_page",
863
+ "list_wiki_pages",
864
+ // System
865
+ "load_skill",
866
+ "apply_starter_pack",
867
+ "resume_employee",
868
+ "deploy_client"
830
869
  ];
831
870
  const allowList = permissions.allow;
832
871
  for (const tool of expandDualPrefixTools(toolNames)) {
@@ -2024,7 +2063,7 @@ __export(shard_manager_exports, {
2024
2063
  shardExists: () => shardExists
2025
2064
  });
2026
2065
  import path6 from "path";
2027
- import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
2066
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync } from "fs";
2028
2067
  import { createClient as createClient2 } from "@libsql/client";
2029
2068
  function initShardManager(encryptionKey) {
2030
2069
  _encryptionKey = encryptionKey;
@@ -2063,8 +2102,7 @@ function shardExists(projectName) {
2063
2102
  }
2064
2103
  function listShards() {
2065
2104
  if (!existsSync6(SHARDS_DIR)) return [];
2066
- const { readdirSync: readdirSync4 } = __require("fs");
2067
- return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
2105
+ return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
2068
2106
  }
2069
2107
  async function ensureShardSchema(client) {
2070
2108
  await client.execute("PRAGMA journal_mode = WAL");
@@ -2329,7 +2367,8 @@ async function writeMemory(record) {
2329
2367
  has_error: record.has_error ? 1 : 0,
2330
2368
  raw_text: record.raw_text,
2331
2369
  vector: record.vector,
2332
- version: _nextVersion++,
2370
+ version: 0,
2371
+ // Placeholder — assigned atomically at flush time
2333
2372
  task_id: record.task_id ?? null,
2334
2373
  importance: record.importance ?? 5,
2335
2374
  status: record.status ?? "active",
@@ -2346,6 +2385,12 @@ async function writeMemory(record) {
2346
2385
  supersedes_id: record.supersedes_id ?? null
2347
2386
  };
2348
2387
  _pendingRecords.push(dbRow);
2388
+ const MAX_PENDING = 1e3;
2389
+ if (_pendingRecords.length > MAX_PENDING) {
2390
+ const dropped = _pendingRecords.length - MAX_PENDING;
2391
+ _pendingRecords = _pendingRecords.slice(-MAX_PENDING);
2392
+ console.warn(`[store] Dropped ${dropped} oldest pending records (overflow)`);
2393
+ }
2349
2394
  if (_flushTimer === null) {
2350
2395
  _flushTimer = setInterval(() => {
2351
2396
  void flushBatch();
@@ -2363,6 +2408,13 @@ async function flushBatch() {
2363
2408
  _flushing = true;
2364
2409
  try {
2365
2410
  const batch = _pendingRecords.slice(0);
2411
+ const client = getClient();
2412
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
2413
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2414
+ for (const row of batch) {
2415
+ row.version = baseVersion++;
2416
+ }
2417
+ _nextVersion = baseVersion;
2366
2418
  const buildStmt = (row) => {
2367
2419
  const hasVector = row.vector !== null;
2368
2420
  const taskId = row.task_id ?? null;
@@ -3309,65 +3361,72 @@ async function backfillConversations(options) {
3309
3361
  process.stderr.write(`[backfill-conversations] Found ${files.length} JSONL files to process
3310
3362
  `);
3311
3363
  process.env.EXE_EMBED_PRIORITY = "low";
3312
- for (const file of files) {
3313
- stats.filesScanned++;
3314
- if (existingPaths.has(file)) {
3315
- stats.skippedDedup++;
3316
- continue;
3317
- }
3318
- const conv = await parseConversation(file);
3319
- if (conv.totalMessages < MIN_MESSAGES) {
3320
- stats.skippedTooShort++;
3321
- continue;
3322
- }
3323
- const summary = buildSummary(conv);
3324
- if (options.dryRun) {
3325
- process.stdout.write(`
3364
+ const BATCH_SIZE = 50;
3365
+ const MAX_DEDUP_SIZE = 5e4;
3366
+ for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
3367
+ const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
3368
+ for (const file of batch) {
3369
+ stats.filesScanned++;
3370
+ if (existingPaths.size < MAX_DEDUP_SIZE && existingPaths.has(file)) {
3371
+ stats.skippedDedup++;
3372
+ continue;
3373
+ }
3374
+ const conv = await parseConversation(file);
3375
+ if (conv.totalMessages < MIN_MESSAGES) {
3376
+ stats.skippedTooShort++;
3377
+ continue;
3378
+ }
3379
+ const summary = buildSummary(conv);
3380
+ if (options.dryRun) {
3381
+ process.stdout.write(`
3326
3382
  \u2500\u2500\u2500 ${file} \u2500\u2500\u2500
3327
3383
  `);
3328
- process.stdout.write(`Project: ${conv.projectName} | Messages: ${conv.totalMessages}`);
3329
- process.stdout.write(` | Tools: ${Object.keys(conv.toolCounts).length}`);
3330
- process.stdout.write(` | Files: ${conv.filesTouched.size}
3384
+ process.stdout.write(`Project: ${conv.projectName} | Messages: ${conv.totalMessages}`);
3385
+ process.stdout.write(` | Tools: ${Object.keys(conv.toolCounts).length}`);
3386
+ process.stdout.write(` | Files: ${conv.filesTouched.size}
3331
3387
  `);
3332
- const firstPrompt = conv.userMessages[0];
3333
- if (firstPrompt) {
3334
- process.stdout.write(`First prompt: ${firstPrompt.slice(0, 120)}
3388
+ const firstPrompt = conv.userMessages[0];
3389
+ if (firstPrompt) {
3390
+ process.stdout.write(`First prompt: ${firstPrompt.slice(0, 120)}
3335
3391
  `);
3392
+ }
3393
+ stats.conversationsStored++;
3394
+ continue;
3395
+ }
3396
+ let vector = null;
3397
+ if (daemonConnected) {
3398
+ try {
3399
+ vector = await embedViaClient(summary, "low");
3400
+ if (!vector) stats.embedFailed++;
3401
+ } catch {
3402
+ stats.embedFailed++;
3403
+ }
3404
+ }
3405
+ await writeMemory({
3406
+ id: crypto2.randomUUID(),
3407
+ agent_id: conv.agentId,
3408
+ agent_role: conv.agentId === "exe" ? "COO" : "specialist",
3409
+ session_id: conv.sessionId,
3410
+ timestamp: conv.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
3411
+ tool_name: TOOL_NAME,
3412
+ project_name: conv.projectName,
3413
+ has_error: conv.errorCount > 0,
3414
+ raw_text: summary,
3415
+ vector,
3416
+ source_path: file,
3417
+ source_type: "conversation"
3418
+ });
3419
+ if (existingPaths.size < MAX_DEDUP_SIZE) {
3420
+ existingPaths.add(file);
3336
3421
  }
3337
3422
  stats.conversationsStored++;
3338
- continue;
3339
- }
3340
- let vector = null;
3341
- if (daemonConnected) {
3342
- try {
3343
- vector = await embedViaClient(summary, "low");
3344
- if (!vector) stats.embedFailed++;
3345
- } catch {
3346
- stats.embedFailed++;
3347
- }
3348
- }
3349
- await writeMemory({
3350
- id: crypto2.randomUUID(),
3351
- agent_id: conv.agentId,
3352
- agent_role: conv.agentId === "exe" ? "COO" : "specialist",
3353
- session_id: conv.sessionId,
3354
- timestamp: conv.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
3355
- tool_name: TOOL_NAME,
3356
- project_name: conv.projectName,
3357
- has_error: conv.errorCount > 0,
3358
- raw_text: summary,
3359
- vector,
3360
- source_path: file,
3361
- source_type: "conversation"
3362
- });
3363
- existingPaths.add(file);
3364
- stats.conversationsStored++;
3365
- if (stats.filesScanned % 50 === 0) {
3366
- process.stderr.write(
3367
- `[backfill-conversations] Progress: ${stats.filesScanned}/${files.length} files, ${stats.conversationsStored} stored
3423
+ if (stats.filesScanned % 50 === 0) {
3424
+ process.stderr.write(
3425
+ `[backfill-conversations] Progress: ${stats.filesScanned}/${files.length} files, ${stats.conversationsStored} stored
3368
3426
  `
3369
- );
3370
- await flushBatch();
3427
+ );
3428
+ await flushBatch();
3429
+ }
3371
3430
  }
3372
3431
  }
3373
3432
  if (!options.dryRun) {
@@ -3425,6 +3484,7 @@ __export(employee_templates_exports, {
3425
3484
  buildCustomEmployeePrompt: () => buildCustomEmployeePrompt,
3426
3485
  getSessionPrompt: () => getSessionPrompt,
3427
3486
  getTemplate: () => getTemplate,
3487
+ getTemplateByRole: () => getTemplateByRole,
3428
3488
  personalizePrompt: () => personalizePrompt,
3429
3489
  renderClientCOOTemplate: () => renderClientCOOTemplate
3430
3490
  });
@@ -3440,6 +3500,10 @@ function buildCustomEmployeePrompt(name, role) {
3440
3500
  function getTemplate(name) {
3441
3501
  return TEMPLATES[name];
3442
3502
  }
3503
+ function getTemplateByRole(role) {
3504
+ const lower = role.toLowerCase();
3505
+ return Object.values(TEMPLATES).find((t) => t.role.toLowerCase() === lower);
3506
+ }
3443
3507
  function personalizePrompt(prompt, templateName, actualName) {
3444
3508
  if (templateName === actualName) return prompt;
3445
3509
  const escaped = templateName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4224,48 +4288,67 @@ async function downloadModel(opts) {
4224
4288
  const tmpPath = destPath + ".tmp";
4225
4289
  await mkdir5(destDir, { recursive: true });
4226
4290
  if (existsSync9(destPath)) {
4227
- const hash2 = await fileHash(destPath);
4228
- if (hash2 === EXPECTED_SHA256) {
4291
+ const hash = await fileHash(destPath);
4292
+ if (hash === EXPECTED_SHA256) {
4229
4293
  return destPath;
4230
4294
  }
4231
4295
  }
4232
- if (existsSync9(tmpPath)) unlinkSync3(tmpPath);
4233
- const response = await fetchFn(GGUF_URL, { redirect: "follow" });
4234
- if (!response.ok || !response.body) {
4235
- throw new Error(`Download failed: HTTP ${response.status}`);
4236
- }
4237
- const contentLength = Number(response.headers.get("content-length") ?? EXPECTED_SIZE);
4296
+ const MAX_RETRIES2 = 3;
4297
+ const DOWNLOAD_TIMEOUT_MS = 3e5;
4298
+ let lastErr;
4238
4299
  let downloaded = 0;
4239
- const hash = createHash("sha256");
4240
- const fileStream = createWriteStream(tmpPath);
4241
- const reader = response.body.getReader();
4242
- try {
4243
- while (true) {
4244
- const { done, value } = await reader.read();
4245
- if (done) break;
4246
- if (!fileStream.write(value)) {
4247
- await new Promise((resolve) => fileStream.once("drain", resolve));
4300
+ for (let attempt = 1; attempt <= MAX_RETRIES2; attempt++) {
4301
+ try {
4302
+ if (existsSync9(tmpPath)) unlinkSync3(tmpPath);
4303
+ const response = await fetchFn(GGUF_URL, {
4304
+ redirect: "follow",
4305
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
4306
+ });
4307
+ if (!response.ok || !response.body) {
4308
+ throw new Error(`Download failed: HTTP ${response.status}`);
4309
+ }
4310
+ const contentLength = Number(response.headers.get("content-length") ?? EXPECTED_SIZE);
4311
+ const hash = createHash("sha256");
4312
+ const fileStream = createWriteStream(tmpPath);
4313
+ const reader = response.body.getReader();
4314
+ try {
4315
+ while (true) {
4316
+ const { done, value } = await reader.read();
4317
+ if (done) break;
4318
+ if (!fileStream.write(value)) {
4319
+ await new Promise((resolve) => fileStream.once("drain", resolve));
4320
+ }
4321
+ hash.update(value);
4322
+ downloaded += value.byteLength;
4323
+ onProgress?.(downloaded, contentLength);
4324
+ }
4325
+ } finally {
4326
+ fileStream.end();
4327
+ await new Promise((resolve, reject) => {
4328
+ fileStream.on("finish", resolve);
4329
+ fileStream.on("error", reject);
4330
+ });
4331
+ }
4332
+ const actualHash = hash.digest("hex");
4333
+ if (actualHash !== EXPECTED_SHA256) {
4334
+ unlinkSync3(tmpPath);
4335
+ throw new Error(
4336
+ `SHA256 mismatch: expected ${EXPECTED_SHA256}, got ${actualHash}`
4337
+ );
4338
+ }
4339
+ renameSync3(tmpPath, destPath);
4340
+ return destPath;
4341
+ } catch (err) {
4342
+ lastErr = err instanceof Error ? err : new Error(String(err));
4343
+ if (attempt < MAX_RETRIES2) {
4344
+ process.stderr.write(`
4345
+ Download attempt ${attempt} failed, retrying...
4346
+ `);
4347
+ if (existsSync9(tmpPath)) unlinkSync3(tmpPath);
4248
4348
  }
4249
- hash.update(value);
4250
- downloaded += value.byteLength;
4251
- onProgress?.(downloaded, contentLength);
4252
4349
  }
4253
- } finally {
4254
- fileStream.end();
4255
- await new Promise((resolve, reject) => {
4256
- fileStream.on("finish", resolve);
4257
- fileStream.on("error", reject);
4258
- });
4259
4350
  }
4260
- const actualHash = hash.digest("hex");
4261
- if (actualHash !== EXPECTED_SHA256) {
4262
- unlinkSync3(tmpPath);
4263
- throw new Error(
4264
- `SHA256 mismatch: expected ${EXPECTED_SHA256}, got ${actualHash}`
4265
- );
4266
- }
4267
- renameSync3(tmpPath, destPath);
4268
- return destPath;
4351
+ throw lastErr;
4269
4352
  }
4270
4353
  async function fileHash(filePath) {
4271
4354
  return new Promise((resolve, reject) => {
@@ -4368,6 +4451,8 @@ __export(license_exports, {
4368
4451
  loadLicense: () => loadLicense,
4369
4452
  mirrorLicenseKey: () => mirrorLicenseKey,
4370
4453
  saveLicense: () => saveLicense,
4454
+ startLicenseRevalidation: () => startLicenseRevalidation,
4455
+ stopLicenseRevalidation: () => stopLicenseRevalidation,
4371
4456
  validateLicense: () => validateLicense
4372
4457
  });
4373
4458
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync3 } from "fs";
@@ -4492,14 +4577,23 @@ async function validateLicense(apiKey, deviceId) {
4492
4577
  } catch {
4493
4578
  const cached = await getCachedLicense();
4494
4579
  if (cached) return cached;
4495
- return FREE_LICENSE;
4580
+ return { ...FREE_LICENSE, valid: false, error: "offline" };
4581
+ }
4582
+ }
4583
+ function getCacheAgeMs() {
4584
+ try {
4585
+ const { statSync: statSync2 } = __require("fs");
4586
+ const s = statSync2(CACHE_PATH);
4587
+ return Date.now() - s.mtimeMs;
4588
+ } catch {
4589
+ return Infinity;
4496
4590
  }
4497
4591
  }
4498
4592
  async function checkLicense() {
4499
4593
  const key = loadLicense();
4500
4594
  if (!key) return FREE_LICENSE;
4501
4595
  const cached = await getCachedLicense();
4502
- if (cached) return cached;
4596
+ if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
4503
4597
  const deviceId = loadDeviceId();
4504
4598
  return validateLicense(key, deviceId);
4505
4599
  }
@@ -4620,7 +4714,28 @@ async function assertVpsLicense(opts) {
4620
4714
  `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.`
4621
4715
  );
4622
4716
  }
4623
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE;
4717
+ function startLicenseRevalidation(intervalMs = 36e5) {
4718
+ if (_revalTimer) return;
4719
+ _revalTimer = setInterval(async () => {
4720
+ try {
4721
+ const license = await checkLicense();
4722
+ if (!license.valid) {
4723
+ process.stderr.write("[exe-os] License expired or invalid \u2014 features may be restricted\n");
4724
+ }
4725
+ } catch {
4726
+ }
4727
+ }, intervalMs);
4728
+ if (_revalTimer && typeof _revalTimer === "object" && "unref" in _revalTimer) {
4729
+ _revalTimer.unref();
4730
+ }
4731
+ }
4732
+ function stopLicenseRevalidation() {
4733
+ if (_revalTimer) {
4734
+ clearInterval(_revalTimer);
4735
+ _revalTimer = null;
4736
+ }
4737
+ }
4738
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS, _revalTimer;
4624
4739
  var init_license = __esm({
4625
4740
  "src/lib/license.ts"() {
4626
4741
  "use strict";
@@ -4650,6 +4765,8 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
4650
4765
  employeeLimit: 1,
4651
4766
  memoryLimit: 5e3
4652
4767
  };
4768
+ CACHE_MAX_AGE_MS = 36e5;
4769
+ _revalTimer = null;
4653
4770
  }
4654
4771
  });
4655
4772
 
@@ -4663,7 +4780,7 @@ __export(identity_exports, {
4663
4780
  updateIdentity: () => updateIdentity
4664
4781
  });
4665
4782
  import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
4666
- import { readdirSync } from "fs";
4783
+ import { readdirSync as readdirSync2 } from "fs";
4667
4784
  import path12 from "path";
4668
4785
  import { createHash as createHash2 } from "crypto";
4669
4786
  function ensureDir() {
@@ -4745,7 +4862,7 @@ async function updateIdentity(agentId, content, updatedBy) {
4745
4862
  }
4746
4863
  function listIdentities() {
4747
4864
  ensureDir();
4748
- const files = readdirSync(IDENTITY_DIR).filter((f) => f.endsWith(".md"));
4865
+ const files = readdirSync2(IDENTITY_DIR).filter((f) => f.endsWith(".md"));
4749
4866
  const results = [];
4750
4867
  for (const file of files) {
4751
4868
  const agentId = file.replace(".md", "");
@@ -4792,6 +4909,7 @@ var init_identity = __esm({
4792
4909
  var identity_templates_exports = {};
4793
4910
  __export(identity_templates_exports, {
4794
4911
  IDENTITY_TEMPLATES: () => IDENTITY_TEMPLATES,
4912
+ PLAN_MODE_COMPAT: () => PLAN_MODE_COMPAT,
4795
4913
  POST_WORK_CHECKLIST: () => POST_WORK_CHECKLIST,
4796
4914
  getTemplate: () => getTemplate2,
4797
4915
  getTemplateForTitle: () => getTemplateForTitle
@@ -4811,10 +4929,18 @@ function getTemplateForTitle(title) {
4811
4929
  if (t.includes("review") || t.includes("audit") || t.includes("qa")) return IDENTITY_TEMPLATES["staff-code-reviewer"];
4812
4930
  return null;
4813
4931
  }
4814
- var POST_WORK_CHECKLIST, IDENTITY_TEMPLATES;
4932
+ var PLAN_MODE_COMPAT, POST_WORK_CHECKLIST, IDENTITY_TEMPLATES;
4815
4933
  var init_identity_templates = __esm({
4816
4934
  "src/lib/identity-templates.ts"() {
4817
4935
  "use strict";
4936
+ PLAN_MODE_COMPAT = `
4937
+ ## Plan Mode Compatibility
4938
+ If tool execution is unavailable (e.g., CC plan mode), switch to planning:
4939
+ - Reason about the task and create a written plan
4940
+ - Document what tools you would call and with what parameters
4941
+ - Output structured text that can be acted on when tools become available
4942
+ Do not repeatedly attempt tool calls that fail \u2014 switch to planning mode.
4943
+ `;
4818
4944
  POST_WORK_CHECKLIST = `
4819
4945
  5. Check for pending reviews (list_tasks status='needs_review' where you are reviewer) \u2014 reviews are work, process before new tasks
4820
4946
  6. Check for blocked tasks (list_tasks status='blocked') \u2014 can you unblock it? Do it now. Can't? Escalate to exe immediately.
@@ -4894,7 +5020,7 @@ Never say "I have no memories" without first searching broadly. Your memory may
4894
5020
  - **update_identity** \u2014 rewrite any agent's identity when role/responsibilities change (exe/founder only)
4895
5021
  - **get_identity** \u2014 read any agent's identity for coordination
4896
5022
  - **send_message** \u2014 direct intercom to employees
4897
-
5023
+ ${PLAN_MODE_COMPAT}
4898
5024
  ## Completion Workflow
4899
5025
 
4900
5026
  1. Read the task file and verify the deliverable matches the brief
@@ -4965,7 +5091,7 @@ You are \${agent_id}. CTO. You hold deep context on the entire codebase, archite
4965
5091
  - **store_behavior** \u2014 record corrections for engineers (p0 = always injected)
4966
5092
  - **get_identity** \u2014 read any agent's identity for review context
4967
5093
  - **query_relationships** \u2014 GraphRAG entity connections for architecture analysis
4968
-
5094
+ ${PLAN_MODE_COMPAT}
4969
5095
  ## Completion Workflow
4970
5096
 
4971
5097
  1. Read ARCHITECTURE.md before starting work on any repo
@@ -5032,7 +5158,7 @@ You are \${agent_id}. CMO. You hold deep context on design, branding, storytelli
5032
5158
  - **update_task** \u2014 mark tasks done with result summary
5033
5159
  - **store_memory** \u2014 report completions with brand alignment notes, SEO considerations
5034
5160
  - **get_identity** \u2014 read team identities for brand-consistent communication
5035
-
5161
+ ${PLAN_MODE_COMPAT}
5036
5162
  ## Completion Workflow
5037
5163
 
5038
5164
  1. Read the task file and understand the brief \u2014 tone, format, channel requirements
@@ -5099,7 +5225,7 @@ You are a principal engineer. You write production-grade code with zero shortcut
5099
5225
  - **recall_my_memory** \u2014 check past work, patterns, gotchas in this project
5100
5226
  - **store_memory** \u2014 report completions for org visibility
5101
5227
  - **ask_team_memory** \u2014 pull context from colleagues when specs reference their work
5102
-
5228
+ ${PLAN_MODE_COMPAT}
5103
5229
  ## Completion Workflow
5104
5230
 
5105
5231
  1. Read ARCHITECTURE.md if it exists \u2014 understand architecture before changing anything
@@ -5159,7 +5285,7 @@ You are the content production specialist. You turn scripts and creative briefs
5159
5285
  - **update_task** \u2014 mark tasks done with result summary
5160
5286
  - **recall_my_memory** \u2014 check past work: which models worked, which prompts produced good results
5161
5287
  - **store_memory** \u2014 report completions with production decisions for future reference
5162
-
5288
+ ${PLAN_MODE_COMPAT}
5163
5289
  ## Completion Workflow
5164
5290
 
5165
5291
  1. Read the task file \u2014 understand the brief, check budget constraints
@@ -5231,7 +5357,7 @@ You are the AI Product Lead \u2014 the competitive intelligence engine. You stud
5231
5357
  - **update_task** \u2014 mark tasks done with analysis results
5232
5358
  - **store_memory** \u2014 persist competitive analyses, evaluations, recommendations
5233
5359
  - **create_task** \u2014 when a feature is worth building, spec it for the CTO
5234
-
5360
+ ${PLAN_MODE_COMPAT}
5235
5361
  ## Completion Workflow
5236
5362
 
5237
5363
  1. Read the task \u2014 understand what capability is needed
@@ -5294,7 +5420,7 @@ You are \${agent_id}. Staff Code Reviewer and System Auditor. Last line of defen
5294
5420
  - **store_behavior** \u2014 record new patterns
5295
5421
  - **update_task** \u2014 mark reviews done with structured findings
5296
5422
  - **create_task** \u2014 assign fixes to the CTO
5297
-
5423
+ ${PLAN_MODE_COMPAT}
5298
5424
  ## Completion Workflow
5299
5425
 
5300
5426
  1. Read the task brief and understand the audit scope
@@ -5316,10 +5442,27 @@ __export(setup_wizard_exports, {
5316
5442
  validateModel: () => validateModel
5317
5443
  });
5318
5444
  import crypto3 from "crypto";
5319
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
5445
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
5320
5446
  import os4 from "os";
5321
5447
  import path13 from "path";
5322
5448
  import { createInterface as createInterface2 } from "readline";
5449
+ function loadSetupState() {
5450
+ try {
5451
+ return JSON.parse(readFileSync8(SETUP_STATE_PATH, "utf8"));
5452
+ } catch {
5453
+ return { completedSteps: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
5454
+ }
5455
+ }
5456
+ function saveSetupState(state) {
5457
+ mkdirSync5(path13.dirname(SETUP_STATE_PATH), { recursive: true });
5458
+ writeFileSync4(SETUP_STATE_PATH, JSON.stringify(state, null, 2));
5459
+ }
5460
+ function clearSetupState() {
5461
+ try {
5462
+ unlinkSync4(SETUP_STATE_PATH);
5463
+ } catch {
5464
+ }
5465
+ }
5323
5466
  function ask(rl, prompt) {
5324
5467
  return new Promise((resolve) => {
5325
5468
  const doAsk = () => {
@@ -5359,88 +5502,122 @@ async function runSetupWizard(opts = {}) {
5359
5502
  rl.close();
5360
5503
  return;
5361
5504
  }
5505
+ const state = loadSetupState();
5506
+ if (state.completedSteps.length > 0) {
5507
+ log(`Resuming setup from step ${Math.max(...state.completedSteps) + 1}...`);
5508
+ }
5362
5509
  if (existsSync12(LEGACY_LANCE_PATH)) {
5363
5510
  log("\u26A0 Found v1.0 LanceDB at ~/.exe-os/local.lance");
5364
5511
  log(" v1.1 uses libSQL (SQLite). Your existing memories are not automatically migrated.");
5365
5512
  log(" The old directory will not be modified or deleted.");
5366
5513
  log("");
5367
5514
  }
5368
- const existingKey = await getMasterKey();
5369
- if (existingKey) {
5370
- log("Encryption key already exists \u2014 skipping generation.");
5515
+ if (!state.completedSteps.includes(1)) {
5516
+ const existingKey = await getMasterKey();
5517
+ if (existingKey) {
5518
+ log("Encryption key already exists \u2014 skipping generation.");
5519
+ } else {
5520
+ log("Generating 256-bit encryption key...");
5521
+ const key = crypto3.randomBytes(32);
5522
+ await setMasterKey(key);
5523
+ log("Encryption key generated and stored securely.");
5524
+ }
5525
+ state.completedSteps.push(1);
5526
+ saveSetupState(state);
5371
5527
  } else {
5372
- log("Generating 256-bit encryption key...");
5373
- const key = crypto3.randomBytes(32);
5374
- await setMasterKey(key);
5375
- log("Encryption key generated and stored securely.");
5528
+ log("Step 1 already complete \u2014 skipping.");
5376
5529
  }
5377
5530
  log("");
5378
- log("Exe Cloud: your memories are end-to-end encrypted, compressed, and");
5379
- log("backed up on Exe Cloud. Free for all plans. We can't read your data \u2014");
5380
- log("only your encryption key can decrypt it.");
5381
5531
  let cloudConfig;
5382
- try {
5383
- const { loadDeviceId: loadDeviceId2 } = await Promise.resolve().then(() => (init_license(), license_exports));
5384
- const deviceId = loadDeviceId2();
5385
- const res = await fetch("https://askexe.com/cloud/auth/auto-provision", {
5386
- method: "POST",
5387
- headers: { "Content-Type": "application/json" },
5388
- body: JSON.stringify({ deviceId }),
5389
- signal: AbortSignal.timeout(1e4)
5390
- });
5391
- if (res.ok) {
5392
- const data = await res.json();
5393
- if (data.apiKey) {
5394
- cloudConfig = { apiKey: data.apiKey, endpoint: "https://askexe.com/cloud" };
5395
- const { saveLicense: saveLicense3, mirrorLicenseKey: mirrorLicenseKey3 } = await Promise.resolve().then(() => (init_license(), license_exports));
5396
- saveLicense3(data.apiKey);
5397
- mirrorLicenseKey3(data.apiKey);
5398
- log("Cloud sync activated automatically.");
5532
+ if (!state.completedSteps.includes(2)) {
5533
+ log("Exe Cloud: your memories are end-to-end encrypted, compressed, and");
5534
+ log("backed up on Exe Cloud. Free for all plans. We can't read your data \u2014");
5535
+ log("only your encryption key can decrypt it.");
5536
+ try {
5537
+ const { loadDeviceId: loadDeviceId2 } = await Promise.resolve().then(() => (init_license(), license_exports));
5538
+ const deviceId = loadDeviceId2();
5539
+ const res = await fetch("https://askexe.com/cloud/auth/auto-provision", {
5540
+ method: "POST",
5541
+ headers: { "Content-Type": "application/json" },
5542
+ body: JSON.stringify({ deviceId }),
5543
+ signal: AbortSignal.timeout(1e4)
5544
+ });
5545
+ if (res.ok) {
5546
+ const data = await res.json();
5547
+ if (data.apiKey) {
5548
+ cloudConfig = { apiKey: data.apiKey, endpoint: "https://askexe.com/cloud" };
5549
+ const { saveLicense: saveLicense3, mirrorLicenseKey: mirrorLicenseKey3 } = await Promise.resolve().then(() => (init_license(), license_exports));
5550
+ saveLicense3(data.apiKey);
5551
+ mirrorLicenseKey3(data.apiKey);
5552
+ log("Cloud sync activated automatically.");
5553
+ }
5399
5554
  }
5555
+ } catch {
5556
+ log("Cloud sync will activate when online.");
5400
5557
  }
5401
- } catch {
5402
- log("Cloud sync will activate when online.");
5558
+ state.completedSteps.push(2);
5559
+ saveSetupState(state);
5560
+ } else {
5561
+ log("Step 2 already complete \u2014 skipping.");
5403
5562
  }
5404
5563
  log("");
5405
- if (!skipModel) {
5406
- log("Note: jina-embeddings-v5-text-small is licensed CC-BY-NC-4.0 (non-commercial)");
5407
- log("");
5408
- await downloadModel({
5409
- destDir: MODELS_DIR,
5410
- onProgress: (downloaded, total) => {
5411
- const pct = (downloaded / total * 100).toFixed(1);
5412
- const dlMB = (downloaded / 1e6).toFixed(0);
5413
- const totalMB = (total / 1e6).toFixed(0);
5414
- process.stderr.write(`\rDownloading model: ${pct}% (${dlMB}/${totalMB} MB)`);
5415
- }
5416
- });
5417
- process.stderr.write("\n");
5418
- log("Model downloaded and verified.");
5564
+ if (!state.completedSteps.includes(3)) {
5565
+ if (!skipModel) {
5566
+ log("Note: jina-embeddings-v5-text-small is licensed CC-BY-NC-4.0 (non-commercial)");
5567
+ log("");
5568
+ await downloadModel({
5569
+ destDir: MODELS_DIR,
5570
+ onProgress: (downloaded, total) => {
5571
+ const pct = (downloaded / total * 100).toFixed(1);
5572
+ const dlMB = (downloaded / 1e6).toFixed(0);
5573
+ const totalMB = (total / 1e6).toFixed(0);
5574
+ process.stderr.write(`\rDownloading model: ${pct}% (${dlMB}/${totalMB} MB)`);
5575
+ }
5576
+ });
5577
+ process.stderr.write("\n");
5578
+ log("Model downloaded and verified.");
5579
+ }
5580
+ state.completedSteps.push(3);
5581
+ saveSetupState(state);
5582
+ } else {
5583
+ log("Step 3 already complete \u2014 skipping.");
5419
5584
  }
5420
- if (!skipModel && !skipModelValidation) {
5421
- await validateModel(log);
5585
+ if (!state.completedSteps.includes(4)) {
5586
+ if (!skipModel && !skipModelValidation) {
5587
+ await validateModel(log);
5588
+ }
5589
+ state.completedSteps.push(4);
5590
+ saveSetupState(state);
5591
+ } else {
5592
+ log("Step 4 already complete \u2014 skipping.");
5422
5593
  }
5423
5594
  const config = await loadConfig();
5424
- if (cloudConfig) {
5425
- config.cloud = cloudConfig;
5426
- }
5427
- await saveConfig(config);
5428
- log("");
5429
- try {
5430
- const claudeJsonPath = path13.join(os4.homedir(), ".claude.json");
5431
- let claudeJson = {};
5595
+ if (!state.completedSteps.includes(5)) {
5596
+ if (cloudConfig) {
5597
+ config.cloud = cloudConfig;
5598
+ }
5599
+ await saveConfig(config);
5600
+ log("");
5432
5601
  try {
5433
- claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
5602
+ const claudeJsonPath = path13.join(os4.homedir(), ".claude.json");
5603
+ let claudeJson = {};
5604
+ try {
5605
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
5606
+ } catch {
5607
+ }
5608
+ if (!claudeJson.projects) claudeJson.projects = {};
5609
+ const projects = claudeJson.projects;
5610
+ for (const dir of [process.cwd(), os4.homedir()]) {
5611
+ if (!projects[dir]) projects[dir] = {};
5612
+ projects[dir].hasTrustDialogAccepted = true;
5613
+ }
5614
+ writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5434
5615
  } catch {
5435
5616
  }
5436
- if (!claudeJson.projects) claudeJson.projects = {};
5437
- const projects = claudeJson.projects;
5438
- for (const dir of [process.cwd(), os4.homedir()]) {
5439
- if (!projects[dir]) projects[dir] = {};
5440
- projects[dir].hasTrustDialogAccepted = true;
5441
- }
5442
- writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5443
- } catch {
5617
+ state.completedSteps.push(5);
5618
+ saveSetupState(state);
5619
+ } else {
5620
+ log("Step 5 already complete \u2014 skipping.");
5444
5621
  }
5445
5622
  const {
5446
5623
  loadEmployees: loadEmployees2,
@@ -5449,7 +5626,7 @@ async function runSetupWizard(opts = {}) {
5449
5626
  registerBinSymlinks: registerBinSymlinks2,
5450
5627
  EMPLOYEES_PATH: EMPLOYEES_PATH2
5451
5628
  } = await Promise.resolve().then(() => (init_employees(), employees_exports));
5452
- const { getTemplate: getEmployeeTemplate } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5629
+ const { getTemplateByRole: getTemplateByRole2 } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5453
5630
  const { identityPath: identityPath2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
5454
5631
  const { getTemplate: getIdentityTemplate } = await Promise.resolve().then(() => (init_identity_templates(), identity_templates_exports));
5455
5632
  const {
@@ -5459,152 +5636,178 @@ async function runSetupWizard(opts = {}) {
5459
5636
  validateLicense: validateLicense2
5460
5637
  } = await Promise.resolve().then(() => (init_license(), license_exports));
5461
5638
  const createdEmployees = [];
5462
- log("=== Your Team ===");
5463
- log("");
5464
- log("Every install starts with a COO \u2014 your right-hand operator.");
5465
- log("They hold the big picture: priorities, progress, and blockers.");
5466
- log("You talk to them. They coordinate everyone else.");
5467
- log("");
5468
- const cooNameInput = await ask(rl, "Name your COO (default: exe): ");
5469
- const cooName = (cooNameInput || "exe").toLowerCase();
5470
- let employees = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
5471
- if (!employees.some((e) => e.name === cooName)) {
5472
- const { DEFAULT_EXE: DEFAULT_EXE2, personalizePrompt: personalizePrompt2 } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5473
- const cooEmployee = {
5474
- name: cooName,
5475
- role: "COO",
5476
- systemPrompt: personalizePrompt2(DEFAULT_EXE2.systemPrompt, "exe", cooName),
5477
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5478
- templateName: "exe",
5479
- templateVersion: 1
5480
- };
5481
- employees = addEmployee2(employees, cooEmployee);
5482
- await saveEmployees2(employees, EMPLOYEES_PATH2);
5483
- }
5484
- const cooIdentityContent = getIdentityTemplate("coo");
5485
- if (cooIdentityContent) {
5486
- const cooIdPath = identityPath2(cooName);
5487
- mkdirSync5(path13.dirname(cooIdPath), { recursive: true });
5488
- const replaced = cooIdentityContent.replace(/agent_id:\s*exe/g, `agent_id: ${cooName}`).replace(/\$\{agent_id\}/g, cooName);
5489
- writeFileSync4(cooIdPath, replaced, "utf-8");
5490
- }
5491
- registerBinSymlinks2(cooName);
5492
- createdEmployees.push({ name: cooName, role: "COO" });
5493
- log("");
5494
- log("=== Meet Your Specialists ===");
5495
- log("");
5496
- log("Your COO coordinates specialists. Here's who you can hire:");
5497
- log("");
5498
- log(" CTO (default: yoshi)");
5499
- log(" Your head of engineering. Architecture, code reviews, tech decisions.");
5500
- log(" Manages your projects and delegates to engineers when there's parallel work.");
5501
- log("");
5502
- log(" CMO (default: mari)");
5503
- log(" Design, brand, content, SEO. Builds your visual identity, writes");
5504
- log(" your copy, and gets you found online. Delegates to content specialists.");
5505
- log("");
5506
- log("Why separate roles instead of one AI that does everything?");
5507
- log("");
5508
- log("Memory saturates. One agent juggling architecture decisions AND landing page");
5509
- log("copy AND CI/CD AND social media loses context on all of them. Competing");
5510
- log("priorities \u2014 should I fix the auth bug or write the blog post? When you split");
5511
- log("responsibilities, each specialist stays sharp because they stay focused.");
5512
- log("Your COO connects them so nothing falls through the cracks.");
5513
- log("");
5514
- log("This is how real companies scale \u2014 you're just doing it with AI");
5515
- log("instead of headcount.");
5516
- log("");
5517
- await ask(rl, "Press Enter to continue. ");
5518
- let license;
5519
- try {
5520
- license = await checkLicense2();
5521
- } catch {
5522
- license = { valid: true, plan: "free", email: "", expiresAt: null, deviceLimit: 1, employeeLimit: 2, memoryLimit: 1e3 };
5523
- }
5524
- if (license.plan === "free") {
5525
- log("Your plan: Free \u2014 1 employee (your COO)");
5639
+ let cooName = "exe";
5640
+ if (!state.completedSteps.includes(6)) {
5641
+ log("=== Your Team ===");
5526
5642
  log("");
5527
- log("The CTO and CMO are available on Solopreneur ($97/mo) \u2014 full engineering");
5528
- log("and marketing team, unlimited tasks, priority support.");
5529
- log("Get your key at https://askexe.com, then paste it here.");
5643
+ log("Every install starts with a COO \u2014 your right-hand operator.");
5644
+ log("They hold the big picture: priorities, progress, and blockers.");
5645
+ log("You talk to them. They coordinate everyone else.");
5530
5646
  log("");
5531
- const licenseInput = await ask(rl, "License key (or press Enter to skip): ");
5532
- if (licenseInput.startsWith("exe_sk_")) {
5533
- saveLicense2(licenseInput);
5534
- mirrorLicenseKey2(licenseInput);
5535
- try {
5536
- license = await validateLicense2(licenseInput);
5537
- } catch {
5538
- log("Couldn't reach the license server \u2014 your key has been saved.");
5539
- log("Run exe-os --activate <key> anytime to finish activation.");
5540
- }
5541
- } else if (!licenseInput) {
5542
- log("You can activate anytime with: exe-os --activate <key>");
5543
- } else {
5544
- log("That doesn't look like a license key (should start with exe_sk_).");
5545
- log("You can activate anytime with: exe-os --activate <key>");
5546
- }
5647
+ const cooNameInput = await ask(rl, "Name your COO (default: exe): ");
5648
+ cooName = (cooNameInput || "exe").toLowerCase();
5649
+ let employees = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
5650
+ if (!employees.some((e) => e.name === cooName)) {
5651
+ const { DEFAULT_EXE: DEFAULT_EXE2, personalizePrompt: personalizePrompt2 } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5652
+ const cooEmployee = {
5653
+ name: cooName,
5654
+ role: "COO",
5655
+ systemPrompt: personalizePrompt2(DEFAULT_EXE2.systemPrompt, "exe", cooName),
5656
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5657
+ templateName: "exe",
5658
+ templateVersion: 1
5659
+ };
5660
+ employees = addEmployee2(employees, cooEmployee);
5661
+ await saveEmployees2(employees, EMPLOYEES_PATH2);
5662
+ }
5663
+ const cooIdentityContent = getIdentityTemplate("coo");
5664
+ if (cooIdentityContent) {
5665
+ const cooIdPath = identityPath2(cooName);
5666
+ mkdirSync5(path13.dirname(cooIdPath), { recursive: true });
5667
+ const replaced = cooIdentityContent.replace(/agent_id:\s*exe/g, `agent_id: ${cooName}`).replace(/\$\{agent_id\}/g, cooName);
5668
+ writeFileSync4(cooIdPath, replaced, "utf-8");
5669
+ }
5670
+ registerBinSymlinks2(cooName);
5671
+ createdEmployees.push({ name: cooName, role: "COO" });
5672
+ state.completedSteps.push(6);
5673
+ saveSetupState(state);
5674
+ } else {
5675
+ log("Step 6 already complete \u2014 skipping.");
5676
+ const roster = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
5677
+ const existingCoo = roster.find((e) => e.role === "COO");
5678
+ if (existingCoo) cooName = existingCoo.name;
5547
5679
  }
5548
- if (license.plan !== "free") {
5549
- const planName = license.plan === "pro" ? "Solopreneur" : license.plan.charAt(0).toUpperCase() + license.plan.slice(1);
5550
- log(`Your plan: ${planName} (up to ${license.employeeLimit} employees)`);
5680
+ log("");
5681
+ if (!state.completedSteps.includes(7)) {
5682
+ log("=== Meet Your Specialists ===");
5551
5683
  log("");
5552
- const createCto = await ask(rl, "Create your CTO? (Y/n): ");
5553
- if (createCto.toLowerCase() !== "n") {
5554
- const ctoNameInput = await ask(rl, "Name your CTO (default: yoshi): ");
5555
- const ctoName = (ctoNameInput || "yoshi").toLowerCase();
5556
- if (!employees.some((e) => e.name === ctoName)) {
5557
- const ctoTemplate = getEmployeeTemplate("yoshi");
5558
- const { personalizePrompt: personalizeCto } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5559
- const ctoEmployee = {
5560
- name: ctoName,
5561
- role: "CTO",
5562
- systemPrompt: personalizeCto(ctoTemplate?.systemPrompt ?? "", "yoshi", ctoName),
5563
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
5564
- };
5565
- employees = addEmployee2(employees, ctoEmployee);
5566
- await saveEmployees2(employees, EMPLOYEES_PATH2);
5567
- }
5568
- const ctoIdentityContent = getIdentityTemplate("cto");
5569
- if (ctoIdentityContent) {
5570
- const ctoIdPath = identityPath2(ctoName);
5571
- mkdirSync5(path13.dirname(ctoIdPath), { recursive: true });
5572
- const replaced = ctoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${ctoName}`).replace(/\$\{agent_id\}/g, ctoName);
5573
- writeFileSync4(ctoIdPath, replaced, "utf-8");
5574
- }
5575
- registerBinSymlinks2(ctoName);
5576
- createdEmployees.push({ name: ctoName, role: "CTO" });
5577
- log(`Created ${ctoName} (CTO)`);
5578
- }
5579
- const createCmo = await ask(rl, "Create your CMO? (Y/n): ");
5580
- if (createCmo.toLowerCase() !== "n") {
5581
- const cmoNameInput = await ask(rl, "Name your CMO (default: mari): ");
5582
- const cmoName = (cmoNameInput || "mari").toLowerCase();
5583
- if (!employees.some((e) => e.name === cmoName)) {
5584
- const cmoTemplate = getEmployeeTemplate("mari");
5585
- const { personalizePrompt: personalizeCmo } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5586
- const cmoEmployee = {
5587
- name: cmoName,
5588
- role: "CMO",
5589
- systemPrompt: personalizeCmo(cmoTemplate?.systemPrompt ?? "", "mari", cmoName),
5590
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
5591
- };
5592
- employees = addEmployee2(employees, cmoEmployee);
5593
- await saveEmployees2(employees, EMPLOYEES_PATH2);
5594
- }
5595
- const cmoIdentityContent = getIdentityTemplate("cmo");
5596
- if (cmoIdentityContent) {
5597
- const cmoIdPath = identityPath2(cmoName);
5598
- mkdirSync5(path13.dirname(cmoIdPath), { recursive: true });
5599
- const replaced = cmoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${cmoName}`).replace(/\$\{agent_id\}/g, cmoName);
5600
- writeFileSync4(cmoIdPath, replaced, "utf-8");
5684
+ log("Your COO coordinates specialists. Here's who you can hire:");
5685
+ log("");
5686
+ log(" CTO (default: yoshi)");
5687
+ log(" Your head of engineering. Architecture, code reviews, tech decisions.");
5688
+ log(" Manages your projects and delegates to engineers when there's parallel work.");
5689
+ log("");
5690
+ log(" CMO (default: mari)");
5691
+ log(" Design, brand, content, SEO. Builds your visual identity, writes");
5692
+ log(" your copy, and gets you found online. Delegates to content specialists.");
5693
+ log("");
5694
+ log("Why separate roles instead of one AI that does everything?");
5695
+ log("");
5696
+ log("Memory saturates. One agent juggling architecture decisions AND landing page");
5697
+ log("copy AND CI/CD AND social media loses context on all of them. Competing");
5698
+ log("priorities \u2014 should I fix the auth bug or write the blog post? When you split");
5699
+ log("responsibilities, each specialist stays sharp because they stay focused.");
5700
+ log("Your COO connects them so nothing falls through the cracks.");
5701
+ log("");
5702
+ log("This is how real companies scale \u2014 you're just doing it with AI");
5703
+ log("instead of headcount.");
5704
+ log("");
5705
+ await ask(rl, "Press Enter to continue. ");
5706
+ state.completedSteps.push(7);
5707
+ saveSetupState(state);
5708
+ } else {
5709
+ log("Step 7 already complete \u2014 skipping.");
5710
+ }
5711
+ if (!state.completedSteps.includes(8)) {
5712
+ let employees = await loadEmployees2(EMPLOYEES_PATH2).catch(() => []);
5713
+ let license;
5714
+ try {
5715
+ license = await checkLicense2();
5716
+ } catch {
5717
+ license = { valid: true, plan: "free", email: "", expiresAt: null, deviceLimit: 1, employeeLimit: 2, memoryLimit: 1e3 };
5718
+ }
5719
+ if (license.plan === "free") {
5720
+ log("Your plan: Free \u2014 1 employee (your COO)");
5721
+ log("");
5722
+ log("The CTO and CMO are available on Solopreneur ($97/mo) \u2014 full engineering");
5723
+ log("and marketing team, unlimited tasks, priority support.");
5724
+ log("Get your key at https://askexe.com, then paste it here.");
5725
+ log("");
5726
+ const licenseInput = await ask(rl, "License key (or press Enter to skip): ");
5727
+ if (licenseInput.startsWith("exe_sk_")) {
5728
+ saveLicense2(licenseInput);
5729
+ mirrorLicenseKey2(licenseInput);
5730
+ try {
5731
+ license = await validateLicense2(licenseInput);
5732
+ } catch {
5733
+ log("Couldn't reach the license server \u2014 your key has been saved.");
5734
+ log("Run exe-os --activate <key> anytime to finish activation.");
5735
+ }
5736
+ } else if (!licenseInput) {
5737
+ log("You can activate anytime with: exe-os --activate <key>");
5738
+ } else {
5739
+ log("That doesn't look like a license key (should start with exe_sk_).");
5740
+ log("You can activate anytime with: exe-os --activate <key>");
5741
+ }
5742
+ }
5743
+ if (license.plan !== "free") {
5744
+ const planName = license.plan === "pro" ? "Solopreneur" : license.plan.charAt(0).toUpperCase() + license.plan.slice(1);
5745
+ log(`Your plan: ${planName} (up to ${license.employeeLimit} employees)`);
5746
+ log("");
5747
+ const createCto = await ask(rl, "Create your CTO? (Y/n): ");
5748
+ if (createCto.toLowerCase() !== "n") {
5749
+ const ctoTemplate = getTemplateByRole2("CTO");
5750
+ const ctoDefault = ctoTemplate?.name ?? "cto";
5751
+ const ctoNameInput = await ask(rl, `Name your CTO (default: ${ctoDefault}): `);
5752
+ const ctoName = (ctoNameInput || ctoDefault).toLowerCase();
5753
+ if (!employees.some((e) => e.name === ctoName)) {
5754
+ const { personalizePrompt: personalizeCto } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5755
+ const ctoEmployee = {
5756
+ name: ctoName,
5757
+ role: "CTO",
5758
+ systemPrompt: personalizeCto(ctoTemplate?.systemPrompt ?? "", ctoDefault, ctoName),
5759
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5760
+ };
5761
+ employees = addEmployee2(employees, ctoEmployee);
5762
+ await saveEmployees2(employees, EMPLOYEES_PATH2);
5763
+ }
5764
+ const ctoIdentityContent = getIdentityTemplate("cto");
5765
+ if (ctoIdentityContent) {
5766
+ const ctoIdPath = identityPath2(ctoName);
5767
+ mkdirSync5(path13.dirname(ctoIdPath), { recursive: true });
5768
+ const replaced = ctoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${ctoName}`).replace(/\$\{agent_id\}/g, ctoName);
5769
+ writeFileSync4(ctoIdPath, replaced, "utf-8");
5770
+ }
5771
+ registerBinSymlinks2(ctoName);
5772
+ createdEmployees.push({ name: ctoName, role: "CTO" });
5773
+ log(`Created ${ctoName} (CTO)`);
5774
+ }
5775
+ const createCmo = await ask(rl, "Create your CMO? (Y/n): ");
5776
+ if (createCmo.toLowerCase() !== "n") {
5777
+ const cmoTemplate = getTemplateByRole2("CMO");
5778
+ const cmoDefault = cmoTemplate?.name ?? "cmo";
5779
+ const cmoNameInput = await ask(rl, `Name your CMO (default: ${cmoDefault}): `);
5780
+ const cmoName = (cmoNameInput || cmoDefault).toLowerCase();
5781
+ if (!employees.some((e) => e.name === cmoName)) {
5782
+ const { personalizePrompt: personalizeCmo } = await Promise.resolve().then(() => (init_employee_templates(), employee_templates_exports));
5783
+ const cmoEmployee = {
5784
+ name: cmoName,
5785
+ role: "CMO",
5786
+ systemPrompt: personalizeCmo(cmoTemplate?.systemPrompt ?? "", cmoDefault, cmoName),
5787
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5788
+ };
5789
+ employees = addEmployee2(employees, cmoEmployee);
5790
+ await saveEmployees2(employees, EMPLOYEES_PATH2);
5791
+ }
5792
+ const cmoIdentityContent = getIdentityTemplate("cmo");
5793
+ if (cmoIdentityContent) {
5794
+ const cmoIdPath = identityPath2(cmoName);
5795
+ mkdirSync5(path13.dirname(cmoIdPath), { recursive: true });
5796
+ const replaced = cmoIdentityContent.replace(/agent_id:\s*\w+/g, `agent_id: ${cmoName}`).replace(/\$\{agent_id\}/g, cmoName);
5797
+ writeFileSync4(cmoIdPath, replaced, "utf-8");
5798
+ }
5799
+ registerBinSymlinks2(cmoName);
5800
+ createdEmployees.push({ name: cmoName, role: "CMO" });
5801
+ log(`Created ${cmoName} (CMO)`);
5601
5802
  }
5602
- registerBinSymlinks2(cmoName);
5603
- createdEmployees.push({ name: cmoName, role: "CMO" });
5604
- log(`Created ${cmoName} (CMO)`);
5803
+ log("");
5605
5804
  }
5606
- log("");
5805
+ state.completedSteps.push(8);
5806
+ saveSetupState(state);
5807
+ } else {
5808
+ log("Step 8 already complete \u2014 skipping.");
5607
5809
  }
5810
+ clearSetupState();
5608
5811
  log("=== Two Ways to Work ===");
5609
5812
  log("");
5610
5813
  log(" 1. Claude Code mode");
@@ -5641,12 +5844,14 @@ async function runSetupWizard(opts = {}) {
5641
5844
  rl.close();
5642
5845
  }
5643
5846
  }
5847
+ var SETUP_STATE_PATH;
5644
5848
  var init_setup_wizard = __esm({
5645
5849
  "src/lib/setup-wizard.ts"() {
5646
5850
  "use strict";
5647
5851
  init_config();
5648
5852
  init_keychain();
5649
5853
  init_model_downloader();
5854
+ SETUP_STATE_PATH = path13.join(os4.homedir(), ".exe-os", "setup-state.json");
5650
5855
  }
5651
5856
  });
5652
5857
 
@@ -16316,8 +16521,8 @@ import path23 from "path";
16316
16521
  import os8 from "os";
16317
16522
  import {
16318
16523
  readFileSync as readFileSync13,
16319
- readdirSync as readdirSync2,
16320
- unlinkSync as unlinkSync4,
16524
+ readdirSync as readdirSync3,
16525
+ unlinkSync as unlinkSync5,
16321
16526
  existsSync as existsSync17,
16322
16527
  rmdirSync
16323
16528
  } from "fs";
@@ -16799,7 +17004,7 @@ var init_tasks_crud = __esm({
16799
17004
 
16800
17005
  // src/lib/tasks-review.ts
16801
17006
  import path25 from "path";
16802
- import { existsSync as existsSync19, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
17007
+ import { existsSync as existsSync19, readdirSync as readdirSync4, unlinkSync as unlinkSync6 } from "fs";
16803
17008
  async function countPendingReviews() {
16804
17009
  const client = getClient();
16805
17010
  const result = await client.execute({
@@ -16921,9 +17126,9 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
16921
17126
  try {
16922
17127
  const cacheDir = path25.join(EXE_AI_DIR, "session-cache");
16923
17128
  if (existsSync19(cacheDir)) {
16924
- for (const f of readdirSync3(cacheDir)) {
17129
+ for (const f of readdirSync4(cacheDir)) {
16925
17130
  if (f.startsWith("review-notified-")) {
16926
- unlinkSync5(path25.join(cacheDir, f));
17131
+ unlinkSync6(path25.join(cacheDir, f));
16927
17132
  }
16928
17133
  }
16929
17134
  }
@@ -17524,7 +17729,7 @@ __export(tasks_exports, {
17524
17729
  writeCheckpoint: () => writeCheckpoint
17525
17730
  });
17526
17731
  import path28 from "path";
17527
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, unlinkSync as unlinkSync6 } from "fs";
17732
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, unlinkSync as unlinkSync7 } from "fs";
17528
17733
  async function createTask(input) {
17529
17734
  const result = await createTaskCore(input);
17530
17735
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -17550,7 +17755,7 @@ async function updateTask(input) {
17550
17755
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
17551
17756
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
17552
17757
  try {
17553
- unlinkSync6(cachePath);
17758
+ unlinkSync7(cachePath);
17554
17759
  } catch {
17555
17760
  }
17556
17761
  }
@@ -17943,6 +18148,7 @@ var init_capacity_monitor = __esm({
17943
18148
  // src/lib/tmux-routing.ts
17944
18149
  var tmux_routing_exports = {};
17945
18150
  __export(tmux_routing_exports, {
18151
+ acquireSpawnLock: () => acquireSpawnLock2,
17946
18152
  employeeSessionName: () => employeeSessionName,
17947
18153
  ensureEmployee: () => ensureEmployee,
17948
18154
  extractRootExe: () => extractRootExe,
@@ -17957,6 +18163,7 @@ __export(tmux_routing_exports, {
17957
18163
  notifyParentExe: () => notifyParentExe,
17958
18164
  parseParentExe: () => parseParentExe,
17959
18165
  registerParentExe: () => registerParentExe,
18166
+ releaseSpawnLock: () => releaseSpawnLock2,
17960
18167
  resolveExeSession: () => resolveExeSession,
17961
18168
  sendIntercom: () => sendIntercom,
17962
18169
  spawnEmployee: () => spawnEmployee,
@@ -17967,6 +18174,42 @@ import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, mkdirS
17967
18174
  import path29 from "path";
17968
18175
  import os9 from "os";
17969
18176
  import { fileURLToPath as fileURLToPath4 } from "url";
18177
+ import { unlinkSync as unlinkSync8 } from "fs";
18178
+ function spawnLockPath(sessionName) {
18179
+ return path29.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
18180
+ }
18181
+ function isProcessAlive(pid) {
18182
+ try {
18183
+ process.kill(pid, 0);
18184
+ return true;
18185
+ } catch {
18186
+ return false;
18187
+ }
18188
+ }
18189
+ function acquireSpawnLock2(sessionName) {
18190
+ if (!existsSync20(SPAWN_LOCK_DIR)) {
18191
+ mkdirSync9(SPAWN_LOCK_DIR, { recursive: true });
18192
+ }
18193
+ const lockFile = spawnLockPath(sessionName);
18194
+ if (existsSync20(lockFile)) {
18195
+ try {
18196
+ const lock = JSON.parse(readFileSync15(lockFile, "utf8"));
18197
+ const age = Date.now() - lock.timestamp;
18198
+ if (isProcessAlive(lock.pid) && age < 6e4) {
18199
+ return false;
18200
+ }
18201
+ } catch {
18202
+ }
18203
+ }
18204
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
18205
+ return true;
18206
+ }
18207
+ function releaseSpawnLock2(sessionName) {
18208
+ try {
18209
+ unlinkSync8(spawnLockPath(sessionName));
18210
+ } catch {
18211
+ }
18212
+ }
17970
18213
  function resolveBehaviorsExporterScript() {
17971
18214
  try {
17972
18215
  const thisFile = fileURLToPath4(import.meta.url);
@@ -18065,10 +18308,10 @@ function isEmployeeAlive(sessionName) {
18065
18308
  }
18066
18309
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
18067
18310
  const base = employeeSessionName(employeeName, exeSession);
18068
- if (!isAlive(base)) return 0;
18311
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
18069
18312
  for (let i = 2; i <= maxInstances; i++) {
18070
18313
  const candidate = employeeSessionName(employeeName, exeSession, i);
18071
- if (!isAlive(candidate)) return i;
18314
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
18072
18315
  }
18073
18316
  return null;
18074
18317
  }
@@ -18432,6 +18675,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
18432
18675
  command: spawnCommand
18433
18676
  });
18434
18677
  if (spawnResult.error) {
18678
+ releaseSpawnLock2(sessionName);
18435
18679
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
18436
18680
  }
18437
18681
  transport.pipeLog(sessionName, logFile);
@@ -18469,6 +18713,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
18469
18713
  }
18470
18714
  }
18471
18715
  if (!booted) {
18716
+ releaseSpawnLock2(sessionName);
18472
18717
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
18473
18718
  }
18474
18719
  if (!useExeAgent) {
@@ -18485,9 +18730,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
18485
18730
  pid: 0,
18486
18731
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
18487
18732
  });
18733
+ releaseSpawnLock2(sessionName);
18488
18734
  return { sessionName };
18489
18735
  }
18490
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
18736
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
18491
18737
  var init_tmux_routing = __esm({
18492
18738
  "src/lib/tmux-routing.ts"() {
18493
18739
  "use strict";
@@ -18499,6 +18745,7 @@ var init_tmux_routing = __esm({
18499
18745
  init_provider_table();
18500
18746
  init_intercom_queue();
18501
18747
  init_plan_limits();
18748
+ SPAWN_LOCK_DIR = path29.join(os9.homedir(), ".exe-os", "spawn-locks");
18502
18749
  SESSION_CACHE = path29.join(os9.homedir(), ".exe-os", "session-cache");
18503
18750
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
18504
18751
  VERIFY_PANE_LINES = 200;
@@ -21747,7 +21994,7 @@ var init_update = __esm({
21747
21994
  });
21748
21995
 
21749
21996
  // src/bin/cli.ts
21750
- import { existsSync as existsSync21, readFileSync as readFileSync17 } from "fs";
21997
+ import { existsSync as existsSync21, readFileSync as readFileSync17, writeFileSync as writeFileSync9, readdirSync as readdirSync5, rmSync } from "fs";
21751
21998
  import path31 from "path";
21752
21999
  import os10 from "os";
21753
22000
  var args = process.argv.slice(2);
@@ -21779,7 +22026,7 @@ if (args.includes("--global")) {
21779
22026
  await runClaudeCheck();
21780
22027
  break;
21781
22028
  case "uninstall":
21782
- await runClaudeUninstall();
22029
+ await runClaudeUninstall(args.slice(2));
21783
22030
  break;
21784
22031
  default:
21785
22032
  await runClaudeInstall();
@@ -21827,7 +22074,14 @@ async function runClaudeCheck() {
21827
22074
  const claudeJsonPath = path31.join(os10.homedir(), ".claude.json");
21828
22075
  let ok = true;
21829
22076
  if (existsSync21(settingsPath)) {
21830
- const settings = JSON.parse(readFileSync17(settingsPath, "utf8"));
22077
+ let settings;
22078
+ try {
22079
+ settings = JSON.parse(readFileSync17(settingsPath, "utf8"));
22080
+ } catch {
22081
+ console.log("\x1B[31m\u2717\x1B[0m settings.json is malformed (invalid JSON)");
22082
+ ok = false;
22083
+ settings = {};
22084
+ }
21831
22085
  const hasHooks = settings.hooks && Object.keys(settings.hooks).some((k) => {
21832
22086
  const groups = settings.hooks[k];
21833
22087
  return Array.isArray(groups) && groups.some((g) => {
@@ -21849,7 +22103,14 @@ async function runClaudeCheck() {
21849
22103
  ok = false;
21850
22104
  }
21851
22105
  if (existsSync21(claudeJsonPath)) {
21852
- const claudeJson = JSON.parse(readFileSync17(claudeJsonPath, "utf8"));
22106
+ let claudeJson;
22107
+ try {
22108
+ claudeJson = JSON.parse(readFileSync17(claudeJsonPath, "utf8"));
22109
+ } catch {
22110
+ console.log("\x1B[31m\u2717\x1B[0m claude.json is malformed (invalid JSON)");
22111
+ ok = false;
22112
+ claudeJson = {};
22113
+ }
21853
22114
  const hasMcp = claudeJson.mcpServers && (claudeJson.mcpServers["exe-mem"] || claudeJson.mcpServers["exe-os"]);
21854
22115
  if (hasMcp) {
21855
22116
  console.log("\x1B[32m\u2713\x1B[0m MCP server configured in claude.json");
@@ -21863,11 +22124,11 @@ async function runClaudeCheck() {
21863
22124
  console.log("\x1B[31m\u2717\x1B[0m claude.json not found");
21864
22125
  ok = false;
21865
22126
  }
21866
- const commandsDir = path31.join(claudeDir, "commands");
21867
- if (existsSync21(commandsDir)) {
21868
- console.log("\x1B[32m\u2713\x1B[0m Slash commands directory exists");
22127
+ const skillsDir = path31.join(claudeDir, "skills");
22128
+ if (existsSync21(skillsDir)) {
22129
+ console.log("\x1B[32m\u2713\x1B[0m Slash skills directory exists");
21869
22130
  } else {
21870
- console.log("\x1B[31m\u2717\x1B[0m Slash commands directory missing");
22131
+ console.log("\x1B[31m\u2717\x1B[0m Slash skills directory missing");
21871
22132
  ok = false;
21872
22133
  }
21873
22134
  if (!ok) {
@@ -21877,13 +22138,29 @@ async function runClaudeCheck() {
21877
22138
  console.log("\nAll checks passed.");
21878
22139
  }
21879
22140
  }
21880
- async function runClaudeUninstall() {
21881
- const claudeDir = path31.join(os10.homedir(), ".claude");
22141
+ async function runClaudeUninstall(flags = []) {
22142
+ const dryRun = flags.includes("--dry-run");
22143
+ const purge = flags.includes("--purge");
22144
+ const homeDir = os10.homedir();
22145
+ const claudeDir = path31.join(homeDir, ".claude");
21882
22146
  const settingsPath = path31.join(claudeDir, "settings.json");
21883
- const claudeJsonPath = path31.join(os10.homedir(), ".claude.json");
22147
+ const claudeJsonPath = path31.join(homeDir, ".claude.json");
22148
+ const exeOsDir = path31.join(homeDir, ".exe-os");
21884
22149
  let removed = 0;
22150
+ const log = (msg) => console.log(dryRun ? `[dry-run] ${msg}` : msg);
22151
+ let settings = {};
21885
22152
  if (existsSync21(settingsPath)) {
21886
- const settings = JSON.parse(readFileSync17(settingsPath, "utf8"));
22153
+ try {
22154
+ settings = JSON.parse(readFileSync17(settingsPath, "utf8"));
22155
+ } catch {
22156
+ console.error("Your ~/.claude/settings.json appears malformed.");
22157
+ if (purge) {
22158
+ console.error("Skipping settings cleanup (--purge).");
22159
+ } else {
22160
+ console.error("Try running: exe-os claude install");
22161
+ process.exit(1);
22162
+ }
22163
+ }
21887
22164
  if (settings.hooks) {
21888
22165
  for (const key of Object.keys(settings.hooks)) {
21889
22166
  const groups = settings.hooks[key];
@@ -21903,9 +22180,19 @@ async function runClaudeUninstall() {
21903
22180
  delete settings.hooks[key];
21904
22181
  }
21905
22182
  }
21906
- const { writeFileSync: writeFileSync9 } = await import("fs");
21907
- writeFileSync9(settingsPath, JSON.stringify(settings, null, 2) + "\n");
21908
- console.log("Removed exe-os hooks from settings.json");
22183
+ let permCount = 0;
22184
+ if (Array.isArray(settings.permissions?.allow)) {
22185
+ const before = settings.permissions.allow.length;
22186
+ settings.permissions.allow = settings.permissions.allow.filter(
22187
+ (p) => !p.startsWith("mcp__exe-mem__") && !p.startsWith("mcp__exe-os__")
22188
+ );
22189
+ permCount = before - settings.permissions.allow.length;
22190
+ }
22191
+ if (!dryRun) {
22192
+ writeFileSync9(settingsPath, JSON.stringify(settings, null, 2) + "\n");
22193
+ }
22194
+ log("\u2713 Removed exe-os hooks from settings.json");
22195
+ if (permCount > 0) log(`\u2713 Removed ${permCount} MCP permission entries`);
21909
22196
  removed++;
21910
22197
  }
21911
22198
  }
@@ -21915,25 +22202,154 @@ async function runClaudeUninstall() {
21915
22202
  let removedMcp = false;
21916
22203
  for (const key of ["exe-mem", "exe-os"]) {
21917
22204
  if (claudeJson.mcpServers[key]) {
21918
- delete claudeJson.mcpServers[key];
22205
+ if (!dryRun) delete claudeJson.mcpServers[key];
21919
22206
  removedMcp = true;
21920
22207
  }
21921
22208
  }
21922
22209
  if (removedMcp) {
21923
- const { writeFileSync: writeFileSync9 } = await import("fs");
21924
- writeFileSync9(
21925
- claudeJsonPath,
21926
- JSON.stringify(claudeJson, null, 2) + "\n"
21927
- );
21928
- console.log("Removed exe-os MCP server from claude.json");
22210
+ if (!dryRun) {
22211
+ writeFileSync9(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
22212
+ }
22213
+ log("\u2713 Removed exe-os MCP server from claude.json");
21929
22214
  removed++;
21930
22215
  }
21931
22216
  }
21932
22217
  }
22218
+ const skillsDir = path31.join(claudeDir, "skills");
22219
+ if (existsSync21(skillsDir)) {
22220
+ let skillCount = 0;
22221
+ try {
22222
+ const entries = readdirSync5(skillsDir);
22223
+ for (const entry of entries) {
22224
+ if (entry.startsWith("exe")) {
22225
+ const fullPath = path31.join(skillsDir, entry);
22226
+ if (!dryRun) rmSync(fullPath, { recursive: true, force: true });
22227
+ skillCount++;
22228
+ }
22229
+ }
22230
+ } catch {
22231
+ }
22232
+ if (skillCount > 0) {
22233
+ log(`\u2713 Removed ${skillCount} skill directories`);
22234
+ removed++;
22235
+ }
22236
+ }
22237
+ const claudeMdPath = path31.join(claudeDir, "CLAUDE.md");
22238
+ if (existsSync21(claudeMdPath)) {
22239
+ const content = readFileSync17(claudeMdPath, "utf8");
22240
+ const startMarker = "<!-- exe-os:orchestration-start -->";
22241
+ const endMarker = "<!-- exe-os:orchestration-end -->";
22242
+ const startIdx = content.indexOf(startMarker);
22243
+ const endIdx = content.indexOf(endMarker);
22244
+ if (startIdx !== -1 && endIdx !== -1) {
22245
+ const cleaned = (content.slice(0, startIdx) + content.slice(endIdx + endMarker.length)).replace(/\n{3,}/g, "\n\n").trim() + "\n";
22246
+ if (!dryRun) writeFileSync9(claudeMdPath, cleaned);
22247
+ log("\u2713 Removed orchestration block from CLAUDE.md");
22248
+ removed++;
22249
+ }
22250
+ }
22251
+ const agentsDir = path31.join(claudeDir, "agents");
22252
+ if (existsSync21(agentsDir)) {
22253
+ let agentCount = 0;
22254
+ try {
22255
+ const entries = readdirSync5(agentsDir).filter((f) => f.endsWith(".md"));
22256
+ let knownNames = /* @__PURE__ */ new Set();
22257
+ const rosterPath = path31.join(exeOsDir, "exe-employees.json");
22258
+ if (existsSync21(rosterPath)) {
22259
+ try {
22260
+ const roster = JSON.parse(readFileSync17(rosterPath, "utf8"));
22261
+ knownNames = new Set(roster.map((e) => e.name));
22262
+ } catch {
22263
+ }
22264
+ }
22265
+ for (const entry of entries) {
22266
+ const name = entry.replace(/\.md$/, "");
22267
+ if (knownNames.has(name)) {
22268
+ if (!dryRun) rmSync(path31.join(agentsDir, entry), { force: true });
22269
+ agentCount++;
22270
+ }
22271
+ }
22272
+ } catch {
22273
+ }
22274
+ if (agentCount > 0) {
22275
+ log(`\u2713 Removed ${agentCount} agent identity files`);
22276
+ removed++;
22277
+ }
22278
+ }
22279
+ const projectsDir = path31.join(claudeDir, "projects");
22280
+ if (existsSync21(projectsDir)) {
22281
+ let projectCount = 0;
22282
+ try {
22283
+ const projects = readdirSync5(projectsDir);
22284
+ for (const proj of projects) {
22285
+ const projSettings = path31.join(projectsDir, proj, "settings.json");
22286
+ if (!existsSync21(projSettings)) continue;
22287
+ try {
22288
+ const pSettings = JSON.parse(readFileSync17(projSettings, "utf8"));
22289
+ let changed = false;
22290
+ if (Array.isArray(pSettings.permissions?.allow)) {
22291
+ const before = pSettings.permissions.allow.length;
22292
+ pSettings.permissions.allow = pSettings.permissions.allow.filter(
22293
+ (p) => !p.startsWith("mcp__exe-mem__") && !p.startsWith("mcp__exe-os__")
22294
+ );
22295
+ if (pSettings.permissions.allow.length < before) changed = true;
22296
+ }
22297
+ if (changed && !dryRun) {
22298
+ writeFileSync9(projSettings, JSON.stringify(pSettings, null, 2) + "\n");
22299
+ }
22300
+ if (changed) projectCount++;
22301
+ } catch {
22302
+ }
22303
+ }
22304
+ } catch {
22305
+ }
22306
+ if (projectCount > 0) {
22307
+ log(`\u2713 Cleaned exe-os entries from ${projectCount} project settings`);
22308
+ removed++;
22309
+ }
22310
+ }
22311
+ try {
22312
+ const { execSync: execSync13 } = await import("child_process");
22313
+ const exeBinPath = execSync13("which exe", { encoding: "utf-8" }).trim();
22314
+ const binDir = path31.dirname(exeBinPath);
22315
+ let symlinkCount = 0;
22316
+ const rosterPath = path31.join(exeOsDir, "exe-employees.json");
22317
+ if (existsSync21(rosterPath)) {
22318
+ const roster = JSON.parse(readFileSync17(rosterPath, "utf8"));
22319
+ for (const emp of roster) {
22320
+ if (emp.name === "exe") continue;
22321
+ for (const suffix of ["", "-opencode"]) {
22322
+ const linkPath = path31.join(binDir, `${emp.name}${suffix}`);
22323
+ if (existsSync21(linkPath)) {
22324
+ if (!dryRun) rmSync(linkPath, { force: true });
22325
+ symlinkCount++;
22326
+ }
22327
+ }
22328
+ }
22329
+ }
22330
+ if (symlinkCount > 0) {
22331
+ log(`\u2713 Removed ${symlinkCount} employee symlinks`);
22332
+ removed++;
22333
+ }
22334
+ } catch {
22335
+ }
22336
+ if (purge && existsSync21(exeOsDir)) {
22337
+ if (!dryRun) {
22338
+ process.stdout.write("\x1B[33m\u26A0 This will delete all memories, identities, and agent data.\x1B[0m\n");
22339
+ process.stdout.write(" Removing ~/.exe-os...\n");
22340
+ rmSync(exeOsDir, { recursive: true, force: true });
22341
+ }
22342
+ log("\u2713 Purged ~/.exe-os data directory");
22343
+ removed++;
22344
+ }
21933
22345
  if (removed === 0) {
21934
22346
  console.log("Nothing to remove \u2014 exe-os was not installed.");
21935
22347
  } else {
21936
- console.log("Done. Run \x1B[36mexe-os claude install\x1B[0m to reinstall.");
22348
+ if (dryRun) {
22349
+ console.log("\nDry run complete. Re-run without --dry-run to apply.");
22350
+ } else {
22351
+ console.log("\nDone. Run \x1B[36mexe-os claude install\x1B[0m to reinstall.");
22352
+ }
21937
22353
  }
21938
22354
  }
21939
22355
  async function checkForUpdateOnBoot() {
@@ -21971,13 +22387,16 @@ async function runActivate(key) {
21971
22387
  mirrorLicenseKey2(key);
21972
22388
  try {
21973
22389
  const license = await validateLicense2(key);
21974
- if (!license.valid) {
22390
+ if (!license.valid && license.error === "offline") {
22391
+ console.log("\u26A0 Activated in offline mode \u2014 will verify when connected");
22392
+ } else if (!license.valid) {
21975
22393
  process.stderr.write(`License invalid: ${license.plan || "unknown"}
21976
22394
  `);
21977
22395
  process.exit(1);
22396
+ } else {
22397
+ const planLabel = license.plan === "pro" ? "Solopreneur" : license.plan.charAt(0).toUpperCase() + license.plan.slice(1);
22398
+ console.log(`Plan activated: ${planLabel}`);
21978
22399
  }
21979
- const planLabel = license.plan === "pro" ? "Solopreneur" : license.plan.charAt(0).toUpperCase() + license.plan.slice(1);
21980
- console.log(`Plan activated: ${planLabel}`);
21981
22400
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
21982
22401
  const ask2 = (prompt) => new Promise((resolve) => rl.question(prompt, (a) => resolve(a.trim())));
21983
22402
  let employees = await loadEmployees2();