haansi 0.1.13 → 0.1.15

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 +252 -133
  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.15",
43
43
  description: "Haansi CLI - Session collector and MCP server for Claude Code",
44
44
  bin: {
45
45
  haansi: "./dist/haansi.js"
@@ -64,6 +64,130 @@ var require_package = __commonJS({
64
64
  }
65
65
  });
66
66
 
67
+ // src/commands/auth-login.ts
68
+ var auth_login_exports = {};
69
+ __export(auth_login_exports, {
70
+ authLogin: () => authLogin
71
+ });
72
+ function openBrowser(url2) {
73
+ const cmd = (0, import_node_os.platform)() === "darwin" ? "open" : (0, import_node_os.platform)() === "win32" ? "start" : "xdg-open";
74
+ try {
75
+ (0, import_node_child_process.exec)(`${cmd} ${JSON.stringify(url2)}`);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ function sleep(ms) {
82
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
83
+ }
84
+ async function authLogin() {
85
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL;
86
+ const appUrl = process.env.HAANSI_APP_URL ?? DEFAULT_APP_URL;
87
+ console.log("Haansi CLI \u2014 Browser Login\n");
88
+ const initiateRes = await fetch(`${apiUrl}/api/v1/auth/extension/initiate`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" }
91
+ });
92
+ if (!initiateRes.ok) {
93
+ const body = await initiateRes.text().catch(() => "");
94
+ throw new Error(
95
+ `Failed to start auth session (${initiateRes.status}): ${body.slice(0, 200)}`
96
+ );
97
+ }
98
+ const { session_id } = await initiateRes.json();
99
+ const authUrl = `${appUrl}/auth/cli?session_id=${session_id}`;
100
+ const opened = openBrowser(authUrl);
101
+ if (opened) {
102
+ console.log("A browser window has been opened for you to log in.");
103
+ } else {
104
+ console.log("Could not open browser automatically.");
105
+ }
106
+ console.log(`
107
+ If the browser didn't open, visit this URL:
108
+ ${authUrl}
109
+ `);
110
+ console.log("Waiting for approval...");
111
+ const deadline = Date.now() + TIMEOUT_MS;
112
+ let jwt2 = null;
113
+ let orgName = null;
114
+ while (Date.now() < deadline) {
115
+ await sleep(POLL_INTERVAL_MS);
116
+ const pollRes = await fetch(
117
+ `${apiUrl}/api/v1/auth/extension/token?session_id=${encodeURIComponent(session_id)}`
118
+ );
119
+ if (pollRes.status === 410) {
120
+ throw new Error("Session expired or was already used. Please try again.");
121
+ }
122
+ if (!pollRes.ok && pollRes.status !== 200) {
123
+ continue;
124
+ }
125
+ const data = await pollRes.json();
126
+ if (data.status === "pending") {
127
+ process.stdout.write(".");
128
+ continue;
129
+ }
130
+ if (data.token) {
131
+ jwt2 = data.token;
132
+ orgName = data.organization?.name ?? null;
133
+ break;
134
+ }
135
+ }
136
+ console.log("");
137
+ if (!jwt2) {
138
+ throw new Error(
139
+ "Timed out waiting for approval. You can authenticate manually with `haansi init`."
140
+ );
141
+ }
142
+ console.log("Creating CLI token...");
143
+ const tokenRes = await fetch(`${apiUrl}/api/v1/cli-tokens`, {
144
+ method: "POST",
145
+ headers: {
146
+ Authorization: `Bearer ${jwt2}`,
147
+ "Content-Type": "application/json"
148
+ },
149
+ body: JSON.stringify({ name: "CLI (haansi auth login)" })
150
+ });
151
+ if (!tokenRes.ok) {
152
+ const body = await tokenRes.text().catch(() => "");
153
+ throw new Error(
154
+ `Failed to create CLI token (${tokenRes.status}): ${body.slice(0, 200)}`
155
+ );
156
+ }
157
+ const { token: hskToken } = await tokenRes.json();
158
+ (0, import_node_fs.mkdirSync)(HAANSI_DIR, { recursive: true });
159
+ (0, import_node_fs.writeFileSync)(
160
+ CREDENTIALS_FILE,
161
+ JSON.stringify({ token: hskToken }, null, 2),
162
+ {
163
+ encoding: "utf-8",
164
+ mode: 384
165
+ }
166
+ );
167
+ console.log(`
168
+ Authenticated successfully${orgName ? ` (${orgName})` : ""}!`);
169
+ console.log(`Token saved to ${CREDENTIALS_FILE}`);
170
+ console.log(
171
+ "Run `haansi collect` to upload sessions or `haansi setup-mcp` to configure Claude Code."
172
+ );
173
+ }
174
+ var import_node_child_process, import_node_fs, import_node_path, import_node_os, DEFAULT_API_URL, DEFAULT_APP_URL, HAANSI_DIR, CREDENTIALS_FILE, POLL_INTERVAL_MS, TIMEOUT_MS;
175
+ var init_auth_login = __esm({
176
+ "src/commands/auth-login.ts"() {
177
+ "use strict";
178
+ import_node_child_process = require("node:child_process");
179
+ import_node_fs = require("node:fs");
180
+ import_node_path = require("node:path");
181
+ import_node_os = require("node:os");
182
+ DEFAULT_API_URL = "https://api.haansi.co";
183
+ DEFAULT_APP_URL = "https://app.haansi.co";
184
+ HAANSI_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".haansi");
185
+ CREDENTIALS_FILE = (0, import_node_path.join)(HAANSI_DIR, "credentials.json");
186
+ POLL_INTERVAL_MS = 2e3;
187
+ TIMEOUT_MS = 5 * 60 * 1e3;
188
+ }
189
+ });
190
+
67
191
  // src/commands/init.ts
