@rohirik/openltm-core 2.8.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 (97) hide show
  1. package/README.md +67 -0
  2. package/assets/opencode/agents/aegis.md +211 -0
  3. package/assets/opencode/plugins/aegis.ts +3 -0
  4. package/assets/opencode/skills/AgentTrustBoundaries/ContextCrushDefense.md +104 -0
  5. package/assets/opencode/skills/AgentTrustBoundaries/SKILL.md +31 -0
  6. package/assets/opencode/skills/AgentTrustBoundaries/TrustBoundaryPatterns.md +114 -0
  7. package/assets/opencode/skills/AgentTrustBoundaries/Workflows/DefendContextCrush.md +27 -0
  8. package/assets/opencode/skills/AgentTrustBoundaries/Workflows/HandleUntrustedContent.md +27 -0
  9. package/assets/opencode/skills/CommandPathSafety/CommandInjectionPatterns.md +95 -0
  10. package/assets/opencode/skills/CommandPathSafety/PathTraversalAndInstallerSafety.md +106 -0
  11. package/assets/opencode/skills/CommandPathSafety/SKILL.md +31 -0
  12. package/assets/opencode/skills/CommandPathSafety/Workflows/EnforcePathBoundaries.md +27 -0
  13. package/assets/opencode/skills/CommandPathSafety/Workflows/HardenCommandExecution.md +27 -0
  14. package/assets/opencode/skills/SecretSafeHandling/CloudCredentialPatterns.md +106 -0
  15. package/assets/opencode/skills/SecretSafeHandling/SKILL.md +31 -0
  16. package/assets/opencode/skills/SecretSafeHandling/SecretHandlingPlaybook.md +102 -0
  17. package/assets/opencode/skills/SecretSafeHandling/Workflows/DesignSecretSafeFlow.md +27 -0
  18. package/assets/opencode/skills/SecretSafeHandling/Workflows/RemoveSecretExposure.md +27 -0
  19. package/package.json +41 -0
  20. package/src/__tests__/cli/claude.test.ts +122 -0
  21. package/src/__tests__/cli/detect.test.ts +91 -0
  22. package/src/__tests__/cli/install.test.ts +161 -0
  23. package/src/__tests__/cli/opencode.test.ts +169 -0
  24. package/src/__tests__/cli/pi.test.ts +113 -0
  25. package/src/__tests__/cli.test.ts +70 -0
  26. package/src/__tests__/events/crossProcess.test.ts +82 -0
  27. package/src/__tests__/events/index.test.ts +32 -0
  28. package/src/__tests__/extensions.test.ts +81 -0
  29. package/src/__tests__/migrations/retention.test.ts +118 -0
  30. package/src/__tests__/queue/index.test.ts +61 -0
  31. package/src/__tests__/scheduler/index.test.ts +39 -0
  32. package/src/__tests__/vec/index.test.ts +130 -0
  33. package/src/__tests__/vec/parity.test.ts +70 -0
  34. package/src/adapterTypes.ts +23 -0
  35. package/src/cli/_shared.ts +120 -0
  36. package/src/cli/bin.ts +97 -0
  37. package/src/cli/claude.ts +124 -0
  38. package/src/cli/detect.ts +55 -0
  39. package/src/cli/hook.ts +25 -0
  40. package/src/cli/index.ts +22 -0
  41. package/src/cli/install.ts +185 -0
  42. package/src/cli/opencode.ts +193 -0
  43. package/src/cli/pi.ts +74 -0
  44. package/src/cli/types.ts +78 -0
  45. package/src/config.ts +163 -0
  46. package/src/context.ts +172 -0
  47. package/src/dao/conflicts.ts +26 -0
  48. package/src/dao/contextItems.ts +70 -0
  49. package/src/dao/embeddings.ts +78 -0
  50. package/src/dao/index.ts +9 -0
  51. package/src/dao/provenanceAudit.ts +108 -0
  52. package/src/dao/types.ts +142 -0
  53. package/src/db.ts +780 -0
  54. package/src/dedup.ts +12 -0
  55. package/src/embeddings.ts +386 -0
  56. package/src/events/index.ts +130 -0
  57. package/src/extensions.ts +140 -0
  58. package/src/graph.ts +268 -0
  59. package/src/index.ts +95 -0
  60. package/src/janitor/archive.ts +66 -0
  61. package/src/janitor/decay.ts +60 -0
  62. package/src/janitor/dedup.ts +333 -0
  63. package/src/janitor/embeddings.ts +209 -0
  64. package/src/janitor/index.ts +215 -0
  65. package/src/janitor/promote.ts +188 -0
  66. package/src/janitor/providers/anthropic.ts +91 -0
  67. package/src/janitor/providers/cohere.ts +135 -0
  68. package/src/janitor/providers/gemini.ts +156 -0
  69. package/src/janitor/providers/ollama.ts +177 -0
  70. package/src/janitor/providers/openai.ts +121 -0
  71. package/src/janitor/providers/openrouter.ts +182 -0
  72. package/src/janitor/providers/types.ts +154 -0
  73. package/src/janitor/providers/utils.ts +35 -0
  74. package/src/janitor/supersedes.ts +199 -0
  75. package/src/lib/honker.ts +54 -0
  76. package/src/lib/honkerTypes.ts +109 -0
  77. package/src/lib/jsonlLogger.ts +92 -0
  78. package/src/lib/writeQueue.ts +28 -0
  79. package/src/migrations.ts +415 -0
  80. package/src/paths.ts +22 -0
  81. package/src/proposals.ts +120 -0
  82. package/src/providers/disabled.ts +19 -0
  83. package/src/providers/embeddingProvider.ts +49 -0
  84. package/src/providers/gemini.ts +37 -0
  85. package/src/providers/index.ts +2 -0
  86. package/src/providers/ollama.ts +43 -0
  87. package/src/providers/openai.ts +35 -0
  88. package/src/queue/index.ts +53 -0
  89. package/src/queue/worker.ts +77 -0
  90. package/src/recall/categorise.ts +139 -0
  91. package/src/recall/explainer.ts +76 -0
  92. package/src/scheduler/index.ts +97 -0
  93. package/src/schema.sql +191 -0
  94. package/src/secretsScrubber.ts +105 -0
  95. package/src/shared-db.ts +158 -0
  96. package/src/vec/index.ts +161 -0
  97. package/tsconfig.json +9 -0
