haansi 0.1.1 → 0.1.2
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/dist/haansi.js +194 -45
- package/package.json +1 -1
package/dist/haansi.js
CHANGED
|
@@ -65,8 +65,10 @@ async function validateToken(token, apiUrl) {
|
|
|
65
65
|
async function init() {
|
|
66
66
|
const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL;
|
|
67
67
|
console.log("Haansi CLI \u2014 Setup\n");
|
|
68
|
-
console.log(
|
|
69
|
-
`
|
|
68
|
+
console.log(
|
|
69
|
+
`You can generate an API token at https://app.haansi.co/settings/tokens
|
|
70
|
+
`
|
|
71
|
+
);
|
|
70
72
|
const token = await prompt("Paste your Haansi API token: ");
|
|
71
73
|
if (!token) {
|
|
72
74
|
console.error("Error: No token provided.");
|
|
@@ -938,6 +940,43 @@ var init_ingester = __esm({
|
|
|
938
940
|
/**
|
|
939
941
|
* Mark a session as processed and persist state.
|
|
940
942
|
*/
|
|
943
|
+
/**
|
|
944
|
+
* Return the number of lines already uploaded for this session (0 if unknown).
|
|
945
|
+
*/
|
|
946
|
+
getUploadedLineCount(sessionId) {
|
|
947
|
+
const state = this.loadState();
|
|
948
|
+
return state.processed[sessionId]?.uploadedLineCount ?? 0;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Return whether this session was previously finalized.
|
|
952
|
+
*/
|
|
953
|
+
isFinalized(sessionId) {
|
|
954
|
+
const state = this.loadState();
|
|
955
|
+
return state.processed[sessionId]?.finalized ?? false;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Return all session IDs that are tracked but not yet finalized.
|
|
959
|
+
* These are sessions that were previously uploaded as open and may
|
|
960
|
+
* need finalization if they no longer appear in the scan results.
|
|
961
|
+
*/
|
|
962
|
+
getOpenSessionIds() {
|
|
963
|
+
const state = this.loadState();
|
|
964
|
+
return Object.values(state.processed).filter((r) => r.finalized === false).map((r) => r.sessionId);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Mark a session as finalized without re-reading its file.
|
|
968
|
+
* Used when a previously-open session is no longer receiving new lines.
|
|
969
|
+
*/
|
|
970
|
+
markFinalized(sessionId) {
|
|
971
|
+
const state = this.loadState();
|
|
972
|
+
const record2 = state.processed[sessionId];
|
|
973
|
+
if (record2) {
|
|
974
|
+
record2.finalized = true;
|
|
975
|
+
record2.processedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
976
|
+
state.lastRunAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
977
|
+
this.saveState(state);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
941
980
|
markProcessed(session, status, extras) {
|
|
942
981
|
const state = this.loadState();
|
|
943
982
|
state.processed[session.sessionId] = {
|
|
@@ -1002,17 +1041,22 @@ function resolveToken() {
|
|
|
1002
1041
|
}
|
|
1003
1042
|
return null;
|
|
1004
1043
|
}
|
|
1005
|
-
async function uploadSession(sessionId, jsonlContent, apiUrl, token) {
|
|
1006
|
-
const
|
|
1044
|
+
async function uploadSession(sessionId, jsonlContent, apiUrl, token, isOpen) {
|
|
1045
|
+
const headers = {
|
|
1046
|
+
Authorization: `Bearer ${token}`,
|
|
1047
|
+
"Content-Type": "application/x-ndjson",
|
|
1048
|
+
"x-session-id": sessionId,
|
|
1049
|
+
"x-session-open": isOpen ? "true" : "false"
|
|
1050
|
+
};
|
|
1051
|
+
let body;
|
|
1052
|
+
if (jsonlContent.length > 0) {
|
|
1053
|
+
body = (0, import_zlib.gzipSync)(Buffer.from(jsonlContent, "utf-8"));
|
|
1054
|
+
headers["Content-Encoding"] = "gzip";
|
|
1055
|
+
}
|
|
1007
1056
|
const response = await fetch(`${apiUrl}/api/v1/sessions/raw`, {
|
|
1008
1057
|
method: "POST",
|
|
1009
|
-
headers
|
|
1010
|
-
|
|
1011
|
-
"Content-Type": "application/x-ndjson",
|
|
1012
|
-
"Content-Encoding": "gzip",
|
|
1013
|
-
"x-session-id": sessionId
|
|
1014
|
-
},
|
|
1015
|
-
body
|
|
1058
|
+
headers,
|
|
1059
|
+
body: body ? new Uint8Array(body) : void 0
|
|
1016
1060
|
});
|
|
1017
1061
|
if (!response.ok) {
|
|
1018
1062
|
const text = await response.text().catch(() => "");
|
|
@@ -1033,39 +1077,97 @@ async function collect(options) {
|
|
|
1033
1077
|
projectPath: options.projectPath
|
|
1034
1078
|
});
|
|
1035
1079
|
const scanResult = options.projectPath ? await ingester.scan({ forceAll: options.forceAll }) : await ingester.scanAll({ forceAll: options.forceAll });
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
logger3.info(`Uploading ${sessions.length} session(s)`, {
|
|
1043
|
-
apiUrl,
|
|
1044
|
-
dryRun: options.dryRun ?? false
|
|
1045
|
-
});
|
|
1080
|
+
const activeSessions = [
|
|
1081
|
+
...scanResult.newSessions,
|
|
1082
|
+
...scanResult.updatedSessions
|
|
1083
|
+
];
|
|
1084
|
+
const activeSessionIds = new Set(activeSessions.map((s) => s.sessionId));
|
|
1085
|
+
const sessions = options.limit ? activeSessions.slice(0, options.limit) : activeSessions;
|
|
1046
1086
|
let uploaded = 0;
|
|
1047
1087
|
let errors = 0;
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1088
|
+
if (sessions.length > 0) {
|
|
1089
|
+
logger3.info(`Uploading ${sessions.length} active session(s)`, {
|
|
1090
|
+
apiUrl,
|
|
1091
|
+
dryRun: options.dryRun ?? false
|
|
1092
|
+
});
|
|
1093
|
+
for (const session of sessions) {
|
|
1094
|
+
const shortId = session.sessionId.slice(0, 8);
|
|
1095
|
+
try {
|
|
1096
|
+
const rawContent = (0, import_fs4.readFileSync)(session.filePath, "utf-8");
|
|
1097
|
+
const allLines = rawContent.split("\n").filter((l) => l.trim().length > 0);
|
|
1098
|
+
const totalLines = allLines.length;
|
|
1099
|
+
let uploadedLineCount = ingester.getUploadedLineCount(
|
|
1100
|
+
session.sessionId
|
|
1101
|
+
);
|
|
1102
|
+
const wasPreviouslyFinalized = ingester.isFinalized(session.sessionId);
|
|
1103
|
+
if (wasPreviouslyFinalized && totalLines > uploadedLineCount) {
|
|
1104
|
+
uploadedLineCount = 0;
|
|
1105
|
+
}
|
|
1106
|
+
if (totalLines < uploadedLineCount) {
|
|
1107
|
+
uploadedLineCount = 0;
|
|
1108
|
+
}
|
|
1109
|
+
const deltaLines = allLines.slice(uploadedLineCount);
|
|
1110
|
+
if (deltaLines.length === 0) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const deltaContent = edgeScrub(deltaLines.join("\n") + "\n");
|
|
1114
|
+
if (options.dryRun) {
|
|
1115
|
+
logger3.info(
|
|
1116
|
+
`[dry-run] Would upload delta for session ${shortId} (${deltaLines.length} new lines, open)`
|
|
1117
|
+
);
|
|
1118
|
+
uploaded++;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
await uploadSession(
|
|
1122
|
+
session.sessionId,
|
|
1123
|
+
deltaContent,
|
|
1124
|
+
apiUrl,
|
|
1125
|
+
token,
|
|
1126
|
+
true
|
|
1127
|
+
);
|
|
1128
|
+
ingester.markProcessed(session, "pending", {
|
|
1129
|
+
uploadedLineCount: totalLines,
|
|
1130
|
+
finalized: false
|
|
1131
|
+
});
|
|
1132
|
+
uploaded++;
|
|
1054
1133
|
logger3.info(
|
|
1055
|
-
`
|
|
1134
|
+
`delta uploaded session ${shortId} (${deltaLines.length} new lines, open)`
|
|
1056
1135
|
);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
errors++;
|
|
1138
|
+
logger3.error(`Failed to upload session ${shortId}`, {
|
|
1139
|
+
err: err.message
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const openSessionIds = ingester.getOpenSessionIds();
|
|
1145
|
+
const toFinalize = openSessionIds.filter((id) => !activeSessionIds.has(id));
|
|
1146
|
+
if (toFinalize.length > 0) {
|
|
1147
|
+
logger3.info(`Finalizing ${toFinalize.length} idle session(s)`);
|
|
1148
|
+
for (const sessionId of toFinalize) {
|
|
1149
|
+
const shortId = sessionId.slice(0, 8);
|
|
1150
|
+
try {
|
|
1151
|
+
if (options.dryRun) {
|
|
1152
|
+
logger3.info(`[dry-run] Would finalize session ${shortId}`);
|
|
1153
|
+
uploaded++;
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
await uploadSession(sessionId, "", apiUrl, token, false);
|
|
1157
|
+
ingester.markFinalized(sessionId);
|
|
1057
1158
|
uploaded++;
|
|
1058
|
-
|
|
1159
|
+
logger3.info(`finalized session ${shortId}`);
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
errors++;
|
|
1162
|
+
logger3.error(`Failed to finalize session ${shortId}`, {
|
|
1163
|
+
err: err.message
|
|
1164
|
+
});
|
|
1059
1165
|
}
|
|
1060
|
-
await uploadSession(session.sessionId, scrubbed, apiUrl, token);
|
|
1061
|
-
ingester.markProcessed(session, "pending", {});
|
|
1062
|
-
uploaded++;
|
|
1063
|
-
logger3.info(`Uploaded session ${shortId}`);
|
|
1064
|
-
} catch (err) {
|
|
1065
|
-
errors++;
|
|
1066
|
-
logger3.error(`Failed to upload session ${shortId}`, { err: err.message });
|
|
1067
1166
|
}
|
|
1068
1167
|
}
|
|
1168
|
+
if (sessions.length === 0 && toFinalize.length === 0) {
|
|
1169
|
+
logger3.info("No new sessions to upload");
|
|
1170
|
+
}
|
|
1069
1171
|
return { uploaded, skipped: scanResult.unchangedCount, errors };
|
|
1070
1172
|
}
|
|
1071
1173
|
async function main2() {
|
|
@@ -31991,6 +32093,25 @@ function resolveToken2() {
|
|
|
31991
32093
|
}
|
|
31992
32094
|
return null;
|
|
31993
32095
|
}
|
|
32096
|
+
function triggerCollectInBackground() {
|
|
32097
|
+
if (isCollecting) return;
|
|
32098
|
+
isCollecting = true;
|
|
32099
|
+
collect({}).then(({ uploaded, skipped, errors }) => {
|
|
32100
|
+
console.error(
|
|
32101
|
+
`[mcp-server] Collection done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
|
|
32102
|
+
);
|
|
32103
|
+
lastCollectTime = Date.now();
|
|
32104
|
+
}).catch((err) => {
|
|
32105
|
+
console.error("[mcp-server] Collection error:", err.message);
|
|
32106
|
+
}).finally(() => {
|
|
32107
|
+
isCollecting = false;
|
|
32108
|
+
});
|
|
32109
|
+
}
|
|
32110
|
+
function maybeCollect() {
|
|
32111
|
+
if (Date.now() - lastCollectTime >= COLLECT_COOLDOWN_MS) {
|
|
32112
|
+
triggerCollectInBackground();
|
|
32113
|
+
}
|
|
32114
|
+
}
|
|
31994
32115
|
async function apiGet(path) {
|
|
31995
32116
|
const response = await fetch(`${API_URL}/api/v1${path}`, {
|
|
31996
32117
|
headers: { Authorization: `Bearer ${TOKEN}` }
|
|
@@ -32005,6 +32126,7 @@ async function main4() {
|
|
|
32005
32126
|
try {
|
|
32006
32127
|
await apiGet("/sessions/recent?limit=1");
|
|
32007
32128
|
console.error("[mcp-server] API connection ok");
|
|
32129
|
+
triggerCollectInBackground();
|
|
32008
32130
|
} catch (err) {
|
|
32009
32131
|
console.error(
|
|
32010
32132
|
"[mcp-server] WARNING: API connection check failed:",
|
|
@@ -32015,7 +32137,7 @@ async function main4() {
|
|
|
32015
32137
|
await mcpServer.connect(transport);
|
|
32016
32138
|
console.error("[mcp-server] haansi-solutions MCP server running on stdio");
|
|
32017
32139
|
}
|
|
32018
|
-
var import_dotenv4, import_path5, import_fs5, import_os4, DEFAULT_API_URL3, CREDENTIALS_FILE3, API_URL, TOKEN, mcpServer, server;
|
|
32140
|
+
var import_dotenv4, import_path5, import_fs5, import_os4, DEFAULT_API_URL3, CREDENTIALS_FILE3, API_URL, TOKEN, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
|
|
32019
32141
|
var init_mcp_server2 = __esm({
|
|
32020
32142
|
"../service-capture/claude-sessions/mcp-server.ts"() {
|
|
32021
32143
|
"use strict";
|
|
@@ -32023,6 +32145,7 @@ var init_mcp_server2 = __esm({
|
|
|
32023
32145
|
import_path5 = require("path");
|
|
32024
32146
|
import_fs5 = require("fs");
|
|
32025
32147
|
import_os4 = require("os");
|
|
32148
|
+
init_collector();
|
|
32026
32149
|
init_mcp();
|
|
32027
32150
|
init_stdio2();
|
|
32028
32151
|
init_types2();
|
|
@@ -32037,6 +32160,9 @@ var init_mcp_server2 = __esm({
|
|
|
32037
32160
|
);
|
|
32038
32161
|
process.exit(1);
|
|
32039
32162
|
}
|
|
32163
|
+
isCollecting = false;
|
|
32164
|
+
lastCollectTime = 0;
|
|
32165
|
+
COLLECT_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
32040
32166
|
mcpServer = new McpServer(
|
|
32041
32167
|
{ name: "haansi-solutions", version: "0.2.0" },
|
|
32042
32168
|
{ capabilities: { tools: {} } }
|
|
@@ -32079,6 +32205,7 @@ var init_mcp_server2 = __esm({
|
|
|
32079
32205
|
]
|
|
32080
32206
|
}));
|
|
32081
32207
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
32208
|
+
maybeCollect();
|
|
32082
32209
|
const { name, arguments: args } = request.params;
|
|
32083
32210
|
console.error(`[mcp-server] Tool: ${name}`);
|
|
32084
32211
|
try {
|
|
@@ -32150,7 +32277,9 @@ async function setupMcp() {
|
|
|
32150
32277
|
try {
|
|
32151
32278
|
config6 = JSON.parse((0, import_node_fs2.readFileSync)(CLAUDE_JSON, "utf-8"));
|
|
32152
32279
|
} catch {
|
|
32153
|
-
console.error(
|
|
32280
|
+
console.error(
|
|
32281
|
+
`Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`
|
|
32282
|
+
);
|
|
32154
32283
|
}
|
|
32155
32284
|
}
|
|
32156
32285
|
if (!config6.mcpServers) {
|
|
@@ -32162,7 +32291,9 @@ async function setupMcp() {
|
|
|
32162
32291
|
};
|
|
32163
32292
|
(0, import_node_fs2.writeFileSync)(CLAUDE_JSON, JSON.stringify(config6, null, 2) + "\n", "utf-8");
|
|
32164
32293
|
console.log(`MCP server configured in ${CLAUDE_JSON}`);
|
|
32165
|
-
console.log(
|
|
32294
|
+
console.log(
|
|
32295
|
+
"Restart Claude Code to activate the haansi-solutions MCP server."
|
|
32296
|
+
);
|
|
32166
32297
|
}
|
|
32167
32298
|
var import_node_fs2, import_node_path2, import_node_os2, CLAUDE_JSON;
|
|
32168
32299
|
var init_setup_mcp = __esm({
|
|
@@ -32184,7 +32315,9 @@ function findHaansiBin() {
|
|
|
32184
32315
|
try {
|
|
32185
32316
|
return (0, import_node_child_process.execSync)("which haansi", { encoding: "utf-8" }).trim();
|
|
32186
32317
|
} catch {
|
|
32187
|
-
throw new Error(
|
|
32318
|
+
throw new Error(
|
|
32319
|
+
"Could not find haansi in PATH. Make sure it is installed globally (npm install -g haansi)."
|
|
32320
|
+
);
|
|
32188
32321
|
}
|
|
32189
32322
|
}
|
|
32190
32323
|
function buildPlist(haansiPath) {
|
|
@@ -32255,7 +32388,12 @@ var init_setup_daemon = __esm({
|
|
|
32255
32388
|
import_node_path3 = require("node:path");
|
|
32256
32389
|
import_node_os3 = require("node:os");
|
|
32257
32390
|
PLIST_LABEL = "co.haansi.daemon";
|
|
32258
|
-
PLIST_PATH = (0, import_node_path3.join)(
|
|
32391
|
+
PLIST_PATH = (0, import_node_path3.join)(
|
|
32392
|
+
(0, import_node_os3.homedir)(),
|
|
32393
|
+
"Library",
|
|
32394
|
+
"LaunchAgents",
|
|
32395
|
+
`${PLIST_LABEL}.plist`
|
|
32396
|
+
);
|
|
32259
32397
|
LOG_FILE = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi", "daemon.log");
|
|
32260
32398
|
}
|
|
32261
32399
|
});
|
|
@@ -32297,7 +32435,11 @@ async function list(options) {
|
|
|
32297
32435
|
apiUrl
|
|
32298
32436
|
);
|
|
32299
32437
|
} else {
|
|
32300
|
-
data = await apiGet2(
|
|
32438
|
+
data = await apiGet2(
|
|
32439
|
+
`/sessions/recent?limit=${options.limit}`,
|
|
32440
|
+
token,
|
|
32441
|
+
apiUrl
|
|
32442
|
+
);
|
|
32301
32443
|
}
|
|
32302
32444
|
if (!data.results || data.results.length === 0) {
|
|
32303
32445
|
console.log("No solutions found. Run `haansi collect` to upload sessions.");
|
|
@@ -32336,9 +32478,16 @@ async function run2() {
|
|
|
32336
32478
|
const limitIdx = process.argv.indexOf("--limit");
|
|
32337
32479
|
const limit = limitIdx !== -1 ? parseInt(process.argv[limitIdx + 1], 10) : void 0;
|
|
32338
32480
|
const projectPath = process.env.CLAUDE_PROJECT_PATH || void 0;
|
|
32339
|
-
const { uploaded, skipped, errors } = await collect2({
|
|
32340
|
-
|
|
32341
|
-
|
|
32481
|
+
const { uploaded, skipped, errors } = await collect2({
|
|
32482
|
+
forceAll,
|
|
32483
|
+
dryRun,
|
|
32484
|
+
limit,
|
|
32485
|
+
projectPath
|
|
32486
|
+
});
|
|
32487
|
+
console.log(
|
|
32488
|
+
`
|
|
32489
|
+
Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
|
|
32490
|
+
);
|
|
32342
32491
|
if (errors > 0) process.exit(1);
|
|
32343
32492
|
break;
|
|
32344
32493
|
}
|