@alecsibilia/luca 13.0.0-alpha.1

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 (128) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +47 -0
  3. package/bin/luca.js +3 -0
  4. package/dist/chunks/branch.mjs +47 -0
  5. package/dist/chunks/bun-runtime.mjs +46 -0
  6. package/dist/chunks/checks.mjs +53 -0
  7. package/dist/chunks/claim-verify.mjs +465 -0
  8. package/dist/chunks/classify.mjs +105 -0
  9. package/dist/chunks/confidence.mjs +199 -0
  10. package/dist/chunks/doctor.mjs +158 -0
  11. package/dist/chunks/hook.mjs +696 -0
  12. package/dist/chunks/init.mjs +715 -0
  13. package/dist/chunks/muninndb-health.mjs +66 -0
  14. package/dist/chunks/phase.mjs +38 -0
  15. package/dist/chunks/pr-review.mjs +122 -0
  16. package/dist/chunks/preferences.mjs +61 -0
  17. package/dist/chunks/repair.mjs +111 -0
  18. package/dist/chunks/repo.mjs +58 -0
  19. package/dist/chunks/retro.mjs +86 -0
  20. package/dist/chunks/roadmap.mjs +58 -0
  21. package/dist/chunks/rules.mjs +527 -0
  22. package/dist/chunks/stale-mcp-server.mjs +90 -0
  23. package/dist/chunks/state.mjs +57 -0
  24. package/dist/chunks/stray-local-install.mjs +200 -0
  25. package/dist/chunks/telemetry.mjs +165 -0
  26. package/dist/chunks/todo.mjs +151 -0
  27. package/dist/chunks/vault-init.mjs +300 -0
  28. package/dist/chunks/verification.mjs +95 -0
  29. package/dist/chunks/version.mjs +70 -0
  30. package/dist/chunks/workflow.mjs +47 -0
  31. package/dist/claude/.claude/agents/architect.md +410 -0
  32. package/dist/claude/.claude/agents/build.md +111 -0
  33. package/dist/claude/.claude/agents/discuss.md +93 -0
  34. package/dist/claude/.claude/agents/discussion.md +149 -0
  35. package/dist/claude/.claude/agents/execute.md +416 -0
  36. package/dist/claude/.claude/agents/executor.md +161 -0
  37. package/dist/claude/.claude/agents/fast.md +84 -0
  38. package/dist/claude/.claude/agents/finalize.md +484 -0
  39. package/dist/claude/.claude/agents/learner.md +160 -0
  40. package/dist/claude/.claude/agents/plan-reviewer.md +129 -0
  41. package/dist/claude/.claude/agents/plan.md +96 -0
  42. package/dist/claude/.claude/agents/research.md +327 -0
  43. package/dist/claude/.claude/agents/researcher.md +78 -0
  44. package/dist/claude/.claude/agents/review.md +283 -0
  45. package/dist/claude/.claude/agents/reviewer.md +163 -0
  46. package/dist/claude/.claude/agents/shadow-scanner.md +257 -0
  47. package/dist/claude/.claude/agents/triage.md +230 -0
  48. package/dist/claude/.claude/agents/verifier.md +131 -0
  49. package/dist/claude/.claude/commands/bug-diagnose.md +12 -0
  50. package/dist/claude/.claude/commands/gh-issue-triage.md +14 -0
  51. package/dist/claude/.claude/commands/gh-pr-address.md +235 -0
  52. package/dist/claude/.claude/commands/gh-prepare.md +12 -0
  53. package/dist/claude/.claude/commands/grill-me.md +12 -0
  54. package/dist/claude/.claude/commands/lu-review.md +51 -0
  55. package/dist/claude/.claude/commands/lu.md +75 -0
  56. package/dist/claude/.claude/commands/luca-init.md +14 -0
  57. package/dist/claude/.claude/commands/luca-telemetry-report.md +12 -0
  58. package/dist/claude/.claude/commands/memory-audit.md +12 -0
  59. package/dist/claude/.claude/commands/milestone-new.md +122 -0
  60. package/dist/claude/.claude/commands/phase-discuss.md +45 -0
  61. package/dist/claude/.claude/commands/phase-execute.md +39 -0
  62. package/dist/claude/.claude/commands/phase-plan.md +53 -0
  63. package/dist/claude/.claude/commands/repo-cleanup.md +80 -0
  64. package/dist/claude/.claude/commands/todo-add.md +28 -0
  65. package/dist/claude/.claude/commands/todo-check.md +36 -0
  66. package/dist/claude/.claude/hooks/context-refresher.ts +285 -0
  67. package/dist/claude/.claude/hooks/continuation-messages.ts +215 -0
  68. package/dist/claude/.claude/hooks/pipeline-guard.ts +182 -0
  69. package/dist/claude/.claude/settings.json +41 -0
  70. package/dist/claude/skills/arch-audit/SKILL.md +161 -0
  71. package/dist/claude/skills/autopilot/SKILL.md +1299 -0
  72. package/dist/claude/skills/bug-diagnose/SKILL.md +102 -0
  73. package/dist/claude/skills/choose/SKILL.md +124 -0
  74. package/dist/claude/skills/gh-issue-triage/SKILL.md +97 -0
  75. package/dist/claude/skills/gh-pr-address/SKILL.md +235 -0
  76. package/dist/claude/skills/gh-prepare/SKILL.md +209 -0
  77. package/dist/claude/skills/grill-me/SKILL.md +46 -0
  78. package/dist/claude/skills/lu/SKILL.md +112 -0
  79. package/dist/claude/skills/lu-review/SKILL.md +51 -0
  80. package/dist/claude/skills/luca-init/SKILL.md +91 -0
  81. package/dist/claude/skills/luca-telemetry-report/SKILL.md +145 -0
  82. package/dist/claude/skills/luca-write-surface/SKILL.md +213 -0
  83. package/dist/claude/skills/memory-audit/SKILL.md +217 -0
  84. package/dist/claude/skills/milestone-audit/SKILL.md +545 -0
  85. package/dist/claude/skills/milestone-complete/SKILL.md +168 -0
  86. package/dist/claude/skills/milestone-gaps/SKILL.md +60 -0
  87. package/dist/claude/skills/milestone-new/SKILL.md +125 -0
  88. package/dist/claude/skills/note/SKILL.md +162 -0
  89. package/dist/claude/skills/phase-add/SKILL.md +91 -0
  90. package/dist/claude/skills/phase-assumptions/SKILL.md +92 -0
  91. package/dist/claude/skills/phase-discuss/SKILL.md +165 -0
  92. package/dist/claude/skills/phase-execute/SKILL.md +1786 -0
  93. package/dist/claude/skills/phase-insert/SKILL.md +100 -0
  94. package/dist/claude/skills/phase-plan/SKILL.md +461 -0
  95. package/dist/claude/skills/phase-remove/SKILL.md +113 -0
  96. package/dist/claude/skills/phase-research/SKILL.md +80 -0
  97. package/dist/claude/skills/post-init-tour/SKILL.md +58 -0
  98. package/dist/claude/skills/progress/SKILL.md +271 -0
  99. package/dist/claude/skills/project-new/SKILL.md +609 -0
  100. package/dist/claude/skills/quick/SKILL.md +256 -0
  101. package/dist/claude/skills/rename-audit/SKILL.md +52 -0
  102. package/dist/claude/skills/repo-audit/SKILL.md +88 -0
  103. package/dist/claude/skills/repo-cleanup/SKILL.md +80 -0
  104. package/dist/claude/skills/seed-memory/SKILL.md +235 -0
  105. package/dist/claude/skills/session-pause/SKILL.md +126 -0
  106. package/dist/claude/skills/session-plan/SKILL.md +112 -0
  107. package/dist/claude/skills/session-resume/SKILL.md +75 -0
  108. package/dist/claude/skills/todo-add/SKILL.md +85 -0
  109. package/dist/claude/skills/todo-check/SKILL.md +77 -0
  110. package/dist/claude/skills/workflow-save/SKILL.md +277 -0
  111. package/dist/index.d.mts +33 -0
  112. package/dist/index.d.ts +33 -0
  113. package/dist/index.mjs +69 -0
  114. package/dist/shared/luca.B3Mimc0P.mjs +52 -0
  115. package/dist/shared/luca.B3saVjJm.mjs +163 -0
  116. package/dist/shared/luca.BYdjkfnz.mjs +217 -0
  117. package/dist/shared/luca.BmhNkYe2.mjs +56 -0
  118. package/dist/shared/luca.C4gMUoBd.mjs +358 -0
  119. package/dist/shared/luca.CQ3g1xrD.mjs +19 -0
  120. package/dist/shared/luca.CRmaAfXR.mjs +713 -0
  121. package/dist/shared/luca.CrXzXueR.mjs +57 -0
  122. package/dist/shared/luca.DTomPq7I.mjs +91 -0
  123. package/dist/shared/luca.DjDTeDCi.mjs +1904 -0
  124. package/dist/shared/luca.HZxBTBgD.mjs +201 -0
  125. package/dist/shared/luca.TSMg1t7I.mjs +10 -0
  126. package/dist/shared/luca.dM-MKlNE.mjs +25 -0
  127. package/dist/shared/luca.naWEcQ4B.mjs +7 -0
  128. package/package.json +76 -0