68
192
  var init_exports = {};
69
193
  __export(init_exports, {
@@ -93,8 +217,11 @@ async function validateToken(token, apiUrl) {
93
217
  }
94
218
  }
95
219
  async function init() {
96
- const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL;
220
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL2;
97
221
  console.log("Haansi CLI \u2014 Setup\n");
222
+ console.log(
223
+ "Tip: For interactive login, run `haansi auth login` to authenticate via browser.\n"
224
+ );
98
225
  console.log(
99
226
  `You can generate an API token at https://app.haansi.co/settings/tokens
100
227
  `
@@ -110,28 +237,28 @@ async function init() {
110
237
  }
111
238
  console.log("\nValidating token...");
112
239
  await validateToken(token, apiUrl);
113
- (0, import_node_fs.mkdirSync)(HAANSI_DIR, { recursive: true });
114
- (0, import_node_fs.writeFileSync)(CREDENTIALS_FILE, JSON.stringify({ token }, null, 2), {
240
+ (0, import_node_fs2.mkdirSync)(HAANSI_DIR2, { recursive: true });
241
+ (0, import_node_fs2.writeFileSync)(CREDENTIALS_FILE2, JSON.stringify({ token }, null, 2), {
115
242
  encoding: "utf-8",
116
243
  mode: 384
117
244
  });
118
245
  console.log(`
119
- Token saved to ${CREDENTIALS_FILE}`);
246
+ Token saved to ${CREDENTIALS_FILE2}`);
120
247
  console.log(
121
248
  "Run `haansi daemon` to start collecting or `haansi setup-mcp` to configure Claude Code."
122
249
  );
123
250
  }
124
- var readline, import_node_fs, import_node_path, import_node_os, DEFAULT_API_URL, HAANSI_DIR, CREDENTIALS_FILE;
251
+ var readline, import_node_fs2, import_node_path2, import_node_os2, DEFAULT_API_URL2, HAANSI_DIR2, CREDENTIALS_FILE2;
125
252
  var init_init = __esm({
126
253
  "src/commands/init.ts"() {
127
254
  "use strict";
128
255
  readline = __toESM(require("node:readline"));
129
- import_node_fs = require("node:fs");
130
- import_node_path = require("node:path");
131
- import_node_os = require("node:os");
132
- DEFAULT_API_URL = "https://api.haansi.co";
133
- HAANSI_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".haansi");
134
- CREDENTIALS_FILE = (0, import_node_path.join)(HAANSI_DIR, "credentials.json");
256
+ import_node_fs2 = require("node:fs");
257
+ import_node_path2 = require("node:path");
258
+ import_node_os2 = require("node:os");
259
+ DEFAULT_API_URL2 = "https://api.haansi.co";
260
+ HAANSI_DIR2 = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".haansi");
261
+ CREDENTIALS_FILE2 = (0, import_node_path2.join)(HAANSI_DIR2, "credentials.json");
135
262
  }
136
263
  });
137
264
 
@@ -567,7 +694,6 @@ function extractGitInfo(projectDir, sessionTimestamp, sessionBranch) {
567
694
  try {
568
695
  (0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", { ...opts, stdio: "pipe" });
569
696
  } catch {
570
- console.warn(`[collector] Not a git repo: ${projectDir}`);
571
697
  return empty;
572
698
  }
573
699
  const result = { ...empty };
@@ -577,7 +703,6 @@ function extractGitInfo(projectDir, sessionTimestamp, sessionBranch) {
577
703
  stdio: "pipe"
578
704
  }).trim() || null;
579
705
  } catch {
580
- console.warn(`[collector] No git remote for: ${projectDir}`);
581
706
  }
582
707
  if (sessionTimestamp) {
583
708
  const branch = sessionBranch ?? "HEAD";
@@ -593,7 +718,6 @@ function extractGitInfo(projectDir, sessionTimestamp, sessionBranch) {
593
718
  try {
594
719
  result.gitCommit = (0, import_child_process.execSync)("git rev-parse HEAD", { ...opts, stdio: "pipe" }).trim() || null;
595
720
  } catch {
596
- console.warn(`[collector] No git commits in: ${projectDir}`);
597
721
  }
598
722
  }
599
723
  try {
@@ -639,13 +763,13 @@ function extractSessionMeta(filePath) {
639
763
  }
640
764
  function resolveToken() {
641
765
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
642
- if ((0, import_fs.existsSync)(CREDENTIALS_FILE2)) {
766
+ if ((0, import_fs.existsSync)(CREDENTIALS_FILE3)) {
643
767
  try {
644
- const creds = JSON.parse((0, import_fs.readFileSync)(CREDENTIALS_FILE2, "utf-8"));
768
+ const creds = JSON.parse((0, import_fs.readFileSync)(CREDENTIALS_FILE3, "utf-8"));
645
769
  if (creds.token) return creds.token;
646
770
  } catch {
647
771
  logger.warn("Could not parse credentials file", {
648
- file: CREDENTIALS_FILE2
772
+ file: CREDENTIALS_FILE3
649
773
  });
650
774
  }
651
775
  }
@@ -699,7 +823,10 @@ async function getUploadedSessions(apiUrl, token) {
699
823
  const data = await apiGet("/sessions/uploaded", apiUrl, token);
700
824
  const map2 = /* @__PURE__ */ new Map();
701
825
  for (const s of data.sessions) {
702
- map2.set(s.sessionId, s.lineCount);
826
+ map2.set(s.sessionId, {
827
+ lineCount: s.lineCount,
828
+ hasGitMeta: s.hasGitMeta ?? false
829
+ });
703
830
  }
704
831
  return map2;
705
832
  } catch {
@@ -731,7 +858,7 @@ async function uploadSession(sessionId, content, apiUrl, token, gitInfo) {
731
858
  }
732
859
  }
733
860
  async function collect(options) {
734
- const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL2;
861
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
735
862
  const token = resolveToken();
736
863
  if (!token && !options.dryRun) {
737
864
  throw new Error(
@@ -749,8 +876,8 @@ async function collect(options) {
749
876
  const remoteMap = options.forceAll ? /* @__PURE__ */ new Map() : await getUploadedSessions(apiUrl, token);
750
877
  const toUpload = [];
751
878
  for (const session of localSessions) {
752
- const remoteLine = remoteMap.get(session.sessionId);
753
- if (remoteLine !== void 0 && remoteLine >= session.lineCount) {
879
+ const remote = remoteMap.get(session.sessionId);
880
+ if (remote && remote.lineCount >= session.lineCount && remote.hasGitMeta) {
754
881
  skipped++;
755
882
  } else {
756
883
  toUpload.push(session);
@@ -811,13 +938,9 @@ async function main() {
811
938
  dryRun,
812
939
  limit
813
940
  });
814
- console.log(
815
- `
816
- Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
817
- );
818
941
  if (errors > 0) process.exit(1);
819
942
  }
820
- 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;
943
+ var import_dotenv, import_path, import_fs, import_os, import_zlib, import_child_process, logger, DEFAULT_API_URL3, CREDENTIALS_FILE3, CLAUDE_PROJECTS_DIR, MIN_LINES, EDGE_SCRUB_PATTERNS;
821
944
  var init_collector = __esm({
822
945
  "../service-capture/claude-sessions/collector.ts"() {
823
946
  "use strict";
@@ -830,8 +953,8 @@ var init_collector = __esm({
830
953
  init_logger();
831
954
  (0, import_dotenv.config)({ path: (0, import_path.resolve)(__dirname, "../../../.env") });
832
955
  logger = createLogger("claude-collector");
833
- DEFAULT_API_URL2 = "https://api.haansi.co";
834
- CREDENTIALS_FILE2 = (0, import_path.join)((0, import_os.homedir)(), ".haansi", "credentials.json");
956
+ DEFAULT_API_URL3 = "https://api.haansi.co";
957
+ CREDENTIALS_FILE3 = (0, import_path.join)((0, import_os.homedir)(), ".haansi", "credentials.json");
835
958
  CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
836
959
  MIN_LINES = 4;
837
960
  EDGE_SCRUB_PATTERNS = [
@@ -896,17 +1019,17 @@ var require_task = __commonJS({
896
1019
  this._execution = execution;
897
1020
  }
898
1021
  execute(now) {
899
- let exec;
1022
+ let exec2;
900
1023
  try {
901
- exec = this._execution(now);
1024
+ exec2 = this._execution(now);
902
1025
  } catch (error2) {
903
1026
  return this.emit("task-failed", error2);
904
1027
  }
905
- if (exec instanceof Promise) {
906
- return exec.then(() => this.emit("task-finished")).catch((error2) => this.emit("task-failed", error2));
1028
+ if (exec2 instanceof Promise) {
1029
+ return exec2.then(() => this.emit("task-finished")).catch((error2) => this.emit("task-failed", error2));
907
1030
  } else {
908
1031
  this.emit("task-finished");
909
- return exec;
1032
+ return exec2;
910
1033
  }
911
1034
  }
912
1035
  };
@@ -50549,7 +50672,7 @@ var require_view = __commonJS({
50549
50672
  var dirname = path.dirname;
50550
50673
  var basename2 = path.basename;
50551
50674
  var extname = path.extname;
50552
- var join8 = path.join;
50675
+ var join9 = path.join;
50553
50676
  var resolve4 = path.resolve;
50554
50677
  module2.exports = View;
50555
50678
  function View(name, options) {
@@ -50611,12 +50734,12 @@ var require_view = __commonJS({
50611
50734
  };
50612
50735
  View.prototype.resolve = function resolve5(dir, file2) {
50613
50736
  var ext = this.ext;
50614
- var path2 = join8(dir, file2);
50737
+ var path2 = join9(dir, file2);
50615
50738
  var stat = tryStat(path2);
50616
50739
  if (stat && stat.isFile()) {
50617
50740
  return path2;
50618
50741
  }
50619
- path2 = join8(dir, basename2(file2, ext), "index" + ext);
50742
+ path2 = join9(dir, basename2(file2, ext), "index" + ext);
50620
50743
  stat = tryStat(path2);
50621
50744
  if (stat && stat.isFile()) {
50622
50745
  return path2;
@@ -54261,7 +54384,7 @@ var require_send = __commonJS({
54261
54384
  var Stream = require("stream");
54262
54385
  var util2 = require("util");
54263
54386
  var extname = path.extname;
54264
- var join8 = path.join;
54387
+ var join9 = path.join;
54265
54388
  var normalize = path.normalize;
54266
54389
  var resolve4 = path.resolve;
54267
54390
  var sep = path.sep;
@@ -54433,7 +54556,7 @@ var require_send = __commonJS({
54433
54556
  return res;
54434
54557
  }
54435
54558
  parts = path2.split(sep);
54436
- path2 = normalize(join8(root, path2));
54559
+ path2 = normalize(join9(root, path2));
54437
54560
  } else {
54438
54561
  if (UP_PATH_REGEXP.test(path2)) {
54439
54562
  debug('malicious path "%s"', path2);
@@ -54566,7 +54689,7 @@ var require_send = __commonJS({
54566
54689
  if (err) return self.onStatError(err);
54567
54690
  return self.error(404);
54568
54691
  }
54569
- var p = join8(path2, self._index[i]);
54692
+ var p = join9(path2, self._index[i]);
54570
54693
  debug('stat "%s"', p);
54571
54694
  fs.stat(p, function(err2, stat) {
54572
54695
  if (err2) return next(err2);
@@ -56663,9 +56786,9 @@ var init_streamableHttp = __esm({
56663
56786
  var mcp_server_exports = {};
56664
56787
  function resolveToken2() {
56665
56788
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
56666
- if ((0, import_fs2.existsSync)(CREDENTIALS_FILE3)) {
56789
+ if ((0, import_fs2.existsSync)(CREDENTIALS_FILE4)) {
56667
56790
  try {
56668
- const creds = JSON.parse((0, import_fs2.readFileSync)(CREDENTIALS_FILE3, "utf-8"));
56791
+ const creds = JSON.parse((0, import_fs2.readFileSync)(CREDENTIALS_FILE4, "utf-8"));
56669
56792
  if (creds.token) return creds.token;
56670
56793
  } catch {
56671
56794
  }
@@ -56675,13 +56798,9 @@ function resolveToken2() {
56675
56798
  function triggerCollectInBackground() {
56676
56799
  if (isCollecting) return;
56677
56800
  isCollecting = true;
56678
- collect({}).then(({ uploaded, skipped, errors }) => {
56679
- console.error(
56680
- `[mcp-server] Collection done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
56681
- );
56801
+ collect({}).then(() => {
56682
56802
  lastCollectTime = Date.now();
56683
- }).catch((err) => {
56684
- console.error("[mcp-server] Collection error:", err.message);
56803
+ }).catch(() => {
56685
56804
  }).finally(() => {
56686
56805
  isCollecting = false;
56687
56806
  });
@@ -56723,23 +56842,10 @@ async function apiPost(path, body, options) {
56723
56842
  async function main3() {
56724
56843
  try {
56725
56844
  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
- );
56845
+ } catch {
56732
56846
  }
56733
56847
  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
- }
56848
+ await apiGet2("/users/me/preferences");
56743
56849
  } catch {
56744
56850
  }
56745
56851
  triggerCollectInBackground();
@@ -56760,18 +56866,13 @@ async function main3() {
56760
56866
  app.get("/health", (_req, res) => {
56761
56867
  res.json({ status: "ok", service: "haansi-solutions-mcp" });
56762
56868
  });
56763
- app.listen(PORT, () => {
56764
- console.error(
56765
- `[mcp-server] Haansi Solutions MCP server running on HTTP port ${PORT}`
56766
- );
56767
- });
56869
+ app.listen(PORT);
56768
56870
  } else {
56769
56871
  const transport = new StdioServerTransport();
56770
56872
  await mcpServer.connect(transport);
56771
- console.error("[mcp-server] haansi-solutions MCP server running on stdio");
56772
56873
  }
56773
56874
  }
56774
- 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;
56875
+ var import_dotenv3, import_path3, import_fs2, import_os2, DEFAULT_API_URL4, CREDENTIALS_FILE4, API_URL, TOKEN, contextSchema, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
56775
56876
  var init_mcp_server2 = __esm({
56776
56877
  "../service-capture/claude-sessions/mcp-server.ts"() {
56777
56878
  "use strict";
@@ -56785,14 +56886,11 @@ var init_mcp_server2 = __esm({
56785
56886
  init_stdio2();
56786
56887
  init_types2();
56787
56888
  (0, import_dotenv3.config)({ path: (0, import_path3.resolve)(__dirname, "../../../.env") });
56788
- DEFAULT_API_URL3 = "https://api.haansi.co";
56789
- CREDENTIALS_FILE3 = (0, import_path3.join)((0, import_os2.homedir)(), ".haansi", "credentials.json");
56790
- API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
56889
+ DEFAULT_API_URL4 = "https://api.haansi.co";
56890
+ CREDENTIALS_FILE4 = (0, import_path3.join)((0, import_os2.homedir)(), ".haansi", "credentials.json");
56891
+ API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL4;
56791
56892
  TOKEN = resolveToken2();
56792
56893
  if (!TOKEN) {
56793
- console.error(
56794
- "[mcp-server] ERROR: HAANSI_TOKEN is required (or set in ~/.haansi/credentials.json)"
56795
- );
56796
56894
  process.exit(1);
56797
56895
  }
56798
56896
  contextSchema = {
@@ -56820,7 +56918,7 @@ var init_mcp_server2 = __esm({
56820
56918
  tools: [
56821
56919
  {
56822
56920
  name: "search_solutions",
56823
- description: "Semantic search over past coding solutions mined from developer sessions. Use this to find how bugs were fixed, what patterns were used, and past resolutions to similar problems. Returns results ranked by relevance.",
56921
+ description: "Search previously solved similar questions from your organization's knowledge base. Finds how bugs were fixed, what patterns were used, and past resolutions to similar problems. Results are ranked by a composite of relevance, quality, recency, and verification status.",
56824
56922
  inputSchema: {
56825
56923
  type: "object",
56826
56924
  properties: {
@@ -56942,7 +57040,6 @@ var init_mcp_server2 = __esm({
56942
57040
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
56943
57041
  maybeCollect();
56944
57042
  const { name, arguments: args } = request.params;
56945
- console.error(`[mcp-server] Tool: ${name}`);
56946
57043
  try {
56947
57044
  switch (name) {
56948
57045
  case "search_solutions": {
@@ -56973,8 +57070,13 @@ var init_mcp_server2 = __esm({
56973
57070
  ]
56974
57071
  };
56975
57072
  }
57073
+ const header = `Found ${data.total} answer(s) from similar questions in your organization:
57074
+
57075
+ `;
56976
57076
  return {
56977
- content: [{ type: "text", text: data.results.join("\n\n---\n\n") }]
57077
+ content: [
57078
+ { type: "text", text: header + data.results.join("\n\n---\n\n") }
57079
+ ]
56978
57080
  };
56979
57081
  }
56980
57082
  case "list_recent_solutions": {
@@ -57067,15 +57169,13 @@ var init_mcp_server2 = __esm({
57067
57169
  throw new Error(`Unknown tool: ${name}`);
57068
57170
  }
57069
57171
  } catch (err) {
57070
- console.error(`[mcp-server] Tool ${name} error:`, err.message);
57071
57172
  return {
57072
57173
  content: [{ type: "text", text: `Error: ${err.message}` }],
57073
57174
  isError: true
57074
57175
  };
57075
57176
  }
57076
57177
  });
57077
- main3().catch((err) => {
57078
- console.error("[mcp-server] Failed to start:", err);
57178
+ main3().catch(() => {
57079
57179
  process.exit(1);
57080
57180
  });
57081
57181
  }
@@ -57088,7 +57188,7 @@ __export(setup_mcp_exports, {
57088
57188
  });
57089
57189
  function resolveHaansiBin() {
57090
57190
  try {
57091
- const binPath = (0, import_node_child_process.execSync)("which haansi", { encoding: "utf-8" }).trim();
57191
+ const binPath = (0, import_node_child_process2.execSync)("which haansi", { encoding: "utf-8" }).trim();
57092
57192
  if (binPath) return binPath;
57093
57193
  } catch {
57094
57194
  }
@@ -57096,9 +57196,9 @@ function resolveHaansiBin() {
57096
57196
  }
57097
57197
  async function setupMcp() {
57098
57198
  let config6 = {};
57099
- if ((0, import_node_fs2.existsSync)(CLAUDE_JSON)) {
57199
+ if ((0, import_node_fs3.existsSync)(CLAUDE_JSON)) {
57100
57200
  try {
57101
- config6 = JSON.parse((0, import_node_fs2.readFileSync)(CLAUDE_JSON, "utf-8"));
57201
+ config6 = JSON.parse((0, import_node_fs3.readFileSync)(CLAUDE_JSON, "utf-8"));
57102
57202
  } catch {
57103
57203
  console.error(
57104
57204
  `Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`
@@ -57113,22 +57213,22 @@ async function setupMcp() {
57113
57213
  command: haansiBin,
57114
57214
  args: ["mcp-server"]
57115
57215
  };
57116
- (0, import_node_fs2.writeFileSync)(CLAUDE_JSON, JSON.stringify(config6, null, 2) + "\n", "utf-8");
57216
+ (0, import_node_fs3.writeFileSync)(CLAUDE_JSON, JSON.stringify(config6, null, 2) + "\n", "utf-8");
57117
57217
  console.log(`MCP server configured in ${CLAUDE_JSON}`);
57118
57218
  console.log(` command: ${haansiBin} mcp-server`);
57119
57219
  console.log(
57120
57220
  "\nRestart Claude Code to activate the haansi-solutions MCP server."
57121
57221
  );
57122
57222
  }
57123
- var import_node_fs2, import_node_path2, import_node_os2, import_node_child_process, CLAUDE_JSON;
57223
+ var import_node_fs3, import_node_path3, import_node_os3, import_node_child_process2, CLAUDE_JSON;
57124
57224
  var init_setup_mcp = __esm({
57125
57225
  "src/commands/setup-mcp.ts"() {
57126
57226
  "use strict";
57127
- import_node_fs2 = require("node:fs");
57128
- import_node_path2 = require("node:path");
57129
- import_node_os2 = require("node:os");
57130
- import_node_child_process = require("node:child_process");
57131
- CLAUDE_JSON = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".claude.json");
57227
+ import_node_fs3 = require("node:fs");
57228
+ import_node_path3 = require("node:path");
57229
+ import_node_os3 = require("node:os");
57230
+ import_node_child_process2 = require("node:child_process");
57231
+ CLAUDE_JSON = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".claude.json");
57132
57232
  }
57133
57233
  });
57134
57234
 
@@ -57139,7 +57239,7 @@ __export(setup_daemon_exports, {
57139
57239
  });
57140
57240
  function findHaansiBin() {
57141
57241
  try {
57142
- return (0, import_node_child_process2.execSync)("which haansi", { encoding: "utf-8" }).trim();
57242
+ return (0, import_node_child_process3.execSync)("which haansi", { encoding: "utf-8" }).trim();
57143
57243
  } catch {
57144
57244
  throw new Error(
57145
57245
  "Could not find haansi in PATH. Make sure it is installed globally (npm install -g haansi)."
@@ -57178,26 +57278,28 @@ function buildPlist(haansiPath) {
57178
57278
  }
57179
57279
  async function setupDaemon(uninstall) {
57180
57280
  if (uninstall) {
57181
- if (!(0, import_node_fs3.existsSync)(PLIST_PATH)) {
57281
+ if (!(0, import_node_fs4.existsSync)(PLIST_PATH)) {
57182
57282
  console.log("Daemon is not installed.");
57183
57283
  return;
57184
57284
  }
57185
57285
  try {
57186
- (0, import_node_child_process2.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
57187
- } catch {
57286
+ (0, import_node_child_process3.spawnSync)("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
57287
+ } catch (err) {
57288
+ console.error("Failed to unload daemon:", err);
57188
57289
  }
57189
- (0, import_node_fs3.unlinkSync)(PLIST_PATH);
57290
+ (0, import_node_fs4.unlinkSync)(PLIST_PATH);
57190
57291
  console.log("Daemon stopped and removed.");
57191
57292
  return;
57192
57293
  }
57193
57294
  const haansiPath = findHaansiBin();
57194
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi"), { recursive: true });
57195
- (0, import_node_fs3.writeFileSync)(PLIST_PATH, buildPlist(haansiPath), "utf-8");
57295
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.join)((0, import_node_os4.homedir)(), ".haansi"), { recursive: true });
57296
+ (0, import_node_fs4.writeFileSync)(PLIST_PATH, buildPlist(haansiPath), "utf-8");
57196
57297
  try {
57197
- (0, import_node_child_process2.execSync)(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
57198
- } catch {
57298
+ (0, import_node_child_process3.spawnSync)("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
57299
+ } catch (err) {
57300
+ console.error("Failed to unload previous daemon:", err);
57199
57301
  }
57200
- (0, import_node_child_process2.execSync)(`launchctl load "${PLIST_PATH}"`, { stdio: "inherit" });
57302
+ (0, import_node_child_process3.spawnSync)("launchctl", ["load", PLIST_PATH], { stdio: "inherit" });
57201
57303
  console.log("Daemon installed and started.");
57202
57304
  console.log(`Plist : ${PLIST_PATH}`);
57203
57305
  console.log(`Logs : ${LOG_FILE}`);
@@ -57205,22 +57307,22 @@ async function setupDaemon(uninstall) {
57205
57307
  The daemon will start automatically on every login.`);
57206
57308
  console.log(`To uninstall: haansi setup-daemon --uninstall`);
57207
57309
  }
57208
- var import_node_child_process2, import_node_fs3, import_node_path3, import_node_os3, PLIST_LABEL, PLIST_PATH, LOG_FILE;
57310
+ var import_node_child_process3, import_node_fs4, import_node_path4, import_node_os4, PLIST_LABEL, PLIST_PATH, LOG_FILE;
57209
57311
  var init_setup_daemon = __esm({
57210
57312
  "src/commands/setup-daemon.ts"() {
57211
57313
  "use strict";
57212
- import_node_child_process2 = require("node:child_process");
57213
- import_node_fs3 = require("node:fs");
57214
- import_node_path3 = require("node:path");
57215
- import_node_os3 = require("node:os");
57314
+ import_node_child_process3 = require("node:child_process");
57315
+ import_node_fs4 = require("node:fs");
57316
+ import_node_path4 = require("node:path");
57317
+ import_node_os4 = require("node:os");
57216
57318
  PLIST_LABEL = "co.haansi.daemon";
57217
- PLIST_PATH = (0, import_node_path3.join)(
57218
- (0, import_node_os3.homedir)(),
57319
+ PLIST_PATH = (0, import_node_path4.join)(
57320
+ (0, import_node_os4.homedir)(),
57219
57321
  "Library",
57220
57322
  "LaunchAgents",
57221
57323
  `${PLIST_LABEL}.plist`
57222
57324
  );
57223
- LOG_FILE = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi", "daemon.log");
57325
+ LOG_FILE = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".haansi", "daemon.log");
57224
57326
  }
57225
57327
  });
57226
57328
 
@@ -57231,11 +57333,12 @@ __export(config_exports, {
57231
57333
  });
57232
57334
  function resolveToken3() {
57233
57335
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
57234
- if ((0, import_node_fs4.existsSync)(CREDENTIALS_FILE4)) {
57336
+ if ((0, import_node_fs5.existsSync)(CREDENTIALS_FILE5)) {
57235
57337
  try {
57236
- const creds = JSON.parse((0, import_node_fs4.readFileSync)(CREDENTIALS_FILE4, "utf-8"));
57338
+ const creds = JSON.parse((0, import_node_fs5.readFileSync)(CREDENTIALS_FILE5, "utf-8"));
57237
57339
  if (creds.token) return creds.token;
57238
- } catch {
57340
+ } catch (err) {
57341
+ console.error("Failed to read credentials file:", err);
57239
57342
  }
57240
57343
  }
57241
57344
  return null;
@@ -57249,7 +57352,7 @@ async function config5(args) {
57249
57352
  process.exit(1);
57250
57353
  return;
57251
57354
  }
57252
- const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL4;
57355
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL5;
57253
57356
  const subcommand = args[0];
57254
57357
  if (subcommand === "get") {
57255
57358
  const key = args[1];
@@ -57324,15 +57427,15 @@ Supported keys:
57324
57427
  if (subcommand !== void 0) process.exit(1);
57325
57428
  }
57326
57429
  }
57327
- var import_node_fs4, import_node_path4, import_node_os4, DEFAULT_API_URL4, CREDENTIALS_FILE4, VALID_SESSION_MODES;
57430
+ var import_node_fs5, import_node_path5, import_node_os5, DEFAULT_API_URL5, CREDENTIALS_FILE5, VALID_SESSION_MODES;
57328
57431
  var init_config = __esm({
57329
57432
  "src/commands/config.ts"() {
57330
57433
  "use strict";
57331
- import_node_fs4 = require("node:fs");
57332
- import_node_path4 = require("node:path");
57333
- import_node_os4 = require("node:os");
57334
- DEFAULT_API_URL4 = "https://api.haansi.co";
57335
- CREDENTIALS_FILE4 = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".haansi", "credentials.json");
57434
+ import_node_fs5 = require("node:fs");
57435
+ import_node_path5 = require("node:path");
57436
+ import_node_os5 = require("node:os");
57437
+ DEFAULT_API_URL5 = "https://api.haansi.co";
57438
+ CREDENTIALS_FILE5 = (0, import_node_path5.join)((0, import_node_os5.homedir)(), ".haansi", "credentials.json");
57336
57439
  VALID_SESSION_MODES = ["private", "org"];
57337
57440
  }
57338
57441
  });
@@ -57346,11 +57449,12 @@ __export(list_exports, {
57346
57449
  });
57347
57450
  function resolveToken4() {
57348
57451
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
57349
- if ((0, import_node_fs5.existsSync)(CREDENTIALS_FILE5)) {
57452
+ if ((0, import_node_fs6.existsSync)(CREDENTIALS_FILE6)) {
57350
57453
  try {
57351
- const creds = JSON.parse((0, import_node_fs5.readFileSync)(CREDENTIALS_FILE5, "utf-8"));
57454
+ const creds = JSON.parse((0, import_node_fs6.readFileSync)(CREDENTIALS_FILE6, "utf-8"));
57352
57455
  if (creds.token) return creds.token;
57353
- } catch {
57456
+ } catch (err) {
57457
+ console.error("Failed to read credentials file:", err);
57354
57458
  }
57355
57459
  }
57356
57460
  throw new Error("No token found. Run `haansi init` first.");
@@ -57381,7 +57485,7 @@ async function apiPost2(path, body, token, apiUrl) {
57381
57485
  return response.json();
57382
57486
  }
57383
57487
  async function list(options) {
57384
- const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL5;
57488
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL6;
57385
57489
  const token = resolveToken4();
57386
57490
  let data;
57387
57491
  if (options.search) {
@@ -57407,7 +57511,7 @@ async function list(options) {
57407
57511
  ${data.results.length} result(s)`);
57408
57512
  }
57409
57513
  async function searchKnowledge(options) {
57410
- const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL5;
57514
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL6;
57411
57515
  const token = resolveToken4();
57412
57516
  let url2 = `/sessions/knowledge/search?q=${encodeURIComponent(options.query)}&limit=${options.limit}`;
57413
57517
  if (options.artifactType) {
@@ -57424,7 +57528,7 @@ async function searchKnowledge(options) {
57424
57528
  ${data.results.length} result(s)`);
57425
57529
  }
57426
57530
  async function saveKnowledge(options) {
57427
- const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL5;
57531
+ const apiUrl = process.env.HAANSI_API_URL ?? DEFAULT_API_URL6;
57428
57532
  const token = resolveToken4();
57429
57533
  const body = {
57430
57534
  problem_description: options.problem,
@@ -57440,15 +57544,15 @@ async function saveKnowledge(options) {
57440
57544
  console.log(` Solution: ${data.solution_summary}`);
57441
57545
  console.log("\nThis is now searchable via `haansi search-solutions`.");
57442
57546
  }
57443
- var import_node_fs5, import_node_path5, import_node_os5, DEFAULT_API_URL5, CREDENTIALS_FILE5;
57547
+ var import_node_fs6, import_node_path6, import_node_os6, DEFAULT_API_URL6, CREDENTIALS_FILE6;
57444
57548
  var init_list = __esm({
57445
57549
  "src/commands/list.ts"() {
57446
57550
  "use strict";
57447
- import_node_fs5 = require("node:fs");
57448
- import_node_path5 = require("node:path");
57449
- import_node_os5 = require("node:os");
57450
- DEFAULT_API_URL5 = "https://api.haansi.co";
57451
- CREDENTIALS_FILE5 = (0, import_node_path5.join)((0, import_node_os5.homedir)(), ".haansi", "credentials.json");
57551
+ import_node_fs6 = require("node:fs");
57552
+ import_node_path6 = require("node:path");
57553
+ import_node_os6 = require("node:os");
57554
+ DEFAULT_API_URL6 = "https://api.haansi.co";
57555
+ CREDENTIALS_FILE6 = (0, import_node_path6.join)((0, import_node_os6.homedir)(), ".haansi", "credentials.json");
57452
57556
  }
57453
57557
  });
57454
57558
 
@@ -57461,6 +57565,20 @@ async function run2() {
57461
57565
  return;
57462
57566
  }
57463
57567
  switch (command) {
57568
+ case "auth": {
57569
+ const subcommand = process.argv[3];
57570
+ if (subcommand === "login") {
57571
+ const { authLogin: authLogin2 } = await Promise.resolve().then(() => (init_auth_login(), auth_login_exports));
57572
+ await authLogin2();
57573
+ } else {
57574
+ console.error(
57575
+ `Unknown auth subcommand: ${subcommand ?? "(none)"}
57576
+ Usage: haansi auth login`
57577
+ );
57578
+ process.exit(1);
57579
+ }
57580
+ break;
57581
+ }
57464
57582
  case "init": {
57465
57583
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
57466
57584
  await init2();
@@ -57587,7 +57705,8 @@ Usage:
57587
57705
  haansi <command> [options]
57588
57706
 
57589
57707
  Commands:
57590
- init Authenticate and save your API token
57708
+ auth login Log in via browser (recommended)
57709
+ init Authenticate with a token (for CI / headless)
57591
57710
  collect Upload new/changed Claude sessions once
57592
57711
  config get [key] Show your preferences (e.g. session_mode)
57593
57712
  config set <key> <val> Update a preference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"