@devness/useai 0.4.3 → 0.4.5

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 (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +1050 -91
  3. package/package.json +11 -12
package/dist/index.js CHANGED
@@ -83,7 +83,7 @@ var init_types = __esm({
83
83
  // ../shared/dist/constants/paths.js
84
84
  import { join } from "path";
85
85
  import { homedir } from "os";
86
- var USEAI_DIR, DATA_DIR, ACTIVE_DIR, SEALED_DIR, KEYSTORE_FILE, CONFIG_FILE, SESSIONS_FILE, MILESTONES_FILE, DAEMON_PID_FILE, DAEMON_PORT, DAEMON_LOG_FILE, DAEMON_MCP_URL, DAEMON_HEALTH_URL, LAUNCHD_PLIST_PATH, SYSTEMD_SERVICE_PATH, WINDOWS_STARTUP_SCRIPT_PATH;
86
+ var USEAI_DIR, DATA_DIR, ACTIVE_DIR, SEALED_DIR, KEYSTORE_FILE, CONFIG_FILE, SESSIONS_FILE, MILESTONES_FILE, DAEMON_PID_FILE, USEAI_HOOKS_DIR, DAEMON_PORT, DAEMON_LOG_FILE, DAEMON_MCP_URL, DAEMON_HEALTH_URL, LAUNCHD_PLIST_PATH, SYSTEMD_SERVICE_PATH, WINDOWS_STARTUP_SCRIPT_PATH;
87
87
  var init_paths = __esm({
88
88
  "../shared/dist/constants/paths.js"() {
89
89
  "use strict";
@@ -96,6 +96,7 @@ var init_paths = __esm({
96
96
  SESSIONS_FILE = join(DATA_DIR, "sessions.json");
97
97
  MILESTONES_FILE = join(DATA_DIR, "milestones.json");
98
98
  DAEMON_PID_FILE = join(USEAI_DIR, "daemon.pid");
99
+ USEAI_HOOKS_DIR = join(USEAI_DIR, "hooks");
99
100
  DAEMON_PORT = 19200;
100
101
  DAEMON_LOG_FILE = join(USEAI_DIR, "daemon.log");
101
102
  DAEMON_MCP_URL = `http://127.0.0.1:${DAEMON_PORT}/mcp`;
@@ -111,7 +112,7 @@ var VERSION;
111
112
  var init_version = __esm({
112
113
  "../shared/dist/constants/version.js"() {
113
114
  "use strict";
114
- VERSION = "0.4.3";
115
+ VERSION = "0.4.5";
115
116
  }
116
117
  });
117
118
 
@@ -397,6 +398,10 @@ var init_format = __esm({
397
398
  });
398
399
 
399
400
  // ../shared/dist/utils/detect-client.js
401
+ function normalizeMcpClientName(mcpName) {
402
+ const lower = mcpName.toLowerCase().trim();
403
+ return MCP_CLIENT_NAME_MAP[lower] ?? lower;
404
+ }
400
405
  function detectClient() {
401
406
  const env = process.env;
402
407
  for (const [envVar, clientName] of Object.entries(AI_CLIENT_ENV_VARS)) {
@@ -407,10 +412,35 @@ function detectClient() {
407
412
  return env.MCP_CLIENT_NAME;
408
413
  return "unknown";
409
414
  }
415
+ var MCP_CLIENT_NAME_MAP;
410
416
  var init_detect_client = __esm({
411
417
  "../shared/dist/utils/detect-client.js"() {
412
418
  "use strict";
413
419
  init_clients();
420
+ MCP_CLIENT_NAME_MAP = {
421
+ "claude-code": "claude-code",
422
+ "claude code": "claude-code",
423
+ "claude-desktop": "claude-desktop",
424
+ "claude desktop": "claude-desktop",
425
+ "cursor": "cursor",
426
+ "windsurf": "windsurf",
427
+ "codeium": "windsurf",
428
+ "vscode": "vscode",
429
+ "visual studio code": "vscode",
430
+ "vscode-insiders": "vscode-insiders",
431
+ "codex": "codex",
432
+ "codex-cli": "codex",
433
+ "gemini-cli": "gemini-cli",
434
+ "gemini cli": "gemini-cli",
435
+ "zed": "zed",
436
+ "cline": "cline",
437
+ "roo-code": "roo-code",
438
+ "roo-cline": "roo-code",
439
+ "amazon-q": "amazon-q",
440
+ "opencode": "opencode",
441
+ "goose": "goose",
442
+ "junie": "junie"
443
+ };
414
444
  }
415
445
  });
416
446
 
@@ -807,6 +837,136 @@ var init_daemon = __esm({
807
837
  }
808
838
  });
809
839
 
840
+ // ../shared/dist/hooks/claude-code.js
841
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3, chmodSync } from "fs";
842
+ import { join as join3 } from "path";
843
+ import { homedir as homedir3 } from "os";
844
+ function readSettings() {
845
+ if (!existsSync5(CLAUDE_SETTINGS_PATH))
846
+ return {};
847
+ try {
848
+ return JSON.parse(readFileSync3(CLAUDE_SETTINGS_PATH, "utf-8"));
849
+ } catch {
850
+ return {};
851
+ }
852
+ }
853
+ function writeSettings(settings) {
854
+ mkdirSync3(join3(homedir3(), ".claude"), { recursive: true });
855
+ writeFileSync3(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
856
+ }
857
+ function installClaudeCodeHooks() {
858
+ mkdirSync3(USEAI_HOOKS_DIR, { recursive: true });
859
+ writeFileSync3(STOP_GUARD_PATH, STOP_GUARD_SCRIPT);
860
+ try {
861
+ chmodSync(STOP_GUARD_PATH, "755");
862
+ } catch {
863
+ }
864
+ const settings = readSettings();
865
+ const hooks = settings["hooks"] ?? {};
866
+ const stopCmd = `node "${STOP_GUARD_PATH}"`;
867
+ const sealCmd = `curl -sf -X POST http://127.0.0.1:${DAEMON_PORT}/api/seal-active --max-time 3 2>/dev/null || true`;
868
+ let changed = false;
869
+ if (!hooks["Stop"])
870
+ hooks["Stop"] = [];
871
+ const stopArr = hooks["Stop"];
872
+ const hasStop = stopArr.some((g) => {
873
+ const inner = g["hooks"];
874
+ return inner?.some((h) => h["command"]?.includes("stop-guard"));
875
+ });
876
+ if (!hasStop) {
877
+ stopArr.push({ hooks: [{ type: "command", command: stopCmd, timeout: 10 }] });
878
+ changed = true;
879
+ }
880
+ if (!hooks["SessionEnd"])
881
+ hooks["SessionEnd"] = [];
882
+ const endArr = hooks["SessionEnd"];
883
+ const hasEnd = endArr.some((g) => {
884
+ const inner = g["hooks"];
885
+ return inner?.some((h) => h["command"]?.includes("seal-active"));
886
+ });
887
+ if (!hasEnd) {
888
+ endArr.push({ hooks: [{ type: "command", command: sealCmd, timeout: 5 }] });
889
+ changed = true;
890
+ }
891
+ settings["hooks"] = hooks;
892
+ writeSettings(settings);
893
+ return changed;
894
+ }
895
+ function removeClaudeCodeHooks() {
896
+ if (existsSync5(CLAUDE_SETTINGS_PATH)) {
897
+ try {
898
+ const settings = readSettings();
899
+ const hooks = settings["hooks"];
900
+ if (hooks) {
901
+ if (hooks["Stop"]) {
902
+ hooks["Stop"] = hooks["Stop"].filter((g) => {
903
+ const inner = g["hooks"];
904
+ return !inner?.some((h) => h["command"]?.includes("stop-guard"));
905
+ });
906
+ if (hooks["Stop"].length === 0)
907
+ delete hooks["Stop"];
908
+ }
909
+ if (hooks["SessionEnd"]) {
910
+ hooks["SessionEnd"] = hooks["SessionEnd"].filter((g) => {
911
+ const inner = g["hooks"];
912
+ return !inner?.some((h) => h["command"]?.includes("seal-active"));
913
+ });
914
+ if (hooks["SessionEnd"].length === 0)
915
+ delete hooks["SessionEnd"];
916
+ }
917
+ if (Object.keys(hooks).length === 0)
918
+ delete settings["hooks"];
919
+ }
920
+ writeSettings(settings);
921
+ } catch {
922
+ }
923
+ }
924
+ try {
925
+ if (existsSync5(STOP_GUARD_PATH))
926
+ unlinkSync3(STOP_GUARD_PATH);
927
+ } catch {
928
+ }
929
+ }
930
+ var STOP_GUARD_PATH, CLAUDE_SETTINGS_PATH, STOP_GUARD_SCRIPT;
931
+ var init_claude_code = __esm({
932
+ "../shared/dist/hooks/claude-code.js"() {
933
+ "use strict";
934
+ init_paths();
935
+ STOP_GUARD_PATH = join3(USEAI_HOOKS_DIR, "stop-guard.js");
936
+ CLAUDE_SETTINGS_PATH = join3(homedir3(), ".claude", "settings.json");
937
+ STOP_GUARD_SCRIPT = `#!/usr/bin/env node
938
+ 'use strict';
939
+ const { readFileSync } = require('node:fs');
940
+ let input = '';
941
+ process.stdin.setEncoding('utf-8');
942
+ process.stdin.on('data', chunk => input += chunk);
943
+ process.stdin.on('end', () => {
944
+ try {
945
+ const hook = JSON.parse(input);
946
+ if (hook.stop_hook_active) process.exit(0);
947
+ if (!hook.transcript_path) process.exit(0);
948
+ let transcript;
949
+ try { transcript = readFileSync(hook.transcript_path, 'utf-8'); } catch { process.exit(0); }
950
+ if (!transcript.includes('useai_session_start')) process.exit(0);
951
+ if (transcript.includes('useai_session_end')) process.exit(0);
952
+ process.stdout.write(JSON.stringify({
953
+ decision: 'block',
954
+ reason: 'Before ending, call useai_session_end with milestones (generic descriptions of accomplishments), languages used, and files_touched_count.'
955
+ }));
956
+ } catch { process.exit(0); }
957
+ });
958
+ `;
959
+ }
960
+ });
961
+
962
+ // ../shared/dist/hooks/index.js
963
+ var init_hooks = __esm({
964
+ "../shared/dist/hooks/index.js"() {
965
+ "use strict";
966
+ init_claude_code();
967
+ }
968
+ });
969
+
810
970
  // ../shared/dist/index.js
811
971
  var init_dist = __esm({
812
972
  "../shared/dist/index.js"() {
@@ -817,12 +977,13 @@ var init_dist = __esm({
817
977
  init_validation();
818
978
  init_utils();
819
979
  init_daemon();
980
+ init_hooks();
820
981
  }
821
982
  });
822
983
 
823
984
  // src/session-state.ts
824
- import { appendFileSync, existsSync as existsSync5 } from "fs";
825
- import { join as join3 } from "path";
985
+ import { appendFileSync, existsSync as existsSync6 } from "fs";
986
+ import { join as join4 } from "path";
826
987
  var SessionState;
827
988
  var init_session_state = __esm({
828
989
  "src/session-state.ts"() {
@@ -872,7 +1033,7 @@ var init_session_state = __esm({
872
1033
  }
873
1034
  initializeKeystore() {
874
1035
  ensureDir();
875
- if (existsSync5(KEYSTORE_FILE)) {
1036
+ if (existsSync6(KEYSTORE_FILE)) {
876
1037
  const ks = readJson(KEYSTORE_FILE, null);
877
1038
  if (ks) {
878
1039
  try {
@@ -890,7 +1051,7 @@ var init_session_state = __esm({
890
1051
  }
891
1052
  /** Path to this session's chain file in the active directory */
892
1053
  sessionChainPath() {
893
- return join3(ACTIVE_DIR, `${this.sessionId}.jsonl`);
1054
+ return join4(ACTIVE_DIR, `${this.sessionId}.jsonl`);
894
1055
  }
895
1056
  appendToChain(type, data) {
896
1057
  const record = buildChainRecord(type, this.sessionId, data, this.chainTipHash, this.signingKey);
@@ -907,8 +1068,8 @@ var init_session_state = __esm({
907
1068
  // src/register-tools.ts
908
1069
  import { z as z2 } from "zod";
909
1070
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
910
- import { existsSync as existsSync6, renameSync as renameSync2 } from "fs";
911
- import { join as join4 } from "path";
1071
+ import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
1072
+ import { join as join5 } from "path";
912
1073
  function getConfig() {
913
1074
  return readJson(CONFIG_FILE, {
914
1075
  milestone_tracking: true,
@@ -1014,10 +1175,10 @@ Session: ${session2.sessionId.slice(0, 8)} \xB7 Chain: ${record.hash.slice(0, 12
1014
1175
  seal: sealData,
1015
1176
  seal_signature: sealSignature
1016
1177
  });
1017
- const activePath = join4(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
1018
- const sealedPath = join4(SEALED_DIR, `${session2.sessionId}.jsonl`);
1178
+ const activePath = join5(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
1179
+ const sealedPath = join5(SEALED_DIR, `${session2.sessionId}.jsonl`);
1019
1180
  try {
1020
- if (existsSync6(activePath)) {
1181
+ if (existsSync7(activePath)) {
1021
1182
  renameSync2(activePath, sealedPath);
1022
1183
  }
1023
1184
  } catch {
@@ -1097,9 +1258,9 @@ var init_register_tools = __esm({
1097
1258
 
1098
1259
  // src/tools.ts
1099
1260
  import { execSync as execSync3 } from "child_process";
1100
- import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3 } from "fs";
1101
- import { dirname as dirname2, join as join5 } from "path";
1102
- import { homedir as homedir3 } from "os";
1261
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4 } from "fs";
1262
+ import { dirname as dirname2, join as join6 } from "path";
1263
+ import { homedir as homedir4 } from "os";
1103
1264
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
1104
1265
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1105
1266
  function installStandardHttp(configPath) {
@@ -1127,9 +1288,9 @@ function hasBinary(name) {
1127
1288
  }
1128
1289
  }
1129
1290
  function readJsonFile(path) {
1130
- if (!existsSync7(path)) return {};
1291
+ if (!existsSync8(path)) return {};
1131
1292
  try {
1132
- const raw = readFileSync3(path, "utf-8").trim();
1293
+ const raw = readFileSync4(path, "utf-8").trim();
1133
1294
  if (!raw) return {};
1134
1295
  return JSON.parse(raw);
1135
1296
  } catch {
@@ -1137,8 +1298,8 @@ function readJsonFile(path) {
1137
1298
  }
1138
1299
  }
1139
1300
  function writeJsonFile(path, data) {
1140
- mkdirSync3(dirname2(path), { recursive: true });
1141
- writeFileSync3(path, JSON.stringify(data, null, 2) + "\n");
1301
+ mkdirSync4(dirname2(path), { recursive: true });
1302
+ writeFileSync4(path, JSON.stringify(data, null, 2) + "\n");
1142
1303
  }
1143
1304
  function isConfiguredStandard(configPath) {
1144
1305
  const config = readJsonFile(configPath);
@@ -1219,9 +1380,9 @@ function removeZed(configPath) {
1219
1380
  }
1220
1381
  }
1221
1382
  function readTomlFile(path) {
1222
- if (!existsSync7(path)) return {};
1383
+ if (!existsSync8(path)) return {};
1223
1384
  try {
1224
- const raw = readFileSync3(path, "utf-8").trim();
1385
+ const raw = readFileSync4(path, "utf-8").trim();
1225
1386
  if (!raw) return {};
1226
1387
  return parseToml(raw);
1227
1388
  } catch {
@@ -1229,8 +1390,8 @@ function readTomlFile(path) {
1229
1390
  }
1230
1391
  }
1231
1392
  function writeTomlFile(path, data) {
1232
- mkdirSync3(dirname2(path), { recursive: true });
1233
- writeFileSync3(path, stringifyToml(data) + "\n");
1393
+ mkdirSync4(dirname2(path), { recursive: true });
1394
+ writeFileSync4(path, stringifyToml(data) + "\n");
1234
1395
  }
1235
1396
  function isConfiguredToml(configPath) {
1236
1397
  const config = readTomlFile(configPath);
@@ -1258,9 +1419,9 @@ function removeToml(configPath) {
1258
1419
  }
1259
1420
  }
1260
1421
  function readYamlFile(path) {
1261
- if (!existsSync7(path)) return {};
1422
+ if (!existsSync8(path)) return {};
1262
1423
  try {
1263
- const raw = readFileSync3(path, "utf-8").trim();
1424
+ const raw = readFileSync4(path, "utf-8").trim();
1264
1425
  if (!raw) return {};
1265
1426
  return parseYaml(raw) ?? {};
1266
1427
  } catch {
@@ -1268,8 +1429,8 @@ function readYamlFile(path) {
1268
1429
  }
1269
1430
  }
1270
1431
  function writeYamlFile(path, data) {
1271
- mkdirSync3(dirname2(path), { recursive: true });
1272
- writeFileSync3(path, stringifyYaml(data));
1432
+ mkdirSync4(dirname2(path), { recursive: true });
1433
+ writeFileSync4(path, stringifyYaml(data));
1273
1434
  }
1274
1435
  function isConfiguredYaml(configPath) {
1275
1436
  const config = readYamlFile(configPath);
@@ -1303,49 +1464,49 @@ function removeYaml(configPath) {
1303
1464
  }
1304
1465
  }
1305
1466
  function hasInstructionsBlock(filePath) {
1306
- if (!existsSync7(filePath)) return false;
1467
+ if (!existsSync8(filePath)) return false;
1307
1468
  try {
1308
- return readFileSync3(filePath, "utf-8").includes(INSTRUCTIONS_START);
1469
+ return readFileSync4(filePath, "utf-8").includes(INSTRUCTIONS_START);
1309
1470
  } catch {
1310
1471
  return false;
1311
1472
  }
1312
1473
  }
1313
1474
  function injectInstructions(config) {
1314
- mkdirSync3(dirname2(config.path), { recursive: true });
1475
+ mkdirSync4(dirname2(config.path), { recursive: true });
1315
1476
  if (config.method === "create") {
1316
- writeFileSync3(config.path, USEAI_INSTRUCTIONS + "\n");
1477
+ writeFileSync4(config.path, USEAI_INSTRUCTIONS + "\n");
1317
1478
  return;
1318
1479
  }
1319
1480
  if (hasInstructionsBlock(config.path)) return;
1320
1481
  let existing = "";
1321
- if (existsSync7(config.path)) {
1322
- existing = readFileSync3(config.path, "utf-8");
1482
+ if (existsSync8(config.path)) {
1483
+ existing = readFileSync4(config.path, "utf-8");
1323
1484
  }
1324
1485
  const separator = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
1325
- writeFileSync3(config.path, existing + separator + USEAI_INSTRUCTIONS_BLOCK + "\n");
1486
+ writeFileSync4(config.path, existing + separator + USEAI_INSTRUCTIONS_BLOCK + "\n");
1326
1487
  }
1327
1488
  function removeInstructions(config) {
1328
1489
  if (config.method === "create") {
1329
- if (existsSync7(config.path)) {
1490
+ if (existsSync8(config.path)) {
1330
1491
  try {
1331
- unlinkSync3(config.path);
1492
+ unlinkSync4(config.path);
1332
1493
  } catch {
1333
1494
  }
1334
1495
  }
1335
1496
  return;
1336
1497
  }
1337
- if (!existsSync7(config.path)) return;
1498
+ if (!existsSync8(config.path)) return;
1338
1499
  try {
1339
- const content = readFileSync3(config.path, "utf-8");
1500
+ const content = readFileSync4(config.path, "utf-8");
1340
1501
  const escaped = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1341
1502
  const regex = new RegExp(
1342
1503
  `\\n?${escaped(INSTRUCTIONS_START)}[\\s\\S]*?${escaped(INSTRUCTIONS_END)}\\n?`
1343
1504
  );
1344
1505
  const cleaned = content.replace(regex, "").trim();
1345
1506
  if (cleaned) {
1346
- writeFileSync3(config.path, cleaned + "\n");
1507
+ writeFileSync4(config.path, cleaned + "\n");
1347
1508
  } else {
1348
- unlinkSync3(config.path);
1509
+ unlinkSync4(config.path);
1349
1510
  }
1350
1511
  } catch {
1351
1512
  }
@@ -1421,7 +1582,7 @@ var init_tools = __esm({
1421
1582
  };
1422
1583
  MCP_HTTP_URL = DAEMON_MCP_URL;
1423
1584
  MCP_HTTP_ENTRY = { type: "http", url: MCP_HTTP_URL };
1424
- home = homedir3();
1585
+ home = homedir4();
1425
1586
  INSTRUCTIONS_START = "<!-- useai:start -->";
1426
1587
  INSTRUCTIONS_END = "<!-- useai:end -->";
1427
1588
  USEAI_INSTRUCTIONS = [
@@ -1441,23 +1602,23 @@ ${INSTRUCTIONS_END}`;
1441
1602
  toml: { isConfigured: isConfiguredToml, install: installToml, remove: removeToml },
1442
1603
  yaml: { isConfigured: isConfiguredYaml, install: installYaml, remove: removeYaml }
1443
1604
  };
1444
- appSupport = join5(home, "Library", "Application Support");
1605
+ appSupport = join6(home, "Library", "Application Support");
1445
1606
  AI_TOOLS = [
1446
1607
  createTool({
1447
1608
  id: "claude-code",
1448
1609
  name: "Claude Code",
1449
1610
  configFormat: "standard",
1450
- configPath: join5(home, ".claude.json"),
1451
- detect: () => hasBinary("claude") || existsSync7(join5(home, ".claude.json")),
1452
- instructions: { method: "append", path: join5(home, ".claude", "CLAUDE.md") },
1611
+ configPath: join6(home, ".claude.json"),
1612
+ detect: () => hasBinary("claude") || existsSync8(join6(home, ".claude.json")),
1613
+ instructions: { method: "append", path: join6(home, ".claude", "CLAUDE.md") },
1453
1614
  supportsUrl: true
1454
1615
  }),
1455
1616
  createTool({
1456
1617
  id: "cursor",
1457
1618
  name: "Cursor",
1458
1619
  configFormat: "standard",
1459
- configPath: join5(home, ".cursor", "mcp.json"),
1460
- detect: () => existsSync7(join5(home, ".cursor")),
1620
+ configPath: join6(home, ".cursor", "mcp.json"),
1621
+ detect: () => existsSync8(join6(home, ".cursor")),
1461
1622
  manualHint: "Open Cursor Settings \u2192 Rules \u2192 User Rules and paste the instructions below.",
1462
1623
  supportsUrl: true
1463
1624
  }),
@@ -1465,51 +1626,51 @@ ${INSTRUCTIONS_END}`;
1465
1626
  id: "windsurf",
1466
1627
  name: "Windsurf",
1467
1628
  configFormat: "standard",
1468
- configPath: join5(home, ".codeium", "windsurf", "mcp_config.json"),
1469
- detect: () => existsSync7(join5(home, ".codeium", "windsurf")),
1470
- instructions: { method: "append", path: join5(home, ".codeium", "windsurf", "memories", "global_rules.md") },
1629
+ configPath: join6(home, ".codeium", "windsurf", "mcp_config.json"),
1630
+ detect: () => existsSync8(join6(home, ".codeium", "windsurf")),
1631
+ instructions: { method: "append", path: join6(home, ".codeium", "windsurf", "memories", "global_rules.md") },
1471
1632
  supportsUrl: true
1472
1633
  }),
1473
1634
  createTool({
1474
1635
  id: "vscode",
1475
1636
  name: "VS Code",
1476
1637
  configFormat: "vscode",
1477
- configPath: join5(appSupport, "Code", "User", "mcp.json"),
1478
- detect: () => existsSync7(join5(appSupport, "Code")),
1479
- instructions: { method: "create", path: join5(appSupport, "Code", "User", "prompts", "useai.instructions.md") },
1638
+ configPath: join6(appSupport, "Code", "User", "mcp.json"),
1639
+ detect: () => existsSync8(join6(appSupport, "Code")),
1640
+ instructions: { method: "create", path: join6(appSupport, "Code", "User", "prompts", "useai.instructions.md") },
1480
1641
  supportsUrl: true
1481
1642
  }),
1482
1643
  createTool({
1483
1644
  id: "vscode-insiders",
1484
1645
  name: "VS Code Insiders",
1485
1646
  configFormat: "vscode",
1486
- configPath: join5(appSupport, "Code - Insiders", "User", "mcp.json"),
1487
- detect: () => existsSync7(join5(appSupport, "Code - Insiders")),
1488
- instructions: { method: "create", path: join5(appSupport, "Code - Insiders", "User", "prompts", "useai.instructions.md") },
1647
+ configPath: join6(appSupport, "Code - Insiders", "User", "mcp.json"),
1648
+ detect: () => existsSync8(join6(appSupport, "Code - Insiders")),
1649
+ instructions: { method: "create", path: join6(appSupport, "Code - Insiders", "User", "prompts", "useai.instructions.md") },
1489
1650
  supportsUrl: true
1490
1651
  }),
1491
1652
  createTool({
1492
1653
  id: "gemini-cli",
1493
1654
  name: "Gemini CLI",
1494
1655
  configFormat: "standard",
1495
- configPath: join5(home, ".gemini", "settings.json"),
1656
+ configPath: join6(home, ".gemini", "settings.json"),
1496
1657
  detect: () => hasBinary("gemini"),
1497
- instructions: { method: "append", path: join5(home, ".gemini", "GEMINI.md") },
1658
+ instructions: { method: "append", path: join6(home, ".gemini", "GEMINI.md") },
1498
1659
  supportsUrl: true
1499
1660
  }),
1500
1661
  createTool({
1501
1662
  id: "zed",
1502
1663
  name: "Zed",
1503
1664
  configFormat: "zed",
1504
- configPath: join5(appSupport, "Zed", "settings.json"),
1505
- detect: () => existsSync7(join5(appSupport, "Zed")),
1665
+ configPath: join6(appSupport, "Zed", "settings.json"),
1666
+ detect: () => existsSync8(join6(appSupport, "Zed")),
1506
1667
  manualHint: "Open Rules Library (\u2318\u2325L) \u2192 click + \u2192 paste the instructions below."
1507
1668
  }),
1508
1669
  createTool({
1509
1670
  id: "cline",
1510
1671
  name: "Cline",
1511
1672
  configFormat: "standard",
1512
- configPath: join5(
1673
+ configPath: join6(
1513
1674
  appSupport,
1514
1675
  "Code",
1515
1676
  "User",
@@ -1518,17 +1679,17 @@ ${INSTRUCTIONS_END}`;
1518
1679
  "settings",
1519
1680
  "cline_mcp_settings.json"
1520
1681
  ),
1521
- detect: () => existsSync7(
1522
- join5(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
1682
+ detect: () => existsSync8(
1683
+ join6(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
1523
1684
  ),
1524
- instructions: { method: "create", path: join5(home, "Documents", "Cline", "Rules", "useai.md") },
1685
+ instructions: { method: "create", path: join6(home, "Documents", "Cline", "Rules", "useai.md") },
1525
1686
  supportsUrl: true
1526
1687
  }),
1527
1688
  createTool({
1528
1689
  id: "roo-code",
1529
1690
  name: "Roo Code",
1530
1691
  configFormat: "standard",
1531
- configPath: join5(
1692
+ configPath: join6(
1532
1693
  appSupport,
1533
1694
  "Code",
1534
1695
  "User",
@@ -1537,59 +1698,59 @@ ${INSTRUCTIONS_END}`;
1537
1698
  "settings",
1538
1699
  "cline_mcp_settings.json"
1539
1700
  ),
1540
- detect: () => existsSync7(
1541
- join5(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
1701
+ detect: () => existsSync8(
1702
+ join6(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
1542
1703
  ),
1543
- instructions: { method: "create", path: join5(home, ".roo", "rules", "useai.md") },
1704
+ instructions: { method: "create", path: join6(home, ".roo", "rules", "useai.md") },
1544
1705
  supportsUrl: true
1545
1706
  }),
1546
1707
  createTool({
1547
1708
  id: "amazon-q-cli",
1548
1709
  name: "Amazon Q CLI",
1549
1710
  configFormat: "standard",
1550
- configPath: join5(home, ".aws", "amazonq", "mcp.json"),
1551
- detect: () => hasBinary("q") || existsSync7(join5(home, ".aws", "amazonq")),
1711
+ configPath: join6(home, ".aws", "amazonq", "mcp.json"),
1712
+ detect: () => hasBinary("q") || existsSync8(join6(home, ".aws", "amazonq")),
1552
1713
  manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
1553
1714
  }),
1554
1715
  createTool({
1555
1716
  id: "amazon-q-ide",
1556
1717
  name: "Amazon Q IDE",
1557
1718
  configFormat: "standard",
1558
- configPath: join5(home, ".aws", "amazonq", "default.json"),
1559
- detect: () => existsSync7(join5(home, ".amazonq")) || existsSync7(join5(home, ".aws", "amazonq")),
1719
+ configPath: join6(home, ".aws", "amazonq", "default.json"),
1720
+ detect: () => existsSync8(join6(home, ".amazonq")) || existsSync8(join6(home, ".aws", "amazonq")),
1560
1721
  manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
1561
1722
  }),
1562
1723
  createTool({
1563
1724
  id: "codex",
1564
1725
  name: "Codex",
1565
1726
  configFormat: "toml",
1566
- configPath: join5(home, ".codex", "config.toml"),
1567
- detect: () => hasBinary("codex") || existsSync7(join5(home, ".codex")) || existsSync7("/Applications/Codex.app"),
1568
- instructions: { method: "append", path: join5(home, ".codex", "AGENTS.md") }
1727
+ configPath: join6(home, ".codex", "config.toml"),
1728
+ detect: () => hasBinary("codex") || existsSync8(join6(home, ".codex")) || existsSync8("/Applications/Codex.app"),
1729
+ instructions: { method: "append", path: join6(home, ".codex", "AGENTS.md") }
1569
1730
  }),
1570
1731
  createTool({
1571
1732
  id: "goose",
1572
1733
  name: "Goose",
1573
1734
  configFormat: "yaml",
1574
- configPath: join5(home, ".config", "goose", "config.yaml"),
1575
- detect: () => existsSync7(join5(home, ".config", "goose")),
1576
- instructions: { method: "append", path: join5(home, ".config", "goose", ".goosehints") }
1735
+ configPath: join6(home, ".config", "goose", "config.yaml"),
1736
+ detect: () => existsSync8(join6(home, ".config", "goose")),
1737
+ instructions: { method: "append", path: join6(home, ".config", "goose", ".goosehints") }
1577
1738
  }),
1578
1739
  createTool({
1579
1740
  id: "opencode",
1580
1741
  name: "OpenCode",
1581
1742
  configFormat: "standard",
1582
- configPath: join5(home, ".config", "opencode", "opencode.json"),
1583
- detect: () => hasBinary("opencode") || existsSync7(join5(home, ".config", "opencode")),
1584
- instructions: { method: "append", path: join5(home, ".config", "opencode", "AGENTS.md") },
1743
+ configPath: join6(home, ".config", "opencode", "opencode.json"),
1744
+ detect: () => hasBinary("opencode") || existsSync8(join6(home, ".config", "opencode")),
1745
+ instructions: { method: "append", path: join6(home, ".config", "opencode", "AGENTS.md") },
1585
1746
  supportsUrl: true
1586
1747
  }),
1587
1748
  createTool({
1588
1749
  id: "junie",
1589
1750
  name: "Junie",
1590
1751
  configFormat: "standard",
1591
- configPath: join5(home, ".junie", "mcp", "mcp.json"),
1592
- detect: () => existsSync7(join5(home, ".junie")),
1752
+ configPath: join6(home, ".junie", "mcp", "mcp.json"),
1753
+ detect: () => existsSync8(join6(home, ".junie")),
1593
1754
  manualHint: "Add the instructions below to .junie/guidelines.md in your project root."
1594
1755
  })
1595
1756
  ];
@@ -1705,6 +1866,14 @@ async function daemonInstallFlow(tools, explicit) {
1705
1866
  console.log(err(`\u2717 ${tool.name.padEnd(18)} \u2014 ${e.message}`));
1706
1867
  }
1707
1868
  }
1869
+ try {
1870
+ const hooksInstalled = installClaudeCodeHooks();
1871
+ if (hooksInstalled) {
1872
+ console.log(ok("\u2713 Claude Code hooks installed (Stop + SessionEnd)"));
1873
+ }
1874
+ } catch {
1875
+ console.log(chalk.yellow(" \u26A0 Could not install Claude Code hooks"));
1876
+ }
1708
1877
  showManualHints(targetTools);
1709
1878
  const mode = useDaemon ? "daemon mode" : "stdio mode";
1710
1879
  console.log(`
@@ -1879,6 +2048,11 @@ async function fullRemoveFlow(tools, autoYes, explicit) {
1879
2048
  }
1880
2049
  }
1881
2050
  }
2051
+ try {
2052
+ removeClaudeCodeHooks();
2053
+ console.log(ok("\u2713 Claude Code hooks removed"));
2054
+ } catch {
2055
+ }
1882
2056
  console.log();
1883
2057
  try {
1884
2058
  await killDaemon();
@@ -1954,6 +2128,610 @@ var init_setup = __esm({
1954
2128
  }
1955
2129
  });
1956
2130
 
2131
+ // src/dashboard/html.ts
2132
+ function getDashboardHtml() {
2133
+ return `<!DOCTYPE html>
2134
+ <html lang="en">
2135
+ <head>
2136
+ <meta charset="UTF-8">
2137
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2138
+ <title>UseAI \u2014 Local Dashboard</title>
2139
+ <style>
2140
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2141
+
2142
+ :root {
2143
+ --bg: #0f0e0c;
2144
+ --surface: #1a1816;
2145
+ --border: #2a2520;
2146
+ --amber: #d4a04a;
2147
+ --amber-dim: #b8892e;
2148
+ --text: #e8e0d4;
2149
+ --muted: #9a9082;
2150
+ --green: #6abf69;
2151
+ --red: #d45a5a;
2152
+ --blue: #5a9fd4;
2153
+ --purple: #a87fd4;
2154
+ --radius: 8px;
2155
+ }
2156
+
2157
+ body {
2158
+ font-family: system-ui, -apple-system, sans-serif;
2159
+ background: var(--bg);
2160
+ color: var(--text);
2161
+ line-height: 1.5;
2162
+ min-height: 100vh;
2163
+ padding: 24px 16px;
2164
+ }
2165
+
2166
+ .container { max-width: 960px; margin: 0 auto; }
2167
+
2168
+ /* Header */
2169
+ .header { margin-bottom: 32px; }
2170
+ .header h1 {
2171
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
2172
+ font-size: 1.5rem;
2173
+ color: var(--amber);
2174
+ font-weight: 700;
2175
+ letter-spacing: -0.02em;
2176
+ }
2177
+ .header .subtitle {
2178
+ color: var(--muted);
2179
+ font-size: 0.85rem;
2180
+ margin-top: 2px;
2181
+ }
2182
+
2183
+ /* Stats row */
2184
+ .stats-row {
2185
+ display: grid;
2186
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
2187
+ gap: 12px;
2188
+ margin-bottom: 28px;
2189
+ }
2190
+ .stat-card {
2191
+ background: var(--surface);
2192
+ border: 1px solid var(--border);
2193
+ border-radius: var(--radius);
2194
+ padding: 16px;
2195
+ }
2196
+ .stat-card .label {
2197
+ font-size: 0.75rem;
2198
+ color: var(--muted);
2199
+ text-transform: uppercase;
2200
+ letter-spacing: 0.05em;
2201
+ margin-bottom: 4px;
2202
+ }
2203
+ .stat-card .value {
2204
+ font-family: 'SF Mono', 'Fira Code', monospace;
2205
+ font-size: 1.6rem;
2206
+ font-weight: 700;
2207
+ color: var(--amber);
2208
+ }
2209
+ .stat-card .unit {
2210
+ font-size: 0.8rem;
2211
+ color: var(--muted);
2212
+ font-weight: 400;
2213
+ margin-left: 4px;
2214
+ }
2215
+
2216
+ /* Section */
2217
+ .section {
2218
+ background: var(--surface);
2219
+ border: 1px solid var(--border);
2220
+ border-radius: var(--radius);
2221
+ padding: 20px;
2222
+ margin-bottom: 16px;
2223
+ }
2224
+ .section h2 {
2225
+ font-size: 0.85rem;
2226
+ color: var(--muted);
2227
+ text-transform: uppercase;
2228
+ letter-spacing: 0.05em;
2229
+ margin-bottom: 16px;
2230
+ font-weight: 600;
2231
+ }
2232
+
2233
+ /* Bar chart */
2234
+ .bar-row { margin-bottom: 10px; }
2235
+ .bar-row:last-child { margin-bottom: 0; }
2236
+ .bar-label {
2237
+ display: flex;
2238
+ justify-content: space-between;
2239
+ font-size: 0.82rem;
2240
+ margin-bottom: 4px;
2241
+ }
2242
+ .bar-label .name {
2243
+ font-family: 'SF Mono', 'Fira Code', monospace;
2244
+ color: var(--text);
2245
+ }
2246
+ .bar-label .hours {
2247
+ color: var(--muted);
2248
+ font-family: 'SF Mono', 'Fira Code', monospace;
2249
+ }
2250
+ .bar-track {
2251
+ height: 8px;
2252
+ background: var(--bg);
2253
+ border-radius: 4px;
2254
+ overflow: hidden;
2255
+ }
2256
+ .bar-fill {
2257
+ height: 100%;
2258
+ background: var(--amber);
2259
+ border-radius: 4px;
2260
+ transition: width 0.6s ease;
2261
+ }
2262
+
2263
+ /* Milestones */
2264
+ .milestone-item {
2265
+ display: flex;
2266
+ align-items: flex-start;
2267
+ gap: 10px;
2268
+ padding: 10px 0;
2269
+ border-bottom: 1px solid var(--border);
2270
+ }
2271
+ .milestone-item:last-child { border-bottom: none; }
2272
+ .milestone-title {
2273
+ flex: 1;
2274
+ font-size: 0.88rem;
2275
+ color: var(--text);
2276
+ }
2277
+ .milestone-meta {
2278
+ display: flex;
2279
+ gap: 8px;
2280
+ align-items: center;
2281
+ flex-shrink: 0;
2282
+ }
2283
+ .badge {
2284
+ font-size: 0.7rem;
2285
+ padding: 2px 8px;
2286
+ border-radius: 4px;
2287
+ font-family: 'SF Mono', 'Fira Code', monospace;
2288
+ text-transform: uppercase;
2289
+ letter-spacing: 0.03em;
2290
+ font-weight: 600;
2291
+ }
2292
+ .badge-feature { background: #2a3520; color: var(--green); }
2293
+ .badge-bugfix { background: #3a2020; color: var(--red); }
2294
+ .badge-refactor { background: #202a3a; color: var(--blue); }
2295
+ .badge-test { background: #2a2035; color: var(--purple); }
2296
+ .badge-docs { background: #2a2820; color: #c4a854; }
2297
+ .badge-setup { background: #252520; color: #b0a070; }
2298
+ .badge-deployment { background: #202525; color: #70b0a0; }
2299
+ .badge-other { background: #252525; color: var(--muted); }
2300
+ .complexity {
2301
+ font-size: 0.7rem;
2302
+ color: var(--muted);
2303
+ font-family: 'SF Mono', 'Fira Code', monospace;
2304
+ }
2305
+ .milestone-date {
2306
+ font-size: 0.72rem;
2307
+ color: var(--muted);
2308
+ font-family: 'SF Mono', 'Fira Code', monospace;
2309
+ white-space: nowrap;
2310
+ }
2311
+
2312
+ /* Sync section */
2313
+ .sync-section { text-align: center; padding: 24px; }
2314
+ .sync-btn {
2315
+ background: var(--amber);
2316
+ color: var(--bg);
2317
+ border: none;
2318
+ padding: 10px 28px;
2319
+ border-radius: var(--radius);
2320
+ font-weight: 600;
2321
+ font-size: 0.9rem;
2322
+ cursor: pointer;
2323
+ font-family: system-ui, sans-serif;
2324
+ transition: opacity 0.15s;
2325
+ }
2326
+ .sync-btn:hover { opacity: 0.85; }
2327
+ .sync-btn:disabled { opacity: 0.5; cursor: not-allowed; }
2328
+ .sync-status {
2329
+ margin-top: 10px;
2330
+ font-size: 0.82rem;
2331
+ color: var(--muted);
2332
+ }
2333
+ .sync-result {
2334
+ margin-top: 8px;
2335
+ font-size: 0.82rem;
2336
+ }
2337
+ .sync-result.success { color: var(--green); }
2338
+ .sync-result.error { color: var(--red); }
2339
+
2340
+ .setup-msg {
2341
+ text-align: center;
2342
+ padding: 20px;
2343
+ color: var(--muted);
2344
+ font-size: 0.88rem;
2345
+ }
2346
+ .setup-msg a {
2347
+ color: var(--amber);
2348
+ text-decoration: underline;
2349
+ }
2350
+
2351
+ .empty {
2352
+ text-align: center;
2353
+ color: var(--muted);
2354
+ padding: 20px;
2355
+ font-size: 0.85rem;
2356
+ }
2357
+
2358
+ @media (max-width: 500px) {
2359
+ body { padding: 16px 10px; }
2360
+ .stats-row { grid-template-columns: repeat(2, 1fr); }
2361
+ .milestone-meta { flex-direction: column; gap: 4px; align-items: flex-end; }
2362
+ }
2363
+ </style>
2364
+ </head>
2365
+ <body>
2366
+ <div class="container">
2367
+ <div class="header">
2368
+ <h1>UseAI</h1>
2369
+ <div class="subtitle">Local Dashboard</div>
2370
+ </div>
2371
+
2372
+ <div class="stats-row" id="stats-row">
2373
+ <div class="stat-card"><div class="label">Total Hours</div><div class="value" id="stat-hours">-</div></div>
2374
+ <div class="stat-card"><div class="label">Sessions</div><div class="value" id="stat-sessions">-</div></div>
2375
+ <div class="stat-card"><div class="label">Current Streak</div><div class="value" id="stat-streak">-<span class="unit">days</span></div></div>
2376
+ <div class="stat-card"><div class="label">Files Touched</div><div class="value" id="stat-files">-</div></div>
2377
+ </div>
2378
+
2379
+ <div class="section" id="clients-section" style="display:none">
2380
+ <h2>Tools / Clients</h2>
2381
+ <div id="clients-bars"></div>
2382
+ </div>
2383
+
2384
+ <div class="section" id="languages-section" style="display:none">
2385
+ <h2>Languages</h2>
2386
+ <div id="languages-bars"></div>
2387
+ </div>
2388
+
2389
+ <div class="section" id="tasks-section" style="display:none">
2390
+ <h2>Task Types</h2>
2391
+ <div id="tasks-bars"></div>
2392
+ </div>
2393
+
2394
+ <div class="section" id="milestones-section" style="display:none">
2395
+ <h2>Recent Milestones</h2>
2396
+ <div id="milestones-list"></div>
2397
+ </div>
2398
+
2399
+ <div class="section" id="sync-section" style="display:none"></div>
2400
+ </div>
2401
+
2402
+ <script>
2403
+ (function() {
2404
+ const API = '';
2405
+
2406
+ function animateCounter(el, target, decimals) {
2407
+ if (decimals === undefined) decimals = 0;
2408
+ var start = 0;
2409
+ var duration = 600;
2410
+ var startTime = null;
2411
+ function step(ts) {
2412
+ if (!startTime) startTime = ts;
2413
+ var progress = Math.min((ts - startTime) / duration, 1);
2414
+ var eased = 1 - Math.pow(1 - progress, 3);
2415
+ var current = start + (target - start) * eased;
2416
+ el.textContent = decimals > 0 ? current.toFixed(decimals) : Math.round(current);
2417
+ if (progress < 1) requestAnimationFrame(step);
2418
+ }
2419
+ requestAnimationFrame(step);
2420
+ }
2421
+
2422
+ function formatHours(seconds) {
2423
+ var h = seconds / 3600;
2424
+ return h < 0.1 ? h.toFixed(2) : h.toFixed(1);
2425
+ }
2426
+
2427
+ function renderBars(containerId, data) {
2428
+ var container = document.getElementById(containerId);
2429
+ if (!container) return;
2430
+ var entries = Object.entries(data).sort(function(a, b) { return b[1] - a[1]; });
2431
+ if (entries.length === 0) { container.innerHTML = '<div class="empty">No data yet</div>'; return; }
2432
+ var max = entries[0][1];
2433
+ container.innerHTML = entries.map(function(e) {
2434
+ var pct = max > 0 ? (e[1] / max * 100) : 0;
2435
+ return '<div class="bar-row">' +
2436
+ '<div class="bar-label"><span class="name">' + escapeHtml(e[0]) + '</span><span class="hours">' + formatHours(e[1]) + 'h</span></div>' +
2437
+ '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%"></div></div>' +
2438
+ '</div>';
2439
+ }).join('');
2440
+ }
2441
+
2442
+ function badgeClass(cat) {
2443
+ var map = { feature: 'feature', bugfix: 'bugfix', refactor: 'refactor', test: 'test', docs: 'docs', setup: 'setup', deployment: 'deployment' };
2444
+ return 'badge badge-' + (map[cat] || 'other');
2445
+ }
2446
+
2447
+ function escapeHtml(s) {
2448
+ var d = document.createElement('div');
2449
+ d.textContent = s;
2450
+ return d.innerHTML;
2451
+ }
2452
+
2453
+ function renderMilestones(milestones) {
2454
+ var section = document.getElementById('milestones-section');
2455
+ var list = document.getElementById('milestones-list');
2456
+ if (!milestones || milestones.length === 0) {
2457
+ section.style.display = 'block';
2458
+ list.innerHTML = '<div class="empty">No milestones recorded yet</div>';
2459
+ return;
2460
+ }
2461
+ section.style.display = 'block';
2462
+ var recent = milestones.slice(-20).reverse();
2463
+ list.innerHTML = recent.map(function(m) {
2464
+ var date = m.created_at ? m.created_at.slice(0, 10) : '';
2465
+ return '<div class="milestone-item">' +
2466
+ '<div class="milestone-title">' + escapeHtml(m.title) + '</div>' +
2467
+ '<div class="milestone-meta">' +
2468
+ '<span class="' + badgeClass(m.category) + '">' + escapeHtml(m.category) + '</span>' +
2469
+ (m.complexity ? '<span class="complexity">' + escapeHtml(m.complexity) + '</span>' : '') +
2470
+ '<span class="milestone-date">' + escapeHtml(date) + '</span>' +
2471
+ '</div>' +
2472
+ '</div>';
2473
+ }).join('');
2474
+ }
2475
+
2476
+ function renderSync(config) {
2477
+ var section = document.getElementById('sync-section');
2478
+ if (!config) { section.style.display = 'none'; return; }
2479
+ section.style.display = 'block';
2480
+
2481
+ if (config.authenticated) {
2482
+ var lastSync = config.last_sync_at ? 'Last sync: ' + config.last_sync_at : 'Never synced';
2483
+ section.innerHTML = '<div class="sync-section">' +
2484
+ '<h2 style="font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:12px;font-weight:600;">Sync</h2>' +
2485
+ '<button class="sync-btn" id="sync-btn">Sync to useai.dev</button>' +
2486
+ '<div class="sync-status" id="sync-status">' + escapeHtml(lastSync) + '</div>' +
2487
+ '<div class="sync-result" id="sync-result"></div>' +
2488
+ '</div>';
2489
+ document.getElementById('sync-btn').addEventListener('click', doSync);
2490
+ } else {
2491
+ section.innerHTML = '<div class="setup-msg">Login at <a href="https://useai.dev" target="_blank">useai.dev</a> to sync your data across devices</div>';
2492
+ }
2493
+ }
2494
+
2495
+ function doSync() {
2496
+ var btn = document.getElementById('sync-btn');
2497
+ var result = document.getElementById('sync-result');
2498
+ var status = document.getElementById('sync-status');
2499
+ btn.disabled = true;
2500
+ btn.textContent = 'Syncing...';
2501
+ result.textContent = '';
2502
+ result.className = 'sync-result';
2503
+
2504
+ fetch(API + '/api/local/sync', { method: 'POST' })
2505
+ .then(function(r) { return r.json(); })
2506
+ .then(function(data) {
2507
+ btn.disabled = false;
2508
+ btn.textContent = 'Sync to useai.dev';
2509
+ if (data.success) {
2510
+ result.textContent = 'Synced successfully';
2511
+ result.className = 'sync-result success';
2512
+ if (data.last_sync_at) status.textContent = 'Last sync: ' + data.last_sync_at;
2513
+ } else {
2514
+ result.textContent = 'Sync failed: ' + (data.error || 'Unknown error');
2515
+ result.className = 'sync-result error';
2516
+ }
2517
+ })
2518
+ .catch(function(err) {
2519
+ btn.disabled = false;
2520
+ btn.textContent = 'Sync to useai.dev';
2521
+ result.textContent = 'Sync failed: ' + err.message;
2522
+ result.className = 'sync-result error';
2523
+ });
2524
+ }
2525
+
2526
+ function loadAll() {
2527
+ fetch(API + '/api/local/stats')
2528
+ .then(function(r) { return r.json(); })
2529
+ .then(function(stats) {
2530
+ animateCounter(document.getElementById('stat-hours'), stats.totalHours, 1);
2531
+ animateCounter(document.getElementById('stat-sessions'), stats.totalSessions);
2532
+ var streakEl = document.getElementById('stat-streak');
2533
+ streakEl.innerHTML = '';
2534
+ var numSpan = document.createElement('span');
2535
+ streakEl.appendChild(numSpan);
2536
+ animateCounter(numSpan, stats.currentStreak);
2537
+ var unitSpan = document.createElement('span');
2538
+ unitSpan.className = 'unit';
2539
+ unitSpan.textContent = ' days';
2540
+ streakEl.appendChild(unitSpan);
2541
+ animateCounter(document.getElementById('stat-files'), stats.filesTouched || 0);
2542
+
2543
+ if (Object.keys(stats.byClient || {}).length > 0) {
2544
+ document.getElementById('clients-section').style.display = 'block';
2545
+ renderBars('clients-bars', stats.byClient);
2546
+ }
2547
+ if (Object.keys(stats.byLanguage || {}).length > 0) {
2548
+ document.getElementById('languages-section').style.display = 'block';
2549
+ renderBars('languages-bars', stats.byLanguage);
2550
+ }
2551
+ if (Object.keys(stats.byTaskType || {}).length > 0) {
2552
+ document.getElementById('tasks-section').style.display = 'block';
2553
+ renderBars('tasks-bars', stats.byTaskType);
2554
+ }
2555
+ })
2556
+ .catch(function() {});
2557
+
2558
+ fetch(API + '/api/local/milestones')
2559
+ .then(function(r) { return r.json(); })
2560
+ .then(function(data) { renderMilestones(data); })
2561
+ .catch(function() {});
2562
+
2563
+ fetch(API + '/api/local/config')
2564
+ .then(function(r) { return r.json(); })
2565
+ .then(function(data) { renderSync(data); })
2566
+ .catch(function() {});
2567
+ }
2568
+
2569
+ loadAll();
2570
+ setInterval(loadAll, 30000);
2571
+ })();
2572
+ </script>
2573
+ </body>
2574
+ </html>`;
2575
+ }
2576
+ var init_html = __esm({
2577
+ "src/dashboard/html.ts"() {
2578
+ "use strict";
2579
+ }
2580
+ });
2581
+
2582
+ // src/dashboard/local-api.ts
2583
+ function json(res, status, data) {
2584
+ const body = JSON.stringify(data);
2585
+ res.writeHead(status, {
2586
+ "Content-Type": "application/json",
2587
+ "Access-Control-Allow-Origin": "*",
2588
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
2589
+ "Access-Control-Allow-Headers": "Content-Type"
2590
+ });
2591
+ res.end(body);
2592
+ }
2593
+ function readBody(req) {
2594
+ return new Promise((resolve, reject) => {
2595
+ const chunks = [];
2596
+ req.on("data", (chunk) => chunks.push(chunk));
2597
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
2598
+ req.on("error", reject);
2599
+ });
2600
+ }
2601
+ function calculateStreak(sessions2) {
2602
+ if (sessions2.length === 0) return 0;
2603
+ const days = /* @__PURE__ */ new Set();
2604
+ for (const s of sessions2) {
2605
+ days.add(s.started_at.slice(0, 10));
2606
+ }
2607
+ const sorted = [...days].sort().reverse();
2608
+ if (sorted.length === 0) return 0;
2609
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2610
+ const yesterday = new Date(Date.now() - 864e5).toISOString().slice(0, 10);
2611
+ if (sorted[0] !== today && sorted[0] !== yesterday) return 0;
2612
+ let streak = 1;
2613
+ for (let i = 1; i < sorted.length; i++) {
2614
+ const prev = new Date(sorted[i - 1]);
2615
+ const curr = new Date(sorted[i]);
2616
+ const diffDays = (prev.getTime() - curr.getTime()) / 864e5;
2617
+ if (diffDays === 1) {
2618
+ streak++;
2619
+ } else {
2620
+ break;
2621
+ }
2622
+ }
2623
+ return streak;
2624
+ }
2625
+ function handleLocalStats(_req, res) {
2626
+ try {
2627
+ const sessions2 = readJson(SESSIONS_FILE, []);
2628
+ let totalSeconds = 0;
2629
+ let filesTouched = 0;
2630
+ const byClient = {};
2631
+ const byLanguage = {};
2632
+ const byTaskType = {};
2633
+ for (const s of sessions2) {
2634
+ totalSeconds += s.duration_seconds;
2635
+ filesTouched += s.files_touched;
2636
+ byClient[s.client] = (byClient[s.client] ?? 0) + s.duration_seconds;
2637
+ for (const lang of s.languages) {
2638
+ byLanguage[lang] = (byLanguage[lang] ?? 0) + s.duration_seconds;
2639
+ }
2640
+ byTaskType[s.task_type] = (byTaskType[s.task_type] ?? 0) + s.duration_seconds;
2641
+ }
2642
+ json(res, 200, {
2643
+ totalHours: totalSeconds / 3600,
2644
+ totalSessions: sessions2.length,
2645
+ currentStreak: calculateStreak(sessions2),
2646
+ filesTouched,
2647
+ byClient,
2648
+ byLanguage,
2649
+ byTaskType
2650
+ });
2651
+ } catch (err2) {
2652
+ json(res, 500, { error: err2.message });
2653
+ }
2654
+ }
2655
+ function handleLocalMilestones(_req, res) {
2656
+ try {
2657
+ const milestones = readJson(MILESTONES_FILE, []);
2658
+ json(res, 200, milestones);
2659
+ } catch (err2) {
2660
+ json(res, 500, { error: err2.message });
2661
+ }
2662
+ }
2663
+ function handleLocalConfig(_req, res) {
2664
+ try {
2665
+ const config = readJson(CONFIG_FILE, {
2666
+ milestone_tracking: true,
2667
+ auto_sync: false,
2668
+ sync_interval_hours: 24
2669
+ });
2670
+ json(res, 200, {
2671
+ authenticated: !!config.auth?.token,
2672
+ email: config.auth?.user?.email ?? null,
2673
+ username: config.auth?.user?.username ?? null,
2674
+ last_sync_at: config.last_sync_at ?? null,
2675
+ auto_sync: config.auto_sync
2676
+ });
2677
+ } catch (err2) {
2678
+ json(res, 500, { error: err2.message });
2679
+ }
2680
+ }
2681
+ async function handleLocalSync(req, res) {
2682
+ try {
2683
+ await readBody(req);
2684
+ const config = readJson(CONFIG_FILE, {
2685
+ milestone_tracking: true,
2686
+ auto_sync: false,
2687
+ sync_interval_hours: 24
2688
+ });
2689
+ if (!config.auth?.token) {
2690
+ json(res, 401, { success: false, error: "Not authenticated. Login at useai.dev first." });
2691
+ return;
2692
+ }
2693
+ const token = config.auth.token;
2694
+ const headers = {
2695
+ "Content-Type": "application/json",
2696
+ "Authorization": `Bearer ${token}`
2697
+ };
2698
+ const sessions2 = readJson(SESSIONS_FILE, []);
2699
+ const sessionsRes = await fetch("https://api.useai.dev/api/sync", {
2700
+ method: "POST",
2701
+ headers,
2702
+ body: JSON.stringify({ sessions: sessions2 })
2703
+ });
2704
+ if (!sessionsRes.ok) {
2705
+ const errBody = await sessionsRes.text();
2706
+ json(res, 502, { success: false, error: `Sessions sync failed: ${sessionsRes.status} ${errBody}` });
2707
+ return;
2708
+ }
2709
+ const milestones = readJson(MILESTONES_FILE, []);
2710
+ const milestonesRes = await fetch("https://api.useai.dev/api/publish", {
2711
+ method: "POST",
2712
+ headers,
2713
+ body: JSON.stringify({ milestones })
2714
+ });
2715
+ if (!milestonesRes.ok) {
2716
+ const errBody = await milestonesRes.text();
2717
+ json(res, 502, { success: false, error: `Milestones publish failed: ${milestonesRes.status} ${errBody}` });
2718
+ return;
2719
+ }
2720
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2721
+ config.last_sync_at = now;
2722
+ writeJson(CONFIG_FILE, config);
2723
+ json(res, 200, { success: true, last_sync_at: now });
2724
+ } catch (err2) {
2725
+ json(res, 500, { success: false, error: err2.message });
2726
+ }
2727
+ }
2728
+ var init_local_api = __esm({
2729
+ "src/dashboard/local-api.ts"() {
2730
+ "use strict";
2731
+ init_dist();
2732
+ }
2733
+ });
2734
+
1957
2735
  // src/daemon.ts
1958
2736
  var daemon_exports = {};
1959
2737
  __export(daemon_exports, {
@@ -1961,11 +2739,125 @@ __export(daemon_exports, {
1961
2739
  });
1962
2740
  import { createServer } from "http";
1963
2741
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
1964
- import { existsSync as existsSync8, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
1965
- import { join as join6 } from "path";
2742
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync5, appendFileSync as appendFileSync2, renameSync as renameSync3, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5 } from "fs";
2743
+ import { join as join7 } from "path";
1966
2744
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1967
2745
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1968
2746
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
2747
+ function getActiveUseaiSessionIds() {
2748
+ const ids = /* @__PURE__ */ new Set();
2749
+ for (const [, active] of sessions) {
2750
+ ids.add(active.session.sessionId);
2751
+ }
2752
+ return ids;
2753
+ }
2754
+ function sealOrphanFile(sessionId) {
2755
+ const filePath = join7(ACTIVE_DIR, `${sessionId}.jsonl`);
2756
+ if (!existsSync9(filePath)) return;
2757
+ try {
2758
+ const content = readFileSync5(filePath, "utf-8").trim();
2759
+ if (!content) return;
2760
+ const lines = content.split("\n").filter(Boolean);
2761
+ if (lines.length === 0) return;
2762
+ const firstRecord = JSON.parse(lines[0]);
2763
+ const lastRecord = JSON.parse(lines[lines.length - 1]);
2764
+ if (lastRecord.type === "session_end" || lastRecord.type === "session_seal") {
2765
+ try {
2766
+ renameSync3(filePath, join7(SEALED_DIR, `${sessionId}.jsonl`));
2767
+ } catch {
2768
+ }
2769
+ return;
2770
+ }
2771
+ const startData = firstRecord.data;
2772
+ const client = startData["client"] ?? "unknown";
2773
+ const taskType = startData["task_type"] ?? "coding";
2774
+ const startTime = firstRecord.timestamp;
2775
+ let heartbeatCount = 0;
2776
+ for (const line of lines) {
2777
+ try {
2778
+ if (JSON.parse(line).type === "heartbeat") heartbeatCount++;
2779
+ } catch {
2780
+ }
2781
+ }
2782
+ const chainTip = lastRecord.hash;
2783
+ const duration = Math.round((Date.now() - new Date(startTime).getTime()) / 1e3);
2784
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2785
+ const endRecord = buildChainRecord("session_end", sessionId, {
2786
+ duration_seconds: duration,
2787
+ task_type: taskType,
2788
+ languages: [],
2789
+ files_touched: 0,
2790
+ heartbeat_count: heartbeatCount,
2791
+ auto_sealed: true
2792
+ }, chainTip, daemonSigningKey);
2793
+ appendFileSync2(filePath, JSON.stringify(endRecord) + "\n");
2794
+ const sealData = JSON.stringify({
2795
+ session_id: sessionId,
2796
+ client,
2797
+ task_type: taskType,
2798
+ languages: [],
2799
+ files_touched: 0,
2800
+ started_at: startTime,
2801
+ ended_at: now,
2802
+ duration_seconds: duration,
2803
+ heartbeat_count: heartbeatCount,
2804
+ record_count: lines.length + 2,
2805
+ chain_end_hash: endRecord.hash
2806
+ });
2807
+ const sealSignature = signHash(
2808
+ createHash4("sha256").update(sealData).digest("hex"),
2809
+ daemonSigningKey
2810
+ );
2811
+ appendFileSync2(filePath, JSON.stringify(
2812
+ buildChainRecord("session_seal", sessionId, {
2813
+ seal: sealData,
2814
+ seal_signature: sealSignature,
2815
+ auto_sealed: true
2816
+ }, endRecord.hash, daemonSigningKey)
2817
+ ) + "\n");
2818
+ try {
2819
+ renameSync3(filePath, join7(SEALED_DIR, `${sessionId}.jsonl`));
2820
+ } catch {
2821
+ }
2822
+ const seal = {
2823
+ session_id: sessionId,
2824
+ client,
2825
+ task_type: taskType,
2826
+ languages: [],
2827
+ files_touched: 0,
2828
+ started_at: startTime,
2829
+ ended_at: now,
2830
+ duration_seconds: duration,
2831
+ heartbeat_count: heartbeatCount,
2832
+ record_count: lines.length + 2,
2833
+ chain_start_hash: firstRecord.prev_hash,
2834
+ chain_end_hash: endRecord.hash,
2835
+ seal_signature: sealSignature
2836
+ };
2837
+ const allSessions = readJson(SESSIONS_FILE, []);
2838
+ allSessions.push(seal);
2839
+ writeJson(SESSIONS_FILE, allSessions);
2840
+ } catch {
2841
+ }
2842
+ }
2843
+ function sealOrphanedSessions() {
2844
+ if (!existsSync9(ACTIVE_DIR)) return;
2845
+ const activeIds = getActiveUseaiSessionIds();
2846
+ let sealed = 0;
2847
+ try {
2848
+ const files = readdirSync(ACTIVE_DIR).filter((f) => f.endsWith(".jsonl"));
2849
+ for (const file of files) {
2850
+ const sessionId = file.replace(".jsonl", "");
2851
+ if (activeIds.has(sessionId)) continue;
2852
+ sealOrphanFile(sessionId);
2853
+ sealed++;
2854
+ }
2855
+ } catch {
2856
+ }
2857
+ if (sealed > 0) {
2858
+ console.log(`Sealed ${sealed} orphaned session${sealed === 1 ? "" : "s"}`);
2859
+ }
2860
+ }
1969
2861
  function autoSealSession(active) {
1970
2862
  const { session: session2 } = active;
1971
2863
  if (session2.sessionRecordCount === 0) return;
@@ -2001,10 +2893,10 @@ function autoSealSession(active) {
2001
2893
  seal_signature: sealSignature,
2002
2894
  auto_sealed: true
2003
2895
  });
2004
- const activePath = join6(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
2005
- const sealedPath = join6(SEALED_DIR, `${session2.sessionId}.jsonl`);
2896
+ const activePath = join7(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
2897
+ const sealedPath = join7(SEALED_DIR, `${session2.sessionId}.jsonl`);
2006
2898
  try {
2007
- if (existsSync8(activePath)) {
2899
+ if (existsSync9(activePath)) {
2008
2900
  renameSync3(activePath, sealedPath);
2009
2901
  }
2010
2902
  } catch {
@@ -2081,12 +2973,71 @@ function parseBody(req) {
2081
2973
  async function startDaemon(port) {
2082
2974
  const listenPort = port ?? DAEMON_PORT;
2083
2975
  ensureDir();
2976
+ try {
2977
+ if (existsSync9(KEYSTORE_FILE)) {
2978
+ const ks = readJson(KEYSTORE_FILE, null);
2979
+ if (ks) daemonSigningKey = decryptKeystore(ks);
2980
+ }
2981
+ if (!daemonSigningKey) {
2982
+ const result = generateKeystore();
2983
+ writeJson(KEYSTORE_FILE, result.keystore);
2984
+ daemonSigningKey = result.signingKey;
2985
+ }
2986
+ } catch {
2987
+ }
2988
+ sealOrphanedSessions();
2989
+ const sweepInterval = setInterval(sealOrphanedSessions, ORPHAN_SWEEP_INTERVAL_MS);
2990
+ sweepInterval.unref();
2084
2991
  const server2 = createServer(async (req, res) => {
2085
2992
  const url = new URL(req.url, `http://${req.headers.host}`);
2086
2993
  if (url.pathname === "/health" && req.method === "GET") {
2087
2994
  handleHealth(res);
2088
2995
  return;
2089
2996
  }
2997
+ if ((url.pathname === "/" || url.pathname === "/dashboard") && req.method === "GET") {
2998
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2999
+ res.end(getDashboardHtml());
3000
+ return;
3001
+ }
3002
+ if (url.pathname.startsWith("/api/local/") && req.method === "GET") {
3003
+ if (url.pathname === "/api/local/stats") {
3004
+ handleLocalStats(req, res);
3005
+ return;
3006
+ }
3007
+ if (url.pathname === "/api/local/milestones") {
3008
+ handleLocalMilestones(req, res);
3009
+ return;
3010
+ }
3011
+ if (url.pathname === "/api/local/config") {
3012
+ handleLocalConfig(req, res);
3013
+ return;
3014
+ }
3015
+ }
3016
+ if (url.pathname === "/api/local/sync" && req.method === "POST") {
3017
+ await handleLocalSync(req, res);
3018
+ return;
3019
+ }
3020
+ if (url.pathname === "/api/seal-active" && req.method === "POST") {
3021
+ const sids = [...sessions.keys()];
3022
+ for (const sid of sids) {
3023
+ await cleanupSession(sid);
3024
+ }
3025
+ res.writeHead(200, {
3026
+ "Content-Type": "application/json",
3027
+ "Access-Control-Allow-Origin": "*"
3028
+ });
3029
+ res.end(JSON.stringify({ sealed: sids.length }));
3030
+ return;
3031
+ }
3032
+ if ((url.pathname.startsWith("/api/local/") || url.pathname === "/api/seal-active") && req.method === "OPTIONS") {
3033
+ res.writeHead(204, {
3034
+ "Access-Control-Allow-Origin": "*",
3035
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3036
+ "Access-Control-Allow-Headers": "Content-Type"
3037
+ });
3038
+ res.end();
3039
+ return;
3040
+ }
2090
3041
  if (url.pathname !== "/mcp") {
2091
3042
  res.writeHead(404, { "Content-Type": "application/json" });
2092
3043
  res.end(JSON.stringify({ error: "Not found" }));
@@ -2120,6 +3071,10 @@ async function startDaemon(port) {
2120
3071
  const transport = new StreamableHTTPServerTransport({
2121
3072
  sessionIdGenerator: () => randomUUID4(),
2122
3073
  onsessioninitialized: (newSid) => {
3074
+ const clientInfo = mcpServer.server.getClientVersion();
3075
+ if (clientInfo?.name) {
3076
+ sessionState.setClient(normalizeMcpClientName(clientInfo.name));
3077
+ }
2123
3078
  const idleTimer = setTimeout(async () => {
2124
3079
  await cleanupSession(newSid);
2125
3080
  }, IDLE_TIMEOUT_MS);
@@ -2187,14 +3142,14 @@ async function startDaemon(port) {
2187
3142
  port: listenPort,
2188
3143
  started_at: (/* @__PURE__ */ new Date()).toISOString()
2189
3144
  });
2190
- writeFileSync4(DAEMON_PID_FILE, pidData + "\n");
3145
+ writeFileSync5(DAEMON_PID_FILE, pidData + "\n");
2191
3146
  const shutdown = async (signal) => {
2192
3147
  for (const [sid] of sessions) {
2193
3148
  await cleanupSession(sid);
2194
3149
  }
2195
3150
  try {
2196
- if (existsSync8(DAEMON_PID_FILE)) {
2197
- unlinkSync4(DAEMON_PID_FILE);
3151
+ if (existsSync9(DAEMON_PID_FILE)) {
3152
+ unlinkSync5(DAEMON_PID_FILE);
2198
3153
  }
2199
3154
  } catch {
2200
3155
  }
@@ -2210,15 +3165,19 @@ async function startDaemon(port) {
2210
3165
  console.log(`PID: ${process.pid}`);
2211
3166
  });
2212
3167
  }
2213
- var IDLE_TIMEOUT_MS, sessions, startedAt;
3168
+ var IDLE_TIMEOUT_MS, sessions, daemonSigningKey, ORPHAN_SWEEP_INTERVAL_MS, startedAt;
2214
3169
  var init_daemon2 = __esm({
2215
3170
  "src/daemon.ts"() {
2216
3171
  "use strict";
2217
3172
  init_dist();
2218
3173
  init_session_state();
2219
3174
  init_register_tools();
3175
+ init_html();
3176
+ init_local_api();
2220
3177
  IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
2221
3178
  sessions = /* @__PURE__ */ new Map();
3179
+ daemonSigningKey = null;
3180
+ ORPHAN_SWEEP_INTERVAL_MS = 15 * 60 * 1e3;
2222
3181
  startedAt = Date.now();
2223
3182
  }
2224
3183
  });