@@ -0,0 +1,715 @@
1
+ import * as p from '@clack/prompts';
2
+ import { defineCommand, runMain } from 'citty';
3
+ import { existsSync, chmodSync } from 'node:fs';
4
+ import { mkdir, writeFile, readFile, readdir, copyFile } from 'node:fs/promises';
5
+ import { join, dirname, delimiter } from 'node:path';
6
+ import { l as lucaStateSchema } from '../shared/luca.CRmaAfXR.mjs';
7
+ import 'node:crypto';
8
+ import 'node:module';
9
+ import 'node:url';
10
+ import 'node:child_process';
11
+ import { g as generateRunId } from '../shared/luca.naWEcQ4B.mjs';
12
+ import { LUCA_VERSION } from '../index.mjs';
13
+ import { d as defaultClaudeHome, r as resolveBundledArtifactsForHooks, i as installSkills } from '../shared/luca.B3saVjJm.mjs';
14
+ import { l as logger } from '../shared/luca.dM-MKlNE.mjs';
15
+ import { M as MuninndbInstallResultSchema, r as resolvePlatformTarget, g as getLucaHomePaths, a as MUNINNDB_BINARY_NAME, b as getCommonBinaryPaths, c as resolveMuninndbPort, d as checkMuninndbService, e as MuninndbServiceStatusSchema, w as waitForMuninndbHealthy, f as ensureLucaHome, h as checkMuninndbBinary, i as MUNINNDB_DEFAULT_PORT } from '../shared/luca.BYdjkfnz.mjs';
16
+ import { join as join$1 } from 'pathe';
17
+ import { c as checkPrerequisites, p as promptBunInstall } from '../shared/luca.DTomPq7I.mjs';
18
+ import 'zod';
19
+ import 'node:os';
20
+ import 'consola';
21
+ import 'semver';
22
+
23
+ async function writeProjectSkeleton(opts) {
24
+ const log = opts.log ?? (() => {
25
+ });
26
+ const lucaDir = join(opts.cwd, ".luca");
27
+ await mkdir(lucaDir, { recursive: true });
28
+ await writeIfMissing({
29
+ path: join(lucaDir, "state.json"),
30
+ contents: JSON.stringify(
31
+ lucaStateSchema.parse({ sessionId: generateRunId() }),
32
+ null,
33
+ 2
34
+ ) + "\n",
35
+ force: opts.force ?? false,
36
+ log
37
+ });
38
+ await writeIfMissing({
39
+ path: join(lucaDir, "config.json"),
40
+ contents: JSON.stringify(
41
+ {
42
+ lucaVersion: LUCA_VERSION,
43
+ vault: null,
44
+ oversight: "full-auto"
45
+ },
46
+ null,
47
+ 2
48
+ ) + "\n",
49
+ force: opts.force ?? false,
50
+ log
51
+ });
52
+ }
53
+ async function writeIfMissing(args) {
54
+ if (existsSync(args.path) && !args.force) {
55
+ args.log(` skip: ${args.path} (already exists)`);
56
+ return;
57
+ }
58
+ await writeFile(args.path, args.contents);
59
+ args.log(` write: ${args.path}`);
60
+ }
61
+
62
+ const STAGE_GATE_MATCHER = "Edit|Write|NotebookEdit|Bash";
63
+ const STAGE_GATE_COMMAND = "luca hook stage-gate";
64
+ async function wireClaudeHooks(opts) {
65
+ const log = opts.log ?? (() => {
66
+ });
67
+ const claudeHome = opts.claudeHome ?? defaultClaudeHome();
68
+ const settingsPath = join(claudeHome, "settings.json");
69
+ await mkdir(claudeHome, { recursive: true });
70
+ const existing = existsSync(settingsPath) ? JSON.parse(
71
+ await readFile(settingsPath, "utf-8")
72
+ ) ?? {} : {};
73
+ const next = mergeStageGateRegistration(existing);
74
+ await writeFile(settingsPath, JSON.stringify(next, null, 2) + "\n");
75
+ log(` write: ${settingsPath}`);
76
+ }
77
+ function mergeStageGateRegistration(settings) {
78
+ const next = { ...settings };
79
+ next.hooks = { ...settings.hooks ?? {} };
80
+ const preToolUse = [...next.hooks.PreToolUse ?? []];
81
+ const alreadyRegistered = preToolUse.some(
82
+ (entry) => entry.hooks?.some((h) => h.command?.includes("stage-gate"))
83
+ );
84
+ if (alreadyRegistered) {
85
+ next.hooks.PreToolUse = preToolUse;
86
+ return next;
87
+ }
88
+ preToolUse.push({
89
+ matcher: STAGE_GATE_MATCHER,
90
+ hooks: [
91
+ {
92
+ type: "command",
93
+ command: STAGE_GATE_COMMAND,
94
+ timeout: 30
95
+ }
96
+ ]
97
+ });
98
+ next.hooks.PreToolUse = preToolUse;
99
+ return next;
100
+ }
101
+
102
+ const LUCA_HOOK_HANDLER_MARKER = "/.claude/hooks/";
103
+ async function installHooks(opts) {
104
+ const log = opts.log ?? (() => {
105
+ });
106
+ const artifactsRoot = opts.claudeArtifactsRoot ?? resolveBundledArtifactsForHooks();
107
+ if (artifactsRoot === null) {
108
+ log(
109
+ " skip: bundled hook artifacts not found \u2014 could not locate the @alecsibilia/luca package root (running from a non-bundled dev tree?)"
110
+ );
111
+ return;
112
+ }
113
+ const hooksSrcDir = join(artifactsRoot, "hooks");
114
+ const hooksDestDir = join(opts.cwd, ".claude", "hooks");
115
+ if (existsSync(hooksSrcDir)) {
116
+ await mkdir(hooksDestDir, { recursive: true });
117
+ const entries = await readdir(hooksSrcDir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) continue;
120
+ const from = join(hooksSrcDir, entry.name);
121
+ const to = join(hooksDestDir, entry.name);
122
+ await copyFile(from, to);
123
+ log(` write: ${to}`);
124
+ }
125
+ } else {
126
+ log(` skip: bundled hook handlers missing (${hooksSrcDir})`);
127
+ }
128
+ const settingsSrc = join(artifactsRoot, "settings.json");
129
+ const settingsDest = join(opts.cwd, ".claude", "settings.json");
130
+ if (existsSync(settingsSrc)) {
131
+ await mkdir(dirname(settingsDest), { recursive: true });
132
+ const bundled = JSON.parse(
133
+ await readFile(settingsSrc, "utf-8")
134
+ );
135
+ const existing = existsSync(settingsDest) ? JSON.parse(
136
+ await readFile(settingsDest, "utf-8")
137
+ ) ?? {} : {};
138
+ const merged = mergeLucaHookSettings(existing, bundled);
139
+ await writeFile(settingsDest, JSON.stringify(merged, null, 2) + "\n");
140
+ log(` write: ${settingsDest}`);
141
+ } else {
142
+ log(` skip: bundled settings.json missing (${settingsSrc})`);
143
+ }
144
+ }
145
+ function mergeLucaHookSettings(existing, bundled) {
146
+ const next = { ...existing };
147
+ const existingHooks = existing.hooks ?? {};
148
+ const bundledHooks = bundled.hooks ?? {};
149
+ const mergedHooks = {};
150
+ const events = /* @__PURE__ */ new Set([
151
+ ...Object.keys(existingHooks),
152
+ ...Object.keys(bundledHooks)
153
+ ]);
154
+ for (const event of events) {
155
+ const existingEntries = existingHooks[event] ?? [];
156
+ const bundledEntries = bundledHooks[event] ?? [];
157
+ const filtered = existingEntries.filter(
158
+ (entry) => !entry.hooks.some(
159
+ (h) => h.command?.includes(LUCA_HOOK_HANDLER_MARKER)
160
+ )
161
+ );
162
+ const combined = [...filtered, ...bundledEntries];
163
+ if (combined.length > 0) {
164
+ mergedHooks[event] = combined;
165
+ }
166
+ }
167
+ next.hooks = mergedHooks;
168
+ return next;
169
+ }
170
+
171
+ const MUNINNDB_DOWNLOAD_BASE = process.env.MUNINNDB_DOWNLOAD_BASE ?? "https://muninndb.com";
172
+ const TRUSTED_INSTALL_HOSTS = /* @__PURE__ */ new Set(["muninndb.com", "www.muninndb.com"]);
173
+ const MUNINNDB_INSTALL_SCRIPT_URL = process.env.MUNINNDB_INSTALL_SCRIPT_URL ?? `${MUNINNDB_DOWNLOAD_BASE}/install.sh`;
174
+ function validateDownloadUrl(url) {
175
+ let parsed;
176
+ try {
177
+ parsed = new URL(url);
178
+ } catch {
179
+ return {
180
+ valid: false,
181
+ error: `Invalid download URL: "${url}" is not a valid URL.`
182
+ };
183
+ }
184
+ if (parsed.protocol !== "https:") {
185
+ return {
186
+ valid: false,
187
+ error: `Download URL must use HTTPS. Got: ${parsed.protocol} in "${url}"`
188
+ };
189
+ }
190
+ return { valid: true };
191
+ }
192
+ function extractPathFromOutput(stdout) {
193
+ const binaryName = MUNINNDB_BINARY_NAME;
194
+ const lines = stdout.split("\n");
195
+ for (const line of lines) {
196
+ const installPattern = new RegExp(
197
+ `(?:installed?\\s+(?:to|at|in)?|saved?\\s+(?:to|at|in)?|binary\\s+(?:at|in)?)\\s+(\\S*${binaryName}\\S*)`,
198
+ "i"
199
+ );
200
+ const installMatch = line.match(installPattern);
201
+ if (installMatch?.[1]) {
202
+ const candidate = installMatch[1].replace(/['"]+/g, "");
203
+ if (candidate.startsWith("/") && existsSync(candidate)) {
204
+ return candidate;
205
+ }
206
+ }
207
+ const pathPattern = new RegExp(`(\\/\\S*\\/${binaryName})(?:\\s|$)`);
208
+ const pathMatch = line.match(pathPattern);
209
+ if (pathMatch?.[1]) {
210
+ const candidate = pathMatch[1].replace(/['"]+/g, "");
211
+ if (existsSync(candidate)) {
212
+ return candidate;
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ async function findInstalledBinary(preferredDir, installStdout) {
219
+ const preferredPath = join$1(preferredDir, MUNINNDB_BINARY_NAME);
220
+ if (existsSync(preferredPath)) {
221
+ return preferredPath;
222
+ }
223
+ if (installStdout) {
224
+ const outputPath = extractPathFromOutput(installStdout);
225
+ if (outputPath) {
226
+ return outputPath;
227
+ }
228
+ }
229
+ const commonPaths = getCommonBinaryPaths();
230
+ for (const candidate of commonPaths) {
231
+ if (existsSync(candidate)) {
232
+ return candidate;
233
+ }
234
+ }
235
+ const whichResult = Bun.which(MUNINNDB_BINARY_NAME);
236
+ if (whichResult && existsSync(whichResult)) {
237
+ return whichResult;
238
+ }
239
+ const home = process.env.HOME;
240
+ if (home) {
241
+ try {
242
+ const findResult = await Bun.$`find ${home} -maxdepth 4 -name ${MUNINNDB_BINARY_NAME} -type f 2>/dev/null | head -1`.nothrow().quiet();
243
+ const foundPath = findResult.stdout.toString().trim();
244
+ if (foundPath && existsSync(foundPath)) {
245
+ return foundPath;
246
+ }
247
+ } catch {
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+ async function downloadMuninndbBinary(targetDir, options = {}) {
253
+ const { showProgress = true } = options;
254
+ const urlValidation = validateDownloadUrl(MUNINNDB_INSTALL_SCRIPT_URL);
255
+ if (!urlValidation.valid) {
256
+ return MuninndbInstallResultSchema.parse({
257
+ success: false,
258
+ binaryPath: null,
259
+ error: urlValidation.error
260
+ });
261
+ }
262
+ if (process.env.MUNINNDB_ALLOW_UNTRUSTED !== "1") {
263
+ try {
264
+ const parsed = new URL(MUNINNDB_INSTALL_SCRIPT_URL);
265
+ if (!TRUSTED_INSTALL_HOSTS.has(parsed.hostname)) {
266
+ return MuninndbInstallResultSchema.parse({
267
+ success: false,
268
+ binaryPath: null,
269
+ error: `Install script URL hostname "${parsed.hostname}" is not in the trusted hosts list. Set MUNINNDB_ALLOW_UNTRUSTED=1 to bypass this check.`
270
+ });
271
+ }
272
+ } catch {
273
+ }
274
+ }
275
+ const platformResult = resolvePlatformTarget();
276
+ if (!platformResult.success) {
277
+ return MuninndbInstallResultSchema.parse({
278
+ success: false,
279
+ binaryPath: null,
280
+ error: platformResult.error
281
+ });
282
+ }
283
+ const dir = getLucaHomePaths().bin;
284
+ const spinner = showProgress ? p.spinner() : null;
285
+ try {
286
+ spinner?.start(
287
+ `Installing MuninnDB via official install script (${platformResult.target})...`
288
+ );
289
+ const curlResult = await Bun.$`curl -sSL -f ${MUNINNDB_INSTALL_SCRIPT_URL}`.nothrow().quiet();
290
+ if (curlResult.exitCode !== 0) {
291
+ const stderr = curlResult.stderr.toString().trim();
292
+ const errorMsg = stderr ? `Failed to download install script (exit ${curlResult.exitCode}): ${stderr}` : `Failed to download install script (exit ${curlResult.exitCode}) from ${MUNINNDB_INSTALL_SCRIPT_URL}`;
293
+ spinner?.stop(`Install failed: ${errorMsg}`);
294
+ return MuninndbInstallResultSchema.parse({
295
+ success: false,
296
+ binaryPath: null,
297
+ error: errorMsg
298
+ });
299
+ }
300
+ const scriptContent = curlResult.stdout.toString();
301
+ if (!scriptContent.trim()) {
302
+ spinner?.stop("Install failed: downloaded script is empty");
303
+ return MuninndbInstallResultSchema.parse({
304
+ success: false,
305
+ binaryPath: null,
306
+ error: "Downloaded install script is empty."
307
+ });
308
+ }
309
+ const installResult = await Bun.$`INSTALL_DIR=${dir} BIN_DIR=${dir} PREFIX=${dir} sh -c ${scriptContent}`.nothrow().quiet();
310
+ const installStdout = installResult.stdout.toString();
311
+ const installStderr = installResult.stderr.toString().trim();
312
+ if (installResult.exitCode !== 0) {
313
+ const errorMsg = installStderr ? `Install script failed (exit ${installResult.exitCode}): ${installStderr}` : `Install script failed with exit code ${installResult.exitCode}`;
314
+ spinner?.stop(`Install failed: ${errorMsg}`);
315
+ return MuninndbInstallResultSchema.parse({
316
+ success: false,
317
+ binaryPath: null,
318
+ error: errorMsg
319
+ });
320
+ }
321
+ const foundPath = await findInstalledBinary(dir, installStdout);
322
+ if (!foundPath) {
323
+ const outputHint = installStdout.trim() ? `
324
+ Install script output:
325
+ ${installStdout.trim().slice(0, 500)}` : "";
326
+ const stderrHint = installStderr ? `
327
+ Install script stderr:
328
+ ${installStderr.slice(0, 300)}` : "";
329
+ spinner?.stop(
330
+ "Install script succeeded but binary not found on system"
331
+ );
332
+ return MuninndbInstallResultSchema.parse({
333
+ success: false,
334
+ binaryPath: null,
335
+ error: `MuninnDB install script completed successfully but the binary could not be found. Check that the install script placed it in a standard location (~/.local/bin/, /usr/local/bin/, ~/bin/, or ~/.muninndb/bin/).${outputHint}${stderrHint}`
336
+ });
337
+ }
338
+ const preferredPath = join$1(dir, MUNINNDB_BINARY_NAME);
339
+ if (foundPath !== preferredPath) {
340
+ try {
341
+ const sourceFile = Bun.file(foundPath);
342
+ await Bun.write(preferredPath, sourceFile);
343
+ await Bun.$`chmod 755 ${preferredPath}`.quiet();
344
+ } catch {
345
+ spinner?.stop(`MuninnDB installed at ${foundPath}`);
346
+ return MuninndbInstallResultSchema.parse({
347
+ success: true,
348
+ binaryPath: foundPath,
349
+ error: null
350
+ });
351
+ }
352
+ }
353
+ spinner?.stop("MuninnDB installed successfully");
354
+ return MuninndbInstallResultSchema.parse({
355
+ success: true,
356
+ binaryPath: preferredPath,
357
+ error: null
358
+ });
359
+ } catch (err) {
360
+ const errorMsg = err instanceof Error ? err.message : "Unknown install error";
361
+ spinner?.stop(`Install failed: ${errorMsg}`);
362
+ return MuninndbInstallResultSchema.parse({
363
+ success: false,
364
+ binaryPath: null,
365
+ error: errorMsg
366
+ });
367
+ }
368
+ }
369
+
370
+ async function startMuninndb(options = {}) {
371
+ const {
372
+ port: rawPort,
373
+ dataDir: rawDataDir,
374
+ timeoutMs = 1e4,
375
+ binaryPath: rawBinaryPath
376
+ } = options;
377
+ const port = resolveMuninndbPort(rawPort);
378
+ const homePaths = getLucaHomePaths();
379
+ const binaryPath = rawBinaryPath ?? join$1(homePaths.bin, MUNINNDB_BINARY_NAME);
380
+ const dataDir = rawDataDir ?? process.env.MUNINNDB_DATA_DIR ?? join$1(homePaths.root, "muninndb-data");
381
+ const pidfilePath = join$1(homePaths.root, "muninndb.pid");
382
+ const existingStatus = await checkMuninndbService(port);
383
+ if (existingStatus.healthy) {
384
+ return existingStatus;
385
+ }
386
+ await cleanStalePidfile(pidfilePath);
387
+ try {
388
+ await Bun.$`mkdir -p ${dataDir}`.quiet();
389
+ } catch {
390
+ }
391
+ try {
392
+ const proc = Bun.spawn(
393
+ [binaryPath, "--port", String(port), "--data-dir", dataDir],
394
+ {
395
+ stdout: "ignore",
396
+ stderr: "ignore",
397
+ stdin: "ignore"
398
+ }
399
+ );
400
+ if (proc.pid) {
401
+ await Bun.write(pidfilePath, String(proc.pid));
402
+ chmodSync(pidfilePath, 384);
403
+ proc.unref();
404
+ }
405
+ } catch {
406
+ return MuninndbServiceStatusSchema.parse({
407
+ running: false,
408
+ port,
409
+ pid: null,
410
+ healthy: false
411
+ });
412
+ }
413
+ return waitForMuninndbHealthy({ port, timeoutMs });
414
+ }
415
+ function isProcessRunning(pid) {
416
+ try {
417
+ process.kill(pid, 0);
418
+ return true;
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
423
+ async function removePidfile(pidfilePath) {
424
+ try {
425
+ await Bun.$`rm -f ${pidfilePath}`.quiet();
426
+ } catch {
427
+ }
428
+ }
429
+ async function cleanStalePidfile(pidfilePath) {
430
+ try {
431
+ const exists = await Bun.file(pidfilePath).exists();
432
+ if (!exists) return;
433
+ const pidStr = await Bun.file(pidfilePath).text();
434
+ const pid = parseInt(pidStr.trim(), 10);
435
+ if (isNaN(pid) || pid <= 0 || !isProcessRunning(pid)) {
436
+ await removePidfile(pidfilePath);
437
+ return;
438
+ }
439
+ if (!await verifyProcessIdentity(pid)) {
440
+ console.warn(
441
+ `[muninndb-service] PID ${pid} in pidfile is not a MuninnDB process. Removing stale pidfile.`
442
+ );
443
+ await removePidfile(pidfilePath);
444
+ }
445
+ } catch {
446
+ }
447
+ }
448
+ async function verifyProcessIdentity(pid) {
449
+ try {
450
+ const result = await Bun.$`ps -p ${pid} -o comm=`.quiet();
451
+ const comm = result.text().trim().toLowerCase();
452
+ return comm.includes("muninn");
453
+ } catch {
454
+ return false;
455
+ }
456
+ }
457
+
458
+ function isOnPath(dir) {
459
+ const pathEnv = process.env.PATH ?? "";
460
+ const entries = pathEnv.split(delimiter);
461
+ return entries.includes(dir);
462
+ }
463
+ function getPathGuidance(dir) {
464
+ const shell = process.env.SHELL ?? "";
465
+ const shellName = shell.split("/").pop() ?? "";
466
+ switch (shellName) {
467
+ case "zsh":
468
+ return [
469
+ "Add to your shell config (~/.zshrc):",
470
+ ` export PATH="${dir}:$PATH"`,
471
+ "",
472
+ "Then reload:",
473
+ " source ~/.zshrc"
474
+ ].join("\n");
475
+ case "bash":
476
+ return [
477
+ "Add to your shell config (~/.bashrc or ~/.bash_profile):",
478
+ ` export PATH="${dir}:$PATH"`,
479
+ "",
480
+ "Then reload:",
481
+ " source ~/.bashrc"
482
+ ].join("\n");
483
+ case "fish":
484
+ return [
485
+ "Add to your fish config (~/.config/fish/config.fish):",
486
+ ` fish_add_path ${dir}`,
487
+ "",
488
+ "Then reload:",
489
+ " source ~/.config/fish/config.fish"
490
+ ].join("\n");
491
+ default:
492
+ return [
493
+ "Add to your shell config:",
494
+ ` export PATH="${dir}:$PATH"`
495
+ ].join("\n");
496
+ }
497
+ }
498
+
499
+ const initCommand = defineCommand({
500
+ meta: {
501
+ name: "init",
502
+ description: "Bootstrap MuninnDB and set up the Luca home directory"
503
+ },
504
+ args: {
505
+ "skip-prerequisites": {
506
+ type: "boolean",
507
+ description: "Skip prerequisite checks",
508
+ default: false
509
+ },
510
+ "skip-muninndb": {
511
+ type: "boolean",
512
+ description: "Skip MuninnDB binary download and service setup",
513
+ default: false
514
+ },
515
+ "skip-claude": {
516
+ type: "boolean",
517
+ description: "Skip the global Claude integration (~/.claude/ skills, commands, agents, stage-gate hook)",
518
+ default: false
519
+ },
520
+ "skip-project": {
521
+ type: "boolean",
522
+ description: "Skip the per-project .luca/ skeleton",
523
+ default: false
524
+ }
525
+ },
526
+ async run({ args }) {
527
+ p.intro("luca init");
528
+ let prereqsVersion = null;
529
+ let prereqsPlatform = "";
530
+ let muninndbHealthy = false;
531
+ let muninndbPort = null;
532
+ let muninndbBinaryPath = null;
533
+ if (!args["skip-prerequisites"]) {
534
+ p.log.step("Step 1/5: Prerequisites");
535
+ const prereqs = checkPrerequisites();
536
+ if (!prereqs.ok) {
537
+ const shouldContinue = await promptBunInstall();
538
+ if (!shouldContinue) {
539
+ p.outro(
540
+ "Setup cancelled. Install Bun and run `luca init` again."
541
+ );
542
+ process.exit(1);
543
+ }
544
+ const recheck = checkPrerequisites();
545
+ if (!recheck.ok) {
546
+ logger.error(
547
+ "Bun still not detected. Please install Bun and try again."
548
+ );
549
+ process.exit(1);
550
+ }
551
+ }
552
+ prereqsVersion = prereqs.bun.version ?? "detected";
553
+ prereqsPlatform = `${prereqs.platform.os}/${prereqs.platform.arch}`;
554
+ p.log.success(`Bun ${prereqsVersion} (${prereqsPlatform})`);
555
+ } else {
556
+ p.log.info("Step 1/5: Prerequisites (skipped)");
557
+ }
558
+ p.log.step("Step 2/5: Luca home directory");
559
+ const homePaths = await ensureLucaHome();
560
+ p.log.success(`Luca home directory: ${homePaths.root}`);
561
+ if (!args["skip-muninndb"]) {
562
+ p.log.step("Step 3/5: MuninnDB");
563
+ const binaryStatus = await checkMuninndbBinary();
564
+ if (!binaryStatus.installed) {
565
+ p.log.info("MuninnDB not found. Downloading...");
566
+ const installResult = await downloadMuninndbBinary();
567
+ if (!installResult.success) {
568
+ p.log.warn(
569
+ `MuninnDB download failed: ${installResult.error ?? "unknown error"}`
570
+ );
571
+ p.log.warn(
572
+ "You can install MuninnDB later or run `luca init` again."
573
+ );
574
+ } else {
575
+ muninndbBinaryPath = installResult.binaryPath;
576
+ p.log.success(
577
+ `MuninnDB binary installed: ${installResult.binaryPath}`
578
+ );
579
+ }
580
+ } else {
581
+ muninndbBinaryPath = binaryStatus.path;
582
+ p.log.success(
583
+ `MuninnDB binary found: ${binaryStatus.path}${binaryStatus.version ? ` (${binaryStatus.version})` : ""}`
584
+ );
585
+ }
586
+ const recheckBinary = await checkMuninndbBinary();
587
+ if (recheckBinary.installed && recheckBinary.executable) {
588
+ p.log.info("Starting MuninnDB service...");
589
+ const serviceStatus = await startMuninndb();
590
+ if (serviceStatus.healthy) {
591
+ muninndbHealthy = true;
592
+ muninndbPort = serviceStatus.port;
593
+ p.log.success(
594
+ `MuninnDB running on port ${serviceStatus.port}${serviceStatus.pid ? ` (PID ${serviceStatus.pid})` : ""}`
595
+ );
596
+ } else {
597
+ p.log.warn(
598
+ "MuninnDB started but health check failed. It may need a moment to initialize."
599
+ );
600
+ p.log.info("Check status with: luca doctor");
601
+ }
602
+ }
603
+ if (!isOnPath(homePaths.bin)) {
604
+ const guidance = getPathGuidance(homePaths.bin);
605
+ p.note(
606
+ [
607
+ `${homePaths.bin} is not on your PATH.`,
608
+ "Add it so the MuninnDB binary is available globally:",
609
+ "",
610
+ guidance
611
+ ].join("\n"),
612
+ "PATH Setup Required"
613
+ );
614
+ }
615
+ } else {
616
+ p.log.info("Step 3/5: MuninnDB (skipped)");
617
+ }
618
+ const claudeHome = defaultClaudeHome();
619
+ let claudeSetupRan = false;
620
+ if (!args["skip-claude"]) {
621
+ p.log.step("Step 4/5: Claude integration (~/.claude/)");
622
+ await installSkills({ log: (msg) => p.log.info(msg) });
623
+ await wireClaudeHooks({ log: (msg) => p.log.info(msg) });
624
+ claudeSetupRan = true;
625
+ p.log.success(`Claude skills and stage-gate hook installed to ${claudeHome}`);
626
+ } else {
627
+ p.log.info("Step 4/5: Claude integration (skipped)");
628
+ }
629
+ let projectSetupRan = false;
630
+ if (!args["skip-project"]) {
631
+ p.log.step("Step 5/5: Project skeleton (.luca/ + .claude/hooks/)");
632
+ const projectCwd = process.cwd();
633
+ await writeProjectSkeleton({
634
+ cwd: projectCwd,
635
+ log: (msg) => p.log.info(msg)
636
+ });
637
+ await installHooks({
638
+ cwd: projectCwd,
639
+ log: (msg) => p.log.info(msg)
640
+ });
641
+ projectSetupRan = true;
642
+ p.log.success(`Per-project skeleton written to ${projectCwd}/.luca/ + ${projectCwd}/.claude/`);
643
+ } else {
644
+ p.log.info("Step 5/5: Project skeleton (skipped)");
645
+ }
646
+ const readout = [];
647
+ readout.push("Prerequisites:");
648
+ if (prereqsVersion) {
649
+ readout.push(` Bun ${prereqsVersion} (${prereqsPlatform})`);
650
+ } else {
651
+ readout.push(" Skipped");
652
+ }
653
+ readout.push("");
654
+ readout.push("MuninnDB:");
655
+ if (args["skip-muninndb"]) {
656
+ readout.push(" Skipped");
657
+ } else if (muninndbHealthy) {
658
+ readout.push(` Running on port ${muninndbPort}`);
659
+ if (muninndbBinaryPath) {
660
+ readout.push(` Binary: ${muninndbBinaryPath}`);
661
+ }
662
+ } else {
663
+ readout.push(
664
+ " Not running (start with `muninn start` or re-run `luca init`)"
665
+ );
666
+ }
667
+ readout.push("");
668
+ readout.push("Directories:");
669
+ readout.push(` ${homePaths.root}/`);
670
+ readout.push(` ${homePaths.bin}/`);
671
+ if (claudeSetupRan) {
672
+ readout.push(` ${claudeHome}/ (Claude skills, agents, hook \u2014 global)`);
673
+ }
674
+ if (projectSetupRan) {
675
+ readout.push(` ${process.cwd()}/.luca/ (per-project planning)`);
676
+ }
677
+ readout.push("");
678
+ readout.push("Next steps:");
679
+ readout.push(
680
+ " To set up a project vault: cd <project> && luca vault:init"
681
+ );
682
+ readout.push(' To start the pipeline: lu "<your task>"');
683
+ readout.push(
684
+ " To seed project conventions: invoke /luca-init from Claude Code"
685
+ );
686
+ readout.push(
687
+ " (probes branching/commits/PR/release/tracker conventions and"
688
+ );
689
+ readout.push(
690
+ " stores them in MuninnDB; downstream pipeline modes consult them)"
691
+ );
692
+ const mcpPort = muninndbPort ?? MUNINNDB_DEFAULT_PORT;
693
+ readout.push(
694
+ " To expose MuninnDB to Claude Code: register it as an MCP server,"
695
+ );
696
+ readout.push(
697
+ ` e.g. claude mcp add --transport http muninn http://localhost:${mcpPort}/mcp \\`
698
+ );
699
+ readout.push(
700
+ ' --header "Authorization: Bearer <your-muninn-api-key>"'
701
+ );
702
+ readout.push(
703
+ ' (or add a "muninn" entry under mcpServers in .mcp.json). Use the'
704
+ );
705
+ readout.push(
706
+ " same key as `luca vault:init` (.env MUNINN_DB_API_KEY). See the"
707
+ );
708
+ readout.push(' README "MuninnDB" section for details.');
709
+ p.note(readout.join("\n"), "Setup Complete");
710
+ p.outro("Luca is ready. Happy building!");
711
+ }
712
+ });
713
+ const runInit = () => runMain(initCommand);
714
+
715
+ export { initCommand, runInit };