@spekn/cli 1.0.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 (159) hide show
  1. package/dist/__tests__/export-cli.test.d.ts +1 -0
  2. package/dist/__tests__/export-cli.test.js +70 -0
  3. package/dist/__tests__/tui-args-policy.test.d.ts +1 -0
  4. package/dist/__tests__/tui-args-policy.test.js +50 -0
  5. package/dist/acp-S2MHZOAD.mjs +23 -0
  6. package/dist/acp-UCCI44JY.mjs +25 -0
  7. package/dist/auth/credentials-store.d.ts +2 -0
  8. package/dist/auth/credentials-store.js +5 -0
  9. package/dist/auth/device-flow.d.ts +36 -0
  10. package/dist/auth/device-flow.js +189 -0
  11. package/dist/auth/jwt.d.ts +1 -0
  12. package/dist/auth/jwt.js +6 -0
  13. package/dist/auth/session.d.ts +67 -0
  14. package/dist/auth/session.js +86 -0
  15. package/dist/auth-login.d.ts +34 -0
  16. package/dist/auth-login.js +202 -0
  17. package/dist/auth-logout.d.ts +25 -0
  18. package/dist/auth-logout.js +115 -0
  19. package/dist/auth-status.d.ts +24 -0
  20. package/dist/auth-status.js +109 -0
  21. package/dist/backlog-generate.d.ts +11 -0
  22. package/dist/backlog-generate.js +308 -0
  23. package/dist/backlog-health.d.ts +11 -0
  24. package/dist/backlog-health.js +287 -0
  25. package/dist/bridge-login.d.ts +40 -0
  26. package/dist/bridge-login.js +277 -0
  27. package/dist/chunk-3PAYRI4G.mjs +2428 -0
  28. package/dist/chunk-M4CS3A25.mjs +2426 -0
  29. package/dist/commands/auth/login.d.ts +30 -0
  30. package/dist/commands/auth/login.js +164 -0
  31. package/dist/commands/auth/logout.d.ts +25 -0
  32. package/dist/commands/auth/logout.js +115 -0
  33. package/dist/commands/auth/status.d.ts +24 -0
  34. package/dist/commands/auth/status.js +109 -0
  35. package/dist/commands/backlog/generate.d.ts +11 -0
  36. package/dist/commands/backlog/generate.js +308 -0
  37. package/dist/commands/backlog/health.d.ts +11 -0
  38. package/dist/commands/backlog/health.js +287 -0
  39. package/dist/commands/bridge/login.d.ts +36 -0
  40. package/dist/commands/bridge/login.js +258 -0
  41. package/dist/commands/export.d.ts +35 -0
  42. package/dist/commands/export.js +485 -0
  43. package/dist/commands/marketplace-export.d.ts +21 -0
  44. package/dist/commands/marketplace-export.js +214 -0
  45. package/dist/commands/project-clean.d.ts +1 -0
  46. package/dist/commands/project-clean.js +126 -0
  47. package/dist/commands/repo/common.d.ts +105 -0
  48. package/dist/commands/repo/common.js +775 -0
  49. package/dist/commands/repo/detach.d.ts +2 -0
  50. package/dist/commands/repo/detach.js +120 -0
  51. package/dist/commands/repo/register.d.ts +21 -0
  52. package/dist/commands/repo/register.js +175 -0
  53. package/dist/commands/repo/sync.d.ts +22 -0
  54. package/dist/commands/repo/sync.js +873 -0
  55. package/dist/commands/skills-import-local.d.ts +16 -0
  56. package/dist/commands/skills-import-local.js +352 -0
  57. package/dist/commands/spec/drift-check.d.ts +3 -0
  58. package/dist/commands/spec/drift-check.js +186 -0
  59. package/dist/commands/spec/frontmatter.d.ts +11 -0
  60. package/dist/commands/spec/frontmatter.js +219 -0
  61. package/dist/commands/spec/lint.d.ts +11 -0
  62. package/dist/commands/spec/lint.js +499 -0
  63. package/dist/commands/spec/parse.d.ts +11 -0
  64. package/dist/commands/spec/parse.js +162 -0
  65. package/dist/export.d.ts +35 -0
  66. package/dist/export.js +485 -0
  67. package/dist/index.d.ts +11 -0
  68. package/dist/index.js +21 -0
  69. package/dist/main.d.ts +1 -0
  70. package/dist/main.js +115280 -0
  71. package/dist/marketplace-export.d.ts +21 -0
  72. package/dist/marketplace-export.js +214 -0
  73. package/dist/project-clean.d.ts +1 -0
  74. package/dist/project-clean.js +126 -0
  75. package/dist/project-context.d.ts +99 -0
  76. package/dist/project-context.js +376 -0
  77. package/dist/repo-common.d.ts +101 -0
  78. package/dist/repo-common.js +671 -0
  79. package/dist/repo-detach.d.ts +2 -0
  80. package/dist/repo-detach.js +102 -0
  81. package/dist/repo-ingest.d.ts +29 -0
  82. package/dist/repo-ingest.js +305 -0
  83. package/dist/repo-register.d.ts +21 -0
  84. package/dist/repo-register.js +175 -0
  85. package/dist/repo-sync.d.ts +16 -0
  86. package/dist/repo-sync.js +152 -0
  87. package/dist/resources/prompt-loader.d.ts +1 -0
  88. package/dist/resources/prompt-loader.js +62 -0
  89. package/dist/resources/prompts/README.md +21 -0
  90. package/dist/resources/prompts/prompts/repo-analysis.prompt.md +126 -0
  91. package/dist/resources/prompts/repo-analysis.prompt.md +151 -0
  92. package/dist/resources/prompts/repo-sync-analysis.prompt.md +85 -0
  93. package/dist/skills-import-local.d.ts +16 -0
  94. package/dist/skills-import-local.js +352 -0
  95. package/dist/spec-drift-check.d.ts +3 -0
  96. package/dist/spec-drift-check.js +186 -0
  97. package/dist/spec-frontmatter.d.ts +11 -0
  98. package/dist/spec-frontmatter.js +219 -0
  99. package/dist/spec-lint.d.ts +11 -0
  100. package/dist/spec-lint.js +499 -0
  101. package/dist/spec-parse.d.ts +11 -0
  102. package/dist/spec-parse.js +162 -0
  103. package/dist/stubs/dotenv.d.ts +5 -0
  104. package/dist/stubs/dotenv.js +6 -0
  105. package/dist/stubs/typeorm.d.ts +22 -0
  106. package/dist/stubs/typeorm.js +28 -0
  107. package/dist/tui/app.d.ts +7 -0
  108. package/dist/tui/app.js +122 -0
  109. package/dist/tui/args.d.ts +8 -0
  110. package/dist/tui/args.js +57 -0
  111. package/dist/tui/capabilities/policy.d.ts +7 -0
  112. package/dist/tui/capabilities/policy.js +64 -0
  113. package/dist/tui/components/frame.d.ts +8 -0
  114. package/dist/tui/components/frame.js +8 -0
  115. package/dist/tui/components/status-bar.d.ts +8 -0
  116. package/dist/tui/components/status-bar.js +8 -0
  117. package/dist/tui/index.d.ts +2 -0
  118. package/dist/tui/index.js +23 -0
  119. package/dist/tui/index.mjs +7563 -0
  120. package/dist/tui/keymap/use-global-keymap.d.ts +19 -0
  121. package/dist/tui/keymap/use-global-keymap.js +82 -0
  122. package/dist/tui/navigation/nav-items.d.ts +3 -0
  123. package/dist/tui/navigation/nav-items.js +18 -0
  124. package/dist/tui/screens/bridge.d.ts +8 -0
  125. package/dist/tui/screens/bridge.js +19 -0
  126. package/dist/tui/screens/decisions.d.ts +5 -0
  127. package/dist/tui/screens/decisions.js +28 -0
  128. package/dist/tui/screens/export.d.ts +5 -0
  129. package/dist/tui/screens/export.js +16 -0
  130. package/dist/tui/screens/home.d.ts +5 -0
  131. package/dist/tui/screens/home.js +33 -0
  132. package/dist/tui/screens/locked.d.ts +5 -0
  133. package/dist/tui/screens/locked.js +9 -0
  134. package/dist/tui/screens/specs.d.ts +5 -0
  135. package/dist/tui/screens/specs.js +31 -0
  136. package/dist/tui/services/client.d.ts +1 -0
  137. package/dist/tui/services/client.js +18 -0
  138. package/dist/tui/services/context-service.d.ts +19 -0
  139. package/dist/tui/services/context-service.js +246 -0
  140. package/dist/tui/shared-enums.d.ts +16 -0
  141. package/dist/tui/shared-enums.js +19 -0
  142. package/dist/tui/state/use-app-state.d.ts +35 -0
  143. package/dist/tui/state/use-app-state.js +177 -0
  144. package/dist/tui/types.d.ts +77 -0
  145. package/dist/tui/types.js +2 -0
  146. package/dist/tui-bundle.d.ts +1 -0
  147. package/dist/tui-bundle.js +5 -0
  148. package/dist/tui-entry.mjs +1407 -0
  149. package/dist/utils/cli-runtime.d.ts +5 -0
  150. package/dist/utils/cli-runtime.js +22 -0
  151. package/dist/utils/help-error.d.ts +7 -0
  152. package/dist/utils/help-error.js +14 -0
  153. package/dist/utils/interaction.d.ts +19 -0
  154. package/dist/utils/interaction.js +93 -0
  155. package/dist/utils/structured-log.d.ts +7 -0
  156. package/dist/utils/structured-log.js +112 -0
  157. package/dist/utils/trpc-url.d.ts +4 -0
  158. package/dist/utils/trpc-url.js +15 -0
  159. package/package.json +59 -0
