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