haansi 0.1.12 → 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 +30 -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.12",
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 = {
@@ -56915,6 +56886,10 @@ var init_mcp_server2 = __esm({
56915
56886
  type: "string",
56916
56887
  description: "Git remote origin URL (e.g. github.com/org/repo)"
56917
56888
  },
56889
+ git_commit: {
56890
+ type: "string",
56891
+ description: "Git commit hash for the code changes"
56892
+ },
56918
56893
  tags: {
56919
56894
  type: "array",
56920
56895
  items: { type: "string" },
@@ -56938,7 +56913,6 @@ var init_mcp_server2 = __esm({
56938
56913
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
56939
56914
  maybeCollect();
56940
56915
  const { name, arguments: args } = request.params;
56941
- console.error(`[mcp-server] Tool: ${name}`);
56942
56916
  try {
56943
56917
  switch (name) {
56944
56918
  case "search_solutions": {
@@ -57012,6 +56986,7 @@ var init_mcp_server2 = __esm({
57012
56986
  project_path,
57013
56987
  git_branch,
57014
56988
  git_remote_url,
56989
+ git_commit,
57015
56990
  tags,
57016
56991
  model,
57017
56992
  source_tool,
@@ -57037,6 +57012,7 @@ var init_mcp_server2 = __esm({
57037
57012
  project_path,
57038
57013
  git_branch,
57039
57014
  git_remote_url,
57015
+ git_commit,
57040
57016
  tags,
57041
57017
  model,
57042
57018
  source_tool
@@ -57061,15 +57037,13 @@ var init_mcp_server2 = __esm({
57061
57037
  throw new Error(`Unknown tool: ${name}`);
57062
57038
  }
57063
57039
  } catch (err) {
57064
- console.error(`[mcp-server] Tool ${name} error:`, err.message);
57065
57040
  return {
57066
57041
  content: [{ type: "text", text: `Error: ${err.message}` }],
57067
57042
  isError: true
57068
57043
  };
57069
57044
  }
57070
57045
  });
57071
- main3().catch((err) => {
57072
- console.error("[mcp-server] Failed to start:", err);
57046
+ main3().catch(() => {
57073
57047
  process.exit(1);
57074
57048
  });
57075
57049
  }
@@ -57177,8 +57151,9 @@ async function setupDaemon(uninstall) {
57177
57151
  return;
57178
57152
  }
57179
57153
  try {
57180
- (0, import_node_child_process2.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
57181
- } 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);
57182
57157
  }
57183
57158
  (0, import_node_fs3.unlinkSync)(PLIST_PATH);
57184
57159
  console.log("Daemon stopped and removed.");
@@ -57188,10 +57163,11 @@ async function setupDaemon(uninstall) {
57188
57163
  (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi"), { recursive: true });
57189
57164
  (0, import_node_fs3.writeFileSync)(PLIST_PATH, buildPlist(haansiPath), "utf-8");
57190
57165
  try {
57191
- (0, import_node_child_process2.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
57192
- } 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);
57193
57169
  }
57194
- (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" });
57195
57171
  console.log("Daemon installed and started.");
57196
57172
  console.log(`Plist : ${PLIST_PATH}`);
57197
57173
  console.log(`Logs : ${LOG_FILE}`);
@@ -57229,7 +57205,8 @@ function resolveToken3() {
57229
57205
  try {
57230
57206
  const creds = JSON.parse((0, import_node_fs4.readFileSync)(CREDENTIALS_FILE4, "utf-8"));
57231
57207
  if (creds.token) return creds.token;
57232
- } catch {
57208
+ } catch (err) {
57209
+ console.error("Failed to read credentials file:", err);
57233
57210
  }
57234
57211
  }
57235
57212
  return null;
@@ -57344,7 +57321,8 @@ function resolveToken4() {
57344
57321
  try {
57345
57322
  const creds = JSON.parse((0, import_node_fs5.readFileSync)(CREDENTIALS_FILE5, "utf-8"));
57346
57323
  if (creds.token) return creds.token;
57347
- } catch {
57324
+ } catch (err) {
57325
+ console.error("Failed to read credentials file:", err);
57348
57326
  }
57349
57327
  }
57350
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.12",
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"