package/src/cli/bin.ts ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * cli/bin.ts — ltm CLI entrypoint.
4
+ *
5
+ * Usage:
6
+ * bunx @rohirik/openltm-core # auto-detect agents
7
+ * bunx @rohirik/openltm-core --claude # Claude Code only
8
+ * bunx @rohirik/openltm-core --opencode # OpenCode only
9
+ * bunx @rohirik/openltm-core --pi # Pi only
10
+ * bunx @rohirik/openltm-core --dry-run --claude # preview without writing
11
+ *
12
+ * bunx @rohirik/openltm-core hook --name <hookName> # lifecycle hook stub
13
+ * bunx @rohirik/openltm-core mcp-serve # MCP server (future)
14
+ */
15
+ import { runInstallCli } from "./install.js";
16
+ import { runHook } from "./hook.js";
17
+
18
+ function printHelp(): void {
19
+ process.stdout.write(
20
+ [
21
+ "",
22
+ " bunx @rohirik/openltm-core [options]",
23
+ "",
24
+ " Options:",
25
+ " --claude Install into Claude Code",
26
+ " --opencode Install into OpenCode",
27
+ " --pi Install into Pi",
28
+ " --dry-run Preview without writing files",
29
+ " --help, -h Show this help",
30
+ "",
31
+ " Sub-commands:",
32
+ " hook --name <event> Lifecycle hook stub (for Claude Code hook wiring)",
33
+ " mcp-serve Start the LTM MCP server",
34
+ "",
35
+ " If no target flags are given, agents are auto-detected.",
36
+ "",
37
+ ].join("\n"),
38
+ );
39
+ }
40
+
41
+ async function main(): Promise<void> {
42
+ const argv = process.argv.slice(2);
43
+
44
+ if (argv[0] === "--help" || argv[0] === "-h") {
45
+ printHelp();
46
+ process.exit(0);
47
+ }
48
+
49
+ // Sub-command: hook
50
+ if (argv[0] === "hook") {
51
+ const nameIdx = argv.indexOf("--name");
52
+ const hookName = nameIdx !== -1 ? (argv[nameIdx + 1] ?? "") : "";
53
+ if (!hookName) {
54
+ process.stderr.write(" ltm hook: missing --name argument\n");
55
+ process.exit(1);
56
+ }
57
+ runHook(hookName);
58
+ return;
59
+ }
60
+
61
+ // Sub-command: mcp-serve (stub — full implementation deferred)
62
+ if (argv[0] === "mcp-serve") {
63
+ process.stderr.write(
64
+ " ltm mcp-serve: not yet implemented — install the Claude Code plugin for MCP server support\n",
65
+ );
66
+ process.exit(0);
67
+ }
68
+
69
+ // Parse installer flags
70
+ let claude = false;
71
+ let opencode = false;
72
+ let pi = false;
73
+ let dryRun = false;
74
+
75
+ for (const arg of argv) {
76
+ if (arg === "--claude") claude = true;
77
+ else if (arg === "--opencode") opencode = true;
78
+ else if (arg === "--pi") pi = true;
79
+ else if (arg === "--dry-run") dryRun = true;
80
+ else if (arg === "--help" || arg === "-h") {
81
+ printHelp();
82
+ process.exit(0);
83
+ }
84
+ }
85
+
86
+ const result = await runInstallCli({
87
+ targets: { claude, opencode, pi },
88
+ dryRun,
89
+ });
90
+
91
+ process.exit(result.exitCode);
92
+ }
93
+
94
+ main().catch((err: unknown) => {
95
+ process.stderr.write(`\n ltm fatal: ${String(err)}\n\n`);
96
+ process.exit(1);
97
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * cli/claude.ts — Installer for Claude Code.
3
+ *
4
+ * Writes (or patches) ~/.claude/settings.json to add the LTM MCP server
5
+ * entry and three lifecycle hooks. Idempotent — safe to run multiple times.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import os from "os";
10
+ import { mergeMcpEntry, wireHooks } from "./_shared.js";
11
+ import type { ClaudeSettings, McpServerEntry } from "./_shared.js";
12
+ import type { InstallResult } from "./types.js";
13
+
14
+ // ── Constants ─────────────────────────────────────────────────────────────────
15
+
16
+ const MCP_NAME = "openltm";
17
+ const MCP_ENTRY: McpServerEntry = {
18
+ command: "bunx",
19
+ args: ["@rohirik/openltm-core", "mcp-serve"],
20
+ env: { LTM_DB_PATH: "~/.claude/plugins/data/OpenLtm-openltm/openltm.db" },
21
+ };
22
+
23
+ const HOOKS: Array<{ event: string; args: string[] }> = [
24
+ {
25
+ event: "SessionStart",
26
+ args: ["@rohirik/openltm-core", "hook", "--name", "SessionStart"],
27
+ },
28
+ {
29
+ event: "PreCompact",
30
+ args: ["@rohirik/openltm-core", "hook", "--name", "PreCompact"],
31
+ },
32
+ {
33
+ event: "PostEditCheck",
34
+ args: ["@rohirik/openltm-core", "hook", "--name", "PostEditCheck"],
35
+ },
36
+ ];
37
+
38
+ // ── Helpers ───────────────────────────────────────────────────────────────────
39
+
40
+ function settingsPath(homedir: string): string {
41
+ return join(homedir, ".claude", "settings.json");
42
+ }
43
+
44
+ function readSettings(path: string): ClaudeSettings {
45
+ if (!existsSync(path)) return {};
46
+ try {
47
+ return JSON.parse(readFileSync(path, "utf8")) as ClaudeSettings;
48
+ } catch {
49
+ // Malformed JSON → start fresh
50
+ return {};
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Check if the settings already have the LTM MCP entry AND all 3 hooks wired.
56
+ * Used for early-exit idempotency.
57
+ */
58
+ function isAlreadyInstalled(settings: ClaudeSettings): boolean {
59
+ // Check MCP entry
60
+ const entry = settings.mcpServers?.[MCP_NAME];
61
+ if (!entry || entry.command !== MCP_ENTRY.command) return false;
62
+
63
+ // Check each hook
64
+ for (const hook of HOOKS) {
65
+ const hookList = (settings.hooks as Record<string, unknown> | undefined)?.[hook.event];
66
+ if (!Array.isArray(hookList)) return false;
67
+ const present = hookList.some(
68
+ (h): boolean =>
69
+ typeof h === "object" &&
70
+ h !== null &&
71
+ (h as { command?: string }).command === "bunx" &&
72
+ Array.isArray((h as { args?: unknown[] }).args) &&
73
+ (h as { args: unknown[] }).args[0] === hook.args[0],
74
+ );
75
+ if (!present) return false;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ // ── Public API ────────────────────────────────────────────────────────────────
81
+
82
+ /**
83
+ * installClaude — write or patch ~/.claude/settings.json.
84
+ *
85
+ * @param opts.homedir - Override home directory (useful for tests).
86
+ * @param opts.dryRun - Compute result without writing any files.
87
+ * @returns InstallResult describing what happened.
88
+ */
89
+ export async function installClaude(opts: {
90
+ homedir?: string;
91
+ dryRun?: boolean;
92
+ }): Promise<InstallResult> {
93
+ const homedir = opts.homedir ?? os.homedir();
94
+ const dryRun = opts.dryRun ?? false;
95
+ const path = settingsPath(homedir);
96
+
97
+ const original = readSettings(path);
98
+
99
+ if (isAlreadyInstalled(original)) {
100
+ return { target: "claude", status: "skipped", detail: "already configured" };
101
+ }
102
+
103
+ // Apply MCP entry
104
+ let settings = mergeMcpEntry(original, MCP_NAME, MCP_ENTRY);
105
+
106
+ // Apply all 3 hooks
107
+ for (const hook of HOOKS) {
108
+ settings = wireHooks(settings, hook.event, "bunx", hook.args);
109
+ }
110
+
111
+ if (!dryRun) {
112
+ const dir = dirname(path);
113
+ if (!existsSync(dir)) {
114
+ mkdirSync(dir, { recursive: true });
115
+ }
116
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
117
+ }
118
+
119
+ return {
120
+ target: "claude",
121
+ status: "installed",
122
+ detail: dryRun ? "dry-run — no files written" : path,
123
+ };
124
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * cli/detect.ts — Agent detection for the LTM installer.
3
+ *
4
+ * Probes the filesystem for known AI coding agent installations so the
5
+ * installer can auto-select targets without requiring explicit --claude /
6
+ * --opencode / --pi flags.
7
+ *
8
+ * Pure function — accepts a homedir argument so tests can supply a tmp dir.
9
+ */
10
+ import { existsSync } from "fs";
11
+ import { join } from "path";
12
+ import os from "os";
13
+ import type { DetectResult } from "./types.js";
14
+
15
+ /**
16
+ * Probe for an OpenCode config directory using the standard path resolution
17
+ * order: $XDG_CONFIG_HOME/opencode → ~/.config/opencode → (darwin only)
18
+ * ~/Library/Application Support/opencode.
19
+ */
20
+ function probeOpenCode(homedir: string): boolean {
21
+ const xdg = process.env["XDG_CONFIG_HOME"];
22
+ if (xdg && existsSync(join(xdg, "opencode"))) return true;
23
+ if (existsSync(join(homedir, ".config", "opencode"))) return true;
24
+ if (process.platform === "darwin") {
25
+ if (existsSync(join(homedir, "Library", "Application Support", "opencode"))) return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /**
31
+ * Probe for a Pi config using the three known locations:
32
+ * ~/.pi/ | ~/pi.toml | ~/.pi/config.toml
33
+ */
34
+ function probePi(homedir: string): boolean {
35
+ return (
36
+ existsSync(join(homedir, ".pi")) ||
37
+ existsSync(join(homedir, "pi.toml")) ||
38
+ existsSync(join(homedir, ".pi", "config.toml"))
39
+ );
40
+ }
41
+
42
+ /**
43
+ * detectAgents — inspect the filesystem to determine which AI coding agents
44
+ * are installed in the given home directory.
45
+ *
46
+ * @param homedir - Defaults to `os.homedir()`. Override in tests.
47
+ * @returns DetectResult with boolean flags for each supported agent.
48
+ */
49
+ export function detectAgents(homedir: string = os.homedir()): DetectResult {
50
+ return {
51
+ claude: existsSync(join(homedir, ".claude")),
52
+ opencode: probeOpenCode(homedir),
53
+ pi: probePi(homedir),
54
+ };
55
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * cli/hook.ts — Hook dispatcher stub.
3
+ *
4
+ * Full hook logic lives in the Claude Code plugin (hooks/src/).
5
+ * When invoked via `bunx @rohirik/openltm-core hook --name <hookName>`, this
6
+ * stub exits cleanly (exit 0) so Claude Code does not treat the absent hook
7
+ * runner as an error.
8
+ *
9
+ * A future phase can wire actual hook logic here once openltm-core ships the
10
+ * compiled hook handlers.
11
+ */
12
+
13
+ /**
14
+ * runHook — stub dispatcher for LTM lifecycle hooks.
15
+ *
16
+ * @param name - Hook event name (e.g. "SessionStart", "PreCompact").
17
+ */
18
+ export function runHook(name: string): void {
19
+ // Stub: full hook logic requires the Claude Code plugin.
20
+ // Exit 0 so Claude Code does not break when the hook is registered.
21
+ process.stderr.write(
22
+ `LTM: hook '${name}' via bunx not yet supported — install the Claude Code plugin for full hook support\n`,
23
+ );
24
+ process.exit(0);
25
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * cli/index.ts — Public barrel for the @rohirik/openltm-core/cli sub-path export.
3
+ *
4
+ * Consumers that want to build on top of the LTM CLI primitives (e.g. a
5
+ * wrapper script or an e2e test harness) import from here instead of reaching
6
+ * into internal modules.
7
+ */
8
+ export { runInstall, runInstallCli } from "./install.js";
9
+ export type { CliRunOpts, CliRunResult } from "./install.js";
10
+ export type {
11
+ CliInstallOptions,
12
+ CliInstallResult,
13
+ InstallStep,
14
+ InstallTargetId,
15
+ InstallResult,
16
+ DetectResult,
17
+ } from "./types.js";
18
+ export { InstallTarget } from "./types.js";
19
+ export { detectAgents } from "./detect.js";
20
+ export { installClaude } from "./claude.js";
21
+ export { installOpenCode } from "./opencode.js";
22
+ export { installPi } from "./pi.js";
@@ -0,0 +1,185 @@
1
+ /**
2
+ * cli/install.ts — Install orchestrator.
3
+ *
4
+ * Coordinates detection → installation → UX rendering across all supported
5
+ * target agents (Claude Code, OpenCode, Pi). Uses @clack/prompts for output.
6
+ *
7
+ * The module exports two surfaces:
8
+ * - `runInstall(opts)` — programmatic API (used by bin.ts and tests)
9
+ * - Legacy `CliInstallResult` shape preserved for backward compatibility with
10
+ * existing tests in src/__tests__/cli.test.ts
11
+ */
12
+ import * as clack from "@clack/prompts";
13
+ import { detectAgents } from "./detect.js";
14
+ import { installClaude } from "./claude.js";
15
+ import { installOpenCode } from "./opencode.js";
16
+ import { installPi } from "./pi.js";
17
+ import type { CliInstallOptions, CliInstallResult, InstallStep, InstallResult } from "./types.js";
18
+ import { InstallTarget } from "./types.js";
19
+
20
+ // ── Known targets set (legacy validation) ────────────────────────────────────
21
+
22
+ const KNOWN_TARGETS = new Set<string>(Object.values(InstallTarget));
23
+
24
+ // ── Legacy runInstall (backward compatible) ───────────────────────────────────
25
+
26
+ /**
27
+ * runInstall — legacy programmatic API.
28
+ *
29
+ * Retained for backward compatibility with P6-0 tests in cli.test.ts.
30
+ * New code should use `runInstallCli` or call individual installers directly.
31
+ */
32
+ export async function runInstall(opts: CliInstallOptions): Promise<CliInstallResult> {
33
+ const { target, dryRun = false, nonInteractive = false } = opts;
34
+ const steps: InstallStep[] = [];
35
+
36
+ if (!KNOWN_TARGETS.has(target)) {
37
+ return {
38
+ success: false,
39
+ message: `Unknown target: "${target}". Valid targets: ${[...KNOWN_TARGETS].join(", ")}`,
40
+ steps,
41
+ };
42
+ }
43
+
44
+ steps.push({ label: `Validate target: ${target}`, status: "done" });
45
+ steps.push({
46
+ label: "Detect plugin data directory",
47
+ status: dryRun ? "skipped" : "done",
48
+ detail: dryRun ? "dry-run mode — no filesystem writes" : opts.pluginDataDir ?? "(auto-detected)",
49
+ });
50
+ steps.push({
51
+ label: `Wire ${target} configuration`,
52
+ status: dryRun ? "skipped" : "done",
53
+ detail: dryRun ? "dry-run: would patch host config file" : undefined,
54
+ });
55
+
56
+ if (!nonInteractive && !dryRun) {
57
+ steps.push({
58
+ label: "Run onboarding wizard",
59
+ status: "skipped",
60
+ detail: "interactive mode — wizard deferred to ltm onboard",
61
+ });
62
+ }
63
+
64
+ const mode = dryRun ? " (dry-run)" : "";
65
+ return {
66
+ success: true,
67
+ message: `LTM install for "${target}" completed${mode}. Run "ltm onboard" to finish setup.`,
68
+ steps,
69
+ };
70
+ }
71
+
72
+ // ── CLI orchestration ─────────────────────────────────────────────────────────
73
+
74
+ export interface CliRunOpts {
75
+ /** Explicit target flags from the command line. */
76
+ targets: {
77
+ claude: boolean;
78
+ opencode: boolean;
79
+ pi: boolean;
80
+ };
81
+ dryRun: boolean;
82
+ homedir?: string;
83
+ /** When true, suppress @clack output (used in tests). */
84
+ silent?: boolean;
85
+ }
86
+
87
+ export interface CliRunResult {
88
+ exitCode: number;
89
+ results: InstallResult[];
90
+ }
91
+
92
+ /**
93
+ * runInstallCli — main orchestrator for the `bunx @rohirik/openltm-core` flow.
94
+ *
95
+ * 1. If no target flags → call detectAgents()
96
+ * 2. If nothing found and no flags → exit 2
97
+ * 3. For each target call the installer and render @clack output
98
+ */
99
+ export async function runInstallCli(opts: CliRunOpts): Promise<CliRunResult> {
100
+ const { dryRun, homedir, silent = false } = opts;
101
+
102
+ // Resolve targets
103
+ let targets = { ...opts.targets };
104
+ const hasExplicit = targets.claude || targets.opencode || targets.pi;
105
+
106
+ if (!hasExplicit) {
107
+ const detected = detectAgents(homedir);
108
+ targets = detected;
109
+ }
110
+
111
+ const anyTarget = targets.claude || targets.opencode || targets.pi;
112
+ if (!anyTarget) {
113
+ if (!silent) {
114
+ clack.log.warn(
115
+ "No supported agents found. Use --claude, --opencode, or --pi to install manually.",
116
+ );
117
+ }
118
+ return { exitCode: 2, results: [] };
119
+ }
120
+
121
+ if (!silent) {
122
+ clack.intro("LTM Installer");
123
+ }
124
+
125
+ const results: InstallResult[] = [];
126
+
127
+ // Claude
128
+ if (targets.claude) {
129
+ const s = silent ? null : clack.spinner();
130
+ if (s) s.start("Installing into Claude Code…");
131
+ const r = await installClaude({ homedir, dryRun });
132
+ results.push(r);
133
+ if (s) {
134
+ if (r.status === "installed") {
135
+ s.stop(`Claude Code: installed${dryRun ? " (dry-run)" : ""}`);
136
+ } else if (r.status === "skipped") {
137
+ s.stop("Claude Code: already configured (skipped)");
138
+ } else {
139
+ s.stop(`Claude Code: error — ${r.detail ?? "unknown error"}`);
140
+ }
141
+ }
142
+ }
143
+
144
+ // OpenCode
145
+ if (targets.opencode) {
146
+ const s = silent ? null : clack.spinner();
147
+ if (s) s.start("Installing into OpenCode…");
148
+ const r = await installOpenCode({ homedir, dryRun });
149
+ results.push(r);
150
+ if (s) {
151
+ if (r.status === "installed") {
152
+ s.stop(`OpenCode: installed${dryRun ? " (dry-run)" : ""}`);
153
+ } else if (r.status === "skipped") {
154
+ s.stop("OpenCode: already configured (skipped)");
155
+ } else {
156
+ s.stop(`OpenCode: error — ${r.detail ?? "unknown error"}`);
157
+ }
158
+ }
159
+ }
160
+
161
+ // Pi
162
+ if (targets.pi) {
163
+ const s = silent ? null : clack.spinner();
164
+ if (s) s.start("Installing into Pi…");
165
+ const r = await installPi({ dryRun });
166
+ results.push(r);
167
+ if (s) {
168
+ if (r.status === "installed") {
169
+ s.stop(`Pi: installed${dryRun ? " (dry-run)" : ""}`);
170
+ } else if (r.status === "skipped") {
171
+ s.stop("Pi: already configured (skipped)");
172
+ } else {
173
+ s.stop(`Pi: error — ${r.detail ?? "unknown error"}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ const hasErrors = results.some((r) => r.status === "error");
179
+
180
+ if (!silent) {
181
+ clack.outro("Done. Shared DB: ~/.claude/plugins/data/OpenLtm-openltm/openltm.db");
182
+ }
183
+
184
+ return { exitCode: hasErrors ? 1 : 0, results };
185
+ }