@lossless-claude/lcm 0.5.0 → 0.7.0

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 (182) hide show
  1. package/.claude-plugin/commands/lcm-compact.md +10 -0
  2. package/.claude-plugin/commands/lcm-curate.md +16 -6
  3. package/.claude-plugin/commands/lcm-diagnose.md +10 -0
  4. package/.claude-plugin/commands/lcm-doctor.md +1 -1
  5. package/.claude-plugin/commands/lcm-dogfood.md +1 -1
  6. package/.claude-plugin/commands/lcm-import.md +10 -0
  7. package/.claude-plugin/commands/lcm-promote.md +10 -0
  8. package/.claude-plugin/commands/lcm-sensitive.md +10 -0
  9. package/.claude-plugin/commands/lcm-stats.md +1 -1
  10. package/.claude-plugin/commands/lcm-status.md +10 -0
  11. package/.claude-plugin/hooks/README.md +1 -1
  12. package/.claude-plugin/marketplace.json +1 -1
  13. package/.claude-plugin/plugin.json +14 -2
  14. package/.claude-plugin/skills/lcm-context/SKILL.md +10 -0
  15. package/.claude-plugin/skills/lcm-dogfood/SKILL.md +1 -1
  16. package/.claude-plugin/skills/lcm-dogfood/references/checks.md +1 -1
  17. package/README.md +2 -2
  18. package/dist/bin/lcm.js +828 -474
  19. package/dist/bin/lcm.js.map +1 -1
  20. package/dist/installer/install.d.ts +6 -5
  21. package/dist/installer/install.js +116 -108
  22. package/dist/installer/install.js.map +1 -1
  23. package/dist/src/batch-compact.d.ts +7 -1
  24. package/dist/src/batch-compact.js +80 -6
  25. package/dist/src/batch-compact.js.map +1 -1
  26. package/dist/src/bootstrap.d.ts +24 -0
  27. package/dist/src/bootstrap.js +76 -0
  28. package/dist/src/bootstrap.js.map +1 -0
  29. package/dist/src/cli/pipeline-runner.d.ts +47 -0
  30. package/dist/src/cli/pipeline-runner.js +106 -0
  31. package/dist/src/cli/pipeline-runner.js.map +1 -0
  32. package/dist/src/cli/pipeline-step.d.ts +20 -0
  33. package/dist/src/cli/pipeline-step.js +6 -0
  34. package/dist/src/cli/pipeline-step.js.map +1 -0
  35. package/dist/src/cli/progress-state.d.ts +61 -0
  36. package/dist/src/cli/progress-state.js +16 -0
  37. package/dist/src/cli/progress-state.js.map +1 -0
  38. package/dist/src/cli/render-frame.d.ts +26 -0
  39. package/dist/src/cli/render-frame.js +153 -0
  40. package/dist/src/cli/render-frame.js.map +1 -0
  41. package/dist/src/cli/render-summary.d.ts +8 -0
  42. package/dist/src/cli/render-summary.js +79 -0
  43. package/dist/src/cli/render-summary.js.map +1 -0
  44. package/dist/src/cli-help.js +15 -9
  45. package/dist/src/cli-help.js.map +1 -1
  46. package/dist/src/codex-transcript.d.ts +50 -0
  47. package/dist/src/codex-transcript.js +207 -0
  48. package/dist/src/codex-transcript.js.map +1 -0
  49. package/dist/src/daemon/auth.d.ts +2 -0
  50. package/dist/src/daemon/auth.js +40 -0
  51. package/dist/src/daemon/auth.js.map +1 -0
  52. package/dist/src/daemon/client.d.ts +5 -1
  53. package/dist/src/daemon/client.js +21 -2
  54. package/dist/src/daemon/client.js.map +1 -1
  55. package/dist/src/daemon/config.d.ts +15 -0
  56. package/dist/src/daemon/config.js +33 -8
  57. package/dist/src/daemon/config.js.map +1 -1
  58. package/dist/src/daemon/content-fence.d.ts +7 -0
  59. package/dist/src/daemon/content-fence.js +14 -0
  60. package/dist/src/daemon/content-fence.js.map +1 -0
  61. package/dist/src/daemon/lifecycle.js +5 -0
  62. package/dist/src/daemon/lifecycle.js.map +1 -1
  63. package/dist/src/daemon/orientation.d.ts +1 -0
  64. package/dist/src/daemon/orientation.js +35 -6
  65. package/dist/src/daemon/orientation.js.map +1 -1
  66. package/dist/src/daemon/project.d.ts +1 -0
  67. package/dist/src/daemon/project.js +95 -3
  68. package/dist/src/daemon/project.js.map +1 -1
  69. package/dist/src/daemon/routes/compact.js +41 -10
  70. package/dist/src/daemon/routes/compact.js.map +1 -1
  71. package/dist/src/daemon/routes/describe.js +12 -1
  72. package/dist/src/daemon/routes/describe.js.map +1 -1
  73. package/dist/src/daemon/routes/expand.js +12 -1
  74. package/dist/src/daemon/routes/expand.js.map +1 -1
  75. package/dist/src/daemon/routes/grep.js +11 -2
  76. package/dist/src/daemon/routes/grep.js.map +1 -1
  77. package/dist/src/daemon/routes/ingest.js +36 -8
  78. package/dist/src/daemon/routes/ingest.js.map +1 -1
  79. package/dist/src/daemon/routes/promote-events.d.ts +3 -0
  80. package/dist/src/daemon/routes/promote-events.js +180 -0
  81. package/dist/src/daemon/routes/promote-events.js.map +1 -0
  82. package/dist/src/daemon/routes/promote.js +18 -3
  83. package/dist/src/daemon/routes/promote.js.map +1 -1
  84. package/dist/src/daemon/routes/prompt-search.js +11 -2
  85. package/dist/src/daemon/routes/prompt-search.js.map +1 -1
  86. package/dist/src/daemon/routes/recent.js +11 -2
  87. package/dist/src/daemon/routes/recent.js.map +1 -1
  88. package/dist/src/daemon/routes/restore.js +117 -69
  89. package/dist/src/daemon/routes/restore.js.map +1 -1
  90. package/dist/src/daemon/routes/search.js +12 -1
  91. package/dist/src/daemon/routes/search.js.map +1 -1
  92. package/dist/src/daemon/routes/session-complete.d.ts +2 -0
  93. package/dist/src/daemon/routes/session-complete.js +36 -0
  94. package/dist/src/daemon/routes/session-complete.js.map +1 -0
  95. package/dist/src/daemon/routes/status.js +77 -64
  96. package/dist/src/daemon/routes/status.js.map +1 -1
  97. package/dist/src/daemon/routes/store.d.ts +2 -1
  98. package/dist/src/daemon/routes/store.js +42 -8
  99. package/dist/src/daemon/routes/store.js.map +1 -1
  100. package/dist/src/daemon/safe-error.d.ts +5 -0
  101. package/dist/src/daemon/safe-error.js +15 -0
  102. package/dist/src/daemon/safe-error.js.map +1 -0
  103. package/dist/src/daemon/server.d.ts +3 -1
  104. package/dist/src/daemon/server.js +37 -17
  105. package/dist/src/daemon/server.js.map +1 -1
  106. package/dist/src/daemon/validate-cwd.d.ts +5 -0
  107. package/dist/src/daemon/validate-cwd.js +38 -0
  108. package/dist/src/daemon/validate-cwd.js.map +1 -0
  109. package/dist/src/daemon/version.d.ts +10 -0
  110. package/dist/src/daemon/version.js +31 -0
  111. package/dist/src/daemon/version.js.map +1 -0
  112. package/dist/src/db/events-path.d.ts +2 -0
  113. package/dist/src/db/events-path.js +11 -0
  114. package/dist/src/db/events-path.js.map +1 -0
  115. package/dist/src/db/events-stats.d.ts +29 -0
  116. package/dist/src/db/events-stats.js +107 -0
  117. package/dist/src/db/events-stats.js.map +1 -0
  118. package/dist/src/db/migration.js +8 -0
  119. package/dist/src/db/migration.js.map +1 -1
  120. package/dist/src/diagnose.js +14 -1
  121. package/dist/src/diagnose.js.map +1 -1
  122. package/dist/src/doctor/doctor.d.ts +1 -1
  123. package/dist/src/doctor/doctor.js +184 -15
  124. package/dist/src/doctor/doctor.js.map +1 -1
  125. package/dist/src/hooks/auto-heal.js +24 -1
  126. package/dist/src/hooks/auto-heal.js.map +1 -1
  127. package/dist/src/hooks/compact.js +7 -0
  128. package/dist/src/hooks/compact.js.map +1 -1
  129. package/dist/src/hooks/dispatch.d.ts +1 -1
  130. package/dist/src/hooks/dispatch.js +19 -1
  131. package/dist/src/hooks/dispatch.js.map +1 -1
  132. package/dist/src/hooks/events-db.d.ts +41 -0
  133. package/dist/src/hooks/events-db.js +190 -0
  134. package/dist/src/hooks/events-db.js.map +1 -0
  135. package/dist/src/hooks/extractors.d.ts +17 -0
  136. package/dist/src/hooks/extractors.js +198 -0
  137. package/dist/src/hooks/extractors.js.map +1 -0
  138. package/dist/src/hooks/hook-errors.d.ts +14 -0
  139. package/dist/src/hooks/hook-errors.js +56 -0
  140. package/dist/src/hooks/hook-errors.js.map +1 -0
  141. package/dist/src/hooks/post-tool.d.ts +4 -0
  142. package/dist/src/hooks/post-tool.js +43 -0
  143. package/dist/src/hooks/post-tool.js.map +1 -0
  144. package/dist/src/hooks/restore.js +31 -1
  145. package/dist/src/hooks/restore.js.map +1 -1
  146. package/dist/src/hooks/session-end.d.ts +3 -0
  147. package/dist/src/hooks/session-end.js +68 -5
  148. package/dist/src/hooks/session-end.js.map +1 -1
  149. package/dist/src/hooks/session-snapshot.d.ts +12 -0
  150. package/dist/src/hooks/session-snapshot.js +100 -0
  151. package/dist/src/hooks/session-snapshot.js.map +1 -0
  152. package/dist/src/hooks/user-prompt.js +44 -5
  153. package/dist/src/hooks/user-prompt.js.map +1 -1
  154. package/dist/src/import-summary.d.ts +4 -0
  155. package/dist/src/import-summary.js +34 -0
  156. package/dist/src/import-summary.js.map +1 -0
  157. package/dist/src/import.d.ts +11 -1
  158. package/dist/src/import.js +218 -88
  159. package/dist/src/import.js.map +1 -1
  160. package/dist/src/installer/settings.d.ts +5 -0
  161. package/dist/src/installer/settings.js +74 -0
  162. package/dist/src/installer/settings.js.map +1 -0
  163. package/dist/src/mcp/server.d.ts +15 -0
  164. package/dist/src/mcp/server.js +83 -6
  165. package/dist/src/mcp/server.js.map +1 -1
  166. package/dist/src/promotion/detector.js +19 -5
  167. package/dist/src/promotion/detector.js.map +1 -1
  168. package/dist/src/scrub.js +26 -0
  169. package/dist/src/scrub.js.map +1 -1
  170. package/dist/src/stats.d.ts +4 -0
  171. package/dist/src/stats.js +23 -0
  172. package/dist/src/stats.js.map +1 -1
  173. package/dist/src/store/conversation-store.js +2 -1
  174. package/dist/src/store/conversation-store.js.map +1 -1
  175. package/dist/src/store/regex-safety.d.ts +1 -0
  176. package/dist/src/store/regex-safety.js +22 -0
  177. package/dist/src/store/regex-safety.js.map +1 -0
  178. package/dist/src/store/summary-store.js +2 -1
  179. package/dist/src/store/summary-store.js.map +1 -1
  180. package/docs/passive-learning.md +138 -0
  181. package/lcm.mjs +20 -1
  182. package/package.json +5 -2
