haansi 0.1.0 → 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 +365 -50
- package/package.json +11 -4
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.");
|
|
@@ -97,7 +99,7 @@ var init_init = __esm({
|
|
|
97
99
|
import_node_fs = require("node:fs");
|
|
98
100
|
import_node_path = require("node:path");
|
|
99
101
|
import_node_os = require("node:os");
|
|
100
|
-
DEFAULT_API_URL = "https://api.haansi.
|
|
102
|
+
DEFAULT_API_URL = "https://api.haansi.co";
|
|
101
103
|
HAANSI_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".haansi");
|
|
102
104
|
CREDENTIALS_FILE = (0, import_node_path.join)(HAANSI_DIR, "credentials.json");
|
|
103
105
|
}
|
|
@@ -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() {
|
|
@@ -1104,7 +1206,7 @@ var init_collector = __esm({
|
|
|
1104
1206
|
init_logger();
|
|
1105
1207
|
(0, import_dotenv2.config)({ path: (0, import_path3.resolve)(__dirname, "../../../.env") });
|
|
1106
1208
|
logger3 = createLogger("claude-collector");
|
|
1107
|
-
DEFAULT_API_URL2 = "https://api.haansi.
|
|
1209
|
+
DEFAULT_API_URL2 = "https://api.haansi.co";
|
|
1108
1210
|
CREDENTIALS_FILE2 = (0, import_path3.join)((0, import_os3.homedir)(), ".haansi", "credentials.json");
|
|
1109
1211
|
EDGE_SCRUB_PATTERNS = [
|
|
1110
1212
|
{
|
|
@@ -30355,8 +30457,8 @@ var require_dist = __commonJS({
|
|
|
30355
30457
|
return ajv;
|
|
30356
30458
|
}
|
|
30357
30459
|
const [formats, exportName] = opts.mode === "fast" ? [formats_1.fastFormats, fastName] : [formats_1.fullFormats, fullName];
|
|
30358
|
-
const
|
|
30359
|
-
addFormats(ajv,
|
|
30460
|
+
const list2 = opts.formats || formats_1.formatNames;
|
|
30461
|
+
addFormats(ajv, list2, formats, exportName);
|
|
30360
30462
|
if (opts.keywords)
|
|
30361
30463
|
(0, limit_1.default)(ajv);
|
|
30362
30464
|
return ajv;
|
|
@@ -30368,11 +30470,11 @@ var require_dist = __commonJS({
|
|
|
30368
30470
|
throw new Error(`Unknown format "${name}"`);
|
|
30369
30471
|
return f;
|
|
30370
30472
|
};
|
|
30371
|
-
function addFormats(ajv,
|
|
30473
|
+
function addFormats(ajv, list2, fs, exportName) {
|
|
30372
30474
|
var _a2;
|
|
30373
30475
|
var _b;
|
|
30374
30476
|
(_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
30375
|
-
for (const f of
|
|
30477
|
+
for (const f of list2)
|
|
30376
30478
|
ajv.addFormat(f, fs[f]);
|
|
30377
30479
|
}
|
|
30378
30480
|
module2.exports = exports2 = formatsPlugin;
|
|
@@ -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,11 +32145,12 @@ 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();
|
|
32029
32152
|
(0, import_dotenv4.config)({ path: (0, import_path5.resolve)(__dirname, "../../../.env") });
|
|
32030
|
-
DEFAULT_API_URL3 = "https://api.haansi.
|
|
32153
|
+
DEFAULT_API_URL3 = "https://api.haansi.co";
|
|
32031
32154
|
CREDENTIALS_FILE3 = (0, import_path5.join)((0, import_os4.homedir)(), ".haansi", "credentials.json");
|
|
32032
32155
|
API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
|
|
32033
32156
|
TOKEN = resolveToken2();
|
|
@@ -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({
|
|
@@ -32175,6 +32306,162 @@ var init_setup_mcp = __esm({
|
|
|
32175
32306
|
}
|
|
32176
32307
|
});
|
|
32177
32308
|
|
|
32309
|
+
// src/commands/setup-daemon.ts
|
|
32310
|
+
var setup_daemon_exports = {};
|
|
32311
|
+
__export(setup_daemon_exports, {
|
|
32312
|
+
setupDaemon: () => setupDaemon
|
|
32313
|
+
});
|
|
32314
|
+
function findHaansiBin() {
|
|
32315
|
+
try {
|
|
32316
|
+
return (0, import_node_child_process.execSync)("which haansi", { encoding: "utf-8" }).trim();
|
|
32317
|
+
} catch {
|
|
32318
|
+
throw new Error(
|
|
32319
|
+
"Could not find haansi in PATH. Make sure it is installed globally (npm install -g haansi)."
|
|
32320
|
+
);
|
|
32321
|
+
}
|
|
32322
|
+
}
|
|
32323
|
+
function buildPlist(haansiPath) {
|
|
32324
|
+
const binDir = haansiPath.replace(/\/[^/]+$/, "");
|
|
32325
|
+
const path = `${binDir}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
32326
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
32327
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
32328
|
+
<plist version="1.0">
|
|
32329
|
+
<dict>
|
|
32330
|
+
<key>Label</key>
|
|
32331
|
+
<string>${PLIST_LABEL}</string>
|
|
32332
|
+
<key>ProgramArguments</key>
|
|
32333
|
+
<array>
|
|
32334
|
+
<string>${haansiPath}</string>
|
|
32335
|
+
<string>daemon</string>
|
|
32336
|
+
</array>
|
|
32337
|
+
<key>EnvironmentVariables</key>
|
|
32338
|
+
<dict>
|
|
32339
|
+
<key>PATH</key>
|
|
32340
|
+
<string>${path}</string>
|
|
32341
|
+
</dict>
|
|
32342
|
+
<key>RunAtLoad</key>
|
|
32343
|
+
<true/>
|
|
32344
|
+
<key>KeepAlive</key>
|
|
32345
|
+
<true/>
|
|
32346
|
+
<key>StandardOutPath</key>
|
|
32347
|
+
<string>${LOG_FILE}</string>
|
|
32348
|
+
<key>StandardErrorPath</key>
|
|
32349
|
+
<string>${LOG_FILE}</string>
|
|
32350
|
+
</dict>
|
|
32351
|
+
</plist>`;
|
|
32352
|
+
}
|
|
32353
|
+
async function setupDaemon(uninstall) {
|
|
32354
|
+
if (uninstall) {
|
|
32355
|
+
if (!(0, import_node_fs3.existsSync)(PLIST_PATH)) {
|
|
32356
|
+
console.log("Daemon is not installed.");
|
|
32357
|
+
return;
|
|
32358
|
+
}
|
|
32359
|
+
try {
|
|
32360
|
+
(0, import_node_child_process.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
|
|
32361
|
+
} catch {
|
|
32362
|
+
}
|
|
32363
|
+
(0, import_node_fs3.unlinkSync)(PLIST_PATH);
|
|
32364
|
+
console.log("Daemon stopped and removed.");
|
|
32365
|
+
return;
|
|
32366
|
+
}
|
|
32367
|
+
const haansiPath = findHaansiBin();
|
|
32368
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi"), { recursive: true });
|
|
32369
|
+
(0, import_node_fs3.writeFileSync)(PLIST_PATH, buildPlist(haansiPath), "utf-8");
|
|
32370
|
+
try {
|
|
32371
|
+
(0, import_node_child_process.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
|
|
32372
|
+
} catch {
|
|
32373
|
+
}
|
|
32374
|
+
(0, import_node_child_process.execSync)(`launchctl load "${PLIST_PATH}"`, { stdio: "inherit" });
|
|
32375
|
+
console.log("Daemon installed and started.");
|
|
32376
|
+
console.log(`Plist : ${PLIST_PATH}`);
|
|
32377
|
+
console.log(`Logs : ${LOG_FILE}`);
|
|
32378
|
+
console.log(`
|
|
32379
|
+
The daemon will start automatically on every login.`);
|
|
32380
|
+
console.log(`To uninstall: haansi setup-daemon --uninstall`);
|
|
32381
|
+
}
|
|
32382
|
+
var import_node_child_process, import_node_fs3, import_node_path3, import_node_os3, PLIST_LABEL, PLIST_PATH, LOG_FILE;
|
|
32383
|
+
var init_setup_daemon = __esm({
|
|
32384
|
+
"src/commands/setup-daemon.ts"() {
|
|
32385
|
+
"use strict";
|
|
32386
|
+
import_node_child_process = require("node:child_process");
|
|
32387
|
+
import_node_fs3 = require("node:fs");
|
|
32388
|
+
import_node_path3 = require("node:path");
|
|
32389
|
+
import_node_os3 = require("node:os");
|
|
32390
|
+
PLIST_LABEL = "co.haansi.daemon";
|
|
32391
|
+
PLIST_PATH = (0, import_node_path3.join)(
|
|
32392
|
+
(0, import_node_os3.homedir)(),
|
|
32393
|
+
"Library",
|
|
32394
|
+
"LaunchAgents",
|
|
32395
|
+
`${PLIST_LABEL}.plist`
|
|
32396
|
+
);
|
|
32397
|
+
LOG_FILE = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi", "daemon.log");
|
|
32398
|
+
}
|
|
32399
|
+
});
|
|
32400
|
+
|
|
32401
|
+
// src/commands/list.ts
|
|
32402
|
+
var list_exports = {};
|
|
32403
|
+
__export(list_exports, {
|
|
32404
|
+
list: () => list
|
|
32405
|
+
});
|
|
32406
|
+
function resolveToken3() {
|
|
32407
|
+
if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
|
|
32408
|
+
if ((0, import_node_fs4.existsSync)(CREDENTIALS_FILE4)) {
|
|
32409
|
+
try {
|
|
32410
|
+
const creds = JSON.parse((0, import_node_fs4.readFileSync)(CREDENTIALS_FILE4, "utf-8"));
|
|
32411
|
+
if (creds.token) return creds.token;
|
|
32412
|
+
} catch {
|
|
32413
|
+
}
|
|
32414
|
+
}
|
|
32415
|
+
throw new Error("No token found. Run `haansi init` first.");
|
|
32416
|
+
}
|
|
32417
|
+
async function apiGet2(path, token, apiUrl) {
|
|
32418
|
+
const response = await fetch(`${apiUrl}/api/v1${path}`, {
|
|
32419
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
32420
|
+
});
|
|
32421
|
+
if (!response.ok) {
|
|
32422
|
+
const body = await response.text().catch(() => "");
|
|
32423
|
+
throw new Error(`API error ${response.status}: ${body.slice(0, 200)}`);
|
|
32424
|
+
}
|
|
32425
|
+
return response.json();
|
|
32426
|
+
}
|
|
32427
|
+
async function list(options) {
|
|
32428
|
+
const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL4;
|
|
32429
|
+
const token = resolveToken3();
|
|
32430
|
+
let data;
|
|
32431
|
+
if (options.search) {
|
|
32432
|
+
data = await apiGet2(
|
|
32433
|
+
`/sessions/search?q=${encodeURIComponent(options.search)}&top_k=${options.limit}`,
|
|
32434
|
+
token,
|
|
32435
|
+
apiUrl
|
|
32436
|
+
);
|
|
32437
|
+
} else {
|
|
32438
|
+
data = await apiGet2(
|
|
32439
|
+
`/sessions/recent?limit=${options.limit}`,
|
|
32440
|
+
token,
|
|
32441
|
+
apiUrl
|
|
32442
|
+
);
|
|
32443
|
+
}
|
|
32444
|
+
if (!data.results || data.results.length === 0) {
|
|
32445
|
+
console.log("No solutions found. Run `haansi collect` to upload sessions.");
|
|
32446
|
+
return;
|
|
32447
|
+
}
|
|
32448
|
+
const separator = "\n" + "\u2500".repeat(60) + "\n";
|
|
32449
|
+
console.log(data.results.join(separator));
|
|
32450
|
+
console.log(`
|
|
32451
|
+
${data.results.length} result(s)`);
|
|
32452
|
+
}
|
|
32453
|
+
var import_node_fs4, import_node_path4, import_node_os4, DEFAULT_API_URL4, CREDENTIALS_FILE4;
|
|
32454
|
+
var init_list = __esm({
|
|
32455
|
+
"src/commands/list.ts"() {
|
|
32456
|
+
"use strict";
|
|
32457
|
+
import_node_fs4 = require("node:fs");
|
|
32458
|
+
import_node_path4 = require("node:path");
|
|
32459
|
+
import_node_os4 = require("node:os");
|
|
32460
|
+
DEFAULT_API_URL4 = "https://api.haansi.co";
|
|
32461
|
+
CREDENTIALS_FILE4 = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".haansi", "credentials.json");
|
|
32462
|
+
}
|
|
32463
|
+
});
|
|
32464
|
+
|
|
32178
32465
|
// src/index.ts
|
|
32179
32466
|
var command = process.argv[2];
|
|
32180
32467
|
async function run2() {
|
|
@@ -32191,9 +32478,16 @@ async function run2() {
|
|
|
32191
32478
|
const limitIdx = process.argv.indexOf("--limit");
|
|
32192
32479
|
const limit = limitIdx !== -1 ? parseInt(process.argv[limitIdx + 1], 10) : void 0;
|
|
32193
32480
|
const projectPath = process.env.CLAUDE_PROJECT_PATH || void 0;
|
|
32194
|
-
const { uploaded, skipped, errors } = await collect2({
|
|
32195
|
-
|
|
32196
|
-
|
|
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
|
+
);
|
|
32197
32491
|
if (errors > 0) process.exit(1);
|
|
32198
32492
|
break;
|
|
32199
32493
|
}
|
|
@@ -32210,6 +32504,21 @@ Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`);
|
|
|
32210
32504
|
await setupMcp2();
|
|
32211
32505
|
break;
|
|
32212
32506
|
}
|
|
32507
|
+
case "setup-daemon": {
|
|
32508
|
+
const { setupDaemon: setupDaemon2 } = await Promise.resolve().then(() => (init_setup_daemon(), setup_daemon_exports));
|
|
32509
|
+
const uninstall = process.argv.includes("--uninstall");
|
|
32510
|
+
await setupDaemon2(uninstall);
|
|
32511
|
+
break;
|
|
32512
|
+
}
|
|
32513
|
+
case "list": {
|
|
32514
|
+
const { list: list2 } = await Promise.resolve().then(() => (init_list(), list_exports));
|
|
32515
|
+
const searchIdx = process.argv.indexOf("--search");
|
|
32516
|
+
const search = searchIdx !== -1 ? process.argv[searchIdx + 1] : void 0;
|
|
32517
|
+
const limitIdx = process.argv.indexOf("--limit");
|
|
32518
|
+
const limit = limitIdx !== -1 ? parseInt(process.argv[limitIdx + 1], 10) : 10;
|
|
32519
|
+
await list2({ search, limit });
|
|
32520
|
+
break;
|
|
32521
|
+
}
|
|
32213
32522
|
case "help":
|
|
32214
32523
|
default: {
|
|
32215
32524
|
printUsage();
|
|
@@ -32231,8 +32540,14 @@ Commands:
|
|
|
32231
32540
|
daemon Run the collector on a schedule (every 30 min)
|
|
32232
32541
|
mcp-server Start the Haansi MCP server (used by Claude Code)
|
|
32233
32542
|
setup-mcp Add haansi-solutions MCP entry to ~/.claude.json
|
|
32543
|
+
list List or search mined solutions
|
|
32544
|
+
setup-daemon Install launchd service to auto-start daemon on login (macOS)
|
|
32234
32545
|
help Show this help message
|
|
32235
32546
|
|
|
32547
|
+
Options for list:
|
|
32548
|
+
--search <query> Semantic search instead of listing recent
|
|
32549
|
+
--limit <n> Number of results (default: 10)
|
|
32550
|
+
|
|
32236
32551
|
Options for collect:
|
|
32237
32552
|
--force-all Re-upload all sessions, not just new ones
|
|
32238
32553
|
--dry-run Scan without uploading
|
|
@@ -32240,7 +32555,7 @@ Options for collect:
|
|
|
32240
32555
|
|
|
32241
32556
|
Environment variables:
|
|
32242
32557
|
HAANSI_TOKEN API token (or use \`haansi init\` to save it)
|
|
32243
|
-
HAANSI_API_URL API base URL (default: https://api.haansi.
|
|
32558
|
+
HAANSI_API_URL API base URL (default: https://api.haansi.co)
|
|
32244
32559
|
CLAUDE_PROJECT_PATH Collect from a specific project path only
|
|
32245
32560
|
CLAUDE_COLLECT_SCHEDULE Cron expression for daemon (default: */30 * * * *)
|
|
32246
32561
|
`);
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haansi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Haansi CLI - Session collector and MCP server for Claude Code",
|
|
5
|
-
"bin": {
|
|
6
|
-
|
|
5
|
+
"bin": {
|
|
6
|
+
"haansi": "./dist/haansi.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
7
12
|
"scripts": {
|
|
8
13
|
"build": "node build.mjs",
|
|
9
14
|
"prepublishOnly": "npm run build"
|
|
@@ -13,5 +18,7 @@
|
|
|
13
18
|
"typescript": "^5.7.2",
|
|
14
19
|
"@types/node": "^22.10.1"
|
|
15
20
|
},
|
|
16
|
-
"engines": {
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
}
|
|
17
24
|
}
|