haansi 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +83 -0
  2. package/dist/haansi.js +24 -52
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Haansi CLI
2
+
3
+ Session collector and MCP server for Claude Code. Uploads Claude Code session data to the Haansi platform for mining, search, and knowledge retrieval.
4
+
5
+ Published to npm as `haansi`.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g haansi
11
+ ```
12
+
13
+ Requires Node.js >= 18.0.0.
14
+
15
+ ## Authentication
16
+
17
+ The CLI uses `hsk_` tokens for authentication. Generate a token at https://app.haansi.co/settings/tokens.
18
+
19
+ ```bash
20
+ haansi init
21
+ ```
22
+
23
+ This prompts for your token, validates it against the API, and saves it to `~/.haansi/credentials.json` (mode 0600). Alternatively, set the `HAANSI_TOKEN` environment variable.
24
+
25
+ ## Commands
26
+
27
+ | Command | Description |
28
+ | --------------------------------- | -------------------------------------------------------- |
29
+ | `haansi init` | Authenticate and save your API token |
30
+ | `haansi collect` | Upload new/changed Claude sessions (once) |
31
+ | `haansi daemon` | Run the collector on a schedule (every 30 min) |
32
+ | `haansi setup-daemon` | Install macOS launchd service for auto-start on login |
33
+ | `haansi setup-mcp` | Add haansi-solutions MCP entry to `~/.claude.json` |
34
+ | `haansi mcp-server` | Start the Haansi MCP server (stdio, used by Claude Code) |
35
+ | `haansi config get [key]` | Show preferences (e.g. `session_mode`) |
36
+ | `haansi config set <key> <val>` | Update a preference |
37
+ | `haansi search-solutions <query>` | Semantic search over mined solutions |
38
+ | `haansi list-solutions` | List recent mined solutions |
39
+ | `haansi search-knowledge <query>` | Search knowledge artifacts |
40
+ | `haansi save-knowledge` | Save a problem/solution for future retrieval |
41
+
42
+ ### Collect options
43
+
44
+ - `--force-all` -- Re-upload all sessions, not just new ones
45
+ - `--dry-run` -- Scan without uploading
46
+ - `--limit <n>` -- Upload at most n sessions
47
+
48
+ ## Configuration
49
+
50
+ | Environment Variable | Description | Default |
51
+ | ------------------------- | ----------------------------------------- | ----------------------- |
52
+ | `HAANSI_TOKEN` | API token (alternative to `haansi init`) | -- |
53
+ | `HAANSI_API_URL` | API base URL | `https://api.haansi.co` |
54
+ | `CLAUDE_PROJECT_PATH` | Collect from a specific project path only | -- |
55
+ | `CLAUDE_COLLECT_SCHEDULE` | Cron expression for daemon | `*/30 * * * *` |
56
+
57
+ ### User preferences
58
+
59
+ Set via `haansi config set <key> <value>`:
60
+
61
+ - `session_mode` -- `private` or `org` (default: `org`)
62
+
63
+ ## Daemon (macOS)
64
+
65
+ ```bash
66
+ haansi setup-daemon # install and start
67
+ haansi setup-daemon --uninstall # stop and remove
68
+ ```
69
+
70
+ Installs a launchd plist at `~/Library/LaunchAgents/co.haansi.daemon.plist`. Logs are written to `~/.haansi/daemon.log`.
71
+
72
+ ## MCP Server
73
+
74
+ ```bash
75
+ haansi setup-mcp
76
+ ```
77
+
78
+ Registers `haansi-solutions` in `~/.claude.json` so Claude Code auto-starts the MCP server, providing search and knowledge tools directly in the AI assistant.
79
+
80
+ ---
81
+
82
+ **Package:** `haansi`
83
+ **Location:** `apps/cli/`
package/dist/haansi.js CHANGED
@@ -39,7 +39,7 @@ var require_package = __commonJS({
39
39
  "package.json"(exports2, module2) {
40
40
  module2.exports = {
41
41
  name: "haansi",
42
- version: "0.1.13",
42
+ version: "0.1.14",
43
43
  description: "Haansi CLI - Session collector and MCP server for Claude Code",
44
44
  bin: {
45
45
  haansi: "./dist/haansi.js"
@@ -567,7 +567,6 @@ function extractGitInfo(projectDir, sessionTimestamp, sessionBranch) {
567
567
  try {
568
568
  (0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", { ...opts, stdio: "pipe" });
569
569
  } catch {
570
- console.warn(`[collector] Not a git repo: ${projectDir}`);
571
570
  return empty;
572
571
  }
573
572
  const result = { ...empty };
@@ -577,7 +576,6 @@ function extractGitInfo(projectDir, sessionTimestamp, sessionBranch) {
577
576
  stdio: "pipe"
578
577
  }).trim() || null;
579
578
  } catch {
580
- console.warn(`[collector] No git remote for: ${projectDir}`);
581
579
  }
582
580
  if (sessionTimestamp) {
583
581
  const branch = sessionBranch ?? "HEAD";
@@ -593,7 +591,6 @@ function extractGitInfo(projectDir, sessionTimestamp, sessionBranch) {
593
591
  try {
594
592
  result.gitCommit = (0, import_child_process.execSync)("git rev-parse HEAD", { ...opts, stdio: "pipe" }).trim() || null;
595
593
  } catch {
596
- console.warn(`[collector] No git commits in: ${projectDir}`);
597
594
  }
598
595
  }
599
596
  try {
@@ -699,7 +696,10 @@ async function getUploadedSessions(apiUrl, token) {
699
696
  const data = await apiGet("/sessions/uploaded", apiUrl, token);
700
697
  const map2 = /* @__PURE__ */ new Map();
701
698
  for (const s of data.sessions) {
702
- map2.set(s.sessionId, s.lineCount);
699
+ map2.set(s.sessionId, {
700
+ lineCount: s.lineCount,
701
+ hasGitMeta: s.hasGitMeta ?? false
702
+ });
703
703
  }
704
704
  return map2;
705
705
  } catch {
@@ -749,8 +749,8 @@ async function collect(options) {
749
749
  const remoteMap = options.forceAll ? /* @__PURE__ */ new Map() : await getUploadedSessions(apiUrl, token);
750
750
  const toUpload = [];
751
751
  for (const session of localSessions) {
752
- const remoteLine = remoteMap.get(session.sessionId);
753
- if (remoteLine !== void 0 && remoteLine >= session.lineCount) {
752
+ const remote = remoteMap.get(session.sessionId);
753
+ if (remote && remote.lineCount >= session.lineCount && remote.hasGitMeta) {
754
754
  skipped++;
755
755
  } else {
756
756
  toUpload.push(session);
@@ -811,10 +811,6 @@ async function main() {
811
811
  dryRun,
812
812
  limit
813
813
  });
814
- console.log(
815
- `
816
- Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
817
- );
818
814
  if (errors > 0) process.exit(1);
819
815
  }
820
816
  var import_dotenv, import_path, import_fs, import_os, import_zlib, import_child_process, logger, DEFAULT_API_URL2, CREDENTIALS_FILE2, CLAUDE_PROJECTS_DIR, MIN_LINES, EDGE_SCRUB_PATTERNS;
@@ -56675,13 +56671,9 @@ function resolveToken2() {
56675
56671
  function triggerCollectInBackground() {
56676
56672
  if (isCollecting) return;
56677
56673
  isCollecting = true;
56678
- collect({}).then(({ uploaded, skipped, errors }) => {
56679
- console.error(
56680
- `[mcp-server] Collection done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
56681
- );
56674
+ collect({}).then(() => {
56682
56675
  lastCollectTime = Date.now();
56683
- }).catch((err) => {
56684
- console.error("[mcp-server] Collection error:", err.message);
56676
+ }).catch(() => {
56685
56677
  }).finally(() => {
56686
56678
  isCollecting = false;
56687
56679
  });
@@ -56723,23 +56715,10 @@ async function apiPost(path, body, options) {
56723
56715
  async function main3() {
56724
56716
  try {
56725
56717
  await apiGet2("/sessions/recent?limit=1");
56726
- console.error("[mcp-server] API connection ok");
56727
- } catch (err) {
56728
- console.error(
56729
- "[mcp-server] WARNING: API connection check failed:",
56730
- err.message
56731
- );
56718
+ } catch {
56732
56719
  }
56733
56720
  try {
56734
- const data = await apiGet2("/users/me/preferences");
56735
- const mode = data?.preferences?.session_mode;
56736
- if (!mode) {
56737
- console.error(
56738
- '[mcp-server] Session mode not configured. Defaulting to "org" (team-wide). To change: haansi config set session_mode private'
56739
- );
56740
- } else {
56741
- console.error(`[mcp-server] Session mode: ${mode}`);
56742
- }
56721
+ await apiGet2("/users/me/preferences");
56743
56722
  } catch {
56744
56723
  }
56745
56724
  triggerCollectInBackground();
@@ -56760,15 +56739,10 @@ async function main3() {
56760
56739
  app.get("/health", (_req, res) => {
56761
56740
  res.json({ status: "ok", service: "haansi-solutions-mcp" });
56762
56741
  });
56763
- app.listen(PORT, () => {
56764
- console.error(
56765
- `[mcp-server] Haansi Solutions MCP server running on HTTP port ${PORT}`
56766
- );
56767
- });
56742
+ app.listen(PORT);
56768
56743
  } else {
56769
56744
  const transport = new StdioServerTransport();
56770
56745
  await mcpServer.connect(transport);
56771
- console.error("[mcp-server] haansi-solutions MCP server running on stdio");
56772
56746
  }
56773
56747
  }
56774
56748
  var import_dotenv3, import_path3, import_fs2, import_os2, DEFAULT_API_URL3, CREDENTIALS_FILE3, API_URL, TOKEN, contextSchema, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
@@ -56790,9 +56764,6 @@ var init_mcp_server2 = __esm({
56790
56764
  API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
56791
56765
  TOKEN = resolveToken2();
56792
56766
  if (!TOKEN) {
56793
- console.error(
56794
- "[mcp-server] ERROR: HAANSI_TOKEN is required (or set in ~/.haansi/credentials.json)"
56795
- );
56796
56767
  process.exit(1);
56797
56768
  }
56798
56769
  contextSchema = {
@@ -56942,7 +56913,6 @@ var init_mcp_server2 = __esm({
56942
56913
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
56943
56914
  maybeCollect();
56944
56915
  const { name, arguments: args } = request.params;
56945
- console.error(`[mcp-server] Tool: ${name}`);
56946
56916
  try {
56947
56917
  switch (name) {
56948
56918
  case "search_solutions": {
@@ -57067,15 +57037,13 @@ var init_mcp_server2 = __esm({
57067
57037
  throw new Error(`Unknown tool: ${name}`);
57068
57038
  }
57069
57039
  } catch (err) {
57070
- console.error(`[mcp-server] Tool ${name} error:`, err.message);
57071
57040
  return {
57072
57041
  content: [{ type: "text", text: `Error: ${err.message}` }],
57073
57042
  isError: true
57074
57043
  };
57075
57044
  }
57076
57045
  });
57077
- main3().catch((err) => {
57078
- console.error("[mcp-server] Failed to start:", err);
57046
+ main3().catch(() => {
57079
57047
  process.exit(1);
57080
57048
  });
57081
57049
  }
@@ -57183,8 +57151,9 @@ async function setupDaemon(uninstall) {
57183
57151
  return;
57184
57152
  }
57185
57153
  try {
57186
- (0, import_node_child_process2.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
57187
- } catch {
57154
+ (0, import_node_child_process2.spawnSync)("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
57155
+ } catch (err) {
57156
+ console.error("Failed to unload daemon:", err);
57188
57157
  }
57189
57158
  (0, import_node_fs3.unlinkSync)(PLIST_PATH);
57190
57159
  console.log("Daemon stopped and removed.");
@@ -57194,10 +57163,11 @@ async function setupDaemon(uninstall) {
57194
57163
  (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi"), { recursive: true });
57195
57164
  (0, import_node_fs3.writeFileSync)(PLIST_PATH, buildPlist(haansiPath), "utf-8");
57196
57165
  try {
57197
- (0, import_node_child_process2.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
57198
- } catch {
57166
+ (0, import_node_child_process2.spawnSync)("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
57167
+ } catch (err) {
57168
+ console.error("Failed to unload previous daemon:", err);
57199
57169
  }
57200
- (0, import_node_child_process2.execSync)(`launchctl load "${PLIST_PATH}"`, { stdio: "inherit" });
57170
+ (0, import_node_child_process2.spawnSync)("launchctl", ["load", PLIST_PATH], { stdio: "inherit" });
57201
57171
  console.log("Daemon installed and started.");
57202
57172
  console.log(`Plist : ${PLIST_PATH}`);
57203
57173
  console.log(`Logs : ${LOG_FILE}`);
@@ -57235,7 +57205,8 @@ function resolveToken3() {
57235
57205
  try {
57236
57206
  const creds = JSON.parse((0, import_node_fs4.readFileSync)(CREDENTIALS_FILE4, "utf-8"));
57237
57207
  if (creds.token) return creds.token;
57238
- } catch {
57208
+ } catch (err) {
57209
+ console.error("Failed to read credentials file:", err);
57239
57210
  }
57240
57211
  }
57241
57212
  return null;
@@ -57350,7 +57321,8 @@ function resolveToken4() {
57350
57321
  try {
57351
57322
  const creds = JSON.parse((0, import_node_fs5.readFileSync)(CREDENTIALS_FILE5, "utf-8"));
57352
57323
  if (creds.token) return creds.token;
57353
- } catch {
57324
+ } catch (err) {
57325
+ console.error("Failed to read credentials file:", err);
57354
57326
  }
57355
57327
  }
57356
57328
  throw new Error("No token found. Run `haansi init` first.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"