package/dist/bin/lcm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { argv, exit, stdin, stdout } from "node:process";
3
- const command = argv[2];
3
+ import { Command, Option } from "commander";
4
4
  function readStdin() {
5
5
  return new Promise((resolve) => {
6
6
  if (stdin.isTTY) {
@@ -12,530 +12,884 @@ function readStdin() {
12
12
  stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
13
13
  });
14
14
  }
15
+ async function withCustomHelp(cmd, commandName) {
16
+ const { printHelp } = await import("../src/cli-help.js");
17
+ printHelp(commandName);
18
+ exit(0);
19
+ }
15
20
  async function main() {
16
- // Handle flags before switch
17
- if (command === "--version" || command === "-V") {
18
- const { readFileSync } = await import("node:fs");
19
- const { join, dirname } = await import("node:path");
20
- const { fileURLToPath } = await import("node:url");
21
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
22
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
23
- stdout.write(pkg.version + "\n");
24
- exit(0);
25
- }
26
- if (command === "--help" || command === "-h" || command === "help") {
21
+ const { readFileSync } = await import("node:fs");
22
+ const { join, dirname } = await import("node:path");
23
+ const { fileURLToPath } = await import("node:url");
24
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
25
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
26
+ const program = new Command();
27
+ program
28
+ .name("lcm")
29
+ .description("lossless context management for Claude Code")
30
+ .version(pkg.version, "-V, --version")
31
+ .helpCommand(false)
32
+ .addHelpCommand(false)
33
+ .configureOutput({
34
+ writeOut: (str) => stdout.write(str),
35
+ writeErr: (str) => process.stderr.write(str),
36
+ });
37
+ // Disable Commander's built-in help entirely — we handle it manually below
38
+ program.helpOption(false);
39
+ // ─── help command ──────────────────────────────────────────────────────────
40
+ program
41
+ .command("help [command]")
42
+ .description("Show help for a command")
43
+ .action(async (subcommand) => {
27
44
  const { printHelp } = await import("../src/cli-help.js");
28
- // 'lcm help compact' or 'lcm compact --help' both show per-command help
29
- const subcommand = argv[3] ?? undefined;
30
45
  printHelp(subcommand);
31
46
  exit(0);
32
- }
33
- switch (command) {
34
- case "daemon": {
35
- if (argv.includes("--help") || argv.includes("-h")) {
36
- const { printHelp } = await import("../src/cli-help.js");
37
- printHelp("daemon");
38
- exit(0);
39
- }
40
- if (argv[3] === "start") {
41
- if (argv.includes("--detach")) {
42
- const { spawn } = await import("node:child_process");
43
- const child = spawn(process.execPath, [process.argv[1], "daemon", "start"], {
44
- detached: true,
45
- stdio: "ignore",
46
- env: process.env,
47
- });
48
- child.unref();
49
- if (child.pid) {
50
- const { writeFileSync, mkdirSync } = await import("node:fs");
51
- const { join } = await import("node:path");
52
- const { homedir } = await import("node:os");
53
- const lcDir = join(homedir(), ".lossless-claude");
54
- mkdirSync(lcDir, { recursive: true });
55
- writeFileSync(join(lcDir, "daemon.pid"), String(child.pid));
56
- console.log(`lcm daemon started in background (PID ${child.pid})`);
57
- }
58
- exit(0);
59
- }
60
- const { createDaemon } = await import("../src/daemon/server.js");
61
- const { loadDaemonConfig } = await import("../src/daemon/config.js");
62
- const { join } = await import("node:path");
63
- const { homedir } = await import("node:os");
64
- const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
65
- const daemon = await createDaemon(config);
66
- console.log(`lcm daemon started on port ${daemon.address().port}`);
67
- process.on("SIGTERM", () => exit(0));
68
- process.on("SIGINT", () => exit(0));
69
- }
70
- break;
47
+ });
48
+ // ─── daemon ────────────────────────────────────────────────────────────────
49
+ const daemonCmd = new Command("daemon").description("Start the context daemon");
50
+ daemonCmd.helpOption(false).option("-h, --help", "Show help");
51
+ daemonCmd.command("start")
52
+ .description("Start the context daemon")
53
+ .option("--detach", "Run in the background")
54
+ .option("-h, --help", "Show help")
55
+ .action(async (opts) => {
56
+ if (opts.help) {
57
+ await withCustomHelp(daemonCmd, "daemon");
58
+ return;
71
59
  }
72
- case "compact": {
73
- if (argv.includes("--help") || argv.includes("-h")) {
74
- const { printHelp } = await import("../src/cli-help.js");
75
- printHelp("compact");
76
- exit(0);
77
- }
78
- const all = argv.includes("--all");
79
- // Interactive batch compact: --all (all projects) or TTY stdin (current project only).
80
- // If stdin is piped (hook invocation), fall through to hook dispatch.
81
- if (all || process.stdin.isTTY) {
82
- const { batchCompact } = await import("../src/batch-compact.js");
83
- const { loadDaemonConfig } = await import("../src/daemon/config.js");
60
+ if (opts.detach) {
61
+ const { spawn } = await import("node:child_process");
62
+ const child = spawn(process.execPath, [process.argv[1], "daemon", "start"], {
63
+ detached: true,
64
+ stdio: "ignore",
65
+ env: process.env,
66
+ });
67
+ child.unref();
68
+ if (child.pid) {
69
+ const { writeFileSync, mkdirSync } = await import("node:fs");
84
70
  const { join } = await import("node:path");
85
71
  const { homedir } = await import("node:os");
86
- const { ensureDaemon } = await import("../src/daemon/lifecycle.js");
87
- const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
88
- const port = config.daemon?.port ?? 3737;
89
- const pidFilePath = join(homedir(), ".lossless-claude", "daemon.pid");
90
- const { connected } = await ensureDaemon({ port, pidFilePath, spawnTimeoutMs: 10000 });
91
- if (!connected) {
92
- console.error("Could not connect to daemon. Start it with: lcm daemon start --detach");
93
- exit(1);
94
- }
95
- const dryRun = argv.includes("--dry-run");
96
- const replay = argv.includes("--replay");
97
- const minTokens = config.compaction.autoCompactMinTokens;
98
- const cwd = all ? undefined : process.cwd();
99
- await batchCompact({ minTokens, dryRun, port, cwd, replay });
100
- break;
72
+ const lcDir = join(homedir(), ".lossless-claude");
73
+ mkdirSync(lcDir, { recursive: true });
74
+ writeFileSync(join(lcDir, "daemon.pid"), String(child.pid));
75
+ console.log(`lcm daemon started in background (PID ${child.pid})`);
101
76
  }
77
+ exit(0);
102
78
  }
103
- // falls through to hook dispatch (piped stdin = PreCompact hook invocation)
104
- case "restore":
105
- case "session-end":
106
- case "user-prompt": {
107
- const { dispatchHook } = await import("../src/hooks/dispatch.js");
108
- const input = await readStdin();
109
- const r = await dispatchHook(command, input);
110
- if (r.stdout)
111
- stdout.write(r.stdout);
112
- exit(r.exitCode);
113
- break;
114
- }
115
- case "mcp": {
116
- const { startMcpServer } = await import("../src/mcp/server.js");
117
- await startMcpServer();
118
- break;
119
- }
120
- case "install": {
121
- if (argv.includes("--help") || argv.includes("-h")) {
122
- const { printHelp } = await import("../src/cli-help.js");
123
- printHelp("install");
124
- exit(0);
125
- }
126
- const dryRun = argv.includes("--dry-run");
127
- const { install } = await import("../installer/install.js");
128
- if (dryRun) {
129
- const { DryRunServiceDeps } = await import("../installer/dry-run-deps.js");
130
- console.log("\n lcm install --dry-run\n");
131
- await install(new DryRunServiceDeps());
132
- console.log("\n No changes written.");
133
- }
134
- else {
135
- await install();
136
- }
137
- break;
79
+ const { createDaemon } = await import("../src/daemon/server.js");
80
+ const { loadDaemonConfig } = await import("../src/daemon/config.js");
81
+ const { ensureAuthToken } = await import("../src/daemon/auth.js");
82
+ const { join } = await import("node:path");
83
+ const { homedir } = await import("node:os");
84
+ const lcDir = join(homedir(), ".lossless-claude");
85
+ const tokenPath = join(lcDir, "daemon.token");
86
+ ensureAuthToken(tokenPath);
87
+ const config = loadDaemonConfig(join(lcDir, "config.json"));
88
+ const daemon = await createDaemon(config, { tokenPath });
89
+ console.log(`lcm daemon started on port ${daemon.address().port}`);
90
+ process.on("SIGTERM", () => exit(0));
91
+ process.on("SIGINT", () => exit(0));
92
+ });
93
+ daemonCmd.action(async (opts) => {
94
+ if (opts.help) {
95
+ await withCustomHelp(daemonCmd, "daemon");
96
+ return;
138
97
  }
139
- case "uninstall": {
140
- if (argv.includes("--help") || argv.includes("-h")) {
141
- const { printHelp } = await import("../src/cli-help.js");
142
- printHelp("uninstall");
143
- exit(0);
144
- }
145
- const dryRun = argv.includes("--dry-run");
146
- const { uninstall } = await import("../installer/uninstall.js");
147
- if (dryRun) {
148
- const { DryRunServiceDeps } = await import("../installer/dry-run-deps.js");
149
- console.log("\n lcm uninstall --dry-run\n");
150
- await uninstall(new DryRunServiceDeps());
151
- console.log("\n No changes written.");
152
- }
153
- else {
154
- await uninstall();
155
- }
156
- break;
98
+ });
99
+ program.addCommand(daemonCmd);
100
+ // ─── compact ───────────────────────────────────────────────────────────────
101
+ program
102
+ .command("compact")
103
+ .description("Compact conversation context into DAG summary nodes")
104
+ .option("--all", "Compact all tracked projects")
105
+ .option("--dry-run", "Show what would be compacted without writing")
106
+ .option("--replay", "Compact sequentially with threaded context")
107
+ .option("--no-promote", "Skip the automatic promote step")
108
+ .option("-v, --verbose", "Show per-session token details")
109
+ .addOption(new Option("--hook", "Hook dispatch mode (internal)").hideHelp())
110
+ .helpOption(false)
111
+ .option("-h, --help", "Show help")
112
+ .action(async (opts) => {
113
+ if (opts.help) {
114
+ const { printHelp } = await import("../src/cli-help.js");
115
+ printHelp("compact");
116
+ exit(0);
157
117
  }
158
- case "status": {
159
- if (argv.includes("--help") || argv.includes("-h")) {
160
- const { printHelp } = await import("../src/cli-help.js");
161
- printHelp("status");
162
- exit(0);
163
- }
118
+ const all = opts.all ?? false;
119
+ const dryRun = opts.dryRun ?? false;
120
+ const verbose = opts.verbose ?? false;
121
+ const replay = opts.replay ?? false;
122
+ // Hook dispatch only when --hook is explicit; all other invocations go to batch.
123
+ const hook = opts.hook ?? false;
124
+ if (!hook) {
125
+ const { batchCompact } = await import("../src/batch-compact.js");
164
126
  const { loadDaemonConfig } = await import("../src/daemon/config.js");
165
127
  const { join } = await import("node:path");
166
128
  const { homedir } = await import("node:os");
129
+ const { ensureDaemon } = await import("../src/daemon/lifecycle.js");
167
130
  const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
168
131
  const port = config.daemon?.port ?? 3737;
169
- const jsonFlag = argv.includes("--json");
170
- let daemonStatus = "down";
171
- let statusData = null;
172
- try {
173
- const res = await fetch(`http://127.0.0.1:${port}/health`);
174
- if (res.ok)
175
- daemonStatus = "up";
176
- // Also fetch /status endpoint if daemon is up
177
- if (daemonStatus === "up") {
178
- const statusRes = await fetch(`http://127.0.0.1:${port}/status`, {
179
- method: "POST",
180
- headers: { "Content-Type": "application/json" },
181
- body: JSON.stringify({ cwd: process.cwd() }),
182
- });
183
- if (statusRes.ok) {
184
- statusData = await statusRes.json();
185
- }
186
- }
132
+ const pidFilePath = join(homedir(), ".lossless-claude", "daemon.pid");
133
+ const { connected } = await ensureDaemon({ port, pidFilePath, spawnTimeoutMs: 10000 });
134
+ if (!connected) {
135
+ console.error("Could not connect to daemon. Start it with: lcm daemon start --detach");
136
+ exit(1);
187
137
  }
188
- catch { }
189
- if (jsonFlag) {
190
- const result = {
191
- daemon: daemonStatus === "up" ? statusData?.daemon : { status: "down" },
192
- project: statusData?.project,
193
- };
194
- stdout.write(JSON.stringify(result, null, 2) + "\n");
138
+ const noPromote = !opts.promote;
139
+ const minTokens = config.compaction.autoCompactMinTokens;
140
+ const cwd = all ? undefined : process.cwd();
141
+ const { NinjaRenderer } = await import("../src/cli/pipeline-runner.js");
142
+ const { makeProgressState } = await import("../src/cli/progress-state.js");
143
+ const isTTY = process.stdout.isTTY ?? false;
144
+ const renderOpts = { isTTY, width: process.stdout.columns ?? 80, color: isTTY, verbose };
145
+ const compactState = makeProgressState({ phases: [{ name: "Compact", status: "active" }], dryRun });
146
+ const compactRenderer = new NinjaRenderer({ state: compactState, renderOpts });
147
+ compactRenderer.start();
148
+ const { compacted } = await batchCompact({
149
+ minTokens, dryRun, port, cwd, replay, verbose,
150
+ onProgress: (patch) => {
151
+ Object.assign(compactState, patch);
152
+ if (patch.lastResult)
153
+ compactRenderer.sessionDone();
154
+ },
155
+ });
156
+ compactRenderer.stop();
157
+ if (isTTY) {
158
+ compactState.phases[0].status = "done";
159
+ compactRenderer.printSummary();
195
160
  }
196
- else {
197
- const provider = config.llm?.provider ?? "unknown";
198
- const providerDisplay = provider === "auto"
199
- ? "auto (Claude->claude-process, Codex->codex-process)"
200
- : provider;
201
- if (statusData) {
202
- console.log(`Daemon: ${daemonStatus}`);
203
- console.log(` Version: ${statusData.daemon.version}`);
204
- console.log(` Uptime: ${statusData.daemon.uptime}s`);
205
- console.log(` Port: ${statusData.daemon.port}`);
206
- console.log(` Provider: ${providerDisplay}`);
207
- console.log();
208
- console.log("Project:");
209
- console.log(` Messages: ${statusData.project.messageCount}`);
210
- console.log(` Summaries: ${statusData.project.summaryCount}`);
211
- console.log(` Promoted: ${statusData.project.promotedCount}`);
212
- if (statusData.project.lastIngest)
213
- console.log(` Last Ingest: ${statusData.project.lastIngest}`);
214
- if (statusData.project.lastCompact)
215
- console.log(` Last Compact: ${statusData.project.lastCompact}`);
216
- if (statusData.project.lastPromote)
217
- console.log(` Last Promote: ${statusData.project.lastPromote}`);
161
+ // Auto-promote after a successful compact: new summaries are prime promotion candidates.
162
+ if (compacted > 0 && !noPromote) {
163
+ const { readdirSync, existsSync, readFileSync } = await import("node:fs");
164
+ const promoteCwds = [];
165
+ if (cwd) {
166
+ promoteCwds.push(cwd);
218
167
  }
219
168
  else {
220
- console.log(`daemon: ${daemonStatus} · provider: ${providerDisplay}`);
221
- }
222
- }
223
- break;
224
- }
225
- case "stats": {
226
- if (argv.includes("--help") || argv.includes("-h")) {
227
- const { printHelp } = await import("../src/cli-help.js");
228
- printHelp("stats");
229
- exit(0);
230
- }
231
- const verbose = argv.includes("--verbose") || argv.includes("-v");
232
- const { collectStats, printStats } = await import("../src/stats.js");
233
- printStats(collectStats(), verbose);
234
- break;
235
- }
236
- case "doctor": {
237
- if (argv.includes("--help") || argv.includes("-h")) {
238
- const { printHelp } = await import("../src/cli-help.js");
239
- printHelp("doctor");
240
- exit(0);
241
- }
242
- const { runDoctor, printResults } = await import("../src/doctor/doctor.js");
243
- const results = await runDoctor();
244
- printResults(results);
245
- const failures = results.filter((r) => r.status === "fail");
246
- exit(failures.length > 0 ? 1 : 0);
247
- break;
248
- }
249
- case "diagnose": {
250
- if (argv.includes("--help") || argv.includes("-h")) {
251
- const { printHelp } = await import("../src/cli-help.js");
252
- printHelp("diagnose");
253
- exit(0);
254
- }
255
- const all = argv.includes("--all");
256
- const verbose = argv.includes("--verbose");
257
- const json = argv.includes("--json");
258
- const daysIndex = argv.indexOf("--days");
259
- const daysValue = daysIndex !== -1 ? argv[daysIndex + 1] : undefined;
260
- const days = daysValue ? Number(daysValue) : 7;
261
- if (!Number.isFinite(days) || days <= 0 || !Number.isInteger(days)) {
262
- console.error("Usage: lcm diagnose [--all] [--days N] [--verbose] [--json]");
263
- exit(1);
264
- }
265
- const { diagnose, formatDiagnoseResult } = await import("../src/diagnose.js");
266
- const result = await diagnose({ all, days, verbose });
267
- if (json) {
268
- stdout.write(JSON.stringify(result, null, 2) + "\n");
269
- }
270
- else {
271
- stdout.write(formatDiagnoseResult(result, { days, verbose }));
272
- }
273
- break;
274
- }
275
- case "connectors": {
276
- if (argv.includes("--help") || argv.includes("-h")) {
277
- const { printHelp } = await import("../src/cli-help.js");
278
- printHelp("connectors");
279
- exit(0);
280
- }
281
- const sub = argv[3];
282
- switch (sub) {
283
- case "list": {
284
- const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "text";
285
- const { listConnectors } = await import("../src/connectors/installer.js");
286
- const { AGENTS } = await import("../src/connectors/registry.js");
287
- const installed = listConnectors();
288
- if (format === "json") {
289
- const result = AGENTS.map(a => ({
290
- id: a.id,
291
- name: a.name,
292
- category: a.category,
293
- defaultType: a.defaultType,
294
- supportedTypes: a.supportedTypes,
295
- installed: installed.filter(c => c.agentId === a.id).map(c => c.type),
296
- }));
297
- stdout.write(JSON.stringify({ agents: result }, null, 2) + "\n");
298
- }
299
- else {
300
- console.log("\n Available agents:\n");
301
- console.log(" %-20s %-15s %-15s %s", "Agent", "Installed", "Default", "Supported");
302
- console.log(" " + "─".repeat(70));
303
- for (const agent of AGENTS) {
304
- const agentInstalled = installed.filter(c => c.agentId === agent.id);
305
- const installedStr = agentInstalled.length > 0
306
- ? agentInstalled.map(c => c.type).join(", ")
307
- : "-";
308
- console.log(" %-20s %-15s %-15s %s", agent.name, installedStr, agent.defaultType, agent.supportedTypes.join(", "));
169
+ const projectsDir = join(homedir(), ".lossless-claude", "projects");
170
+ if (existsSync(projectsDir)) {
171
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
172
+ if (!entry.isDirectory())
173
+ continue;
174
+ const metaPath = join(projectsDir, entry.name, "meta.json");
175
+ if (!existsSync(metaPath))
176
+ continue;
177
+ try {
178
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
179
+ if (meta.cwd)
180
+ promoteCwds.push(meta.cwd);
181
+ }
182
+ catch { /* skip unreadable */ }
309
183
  }
310
- console.log();
311
184
  }
312
- break;
313
185
  }
314
- case "install": {
315
- const agentName = argv.slice(4).filter(a => !a.startsWith("--")).join(" ");
316
- if (!agentName) {
317
- console.error("Usage: lcm connectors install <agent> [--type rules|mcp|skill]");
318
- exit(1);
319
- }
320
- const typeIdx = argv.indexOf("--type");
321
- const type = typeIdx !== -1 ? argv[typeIdx + 1] : undefined;
322
- const { installConnector } = await import("../src/connectors/installer.js");
186
+ let totalPromoted = 0;
187
+ for (const promoteCwd of promoteCwds) {
323
188
  try {
324
- const result = installConnector(agentName, type);
325
- if (result.manual) {
326
- console.log(`\n ${result.manual}\n`);
327
- }
328
- else {
329
- console.log(`\n ✓ Installed ${type ?? "default"} connector for ${agentName}`);
330
- console.log(` Path: ${result.path}`);
331
- if (result.requiresRestart)
332
- console.log(" Restart the agent to activate.");
333
- console.log();
189
+ const res = await fetch(`http://127.0.0.1:${port}/promote`, {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify({ cwd: promoteCwd, dry_run: dryRun }),
193
+ });
194
+ if (res.ok) {
195
+ const result = await res.json();
196
+ totalPromoted += result.promoted;
334
197
  }
335
198
  }
336
- catch (err) {
337
- console.error(` Error: ${err.message}`);
338
- exit(1);
339
- }
340
- break;
199
+ catch { /* non-fatal: promote is best-effort */ }
341
200
  }
342
- case "remove": {
343
- const agentName = argv.slice(4).filter(a => !a.startsWith("--")).join(" ");
344
- if (!agentName) {
345
- console.error("Usage: lcm connectors remove <agent> [--type rules|mcp|skill]");
346
- exit(1);
347
- }
348
- const typeIdx = argv.indexOf("--type");
349
- const type = typeIdx !== -1 ? argv[typeIdx + 1] : undefined;
350
- const { removeConnector } = await import("../src/connectors/installer.js");
351
- try {
352
- const removed = removeConnector(agentName, type);
353
- if (removed) {
354
- console.log(`\n ✓ Removed connector for ${agentName}\n`);
355
- }
356
- else {
357
- console.log(`\n No connector found for ${agentName}\n`);
358
- }
359
- }
360
- catch (err) {
361
- console.error(` Error: ${err.message}`);
362
- exit(1);
363
- }
364
- break;
201
+ if (totalPromoted > 0) {
202
+ console.log(` → ${totalPromoted} insight${totalPromoted !== 1 ? "s" : ""} promoted`);
365
203
  }
366
- case "doctor": {
367
- const agentName = argv.slice(4).filter(a => !a.startsWith("--")).join(" ");
368
- const { AGENTS } = await import("../src/connectors/registry.js");
369
- const { listConnectors } = await import("../src/connectors/installer.js");
370
- const { findAgent } = await import("../src/connectors/registry.js");
371
- const found = agentName ? findAgent(agentName) : undefined;
372
- const agents = found ? [found] : agentName ? [] : AGENTS;
373
- if (agents.length === 0) {
374
- console.error(` Unknown agent: ${agentName}`);
375
- exit(1);
376
- }
377
- const installed = listConnectors();
378
- console.log("\n Connector health:\n");
379
- for (const agent of agents) {
380
- const agentConnectors = installed.filter((c) => c.agentId === agent.id);
381
- if (agentConnectors.length === 0) {
382
- console.log(` ⚠ ${agent.name}: no connectors installed`);
383
- }
384
- else {
385
- for (const c of agentConnectors) {
386
- console.log(` ✓ ${agent.name}: ${c.type} at ${c.path}`);
387
- }
388
- }
389
- }
390
- console.log();
391
- break;
204
+ }
205
+ return;
206
+ }
207
+ // Piped stdin hook dispatch (PreCompact hook invocation)
208
+ const { dispatchHook } = await import("../src/hooks/dispatch.js");
209
+ const input = await readStdin();
210
+ const r = await dispatchHook("compact", input);
211
+ if (r.stdout)
212
+ stdout.write(r.stdout);
213
+ exit(r.exitCode);
214
+ });
215
+ // ─── restore (hook) ────────────────────────────────────────────────────────
216
+ program
217
+ .command("restore")
218
+ .description("Dispatch the restore hook")
219
+ .helpOption(false)
220
+ .option("-h, --help", "Show help")
221
+ .action(async (opts) => {
222
+ if (opts.help) {
223
+ const { printHelp } = await import("../src/cli-help.js");
224
+ printHelp("restore");
225
+ exit(0);
226
+ }
227
+ const { dispatchHook } = await import("../src/hooks/dispatch.js");
228
+ const input = await readStdin();
229
+ const r = await dispatchHook("restore", input);
230
+ if (r.stdout)
231
+ stdout.write(r.stdout);
232
+ exit(r.exitCode);
233
+ });
234
+ // ─── session-end (hook) ────────────────────────────────────────────────────
235
+ program
236
+ .command("session-end")
237
+ .description("Dispatch the session-end hook")
238
+ .helpOption(false)
239
+ .option("-h, --help", "Show help")
240
+ .action(async (opts) => {
241
+ if (opts.help) {
242
+ const { printHelp } = await import("../src/cli-help.js");
243
+ printHelp("session-end");
244
+ exit(0);
245
+ }
246
+ const { dispatchHook } = await import("../src/hooks/dispatch.js");
247
+ const input = await readStdin();
248
+ const r = await dispatchHook("session-end", input);
249
+ if (r.stdout)
250
+ stdout.write(r.stdout);
251
+ exit(r.exitCode);
252
+ });
253
+ // ─── user-prompt (hook) ────────────────────────────────────────────────────
254
+ program
255
+ .command("user-prompt")
256
+ .description("Dispatch the user-prompt hook")
257
+ .helpOption(false)
258
+ .option("-h, --help", "Show help")
259
+ .action(async (opts) => {
260
+ if (opts.help) {
261
+ const { printHelp } = await import("../src/cli-help.js");
262
+ printHelp("user-prompt");
263
+ exit(0);
264
+ }
265
+ const { dispatchHook } = await import("../src/hooks/dispatch.js");
266
+ const input = await readStdin();
267
+ const r = await dispatchHook("user-prompt", input);
268
+ if (r.stdout)
269
+ stdout.write(r.stdout);
270
+ exit(r.exitCode);
271
+ });
272
+ // ─── session-snapshot (hook) ─────────────────────────────────────────────
273
+ program
274
+ .command("session-snapshot")
275
+ .description("Rolling ingest snapshot (called by Stop hook)")
276
+ .helpOption(false)
277
+ .action(async () => {
278
+ const { dispatchHook } = await import("../src/hooks/dispatch.js");
279
+ const input = await readStdin();
280
+ const r = await dispatchHook("session-snapshot", input);
281
+ if (r.stdout)
282
+ stdout.write(r.stdout);
283
+ exit(r.exitCode);
284
+ });
285
+ // ─── mcp ───────────────────────────────────────────────────────────────────
286
+ program
287
+ .command("mcp")
288
+ .description("Start the lcm MCP server")
289
+ .helpOption(false)
290
+ .option("-h, --help", "Show help")
291
+ .action(async (opts) => {
292
+ if (opts.help) {
293
+ const { printHelp } = await import("../src/cli-help.js");
294
+ printHelp("mcp");
295
+ exit(0);
296
+ }
297
+ const { startMcpServer } = await import("../src/mcp/server.js");
298
+ await startMcpServer();
299
+ });
300
+ // ─── install ───────────────────────────────────────────────────────────────
301
+ program
302
+ .command("install")
303
+ .description("Set up lcm: register hooks, configure daemon, connect MCP")
304
+ .option("--dry-run", "Preview all changes without writing anything")
305
+ .helpOption(false)
306
+ .option("-h, --help", "Show help")
307
+ .action(async (opts) => {
308
+ if (opts.help) {
309
+ const { printHelp } = await import("../src/cli-help.js");
310
+ printHelp("install");
311
+ exit(0);
312
+ }
313
+ const dryRun = opts.dryRun ?? false;
314
+ const { install } = await import("../installer/install.js");
315
+ if (dryRun) {
316
+ const { DryRunServiceDeps } = await import("../installer/dry-run-deps.js");
317
+ console.log("\n lcm install --dry-run\n");
318
+ await install(new DryRunServiceDeps());
319
+ console.log("\n No changes written.");
320
+ }
321
+ else {
322
+ await install();
323
+ }
324
+ });
325
+ // ─── uninstall ─────────────────────────────────────────────────────────────
326
+ program
327
+ .command("uninstall")
328
+ .description("Remove lcm hooks and MCP registration")
329
+ .option("--dry-run", "Preview removals without writing anything")
330
+ .helpOption(false)
331
+ .option("-h, --help", "Show help")
332
+ .action(async (opts) => {
333
+ if (opts.help) {
334
+ const { printHelp } = await import("../src/cli-help.js");
335
+ printHelp("uninstall");
336
+ exit(0);
337
+ }
338
+ const dryRun = opts.dryRun ?? false;
339
+ const { uninstall } = await import("../installer/uninstall.js");
340
+ if (dryRun) {
341
+ const { DryRunServiceDeps } = await import("../installer/dry-run-deps.js");
342
+ console.log("\n lcm uninstall --dry-run\n");
343
+ await uninstall(new DryRunServiceDeps());
344
+ console.log("\n No changes written.");
345
+ }
346
+ else {
347
+ await uninstall();
348
+ }
349
+ });
350
+ // ─── status ────────────────────────────────────────────────────────────────
351
+ program
352
+ .command("status")
353
+ .description("Show daemon status and project memory statistics")
354
+ .option("--json", "Output structured JSON")
355
+ .helpOption(false)
356
+ .option("-h, --help", "Show help")
357
+ .action(async (opts) => {
358
+ if (opts.help) {
359
+ const { printHelp } = await import("../src/cli-help.js");
360
+ printHelp("status");
361
+ exit(0);
362
+ }
363
+ const { loadDaemonConfig } = await import("../src/daemon/config.js");
364
+ const { join } = await import("node:path");
365
+ const { homedir } = await import("node:os");
366
+ const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
367
+ const port = config.daemon?.port ?? 3737;
368
+ const jsonFlag = opts.json ?? false;
369
+ let daemonStatus = "down";
370
+ let statusData = null;
371
+ try {
372
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
373
+ if (res.ok)
374
+ daemonStatus = "up";
375
+ // Also fetch /status endpoint if daemon is up
376
+ if (daemonStatus === "up") {
377
+ const statusRes = await fetch(`http://127.0.0.1:${port}/status`, {
378
+ method: "POST",
379
+ headers: { "Content-Type": "application/json" },
380
+ body: JSON.stringify({ cwd: process.cwd() }),
381
+ });
382
+ if (statusRes.ok) {
383
+ statusData = await statusRes.json();
392
384
  }
393
- default:
394
- console.error("Usage: lcm connectors <list|install|remove|doctor> [options]");
395
- exit(1);
396
385
  }
397
- break;
398
386
  }
399
- case "sensitive": {
400
- if (argv.includes("--help") || argv.includes("-h")) {
401
- const { printHelp } = await import("../src/cli-help.js");
402
- printHelp("sensitive");
403
- exit(0);
387
+ catch { }
388
+ if (jsonFlag) {
389
+ const result = {
390
+ daemon: daemonStatus === "up" ? statusData?.daemon : { status: "down" },
391
+ project: statusData?.project,
392
+ };
393
+ stdout.write(JSON.stringify(result, null, 2) + "\n");
394
+ }
395
+ else {
396
+ const provider = config.llm?.provider ?? "unknown";
397
+ const providerDisplay = provider === "auto"
398
+ ? "auto (Claude->claude-process, Codex->codex-process)"
399
+ : provider;
400
+ if (statusData) {
401
+ console.log(`Daemon: ${daemonStatus}`);
402
+ console.log(` Version: ${statusData.daemon.version}`);
403
+ console.log(` Uptime: ${statusData.daemon.uptime}s`);
404
+ console.log(` Port: ${statusData.daemon.port}`);
405
+ console.log(` Provider: ${providerDisplay}`);
406
+ console.log();
407
+ console.log("Project:");
408
+ console.log(` Messages: ${statusData.project.messageCount}`);
409
+ console.log(` Summaries: ${statusData.project.summaryCount}`);
410
+ console.log(` Promoted: ${statusData.project.promotedCount}`);
411
+ if (statusData.project.lastIngest)
412
+ console.log(` Last Ingest: ${statusData.project.lastIngest}`);
413
+ if (statusData.project.lastCompact)
414
+ console.log(` Last Compact: ${statusData.project.lastCompact}`);
415
+ if (statusData.project.lastPromote)
416
+ console.log(` Last Promote: ${statusData.project.lastPromote}`);
404
417
  }
405
- const { handleSensitive } = await import("../src/sensitive.js");
406
- const { join } = await import("node:path");
407
- const { homedir } = await import("node:os");
408
- const configPath = join(homedir(), ".lossless-claude", "config.json");
409
- const r = await handleSensitive(argv.slice(3), process.cwd(), configPath);
410
- if (r.stdout)
411
- stdout.write(r.stdout);
412
- exit(r.exitCode);
413
- break;
414
- }
415
- case "import": {
416
- if (argv.includes("--help") || argv.includes("-h")) {
417
- const { printHelp } = await import("../src/cli-help.js");
418
- printHelp("import");
419
- exit(0);
418
+ else {
419
+ console.log(`daemon: ${daemonStatus} · provider: ${providerDisplay}`);
420
420
  }
421
- const all = argv.includes("--all");
422
- const verbose = argv.includes("--verbose");
423
- const dryRun = argv.includes("--dry-run");
424
- const replay = argv.includes("--replay");
425
- const { ensureDaemon } = await import("../src/daemon/lifecycle.js");
426
- const { DaemonClient } = await import("../src/daemon/client.js");
427
- const { loadDaemonConfig } = await import("../src/daemon/config.js");
428
- const { importSessions } = await import("../src/import.js");
429
- const { join } = await import("node:path");
430
- const { homedir } = await import("node:os");
431
- const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
432
- const port = config.daemon?.port ?? 3737;
433
- const pidFilePath = join(homedir(), ".lossless-claude", "daemon.pid");
434
- const { connected } = await ensureDaemon({ port, pidFilePath, spawnTimeoutMs: 5000 });
435
- if (!connected) {
436
- console.error(" Daemon not available");
437
- exit(1);
421
+ }
422
+ });
423
+ // ─── stats ─────────────────────────────────────────────────────────────────
424
+ program
425
+ .command("stats")
426
+ .description("Show memory inventory and compression ratios")
427
+ .option("-v, --verbose", "Show per-conversation breakdown")
428
+ .helpOption(false)
429
+ .option("-h, --help", "Show help")
430
+ .action(async (opts) => {
431
+ if (opts.help) {
432
+ const { printHelp } = await import("../src/cli-help.js");
433
+ printHelp("stats");
434
+ exit(0);
435
+ }
436
+ const verbose = opts.verbose ?? false;
437
+ const { collectStats, printStats } = await import("../src/stats.js");
438
+ printStats(collectStats(), verbose);
439
+ });
440
+ // ─── doctor ────────────────────────────────────────────────────────────────
441
+ program
442
+ .command("doctor")
443
+ .description("Run diagnostics: daemon, hooks, MCP, summarizer")
444
+ .helpOption(false)
445
+ .option("-h, --help", "Show help")
446
+ .action(async (opts) => {
447
+ if (opts.help) {
448
+ const { printHelp } = await import("../src/cli-help.js");
449
+ printHelp("doctor");
450
+ exit(0);
451
+ }
452
+ const { runDoctor, printResults } = await import("../src/doctor/doctor.js");
453
+ const results = await runDoctor();
454
+ printResults(results);
455
+ const failures = results.filter((r) => r.status === "fail");
456
+ exit(failures.length > 0 ? 1 : 0);
457
+ });
458
+ // ─── diagnose ──────────────────────────────────────────────────────────────
459
+ program
460
+ .command("diagnose")
461
+ .description("Scan recent sessions for hook failures and issues")
462
+ .option("--all", "Scan all tracked projects")
463
+ .option("--days <n>", "Scan the last N days (default: 7)", "7")
464
+ .option("--verbose", "Include full event details")
465
+ .option("--json", "Output structured JSON")
466
+ .helpOption(false)
467
+ .option("-h, --help", "Show help")
468
+ .action(async (opts) => {
469
+ if (opts.help) {
470
+ const { printHelp } = await import("../src/cli-help.js");
471
+ printHelp("diagnose");
472
+ exit(0);
473
+ }
474
+ const all = opts.all ?? false;
475
+ const verbose = opts.verbose ?? false;
476
+ const json = opts.json ?? false;
477
+ const days = Number(opts.days);
478
+ if (!Number.isFinite(days) || days <= 0 || !Number.isInteger(days)) {
479
+ console.error("Usage: lcm diagnose [--all] [--days N] [--verbose] [--json]");
480
+ exit(1);
481
+ }
482
+ const { diagnose, formatDiagnoseResult } = await import("../src/diagnose.js");
483
+ const result = await diagnose({ all, days, verbose });
484
+ if (json) {
485
+ stdout.write(JSON.stringify(result, null, 2) + "\n");
486
+ }
487
+ else {
488
+ stdout.write(formatDiagnoseResult(result, { days, verbose }));
489
+ }
490
+ });
491
+ // ─── connectors ────────────────────────────────────────────────────────────
492
+ const connectorsCmd = new Command("connectors").description("Manage connectors for coding agents");
493
+ connectorsCmd.helpOption(false).option("-h, --help", "Show help");
494
+ connectorsCmd.action(async (opts) => {
495
+ if (opts.help) {
496
+ const { printHelp } = await import("../src/cli-help.js");
497
+ printHelp("connectors");
498
+ exit(0);
499
+ }
500
+ console.error("Usage: lcm connectors <list|install|remove|doctor> [options]");
501
+ exit(1);
502
+ });
503
+ connectorsCmd
504
+ .command("list")
505
+ .description("List available agents and installed connectors")
506
+ .option("--format <format>", "Output format: text or json", "text")
507
+ .helpOption(false)
508
+ .option("-h, --help", "Show help")
509
+ .action(async (opts) => {
510
+ if (opts.help) {
511
+ const { printHelp } = await import("../src/cli-help.js");
512
+ printHelp("connectors");
513
+ exit(0);
514
+ }
515
+ const format = opts.format ?? "text";
516
+ const { listConnectors } = await import("../src/connectors/installer.js");
517
+ const { AGENTS } = await import("../src/connectors/registry.js");
518
+ const installed = listConnectors();
519
+ if (format === "json") {
520
+ const result = AGENTS.map((a) => ({
521
+ id: a.id,
522
+ name: a.name,
523
+ category: a.category,
524
+ defaultType: a.defaultType,
525
+ supportedTypes: a.supportedTypes,
526
+ installed: installed.filter((c) => c.agentId === a.id).map((c) => c.type),
527
+ }));
528
+ stdout.write(JSON.stringify({ agents: result }, null, 2) + "\n");
529
+ }
530
+ else {
531
+ console.log("\n Available agents:\n");
532
+ console.log(" %-20s %-15s %-15s %s", "Agent", "Installed", "Default", "Supported");
533
+ console.log(" " + "─".repeat(70));
534
+ for (const agent of AGENTS) {
535
+ const agentInstalled = installed.filter((c) => c.agentId === agent.id);
536
+ const installedStr = agentInstalled.length > 0
537
+ ? agentInstalled.map((c) => c.type).join(", ")
538
+ : "-";
539
+ console.log(" %-20s %-15s %-15s %s", agent.name, installedStr, agent.defaultType, agent.supportedTypes.join(", "));
438
540
  }
439
- const client = new DaemonClient(`http://127.0.0.1:${port}`);
440
- console.log(`\n Importing Claude Code sessions${all ? " (all projects)" : ""}...\n`);
441
- const result = await importSessions(client, { all, verbose, dryRun, replay });
442
- if (dryRun)
443
- console.log(" [dry-run] No changes written.\n");
444
- console.log(` ${result.imported} sessions imported (${result.totalMessages} messages)`);
445
- if (result.skippedEmpty > 0)
446
- console.log(` ${result.skippedEmpty} skipped (empty transcript)`);
447
- if (result.failed > 0)
448
- console.log(` ${result.failed} failed`);
449
- if (replay)
450
- console.log(" [replay] Sessions compacted sequentially with threaded context.\n");
451
541
  console.log();
452
- break;
453
542
  }
454
- case "promote": {
455
- if (argv.includes("--help") || argv.includes("-h")) {
456
- const { printHelp } = await import("../src/cli-help.js");
457
- printHelp("promote");
458
- exit(0);
543
+ });
544
+ connectorsCmd
545
+ .command("install <agent>")
546
+ .description("Install a connector for an agent")
547
+ .option("--type <type>", "Connector type: rules, mcp, or skill")
548
+ .helpOption(false)
549
+ .option("-h, --help", "Show help")
550
+ .action(async (agentName, opts) => {
551
+ if (opts.help) {
552
+ const { printHelp } = await import("../src/cli-help.js");
553
+ printHelp("connectors");
554
+ exit(0);
555
+ }
556
+ if (!agentName) {
557
+ console.error("Usage: lcm connectors install <agent> [--type rules|mcp|skill]");
558
+ exit(1);
559
+ }
560
+ const type = opts.type;
561
+ const { installConnector } = await import("../src/connectors/installer.js");
562
+ try {
563
+ const result = installConnector(agentName, type);
564
+ if (result.manual) {
565
+ console.log(`\n ${result.manual}\n`);
459
566
  }
460
- const all = argv.includes("--all");
461
- const verbose = argv.includes("--verbose");
462
- const dryRun = argv.includes("--dry-run");
463
- const { ensureDaemon } = await import("../src/daemon/lifecycle.js");
464
- const { loadDaemonConfig } = await import("../src/daemon/config.js");
465
- const { join } = await import("node:path");
466
- const { homedir } = await import("node:os");
467
- const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
468
- const port = config.daemon?.port ?? 3737;
469
- const pidFilePath = join(homedir(), ".lossless-claude", "daemon.pid");
470
- const { connected } = await ensureDaemon({ port, pidFilePath, spawnTimeoutMs: 5000 });
471
- if (!connected) {
472
- console.error(" Daemon not available. Start it with: lcm daemon start --detach");
473
- exit(1);
567
+ else {
568
+ console.log(`\n ✓ Installed ${type ?? "default"} connector for ${agentName}`);
569
+ console.log(` Path: ${result.path}`);
570
+ if (result.requiresRestart)
571
+ console.log(" Restart the agent to activate.");
572
+ console.log();
474
573
  }
475
- const baseUrl = `http://127.0.0.1:${port}`;
476
- const { readdirSync, existsSync, readFileSync } = await import("node:fs");
477
- if (dryRun)
478
- console.log(" [dry-run] No changes will be written.\n");
479
- // Collect project cwds to promote
480
- const cwds = [];
481
- if (all) {
482
- const projectsDir = join(homedir(), ".lossless-claude", "projects");
483
- if (existsSync(projectsDir)) {
484
- for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
485
- if (!entry.isDirectory())
486
- continue;
487
- const metaPath = join(projectsDir, entry.name, "meta.json");
488
- if (!existsSync(metaPath))
489
- continue;
490
- try {
491
- const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
492
- if (meta.cwd)
493
- cwds.push(meta.cwd);
494
- }
495
- catch { /* skip unreadable */ }
496
- }
497
- }
574
+ }
575
+ catch (err) {
576
+ console.error(` Error: ${err.message}`);
577
+ exit(1);
578
+ }
579
+ });
580
+ connectorsCmd
581
+ .command("remove <agent>")
582
+ .description("Remove a connector for an agent")
583
+ .option("--type <type>", "Connector type: rules, mcp, or skill")
584
+ .helpOption(false)
585
+ .option("-h, --help", "Show help")
586
+ .action(async (agentName, opts) => {
587
+ if (opts.help) {
588
+ const { printHelp } = await import("../src/cli-help.js");
589
+ printHelp("connectors");
590
+ exit(0);
591
+ }
592
+ if (!agentName) {
593
+ console.error("Usage: lcm connectors remove <agent> [--type rules|mcp|skill]");
594
+ exit(1);
595
+ }
596
+ const type = opts.type;
597
+ const { removeConnector } = await import("../src/connectors/installer.js");
598
+ try {
599
+ const removed = removeConnector(agentName, type);
600
+ if (removed) {
601
+ console.log(`\n ✓ Removed connector for ${agentName}\n`);
498
602
  }
499
603
  else {
500
- cwds.push(process.cwd());
604
+ console.log(`\n No connector found for ${agentName}\n`);
501
605
  }
502
- let totalProcessed = 0;
503
- let totalPromoted = 0;
504
- for (const cwd of cwds) {
505
- const res = await fetch(`${baseUrl}/promote`, {
506
- method: "POST",
507
- headers: { "Content-Type": "application/json" },
508
- body: JSON.stringify({ cwd, dry_run: dryRun }),
509
- });
510
- if (!res.ok) {
511
- if (verbose)
512
- console.error(` promote failed for ${cwd}: ${res.status}`);
513
- continue;
606
+ }
607
+ catch (err) {
608
+ console.error(` Error: ${err.message}`);
609
+ exit(1);
610
+ }
611
+ });
612
+ connectorsCmd
613
+ .command("doctor [agent]")
614
+ .description("Check connector health")
615
+ .helpOption(false)
616
+ .option("-h, --help", "Show help")
617
+ .action(async (agentName, opts) => {
618
+ if (opts.help) {
619
+ const { printHelp } = await import("../src/cli-help.js");
620
+ printHelp("connectors");
621
+ exit(0);
622
+ }
623
+ const { AGENTS } = await import("../src/connectors/registry.js");
624
+ const { listConnectors } = await import("../src/connectors/installer.js");
625
+ const { findAgent } = await import("../src/connectors/registry.js");
626
+ const found = agentName ? findAgent(agentName) : undefined;
627
+ const agents = found ? [found] : agentName ? [] : AGENTS;
628
+ if (agents.length === 0) {
629
+ console.error(` Unknown agent: ${agentName}`);
630
+ exit(1);
631
+ }
632
+ const installed = listConnectors();
633
+ console.log("\n Connector health:\n");
634
+ for (const agent of agents) {
635
+ const agentConnectors = installed.filter((c) => c.agentId === agent.id);
636
+ if (agentConnectors.length === 0) {
637
+ console.log(` ⚠ ${agent.name}: no connectors installed`);
638
+ }
639
+ else {
640
+ for (const c of agentConnectors) {
641
+ console.log(` ✓ ${agent.name}: ${c.type} at ${c.path}`);
514
642
  }
515
- const result = await res.json();
516
- totalProcessed += result.processed;
517
- totalPromoted += result.promoted;
518
- if (verbose) {
519
- console.log(` ${cwd}: ${result.processed} scanned, ${result.promoted} promoted`);
643
+ }
644
+ }
645
+ console.log();
646
+ });
647
+ program.addCommand(connectorsCmd);
648
+ // ─── sensitive ─────────────────────────────────────────────────────────────
649
+ program
650
+ .command("sensitive [args...]")
651
+ .description("Manage sensitive patterns for automatic redaction")
652
+ .helpOption(false)
653
+ .option("-h, --help", "Show help")
654
+ .allowUnknownOption(true)
655
+ .action(async (args, opts) => {
656
+ if (opts.help) {
657
+ const { printHelp } = await import("../src/cli-help.js");
658
+ printHelp("sensitive");
659
+ exit(0);
660
+ }
661
+ const { handleSensitive } = await import("../src/sensitive.js");
662
+ const { join } = await import("node:path");
663
+ const { homedir } = await import("node:os");
664
+ const configPath = join(homedir(), ".lossless-claude", "config.json");
665
+ const r = await handleSensitive(args, process.cwd(), configPath);
666
+ if (r.stdout)
667
+ stdout.write(r.stdout);
668
+ exit(r.exitCode);
669
+ });
670
+ // ─── import ────────────────────────────────────────────────────────────────
671
+ program
672
+ .command("import")
673
+ .description("Import Claude Code session transcripts into lossless memory")
674
+ .option("--all", "Import all projects")
675
+ .option("--verbose", "Show per-session import detail")
676
+ .option("--dry-run", "Preview without importing")
677
+ .option("--replay", "Replay compaction for each imported session")
678
+ .helpOption(false)
679
+ .option("-h, --help", "Show help")
680
+ .action(async (opts) => {
681
+ if (opts.help) {
682
+ const { printHelp } = await import("../src/cli-help.js");
683
+ printHelp("import");
684
+ exit(0);
685
+ }
686
+ const all = opts.all ?? false;
687
+ const verbose = opts.verbose ?? false;
688
+ const dryRun = opts.dryRun ?? false;
689
+ const replay = opts.replay ?? false;
690
+ const { ensureDaemon } = await import("../src/daemon/lifecycle.js");
691
+ const { DaemonClient } = await import("../src/daemon/client.js");
692
+ const { loadDaemonConfig } = await import("../src/daemon/config.js");
693
+ const { NinjaRenderer } = await import("../src/cli/pipeline-runner.js");
694
+ const { makeProgressState } = await import("../src/cli/progress-state.js");
695
+ const { join } = await import("node:path");
696
+ const { homedir } = await import("node:os");
697
+ const { existsSync, readdirSync } = await import("node:fs");
698
+ const { importSessions, cwdToProjectHash, findSessionFiles } = await import("../src/import.js");
699
+ // --codex is a shorthand for --provider codex
700
+ let provider = "claude";
701
+ if (opts.codex) {
702
+ provider = "codex";
703
+ }
704
+ else if (opts.provider) {
705
+ const provVal = opts.provider;
706
+ if (provVal === "claude" || provVal === "codex" || provVal === "all") {
707
+ provider = provVal;
708
+ }
709
+ else {
710
+ console.error(` Unknown provider "${provVal}". Use: claude, codex, all`);
711
+ exit(1);
712
+ }
713
+ }
714
+ const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
715
+ const port = config.daemon?.port ?? 3737;
716
+ const pidFilePath = join(homedir(), ".lossless-claude", "daemon.pid");
717
+ const { connected } = await ensureDaemon({ port, pidFilePath, spawnTimeoutMs: 5000 });
718
+ if (!connected) {
719
+ console.error(" Daemon not available");
720
+ exit(1);
721
+ }
722
+ // Pre-scan for session count (enables accurate live progress bar)
723
+ const claudeProjectsDir = join(homedir(), ".claude", "projects");
724
+ let sessionCount = 0;
725
+ if (all) {
726
+ if (existsSync(claudeProjectsDir)) {
727
+ for (const entry of readdirSync(claudeProjectsDir, { withFileTypes: true })) {
728
+ if (!entry.isDirectory())
729
+ continue;
730
+ sessionCount += findSessionFiles(join(claudeProjectsDir, entry.name)).length;
520
731
  }
521
732
  }
522
- console.log(` ${totalPromoted} insight${totalPromoted !== 1 ? "s" : ""} promoted to long-term memory`);
523
- if (verbose)
524
- console.log(` (${totalProcessed} summaries scanned across ${cwds.length} project${cwds.length !== 1 ? "s" : ""})`);
733
+ }
734
+ else {
735
+ const cwd = process.cwd();
736
+ const hash = cwdToProjectHash(cwd);
737
+ const dir = join(claudeProjectsDir, hash);
738
+ if (existsSync(dir))
739
+ sessionCount = findSessionFiles(dir).length;
740
+ }
741
+ const isTTY = process.stdout.isTTY ?? false;
742
+ const renderOpts = { isTTY, width: process.stdout.columns ?? 80, color: isTTY, verbose };
743
+ const state = makeProgressState({
744
+ phases: [{ name: "Import", status: "active" }],
745
+ total: sessionCount,
746
+ dryRun,
747
+ });
748
+ const renderer = new NinjaRenderer({ state, renderOpts });
749
+ const providerLabel = provider === "codex" ? "Codex CLI" :
750
+ provider === "all" ? "Claude Code + Codex CLI" :
751
+ "Claude Code";
752
+ console.log(`\n Importing ${providerLabel} sessions${all ? " (all projects)" : ""}...\n`);
753
+ renderer.start();
754
+ const client = new DaemonClient(`http://127.0.0.1:${port}`);
755
+ const result = await importSessions(client, {
756
+ all, verbose, dryRun, replay, provider,
757
+ onProgress: (patch) => {
758
+ Object.assign(state, patch);
759
+ if (patch.lastResult)
760
+ renderer.sessionDone();
761
+ },
762
+ });
763
+ renderer.stop();
764
+ if (isTTY && !verbose) {
765
+ state.phases[0].status = "done";
766
+ renderer.printSummary();
767
+ }
768
+ else {
769
+ const { printImportSummary } = await import("../src/import-summary.js");
525
770
  if (dryRun)
526
- console.log(" [dry-run] No changes written.");
771
+ console.log(" [dry-run] No changes written.\n");
772
+ printImportSummary(result, { replay });
527
773
  console.log();
528
- break;
529
774
  }
530
- default: {
775
+ });
776
+ // ─── promote ───────────────────────────────────────────────────────────────
777
+ program
778
+ .command("promote")
779
+ .description("Scan summaries and promote durable insights to long-term memory")
780
+ .option("--all", "Promote across all tracked projects")
781
+ .option("--verbose", "Show per-project counts")
782
+ .option("--dry-run", "Preview promotions without writing")
783
+ .helpOption(false)
784
+ .option("-h, --help", "Show help")
785
+ .action(async (opts) => {
786
+ if (opts.help) {
531
787
  const { printHelp } = await import("../src/cli-help.js");
532
- if (command) {
533
- process.stderr.write(`lcm: unknown command '${command}'\n\n`);
788
+ printHelp("promote");
789
+ exit(0);
790
+ }
791
+ const all = opts.all ?? false;
792
+ const verbose = opts.verbose ?? false;
793
+ const dryRun = opts.dryRun ?? false;
794
+ const { ensureDaemon } = await import("../src/daemon/lifecycle.js");
795
+ const { loadDaemonConfig } = await import("../src/daemon/config.js");
796
+ const { join } = await import("node:path");
797
+ const { homedir } = await import("node:os");
798
+ const config = loadDaemonConfig(join(homedir(), ".lossless-claude", "config.json"));
799
+ const port = config.daemon?.port ?? 3737;
800
+ const pidFilePath = join(homedir(), ".lossless-claude", "daemon.pid");
801
+ const { connected } = await ensureDaemon({ port, pidFilePath, spawnTimeoutMs: 5000 });
802
+ if (!connected) {
803
+ console.error(" Daemon not available. Start it with: lcm daemon start --detach");
804
+ exit(1);
805
+ }
806
+ const baseUrl = `http://127.0.0.1:${port}`;
807
+ const { readdirSync, existsSync, readFileSync } = await import("node:fs");
808
+ if (dryRun)
809
+ console.log(" [dry-run] No changes will be written.\n");
810
+ // Collect project cwds to promote
811
+ const cwds = [];
812
+ if (all) {
813
+ const projectsDir = join(homedir(), ".lossless-claude", "projects");
814
+ if (existsSync(projectsDir)) {
815
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
816
+ if (!entry.isDirectory())
817
+ continue;
818
+ const metaPath = join(projectsDir, entry.name, "meta.json");
819
+ if (!existsSync(metaPath))
820
+ continue;
821
+ try {
822
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
823
+ if (meta.cwd)
824
+ cwds.push(meta.cwd);
825
+ }
826
+ catch { /* skip unreadable */ }
827
+ }
534
828
  }
535
- printHelp();
536
- exit(command ? 1 : 0);
537
829
  }
830
+ else {
831
+ cwds.push(process.cwd());
832
+ }
833
+ let totalProcessed = 0;
834
+ let totalPromoted = 0;
835
+ const total = cwds.length;
836
+ for (let i = 0; i < cwds.length; i++) {
837
+ const cwd = cwds[i];
838
+ if (total > 1) {
839
+ process.stdout.write(`\r scanning project ${i + 1}/${total}...`);
840
+ }
841
+ else {
842
+ process.stdout.write(`\r scanning...`);
843
+ }
844
+ const res = await fetch(`${baseUrl}/promote`, {
845
+ method: "POST",
846
+ headers: { "Content-Type": "application/json" },
847
+ body: JSON.stringify({ cwd, dry_run: dryRun }),
848
+ });
849
+ if (!res.ok) {
850
+ if (verbose)
851
+ console.error(` promote failed for ${cwd}: ${res.status}`);
852
+ continue;
853
+ }
854
+ const result = await res.json();
855
+ totalProcessed += result.processed;
856
+ totalPromoted += result.promoted;
857
+ if (verbose) {
858
+ process.stdout.write("\r");
859
+ const convLabel = result.conversations !== undefined ? `, ${result.conversations} conversation${result.conversations !== 1 ? "s" : ""}` : "";
860
+ console.log(` ${cwd}: ${result.processed} scanned${convLabel}, ${result.promoted} promoted`);
861
+ }
862
+ }
863
+ // Clear the progress line
864
+ process.stdout.write("\r \r");
865
+ if (totalPromoted === 0) {
866
+ console.log(" Nothing to promote — no new insights found.");
867
+ }
868
+ else {
869
+ console.log(` ${totalPromoted} insight${totalPromoted !== 1 ? "s" : ""} promoted to long-term memory`);
870
+ }
871
+ if (verbose)
872
+ console.log(` (${totalProcessed} summaries scanned across ${cwds.length} project${cwds.length !== 1 ? "s" : ""})`);
873
+ if (dryRun)
874
+ console.log(" [dry-run] No changes written.");
875
+ console.log();
876
+ });
877
+ // ─── Unknown command fallback ──────────────────────────────────────────────
878
+ program.on("command:*", async (operands) => {
879
+ process.stderr.write(`lcm: unknown command '${operands[0]}'\n\n`);
880
+ const { printHelp } = await import("../src/cli-help.js");
881
+ printHelp();
882
+ exit(1);
883
+ });
884
+ // Handle root-level help and no-args before Commander parses — this prevents
885
+ // Commander from seeing --help at the root level and intercepting it before
886
+ // dispatching to subcommands (lcm import --help would otherwise show root help).
887
+ if (argv.length <= 2 || (argv.length === 3 && (argv[2] === "-h" || argv[2] === "--help"))) {
888
+ const { printHelp } = await import("../src/cli-help.js");
889
+ printHelp();
890
+ exit(0);
538
891
  }
892
+ await program.parseAsync(argv);
539
893
  }
540
894
  main().catch((err) => { console.error(err); exit(1); });
541
895
  //# sourceMappingURL=lcm.js.map