@devness/useai 0.4.4 → 0.4.6

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 +651 -96
  3. package/package.json +11 -12
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 useai.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.4";
115
+ VERSION = "0.4.6";
115
116
  }
116
117
  });
117
118
 
@@ -836,6 +837,136 @@ var init_daemon = __esm({
836
837
  }
837
838
  });
838
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
+
839
970
  // ../shared/dist/index.js
840
971
  var init_dist = __esm({
841
972
  "../shared/dist/index.js"() {
@@ -846,12 +977,13 @@ var init_dist = __esm({
846
977
  init_validation();
847
978
  init_utils();
848
979
  init_daemon();
980
+ init_hooks();
849
981
  }
850
982
  });
851
983
 
852
984
  // src/session-state.ts
853
- import { appendFileSync, existsSync as existsSync5 } from "fs";
854
- import { join as join3 } from "path";
985
+ import { appendFileSync, existsSync as existsSync6 } from "fs";
986
+ import { join as join4 } from "path";
855
987
  var SessionState;
856
988
  var init_session_state = __esm({
857
989
  "src/session-state.ts"() {
@@ -901,7 +1033,7 @@ var init_session_state = __esm({
901
1033
  }
902
1034
  initializeKeystore() {
903
1035
  ensureDir();
904
- if (existsSync5(KEYSTORE_FILE)) {
1036
+ if (existsSync6(KEYSTORE_FILE)) {
905
1037
  const ks = readJson(KEYSTORE_FILE, null);
906
1038
  if (ks) {
907
1039
  try {
@@ -919,7 +1051,7 @@ var init_session_state = __esm({
919
1051
  }
920
1052
  /** Path to this session's chain file in the active directory */
921
1053
  sessionChainPath() {
922
- return join3(ACTIVE_DIR, `${this.sessionId}.jsonl`);
1054
+ return join4(ACTIVE_DIR, `${this.sessionId}.jsonl`);
923
1055
  }
924
1056
  appendToChain(type, data) {
925
1057
  const record = buildChainRecord(type, this.sessionId, data, this.chainTipHash, this.signingKey);
@@ -936,8 +1068,8 @@ var init_session_state = __esm({
936
1068
  // src/register-tools.ts
937
1069
  import { z as z2 } from "zod";
938
1070
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
939
- import { existsSync as existsSync6, renameSync as renameSync2 } from "fs";
940
- import { join as join4 } from "path";
1071
+ import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
1072
+ import { join as join5 } from "path";
941
1073
  function getConfig() {
942
1074
  return readJson(CONFIG_FILE, {
943
1075
  milestone_tracking: true,
@@ -1043,10 +1175,10 @@ Session: ${session2.sessionId.slice(0, 8)} \xB7 Chain: ${record.hash.slice(0, 12
1043
1175
  seal: sealData,
1044
1176
  seal_signature: sealSignature
1045
1177
  });
1046
- const activePath = join4(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
1047
- 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`);
1048
1180
  try {
1049
- if (existsSync6(activePath)) {
1181
+ if (existsSync7(activePath)) {
1050
1182
  renameSync2(activePath, sealedPath);
1051
1183
  }
1052
1184
  } catch {
@@ -1126,9 +1258,9 @@ var init_register_tools = __esm({
1126
1258
 
1127
1259
  // src/tools.ts
1128
1260
  import { execSync as execSync3 } from "child_process";
1129
- import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3 } from "fs";
1130
- import { dirname as dirname2, join as join5 } from "path";
1131
- 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";
1132
1264
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
1133
1265
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1134
1266
  function installStandardHttp(configPath) {
@@ -1156,9 +1288,9 @@ function hasBinary(name) {
1156
1288
  }
1157
1289
  }
1158
1290
  function readJsonFile(path) {
1159
- if (!existsSync7(path)) return {};
1291
+ if (!existsSync8(path)) return {};
1160
1292
  try {
1161
- const raw = readFileSync3(path, "utf-8").trim();
1293
+ const raw = readFileSync4(path, "utf-8").trim();
1162
1294
  if (!raw) return {};
1163
1295
  return JSON.parse(raw);
1164
1296
  } catch {
@@ -1166,8 +1298,8 @@ function readJsonFile(path) {
1166
1298
  }
1167
1299
  }
1168
1300
  function writeJsonFile(path, data) {
1169
- mkdirSync3(dirname2(path), { recursive: true });
1170
- writeFileSync3(path, JSON.stringify(data, null, 2) + "\n");
1301
+ mkdirSync4(dirname2(path), { recursive: true });
1302
+ writeFileSync4(path, JSON.stringify(data, null, 2) + "\n");
1171
1303
  }
1172
1304
  function isConfiguredStandard(configPath) {
1173
1305
  const config = readJsonFile(configPath);
@@ -1248,9 +1380,9 @@ function removeZed(configPath) {
1248
1380
  }
1249
1381
  }
1250
1382
  function readTomlFile(path) {
1251
- if (!existsSync7(path)) return {};
1383
+ if (!existsSync8(path)) return {};
1252
1384
  try {
1253
- const raw = readFileSync3(path, "utf-8").trim();
1385
+ const raw = readFileSync4(path, "utf-8").trim();
1254
1386
  if (!raw) return {};
1255
1387
  return parseToml(raw);
1256
1388
  } catch {
@@ -1258,8 +1390,8 @@ function readTomlFile(path) {
1258
1390
  }
1259
1391
  }
1260
1392
  function writeTomlFile(path, data) {
1261
- mkdirSync3(dirname2(path), { recursive: true });
1262
- writeFileSync3(path, stringifyToml(data) + "\n");
1393
+ mkdirSync4(dirname2(path), { recursive: true });
1394
+ writeFileSync4(path, stringifyToml(data) + "\n");
1263
1395
  }
1264
1396
  function isConfiguredToml(configPath) {
1265
1397
  const config = readTomlFile(configPath);
@@ -1287,9 +1419,9 @@ function removeToml(configPath) {
1287
1419
  }
1288
1420
  }
1289
1421
  function readYamlFile(path) {
1290
- if (!existsSync7(path)) return {};
1422
+ if (!existsSync8(path)) return {};
1291
1423
  try {
1292
- const raw = readFileSync3(path, "utf-8").trim();
1424
+ const raw = readFileSync4(path, "utf-8").trim();
1293
1425
  if (!raw) return {};
1294
1426
  return parseYaml(raw) ?? {};
1295
1427
  } catch {
@@ -1297,8 +1429,8 @@ function readYamlFile(path) {
1297
1429
  }
1298
1430
  }
1299
1431
  function writeYamlFile(path, data) {
1300
- mkdirSync3(dirname2(path), { recursive: true });
1301
- writeFileSync3(path, stringifyYaml(data));
1432
+ mkdirSync4(dirname2(path), { recursive: true });
1433
+ writeFileSync4(path, stringifyYaml(data));
1302
1434
  }
1303
1435
  function isConfiguredYaml(configPath) {
1304
1436
  const config = readYamlFile(configPath);
@@ -1332,49 +1464,49 @@ function removeYaml(configPath) {
1332
1464
  }
1333
1465
  }
1334
1466
  function hasInstructionsBlock(filePath) {
1335
- if (!existsSync7(filePath)) return false;
1467
+ if (!existsSync8(filePath)) return false;
1336
1468
  try {
1337
- return readFileSync3(filePath, "utf-8").includes(INSTRUCTIONS_START);
1469
+ return readFileSync4(filePath, "utf-8").includes(INSTRUCTIONS_START);
1338
1470
  } catch {
1339
1471
  return false;
1340
1472
  }
1341
1473
  }
1342
1474
  function injectInstructions(config) {
1343
- mkdirSync3(dirname2(config.path), { recursive: true });
1475
+ mkdirSync4(dirname2(config.path), { recursive: true });
1344
1476
  if (config.method === "create") {
1345
- writeFileSync3(config.path, USEAI_INSTRUCTIONS + "\n");
1477
+ writeFileSync4(config.path, USEAI_INSTRUCTIONS + "\n");
1346
1478
  return;
1347
1479
  }
1348
1480
  if (hasInstructionsBlock(config.path)) return;
1349
1481
  let existing = "";
1350
- if (existsSync7(config.path)) {
1351
- existing = readFileSync3(config.path, "utf-8");
1482
+ if (existsSync8(config.path)) {
1483
+ existing = readFileSync4(config.path, "utf-8");
1352
1484
  }
1353
1485
  const separator = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
1354
- writeFileSync3(config.path, existing + separator + USEAI_INSTRUCTIONS_BLOCK + "\n");
1486
+ writeFileSync4(config.path, existing + separator + USEAI_INSTRUCTIONS_BLOCK + "\n");
1355
1487
  }
1356
1488
  function removeInstructions(config) {
1357
1489
  if (config.method === "create") {
1358
- if (existsSync7(config.path)) {
1490
+ if (existsSync8(config.path)) {
1359
1491
  try {
1360
- unlinkSync3(config.path);
1492
+ unlinkSync4(config.path);
1361
1493
  } catch {
1362
1494
  }
1363
1495
  }
1364
1496
  return;
1365
1497
  }
1366
- if (!existsSync7(config.path)) return;
1498
+ if (!existsSync8(config.path)) return;
1367
1499
  try {
1368
- const content = readFileSync3(config.path, "utf-8");
1500
+ const content = readFileSync4(config.path, "utf-8");
1369
1501
  const escaped = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1370
1502
  const regex = new RegExp(
1371
1503
  `\\n?${escaped(INSTRUCTIONS_START)}[\\s\\S]*?${escaped(INSTRUCTIONS_END)}\\n?`
1372
1504
  );
1373
1505
  const cleaned = content.replace(regex, "").trim();
1374
1506
  if (cleaned) {
1375
- writeFileSync3(config.path, cleaned + "\n");
1507
+ writeFileSync4(config.path, cleaned + "\n");
1376
1508
  } else {
1377
- unlinkSync3(config.path);
1509
+ unlinkSync4(config.path);
1378
1510
  }
1379
1511
  } catch {
1380
1512
  }
@@ -1450,7 +1582,7 @@ var init_tools = __esm({
1450
1582
  };
1451
1583
  MCP_HTTP_URL = DAEMON_MCP_URL;
1452
1584
  MCP_HTTP_ENTRY = { type: "http", url: MCP_HTTP_URL };
1453
- home = homedir3();
1585
+ home = homedir4();
1454
1586
  INSTRUCTIONS_START = "<!-- useai:start -->";
1455
1587
  INSTRUCTIONS_END = "<!-- useai:end -->";
1456
1588
  USEAI_INSTRUCTIONS = [
@@ -1470,23 +1602,23 @@ ${INSTRUCTIONS_END}`;
1470
1602
  toml: { isConfigured: isConfiguredToml, install: installToml, remove: removeToml },
1471
1603
  yaml: { isConfigured: isConfiguredYaml, install: installYaml, remove: removeYaml }
1472
1604
  };
1473
- appSupport = join5(home, "Library", "Application Support");
1605
+ appSupport = join6(home, "Library", "Application Support");
1474
1606
  AI_TOOLS = [
1475
1607
  createTool({
1476
1608
  id: "claude-code",
1477
1609
  name: "Claude Code",
1478
1610
  configFormat: "standard",
1479
- configPath: join5(home, ".claude.json"),
1480
- detect: () => hasBinary("claude") || existsSync7(join5(home, ".claude.json")),
1481
- 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") },
1482
1614
  supportsUrl: true
1483
1615
  }),
1484
1616
  createTool({
1485
1617
  id: "cursor",
1486
1618
  name: "Cursor",
1487
1619
  configFormat: "standard",
1488
- configPath: join5(home, ".cursor", "mcp.json"),
1489
- detect: () => existsSync7(join5(home, ".cursor")),
1620
+ configPath: join6(home, ".cursor", "mcp.json"),
1621
+ detect: () => existsSync8(join6(home, ".cursor")),
1490
1622
  manualHint: "Open Cursor Settings \u2192 Rules \u2192 User Rules and paste the instructions below.",
1491
1623
  supportsUrl: true
1492
1624
  }),
@@ -1494,51 +1626,51 @@ ${INSTRUCTIONS_END}`;
1494
1626
  id: "windsurf",
1495
1627
  name: "Windsurf",
1496
1628
  configFormat: "standard",
1497
- configPath: join5(home, ".codeium", "windsurf", "mcp_config.json"),
1498
- detect: () => existsSync7(join5(home, ".codeium", "windsurf")),
1499
- 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") },
1500
1632
  supportsUrl: true
1501
1633
  }),
1502
1634
  createTool({
1503
1635
  id: "vscode",
1504
1636
  name: "VS Code",
1505
1637
  configFormat: "vscode",
1506
- configPath: join5(appSupport, "Code", "User", "mcp.json"),
1507
- detect: () => existsSync7(join5(appSupport, "Code")),
1508
- 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") },
1509
1641
  supportsUrl: true
1510
1642
  }),
1511
1643
  createTool({
1512
1644
  id: "vscode-insiders",
1513
1645
  name: "VS Code Insiders",
1514
1646
  configFormat: "vscode",
1515
- configPath: join5(appSupport, "Code - Insiders", "User", "mcp.json"),
1516
- detect: () => existsSync7(join5(appSupport, "Code - Insiders")),
1517
- 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") },
1518
1650
  supportsUrl: true
1519
1651
  }),
1520
1652
  createTool({
1521
1653
  id: "gemini-cli",
1522
1654
  name: "Gemini CLI",
1523
1655
  configFormat: "standard",
1524
- configPath: join5(home, ".gemini", "settings.json"),
1656
+ configPath: join6(home, ".gemini", "settings.json"),
1525
1657
  detect: () => hasBinary("gemini"),
1526
- instructions: { method: "append", path: join5(home, ".gemini", "GEMINI.md") },
1658
+ instructions: { method: "append", path: join6(home, ".gemini", "GEMINI.md") },
1527
1659
  supportsUrl: true
1528
1660
  }),
1529
1661
  createTool({
1530
1662
  id: "zed",
1531
1663
  name: "Zed",
1532
1664
  configFormat: "zed",
1533
- configPath: join5(appSupport, "Zed", "settings.json"),
1534
- detect: () => existsSync7(join5(appSupport, "Zed")),
1665
+ configPath: join6(appSupport, "Zed", "settings.json"),
1666
+ detect: () => existsSync8(join6(appSupport, "Zed")),
1535
1667
  manualHint: "Open Rules Library (\u2318\u2325L) \u2192 click + \u2192 paste the instructions below."
1536
1668
  }),
1537
1669
  createTool({
1538
1670
  id: "cline",
1539
1671
  name: "Cline",
1540
1672
  configFormat: "standard",
1541
- configPath: join5(
1673
+ configPath: join6(
1542
1674
  appSupport,
1543
1675
  "Code",
1544
1676
  "User",
@@ -1547,17 +1679,17 @@ ${INSTRUCTIONS_END}`;
1547
1679
  "settings",
1548
1680
  "cline_mcp_settings.json"
1549
1681
  ),
1550
- detect: () => existsSync7(
1551
- join5(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
1682
+ detect: () => existsSync8(
1683
+ join6(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
1552
1684
  ),
1553
- instructions: { method: "create", path: join5(home, "Documents", "Cline", "Rules", "useai.md") },
1685
+ instructions: { method: "create", path: join6(home, "Documents", "Cline", "Rules", "useai.md") },
1554
1686
  supportsUrl: true
1555
1687
  }),
1556
1688
  createTool({
1557
1689
  id: "roo-code",
1558
1690
  name: "Roo Code",
1559
1691
  configFormat: "standard",
1560
- configPath: join5(
1692
+ configPath: join6(
1561
1693
  appSupport,
1562
1694
  "Code",
1563
1695
  "User",
@@ -1566,59 +1698,59 @@ ${INSTRUCTIONS_END}`;
1566
1698
  "settings",
1567
1699
  "cline_mcp_settings.json"
1568
1700
  ),
1569
- detect: () => existsSync7(
1570
- join5(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
1701
+ detect: () => existsSync8(
1702
+ join6(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
1571
1703
  ),
1572
- instructions: { method: "create", path: join5(home, ".roo", "rules", "useai.md") },
1704
+ instructions: { method: "create", path: join6(home, ".roo", "rules", "useai.md") },
1573
1705
  supportsUrl: true
1574
1706
  }),
1575
1707
  createTool({
1576
1708
  id: "amazon-q-cli",
1577
1709
  name: "Amazon Q CLI",
1578
1710
  configFormat: "standard",
1579
- configPath: join5(home, ".aws", "amazonq", "mcp.json"),
1580
- 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")),
1581
1713
  manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
1582
1714
  }),
1583
1715
  createTool({
1584
1716
  id: "amazon-q-ide",
1585
1717
  name: "Amazon Q IDE",
1586
1718
  configFormat: "standard",
1587
- configPath: join5(home, ".aws", "amazonq", "default.json"),
1588
- 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")),
1589
1721
  manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
1590
1722
  }),
1591
1723
  createTool({
1592
1724
  id: "codex",
1593
1725
  name: "Codex",
1594
1726
  configFormat: "toml",
1595
- configPath: join5(home, ".codex", "config.toml"),
1596
- detect: () => hasBinary("codex") || existsSync7(join5(home, ".codex")) || existsSync7("/Applications/Codex.app"),
1597
- 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") }
1598
1730
  }),
1599
1731
  createTool({
1600
1732
  id: "goose",
1601
1733
  name: "Goose",
1602
1734
  configFormat: "yaml",
1603
- configPath: join5(home, ".config", "goose", "config.yaml"),
1604
- detect: () => existsSync7(join5(home, ".config", "goose")),
1605
- 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") }
1606
1738
  }),
1607
1739
  createTool({
1608
1740
  id: "opencode",
1609
1741
  name: "OpenCode",
1610
1742
  configFormat: "standard",
1611
- configPath: join5(home, ".config", "opencode", "opencode.json"),
1612
- detect: () => hasBinary("opencode") || existsSync7(join5(home, ".config", "opencode")),
1613
- 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") },
1614
1746
  supportsUrl: true
1615
1747
  }),
1616
1748
  createTool({
1617
1749
  id: "junie",
1618
1750
  name: "Junie",
1619
1751
  configFormat: "standard",
1620
- configPath: join5(home, ".junie", "mcp", "mcp.json"),
1621
- detect: () => existsSync7(join5(home, ".junie")),
1752
+ configPath: join6(home, ".junie", "mcp", "mcp.json"),
1753
+ detect: () => existsSync8(join6(home, ".junie")),
1622
1754
  manualHint: "Add the instructions below to .junie/guidelines.md in your project root."
1623
1755
  })
1624
1756
  ];
@@ -1734,6 +1866,14 @@ async function daemonInstallFlow(tools, explicit) {
1734
1866
  console.log(err(`\u2717 ${tool.name.padEnd(18)} \u2014 ${e.message}`));
1735
1867
  }
1736
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
+ }
1737
1877
  showManualHints(targetTools);
1738
1878
  const mode = useDaemon ? "daemon mode" : "stdio mode";
1739
1879
  console.log(`
@@ -1908,6 +2048,11 @@ async function fullRemoveFlow(tools, autoYes, explicit) {
1908
2048
  }
1909
2049
  }
1910
2050
  }
2051
+ try {
2052
+ removeClaudeCodeHooks();
2053
+ console.log(ok("\u2713 Claude Code hooks removed"));
2054
+ } catch {
2055
+ }
1911
2056
  console.log();
1912
2057
  try {
1913
2058
  await killDaemon();
@@ -2157,6 +2302,12 @@ function getDashboardHtml() {
2157
2302
  color: var(--muted);
2158
2303
  font-family: 'SF Mono', 'Fira Code', monospace;
2159
2304
  }
2305
+ .milestone-duration {
2306
+ font-size: 0.7rem;
2307
+ color: var(--muted);
2308
+ font-family: 'SF Mono', 'Fira Code', monospace;
2309
+ white-space: nowrap;
2310
+ }
2160
2311
  .milestone-date {
2161
2312
  font-size: 0.72rem;
2162
2313
  color: var(--muted);
@@ -2203,6 +2354,77 @@ function getDashboardHtml() {
2203
2354
  text-decoration: underline;
2204
2355
  }
2205
2356
 
2357
+ /* Login form */
2358
+ .login-form {
2359
+ max-width: 320px;
2360
+ margin: 0 auto;
2361
+ text-align: center;
2362
+ }
2363
+ .login-form h3 {
2364
+ font-size: 0.95rem;
2365
+ color: var(--text);
2366
+ margin-bottom: 4px;
2367
+ font-weight: 600;
2368
+ }
2369
+ .login-form .login-sub {
2370
+ font-size: 0.8rem;
2371
+ color: var(--muted);
2372
+ margin-bottom: 16px;
2373
+ }
2374
+ .login-input {
2375
+ width: 100%;
2376
+ padding: 10px 12px;
2377
+ background: var(--bg);
2378
+ border: 1px solid var(--border);
2379
+ border-radius: var(--radius);
2380
+ color: var(--text);
2381
+ font-size: 0.88rem;
2382
+ font-family: system-ui, sans-serif;
2383
+ outline: none;
2384
+ margin-bottom: 10px;
2385
+ }
2386
+ .login-input:focus { border-color: var(--amber); }
2387
+ .login-input::placeholder { color: #5a5248; }
2388
+ .login-input.otp-input {
2389
+ text-align: center;
2390
+ font-family: 'SF Mono', 'Fira Code', monospace;
2391
+ font-size: 1.2rem;
2392
+ letter-spacing: 6px;
2393
+ }
2394
+ .login-btn {
2395
+ width: 100%;
2396
+ padding: 10px;
2397
+ background: var(--amber);
2398
+ color: var(--bg);
2399
+ border: none;
2400
+ border-radius: var(--radius);
2401
+ font-weight: 600;
2402
+ font-size: 0.88rem;
2403
+ cursor: pointer;
2404
+ font-family: system-ui, sans-serif;
2405
+ transition: opacity 0.15s;
2406
+ }
2407
+ .login-btn:hover { opacity: 0.85; }
2408
+ .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
2409
+ .login-msg {
2410
+ margin-top: 10px;
2411
+ font-size: 0.8rem;
2412
+ }
2413
+ .login-msg.error { color: var(--red); }
2414
+ .login-msg.success { color: var(--green); }
2415
+ .login-msg.dim { color: var(--muted); }
2416
+ .login-link {
2417
+ background: none;
2418
+ border: none;
2419
+ color: var(--amber);
2420
+ cursor: pointer;
2421
+ font-size: 0.8rem;
2422
+ font-family: system-ui, sans-serif;
2423
+ text-decoration: underline;
2424
+ padding: 0;
2425
+ margin-top: 6px;
2426
+ }
2427
+
2206
2428
  .empty {
2207
2429
  text-align: center;
2208
2430
  color: var(--muted);
@@ -2305,6 +2527,14 @@ function getDashboardHtml() {
2305
2527
  return d.innerHTML;
2306
2528
  }
2307
2529
 
2530
+ function fmtDuration(mins) {
2531
+ if (!mins || mins <= 0) return '';
2532
+ if (mins < 60) return mins + 'm';
2533
+ var h = Math.floor(mins / 60);
2534
+ var m = mins % 60;
2535
+ return m > 0 ? h + 'h ' + m + 'm' : h + 'h';
2536
+ }
2537
+
2308
2538
  function renderMilestones(milestones) {
2309
2539
  var section = document.getElementById('milestones-section');
2310
2540
  var list = document.getElementById('milestones-list');
@@ -2317,17 +2547,21 @@ function getDashboardHtml() {
2317
2547
  var recent = milestones.slice(-20).reverse();
2318
2548
  list.innerHTML = recent.map(function(m) {
2319
2549
  var date = m.created_at ? m.created_at.slice(0, 10) : '';
2550
+ var dur = fmtDuration(m.duration_minutes);
2320
2551
  return '<div class="milestone-item">' +
2321
2552
  '<div class="milestone-title">' + escapeHtml(m.title) + '</div>' +
2322
2553
  '<div class="milestone-meta">' +
2323
2554
  '<span class="' + badgeClass(m.category) + '">' + escapeHtml(m.category) + '</span>' +
2324
2555
  (m.complexity ? '<span class="complexity">' + escapeHtml(m.complexity) + '</span>' : '') +
2556
+ (dur ? '<span class="milestone-duration">' + dur + '</span>' : '') +
2325
2557
  '<span class="milestone-date">' + escapeHtml(date) + '</span>' +
2326
2558
  '</div>' +
2327
2559
  '</div>';
2328
2560
  }).join('');
2329
2561
  }
2330
2562
 
2563
+ var loginEmail = '';
2564
+
2331
2565
  function renderSync(config) {
2332
2566
  var section = document.getElementById('sync-section');
2333
2567
  if (!config) { section.style.display = 'none'; return; }
@@ -2337,20 +2571,134 @@ function getDashboardHtml() {
2337
2571
  var lastSync = config.last_sync_at ? 'Last sync: ' + config.last_sync_at : 'Never synced';
2338
2572
  section.innerHTML = '<div class="sync-section">' +
2339
2573
  '<h2 style="font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:12px;font-weight:600;">Sync</h2>' +
2574
+ '<div class="sync-status" id="sync-status" style="margin-bottom:12px;">Logged in as <strong>' + escapeHtml(config.email || '') + '</strong></div>' +
2340
2575
  '<button class="sync-btn" id="sync-btn">Sync to useai.dev</button>' +
2341
- '<div class="sync-status" id="sync-status">' + escapeHtml(lastSync) + '</div>' +
2576
+ '<div class="sync-status" id="sync-time">' + escapeHtml(lastSync) + '</div>' +
2342
2577
  '<div class="sync-result" id="sync-result"></div>' +
2343
2578
  '</div>';
2344
2579
  document.getElementById('sync-btn').addEventListener('click', doSync);
2345
2580
  } else {
2346
- 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>';
2581
+ renderLoginForm();
2347
2582
  }
2348
2583
  }
2349
2584
 
2585
+ function renderLoginForm() {
2586
+ var section = document.getElementById('sync-section');
2587
+ section.innerHTML = '<div class="login-form" id="login-form">' +
2588
+ '<h3>Sign in to sync</h3>' +
2589
+ '<div class="login-sub">Sync sessions & milestones to useai.dev</div>' +
2590
+ '<input class="login-input" id="login-email" type="email" placeholder="you@email.com" autocomplete="email">' +
2591
+ '<button class="login-btn" id="login-send-btn">Send verification code</button>' +
2592
+ '<div class="login-msg" id="login-msg"></div>' +
2593
+ '</div>';
2594
+ document.getElementById('login-send-btn').addEventListener('click', sendOtp);
2595
+ document.getElementById('login-email').addEventListener('keydown', function(e) {
2596
+ if (e.key === 'Enter') sendOtp();
2597
+ });
2598
+ }
2599
+
2600
+ function renderOtpForm() {
2601
+ var section = document.getElementById('sync-section');
2602
+ section.innerHTML = '<div class="login-form" id="otp-form">' +
2603
+ '<h3>Check your email</h3>' +
2604
+ '<div class="login-sub">Enter the 6-digit code sent to ' + escapeHtml(loginEmail) + '</div>' +
2605
+ '<input class="login-input otp-input" id="login-otp" type="text" maxlength="6" placeholder="000000" inputmode="numeric" autocomplete="one-time-code">' +
2606
+ '<button class="login-btn" id="login-verify-btn">Verify</button>' +
2607
+ '<div class="login-msg" id="login-msg"></div>' +
2608
+ '<button class="login-link" id="login-resend">Resend code</button>' +
2609
+ '</div>';
2610
+ document.getElementById('login-verify-btn').addEventListener('click', verifyOtp);
2611
+ document.getElementById('login-resend').addEventListener('click', function() { sendOtp(); });
2612
+ document.getElementById('login-otp').addEventListener('keydown', function(e) {
2613
+ if (e.key === 'Enter') verifyOtp();
2614
+ });
2615
+ document.getElementById('login-otp').focus();
2616
+ }
2617
+
2618
+ function sendOtp() {
2619
+ var emailEl = document.getElementById('login-email');
2620
+ var msg = document.getElementById('login-msg');
2621
+ var btn = document.getElementById('login-send-btn') || document.getElementById('login-resend');
2622
+ var email = emailEl ? emailEl.value.trim() : loginEmail;
2623
+
2624
+ if (!email || email.indexOf('@') === -1) {
2625
+ msg.textContent = 'Please enter a valid email';
2626
+ msg.className = 'login-msg error';
2627
+ return;
2628
+ }
2629
+
2630
+ loginEmail = email;
2631
+ if (btn) { btn.disabled = true; btn.textContent = btn.id === 'login-resend' ? 'Sending...' : 'Sending code...'; }
2632
+
2633
+ fetch(API + '/api/local/auth/send-otp', {
2634
+ method: 'POST',
2635
+ headers: { 'Content-Type': 'application/json' },
2636
+ body: JSON.stringify({ email: email }),
2637
+ })
2638
+ .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
2639
+ .then(function(res) {
2640
+ if (!res.ok) {
2641
+ msg.textContent = res.data.message || 'Failed to send code';
2642
+ msg.className = 'login-msg error';
2643
+ if (btn) { btn.disabled = false; btn.textContent = 'Send verification code'; }
2644
+ return;
2645
+ }
2646
+ renderOtpForm();
2647
+ })
2648
+ .catch(function(err) {
2649
+ msg.textContent = 'Error: ' + err.message;
2650
+ msg.className = 'login-msg error';
2651
+ if (btn) { btn.disabled = false; btn.textContent = 'Send verification code'; }
2652
+ });
2653
+ }
2654
+
2655
+ function verifyOtp() {
2656
+ var otpEl = document.getElementById('login-otp');
2657
+ var msg = document.getElementById('login-msg');
2658
+ var btn = document.getElementById('login-verify-btn');
2659
+ var code = otpEl ? otpEl.value.trim() : '';
2660
+
2661
+ if (!/^d{6}$/.test(code)) {
2662
+ msg.textContent = 'Please enter the 6-digit code';
2663
+ msg.className = 'login-msg error';
2664
+ return;
2665
+ }
2666
+
2667
+ btn.disabled = true;
2668
+ btn.textContent = 'Verifying...';
2669
+ msg.textContent = '';
2670
+
2671
+ fetch(API + '/api/local/auth/verify-otp', {
2672
+ method: 'POST',
2673
+ headers: { 'Content-Type': 'application/json' },
2674
+ body: JSON.stringify({ email: loginEmail, code: code }),
2675
+ })
2676
+ .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
2677
+ .then(function(res) {
2678
+ if (!res.ok) {
2679
+ msg.textContent = res.data.message || 'Invalid code';
2680
+ msg.className = 'login-msg error';
2681
+ btn.disabled = false;
2682
+ btn.textContent = 'Verify';
2683
+ return;
2684
+ }
2685
+ // Success \u2014 reload config and re-render sync section
2686
+ msg.textContent = 'Logged in!';
2687
+ msg.className = 'login-msg success';
2688
+ setTimeout(function() { loadAll(); }, 500);
2689
+ })
2690
+ .catch(function(err) {
2691
+ msg.textContent = 'Error: ' + err.message;
2692
+ msg.className = 'login-msg error';
2693
+ btn.disabled = false;
2694
+ btn.textContent = 'Verify';
2695
+ });
2696
+ }
2697
+
2350
2698
  function doSync() {
2351
2699
  var btn = document.getElementById('sync-btn');
2352
2700
  var result = document.getElementById('sync-result');
2353
- var status = document.getElementById('sync-status');
2701
+ var timeEl = document.getElementById('sync-time');
2354
2702
  btn.disabled = true;
2355
2703
  btn.textContent = 'Syncing...';
2356
2704
  result.textContent = '';
@@ -2364,7 +2712,7 @@ function getDashboardHtml() {
2364
2712
  if (data.success) {
2365
2713
  result.textContent = 'Synced successfully';
2366
2714
  result.className = 'sync-result success';
2367
- if (data.last_sync_at) status.textContent = 'Last sync: ' + data.last_sync_at;
2715
+ if (data.last_sync_at && timeEl) timeEl.textContent = 'Last sync: ' + data.last_sync_at;
2368
2716
  } else {
2369
2717
  result.textContent = 'Sync failed: ' + (data.error || 'Unknown error');
2370
2718
  result.className = 'sync-result error';
@@ -2580,10 +2928,66 @@ async function handleLocalSync(req, res) {
2580
2928
  json(res, 500, { success: false, error: err2.message });
2581
2929
  }
2582
2930
  }
2931
+ async function handleLocalSendOtp(req, res) {
2932
+ try {
2933
+ const raw = await readBody(req);
2934
+ const body = raw ? JSON.parse(raw) : {};
2935
+ const apiRes = await fetch(`${USEAI_API}/api/auth/send-otp`, {
2936
+ method: "POST",
2937
+ headers: { "Content-Type": "application/json" },
2938
+ body: JSON.stringify({ email: body.email })
2939
+ });
2940
+ const data = await apiRes.json();
2941
+ if (!apiRes.ok) {
2942
+ json(res, apiRes.status, data);
2943
+ return;
2944
+ }
2945
+ json(res, 200, data);
2946
+ } catch (err2) {
2947
+ json(res, 500, { error: err2.message });
2948
+ }
2949
+ }
2950
+ async function handleLocalVerifyOtp(req, res) {
2951
+ try {
2952
+ const raw = await readBody(req);
2953
+ const body = raw ? JSON.parse(raw) : {};
2954
+ const apiRes = await fetch(`${USEAI_API}/api/auth/verify-otp`, {
2955
+ method: "POST",
2956
+ headers: { "Content-Type": "application/json" },
2957
+ body: JSON.stringify({ email: body.email, code: body.code })
2958
+ });
2959
+ const data = await apiRes.json();
2960
+ if (!apiRes.ok) {
2961
+ json(res, apiRes.status, data);
2962
+ return;
2963
+ }
2964
+ if (data.token && data.user) {
2965
+ const config = readJson(CONFIG_FILE, {
2966
+ milestone_tracking: true,
2967
+ auto_sync: true,
2968
+ sync_interval_hours: 24
2969
+ });
2970
+ config.auth = {
2971
+ token: data.token,
2972
+ user: {
2973
+ id: data.user.id,
2974
+ email: data.user.email,
2975
+ username: data.user.username
2976
+ }
2977
+ };
2978
+ writeJson(CONFIG_FILE, config);
2979
+ }
2980
+ json(res, 200, { success: true, email: data.user?.email, username: data.user?.username });
2981
+ } catch (err2) {
2982
+ json(res, 500, { error: err2.message });
2983
+ }
2984
+ }
2985
+ var USEAI_API;
2583
2986
  var init_local_api = __esm({
2584
2987
  "src/dashboard/local-api.ts"() {
2585
2988
  "use strict";
2586
2989
  init_dist();
2990
+ USEAI_API = "https://api.useai.dev";
2587
2991
  }
2588
2992
  });
2589
2993
 
@@ -2594,11 +2998,125 @@ __export(daemon_exports, {
2594
2998
  });
2595
2999
  import { createServer } from "http";
2596
3000
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
2597
- import { existsSync as existsSync8, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
2598
- import { join as join6 } from "path";
3001
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync5, appendFileSync as appendFileSync2, renameSync as renameSync3, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5 } from "fs";
3002
+ import { join as join7 } from "path";
2599
3003
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2600
3004
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2601
3005
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3006
+ function getActiveUseaiSessionIds() {
3007
+ const ids = /* @__PURE__ */ new Set();
3008
+ for (const [, active] of sessions) {
3009
+ ids.add(active.session.sessionId);
3010
+ }
3011
+ return ids;
3012
+ }
3013
+ function sealOrphanFile(sessionId) {
3014
+ const filePath = join7(ACTIVE_DIR, `${sessionId}.jsonl`);
3015
+ if (!existsSync9(filePath)) return;
3016
+ try {
3017
+ const content = readFileSync5(filePath, "utf-8").trim();
3018
+ if (!content) return;
3019
+ const lines = content.split("\n").filter(Boolean);
3020
+ if (lines.length === 0) return;
3021
+ const firstRecord = JSON.parse(lines[0]);
3022
+ const lastRecord = JSON.parse(lines[lines.length - 1]);
3023
+ if (lastRecord.type === "session_end" || lastRecord.type === "session_seal") {
3024
+ try {
3025
+ renameSync3(filePath, join7(SEALED_DIR, `${sessionId}.jsonl`));
3026
+ } catch {
3027
+ }
3028
+ return;
3029
+ }
3030
+ const startData = firstRecord.data;
3031
+ const client = startData["client"] ?? "unknown";
3032
+ const taskType = startData["task_type"] ?? "coding";
3033
+ const startTime = firstRecord.timestamp;
3034
+ let heartbeatCount = 0;
3035
+ for (const line of lines) {
3036
+ try {
3037
+ if (JSON.parse(line).type === "heartbeat") heartbeatCount++;
3038
+ } catch {
3039
+ }
3040
+ }
3041
+ const chainTip = lastRecord.hash;
3042
+ const duration = Math.round((Date.now() - new Date(startTime).getTime()) / 1e3);
3043
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3044
+ const endRecord = buildChainRecord("session_end", sessionId, {
3045
+ duration_seconds: duration,
3046
+ task_type: taskType,
3047
+ languages: [],
3048
+ files_touched: 0,
3049
+ heartbeat_count: heartbeatCount,
3050
+ auto_sealed: true
3051
+ }, chainTip, daemonSigningKey);
3052
+ appendFileSync2(filePath, JSON.stringify(endRecord) + "\n");
3053
+ const sealData = JSON.stringify({
3054
+ session_id: sessionId,
3055
+ client,
3056
+ task_type: taskType,
3057
+ languages: [],
3058
+ files_touched: 0,
3059
+ started_at: startTime,
3060
+ ended_at: now,
3061
+ duration_seconds: duration,
3062
+ heartbeat_count: heartbeatCount,
3063
+ record_count: lines.length + 2,
3064
+ chain_end_hash: endRecord.hash
3065
+ });
3066
+ const sealSignature = signHash(
3067
+ createHash4("sha256").update(sealData).digest("hex"),
3068
+ daemonSigningKey
3069
+ );
3070
+ appendFileSync2(filePath, JSON.stringify(
3071
+ buildChainRecord("session_seal", sessionId, {
3072
+ seal: sealData,
3073
+ seal_signature: sealSignature,
3074
+ auto_sealed: true
3075
+ }, endRecord.hash, daemonSigningKey)
3076
+ ) + "\n");
3077
+ try {
3078
+ renameSync3(filePath, join7(SEALED_DIR, `${sessionId}.jsonl`));
3079
+ } catch {
3080
+ }
3081
+ const seal = {
3082
+ session_id: sessionId,
3083
+ client,
3084
+ task_type: taskType,
3085
+ languages: [],
3086
+ files_touched: 0,
3087
+ started_at: startTime,
3088
+ ended_at: now,
3089
+ duration_seconds: duration,
3090
+ heartbeat_count: heartbeatCount,
3091
+ record_count: lines.length + 2,
3092
+ chain_start_hash: firstRecord.prev_hash,
3093
+ chain_end_hash: endRecord.hash,
3094
+ seal_signature: sealSignature
3095
+ };
3096
+ const allSessions = readJson(SESSIONS_FILE, []);
3097
+ allSessions.push(seal);
3098
+ writeJson(SESSIONS_FILE, allSessions);
3099
+ } catch {
3100
+ }
3101
+ }
3102
+ function sealOrphanedSessions() {
3103
+ if (!existsSync9(ACTIVE_DIR)) return;
3104
+ const activeIds = getActiveUseaiSessionIds();
3105
+ let sealed = 0;
3106
+ try {
3107
+ const files = readdirSync(ACTIVE_DIR).filter((f) => f.endsWith(".jsonl"));
3108
+ for (const file of files) {
3109
+ const sessionId = file.replace(".jsonl", "");
3110
+ if (activeIds.has(sessionId)) continue;
3111
+ sealOrphanFile(sessionId);
3112
+ sealed++;
3113
+ }
3114
+ } catch {
3115
+ }
3116
+ if (sealed > 0) {
3117
+ console.log(`Sealed ${sealed} orphaned session${sealed === 1 ? "" : "s"}`);
3118
+ }
3119
+ }
2602
3120
  function autoSealSession(active) {
2603
3121
  const { session: session2 } = active;
2604
3122
  if (session2.sessionRecordCount === 0) return;
@@ -2634,10 +3152,10 @@ function autoSealSession(active) {
2634
3152
  seal_signature: sealSignature,
2635
3153
  auto_sealed: true
2636
3154
  });
2637
- const activePath = join6(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
2638
- const sealedPath = join6(SEALED_DIR, `${session2.sessionId}.jsonl`);
3155
+ const activePath = join7(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
3156
+ const sealedPath = join7(SEALED_DIR, `${session2.sessionId}.jsonl`);
2639
3157
  try {
2640
- if (existsSync8(activePath)) {
3158
+ if (existsSync9(activePath)) {
2641
3159
  renameSync3(activePath, sealedPath);
2642
3160
  }
2643
3161
  } catch {
@@ -2714,6 +3232,21 @@ function parseBody(req) {
2714
3232
  async function startDaemon(port) {
2715
3233
  const listenPort = port ?? DAEMON_PORT;
2716
3234
  ensureDir();
3235
+ try {
3236
+ if (existsSync9(KEYSTORE_FILE)) {
3237
+ const ks = readJson(KEYSTORE_FILE, null);
3238
+ if (ks) daemonSigningKey = decryptKeystore(ks);
3239
+ }
3240
+ if (!daemonSigningKey) {
3241
+ const result = generateKeystore();
3242
+ writeJson(KEYSTORE_FILE, result.keystore);
3243
+ daemonSigningKey = result.signingKey;
3244
+ }
3245
+ } catch {
3246
+ }
3247
+ sealOrphanedSessions();
3248
+ const sweepInterval = setInterval(sealOrphanedSessions, ORPHAN_SWEEP_INTERVAL_MS);
3249
+ sweepInterval.unref();
2717
3250
  const server2 = createServer(async (req, res) => {
2718
3251
  const url = new URL(req.url, `http://${req.headers.host}`);
2719
3252
  if (url.pathname === "/health" && req.method === "GET") {
@@ -2743,7 +3276,27 @@ async function startDaemon(port) {
2743
3276
  await handleLocalSync(req, res);
2744
3277
  return;
2745
3278
  }
2746
- if (url.pathname.startsWith("/api/local/") && req.method === "OPTIONS") {
3279
+ if (url.pathname === "/api/local/auth/send-otp" && req.method === "POST") {
3280
+ await handleLocalSendOtp(req, res);
3281
+ return;
3282
+ }
3283
+ if (url.pathname === "/api/local/auth/verify-otp" && req.method === "POST") {
3284
+ await handleLocalVerifyOtp(req, res);
3285
+ return;
3286
+ }
3287
+ if (url.pathname === "/api/seal-active" && req.method === "POST") {
3288
+ const sids = [...sessions.keys()];
3289
+ for (const sid of sids) {
3290
+ await cleanupSession(sid);
3291
+ }
3292
+ res.writeHead(200, {
3293
+ "Content-Type": "application/json",
3294
+ "Access-Control-Allow-Origin": "*"
3295
+ });
3296
+ res.end(JSON.stringify({ sealed: sids.length }));
3297
+ return;
3298
+ }
3299
+ if ((url.pathname.startsWith("/api/local/") || url.pathname === "/api/seal-active") && req.method === "OPTIONS") {
2747
3300
  res.writeHead(204, {
2748
3301
  "Access-Control-Allow-Origin": "*",
2749
3302
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
@@ -2856,14 +3409,14 @@ async function startDaemon(port) {
2856
3409
  port: listenPort,
2857
3410
  started_at: (/* @__PURE__ */ new Date()).toISOString()
2858
3411
  });
2859
- writeFileSync4(DAEMON_PID_FILE, pidData + "\n");
3412
+ writeFileSync5(DAEMON_PID_FILE, pidData + "\n");
2860
3413
  const shutdown = async (signal) => {
2861
3414
  for (const [sid] of sessions) {
2862
3415
  await cleanupSession(sid);
2863
3416
  }
2864
3417
  try {
2865
- if (existsSync8(DAEMON_PID_FILE)) {
2866
- unlinkSync4(DAEMON_PID_FILE);
3418
+ if (existsSync9(DAEMON_PID_FILE)) {
3419
+ unlinkSync5(DAEMON_PID_FILE);
2867
3420
  }
2868
3421
  } catch {
2869
3422
  }
@@ -2879,7 +3432,7 @@ async function startDaemon(port) {
2879
3432
  console.log(`PID: ${process.pid}`);
2880
3433
  });
2881
3434
  }
2882
- var IDLE_TIMEOUT_MS, sessions, startedAt;
3435
+ var IDLE_TIMEOUT_MS, sessions, daemonSigningKey, ORPHAN_SWEEP_INTERVAL_MS, startedAt;
2883
3436
  var init_daemon2 = __esm({
2884
3437
  "src/daemon.ts"() {
2885
3438
  "use strict";
@@ -2890,6 +3443,8 @@ var init_daemon2 = __esm({
2890
3443
  init_local_api();
2891
3444
  IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
2892
3445
  sessions = /* @__PURE__ */ new Map();
3446
+ daemonSigningKey = null;
3447
+ ORPHAN_SWEEP_INTERVAL_MS = 15 * 60 * 1e3;
2893
3448
  startedAt = Date.now();
2894
3449
  }
2895
3450
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Track your AI-assisted development workflow. MCP server that records usage metrics across all your AI tools.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -28,14 +28,6 @@
28
28
  "README.md",
29
29
  "LICENSE"
30
30
  ],
31
- "scripts": {
32
- "build": "tsc -p tsconfig.build.json",
33
- "dev": "tsc --watch",
34
- "bundle": "tsup src/index.ts --format esm --target node18 --no-splitting",
35
- "prepublishOnly": "pnpm run bundle",
36
- "typecheck": "tsc --noEmit",
37
- "clean": "rm -rf dist"
38
- },
39
31
  "dependencies": {
40
32
  "@inquirer/prompts": "^8.2.1",
41
33
  "@modelcontextprotocol/sdk": "^1.26.0",
@@ -46,9 +38,9 @@
46
38
  },
47
39
  "devDependencies": {
48
40
  "@types/node": "^22.13.4",
49
- "@useai/shared": "workspace:*",
50
41
  "tsup": "^8.0.0",
51
- "typescript": "^5.7.3"
42
+ "typescript": "^5.7.3",
43
+ "@useai/shared": "0.3.0"
52
44
  },
53
45
  "repository": {
54
46
  "type": "git",
@@ -57,5 +49,12 @@
57
49
  "homepage": "https://useai.dev",
58
50
  "engines": {
59
51
  "node": ">=18"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.build.json",
55
+ "dev": "tsc --watch",
56
+ "bundle": "tsup src/index.ts --format esm --target node18 --no-splitting",
57
+ "typecheck": "tsc --noEmit",
58
+ "clean": "rm -rf dist"
60
59
  }
61
- }
60
+ }