@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.
- package/LICENSE +21 -0
- package/dist/index.js +1050 -91
- 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.
|
|
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
|
|
825
|
-
import { join as
|
|
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 (
|
|
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
|
|
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
|
|
911
|
-
import { join as
|
|
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 =
|
|
1018
|
-
const sealedPath =
|
|
1178
|
+
const activePath = join5(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
|
|
1179
|
+
const sealedPath = join5(SEALED_DIR, `${session2.sessionId}.jsonl`);
|
|
1019
1180
|
try {
|
|
1020
|
-
if (
|
|
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
|
|
1101
|
-
import { dirname as dirname2, join as
|
|
1102
|
-
import { homedir as
|
|
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 (!
|
|
1291
|
+
if (!existsSync8(path)) return {};
|
|
1131
1292
|
try {
|
|
1132
|
-
const raw =
|
|
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
|
-
|
|
1141
|
-
|
|
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 (!
|
|
1383
|
+
if (!existsSync8(path)) return {};
|
|
1223
1384
|
try {
|
|
1224
|
-
const raw =
|
|
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
|
-
|
|
1233
|
-
|
|
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 (!
|
|
1422
|
+
if (!existsSync8(path)) return {};
|
|
1262
1423
|
try {
|
|
1263
|
-
const raw =
|
|
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
|
-
|
|
1272
|
-
|
|
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 (!
|
|
1467
|
+
if (!existsSync8(filePath)) return false;
|
|
1307
1468
|
try {
|
|
1308
|
-
return
|
|
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
|
-
|
|
1475
|
+
mkdirSync4(dirname2(config.path), { recursive: true });
|
|
1315
1476
|
if (config.method === "create") {
|
|
1316
|
-
|
|
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 (
|
|
1322
|
-
existing =
|
|
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
|
-
|
|
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 (
|
|
1490
|
+
if (existsSync8(config.path)) {
|
|
1330
1491
|
try {
|
|
1331
|
-
|
|
1492
|
+
unlinkSync4(config.path);
|
|
1332
1493
|
} catch {
|
|
1333
1494
|
}
|
|
1334
1495
|
}
|
|
1335
1496
|
return;
|
|
1336
1497
|
}
|
|
1337
|
-
if (!
|
|
1498
|
+
if (!existsSync8(config.path)) return;
|
|
1338
1499
|
try {
|
|
1339
|
-
const content =
|
|
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
|
-
|
|
1507
|
+
writeFileSync4(config.path, cleaned + "\n");
|
|
1347
1508
|
} else {
|
|
1348
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
1451
|
-
detect: () => hasBinary("claude") ||
|
|
1452
|
-
instructions: { method: "append", path:
|
|
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:
|
|
1460
|
-
detect: () =>
|
|
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:
|
|
1469
|
-
detect: () =>
|
|
1470
|
-
instructions: { method: "append", path:
|
|
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:
|
|
1478
|
-
detect: () =>
|
|
1479
|
-
instructions: { method: "create", path:
|
|
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:
|
|
1487
|
-
detect: () =>
|
|
1488
|
-
instructions: { method: "create", path:
|
|
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:
|
|
1656
|
+
configPath: join6(home, ".gemini", "settings.json"),
|
|
1496
1657
|
detect: () => hasBinary("gemini"),
|
|
1497
|
-
instructions: { method: "append", path:
|
|
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:
|
|
1505
|
-
detect: () =>
|
|
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:
|
|
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: () =>
|
|
1522
|
-
|
|
1682
|
+
detect: () => existsSync8(
|
|
1683
|
+
join6(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
|
|
1523
1684
|
),
|
|
1524
|
-
instructions: { method: "create", path:
|
|
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:
|
|
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: () =>
|
|
1541
|
-
|
|
1701
|
+
detect: () => existsSync8(
|
|
1702
|
+
join6(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
|
|
1542
1703
|
),
|
|
1543
|
-
instructions: { method: "create", path:
|
|
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:
|
|
1551
|
-
detect: () => hasBinary("q") ||
|
|
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:
|
|
1559
|
-
detect: () =>
|
|
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:
|
|
1567
|
-
detect: () => hasBinary("codex") ||
|
|
1568
|
-
instructions: { method: "append", path:
|
|
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:
|
|
1575
|
-
detect: () =>
|
|
1576
|
-
instructions: { method: "append", path:
|
|
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:
|
|
1583
|
-
detect: () => hasBinary("opencode") ||
|
|
1584
|
-
instructions: { method: "append", path:
|
|
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:
|
|
1592
|
-
detect: () =>
|
|
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
|
|
1965
|
-
import { join as
|
|
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 =
|
|
2005
|
-
const sealedPath =
|
|
2896
|
+
const activePath = join7(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
|
|
2897
|
+
const sealedPath = join7(SEALED_DIR, `${session2.sessionId}.jsonl`);
|
|
2006
2898
|
try {
|
|
2007
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
2197
|
-
|
|
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
|
});
|