@@ -0,0 +1,671 @@
1
+ "use strict";
2
+ /**
3
+ * Shared utilities for repo register / repo sync CLI commands.
4
+ *
5
+ * Extracts common concerns: deps, auth, git metadata, tRPC client,
6
+ * file discovery display, and the full analysis phase.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.HelpRequestedError = exports.defaultDeps = void 0;
43
+ exports.parseCommonFlag = parseCommonFlag;
44
+ exports.commonDefaults = commonDefaults;
45
+ exports.finalizeOptions = finalizeOptions;
46
+ exports.resolveAuth = resolveAuth;
47
+ exports.createApiClient = createApiClient;
48
+ exports.repoNameFromUrl = repoNameFromUrl;
49
+ exports.resolveDefaultBranch = resolveDefaultBranch;
50
+ exports.readGitMetadata = readGitMetadata;
51
+ exports.discoverFiles = discoverFiles;
52
+ exports.discoverAndDisplay = discoverAndDisplay;
53
+ exports.buildAnalysisPrompt = buildAnalysisPrompt;
54
+ exports.validateAnalysisReport = validateAnalysisReport;
55
+ exports.verifyMcpServer = verifyMcpServer;
56
+ exports.executeAcpSession = executeAcpSession;
57
+ exports.runAnalysisWithAgent = runAnalysisWithAgent;
58
+ exports.runAnalysisPhase = runAnalysisPhase;
59
+ const fs = __importStar(require("node:fs"));
60
+ const path = __importStar(require("node:path"));
61
+ const node_child_process_1 = require("node:child_process");
62
+ const client_1 = require("@trpc/client");
63
+ const credentials_store_1 = require("./auth/credentials-store");
64
+ const trpc_url_1 = require("./utils/trpc-url");
65
+ const help_error_1 = require("./utils/help-error");
66
+ Object.defineProperty(exports, "HelpRequestedError", { enumerable: true, get: function () { return help_error_1.HelpRequestedError; } });
67
+ const interaction_1 = require("./utils/interaction");
68
+ const project_context_1 = require("./project-context");
69
+ const prompt_loader_1 = require("./resources/prompt-loader");
70
+ exports.defaultDeps = {
71
+ execGit: (args) => (0, node_child_process_1.execFileSync)("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
72
+ stdout: (content) => process.stdout.write(content),
73
+ stderr: (content) => process.stderr.write(content),
74
+ credentialsStore: new credentials_store_1.CredentialsStore(),
75
+ };
76
+ /**
77
+ * Parse flags shared by both register and sync. Returns consumed arg
78
+ * count (0 if the flag was not recognized). Mutates `out` in place.
79
+ */
80
+ function parseCommonFlag(args, index, out) {
81
+ const arg = args[index];
82
+ if (arg === "--help" || arg === "-h")
83
+ throw new help_error_1.HelpRequestedError();
84
+ if (arg === "--project-id" && args[index + 1]) {
85
+ out.projectId = args[index + 1];
86
+ return 2;
87
+ }
88
+ if (arg?.startsWith("--project-id=")) {
89
+ out.projectId = arg.slice("--project-id=".length);
90
+ return 1;
91
+ }
92
+ if (arg === "--api-url" && args[index + 1]) {
93
+ out.apiUrl = args[index + 1];
94
+ return 2;
95
+ }
96
+ if (arg?.startsWith("--api-url=")) {
97
+ out.apiUrl = arg.slice("--api-url=".length);
98
+ return 1;
99
+ }
100
+ if (arg === "--agent" && args[index + 1]) {
101
+ out.agent = args[index + 1];
102
+ return 2;
103
+ }
104
+ if (arg?.startsWith("--agent=")) {
105
+ out.agent = arg.slice("--agent=".length);
106
+ return 1;
107
+ }
108
+ if (arg === "--path" && args[index + 1]) {
109
+ out.repoPath = args[index + 1];
110
+ return 2;
111
+ }
112
+ if (arg?.startsWith("--path=")) {
113
+ out.repoPath = arg.slice("--path=".length);
114
+ return 1;
115
+ }
116
+ if (arg === "--mcp-url" && args[index + 1]) {
117
+ out.mcpUrl = args[index + 1];
118
+ return 2;
119
+ }
120
+ if (arg?.startsWith("--mcp-url=")) {
121
+ out.mcpUrl = arg.slice("--mcp-url=".length);
122
+ return 1;
123
+ }
124
+ if (arg === "--dry-run") {
125
+ out.dryRun = true;
126
+ return 1;
127
+ }
128
+ if (arg === "--debug") {
129
+ out.debug = true;
130
+ return 1;
131
+ }
132
+ if (arg === "--analyze") {
133
+ out.analyze = true;
134
+ return 1;
135
+ }
136
+ if (arg === "--no-analyze") {
137
+ out.analyze = false;
138
+ return 1;
139
+ }
140
+ return 0; // not consumed
141
+ }
142
+ function commonDefaults(analyzeDefault) {
143
+ return {
144
+ projectId: "",
145
+ apiUrl: process.env.SPEKN_API_URL ?? "https://app.spekn.com",
146
+ analyze: analyzeDefault,
147
+ agent: null,
148
+ repoPath: ".",
149
+ dryRun: false,
150
+ mcpUrl: process.env.MCP_HTTP_URL ?? "https://app.spekn.com/mcp",
151
+ debug: false,
152
+ };
153
+ }
154
+ function finalizeOptions(opts) {
155
+ opts.repoPath = path.resolve(opts.repoPath);
156
+ return opts;
157
+ }
158
+ // ── Auth ────────────────────────────────────────────────────────────
159
+ async function resolveAuth(deps, options) {
160
+ const storedToken = await deps.credentialsStore.getValidToken();
161
+ const authToken = storedToken ?? process.env.SPEKN_AUTH_TOKEN;
162
+ const storedCreds = deps.credentialsStore.load();
163
+ const context = (0, project_context_1.resolveProjectContext)({
164
+ explicitProjectId: options?.projectId || undefined,
165
+ repoPath: options?.repoPath,
166
+ credentialsOrganizationId: storedCreds?.organizationId,
167
+ envOrganizationId: process.env.SPEKN_ORGANIZATION_ID,
168
+ });
169
+ return {
170
+ authToken: authToken ?? undefined,
171
+ organizationId: context.organizationId,
172
+ projectId: context.projectId,
173
+ };
174
+ }
175
+ // ── tRPC Client ─────────────────────────────────────────────────────
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ function createApiClient(apiUrl, authToken, organizationId) {
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ return (0, client_1.createTRPCProxyClient)({
180
+ links: [
181
+ (0, client_1.httpBatchLink)({
182
+ url: (0, trpc_url_1.normalizeTrpcUrl)(apiUrl),
183
+ headers: {
184
+ "x-organization-id": organizationId,
185
+ authorization: authToken ? `Bearer ${authToken}` : "",
186
+ },
187
+ }),
188
+ ],
189
+ });
190
+ }
191
+ // ── Git Helpers ─────────────────────────────────────────────────────
192
+ function repoNameFromUrl(url) {
193
+ const withoutGit = url.replace(/\.git$/, "");
194
+ const parts = withoutGit.split(/[/:]/).filter(Boolean);
195
+ return parts[parts.length - 1] ?? "unknown";
196
+ }
197
+ function resolveDefaultBranch(execGit) {
198
+ try {
199
+ const ref = execGit(["symbolic-ref", "refs/remotes/origin/HEAD"]);
200
+ const parts = ref.split("/");
201
+ return parts[parts.length - 1] ?? "main";
202
+ }
203
+ catch {
204
+ return "main";
205
+ }
206
+ }
207
+ function readGitMetadata(repoPath, deps) {
208
+ let remoteUrl;
209
+ try {
210
+ remoteUrl = deps.execGit(["-C", repoPath, "remote", "get-url", "origin"]);
211
+ }
212
+ catch {
213
+ deps.stderr("Error: Could not read git remote URL. Make sure the path is a git repository with an 'origin' remote.\n");
214
+ return null;
215
+ }
216
+ const name = repoNameFromUrl(remoteUrl);
217
+ const defaultBranch = resolveDefaultBranch((gitArgs) => deps.execGit(["-C", repoPath, ...gitArgs]));
218
+ return { remoteUrl, name, defaultBranch };
219
+ }
220
+ // ── File Discovery ──────────────────────────────────────────────────
221
+ const IGNORE_DIRS = new Set([
222
+ "node_modules", "dist", ".git", "build", "coverage",
223
+ ".next", ".nuxt", "__pycache__", ".venv", "target",
224
+ ]);
225
+ function collectMarkdownFiles(dirPath) {
226
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
227
+ return [];
228
+ const results = [];
229
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
230
+ for (const entry of entries) {
231
+ if (IGNORE_DIRS.has(entry.name))
232
+ continue;
233
+ if (entry.name.startsWith(".") && entry.name !== ".cursorrules" && entry.name !== ".github")
234
+ continue;
235
+ const full = path.join(dirPath, entry.name);
236
+ if (entry.isDirectory()) {
237
+ results.push(...collectMarkdownFiles(full));
238
+ }
239
+ else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
240
+ results.push(full);
241
+ }
242
+ }
243
+ return results;
244
+ }
245
+ function discoverFiles(repoPath) {
246
+ const files = [];
247
+ const governanceFiles = [
248
+ "CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".cursorrules",
249
+ ".github/copilot-instructions.md", "README.md",
250
+ ];
251
+ for (const file of governanceFiles) {
252
+ const abs = path.join(repoPath, file);
253
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
254
+ files.push({ relativePath: file, absolutePath: abs, category: "governance" });
255
+ }
256
+ }
257
+ const rulesDir = path.join(repoPath, ".claude", "rules");
258
+ for (const f of collectMarkdownFiles(rulesDir)) {
259
+ files.push({ relativePath: path.relative(repoPath, f), absolutePath: f, category: "governance" });
260
+ }
261
+ const allMd = collectMarkdownFiles(repoPath);
262
+ for (const f of allMd) {
263
+ const rel = path.relative(repoPath, f);
264
+ if (files.some((d) => d.relativePath === rel))
265
+ continue;
266
+ const lower = rel.toLowerCase();
267
+ if (lower.includes("decision") || lower.includes("/adr/")) {
268
+ files.push({ relativePath: rel, absolutePath: f, category: "decision" });
269
+ }
270
+ else if (lower.includes("spec") || lower.includes("design") || lower.includes("rfc")) {
271
+ files.push({ relativePath: rel, absolutePath: f, category: "spec" });
272
+ }
273
+ else {
274
+ files.push({ relativePath: rel, absolutePath: f, category: "config" });
275
+ }
276
+ }
277
+ return files;
278
+ }
279
+ /** Discover files, print summary. Returns files or null if empty. */
280
+ function discoverAndDisplay(repoPath, stdout) {
281
+ stdout("\nScanning repository for analysis...\n");
282
+ const discovered = discoverFiles(repoPath);
283
+ if (discovered.length === 0) {
284
+ stdout("No files found in " + repoPath + "\n");
285
+ return null;
286
+ }
287
+ const byCat = {
288
+ governance: discovered.filter((f) => f.category === "governance"),
289
+ spec: discovered.filter((f) => f.category === "spec"),
290
+ decision: discovered.filter((f) => f.category === "decision"),
291
+ config: discovered.filter((f) => f.category === "config"),
292
+ };
293
+ stdout(`\nDiscovered ${discovered.length} files:\n`);
294
+ if (byCat.governance.length > 0)
295
+ stdout(` Governance: ${byCat.governance.map((f) => f.relativePath).join(", ")}\n`);
296
+ if (byCat.spec.length > 0)
297
+ stdout(` Specs: ${byCat.spec.map((f) => f.relativePath).join(", ")}\n`);
298
+ if (byCat.decision.length > 0)
299
+ stdout(` Decisions: ${byCat.decision.map((f) => f.relativePath).join(", ")}\n`);
300
+ if (byCat.config.length > 0)
301
+ stdout(` Other: ${byCat.config.length} markdown files\n`);
302
+ return discovered;
303
+ }
304
+ // ── Prompt Builder ──────────────────────────────────────────────────
305
+ function buildAnalysisPrompt(projectId, repoPath, files, organizationId) {
306
+ const fileList = files.map((f) => ` - ${f.relativePath} [${f.category}]`).join("\n");
307
+ const orgIdInstruction = organizationId
308
+ ? `\nORGANIZATION ID: ${organizationId}\nIMPORTANT: You MUST include "organizationId": "${organizationId}" as a parameter in EVERY Spekn MCP tool call.\n`
309
+ : "";
310
+ const template = (0, prompt_loader_1.loadPromptTemplate)("repo-analysis.prompt.md");
311
+ return template
312
+ .replaceAll("{{PROJECT_ID}}", projectId)
313
+ .replaceAll("{{ORG_INSTRUCTION}}", orgIdInstruction)
314
+ .replaceAll("{{REPO_PATH}}", repoPath)
315
+ .replaceAll("{{FILE_LIST}}", fileList);
316
+ }
317
+ const REQUIRED_ANALYSIS_SECTIONS = [
318
+ "FEATURE_COVERAGE",
319
+ "MISSING_PLAN_FEATURES",
320
+ "DECISIONS_CREATED",
321
+ ];
322
+ function validateAnalysisReport(text) {
323
+ const missing = REQUIRED_ANALYSIS_SECTIONS.filter((section) => {
324
+ const pattern = new RegExp(`^\\s*${section}\\b`, "mi");
325
+ return !pattern.test(text);
326
+ });
327
+ return { ok: missing.length === 0, missing: [...missing] };
328
+ }
329
+ // ── MCP Server Verification ─────────────────────────────────────────
330
+ /**
331
+ * Connect to the MCP HTTP server via raw JSON-RPC, perform the MCP
332
+ * initialize handshake, then list tools and verify Spekn tools exist.
333
+ * Returns tool names on success or null on failure.
334
+ */
335
+ async function verifyMcpServer(mcpUrl, authToken, stderr, debug, sessionPurpose = "ingestion_sync") {
336
+ const dbg = (msg) => { if (debug)
337
+ stderr(` [debug] ${msg}\n`); };
338
+ const headers = {
339
+ "Content-Type": "application/json",
340
+ Accept: "application/json, text/event-stream",
341
+ Authorization: `Bearer ${authToken}`,
342
+ "x-spekn-mcp-purpose": sessionPurpose,
343
+ };
344
+ dbg(`MCP URL: ${mcpUrl}`);
345
+ dbg(`Auth token (first 20 chars): ${authToken.slice(0, 20)}...`);
346
+ dbg(`Auth token length: ${authToken.length}`);
347
+ try {
348
+ // Step 1: MCP initialize handshake
349
+ const initBody = JSON.stringify({
350
+ jsonrpc: "2.0",
351
+ method: "initialize",
352
+ params: {
353
+ protocolVersion: "2025-03-26",
354
+ capabilities: {},
355
+ clientInfo: { name: "spekn-cli", version: "1.0.0" },
356
+ },
357
+ id: 1,
358
+ });
359
+ dbg(`POST initialize: ${initBody}`);
360
+ const initRes = await fetch(mcpUrl, {
361
+ method: "POST",
362
+ headers,
363
+ body: initBody,
364
+ signal: AbortSignal.timeout(10_000),
365
+ });
366
+ const initResBody = await initRes.text();
367
+ const sessionId = initRes.headers.get("mcp-session-id");
368
+ dbg(`initialize response: HTTP ${initRes.status}`);
369
+ dbg(`initialize content-type: ${initRes.headers.get("content-type") ?? ""}`);
370
+ dbg(`initialize mcp-session-id: ${sessionId ?? "(none)"}`);
371
+ dbg(`initialize response headers: ${JSON.stringify(Object.fromEntries(initRes.headers.entries()))}`);
372
+ dbg(`initialize body: ${initResBody.slice(0, 500)}`);
373
+ if (!initRes.ok) {
374
+ stderr(` MCP initialize failed: HTTP ${initRes.status} — ${initResBody.slice(0, 200)}\n`);
375
+ return null;
376
+ }
377
+ if (!sessionId) {
378
+ stderr(" MCP initialize succeeded but no session ID returned.\n");
379
+ return null;
380
+ }
381
+ // Step 2: Send initialized notification
382
+ const sessionHeaders = { ...headers, "mcp-session-id": sessionId };
383
+ dbg(`POST notifications/initialized (session: ${sessionId})`);
384
+ const notifRes = await fetch(mcpUrl, {
385
+ method: "POST",
386
+ headers: sessionHeaders,
387
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
388
+ signal: AbortSignal.timeout(5_000),
389
+ });
390
+ dbg(`notifications/initialized response: HTTP ${notifRes.status}`);
391
+ const notifResBody = await notifRes.text();
392
+ dbg(`notifications/initialized body: ${notifResBody.slice(0, 300)}`);
393
+ // Step 3: List tools
394
+ dbg(`POST tools/list (session: ${sessionId})`);
395
+ const toolsRes = await fetch(mcpUrl, {
396
+ method: "POST",
397
+ headers: sessionHeaders,
398
+ body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", params: {}, id: 2 }),
399
+ signal: AbortSignal.timeout(10_000),
400
+ });
401
+ const toolsResBody = await toolsRes.text();
402
+ const toolsContentType = toolsRes.headers.get("content-type") ?? "";
403
+ dbg(`tools/list response: HTTP ${toolsRes.status}`);
404
+ dbg(`tools/list content-type: ${toolsContentType}`);
405
+ dbg(`tools/list response headers: ${JSON.stringify(Object.fromEntries(toolsRes.headers.entries()))}`);
406
+ dbg(`tools/list body (first 500): ${toolsResBody.slice(0, 500)}`);
407
+ if (!toolsRes.ok) {
408
+ stderr(` MCP tools/list failed: HTTP ${toolsRes.status} — ${toolsResBody.slice(0, 200)}\n`);
409
+ // Try to parse error response for better diagnostics
410
+ try {
411
+ const errorObj = JSON.parse(toolsResBody);
412
+ if (errorObj.error && errorObj.error.message) {
413
+ const errorMsg = errorObj.error.message;
414
+ if (errorMsg.includes("Authentication failed")) {
415
+ stderr(` Authentication issue: ${errorMsg}\n`);
416
+ stderr(` Please check your auth token and ensure the MCP server is properly configured.\n`);
417
+ }
418
+ else if (errorMsg.includes("Server not initialized")) {
419
+ stderr(` MCP server initialization failed. This may indicate a database connection issue.\n`);
420
+ }
421
+ }
422
+ }
423
+ catch {
424
+ // Ignore JSON parse errors
425
+ }
426
+ return null;
427
+ }
428
+ // Parse tools response (may be SSE or JSON)
429
+ let toolsParseable;
430
+ if (toolsContentType.includes("text/event-stream")) {
431
+ const dataLine = toolsResBody.split("\n").find((l) => l.startsWith("data: "));
432
+ toolsParseable = dataLine ? dataLine.slice(6) : toolsResBody;
433
+ }
434
+ else {
435
+ toolsParseable = toolsResBody;
436
+ }
437
+ dbg(`tools/list parsed payload: ${toolsParseable.slice(0, 500)}`);
438
+ const toolsResult = JSON.parse(toolsParseable);
439
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
440
+ const tools = (toolsResult.result?.tools ?? []).map((t) => t.name);
441
+ dbg(`tools found: ${tools.length} — ${tools.join(", ")}`);
442
+ // Step 4: Clean up — terminate session
443
+ await fetch(mcpUrl, {
444
+ method: "DELETE",
445
+ headers: sessionHeaders,
446
+ signal: AbortSignal.timeout(5_000),
447
+ }).catch(() => { });
448
+ if (tools.length === 0) {
449
+ stderr(` MCP server at ${mcpUrl}: connected but no tools available.\n`);
450
+ return null;
451
+ }
452
+ const speknTools = tools.filter((n) => n.startsWith("spekn_"));
453
+ stderr(` MCP server verified: ${speknTools.length} Spekn tools available (${tools.length} total)\n`);
454
+ return tools;
455
+ }
456
+ catch (err) {
457
+ const msg = err instanceof Error ? err.message : String(err);
458
+ stderr(` MCP server verification failed at ${mcpUrl}: ${msg}\n`);
459
+ dbg(`Full error: ${err instanceof Error ? err.stack ?? msg : msg}`);
460
+ return null;
461
+ }
462
+ }
463
+ // ── ACP Session Execution ───────────────────────────────────────────
464
+ async function executeAcpSession(agent, prompt, repoPath, mcpUrl, authToken, stderr, organizationId, sessionPurpose = "ingestion_sync") {
465
+ const { executeAcpSession: sharedExecuteAcpSession } = await Promise.resolve().then(() => __importStar(require("@spekn/check")));
466
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
467
+ const mcpHeaders = [
468
+ { name: "Authorization", value: `Bearer ${authToken}` },
469
+ { name: "x-spekn-mcp-purpose", value: sessionPurpose },
470
+ ];
471
+ if (organizationId) {
472
+ mcpHeaders.push({ name: "x-organization-id", value: organizationId });
473
+ }
474
+ const DEFAULT_ACP_TIMEOUT_MS = 5 * 60 * 1000;
475
+ const timeoutMs = Number(process.env.ACP_AGENT_TIMEOUT_MS) || DEFAULT_ACP_TIMEOUT_MS;
476
+ const result = await sharedExecuteAcpSession({
477
+ command: agent.command,
478
+ args: agent.args,
479
+ prompt,
480
+ cwd: repoPath,
481
+ timeoutMs,
482
+ mcpServers: [{
483
+ type: "http",
484
+ name: "spekn",
485
+ url: mcpUrl,
486
+ headers: mcpHeaders,
487
+ }],
488
+ requireHttpMcp: true,
489
+ onChunk: (text) => process.stderr.write(text),
490
+ onStderr: (text) => stderr(`[agent] ${text.slice(0, 200)}\n`),
491
+ onSpawnError: (err) => stderr(`ACP agent spawn error: ${err.message}\n`),
492
+ onSessionStart: (sessionId) => {
493
+ stderr(` Session created: ${sessionId}\n`);
494
+ stderr("\nAgent session started. Analyzing repository...\n\n");
495
+ },
496
+ });
497
+ // Map agentCapabilities info for caller
498
+ if (result.error === "no-http-mcp") {
499
+ stderr(` Agent "${agent.command}" does not advertise HTTP MCP support.\n`);
500
+ }
501
+ return { text: result.text, error: result.error };
502
+ }
503
+ async function issuePurposeToken(apiUrl, authToken, organizationId, purpose) {
504
+ if (!organizationId) {
505
+ throw new Error("Organization context is required to issue purpose-scoped MCP tokens");
506
+ }
507
+ const client = createApiClient(apiUrl, authToken, organizationId);
508
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
509
+ const result = await client.auth.issueMcpToken.mutate({
510
+ purpose,
511
+ ttlSeconds: 600,
512
+ });
513
+ if (!result?.token || typeof result.token !== "string") {
514
+ throw new Error("Failed to issue MCP purpose token");
515
+ }
516
+ return result.token;
517
+ }
518
+ /**
519
+ * Verify MCP server, resolve an agent, run the ACP session.
520
+ * Retries with the next agent if one doesn't support HTTP MCP.
521
+ */
522
+ async function runAnalysisWithAgent(options) {
523
+ const { apiUrl, agentName, prompt, repoPath, mcpUrl, authToken, organizationId, stdout, stderr, debug } = options;
524
+ const mcpPurpose = "ingestion_sync";
525
+ let mcpAuthToken;
526
+ try {
527
+ mcpAuthToken = await issuePurposeToken(apiUrl, authToken, organizationId, mcpPurpose);
528
+ }
529
+ catch (error) {
530
+ const message = error instanceof Error ? error.message : String(error);
531
+ stderr(`Error: Could not issue MCP purpose token (${mcpPurpose}): ${message}\n`);
532
+ stderr("Run 'spekn auth login' and ensure an organization is selected.\n");
533
+ return { text: "", error: "purpose-token-issue-failed" };
534
+ }
535
+ // Step 1: Verify MCP server before spawning any agent
536
+ stderr("Verifying MCP server...\n");
537
+ const mcpTools = await verifyMcpServer(mcpUrl, mcpAuthToken, stderr, debug, mcpPurpose);
538
+ if (!mcpTools) {
539
+ stderr("Error: Cannot proceed with analysis — MCP server not available.\n");
540
+ stderr(` Make sure the MCP HTTP server is running at ${mcpUrl}\n`);
541
+ stderr(" Start it with: npm run dev:mcp\n");
542
+ stderr("\n");
543
+ stderr("Troubleshooting steps:\n");
544
+ stderr(` 1. Check if MCP server is running: curl ${mcpUrl}/health\n`);
545
+ stderr(" 2. Enable debug logging: MCP_DEBUG=true npm run dev:mcp\n");
546
+ stderr(" 3. Verify your auth token is valid: spekn auth status\n");
547
+ stderr(" 4. Check Keycloak client configuration for spekn-cli\n");
548
+ return { text: "", error: "MCP server not available" };
549
+ }
550
+ // Step 2: Detect installed agents
551
+ const { detectInstalledAgents, promptForAgentSelection, resolveFromRegistry } = await Promise.resolve().then(() => __importStar(require("@spekn/check")));
552
+ const detected = await detectInstalledAgents();
553
+ const installed = detected.filter((a) => a.installed);
554
+ if (installed.length === 0) {
555
+ stderr("Error: No ACP agents found. Install claude, codex, or opencode.\n");
556
+ return { text: "", error: "No agents available" };
557
+ }
558
+ const agentArgsEnv = process.env.AGENT_ARGS ?? process.env.ACP_ARGS;
559
+ // env override — try only that one agent
560
+ if (agentName && agentArgsEnv !== undefined) {
561
+ const agent = { command: agentName, args: agentArgsEnv ? agentArgsEnv.split(" ") : [] };
562
+ stdout(`Using agent: ${agentName} (AGENT_ARGS override)\n`);
563
+ return executeAcpSession(agent, prompt, repoPath, mcpUrl, mcpAuthToken, stderr, organizationId, mcpPurpose);
564
+ }
565
+ // Determine initial agent selection
566
+ let firstChoice = null;
567
+ if (agentName) {
568
+ firstChoice = agentName;
569
+ }
570
+ else if (installed.length === 1) {
571
+ firstChoice = installed[0].name;
572
+ stderr(`Using detected agent: ${firstChoice}\n`);
573
+ }
574
+ else {
575
+ let selected = null;
576
+ if (process.stdin.isTTY && process.stdout.isTTY) {
577
+ selected = await promptForAgentSelection(detected);
578
+ }
579
+ else {
580
+ selected = await (0, interaction_1.requestSelectionFromController)({
581
+ title: "Agent selection required",
582
+ message: "Multiple ACP agents are installed. Select one for repository analysis.",
583
+ options: installed.map((agent) => ({
584
+ value: agent.name,
585
+ label: agent.name,
586
+ })),
587
+ allowSkip: true,
588
+ });
589
+ }
590
+ if (selected === "skip") {
591
+ return { text: "", error: "No agent selected" };
592
+ }
593
+ if (!selected) {
594
+ firstChoice = installed[0].name;
595
+ stderr(`No interactive selection available. Defaulting to detected agent: ${firstChoice}\n`);
596
+ }
597
+ else {
598
+ firstChoice = selected;
599
+ }
600
+ }
601
+ // Try user's choice first, then remaining agents
602
+ const remaining = [firstChoice, ...installed.map((a) => a.name).filter((n) => n !== firstChoice)];
603
+ for (const candidateName of remaining) {
604
+ const resolved = await resolveFromRegistry(candidateName);
605
+ const agent = resolved
606
+ ? { command: resolved.command, args: resolved.args }
607
+ : { command: candidateName, args: [] };
608
+ stdout(`Trying agent: ${candidateName}...\n`);
609
+ const result = await executeAcpSession(agent, prompt, repoPath, mcpUrl, mcpAuthToken, stderr, organizationId, mcpPurpose);
610
+ if (result.error === "no-http-mcp") {
611
+ stderr(`Agent "${candidateName}" does not support HTTP MCP servers. Trying next...\n`);
612
+ continue;
613
+ }
614
+ return result;
615
+ }
616
+ return { text: "", error: "No agents with HTTP MCP support available" };
617
+ }
618
+ // ── Full Analysis Phase ─────────────────────────────────────────────
619
+ /**
620
+ * Complete analysis phase: discover files → build prompt → run agent.
621
+ * Returns exit code (0 = success, 1 = error).
622
+ */
623
+ async function runAnalysisPhase(options, authToken, deps, organizationId) {
624
+ if (!authToken) {
625
+ deps.stderr("Warning: Not authenticated. Skipping AI analysis. Run 'spekn auth login' first.\n");
626
+ return 0;
627
+ }
628
+ const discovered = discoverAndDisplay(options.repoPath, deps.stdout);
629
+ if (!discovered)
630
+ return 0;
631
+ if (options.dryRun) {
632
+ deps.stdout("\n[dry-run] Skipping AI analysis.\n");
633
+ return 0;
634
+ }
635
+ deps.stdout("\nResolving AI agent...\n");
636
+ const prompt = buildAnalysisPrompt(options.projectId, options.repoPath, discovered, organizationId);
637
+ const result = await runAnalysisWithAgent({
638
+ apiUrl: options.apiUrl,
639
+ agentName: options.agent,
640
+ prompt,
641
+ repoPath: options.repoPath,
642
+ mcpUrl: options.mcpUrl,
643
+ authToken,
644
+ organizationId,
645
+ stdout: deps.stdout,
646
+ stderr: deps.stderr,
647
+ debug: options.debug,
648
+ });
649
+ if (result.error) {
650
+ const errorMessage = typeof result.error === "string"
651
+ ? result.error
652
+ : (() => {
653
+ try {
654
+ return JSON.stringify(result.error);
655
+ }
656
+ catch {
657
+ return String(result.error);
658
+ }
659
+ })();
660
+ deps.stderr(`\nAgent error: ${errorMessage}\n`);
661
+ return 1;
662
+ }
663
+ const reportValidation = validateAnalysisReport(result.text);
664
+ if (!reportValidation.ok) {
665
+ deps.stderr(`\nAnalysis failed quality gate: missing required report sections: ${reportValidation.missing.join(", ")}\n`);
666
+ deps.stderr("The ingestion report must include FEATURE_COVERAGE, MISSING_PLAN_FEATURES, and DECISIONS_CREATED sections.\n");
667
+ return 1;
668
+ }
669
+ deps.stdout("\nAnalysis complete.\n");
670
+ return 0;
671
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runRepoDetachCli(args: string[]): Promise<number>;