fifony 0.1.42 → 0.1.43
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/app/dist/assets/{CommandPalette-DNR5umI1.js → CommandPalette-M4VAMxCU.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-Dpl19F20.js → KeyboardShortcutsHelp-DkvPUXQq.js} +1 -1
- package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +1 -0
- package/app/dist/assets/analytics.lazy-zVJdF880.js +1 -0
- package/app/dist/assets/{api-ChEctgc5.js → api-CkVfYg_m.js} +1 -1
- package/app/dist/assets/{createLucideIcon-R47sXufx.js → createLucideIcon-Dfk_Hxud.js} +1 -1
- package/app/dist/assets/index-BpiCi7Ew.css +1 -0
- package/app/dist/assets/index-D2INW0zc.js +47 -0
- package/app/dist/assets/vendor-BEoYbFV1.js +9 -0
- package/app/dist/index.html +5 -5
- package/app/dist/service-worker.js +9 -4
- package/bin/fifony.js +3 -0
- package/dist/agent/pty-daemon.js +177 -0
- package/dist/agent/run-local.js +177 -43
- package/dist/{agent-NNGZEKZH.js → agent-RMQTTUEC.js} +37 -16
- package/dist/analytics-broadcaster-O6YBP66L.js +145 -0
- package/dist/chunk-3NE23NYW.js +82 -0
- package/dist/chunk-42AMQAJG.js +404 -0
- package/dist/{chunk-H5N7O5NP.js → chunk-AILXZ2TD.js} +79 -147
- package/dist/{chunk-I2UHVKHS.js → chunk-BRSR26VK.js} +2 -2
- package/dist/chunk-E2EWEYA4.js +1302 -0
- package/dist/chunk-ESWHDHH6.js +102 -0
- package/dist/{chunk-NB44PCD2.js → chunk-FJNH3G2Z.js} +1061 -1138
- package/dist/chunk-MVTGAKQK.js +493 -0
- package/dist/chunk-QQQLP3PL.js +155 -0
- package/dist/chunk-SOBLO4YZ.js +2016 -0
- package/dist/chunk-YRSH2CLW.js +13784 -0
- package/dist/cli.js +335 -44
- package/dist/{issue-state-machine-GPQNZYUZ.js → fsm-issue-YGGF7SIL.js} +9 -5
- package/dist/helpers-L7NYO5XS.js +53 -0
- package/dist/issue-log-broadcaster-WZAHISYB.js +84 -0
- package/dist/{issues-MZLRSXD6.js → issues-3QRR7KM6.js} +10 -8
- package/dist/log-analyzer-K7MXQB4T.js +287 -0
- package/dist/mcp/server.js +109 -137
- package/dist/parallel-executor-6INE6NDO.js +118 -0
- package/dist/pid-manager-UBWXVSMD.js +21 -0
- package/dist/queue-workers-XFZK3TT5.js +32 -0
- package/dist/replan-issue.command-4UCWYHGZ.js +15 -0
- package/dist/scheduler-ZP7GOZDW.js +26 -0
- package/dist/{settings-NGY33WQE.js → settings-ZAWDCFP2.js} +32 -8
- package/dist/settings.resource-5CW456AZ.js +24 -0
- package/dist/store-M6NCKMZY.js +97 -0
- package/dist/{web-push-CRVDJKWR.js → web-push-AX5IIK3P.js} +2 -2
- package/dist/{workspace-D3F3XGSI.js → workspace-CJTWFWTJ.js} +5 -4
- package/package.json +8 -7
- package/app/dist/assets/OnboardingWizard-CijMhJDW.js +0 -1
- package/app/dist/assets/analytics.lazy-Dq90a756.js +0 -1
- package/app/dist/assets/index-Dy_fM427.js +0 -54
- package/app/dist/assets/index-Q9jBP0Pz.css +0 -1
- package/app/dist/assets/vendor-DkWeBvNl.js +0 -9
- package/dist/chunk-2CVTK5F2.js +0 -288
- package/dist/chunk-37N5OFHM.js +0 -125
- package/dist/chunk-JTKUWIQD.js +0 -8406
- package/dist/chunk-RBDBGU2C.js +0 -303
- package/dist/issue-runner-CMZPSVC7.js +0 -16
- package/dist/queue-workers-XZ6DGH4W.js +0 -23
- package/dist/scheduler-NVE6L3P7.js +0 -22
- package/dist/store-4HCGBN4L.js +0 -65
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolvePlanStageConfig
|
|
3
|
+
} from "./chunk-YRSH2CLW.js";
|
|
4
|
+
import "./chunk-BRSR26VK.js";
|
|
5
|
+
import "./chunk-QQQLP3PL.js";
|
|
6
|
+
import "./chunk-AILXZ2TD.js";
|
|
7
|
+
import "./chunk-SOBLO4YZ.js";
|
|
8
|
+
import "./chunk-E2EWEYA4.js";
|
|
9
|
+
import "./chunk-MVTGAKQK.js";
|
|
10
|
+
import {
|
|
11
|
+
ADAPTERS,
|
|
12
|
+
detectAvailableProviders,
|
|
13
|
+
resolveProviderCapabilities
|
|
14
|
+
} from "./chunk-FJNH3G2Z.js";
|
|
15
|
+
import {
|
|
16
|
+
logger
|
|
17
|
+
} from "./chunk-DVU3CXWA.js";
|
|
18
|
+
import "./chunk-ESWHDHH6.js";
|
|
19
|
+
import {
|
|
20
|
+
TARGET_ROOT,
|
|
21
|
+
appendFileTail,
|
|
22
|
+
extractJsonObjects
|
|
23
|
+
} from "./chunk-42AMQAJG.js";
|
|
24
|
+
import "./chunk-3NE23NYW.js";
|
|
25
|
+
|
|
26
|
+
// src/agents/planning/log-analyzer.ts
|
|
27
|
+
import { env } from "process";
|
|
28
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
29
|
+
import { spawn } from "child_process";
|
|
30
|
+
import { tmpdir } from "os";
|
|
31
|
+
import { join } from "path";
|
|
32
|
+
var HEALTHCHECK_JSON_SCHEMA = JSON.stringify({
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
host: { type: "string" },
|
|
36
|
+
port: { type: "number" },
|
|
37
|
+
protocol: { type: "string", enum: ["http", "https", "tcp"] }
|
|
38
|
+
},
|
|
39
|
+
required: ["host", "port", "protocol"],
|
|
40
|
+
additionalProperties: false
|
|
41
|
+
});
|
|
42
|
+
var FIX_JSON_SCHEMA = JSON.stringify({
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
hasProblem: { type: "boolean" },
|
|
46
|
+
title: { type: "string" },
|
|
47
|
+
description: { type: "string" },
|
|
48
|
+
issueType: { type: "string", enum: ["bug", "chore", "feature"] }
|
|
49
|
+
},
|
|
50
|
+
required: ["hasProblem"],
|
|
51
|
+
additionalProperties: false
|
|
52
|
+
});
|
|
53
|
+
function buildHealthcheckPrompt(logTail, serviceName) {
|
|
54
|
+
return `You are analyzing the startup log of a service called "${serviceName}".
|
|
55
|
+
|
|
56
|
+
Your task: identify the host (IP address or hostname) and port where this service is listening for incoming connections.
|
|
57
|
+
|
|
58
|
+
Look for patterns like:
|
|
59
|
+
- "Listening on http://localhost:3000"
|
|
60
|
+
- "Server started on port 3000"
|
|
61
|
+
- "running on 0.0.0.0:8080"
|
|
62
|
+
- "started on 127.0.0.1:4000"
|
|
63
|
+
- Any URL or address/port combination that indicates where the service is reachable
|
|
64
|
+
|
|
65
|
+
Return ONLY a JSON object with this exact structure:
|
|
66
|
+
{"host": "localhost", "port": 3000, "protocol": "http"}
|
|
67
|
+
|
|
68
|
+
Use protocol "http" unless you clearly see "https" or it's a raw TCP service (then use "tcp").
|
|
69
|
+
If you cannot find a clear host+port in the log, return: {"host": "localhost", "port": 0, "protocol": "tcp"}
|
|
70
|
+
|
|
71
|
+
SERVICE LOG (last lines):
|
|
72
|
+
\`\`\`
|
|
73
|
+
${logTail}
|
|
74
|
+
\`\`\``;
|
|
75
|
+
}
|
|
76
|
+
function buildFixPrompt(logTail, serviceName) {
|
|
77
|
+
return `You are analyzing the log of a service called "${serviceName}".
|
|
78
|
+
|
|
79
|
+
Step 1 \u2014 decide if there is a real actionable problem:
|
|
80
|
+
- Set hasProblem=true if the log contains errors, crashes, unresolved dependencies, misconfigurations, or anything preventing the service from working correctly.
|
|
81
|
+
- Set hasProblem=false if the service started and is running normally (no errors, just startup output or healthy traffic logs).
|
|
82
|
+
|
|
83
|
+
Step 2 \u2014 if hasProblem=true, fill in title, description, and issueType to create a clear issue report a developer can act on:
|
|
84
|
+
- title: short (max 80 chars), specific to the actual error
|
|
85
|
+
- description: root cause, relevant error messages, file paths or commands from the log, and what to investigate
|
|
86
|
+
- issueType: "bug" for crashes/errors, "chore" for config/dependency/tooling issues, "feature" for missing functionality
|
|
87
|
+
|
|
88
|
+
Return ONLY a JSON object:
|
|
89
|
+
{ "hasProblem": true, "title": "...", "description": "...", "issueType": "bug" }
|
|
90
|
+
or
|
|
91
|
+
{ "hasProblem": false }
|
|
92
|
+
|
|
93
|
+
SERVICE LOG (last lines):
|
|
94
|
+
\`\`\`
|
|
95
|
+
${logTail}
|
|
96
|
+
\`\`\``;
|
|
97
|
+
}
|
|
98
|
+
function readProviderOutput(resultFile, fallback) {
|
|
99
|
+
if (existsSync(resultFile)) {
|
|
100
|
+
try {
|
|
101
|
+
return readFileSync(resultFile, "utf8").trim();
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
async function runOneShot(command, provider, prompt, timeoutMs) {
|
|
108
|
+
const tempDir = mkdtempSync(join(tmpdir(), "fifony-log-analyze-"));
|
|
109
|
+
const promptFile = join(tempDir, "prompt.md");
|
|
110
|
+
const resultFile = join(tempDir, "result.txt");
|
|
111
|
+
writeFileSync(promptFile, `${prompt}
|
|
112
|
+
`, "utf8");
|
|
113
|
+
const spawnEnv = {
|
|
114
|
+
...env,
|
|
115
|
+
FIFONY_PROMPT_FILE: promptFile,
|
|
116
|
+
FIFONY_PROMPT: prompt,
|
|
117
|
+
FIFONY_AGENT_PROVIDER: provider,
|
|
118
|
+
FIFONY_RESULT_FILE: resultFile
|
|
119
|
+
};
|
|
120
|
+
return await new Promise((resolve, reject) => {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
let output = "";
|
|
123
|
+
let timedOut = false;
|
|
124
|
+
const child = spawn(command, { shell: true, cwd: TARGET_ROOT, env: spawnEnv });
|
|
125
|
+
if (child.stdin) child.stdin.end();
|
|
126
|
+
child.stdout?.on("data", (chunk) => {
|
|
127
|
+
output = appendFileTail(output, String(chunk), 12e3);
|
|
128
|
+
});
|
|
129
|
+
child.stderr?.on("data", (chunk) => {
|
|
130
|
+
output = appendFileTail(output, String(chunk), 12e3);
|
|
131
|
+
});
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
timedOut = true;
|
|
134
|
+
child.kill("SIGTERM");
|
|
135
|
+
}, Math.max(timeoutMs, 1e3));
|
|
136
|
+
child.on("error", () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
139
|
+
reject(new Error("Could not execute AI command."));
|
|
140
|
+
});
|
|
141
|
+
child.on("close", (code) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
const commandOutput = readProviderOutput(resultFile, output);
|
|
144
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
145
|
+
if (timedOut) {
|
|
146
|
+
reject(new Error(`Log analysis timed out after ${Date.now() - startedAt}ms.`));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (code !== 0 && !commandOutput.trim()) {
|
|
150
|
+
reject(new Error(`Log analysis command failed (exit ${code ?? "unknown"}) with no output.`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (code !== 0) {
|
|
154
|
+
logger.warn({ exitCode: code, provider }, "[LogAnalyzer] Provider exited non-zero but produced output \u2014 attempting to use it");
|
|
155
|
+
}
|
|
156
|
+
resolve(commandOutput);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function extractJsonFromOutput(raw) {
|
|
161
|
+
const text = raw.trim();
|
|
162
|
+
if (!text) return null;
|
|
163
|
+
const codeBlocks = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/gi)].map((m) => m[1].trim()).reverse();
|
|
164
|
+
for (const block of codeBlocks) {
|
|
165
|
+
for (const candidate of extractJsonObjects(block)) {
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(candidate);
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const candidates = extractJsonObjects(text).reverse();
|
|
173
|
+
for (const candidate of candidates) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(candidate);
|
|
176
|
+
if (parsed && typeof parsed === "object") {
|
|
177
|
+
const p = parsed;
|
|
178
|
+
for (const key of ["structured_output", "result", "response", "output"]) {
|
|
179
|
+
const r = p[key];
|
|
180
|
+
if (!r) continue;
|
|
181
|
+
if (typeof r === "object") return r;
|
|
182
|
+
if (typeof r === "string") {
|
|
183
|
+
const clean = r.trim().replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
|
|
184
|
+
for (const inner of extractJsonObjects(clean)) {
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(inner);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(r);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return parsed;
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
async function resolveProvider(config) {
|
|
204
|
+
const { provider: selectedProvider, model: selectedModel } = await resolvePlanStageConfig(config);
|
|
205
|
+
const providers = detectAvailableProviders();
|
|
206
|
+
const isAvailable = providers.some((p) => p.name === selectedProvider && p.available);
|
|
207
|
+
if (!isAvailable) {
|
|
208
|
+
const known = providers.map((e) => `${e.name}:${e.available ? "ok" : "missing"}`).join(", ");
|
|
209
|
+
throw new Error(`Plan provider "${selectedProvider}" is not available. Detected: ${known}`);
|
|
210
|
+
}
|
|
211
|
+
const adapter = ADAPTERS[selectedProvider];
|
|
212
|
+
if (!adapter) throw new Error(`No adapter for provider "${selectedProvider}".`);
|
|
213
|
+
return { provider: selectedProvider, model: selectedModel, adapter };
|
|
214
|
+
}
|
|
215
|
+
function truncateLog(log, maxLines) {
|
|
216
|
+
const lines = log.split("\n");
|
|
217
|
+
return lines.length > maxLines ? lines.slice(-maxLines).join("\n") : log;
|
|
218
|
+
}
|
|
219
|
+
async function analyzeLogForHealthcheck(logTail, serviceName, config) {
|
|
220
|
+
const { provider, model, adapter } = await resolveProvider(config);
|
|
221
|
+
const caps = resolveProviderCapabilities(provider);
|
|
222
|
+
const command = adapter.buildCommand({
|
|
223
|
+
model,
|
|
224
|
+
readOnly: true,
|
|
225
|
+
jsonSchema: caps.structuredOutput.mode !== "none" ? HEALTHCHECK_JSON_SCHEMA : void 0
|
|
226
|
+
});
|
|
227
|
+
const prompt = buildHealthcheckPrompt(truncateLog(logTail, 150), serviceName);
|
|
228
|
+
const timeoutMs = config.commandTimeoutMs ?? 6e4;
|
|
229
|
+
logger.debug({ provider, serviceName }, "[LogAnalyzer] Detecting healthcheck config from log");
|
|
230
|
+
const raw = await runOneShot(command, provider, prompt, timeoutMs);
|
|
231
|
+
const result = extractJsonFromOutput(raw);
|
|
232
|
+
if (!result?.port || result.port <= 0) {
|
|
233
|
+
logger.debug({ provider, serviceName }, "[LogAnalyzer] Could not extract host/port from log");
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const protocol = result.protocol === "https" ? "https" : result.protocol === "tcp" ? "tcp" : "http";
|
|
237
|
+
const host = result.host || "localhost";
|
|
238
|
+
const port = result.port;
|
|
239
|
+
const healthcheck = protocol === "tcp" ? { type: "tcp", port } : { type: "http", endpoint: `${protocol}://${host}:${port}/health`, port };
|
|
240
|
+
logger.info({ provider, serviceName, healthcheck }, "[LogAnalyzer] Healthcheck config detected");
|
|
241
|
+
return healthcheck;
|
|
242
|
+
}
|
|
243
|
+
async function analyzeLogForFix(logTail, serviceName, config) {
|
|
244
|
+
const { provider, model, adapter } = await resolveProvider(config);
|
|
245
|
+
const caps = resolveProviderCapabilities(provider);
|
|
246
|
+
const command = adapter.buildCommand({
|
|
247
|
+
model,
|
|
248
|
+
readOnly: true,
|
|
249
|
+
jsonSchema: caps.structuredOutput.mode !== "none" ? FIX_JSON_SCHEMA : void 0
|
|
250
|
+
});
|
|
251
|
+
const prompt = buildFixPrompt(truncateLog(logTail, 100), serviceName);
|
|
252
|
+
const timeoutMs = config.commandTimeoutMs ?? 6e4;
|
|
253
|
+
logger.debug({ provider, serviceName }, "[LogAnalyzer] Analyzing log for fix suggestion");
|
|
254
|
+
const raw = await runOneShot(command, provider, prompt, timeoutMs);
|
|
255
|
+
const result = extractJsonFromOutput(raw);
|
|
256
|
+
if (result === null) {
|
|
257
|
+
let envelopeKeys = [];
|
|
258
|
+
try {
|
|
259
|
+
const { extractJsonObjects: extractJsonObjects2 } = await import("./helpers-L7NYO5XS.js");
|
|
260
|
+
const candidates = extractJsonObjects2(raw.trim());
|
|
261
|
+
if (candidates.length > 0) envelopeKeys = Object.keys(JSON.parse(candidates[candidates.length - 1]));
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
logger.warn({ provider, serviceName, rawLength: raw.length, envelopeKeys, rawTail: raw.slice(-400) }, "[LogAnalyzer] Could not extract fix suggestion from log");
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (!result.hasProblem) {
|
|
268
|
+
logger.debug({ provider, serviceName }, "[LogAnalyzer] No problem detected in log");
|
|
269
|
+
return { hasProblem: false };
|
|
270
|
+
}
|
|
271
|
+
if (!result.title) {
|
|
272
|
+
logger.warn({ provider, serviceName }, "[LogAnalyzer] hasProblem=true but no title in result");
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const issueType = ["bug", "chore", "feature"].includes(result.issueType ?? "") ? result.issueType : "bug";
|
|
276
|
+
return {
|
|
277
|
+
hasProblem: true,
|
|
278
|
+
title: result.title.slice(0, 120),
|
|
279
|
+
description: result.description ?? "",
|
|
280
|
+
issueType
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
analyzeLogForFix,
|
|
285
|
+
analyzeLogForHealthcheck
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=log-analyzer-K7MXQB4T.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
parseIssueState,
|
|
3
2
|
renderPrompt
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import
|
|
3
|
+
} from "../chunk-ESWHDHH6.js";
|
|
4
|
+
import {
|
|
5
|
+
parseIssueState
|
|
6
|
+
} from "../chunk-42AMQAJG.js";
|
|
6
7
|
|
|
7
8
|
// src/mcp/server.ts
|
|
8
9
|
import { env as env3, stdin } from "process";
|
|
@@ -210,7 +211,7 @@ async function getIssue(issueId) {
|
|
|
210
211
|
const issue = await issueResource?.get(issueId);
|
|
211
212
|
return issue ?? null;
|
|
212
213
|
}
|
|
213
|
-
async function appendEvent(level, message,
|
|
214
|
+
async function appendEvent(level, message, _payload = {}, issueId) {
|
|
214
215
|
await initDatabase();
|
|
215
216
|
const { randomUUID } = await import("crypto");
|
|
216
217
|
await eventResource?.insert({
|
|
@@ -488,7 +489,7 @@ async function readResource(uri) {
|
|
|
488
489
|
if (diffMatch) {
|
|
489
490
|
const issueId2 = decodeURIComponent(diffMatch[1]);
|
|
490
491
|
try {
|
|
491
|
-
const result = await apiGet(`/api/
|
|
492
|
+
const result = await apiGet(`/api/issues/${encodeURIComponent(issueId2)}/diff`);
|
|
492
493
|
return [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }];
|
|
493
494
|
} catch (error) {
|
|
494
495
|
return [{ uri, mimeType: "application/json", text: JSON.stringify({ error: String(error) }, null, 2) }];
|
|
@@ -530,6 +531,7 @@ function listTools() {
|
|
|
530
531
|
{ name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
531
532
|
{ name: "fifony.integration_snippet", description: "Generate a workflow or prompt snippet for a discovered integration.", inputSchema: { type: "object", properties: { integration: { type: "string" } }, required: ["integration"], additionalProperties: false } },
|
|
532
533
|
{ name: "fifony.get_issue", description: "Get full detail of a single issue including plan, history, events, and diff status.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
|
|
534
|
+
{ name: "fifony.build_context", description: "Build the same ranked context pack used internally by Fifony for planner/executor/reviewer prompts.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." }, role: { type: "string", enum: ["planner", "executor", "reviewer"], description: "Optional role override for context building." } }, required: ["issueId"], additionalProperties: false } },
|
|
533
535
|
{ name: "fifony.cancel_issue", description: "Cancel an issue, moving it to Cancelled state.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to cancel." } }, required: ["issueId"], additionalProperties: false } },
|
|
534
536
|
{ name: "fifony.retry_issue", description: "Retry a failed or blocked issue, re-queuing it for execution.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to retry." } }, required: ["issueId"], additionalProperties: false } },
|
|
535
537
|
{ name: "fifony.enhance", description: "AI-enhance an issue title or description. Provide either an issueId to enhance an existing issue, or title+description for standalone enhancement.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "Issue identifier (optional, for existing issues)." }, title: { type: "string", description: "Issue title (for standalone enhancement)." }, description: { type: "string", description: "Issue description (for standalone enhancement)." }, field: { type: "string", enum: ["title", "description"], description: "Which field to enhance." } }, required: ["field"], additionalProperties: false } },
|
|
@@ -692,7 +694,7 @@ async function callTool(name, args = {}) {
|
|
|
692
694
|
let diff = null;
|
|
693
695
|
let events = [];
|
|
694
696
|
try {
|
|
695
|
-
diff = await apiGet(`/api/
|
|
697
|
+
diff = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/diff`);
|
|
696
698
|
} catch {
|
|
697
699
|
}
|
|
698
700
|
try {
|
|
@@ -709,6 +711,18 @@ async function callTool(name, args = {}) {
|
|
|
709
711
|
recentEvents: events.slice(0, 20)
|
|
710
712
|
}, null, 2));
|
|
711
713
|
}
|
|
714
|
+
if (name === "fifony.build_context") {
|
|
715
|
+
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
716
|
+
const role = typeof args.role === "string" ? args.role.trim() : "";
|
|
717
|
+
if (!issueId) throw new Error("issueId is required");
|
|
718
|
+
try {
|
|
719
|
+
const suffix = role ? `?role=${encodeURIComponent(role)}` : "";
|
|
720
|
+
const result = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/context${suffix}`);
|
|
721
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
722
|
+
} catch (error) {
|
|
723
|
+
throw new Error(`Failed to build context for ${issueId}: ${String(error)}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
712
726
|
if (name === "fifony.cancel_issue") {
|
|
713
727
|
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
714
728
|
if (!issueId) throw new Error("issueId is required");
|
|
@@ -754,7 +768,7 @@ async function callTool(name, args = {}) {
|
|
|
754
768
|
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
755
769
|
if (!issueId) throw new Error("issueId is required");
|
|
756
770
|
try {
|
|
757
|
-
const result = await apiGet(`/api/
|
|
771
|
+
const result = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/diff`);
|
|
758
772
|
return toolText(JSON.stringify(result, null, 2));
|
|
759
773
|
} catch (error) {
|
|
760
774
|
throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
|
|
@@ -764,7 +778,7 @@ async function callTool(name, args = {}) {
|
|
|
764
778
|
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
765
779
|
if (!issueId) throw new Error("issueId is required");
|
|
766
780
|
try {
|
|
767
|
-
const result = await apiGet(`/api/
|
|
781
|
+
const result = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/live`);
|
|
768
782
|
return toolText(JSON.stringify(result, null, 2));
|
|
769
783
|
} catch (error) {
|
|
770
784
|
throw new Error(`Failed to get live output for ${issueId}: ${String(error)}`);
|
|
@@ -857,6 +871,11 @@ function listPrompts() {
|
|
|
857
871
|
}
|
|
858
872
|
|
|
859
873
|
// src/mcp/prompts/prompt-handler.ts
|
|
874
|
+
function stringifyValue(value, fallback) {
|
|
875
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
876
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
877
|
+
return fallback;
|
|
878
|
+
}
|
|
860
879
|
async function getPrompt(name, args = {}) {
|
|
861
880
|
if (name === "fifony-integrate-client") {
|
|
862
881
|
const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
|
|
@@ -916,40 +935,33 @@ async function getPrompt(name, args = {}) {
|
|
|
916
935
|
const state = issueData.state ?? "Unknown";
|
|
917
936
|
const attempts = issueData.attempts ?? 0;
|
|
918
937
|
const maxAttempts = issueData.maxAttempts ?? 3;
|
|
919
|
-
const diagnosticText = [
|
|
920
|
-
`# Diagnostic Report for Issue ${issueId}`,
|
|
921
|
-
``,
|
|
922
|
-
`## Issue Details`,
|
|
923
|
-
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
924
|
-
`- **State**: ${state}`,
|
|
925
|
-
`- **Attempts**: ${attempts} / ${maxAttempts}`,
|
|
926
|
-
`- **Last Error**: ${lastError ?? "None"}`,
|
|
927
|
-
`- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
|
|
928
|
-
``,
|
|
929
|
-
`## Plan`,
|
|
930
|
-
plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
|
|
931
|
-
plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
|
|
932
|
-
plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
|
|
933
|
-
``,
|
|
934
|
-
`## History`,
|
|
935
|
-
...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
|
|
936
|
-
``,
|
|
937
|
-
`## Recent Events`,
|
|
938
|
-
...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
|
|
939
|
-
``,
|
|
940
|
-
`## Diagnostic Questions`,
|
|
941
|
-
`Based on the information above, please analyze:`,
|
|
942
|
-
`1. What is the root cause of the issue being in "${state}" state?`,
|
|
943
|
-
`2. Is the error recoverable? If so, what steps should be taken?`,
|
|
944
|
-
`3. Does the plan need modification before retrying?`,
|
|
945
|
-
`4. Are there any dependency or configuration issues that need resolution?`,
|
|
946
|
-
`5. What is the recommended next action?`
|
|
947
|
-
].filter((line) => line !== void 0).join("\n");
|
|
948
938
|
return {
|
|
949
939
|
description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
|
|
950
940
|
messages: [{
|
|
951
941
|
role: "user",
|
|
952
|
-
content: {
|
|
942
|
+
content: {
|
|
943
|
+
type: "text",
|
|
944
|
+
text: await renderPrompt("mcp-diagnose-blocked", {
|
|
945
|
+
issueId,
|
|
946
|
+
title: stringifyValue(issueData.title, "Unknown"),
|
|
947
|
+
state,
|
|
948
|
+
attempts,
|
|
949
|
+
maxAttempts,
|
|
950
|
+
lastError: stringifyValue(lastError, "None"),
|
|
951
|
+
updatedAt: stringifyValue(issueData.updatedAt, "Unknown"),
|
|
952
|
+
hasPlan: !!plan,
|
|
953
|
+
planSummary: stringifyValue(plan?.summary ?? plan?.title, "No summary"),
|
|
954
|
+
hasPlanSteps: Array.isArray(plan?.steps) && plan.steps.length > 0,
|
|
955
|
+
planStepsCount: Array.isArray(plan?.steps) ? plan.steps.length : 0,
|
|
956
|
+
planComplexity: stringifyValue(plan?.estimatedComplexity, ""),
|
|
957
|
+
history: history.length > 0 ? history.slice(-15).map((entry) => stringifyValue(entry, "Unknown history entry")) : [],
|
|
958
|
+
recentEvents: events.length > 0 ? events.slice(0, 15).map((event) => ({
|
|
959
|
+
kind: stringifyValue(event?.kind, "info"),
|
|
960
|
+
at: stringifyValue(event?.at, "unknown time"),
|
|
961
|
+
message: stringifyValue(event?.message, "")
|
|
962
|
+
})) : []
|
|
963
|
+
})
|
|
964
|
+
}
|
|
953
965
|
}]
|
|
954
966
|
};
|
|
955
967
|
}
|
|
@@ -977,39 +989,26 @@ async function getPrompt(name, args = {}) {
|
|
|
977
989
|
const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
|
|
978
990
|
const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
|
|
979
991
|
const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
|
|
980
|
-
const summaryText = [
|
|
981
|
-
`# Fifony Weekly Progress Summary`,
|
|
982
|
-
``,
|
|
983
|
-
`## Issue Statistics`,
|
|
984
|
-
`| Status | Count |`,
|
|
985
|
-
`|--------|-------|`,
|
|
986
|
-
`| Total Issues | ${totalIssues} |`,
|
|
987
|
-
`| Completed (Done) | ${completed} |`,
|
|
988
|
-
`| In Progress | ${inProgress} |`,
|
|
989
|
-
`| Planned | ${planned} |`,
|
|
990
|
-
`| Planning | ${planning} |`,
|
|
991
|
-
`| Blocked/Failed | ${blocked} |`,
|
|
992
|
-
`| Cancelled | ${cancelled} |`,
|
|
993
|
-
``,
|
|
994
|
-
`## Token Usage`,
|
|
995
|
-
`- **Total Tokens**: ${totalTokens.toLocaleString()}`,
|
|
996
|
-
`- **Input Tokens**: ${inputTokens.toLocaleString()}`,
|
|
997
|
-
`- **Output Tokens**: ${outputTokens.toLocaleString()}`,
|
|
998
|
-
`- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
|
|
999
|
-
``,
|
|
1000
|
-
`## Analysis Request`,
|
|
1001
|
-
`Based on these metrics, please provide:`,
|
|
1002
|
-
`1. A brief summary of overall progress this week`,
|
|
1003
|
-
`2. Identification of any bottlenecks (blocked/failed issues)`,
|
|
1004
|
-
`3. Token usage efficiency assessment`,
|
|
1005
|
-
`4. Recommendations for improving throughput`,
|
|
1006
|
-
`5. Priority items for next week`
|
|
1007
|
-
].join("\n");
|
|
1008
992
|
return {
|
|
1009
993
|
description: "Weekly progress summary prompt for the Fifony workspace.",
|
|
1010
994
|
messages: [{
|
|
1011
995
|
role: "user",
|
|
1012
|
-
content: {
|
|
996
|
+
content: {
|
|
997
|
+
type: "text",
|
|
998
|
+
text: await renderPrompt("mcp-weekly-summary", {
|
|
999
|
+
totalIssues,
|
|
1000
|
+
completed,
|
|
1001
|
+
inProgress,
|
|
1002
|
+
planned,
|
|
1003
|
+
planning,
|
|
1004
|
+
blocked,
|
|
1005
|
+
cancelled,
|
|
1006
|
+
totalTokensFormatted: totalTokens.toLocaleString(),
|
|
1007
|
+
inputTokensFormatted: inputTokens.toLocaleString(),
|
|
1008
|
+
outputTokensFormatted: outputTokens.toLocaleString(),
|
|
1009
|
+
estimatedCostFormatted: (Math.round(estimatedCost * 100) / 100).toFixed(2)
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1013
1012
|
}]
|
|
1014
1013
|
};
|
|
1015
1014
|
}
|
|
@@ -1021,41 +1020,28 @@ async function getPrompt(name, args = {}) {
|
|
|
1021
1020
|
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
1022
1021
|
const issueData = issue;
|
|
1023
1022
|
const plan = issueData.plan ?? null;
|
|
1024
|
-
const steps = plan?.steps
|
|
1025
|
-
const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
|
|
1026
|
-
${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
|
|
1027
|
-
const refinementText = [
|
|
1028
|
-
`# Plan Refinement for Issue ${issueId}`,
|
|
1029
|
-
``,
|
|
1030
|
-
`## Issue`,
|
|
1031
|
-
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
1032
|
-
`- **Description**: ${issueData.description ?? "No description"}`,
|
|
1033
|
-
``,
|
|
1034
|
-
`## Current Plan`,
|
|
1035
|
-
plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
|
|
1036
|
-
plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
|
|
1037
|
-
``,
|
|
1038
|
-
`### Steps`,
|
|
1039
|
-
stepsText,
|
|
1040
|
-
``,
|
|
1041
|
-
concern ? `## Specific Concern
|
|
1042
|
-
${concern}
|
|
1043
|
-
` : "",
|
|
1044
|
-
`## Refinement Guidance`,
|
|
1045
|
-
`Please review the current plan and provide specific, actionable feedback:`,
|
|
1046
|
-
`1. Are the steps correctly ordered and complete?`,
|
|
1047
|
-
`2. Are there missing edge cases or error handling steps?`,
|
|
1048
|
-
`3. Is the complexity estimate accurate?`,
|
|
1049
|
-
`4. Are the file paths and affected areas correct?`,
|
|
1050
|
-
`5. Should any steps be split, merged, or removed?`,
|
|
1051
|
-
``,
|
|
1052
|
-
`Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
|
|
1053
|
-
].filter((line) => line !== void 0).join("\n");
|
|
1023
|
+
const steps = Array.isArray(plan?.steps) ? plan.steps : [];
|
|
1054
1024
|
return {
|
|
1055
1025
|
description: `Plan refinement prompt for issue ${issueId}.`,
|
|
1056
1026
|
messages: [{
|
|
1057
1027
|
role: "user",
|
|
1058
|
-
content: {
|
|
1028
|
+
content: {
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: await renderPrompt("mcp-refine-plan", {
|
|
1031
|
+
issueId,
|
|
1032
|
+
title: stringifyValue(issueData.title, "Unknown"),
|
|
1033
|
+
description: stringifyValue(issueData.description, "No description"),
|
|
1034
|
+
hasPlan: !!plan,
|
|
1035
|
+
planSummary: stringifyValue(plan?.summary ?? plan?.title, "No summary"),
|
|
1036
|
+
planComplexity: stringifyValue(plan?.estimatedComplexity, ""),
|
|
1037
|
+
steps: steps.map((step, index) => ({
|
|
1038
|
+
index: index + 1,
|
|
1039
|
+
title: stringifyValue(step?.title ?? step?.description, "Step"),
|
|
1040
|
+
description: stringifyValue(step?.description ?? step?.detail, "")
|
|
1041
|
+
})),
|
|
1042
|
+
concern
|
|
1043
|
+
})
|
|
1044
|
+
}
|
|
1059
1045
|
}]
|
|
1060
1046
|
};
|
|
1061
1047
|
}
|
|
@@ -1067,7 +1053,7 @@ ${concern}
|
|
|
1067
1053
|
const issueData = issue;
|
|
1068
1054
|
let diffData = {};
|
|
1069
1055
|
try {
|
|
1070
|
-
diffData = await apiGet(`/api/
|
|
1056
|
+
diffData = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/diff`);
|
|
1071
1057
|
} catch (error) {
|
|
1072
1058
|
throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
|
|
1073
1059
|
}
|
|
@@ -1080,51 +1066,37 @@ ${concern}
|
|
|
1080
1066
|
description: `Code review prompt for issue ${issueId} (no changes).`,
|
|
1081
1067
|
messages: [{
|
|
1082
1068
|
role: "user",
|
|
1083
|
-
content: {
|
|
1084
|
-
|
|
1085
|
-
|
|
1069
|
+
content: {
|
|
1070
|
+
type: "text",
|
|
1071
|
+
text: await renderPrompt("mcp-code-review-empty", { issueId })
|
|
1072
|
+
}
|
|
1086
1073
|
}]
|
|
1087
1074
|
};
|
|
1088
1075
|
}
|
|
1089
|
-
const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
|
|
1090
|
-
const reviewText = [
|
|
1091
|
-
`# Code Review for Issue ${issueId}`,
|
|
1092
|
-
``,
|
|
1093
|
-
`## Issue Context`,
|
|
1094
|
-
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
1095
|
-
`- **Description**: ${issueData.description ?? "No description"}`,
|
|
1096
|
-
`- **State**: ${issueData.state ?? "Unknown"}`,
|
|
1097
|
-
``,
|
|
1098
|
-
`## Change Summary`,
|
|
1099
|
-
`- **Files Changed**: ${files.length}`,
|
|
1100
|
-
`- **Total Additions**: +${totalAdditions}`,
|
|
1101
|
-
`- **Total Deletions**: -${totalDeletions}`,
|
|
1102
|
-
``,
|
|
1103
|
-
`### Files`,
|
|
1104
|
-
`| Path | Status | Additions | Deletions |`,
|
|
1105
|
-
`|------|--------|-----------|-----------|`,
|
|
1106
|
-
filesTable,
|
|
1107
|
-
``,
|
|
1108
|
-
`## Diff`,
|
|
1109
|
-
"```diff",
|
|
1110
|
-
diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
|
|
1111
|
-
"```",
|
|
1112
|
-
``,
|
|
1113
|
-
`## Review Checklist`,
|
|
1114
|
-
`Please review the changes and evaluate:`,
|
|
1115
|
-
`1. **Correctness**: Do the changes correctly implement what the issue describes?`,
|
|
1116
|
-
`2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
|
|
1117
|
-
`3. **Error Handling**: Are edge cases and errors properly handled?`,
|
|
1118
|
-
`4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
|
|
1119
|
-
`5. **Performance**: Are there any performance concerns or inefficiencies?`,
|
|
1120
|
-
`6. **Tests**: Are changes adequately covered by tests?`,
|
|
1121
|
-
`7. **Breaking Changes**: Do any changes break backward compatibility?`
|
|
1122
|
-
].join("\n");
|
|
1123
1076
|
return {
|
|
1124
1077
|
description: `Code review prompt for issue ${issueId}.`,
|
|
1125
1078
|
messages: [{
|
|
1126
1079
|
role: "user",
|
|
1127
|
-
content: {
|
|
1080
|
+
content: {
|
|
1081
|
+
type: "text",
|
|
1082
|
+
text: await renderPrompt("mcp-code-review", {
|
|
1083
|
+
issueId,
|
|
1084
|
+
title: stringifyValue(issueData.title, "Unknown"),
|
|
1085
|
+
description: stringifyValue(issueData.description, "No description"),
|
|
1086
|
+
state: stringifyValue(issueData.state, "Unknown"),
|
|
1087
|
+
filesChanged: files.length,
|
|
1088
|
+
totalAdditions,
|
|
1089
|
+
totalDeletions,
|
|
1090
|
+
files: files.map((file) => ({
|
|
1091
|
+
path: stringifyValue(file?.path, "(unknown)"),
|
|
1092
|
+
status: stringifyValue(file?.status, "modified"),
|
|
1093
|
+
additions: typeof file?.additions === "number" ? file.additions : 0,
|
|
1094
|
+
deletions: typeof file?.deletions === "number" ? file.deletions : 0
|
|
1095
|
+
})),
|
|
1096
|
+
diff: diff.length > 5e4 ? `${diff.substring(0, 5e4)}
|
|
1097
|
+
... (diff truncated at 50KB)` : diff
|
|
1098
|
+
})
|
|
1099
|
+
}
|
|
1128
1100
|
}]
|
|
1129
1101
|
};
|
|
1130
1102
|
}
|