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.
Files changed (2) hide show
  1. package/dist/haansi.js +365 -50
  2. 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(`You can generate an API token at https://app.haansi.co/settings/tokens
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.com";
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 body = (0, import_zlib.gzipSync)(Buffer.from(jsonlContent, "utf-8"));
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
- Authorization: `Bearer ${token}`,
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 toUpload = [...scanResult.newSessions, ...scanResult.updatedSessions];
1037
- const sessions = options.limit ? toUpload.slice(0, options.limit) : toUpload;
1038
- if (sessions.length === 0) {
1039
- logger3.info("No new sessions to upload");
1040
- return { uploaded: 0, skipped: scanResult.unchangedCount, errors: 0 };
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
- for (const session of sessions) {
1049
- const shortId = session.sessionId.slice(0, 8);
1050
- try {
1051
- const rawContent = (0, import_fs4.readFileSync)(session.filePath, "utf-8");
1052
- const scrubbed = edgeScrub(rawContent);
1053
- if (options.dryRun) {
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
- `[dry-run] Would upload session ${shortId} (${rawContent.length} bytes)`
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
- continue;
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.com";
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 list = opts.formats || formats_1.formatNames;
30359
- addFormats(ajv, list, formats, exportName);
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, list, fs, exportName) {
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 list)
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.com";
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(`Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`);
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("Restart Claude Code to activate the haansi-solutions MCP server.");
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({ forceAll, dryRun, limit, projectPath });
32195
- console.log(`
32196
- Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`);
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.com)
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.0",
3
+ "version": "0.1.2",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code",
5
- "bin": { "haansi": "./dist/haansi.js" },
6
- "files": ["dist/", "README.md"],
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": { "node": ">=18.0.0" }
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ }
17
24
  }