haansi 0.1.1 → 0.1.3

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 +261 -618
  2. package/package.json +1 -1
package/dist/haansi.js CHANGED
@@ -44,10 +44,10 @@ function prompt(question) {
44
44
  input: process.stdin,
45
45
  output: process.stdout
46
46
  });
47
- return new Promise((resolve5) => {
47
+ return new Promise((resolve4) => {
48
48
  rl.question(question, (answer) => {
49
49
  rl.close();
50
- resolve5(answer.trim());
50
+ resolve4(answer.trim());
51
51
  });
52
52
  });
53
53
  }
@@ -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.");
@@ -378,7 +380,7 @@ var require_main = __commonJS({
378
380
  return { parsed: parsedAll };
379
381
  }
380
382
  }
381
- function config6(options) {
383
+ function config5(options) {
382
384
  if (_dotenvKey(options).length === 0) {
383
385
  return DotenvModule.configDotenv(options);
384
386
  }
@@ -445,7 +447,7 @@ var require_main = __commonJS({
445
447
  configDotenv,
446
448
  _configVault,
447
449
  _parseVault,
448
- config: config6,
450
+ config: config5,
449
451
  decrypt,
450
452
  parse: parse4,
451
453
  populate
@@ -513,469 +515,6 @@ var init_logger = __esm({
513
515
  }
514
516
  });
515
517
 
516
- // ../service-capture/claude-sessions/reader.ts
517
- var import_fs, import_fs2, import_path, import_os, logger, CLAUDE_DIR, PROJECTS_DIR, ClaudeSessionReader;
518
- var init_reader = __esm({
519
- "../service-capture/claude-sessions/reader.ts"() {
520
- "use strict";
521
- import_fs = require("fs");
522
- import_fs2 = require("fs");
523
- import_path = require("path");
524
- import_os = require("os");
525
- init_logger();
526
- logger = createLogger("claude-session-reader");
527
- CLAUDE_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude");
528
- PROJECTS_DIR = (0, import_path.join)(CLAUDE_DIR, "projects");
529
- ClaudeSessionReader = class {
530
- /**
531
- * Convert a filesystem path to the ~/.claude/projects/ directory name format.
532
- * Claude Code replaces both '/' and '_' with '-'.
533
- * e.g. /Users/foo/git_repos/haansi-platform → -Users-foo-git-repos-haansi-platform
534
- */
535
- static projectDirName(absolutePath) {
536
- return absolutePath.replace(/[/_]/g, "-");
537
- }
538
- /**
539
- * Returns the ~/.claude/projects/[encoded-path]/ directory for a given project.
540
- */
541
- static getProjectSessionsDir(projectAbsolutePath) {
542
- return (0, import_path.join)(PROJECTS_DIR, this.projectDirName(projectAbsolutePath));
543
- }
544
- /**
545
- * List all project directories in ~/.claude/projects/.
546
- */
547
- listProjectDirs() {
548
- if (!(0, import_fs.existsSync)(PROJECTS_DIR)) return [];
549
- return (0, import_fs2.readdirSync)(PROJECTS_DIR).map((name) => (0, import_path.join)(PROJECTS_DIR, name)).filter((p) => (0, import_fs2.statSync)(p).isDirectory());
550
- }
551
- /**
552
- * Read and parse sessions-index.json for a given project sessions directory.
553
- * Returns null if the file doesn't exist or can't be parsed.
554
- */
555
- readSessionsIndex(sessionsDir) {
556
- const indexPath = (0, import_path.join)(sessionsDir, "sessions-index.json");
557
- if (!(0, import_fs.existsSync)(indexPath)) return null;
558
- try {
559
- const raw = (0, import_fs.readFileSync)(indexPath, "utf-8");
560
- return JSON.parse(raw);
561
- } catch (err) {
562
- logger.warn("Failed to parse sessions-index.json", {
563
- sessionsDir,
564
- err: String(err)
565
- });
566
- return null;
567
- }
568
- }
569
- /**
570
- * Parse all JSONL entries from a session file. Returns raw entries in file order.
571
- * Lines that fail to parse are skipped with a warning.
572
- */
573
- parseSessionFile(filePath) {
574
- if (!(0, import_fs.existsSync)(filePath)) {
575
- logger.warn("Session file not found", { filePath });
576
- return [];
577
- }
578
- const entries = [];
579
- const lines = (0, import_fs.readFileSync)(filePath, "utf-8").split("\n");
580
- for (let i = 0; i < lines.length; i++) {
581
- const line = lines[i].trim();
582
- if (!line) continue;
583
- try {
584
- entries.push(JSON.parse(line));
585
- } catch {
586
- }
587
- }
588
- return entries;
589
- }
590
- /**
591
- * Extract plain text from a message content value.
592
- * Handles both legacy string content and structured block arrays.
593
- */
594
- extractText(content) {
595
- if (typeof content === "string") return content;
596
- return content.filter(
597
- (b) => b.type === "text" || b.type === "thinking"
598
- ).map((b) => "text" in b ? b.text : b.thinking ?? "").join("\n").trim();
599
- }
600
- /**
601
- * Extract tool_use blocks from a message content array (assistant turns).
602
- */
603
- extractToolCalls(content) {
604
- if (typeof content === "string") return [];
605
- return content.filter(
606
- (b) => b.type === "tool_use"
607
- ).map((b) => ({
608
- id: b.id,
609
- name: b.name,
610
- input: b.input
611
- }));
612
- }
613
- /**
614
- * Extract tool_result blocks from a message content array (user turns).
615
- */
616
- extractToolResults(content) {
617
- if (typeof content === "string") return [];
618
- return content.filter(
619
- (b) => b.type === "tool_result"
620
- ).map((b) => {
621
- const rawContent = b.content;
622
- let text;
623
- if (typeof rawContent === "string") {
624
- text = rawContent;
625
- } else if (Array.isArray(rawContent)) {
626
- text = rawContent.map((item) => "text" in item ? item.text ?? "" : "").join("\n");
627
- } else {
628
- text = "";
629
- }
630
- return {
631
- toolUseId: b.tool_use_id,
632
- content: text,
633
- isError: b.is_error ?? false
634
- };
635
- });
636
- }
637
- /**
638
- * Convert raw JSONL entries into ordered ClauseTurns.
639
- * Reconstruction order: build a parentUuid → child map, then walk from roots.
640
- * Falls back to file order if the graph has cycles or disconnected nodes.
641
- */
642
- reconstructThread(entries) {
643
- const messageEntries = entries.filter(
644
- (e) => (e.type === "user" || e.type === "assistant") && e.message
645
- );
646
- if (messageEntries.length === 0) return [];
647
- const byUuid = /* @__PURE__ */ new Map();
648
- const childrenOf = /* @__PURE__ */ new Map();
649
- for (const entry of messageEntries) {
650
- byUuid.set(entry.uuid, entry);
651
- const parent = entry.parentUuid ?? "__root__";
652
- if (!childrenOf.has(parent)) childrenOf.set(parent, []);
653
- childrenOf.get(parent).push(entry.uuid);
654
- }
655
- const ordered = [];
656
- const visited = /* @__PURE__ */ new Set();
657
- const queue = childrenOf.get("__root__") ?? [];
658
- while (queue.length > 0) {
659
- const uuid3 = queue.shift();
660
- if (visited.has(uuid3)) continue;
661
- visited.add(uuid3);
662
- const entry = byUuid.get(uuid3);
663
- if (entry) {
664
- ordered.push(entry);
665
- const children = childrenOf.get(uuid3) ?? [];
666
- queue.push(...children);
667
- }
668
- }
669
- for (const entry of messageEntries) {
670
- if (!visited.has(entry.uuid)) {
671
- ordered.push(entry);
672
- }
673
- }
674
- return ordered.map((entry) => {
675
- const content = entry.message.content;
676
- return {
677
- uuid: entry.uuid,
678
- parentUuid: entry.parentUuid,
679
- role: entry.message.role,
680
- text: this.extractText(content),
681
- toolCalls: this.extractToolCalls(content),
682
- toolResults: this.extractToolResults(content),
683
- timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
684
- isSidechain: entry.isSidechain ?? false,
685
- agentId: entry.agentId,
686
- gitBranch: entry.gitBranch
687
- };
688
- });
689
- }
690
- /**
691
- * Read a single session JSONL file and return a ClaudeSession.
692
- * Uses index metadata when provided for faster startup (avoids full parse for summary/firstPrompt).
693
- */
694
- readSession(filePath, indexEntry) {
695
- const entries = this.parseSessionFile(filePath);
696
- const turns = this.reconstructThread(entries);
697
- const stat = (0, import_fs2.statSync)(filePath);
698
- const sessionId = indexEntry?.sessionId ?? (0, import_path.basename)(filePath, ".jsonl");
699
- const summaryEntry = entries.find((e) => e.type === "summary");
700
- const summary = indexEntry?.summary ?? summaryEntry?.summary;
701
- const firstUserTurn = turns.find(
702
- (t) => t.role === "user" && !t.isSidechain
703
- );
704
- const firstPrompt = indexEntry?.firstPrompt ?? firstUserTurn?.text?.slice(0, 500) ?? "";
705
- return {
706
- sessionId,
707
- projectPath: indexEntry?.projectPath ?? "",
708
- filePath,
709
- fileMtime: stat.mtimeMs,
710
- gitBranch: indexEntry?.gitBranch ?? turns[0]?.gitBranch,
711
- firstPrompt,
712
- summary,
713
- created: indexEntry?.created ?? turns[0]?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
714
- modified: indexEntry?.modified ?? turns[turns.length - 1]?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
715
- turns,
716
- subagentSessions: [],
717
- // populated separately by ingester if needed
718
- messageCount: turns.length
719
- };
720
- }
721
- /**
722
- * Read all sessions for a project directory using the sessions-index for efficiency.
723
- * Only reads JSONL files that are present and parseable.
724
- */
725
- readAllSessions(sessionsDir) {
726
- const index = this.readSessionsIndex(sessionsDir);
727
- const sessions = [];
728
- if (!index) {
729
- logger.warn(
730
- "No sessions-index.json found, falling back to directory scan",
731
- { sessionsDir }
732
- );
733
- return this.scanSessionFiles(sessionsDir);
734
- }
735
- const mainEntries = index.entries.filter((e) => !e.isSidechain);
736
- for (const entry of mainEntries) {
737
- if (!(0, import_fs.existsSync)(entry.fullPath)) continue;
738
- try {
739
- const session = this.readSession(entry.fullPath, entry);
740
- sessions.push(session);
741
- } catch (err) {
742
- logger.warn("Failed to read session", {
743
- sessionId: entry.sessionId,
744
- err: String(err)
745
- });
746
- }
747
- }
748
- logger.info(`Read ${sessions.length} sessions from index`, { sessionsDir });
749
- return sessions;
750
- }
751
- /**
752
- * Fallback: scan a directory for *.jsonl files and parse each one.
753
- * Skips the subagents/ subdirectory (handled separately).
754
- */
755
- scanSessionFiles(sessionsDir) {
756
- if (!(0, import_fs.existsSync)(sessionsDir)) return [];
757
- const sessions = [];
758
- const entries = (0, import_fs2.readdirSync)(sessionsDir);
759
- for (const name of entries) {
760
- if (!name.endsWith(".jsonl")) continue;
761
- const filePath = (0, import_path.join)(sessionsDir, name);
762
- try {
763
- sessions.push(this.readSession(filePath));
764
- } catch (err) {
765
- logger.warn("Failed to parse session file", { name, err: String(err) });
766
- }
767
- }
768
- return sessions;
769
- }
770
- };
771
- }
772
- });
773
-
774
- // ../service-capture/claude-sessions/ingester.ts
775
- async function main() {
776
- const projectPath = process.env.CLAUDE_PROJECT_PATH ?? process.cwd();
777
- const forceAll = process.argv.includes("--force-all");
778
- logger2.info("Claude Session Ingester starting", { projectPath, forceAll });
779
- const ingester = new ClaudeSessionIngester({ projectPath });
780
- const result = await ingester.scan({ forceAll });
781
- console.log("\n=== Scan Results ===");
782
- console.log(`Total sessions scanned : ${result.totalScanned}`);
783
- console.log(`New sessions : ${result.newSessions.length}`);
784
- console.log(`Updated sessions : ${result.updatedSessions.length}`);
785
- console.log(`Unchanged (skipped) : ${result.unchangedCount}`);
786
- const toProcess = [...result.newSessions, ...result.updatedSessions];
787
- if (toProcess.length === 0) {
788
- console.log("\nNo new sessions to process.");
789
- return;
790
- }
791
- console.log("\n=== Sessions to Process ===");
792
- for (const session of toProcess) {
793
- const preview = session.firstPrompt.slice(0, 100).replace(/\n/g, " ");
794
- console.log(
795
- `
796
- [${session.sessionId.slice(0, 8)}] ${session.gitBranch ?? "unknown-branch"}`
797
- );
798
- console.log(` Turns : ${session.messageCount}`);
799
- console.log(` Created : ${session.created.slice(0, 19)}`);
800
- console.log(` Modified : ${session.modified.slice(0, 19)}`);
801
- console.log(` Preview : ${preview}${preview.length === 100 ? "\u2026" : ""}`);
802
- }
803
- console.log(
804
- "\n\u2713 Reader and ingester working. Next step: resolution detection."
805
- );
806
- }
807
- var import_dotenv, import_path2, import_fs3, import_os2, logger2, DEFAULT_STATE_FILE, STATE_VERSION, ClaudeSessionIngester;
808
- var init_ingester = __esm({
809
- "../service-capture/claude-sessions/ingester.ts"() {
810
- "use strict";
811
- import_dotenv = __toESM(require_main());
812
- import_path2 = require("path");
813
- import_fs3 = require("fs");
814
- import_os2 = require("os");
815
- init_reader();
816
- init_logger();
817
- (0, import_dotenv.config)({ path: (0, import_path2.resolve)(__dirname, "../../../.env") });
818
- logger2 = createLogger("claude-session-ingester");
819
- DEFAULT_STATE_FILE = (0, import_path2.join)(
820
- (0, import_os2.homedir)(),
821
- ".claude",
822
- "haansi-mining-state.json"
823
- );
824
- STATE_VERSION = 1;
825
- ClaudeSessionIngester = class {
826
- reader;
827
- stateFile;
828
- projectPath;
829
- constructor(options) {
830
- this.reader = new ClaudeSessionReader();
831
- this.projectPath = options?.projectPath ?? process.env.CLAUDE_PROJECT_PATH ?? process.cwd();
832
- this.stateFile = options?.stateFile ?? process.env.CLAUDE_STATE_FILE ?? DEFAULT_STATE_FILE;
833
- }
834
- // ---------------------------------------------------------------------------
835
- // State management
836
- // ---------------------------------------------------------------------------
837
- loadState() {
838
- if (!(0, import_fs3.existsSync)(this.stateFile)) {
839
- return {
840
- version: STATE_VERSION,
841
- lastRunAt: (/* @__PURE__ */ new Date(0)).toISOString(),
842
- projectPath: this.projectPath,
843
- processed: {}
844
- };
845
- }
846
- try {
847
- return JSON.parse((0, import_fs3.readFileSync)(this.stateFile, "utf-8"));
848
- } catch {
849
- logger2.warn("Could not parse state file, starting fresh", {
850
- stateFile: this.stateFile
851
- });
852
- return {
853
- version: STATE_VERSION,
854
- lastRunAt: (/* @__PURE__ */ new Date(0)).toISOString(),
855
- projectPath: this.projectPath,
856
- processed: {}
857
- };
858
- }
859
- }
860
- saveState(state) {
861
- (0, import_fs3.writeFileSync)(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
862
- logger2.debug("State saved", { stateFile: this.stateFile });
863
- }
864
- // ---------------------------------------------------------------------------
865
- // Core scan logic
866
- // ---------------------------------------------------------------------------
867
- /**
868
- * Scan for new or changed sessions relative to the persisted state.
869
- * Returns only sessions that need processing (new or mtime changed).
870
- */
871
- async scan(options) {
872
- const sessionsDir = ClaudeSessionReader.getProjectSessionsDir(
873
- this.projectPath
874
- );
875
- if (!(0, import_fs3.existsSync)(sessionsDir)) {
876
- logger2.warn("No Claude sessions directory found for project", {
877
- projectPath: this.projectPath,
878
- sessionsDir
879
- });
880
- return {
881
- newSessions: [],
882
- updatedSessions: [],
883
- unchangedCount: 0,
884
- totalScanned: 0
885
- };
886
- }
887
- logger2.info("Scanning for Claude sessions", { sessionsDir });
888
- return this.scanDirs([sessionsDir], options);
889
- }
890
- /**
891
- * Scan ALL project directories under ~/.claude/projects/ regardless of project path.
892
- * Returns combined results across every project.
893
- */
894
- async scanAll(options) {
895
- const dirs = this.reader.listProjectDirs();
896
- logger2.info("Scanning all Claude project directories", {
897
- count: dirs.length
898
- });
899
- return this.scanDirs(dirs, options);
900
- }
901
- async scanDirs(sessionsDirs, options) {
902
- const state = this.loadState();
903
- const newSessions = [];
904
- const updatedSessions = [];
905
- let unchangedCount = 0;
906
- let totalScanned = 0;
907
- for (const sessionsDir of sessionsDirs) {
908
- const allSessions = this.reader.readAllSessions(sessionsDir);
909
- totalScanned += allSessions.length;
910
- for (const session of allSessions) {
911
- if (session.turns.length < 2) {
912
- unchangedCount++;
913
- continue;
914
- }
915
- const record2 = state.processed[session.sessionId];
916
- if (options?.forceAll) {
917
- newSessions.push(session);
918
- continue;
919
- }
920
- if (!record2) {
921
- newSessions.push(session);
922
- } else if (session.fileMtime > record2.fileMtime) {
923
- updatedSessions.push(session);
924
- } else {
925
- unchangedCount++;
926
- }
927
- }
928
- }
929
- logger2.info("Scan complete", {
930
- dirs: sessionsDirs.length,
931
- total: totalScanned,
932
- new: newSessions.length,
933
- updated: updatedSessions.length,
934
- unchanged: unchangedCount
935
- });
936
- return { newSessions, updatedSessions, unchangedCount, totalScanned };
937
- }
938
- /**
939
- * Mark a session as processed and persist state.
940
- */
941
- markProcessed(session, status, extras) {
942
- const state = this.loadState();
943
- state.processed[session.sessionId] = {
944
- sessionId: session.sessionId,
945
- filePath: session.filePath,
946
- fileMtime: session.fileMtime,
947
- processedAt: (/* @__PURE__ */ new Date()).toISOString(),
948
- status,
949
- ...extras
950
- };
951
- state.lastRunAt = (/* @__PURE__ */ new Date()).toISOString();
952
- this.saveState(state);
953
- }
954
- /**
955
- * Return persisted processing records (useful for reporting).
956
- */
957
- getState() {
958
- return this.loadState();
959
- }
960
- /**
961
- * Reset state for a specific session (forces re-processing on next scan).
962
- */
963
- resetSession(sessionId) {
964
- const state = this.loadState();
965
- delete state.processed[sessionId];
966
- this.saveState(state);
967
- logger2.info("Reset session state", { sessionId });
968
- }
969
- };
970
- if (require.main === module) {
971
- main().catch((err) => {
972
- logger2.error("Ingester failed", err);
973
- process.exit(1);
974
- });
975
- }
976
- }
977
- });
978
-
979
518
  // ../service-capture/claude-sessions/collector.ts
980
519
  var collector_exports = {};
981
520
  __export(collector_exports, {
@@ -990,20 +529,76 @@ function edgeScrub(content) {
990
529
  }
991
530
  function resolveToken() {
992
531
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
993
- if ((0, import_fs4.existsSync)(CREDENTIALS_FILE2)) {
532
+ if ((0, import_fs.existsSync)(CREDENTIALS_FILE2)) {
994
533
  try {
995
- const creds = JSON.parse((0, import_fs4.readFileSync)(CREDENTIALS_FILE2, "utf-8"));
534
+ const creds = JSON.parse((0, import_fs.readFileSync)(CREDENTIALS_FILE2, "utf-8"));
996
535
  if (creds.token) return creds.token;
997
536
  } catch {
998
- logger3.warn("Could not parse credentials file", {
537
+ logger.warn("Could not parse credentials file", {
999
538
  file: CREDENTIALS_FILE2
1000
539
  });
1001
540
  }
1002
541
  }
1003
542
  return null;
1004
543
  }
1005
- async function uploadSession(sessionId, jsonlContent, apiUrl, token) {
1006
- const body = (0, import_zlib.gzipSync)(Buffer.from(jsonlContent, "utf-8"));
544
+ function discoverSessions() {
545
+ if (!(0, import_fs.existsSync)(CLAUDE_PROJECTS_DIR)) return [];
546
+ const sessions = [];
547
+ const projectDirs = (0, import_fs.readdirSync)(CLAUDE_PROJECTS_DIR).map((name) => (0, import_path.join)(CLAUDE_PROJECTS_DIR, name)).filter((p) => {
548
+ try {
549
+ return (0, import_fs.statSync)(p).isDirectory();
550
+ } catch {
551
+ return false;
552
+ }
553
+ });
554
+ for (const dir of projectDirs) {
555
+ const files = (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl"));
556
+ for (const file2 of files) {
557
+ const filePath = (0, import_path.join)(dir, file2);
558
+ const sessionId = (0, import_path.basename)(file2, ".jsonl");
559
+ try {
560
+ const stat = (0, import_fs.statSync)(filePath);
561
+ const content = (0, import_fs.readFileSync)(filePath, "utf-8");
562
+ const lineCount = content.split("\n").filter((l) => l.trim()).length;
563
+ if (lineCount >= MIN_LINES) {
564
+ sessions.push({
565
+ sessionId,
566
+ filePath,
567
+ lineCount,
568
+ sizeBytes: stat.size
569
+ });
570
+ }
571
+ } catch {
572
+ }
573
+ }
574
+ }
575
+ return sessions;
576
+ }
577
+ async function apiGet(path, apiUrl, token) {
578
+ const response = await fetch(`${apiUrl}/api/v1${path}`, {
579
+ headers: { Authorization: `Bearer ${token}` }
580
+ });
581
+ if (!response.ok) {
582
+ const text = await response.text().catch(() => "");
583
+ throw new Error(`API ${response.status}: ${text.slice(0, 200)}`);
584
+ }
585
+ return response.json();
586
+ }
587
+ async function getUploadedSessions(apiUrl, token) {
588
+ try {
589
+ const data = await apiGet("/sessions/uploaded", apiUrl, token);
590
+ const map2 = /* @__PURE__ */ new Map();
591
+ for (const s of data.sessions) {
592
+ map2.set(s.sessionId, s.lineCount);
593
+ }
594
+ return map2;
595
+ } catch {
596
+ logger.warn("Could not fetch uploaded sessions list, will upload all");
597
+ return /* @__PURE__ */ new Map();
598
+ }
599
+ }
600
+ async function uploadSession(sessionId, content, apiUrl, token) {
601
+ const body = (0, import_zlib.gzipSync)(Buffer.from(content, "utf-8"));
1007
602
  const response = await fetch(`${apiUrl}/api/v1/sessions/raw`, {
1008
603
  method: "POST",
1009
604
  headers: {
@@ -1012,7 +607,7 @@ async function uploadSession(sessionId, jsonlContent, apiUrl, token) {
1012
607
  "Content-Encoding": "gzip",
1013
608
  "x-session-id": sessionId
1014
609
  },
1015
- body
610
+ body: new Uint8Array(body)
1016
611
  });
1017
612
  if (!response.ok) {
1018
613
  const text = await response.text().catch(() => "");
@@ -1029,61 +624,61 @@ async function collect(options) {
1029
624
  "HAANSI_TOKEN is required. Set it as env var or in ~/.haansi/credentials.json"
1030
625
  );
1031
626
  }
1032
- const ingester = new ClaudeSessionIngester({
1033
- projectPath: options.projectPath
1034
- });
1035
- 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
- });
627
+ const localSessions = discoverSessions();
628
+ logger.info(`Found ${localSessions.length} session files on disk`);
629
+ if (localSessions.length === 0) {
630
+ return { uploaded: 0, skipped: 0, errors: 0 };
631
+ }
1046
632
  let uploaded = 0;
633
+ let skipped = 0;
1047
634
  let errors = 0;
1048
- for (const session of sessions) {
635
+ const remoteMap = options.forceAll ? /* @__PURE__ */ new Map() : await getUploadedSessions(apiUrl, token);
636
+ const toUpload = [];
637
+ for (const session of localSessions) {
638
+ const remoteLine = remoteMap.get(session.sessionId);
639
+ if (remoteLine !== void 0 && remoteLine >= session.lineCount) {
640
+ skipped++;
641
+ } else {
642
+ toUpload.push(session);
643
+ }
644
+ }
645
+ const batch = options.limit ? toUpload.slice(0, options.limit) : toUpload;
646
+ skipped += toUpload.length - batch.length;
647
+ logger.info(
648
+ `Uploading ${batch.length} session(s), skipping ${skipped} unchanged`
649
+ );
650
+ for (const session of batch) {
1049
651
  const shortId = session.sessionId.slice(0, 8);
1050
652
  try {
1051
- const rawContent = (0, import_fs4.readFileSync)(session.filePath, "utf-8");
653
+ const rawContent = (0, import_fs.readFileSync)(session.filePath, "utf-8");
1052
654
  const scrubbed = edgeScrub(rawContent);
1053
655
  if (options.dryRun) {
1054
- logger3.info(
1055
- `[dry-run] Would upload session ${shortId} (${rawContent.length} bytes)`
656
+ logger.info(
657
+ `[dry-run] Would upload ${shortId} (${session.lineCount} lines)`
1056
658
  );
1057
659
  uploaded++;
1058
660
  continue;
1059
661
  }
1060
662
  await uploadSession(session.sessionId, scrubbed, apiUrl, token);
1061
- ingester.markProcessed(session, "pending", {});
1062
663
  uploaded++;
1063
- logger3.info(`Uploaded session ${shortId}`);
664
+ logger.info(`Uploaded ${shortId} (${session.lineCount} lines)`);
1064
665
  } catch (err) {
1065
666
  errors++;
1066
- logger3.error(`Failed to upload session ${shortId}`, { err: err.message });
667
+ logger.error(`Failed to upload ${shortId}`, { err: err.message });
1067
668
  }
1068
669
  }
1069
- return { uploaded, skipped: scanResult.unchangedCount, errors };
670
+ return { uploaded, skipped, errors };
1070
671
  }
1071
- async function main2() {
672
+ async function main() {
1072
673
  const forceAll = process.argv.includes("--force-all");
1073
674
  const dryRun = process.argv.includes("--dry-run");
1074
- const projectPath = process.env.CLAUDE_PROJECT_PATH || void 0;
1075
675
  const limitIdx = process.argv.indexOf("--limit");
1076
676
  const limit = limitIdx !== -1 ? parseInt(process.argv[limitIdx + 1], 10) : void 0;
1077
- logger3.info("Collector starting", {
1078
- forceAll,
1079
- dryRun,
1080
- projectPath: projectPath ?? "all"
1081
- });
677
+ logger.info("Collector starting", { forceAll, dryRun });
1082
678
  const { uploaded, skipped, errors } = await collect({
1083
679
  forceAll,
1084
680
  dryRun,
1085
- limit,
1086
- projectPath
681
+ limit
1087
682
  });
1088
683
  console.log(
1089
684
  `
@@ -1091,21 +686,22 @@ Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
1091
686
  );
1092
687
  if (errors > 0) process.exit(1);
1093
688
  }
1094
- var import_dotenv2, import_path3, import_fs4, import_os3, import_zlib, logger3, DEFAULT_API_URL2, CREDENTIALS_FILE2, EDGE_SCRUB_PATTERNS;
689
+ var import_dotenv, import_path, import_fs, import_os, import_zlib, logger, DEFAULT_API_URL2, CREDENTIALS_FILE2, CLAUDE_PROJECTS_DIR, MIN_LINES, EDGE_SCRUB_PATTERNS;
1095
690
  var init_collector = __esm({
1096
691
  "../service-capture/claude-sessions/collector.ts"() {
1097
692
  "use strict";
1098
- import_dotenv2 = __toESM(require_main());
1099
- import_path3 = require("path");
1100
- import_fs4 = require("fs");
1101
- import_os3 = require("os");
693
+ import_dotenv = __toESM(require_main());
694
+ import_path = require("path");
695
+ import_fs = require("fs");
696
+ import_os = require("os");
1102
697
  import_zlib = require("zlib");
1103
- init_ingester();
1104
698
  init_logger();
1105
- (0, import_dotenv2.config)({ path: (0, import_path3.resolve)(__dirname, "../../../.env") });
1106
- logger3 = createLogger("claude-collector");
699
+ (0, import_dotenv.config)({ path: (0, import_path.resolve)(__dirname, "../../../.env") });
700
+ logger = createLogger("claude-collector");
1107
701
  DEFAULT_API_URL2 = "https://api.haansi.co";
1108
- CREDENTIALS_FILE2 = (0, import_path3.join)((0, import_os3.homedir)(), ".haansi", "credentials.json");
702
+ CREDENTIALS_FILE2 = (0, import_path.join)((0, import_os.homedir)(), ".haansi", "credentials.json");
703
+ CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
704
+ MIN_LINES = 4;
1109
705
  EDGE_SCRUB_PATTERNS = [
1110
706
  {
1111
707
  pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
@@ -1146,8 +742,8 @@ var init_collector = __esm({
1146
742
  }
1147
743
  ];
1148
744
  if (require.main === module) {
1149
- main2().catch((err) => {
1150
- logger3.error("Collector failed", err);
745
+ main().catch((err) => {
746
+ logger.error("Collector failed", err);
1151
747
  process.exit(1);
1152
748
  });
1153
749
  }
@@ -2068,49 +1664,49 @@ var require_node_cron = __commonJS({
2068
1664
  var collector_daemon_exports = {};
2069
1665
  async function run() {
2070
1666
  const projectPath = process.env.CLAUDE_PROJECT_PATH || void 0;
2071
- logger4.info("Collect run starting", { projectPath: projectPath ?? "all" });
1667
+ logger2.info("Collect run starting", { projectPath: projectPath ?? "all" });
2072
1668
  const start = Date.now();
2073
1669
  try {
2074
1670
  const { uploaded, skipped, errors } = await collect({ projectPath });
2075
1671
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
2076
- logger4.info(`Collect run complete in ${elapsed}s`, {
1672
+ logger2.info(`Collect run complete in ${elapsed}s`, {
2077
1673
  uploaded,
2078
1674
  skipped,
2079
1675
  errors
2080
1676
  });
2081
1677
  } catch (err) {
2082
- logger4.error("Collect run failed", { err: err.message });
1678
+ logger2.error("Collect run failed", { err: err.message });
2083
1679
  }
2084
1680
  }
2085
- async function main3() {
2086
- logger4.info("Collector daemon starting", { schedule: SCHEDULE });
1681
+ async function main2() {
1682
+ logger2.info("Collector daemon starting", { schedule: SCHEDULE });
2087
1683
  await run();
2088
1684
  import_node_cron.default.schedule(SCHEDULE, async () => {
2089
- logger4.info("Scheduled collect run triggered");
1685
+ logger2.info("Scheduled collect run triggered");
2090
1686
  await run();
2091
1687
  });
2092
- logger4.info(`Daemon running. Next run scheduled per cron: "${SCHEDULE}"`);
1688
+ logger2.info(`Daemon running. Next run scheduled per cron: "${SCHEDULE}"`);
2093
1689
  const shutdown = async (signal) => {
2094
- logger4.info(`Received ${signal}, shutting down`);
1690
+ logger2.info(`Received ${signal}, shutting down`);
2095
1691
  process.exit(0);
2096
1692
  };
2097
1693
  process.on("SIGTERM", () => shutdown("SIGTERM"));
2098
1694
  process.on("SIGINT", () => shutdown("SIGINT"));
2099
1695
  }
2100
- var import_dotenv3, import_path4, import_node_cron, logger4, SCHEDULE;
1696
+ var import_dotenv2, import_path2, import_node_cron, logger2, SCHEDULE;
2101
1697
  var init_collector_daemon = __esm({
2102
1698
  "../service-capture/claude-sessions/collector-daemon.ts"() {
2103
1699
  "use strict";
2104
- import_dotenv3 = __toESM(require_main());
2105
- import_path4 = require("path");
1700
+ import_dotenv2 = __toESM(require_main());
1701
+ import_path2 = require("path");
2106
1702
  import_node_cron = __toESM(require_node_cron());
2107
1703
  init_collector();
2108
1704
  init_logger();
2109
- (0, import_dotenv3.config)({ path: (0, import_path4.resolve)(__dirname, "../../../.env") });
2110
- logger4 = createLogger("collector-daemon");
1705
+ (0, import_dotenv2.config)({ path: (0, import_path2.resolve)(__dirname, "../../../.env") });
1706
+ logger2 = createLogger("collector-daemon");
2111
1707
  SCHEDULE = process.env.CLAUDE_COLLECT_SCHEDULE ?? "*/30 * * * *";
2112
- main3().catch((err) => {
2113
- logger4.error("Daemon failed to start", err);
1708
+ main2().catch((err) => {
1709
+ logger2.error("Daemon failed to start", err);
2114
1710
  process.exit(1);
2115
1711
  });
2116
1712
  }
@@ -6113,7 +5709,7 @@ function $constructor(name, initializer3, params) {
6113
5709
  Object.defineProperty(_, "name", { value: name });
6114
5710
  return _;
6115
5711
  }
6116
- function config4(newConfig) {
5712
+ function config3(newConfig) {
6117
5713
  if (newConfig)
6118
5714
  Object.assign(globalConfig, newConfig);
6119
5715
  return globalConfig;
@@ -6638,10 +6234,10 @@ function prefixIssues(path, issues) {
6638
6234
  function unwrapMessage(message) {
6639
6235
  return typeof message === "string" ? message : message?.message;
6640
6236
  }
6641
- function finalizeIssue(iss, ctx, config6) {
6237
+ function finalizeIssue(iss, ctx, config5) {
6642
6238
  const full = { ...iss, path: iss.path ?? [] };
6643
6239
  if (!iss.message) {
6644
- const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config6.customError?.(iss)) ?? unwrapMessage(config6.localeError?.(iss)) ?? "Invalid input";
6240
+ const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config5.customError?.(iss)) ?? unwrapMessage(config5.localeError?.(iss)) ?? "Invalid input";
6645
6241
  full.message = message;
6646
6242
  }
6647
6243
  delete full.inst;
@@ -6911,7 +6507,7 @@ var init_parse2 = __esm({
6911
6507
  throw new $ZodAsyncError();
6912
6508
  }
6913
6509
  if (result.issues.length) {
6914
- const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config4())));
6510
+ const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config3())));
6915
6511
  captureStackTrace(e, _params?.callee);
6916
6512
  throw e;
6917
6513
  }
@@ -6924,7 +6520,7 @@ var init_parse2 = __esm({
6924
6520
  if (result instanceof Promise)
6925
6521
  result = await result;
6926
6522
  if (result.issues.length) {
6927
- const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config4())));
6523
+ const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config3())));
6928
6524
  captureStackTrace(e, params?.callee);
6929
6525
  throw e;
6930
6526
  }
@@ -6939,7 +6535,7 @@ var init_parse2 = __esm({
6939
6535
  }
6940
6536
  return result.issues.length ? {
6941
6537
  success: false,
6942
- error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config4())))
6538
+ error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config3())))
6943
6539
  } : { success: true, data: result.value };
6944
6540
  };
6945
6541
  safeParse = /* @__PURE__ */ _safeParse($ZodRealError);
@@ -6950,7 +6546,7 @@ var init_parse2 = __esm({
6950
6546
  result = await result;
6951
6547
  return result.issues.length ? {
6952
6548
  success: false,
6953
- error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config4())))
6549
+ error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config3())))
6954
6550
  } : { success: true, data: result.value };
6955
6551
  };
6956
6552
  safeParseAsync = /* @__PURE__ */ _safeParseAsync($ZodRealError);
@@ -7884,7 +7480,7 @@ function handleUnionResults(results, final, inst, ctx) {
7884
7480
  code: "invalid_union",
7885
7481
  input: final.value,
7886
7482
  inst,
7887
- errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config4())))
7483
+ errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config3())))
7888
7484
  });
7889
7485
  return final;
7890
7486
  }
@@ -7899,7 +7495,7 @@ function handleExclusiveUnionResults(results, final, inst, ctx) {
7899
7495
  code: "invalid_union",
7900
7496
  input: final.value,
7901
7497
  inst,
7902
- errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config4())))
7498
+ errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config3())))
7903
7499
  });
7904
7500
  } else {
7905
7501
  final.issues.push({
@@ -8011,7 +7607,7 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
8011
7607
  origin: "map",
8012
7608
  input,
8013
7609
  inst,
8014
- issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config4()))
7610
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config3()))
8015
7611
  });
8016
7612
  }
8017
7613
  }
@@ -8025,7 +7621,7 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
8025
7621
  input,
8026
7622
  inst,
8027
7623
  key,
8028
- issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config4()))
7624
+ issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config3()))
8029
7625
  });
8030
7626
  }
8031
7627
  }
@@ -9187,7 +8783,7 @@ var init_schemas = __esm({
9187
8783
  payload.issues.push({
9188
8784
  code: "invalid_key",
9189
8785
  origin: "record",
9190
- issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config4())),
8786
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config3())),
9191
8787
  input: key,
9192
8788
  path: [key],
9193
8789
  inst
@@ -9483,7 +9079,7 @@ var init_schemas = __esm({
9483
9079
  payload.value = def.catchValue({
9484
9080
  ...payload,
9485
9081
  error: {
9486
- issues: result2.issues.map((iss) => finalizeIssue(iss, ctx, config4()))
9082
+ issues: result2.issues.map((iss) => finalizeIssue(iss, ctx, config3()))
9487
9083
  },
9488
9084
  input: payload.value
9489
9085
  });
@@ -9497,7 +9093,7 @@ var init_schemas = __esm({
9497
9093
  payload.value = def.catchValue({
9498
9094
  ...payload,
9499
9095
  error: {
9500
- issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config4()))
9096
+ issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config3()))
9501
9097
  },
9502
9098
  input: payload.value
9503
9099
  });
@@ -13795,7 +13391,7 @@ var init_external3 = __esm({
13795
13391
  init_iso2();
13796
13392
  init_iso2();
13797
13393
  init_coerce2();
13798
- config4(en_default2());
13394
+ config3(en_default2());
13799
13395
  }
13800
13396
  });
13801
13397
 
@@ -16822,13 +16418,13 @@ var init_zodToJsonSchema = __esm({
16822
16418
  }, true) ?? parseAnyDef(refs)
16823
16419
  }), {}) : void 0;
16824
16420
  const name = typeof options === "string" ? options : options?.nameStrategy === "title" ? void 0 : options?.name;
16825
- const main5 = parseDef(schema._def, name === void 0 ? refs : {
16421
+ const main4 = parseDef(schema._def, name === void 0 ? refs : {
16826
16422
  ...refs,
16827
16423
  currentPath: [...refs.basePath, refs.definitionPath, name]
16828
16424
  }, false) ?? parseAnyDef(refs);
16829
16425
  const title = typeof options === "object" && options.name !== void 0 && options.nameStrategy === "title" ? options.name : void 0;
16830
16426
  if (title !== void 0) {
16831
- main5.title = title;
16427
+ main4.title = title;
16832
16428
  }
16833
16429
  if (refs.flags.hasReferencedOpenAiAnyType) {
16834
16430
  if (!definitions) {
@@ -16849,9 +16445,9 @@ var init_zodToJsonSchema = __esm({
16849
16445
  }
16850
16446
  }
16851
16447
  const combined = name === void 0 ? definitions ? {
16852
- ...main5,
16448
+ ...main4,
16853
16449
  [refs.definitionPath]: definitions
16854
- } : main5 : {
16450
+ } : main4 : {
16855
16451
  $ref: [
16856
16452
  ...refs.$refStrategy === "relative" ? [] : refs.basePath,
16857
16453
  refs.definitionPath,
@@ -16859,7 +16455,7 @@ var init_zodToJsonSchema = __esm({
16859
16455
  ].join("/"),
16860
16456
  [refs.definitionPath]: {
16861
16457
  ...definitions,
16862
- [name]: main5
16458
+ [name]: main4
16863
16459
  }
16864
16460
  };
16865
16461
  if (refs.target === "jsonSchema7") {
@@ -17489,7 +17085,7 @@ var init_protocol = __esm({
17489
17085
  return;
17490
17086
  }
17491
17087
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
17492
- await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
17088
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
17493
17089
  options?.signal?.throwIfAborted();
17494
17090
  }
17495
17091
  } catch (error2) {
@@ -17506,7 +17102,7 @@ var init_protocol = __esm({
17506
17102
  */
17507
17103
  request(request, resultSchema, options) {
17508
17104
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
17509
- return new Promise((resolve5, reject) => {
17105
+ return new Promise((resolve4, reject) => {
17510
17106
  const earlyReject = (error2) => {
17511
17107
  reject(error2);
17512
17108
  };
@@ -17584,7 +17180,7 @@ var init_protocol = __esm({
17584
17180
  if (!parseResult.success) {
17585
17181
  reject(parseResult.error);
17586
17182
  } else {
17587
- resolve5(parseResult.data);
17183
+ resolve4(parseResult.data);
17588
17184
  }
17589
17185
  } catch (error2) {
17590
17186
  reject(error2);
@@ -17845,12 +17441,12 @@ var init_protocol = __esm({
17845
17441
  }
17846
17442
  } catch {
17847
17443
  }
17848
- return new Promise((resolve5, reject) => {
17444
+ return new Promise((resolve4, reject) => {
17849
17445
  if (signal.aborted) {
17850
17446
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
17851
17447
  return;
17852
17448
  }
17853
- const timeoutId = setTimeout(resolve5, interval);
17449
+ const timeoutId = setTimeout(resolve4, interval);
17854
17450
  signal.addEventListener("abort", () => {
17855
17451
  clearTimeout(timeoutId);
17856
17452
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -20877,7 +20473,7 @@ var require_compile = __commonJS({
20877
20473
  const schOrFunc = root.refs[ref];
20878
20474
  if (schOrFunc)
20879
20475
  return schOrFunc;
20880
- let _sch = resolve5.call(this, root, ref);
20476
+ let _sch = resolve4.call(this, root, ref);
20881
20477
  if (_sch === void 0) {
20882
20478
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
20883
20479
  const { schemaId } = this.opts;
@@ -20904,7 +20500,7 @@ var require_compile = __commonJS({
20904
20500
  function sameSchemaEnv(s1, s2) {
20905
20501
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
20906
20502
  }
20907
- function resolve5(root, ref) {
20503
+ function resolve4(root, ref) {
20908
20504
  let sch;
20909
20505
  while (typeof (sch = this.refs[ref]) == "string")
20910
20506
  ref = sch;
@@ -21479,7 +21075,7 @@ var require_fast_uri = __commonJS({
21479
21075
  }
21480
21076
  return uri;
21481
21077
  }
21482
- function resolve5(baseURI, relativeURI, options) {
21078
+ function resolve4(baseURI, relativeURI, options) {
21483
21079
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
21484
21080
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
21485
21081
  schemelessOptions.skipEscape = true;
@@ -21706,7 +21302,7 @@ var require_fast_uri = __commonJS({
21706
21302
  var fastUri = {
21707
21303
  SCHEMES,
21708
21304
  normalize,
21709
- resolve: resolve5,
21305
+ resolve: resolve4,
21710
21306
  resolveComponent,
21711
21307
  equal,
21712
21308
  serialize,
@@ -22261,13 +21857,13 @@ var require_core = __commonJS({
22261
21857
  }, warn() {
22262
21858
  }, error() {
22263
21859
  } };
22264
- function getLogger(logger5) {
22265
- if (logger5 === false)
21860
+ function getLogger(logger3) {
21861
+ if (logger3 === false)
22266
21862
  return noLogs;
22267
- if (logger5 === void 0)
21863
+ if (logger3 === void 0)
22268
21864
  return console;
22269
- if (logger5.log && logger5.warn && logger5.error)
22270
- return logger5;
21865
+ if (logger3.log && logger3.warn && logger3.error)
21866
+ return logger3;
22271
21867
  throw new Error("logger must implement log, warn and error methods");
22272
21868
  }
22273
21869
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
@@ -27497,7 +27093,7 @@ var require_compile2 = __commonJS({
27497
27093
  const schOrFunc = root.refs[ref];
27498
27094
  if (schOrFunc)
27499
27095
  return schOrFunc;
27500
- let _sch = resolve5.call(this, root, ref);
27096
+ let _sch = resolve4.call(this, root, ref);
27501
27097
  if (_sch === void 0) {
27502
27098
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
27503
27099
  const { schemaId } = this.opts;
@@ -27524,7 +27120,7 @@ var require_compile2 = __commonJS({
27524
27120
  function sameSchemaEnv(s1, s2) {
27525
27121
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
27526
27122
  }
27527
- function resolve5(root, ref) {
27123
+ function resolve4(root, ref) {
27528
27124
  let sch;
27529
27125
  while (typeof (sch = this.refs[ref]) == "string")
27530
27126
  ref = sch;
@@ -28159,13 +27755,13 @@ var require_core3 = __commonJS({
28159
27755
  }, warn() {
28160
27756
  }, error() {
28161
27757
  } };
28162
- function getLogger(logger5) {
28163
- if (logger5 === false)
27758
+ function getLogger(logger3) {
27759
+ if (logger3 === false)
28164
27760
  return noLogs;
28165
- if (logger5 === void 0)
27761
+ if (logger3 === void 0)
28166
27762
  return console;
28167
- if (logger5.log && logger5.warn && logger5.error)
28168
- return logger5;
27763
+ if (logger3.log && logger3.warn && logger3.error)
27764
+ return logger3;
28169
27765
  throw new Error("logger must implement log, warn and error methods");
28170
27766
  }
28171
27767
  var KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i;
@@ -31056,13 +30652,13 @@ var init_mcp_server = __esm({
31056
30652
  constructor(_mcpServer) {
31057
30653
  this._mcpServer = _mcpServer;
31058
30654
  }
31059
- registerToolTask(name, config6, handler) {
31060
- const execution = { taskSupport: "required", ...config6.execution };
30655
+ registerToolTask(name, config5, handler) {
30656
+ const execution = { taskSupport: "required", ...config5.execution };
31061
30657
  if (execution.taskSupport === "forbidden") {
31062
30658
  throw new Error(`Cannot register task-based tool '${name}' with taskSupport 'forbidden'. Use registerTool() instead.`);
31063
30659
  }
31064
30660
  const mcpServerInternal = this._mcpServer;
31065
- return mcpServerInternal._createRegisteredTool(name, config6.title, config6.description, config6.inputSchema, config6.outputSchema, config6.annotations, execution, config6._meta, handler);
30661
+ return mcpServerInternal._createRegisteredTool(name, config5.title, config5.description, config5.inputSchema, config5.outputSchema, config5.annotations, execution, config5._meta, handler);
31066
30662
  }
31067
30663
  };
31068
30664
  }
@@ -31374,7 +30970,7 @@ var init_mcp = __esm({
31374
30970
  let task = createTaskResult.task;
31375
30971
  const pollInterval = task.pollInterval ?? 5e3;
31376
30972
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
31377
- await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
30973
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
31378
30974
  const updatedTask = await extra.taskStore.getTask(taskId);
31379
30975
  if (!updatedTask) {
31380
30976
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -31575,12 +31171,12 @@ var init_mcp = __esm({
31575
31171
  return registeredResourceTemplate;
31576
31172
  }
31577
31173
  }
31578
- registerResource(name, uriOrTemplate, config6, readCallback) {
31174
+ registerResource(name, uriOrTemplate, config5, readCallback) {
31579
31175
  if (typeof uriOrTemplate === "string") {
31580
31176
  if (this._registeredResources[uriOrTemplate]) {
31581
31177
  throw new Error(`Resource ${uriOrTemplate} is already registered`);
31582
31178
  }
31583
- const registeredResource = this._createRegisteredResource(name, config6.title, uriOrTemplate, config6, readCallback);
31179
+ const registeredResource = this._createRegisteredResource(name, config5.title, uriOrTemplate, config5, readCallback);
31584
31180
  this.setResourceRequestHandlers();
31585
31181
  this.sendResourceListChanged();
31586
31182
  return registeredResource;
@@ -31588,7 +31184,7 @@ var init_mcp = __esm({
31588
31184
  if (this._registeredResourceTemplates[name]) {
31589
31185
  throw new Error(`Resource template ${name} is already registered`);
31590
31186
  }
31591
- const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config6.title, uriOrTemplate, config6, readCallback);
31187
+ const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config5.title, uriOrTemplate, config5, readCallback);
31592
31188
  this.setResourceRequestHandlers();
31593
31189
  this.sendResourceListChanged();
31594
31190
  return registeredResourceTemplate;
@@ -31783,11 +31379,11 @@ var init_mcp = __esm({
31783
31379
  /**
31784
31380
  * Registers a tool with a config object and callback.
31785
31381
  */
31786
- registerTool(name, config6, cb) {
31382
+ registerTool(name, config5, cb) {
31787
31383
  if (this._registeredTools[name]) {
31788
31384
  throw new Error(`Tool ${name} is already registered`);
31789
31385
  }
31790
- const { title, description, inputSchema, outputSchema, annotations, _meta } = config6;
31386
+ const { title, description, inputSchema, outputSchema, annotations, _meta } = config5;
31791
31387
  return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, _meta, cb);
31792
31388
  }
31793
31389
  prompt(name, ...rest) {
@@ -31811,11 +31407,11 @@ var init_mcp = __esm({
31811
31407
  /**
31812
31408
  * Registers a prompt with a config object and callback.
31813
31409
  */
31814
- registerPrompt(name, config6, cb) {
31410
+ registerPrompt(name, config5, cb) {
31815
31411
  if (this._registeredPrompts[name]) {
31816
31412
  throw new Error(`Prompt ${name} is already registered`);
31817
31413
  }
31818
- const { title, description, argsSchema } = config6;
31414
+ const { title, description, argsSchema } = config5;
31819
31415
  const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb);
31820
31416
  this.setPromptRequestHandlers();
31821
31417
  this.sendPromptListChanged();
@@ -31965,12 +31561,12 @@ var init_stdio2 = __esm({
31965
31561
  this.onclose?.();
31966
31562
  }
31967
31563
  send(message) {
31968
- return new Promise((resolve5) => {
31564
+ return new Promise((resolve4) => {
31969
31565
  const json2 = serializeMessage(message);
31970
31566
  if (this._stdout.write(json2)) {
31971
- resolve5();
31567
+ resolve4();
31972
31568
  } else {
31973
- this._stdout.once("drain", resolve5);
31569
+ this._stdout.once("drain", resolve4);
31974
31570
  }
31975
31571
  });
31976
31572
  }
@@ -31982,16 +31578,35 @@ var init_stdio2 = __esm({
31982
31578
  var mcp_server_exports = {};
31983
31579
  function resolveToken2() {
31984
31580
  if (process.env.HAANSI_TOKEN) return process.env.HAANSI_TOKEN;
31985
- if ((0, import_fs5.existsSync)(CREDENTIALS_FILE3)) {
31581
+ if ((0, import_fs2.existsSync)(CREDENTIALS_FILE3)) {
31986
31582
  try {
31987
- const creds = JSON.parse((0, import_fs5.readFileSync)(CREDENTIALS_FILE3, "utf-8"));
31583
+ const creds = JSON.parse((0, import_fs2.readFileSync)(CREDENTIALS_FILE3, "utf-8"));
31988
31584
  if (creds.token) return creds.token;
31989
31585
  } catch {
31990
31586
  }
31991
31587
  }
31992
31588
  return null;
31993
31589
  }
31994
- async function apiGet(path) {
31590
+ function triggerCollectInBackground() {
31591
+ if (isCollecting) return;
31592
+ isCollecting = true;
31593
+ collect({}).then(({ uploaded, skipped, errors }) => {
31594
+ console.error(
31595
+ `[mcp-server] Collection done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
31596
+ );
31597
+ lastCollectTime = Date.now();
31598
+ }).catch((err) => {
31599
+ console.error("[mcp-server] Collection error:", err.message);
31600
+ }).finally(() => {
31601
+ isCollecting = false;
31602
+ });
31603
+ }
31604
+ function maybeCollect() {
31605
+ if (Date.now() - lastCollectTime >= COLLECT_COOLDOWN_MS) {
31606
+ triggerCollectInBackground();
31607
+ }
31608
+ }
31609
+ async function apiGet2(path) {
31995
31610
  const response = await fetch(`${API_URL}/api/v1${path}`, {
31996
31611
  headers: { Authorization: `Bearer ${TOKEN}` }
31997
31612
  });
@@ -32001,10 +31616,11 @@ async function apiGet(path) {
32001
31616
  }
32002
31617
  return response.json();
32003
31618
  }
32004
- async function main4() {
31619
+ async function main3() {
32005
31620
  try {
32006
- await apiGet("/sessions/recent?limit=1");
31621
+ await apiGet2("/sessions/recent?limit=1");
32007
31622
  console.error("[mcp-server] API connection ok");
31623
+ triggerCollectInBackground();
32008
31624
  } catch (err) {
32009
31625
  console.error(
32010
31626
  "[mcp-server] WARNING: API connection check failed:",
@@ -32015,20 +31631,21 @@ async function main4() {
32015
31631
  await mcpServer.connect(transport);
32016
31632
  console.error("[mcp-server] haansi-solutions MCP server running on stdio");
32017
31633
  }
32018
- var import_dotenv4, import_path5, import_fs5, import_os4, DEFAULT_API_URL3, CREDENTIALS_FILE3, API_URL, TOKEN, mcpServer, server;
31634
+ var import_dotenv3, import_path3, import_fs2, import_os2, DEFAULT_API_URL3, CREDENTIALS_FILE3, API_URL, TOKEN, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
32019
31635
  var init_mcp_server2 = __esm({
32020
31636
  "../service-capture/claude-sessions/mcp-server.ts"() {
32021
31637
  "use strict";
32022
- import_dotenv4 = __toESM(require_main());
32023
- import_path5 = require("path");
32024
- import_fs5 = require("fs");
32025
- import_os4 = require("os");
31638
+ import_dotenv3 = __toESM(require_main());
31639
+ import_path3 = require("path");
31640
+ import_fs2 = require("fs");
31641
+ import_os2 = require("os");
31642
+ init_collector();
32026
31643
  init_mcp();
32027
31644
  init_stdio2();
32028
31645
  init_types2();
32029
- (0, import_dotenv4.config)({ path: (0, import_path5.resolve)(__dirname, "../../../.env") });
31646
+ (0, import_dotenv3.config)({ path: (0, import_path3.resolve)(__dirname, "../../../.env") });
32030
31647
  DEFAULT_API_URL3 = "https://api.haansi.co";
32031
- CREDENTIALS_FILE3 = (0, import_path5.join)((0, import_os4.homedir)(), ".haansi", "credentials.json");
31648
+ CREDENTIALS_FILE3 = (0, import_path3.join)((0, import_os2.homedir)(), ".haansi", "credentials.json");
32032
31649
  API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
32033
31650
  TOKEN = resolveToken2();
32034
31651
  if (!TOKEN) {
@@ -32037,6 +31654,9 @@ var init_mcp_server2 = __esm({
32037
31654
  );
32038
31655
  process.exit(1);
32039
31656
  }
31657
+ isCollecting = false;
31658
+ lastCollectTime = 0;
31659
+ COLLECT_COOLDOWN_MS = 5 * 60 * 1e3;
32040
31660
  mcpServer = new McpServer(
32041
31661
  { name: "haansi-solutions", version: "0.2.0" },
32042
31662
  { capabilities: { tools: {} } }
@@ -32079,6 +31699,7 @@ var init_mcp_server2 = __esm({
32079
31699
  ]
32080
31700
  }));
32081
31701
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
31702
+ maybeCollect();
32082
31703
  const { name, arguments: args } = request.params;
32083
31704
  console.error(`[mcp-server] Tool: ${name}`);
32084
31705
  try {
@@ -32086,7 +31707,7 @@ var init_mcp_server2 = __esm({
32086
31707
  case "search_solutions": {
32087
31708
  const { query, top_k = 5 } = args;
32088
31709
  const k = Math.min(Math.max(1, top_k), 20);
32089
- const data = await apiGet(
31710
+ const data = await apiGet2(
32090
31711
  `/sessions/search?q=${encodeURIComponent(query)}&top_k=${k}`
32091
31712
  );
32092
31713
  if (!data.results || data.results.length === 0) {
@@ -32106,7 +31727,7 @@ var init_mcp_server2 = __esm({
32106
31727
  case "list_recent_solutions": {
32107
31728
  const { limit = 10 } = args;
32108
31729
  const n = Math.min(Math.max(1, limit), 50);
32109
- const data = await apiGet(`/sessions/recent?limit=${n}`);
31730
+ const data = await apiGet2(`/sessions/recent?limit=${n}`);
32110
31731
  if (!data.results || data.results.length === 0) {
32111
31732
  return {
32112
31733
  content: [
@@ -32132,7 +31753,7 @@ var init_mcp_server2 = __esm({
32132
31753
  };
32133
31754
  }
32134
31755
  });
32135
- main4().catch((err) => {
31756
+ main3().catch((err) => {
32136
31757
  console.error("[mcp-server] Failed to start:", err);
32137
31758
  process.exit(1);
32138
31759
  });
@@ -32145,24 +31766,28 @@ __export(setup_mcp_exports, {
32145
31766
  setupMcp: () => setupMcp
32146
31767
  });
32147
31768
  async function setupMcp() {
32148
- let config6 = {};
31769
+ let config5 = {};
32149
31770
  if ((0, import_node_fs2.existsSync)(CLAUDE_JSON)) {
32150
31771
  try {
32151
- config6 = JSON.parse((0, import_node_fs2.readFileSync)(CLAUDE_JSON, "utf-8"));
31772
+ config5 = JSON.parse((0, import_node_fs2.readFileSync)(CLAUDE_JSON, "utf-8"));
32152
31773
  } catch {
32153
- console.error(`Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`);
31774
+ console.error(
31775
+ `Warning: Could not parse ${CLAUDE_JSON} \u2014 will overwrite with new config.`
31776
+ );
32154
31777
  }
32155
31778
  }
32156
- if (!config6.mcpServers) {
32157
- config6.mcpServers = {};
31779
+ if (!config5.mcpServers) {
31780
+ config5.mcpServers = {};
32158
31781
  }
32159
- config6.mcpServers["haansi-solutions"] = {
31782
+ config5.mcpServers["haansi-solutions"] = {
32160
31783
  command: "npx",
32161
31784
  args: ["haansi", "mcp-server"]
32162
31785
  };
32163
- (0, import_node_fs2.writeFileSync)(CLAUDE_JSON, JSON.stringify(config6, null, 2) + "\n", "utf-8");
31786
+ (0, import_node_fs2.writeFileSync)(CLAUDE_JSON, JSON.stringify(config5, null, 2) + "\n", "utf-8");
32164
31787
  console.log(`MCP server configured in ${CLAUDE_JSON}`);
32165
- console.log("Restart Claude Code to activate the haansi-solutions MCP server.");
31788
+ console.log(
31789
+ "Restart Claude Code to activate the haansi-solutions MCP server."
31790
+ );
32166
31791
  }
32167
31792
  var import_node_fs2, import_node_path2, import_node_os2, CLAUDE_JSON;
32168
31793
  var init_setup_mcp = __esm({
@@ -32184,7 +31809,9 @@ function findHaansiBin() {
32184
31809
  try {
32185
31810
  return (0, import_node_child_process.execSync)("which haansi", { encoding: "utf-8" }).trim();
32186
31811
  } catch {
32187
- throw new Error("Could not find haansi in PATH. Make sure it is installed globally (npm install -g haansi).");
31812
+ throw new Error(
31813
+ "Could not find haansi in PATH. Make sure it is installed globally (npm install -g haansi)."
31814
+ );
32188
31815
  }
32189
31816
  }
32190
31817
  function buildPlist(haansiPath) {
@@ -32255,7 +31882,12 @@ var init_setup_daemon = __esm({
32255
31882
  import_node_path3 = require("node:path");
32256
31883
  import_node_os3 = require("node:os");
32257
31884
  PLIST_LABEL = "co.haansi.daemon";
32258
- PLIST_PATH = (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
31885
+ PLIST_PATH = (0, import_node_path3.join)(
31886
+ (0, import_node_os3.homedir)(),
31887
+ "Library",
31888
+ "LaunchAgents",
31889
+ `${PLIST_LABEL}.plist`
31890
+ );
32259
31891
  LOG_FILE = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".haansi", "daemon.log");
32260
31892
  }
32261
31893
  });
@@ -32276,7 +31908,7 @@ function resolveToken3() {
32276
31908
  }
32277
31909
  throw new Error("No token found. Run `haansi init` first.");
32278
31910
  }
32279
- async function apiGet2(path, token, apiUrl) {
31911
+ async function apiGet3(path, token, apiUrl) {
32280
31912
  const response = await fetch(`${apiUrl}/api/v1${path}`, {
32281
31913
  headers: { Authorization: `Bearer ${token}` }
32282
31914
  });
@@ -32291,13 +31923,17 @@ async function list(options) {
32291
31923
  const token = resolveToken3();
32292
31924
  let data;
32293
31925
  if (options.search) {
32294
- data = await apiGet2(
31926
+ data = await apiGet3(
32295
31927
  `/sessions/search?q=${encodeURIComponent(options.search)}&top_k=${options.limit}`,
32296
31928
  token,
32297
31929
  apiUrl
32298
31930
  );
32299
31931
  } else {
32300
- data = await apiGet2(`/sessions/recent?limit=${options.limit}`, token, apiUrl);
31932
+ data = await apiGet3(
31933
+ `/sessions/recent?limit=${options.limit}`,
31934
+ token,
31935
+ apiUrl
31936
+ );
32301
31937
  }
32302
31938
  if (!data.results || data.results.length === 0) {
32303
31939
  console.log("No solutions found. Run `haansi collect` to upload sessions.");
@@ -32336,9 +31972,16 @@ async function run2() {
32336
31972
  const limitIdx = process.argv.indexOf("--limit");
32337
31973
  const limit = limitIdx !== -1 ? parseInt(process.argv[limitIdx + 1], 10) : void 0;
32338
31974
  const projectPath = process.env.CLAUDE_PROJECT_PATH || void 0;
32339
- const { uploaded, skipped, errors } = await collect2({ forceAll, dryRun, limit, projectPath });
32340
- console.log(`
32341
- Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`);
31975
+ const { uploaded, skipped, errors } = await collect2({
31976
+ forceAll,
31977
+ dryRun,
31978
+ limit,
31979
+ projectPath
31980
+ });
31981
+ console.log(
31982
+ `
31983
+ Collector done: ${uploaded} uploaded, ${skipped} unchanged, ${errors} errors`
31984
+ );
32342
31985
  if (errors > 0) process.exit(1);
32343
31986
  break;
32344
31987
  }