@askexenow/exe-os 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6125,6 +6125,14 @@ var init_agent_config = __esm({
6125
6125
  });
6126
6126
 
6127
6127
  // src/lib/intercom-queue.ts
6128
+ var intercom_queue_exports = {};
6129
+ __export(intercom_queue_exports, {
6130
+ clearQueueForAgent: () => clearQueueForAgent,
6131
+ drainForSession: () => drainForSession,
6132
+ drainQueue: () => drainQueue,
6133
+ queueIntercom: () => queueIntercom,
6134
+ readQueue: () => readQueue
6135
+ });
6128
6136
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, renameSync as renameSync3, existsSync as existsSync13, mkdirSync as mkdirSync6 } from "fs";
6129
6137
  import path17 from "path";
6130
6138
  import os7 from "os";
@@ -6163,11 +6171,80 @@ function queueIntercom(targetSession, reason) {
6163
6171
  }
6164
6172
  writeQueue(queue);
6165
6173
  }
6166
- var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
6174
+ function drainQueue(isSessionBusy2, sendKeys) {
6175
+ const queue = readQueue();
6176
+ if (queue.length === 0) return { drained: 0, failed: 0 };
6177
+ const remaining = [];
6178
+ let drained = 0;
6179
+ let failed = 0;
6180
+ for (const item of queue) {
6181
+ const age = Date.now() - new Date(item.queuedAt).getTime();
6182
+ if (age > TTL_MS) {
6183
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6184
+ failed++;
6185
+ continue;
6186
+ }
6187
+ try {
6188
+ if (!isSessionBusy2(item.targetSession)) {
6189
+ const success = sendKeys(item.targetSession);
6190
+ if (success) {
6191
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6192
+ drained++;
6193
+ continue;
6194
+ }
6195
+ }
6196
+ } catch {
6197
+ }
6198
+ item.attempts++;
6199
+ if (item.attempts >= MAX_RETRIES2) {
6200
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6201
+ failed++;
6202
+ continue;
6203
+ }
6204
+ remaining.push(item);
6205
+ }
6206
+ writeQueue(remaining);
6207
+ return { drained, failed };
6208
+ }
6209
+ function drainForSession(targetSession, sendKeys) {
6210
+ const queue = readQueue();
6211
+ const match = queue.findIndex((q) => q.targetSession === targetSession);
6212
+ if (match < 0) return false;
6213
+ const success = sendKeys(targetSession);
6214
+ if (success) {
6215
+ queue.splice(match, 1);
6216
+ writeQueue(queue);
6217
+ logQueue(`DRAINED \u2192 ${targetSession} (prompt-submit trigger)`);
6218
+ return true;
6219
+ }
6220
+ return false;
6221
+ }
6222
+ function clearQueueForAgent(agentName) {
6223
+ const queue = readQueue();
6224
+ const before = queue.length;
6225
+ const filtered = queue.filter((q) => !q.targetSession.startsWith(`${agentName}-`));
6226
+ if (filtered.length < before) {
6227
+ writeQueue(filtered);
6228
+ logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
6229
+ }
6230
+ }
6231
+ function logQueue(msg) {
6232
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
6233
+ `;
6234
+ process.stderr.write(`[intercom-queue] ${msg}
6235
+ `);
6236
+ try {
6237
+ const { appendFileSync: appendFileSync3 } = __require("fs");
6238
+ appendFileSync3(INTERCOM_LOG, line);
6239
+ } catch {
6240
+ }
6241
+ }
6242
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
6167
6243
  var init_intercom_queue = __esm({
6168
6244
  "src/lib/intercom-queue.ts"() {
6169
6245
  "use strict";
6170
6246
  QUEUE_PATH = path17.join(os7.homedir(), ".exe-os", "intercom-queue.json");
6247
+ MAX_RETRIES2 = 5;
6171
6248
  TTL_MS = 60 * 60 * 1e3;
6172
6249
  INTERCOM_LOG = path17.join(os7.homedir(), ".exe-os", "intercom.log");
6173
6250
  }
@@ -7806,6 +7883,13 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
7806
7883
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
7807
7884
  } catch {
7808
7885
  }
7886
+ if (input.status === "done" || input.status === "cancelled") {
7887
+ try {
7888
+ const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
7889
+ clearQueueForAgent2(String(row.assigned_to));
7890
+ } catch {
7891
+ }
7892
+ }
7809
7893
  try {
7810
7894
  await writeCheckpoint({
7811
7895
  taskId,
@@ -9651,7 +9735,7 @@ async function deliverLocalMessage(messageId) {
9651
9735
  return true;
9652
9736
  } catch {
9653
9737
  const newRetryCount = msg.retryCount + 1;
9654
- if (newRetryCount >= MAX_RETRIES2) {
9738
+ if (newRetryCount >= MAX_RETRIES3) {
9655
9739
  await markFailed(messageId, "session unavailable after 10 retries");
9656
9740
  } else {
9657
9741
  await client.execute({
@@ -9669,13 +9753,13 @@ async function markFailed(messageId, reason) {
9669
9753
  args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
9670
9754
  });
9671
9755
  }
9672
- var MAX_RETRIES2, _wsClientSend;
9756
+ var MAX_RETRIES3, _wsClientSend;
9673
9757
  var init_messaging = __esm({
9674
9758
  "src/lib/messaging.ts"() {
9675
9759
  "use strict";
9676
9760
  init_database();
9677
9761
  init_tmux_routing();
9678
- MAX_RETRIES2 = 10;
9762
+ MAX_RETRIES3 = 10;
9679
9763
  _wsClientSend = null;
9680
9764
  }
9681
9765
  });
@@ -10107,7 +10191,7 @@ async function wikiFetch(config2, path38, method = "GET", body) {
10107
10191
  "Content-Type": "application/json"
10108
10192
  };
10109
10193
  const controller = new AbortController();
10110
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
10194
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS3);
10111
10195
  try {
10112
10196
  let response;
10113
10197
  try {
@@ -10120,7 +10204,7 @@ async function wikiFetch(config2, path38, method = "GET", body) {
10120
10204
  } catch {
10121
10205
  clearTimeout(timeout);
10122
10206
  const retryController = new AbortController();
10123
- const retryTimeout = setTimeout(() => retryController.abort(), REQUEST_TIMEOUT_MS2);
10207
+ const retryTimeout = setTimeout(() => retryController.abort(), REQUEST_TIMEOUT_MS3);
10124
10208
  try {
10125
10209
  await new Promise((r) => setTimeout(r, 500));
10126
10210
  response = await fetch(url, {
@@ -10224,12 +10308,12 @@ async function getChatHistory(client, workspaceSlug, limit = 50) {
10224
10308
  sentAt: h.sentAt ?? 0
10225
10309
  }));
10226
10310
  }
10227
- var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
10311
+ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS3;
10228
10312
  var init_wiki_client = __esm({
10229
10313
  "src/lib/wiki-client.ts"() {
10230
10314
  "use strict";
10231
10315
  LOCAL_WIKI_URL = process.env.EXE_WIKI_URL || "http://localhost:3001";
10232
- REQUEST_TIMEOUT_MS2 = 8e3;
10316
+ REQUEST_TIMEOUT_MS3 = 8e3;
10233
10317
  }
10234
10318
  });
10235
10319
 
@@ -10362,7 +10446,7 @@ __export(crdt_sync_exports, {
10362
10446
  rebuildFromDb: () => rebuildFromDb
10363
10447
  });
10364
10448
  import * as Y from "yjs";
10365
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, existsSync as existsSync27, mkdirSync as mkdirSync13, unlinkSync as unlinkSync9 } from "fs";
10449
+ import { readFileSync as readFileSync22, writeFileSync as writeFileSync16, existsSync as existsSync27, mkdirSync as mkdirSync13, unlinkSync as unlinkSync9 } from "fs";
10366
10450
  import path34 from "path";
10367
10451
  import { homedir as homedir5 } from "os";
10368
10452
  function getStatePath() {
@@ -10377,7 +10461,7 @@ function initCrdtDoc() {
10377
10461
  const sp = getStatePath();
10378
10462
  if (existsSync27(sp)) {
10379
10463
  try {
10380
- const state = readFileSync21(sp);
10464
+ const state = readFileSync22(sp);
10381
10465
  Y.applyUpdate(doc, new Uint8Array(state));
10382
10466
  } catch {
10383
10467
  console.warn("[crdt-sync] WARN: corrupted state file, rebuilding from DB");
@@ -14645,13 +14729,123 @@ var HostingerError = class extends Error {
14645
14729
  }
14646
14730
  };
14647
14731
 
14732
+ // src/lib/cloudflare-dns.ts
14733
+ var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
14734
+ var DNS_RECORD_TYPE_A = "A";
14735
+ var DEFAULT_PROXIED = true;
14736
+ var DEFAULT_TTL = 1;
14737
+ var REQUEST_TIMEOUT_MS2 = 3e4;
14738
+ var UNKNOWN_ERROR_CODE = "unknown";
14739
+ async function createARecord(cfApiToken, zoneId, domain, ip, opts) {
14740
+ const proxied = opts?.proxied ?? DEFAULT_PROXIED;
14741
+ const ttl = opts?.ttl ?? DEFAULT_TTL;
14742
+ const response = await requestCloudflare(cfApiToken, zoneId, {
14743
+ method: "POST",
14744
+ body: {
14745
+ type: DNS_RECORD_TYPE_A,
14746
+ name: domain,
14747
+ content: ip,
14748
+ proxied,
14749
+ ttl
14750
+ }
14751
+ });
14752
+ return mapDnsRecord(response);
14753
+ }
14754
+ async function requestCloudflare(cfApiToken, zoneId, options) {
14755
+ const response = await fetch(buildUrl(zoneId, options.path, options.query), {
14756
+ method: options.method,
14757
+ headers: buildHeaders(cfApiToken),
14758
+ body: options.body ? JSON.stringify(options.body) : void 0,
14759
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
14760
+ });
14761
+ const envelope = await parseEnvelope(response);
14762
+ if (!response.ok || !envelope.success) {
14763
+ throw toCloudflareError(response, envelope);
14764
+ }
14765
+ return envelope.result;
14766
+ }
14767
+ function buildUrl(zoneId, path38 = "/dns_records", query) {
14768
+ const normalizedPath = path38.startsWith("/") ? path38 : `/${path38}`;
14769
+ const url = new URL(
14770
+ `${CLOUDFLARE_API_BASE_URL}/zones/${zoneId}${normalizedPath}`
14771
+ );
14772
+ if (query) {
14773
+ url.search = query.toString();
14774
+ }
14775
+ return url.toString();
14776
+ }
14777
+ function buildHeaders(cfApiToken) {
14778
+ return {
14779
+ Authorization: `Bearer ${cfApiToken}`,
14780
+ "Content-Type": "application/json",
14781
+ Accept: "application/json"
14782
+ };
14783
+ }
14784
+ async function parseEnvelope(response) {
14785
+ try {
14786
+ return await response.json();
14787
+ } catch {
14788
+ return {
14789
+ success: false,
14790
+ errors: [
14791
+ {
14792
+ code: UNKNOWN_ERROR_CODE,
14793
+ message: `HTTP ${response.status}: ${response.statusText}`
14794
+ }
14795
+ ],
14796
+ messages: [],
14797
+ result: null
14798
+ };
14799
+ }
14800
+ }
14801
+ function toCloudflareError(response, envelope) {
14802
+ const firstError = envelope.errors[0];
14803
+ const errorCode = String(firstError?.code ?? UNKNOWN_ERROR_CODE);
14804
+ const message = firstError?.message ?? `HTTP ${response.status}: ${response.statusText}`;
14805
+ return new CloudflareError(response.status, errorCode, message);
14806
+ }
14807
+ function mapDnsRecord(record) {
14808
+ return {
14809
+ recordId: record.id,
14810
+ name: record.name,
14811
+ ip: record.content,
14812
+ proxied: record.proxied
14813
+ };
14814
+ }
14815
+ var CloudflareError = class extends Error {
14816
+ status;
14817
+ errorCode;
14818
+ constructor(status, errorCode, message) {
14819
+ super(message);
14820
+ this.name = "CloudflareError";
14821
+ this.status = status;
14822
+ this.errorCode = errorCode;
14823
+ }
14824
+ };
14825
+
14648
14826
  // src/mcp/tools/deploy-client.ts
14649
14827
  init_config();
14650
14828
  var execFileAsync = promisify(execFile);
14651
14829
  var POLL_INTERVAL_MS = 1e4;
14652
14830
  var POLL_MAX_ATTEMPTS = 60;
14831
+ var DEFAULT_VPS_TEMPLATE = "ubuntu-24.04";
14832
+ var DNS_PROXIED_OPTIONS = { proxied: true };
14833
+ var DEPLOYED_STATUS = "deployed";
14834
+ var PLAYBOOK_COMPLETE_HEALTH_PENDING_STATUS = "playbook_complete_health_pending";
14835
+ var ACTIVE_INVENTORY_STATUS = "active";
14836
+ var PROVISIONING_INVENTORY_STATUS = "provisioning";
14653
14837
  async function executeDeployment(params, client) {
14654
- const { client_name, domain, region, plan, ssl_email, apiKey } = params;
14838
+ const {
14839
+ client_name,
14840
+ domain,
14841
+ region,
14842
+ plan,
14843
+ ssl_email,
14844
+ user_id,
14845
+ apiKey,
14846
+ cfApiToken,
14847
+ cfZoneId
14848
+ } = params;
14655
14849
  const hostinger = client ?? new HostingerApiClient(apiKey);
14656
14850
  const hostname = `exe-${client_name.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
14657
14851
  process.stderr.write(`[deploy_client] Provisioning VPS: ${hostname} (${plan}, ${region})...
@@ -14660,7 +14854,7 @@ async function executeDeployment(params, client) {
14660
14854
  hostname,
14661
14855
  plan,
14662
14856
  region,
14663
- os_template: "ubuntu-24.04"
14857
+ os_template: DEFAULT_VPS_TEMPLATE
14664
14858
  });
14665
14859
  process.stderr.write(`[deploy_client] VPS created (ID: ${created.id}). Waiting for ready state...
14666
14860
  `);
@@ -14668,9 +14862,27 @@ async function executeDeployment(params, client) {
14668
14862
  process.stderr.write(`[deploy_client] VPS ready at ${vps.ip_address}. Running Ansible playbook...
14669
14863
  `);
14670
14864
  const playbookResult = await runAnsiblePlaybook(vps.ip_address, domain, ssl_email, client_name);
14865
+ const dnsRecordId = await createDnsRecordIfConfigured({
14866
+ domain,
14867
+ ipAddress: vps.ip_address,
14868
+ cfApiToken,
14869
+ cfZoneId
14870
+ });
14671
14871
  process.stderr.write(`[deploy_client] Playbook complete. Verifying health...
14672
14872
  `);
14673
14873
  const healthy = await verifyHealth(domain);
14874
+ const status = healthy ? DEPLOYED_STATUS : PLAYBOOK_COMPLETE_HEALTH_PENDING_STATUS;
14875
+ process.stderr.write("[deploy_client] Recording VPS inventory...\n");
14876
+ const inventory = buildInventoryRecord({
14877
+ userId: user_id,
14878
+ clientName: client_name,
14879
+ vpsId: vps.id,
14880
+ ipAddress: vps.ip_address,
14881
+ domain,
14882
+ region: vps.region,
14883
+ plan: vps.plan,
14884
+ healthy
14885
+ });
14674
14886
  return {
14675
14887
  vps_id: vps.id,
14676
14888
  hostname: vps.hostname,
@@ -14680,8 +14892,10 @@ async function executeDeployment(params, client) {
14680
14892
  plan: vps.plan,
14681
14893
  ssh_port: vps.ssh_port,
14682
14894
  ssh_access: `ssh exeai@${vps.ip_address}`,
14683
- status: healthy ? "deployed" : "playbook_complete_health_pending",
14684
- ansible_output: playbookResult
14895
+ status,
14896
+ ansible_output: playbookResult,
14897
+ dns_record_id: dnsRecordId,
14898
+ inventory
14685
14899
  };
14686
14900
  }
14687
14901
  function registerDeployClient(server2) {
@@ -14695,12 +14909,15 @@ function registerDeployClient(server2) {
14695
14909
  domain: z37.string().min(1).describe("Domain name for the deployment (e.g., client.exe.ai)"),
14696
14910
  region: z37.string().default("jakarta").describe("VPS region (default: jakarta)"),
14697
14911
  plan: z37.string().default("kvm-2").describe("Hostinger VPS plan (default: kvm-2)"),
14698
- ssl_email: z37.string().email().describe("Email for Let's Encrypt SSL certificate")
14912
+ ssl_email: z37.string().email().describe("Email for Let's Encrypt SSL certificate"),
14913
+ user_id: z37.string().min(1).describe("User/customer ID for inventory tracking")
14699
14914
  }
14700
14915
  },
14701
- async ({ client_name, domain, region, plan, ssl_email }) => {
14916
+ async ({ client_name, domain, region, plan, ssl_email, user_id }) => {
14702
14917
  const config2 = await loadConfig();
14703
14918
  const apiKey = config2.hostinger?.apiKey;
14919
+ const cfApiToken = config2.cloudflare?.apiToken;
14920
+ const cfZoneId = config2.cloudflare?.zoneId;
14704
14921
  if (!apiKey) {
14705
14922
  return {
14706
14923
  content: [
@@ -14718,7 +14935,10 @@ function registerDeployClient(server2) {
14718
14935
  region,
14719
14936
  plan,
14720
14937
  ssl_email,
14721
- apiKey
14938
+ user_id,
14939
+ apiKey,
14940
+ cfApiToken,
14941
+ cfZoneId
14722
14942
  });
14723
14943
  return {
14724
14944
  content: [
@@ -14816,6 +15036,39 @@ async function verifyHealth(domain) {
14816
15036
  return false;
14817
15037
  }
14818
15038
  }
15039
+ async function createDnsRecordIfConfigured(params) {
15040
+ const { domain, ipAddress, cfApiToken, cfZoneId } = params;
15041
+ if (!cfApiToken || !cfZoneId) {
15042
+ process.stderr.write(
15043
+ `[deploy_client] Warning: Cloudflare config missing. Skipping DNS setup for ${domain}; configure cloudflare.apiToken and cloudflare.zoneId to automate this step.
15044
+ `
15045
+ );
15046
+ return void 0;
15047
+ }
15048
+ process.stderr.write(`[deploy_client] Setting up DNS: ${domain} \u2192 ${ipAddress}...
15049
+ `);
15050
+ const dnsResult = await createARecord(
15051
+ cfApiToken,
15052
+ cfZoneId,
15053
+ domain,
15054
+ ipAddress,
15055
+ DNS_PROXIED_OPTIONS
15056
+ );
15057
+ return dnsResult.recordId;
15058
+ }
15059
+ function buildInventoryRecord(params) {
15060
+ const { userId, clientName, vpsId, ipAddress, domain, region, plan, healthy } = params;
15061
+ return {
15062
+ user_id: userId,
15063
+ client_name: clientName,
15064
+ hostinger_vps_id: vpsId,
15065
+ ip_address: ipAddress,
15066
+ domain,
15067
+ region,
15068
+ plan,
15069
+ status: healthy ? ACTIVE_INVENTORY_STATUS : PROVISIONING_INVENTORY_STATUS
15070
+ };
15071
+ }
14819
15072
 
14820
15073
  // src/mcp/tools/query-conversations.ts
14821
15074
  init_database();
@@ -16790,7 +17043,7 @@ function isMainModule(importMetaUrl) {
16790
17043
  }
16791
17044
 
16792
17045
  // src/bin/exe-doctor.ts
16793
- import { existsSync as existsSync26 } from "fs";
17046
+ import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
16794
17047
  import { spawn as spawn2 } from "child_process";
16795
17048
  import path33 from "path";
16796
17049
  import { randomUUID as randomUUID6 } from "crypto";
@@ -17202,6 +17455,46 @@ async function auditOrphanedProjects(client) {
17202
17455
  }
17203
17456
  return orphans;
17204
17457
  }
17458
+ function auditHookHealth() {
17459
+ const logPath = path33.join(
17460
+ process.env.HOME ?? process.env.USERPROFILE ?? "",
17461
+ ".exe-os",
17462
+ "logs",
17463
+ "hooks.log"
17464
+ );
17465
+ if (!existsSync26(logPath)) {
17466
+ return { logExists: false, totalLines: 0, errorsLastHour: 0, topPatterns: [] };
17467
+ }
17468
+ let content;
17469
+ try {
17470
+ content = readFileSync21(logPath, "utf-8");
17471
+ } catch {
17472
+ return { logExists: false, totalLines: 0, errorsLastHour: 0, topPatterns: [] };
17473
+ }
17474
+ const lines = content.split("\n").filter(Boolean);
17475
+ const totalLines = lines.length;
17476
+ const recent = lines.slice(-200);
17477
+ const oneHourAgo = new Date(Date.now() - 36e5).toISOString();
17478
+ let errorsLastHour = 0;
17479
+ const patternCounts = /* @__PURE__ */ new Map();
17480
+ for (const line of recent) {
17481
+ const isError = /error|Error|ERR|FAIL|throw|exception|TypeError|ReferenceError|SyntaxError/i.test(line);
17482
+ if (!isError) continue;
17483
+ const tsMatch = line.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/);
17484
+ if (tsMatch && tsMatch[1] >= oneHourAgo) {
17485
+ errorsLastHour++;
17486
+ } else if (!tsMatch) {
17487
+ errorsLastHour++;
17488
+ }
17489
+ const patternMatch = line.match(/((?:TypeError|ReferenceError|SyntaxError|Error):[^\n]{0,80})/);
17490
+ if (patternMatch) {
17491
+ const p = patternMatch[1];
17492
+ patternCounts.set(p, (patternCounts.get(p) ?? 0) + 1);
17493
+ }
17494
+ }
17495
+ const topPatterns = [...patternCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([pattern, count]) => ({ pattern, count }));
17496
+ return { logExists: true, totalLines, errorsLastHour, topPatterns };
17497
+ }
17205
17498
  async function runAudit(client, flags) {
17206
17499
  const [stats, nullVectors, duplicates, bloated, fts, orphanedProjects, conflicts] = await Promise.all([
17207
17500
  auditStats(client, flags),
@@ -17213,7 +17506,8 @@ async function runAudit(client, flags) {
17213
17506
  detectConflicts(client, flags.project, flags.agent)
17214
17507
  ]);
17215
17508
  const duplicateCount = duplicates.reduce((sum, d) => sum + d.delete_ids.length, 0);
17216
- return { stats, nullVectors, duplicates, duplicateCount, bloated, fts, orphanedProjects, conflicts };
17509
+ const hookHealth = auditHookHealth();
17510
+ return { stats, nullVectors, duplicates, duplicateCount, bloated, fts, orphanedProjects, conflicts, hookHealth };
17217
17511
  }
17218
17512
  function indicator(value, warn) {
17219
17513
  if (value === 0) return "\u{1F7E2}";
@@ -17281,6 +17575,17 @@ function formatReport(report, flags) {
17281
17575
  } else {
17282
17576
  lines.push("\u{1F7E2} Conflicts: none detected");
17283
17577
  }
17578
+ const hh = report.hookHealth;
17579
+ if (!hh.logExists) {
17580
+ lines.push("\u{1F7E0} Hook logs: no log file (run installer to enable stderr capture)");
17581
+ } else if (hh.errorsLastHour === 0) {
17582
+ lines.push(`\u{1F7E2} Hook logs: ${fmtNum(hh.totalLines)} lines, 0 errors in last hour`);
17583
+ } else {
17584
+ lines.push(`\u{1F534} Hook logs: ${fmtNum(hh.errorsLastHour)} errors in last hour (${fmtNum(hh.totalLines)} total lines)`);
17585
+ for (const p of hh.topPatterns) {
17586
+ lines.push(` ${p.count}x: ${p.pattern}`);
17587
+ }
17588
+ }
17284
17589
  lines.push("");
17285
17590
  if (flags.verbose) {
17286
17591
  if (report.duplicates.length > 0) {
@@ -17557,7 +17862,7 @@ import { z as z58 } from "zod";
17557
17862
 
17558
17863
  // src/lib/cloud-sync.ts
17559
17864
  init_database();
17560
- import { readFileSync as readFileSync22, writeFileSync as writeFileSync17, existsSync as existsSync28, readdirSync as readdirSync11, mkdirSync as mkdirSync14, appendFileSync as appendFileSync2, unlinkSync as unlinkSync10, openSync as openSync2, closeSync as closeSync2 } from "fs";
17865
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync28, readdirSync as readdirSync11, mkdirSync as mkdirSync14, appendFileSync as appendFileSync2, unlinkSync as unlinkSync10, openSync as openSync2, closeSync as closeSync2 } from "fs";
17561
17866
  import crypto16 from "crypto";
17562
17867
  import path35 from "path";
17563
17868
  import { homedir as homedir6 } from "os";
@@ -17652,7 +17957,7 @@ async function withRosterLock(fn) {
17652
17957
  } catch (err) {
17653
17958
  if (err.code === "EEXIST") {
17654
17959
  try {
17655
- const ts2 = parseInt(readFileSync22(ROSTER_LOCK_PATH, "utf-8"), 10);
17960
+ const ts2 = parseInt(readFileSync23(ROSTER_LOCK_PATH, "utf-8"), 10);
17656
17961
  if (Date.now() - ts2 < LOCK_STALE_MS) {
17657
17962
  throw new Error("Roster merge already in progress \u2014 another sync is running");
17658
17963
  }
@@ -17678,21 +17983,21 @@ async function withRosterLock(fn) {
17678
17983
  }
17679
17984
  }
17680
17985
  async function fetchWithRetry(url, init) {
17681
- const MAX_RETRIES3 = 3;
17986
+ const MAX_RETRIES4 = 3;
17682
17987
  const BASE_DELAY_MS2 = 200;
17683
17988
  let lastError;
17684
- for (let attempt = 0; attempt <= MAX_RETRIES3; attempt++) {
17989
+ for (let attempt = 0; attempt <= MAX_RETRIES4; attempt++) {
17685
17990
  try {
17686
17991
  const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS4);
17687
17992
  const resp = await fetch(url, { ...init, signal });
17688
- if (resp && resp.status >= 500 && attempt < MAX_RETRIES3) {
17993
+ if (resp && resp.status >= 500 && attempt < MAX_RETRIES4) {
17689
17994
  await new Promise((r) => setTimeout(r, BASE_DELAY_MS2 * Math.pow(2, attempt)));
17690
17995
  continue;
17691
17996
  }
17692
17997
  return resp;
17693
17998
  } catch (err) {
17694
17999
  lastError = err;
17695
- if (attempt === MAX_RETRIES3) throw err;
18000
+ if (attempt === MAX_RETRIES4) throw err;
17696
18001
  await new Promise((r) => setTimeout(r, BASE_DELAY_MS2 * Math.pow(2, attempt)));
17697
18002
  }
17698
18003
  }
@@ -18056,7 +18361,7 @@ var ROSTER_DELETIONS_PATH = path35.join(EXE_AI_DIR, "roster-deletions.json");
18056
18361
  function consumeRosterDeletions() {
18057
18362
  try {
18058
18363
  if (!existsSync28(ROSTER_DELETIONS_PATH)) return [];
18059
- const deletions = JSON.parse(readFileSync22(ROSTER_DELETIONS_PATH, "utf-8"));
18364
+ const deletions = JSON.parse(readFileSync23(ROSTER_DELETIONS_PATH, "utf-8"));
18060
18365
  writeFileSync17(ROSTER_DELETIONS_PATH, "[]");
18061
18366
  return deletions;
18062
18367
  } catch {
@@ -18070,7 +18375,7 @@ function buildRosterBlob(paths) {
18070
18375
  let roster = [];
18071
18376
  if (existsSync28(rosterPath)) {
18072
18377
  try {
18073
- roster = JSON.parse(readFileSync22(rosterPath, "utf-8"));
18378
+ roster = JSON.parse(readFileSync23(rosterPath, "utf-8"));
18074
18379
  } catch {
18075
18380
  }
18076
18381
  }
@@ -18078,7 +18383,7 @@ function buildRosterBlob(paths) {
18078
18383
  if (existsSync28(identityDir)) {
18079
18384
  for (const file of readdirSync11(identityDir).filter((f) => f.endsWith(".md"))) {
18080
18385
  try {
18081
- identities[file] = readFileSync22(path35.join(identityDir, file), "utf-8");
18386
+ identities[file] = readFileSync23(path35.join(identityDir, file), "utf-8");
18082
18387
  } catch {
18083
18388
  }
18084
18389
  }
@@ -18086,7 +18391,7 @@ function buildRosterBlob(paths) {
18086
18391
  let config2;
18087
18392
  if (existsSync28(configPath)) {
18088
18393
  try {
18089
- config2 = JSON.parse(readFileSync22(configPath, "utf-8"));
18394
+ config2 = JSON.parse(readFileSync23(configPath, "utf-8"));
18090
18395
  } catch {
18091
18396
  }
18092
18397
  }
@@ -18094,7 +18399,7 @@ function buildRosterBlob(paths) {
18094
18399
  const agentConfigPath = path35.join(EXE_AI_DIR, "agent-config.json");
18095
18400
  if (existsSync28(agentConfigPath)) {
18096
18401
  try {
18097
- agentConfig = JSON.parse(readFileSync22(agentConfigPath, "utf-8"));
18402
+ agentConfig = JSON.parse(readFileSync23(agentConfigPath, "utf-8"));
18098
18403
  } catch {
18099
18404
  }
18100
18405
  }
@@ -18174,7 +18479,7 @@ function mergeConfig(remoteConfig, configPath) {
18174
18479
  let local = {};
18175
18480
  if (existsSync28(cfgPath)) {
18176
18481
  try {
18177
- local = JSON.parse(readFileSync22(cfgPath, "utf-8"));
18482
+ local = JSON.parse(readFileSync23(cfgPath, "utf-8"));
18178
18483
  } catch {
18179
18484
  }
18180
18485
  }
@@ -18211,7 +18516,7 @@ async function mergeRosterFromRemote(remote, paths) {
18211
18516
  const idPath = path35.join(identityDir, `${remoteEmp.name}.md`);
18212
18517
  let localIdentity = null;
18213
18518
  try {
18214
- localIdentity = existsSync28(idPath) ? readFileSync22(idPath, "utf-8") : null;
18519
+ localIdentity = existsSync28(idPath) ? readFileSync23(idPath, "utf-8") : null;
18215
18520
  } catch {
18216
18521
  }
18217
18522
  if (localIdentity !== remoteIdentity) {
@@ -18245,7 +18550,7 @@ async function mergeRosterFromRemote(remote, paths) {
18245
18550
  let local = {};
18246
18551
  if (existsSync28(agentConfigPath)) {
18247
18552
  try {
18248
- local = JSON.parse(readFileSync22(agentConfigPath, "utf-8"));
18553
+ local = JSON.parse(readFileSync23(agentConfigPath, "utf-8"));
18249
18554
  } catch {
18250
18555
  }
18251
18556
  }
@@ -18976,7 +19281,7 @@ import { z as z62 } from "zod";
18976
19281
  // src/lib/people.ts
18977
19282
  init_config();
18978
19283
  import { readFile as readFile5, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
18979
- import { existsSync as existsSync29, readFileSync as readFileSync23 } from "fs";
19284
+ import { existsSync as existsSync29, readFileSync as readFileSync24 } from "fs";
18980
19285
  import path36 from "path";
18981
19286
  var PEOPLE_PATH = path36.join(EXE_AI_DIR, "people.json");
18982
19287
  async function loadPeople() {