fifony 0.1.21 → 0.1.22
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 +7 -0
- package/app/dist/assets/{KeyboardShortcutsHelp-BTjiQe_Y.js → KeyboardShortcutsHelp-DFstgyXD.js} +1 -1
- package/app/dist/assets/OnboardingWizard-Daehu2Uj.js +1 -0
- package/app/dist/assets/analytics.lazy-C1-iSRM_.js +1 -0
- package/app/dist/assets/{createLucideIcon-DtZs0TX0.js → createLucideIcon-BWC-guQt.js} +1 -1
- package/app/dist/assets/index-DbIrs0MK.css +1 -0
- package/app/dist/assets/index-O_FDwkw6.js +43 -0
- package/app/dist/assets/vendor-BTlTWMUF.js +9 -0
- package/app/dist/index.html +4 -5
- package/app/dist/service-worker.js +1 -1
- package/bin/fifony-wrap.js +53 -0
- package/dist/agent/cli-wrapper.js +78 -0
- package/dist/agent/cli-wrapper.js.map +1 -0
- package/dist/agent/run-local.js +93 -7889
- package/dist/agent/run-local.js.map +1 -1
- package/dist/chunk-F6JEQIP2.js +449 -0
- package/dist/chunk-F6JEQIP2.js.map +1 -0
- package/dist/{chunk-SMGXYOWU.js → chunk-O665NS5E.js} +411 -9
- package/dist/chunk-O665NS5E.js.map +1 -0
- package/dist/chunk-VP6TGOMT.js +8915 -0
- package/dist/chunk-VP6TGOMT.js.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -1
- package/dist/issue-runner-MDCJ4G26.js +11 -0
- package/dist/issue-runner-MDCJ4G26.js.map +1 -0
- package/dist/mcp/server.js +589 -595
- package/dist/mcp/server.js.map +1 -1
- package/dist/queue-workers-LAYOT4E5.js +21 -0
- package/dist/queue-workers-LAYOT4E5.js.map +1 -0
- package/package.json +10 -9
- package/app/dist/assets/OnboardingWizard-BALlquG0.js +0 -1
- package/app/dist/assets/analytics.lazy-DjSzXIey.js +0 -1
- package/app/dist/assets/index-BV11ScVl.js +0 -42
- package/app/dist/assets/index-DWbxgKSd.css +0 -1
- package/app/dist/assets/vendor-BoGBoEwT.js +0 -9
- package/app/dist/assets/zap-DpjdVd1i.js +0 -1
- package/dist/chunk-SMGXYOWU.js.map +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,105 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
inferCapabilityPaths,
|
|
3
|
+
parseIssueState,
|
|
3
4
|
renderPrompt,
|
|
4
5
|
resolveTaskCapabilities
|
|
5
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-O665NS5E.js";
|
|
6
7
|
|
|
7
8
|
// src/mcp/server.ts
|
|
8
|
-
import {
|
|
9
|
-
import { existsSync as existsSync3 } from "fs";
|
|
10
|
-
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
11
|
-
import { env as env2, stdin, stdout } from "process";
|
|
12
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13
|
-
|
|
14
|
-
// src/integrations/catalog.ts
|
|
15
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join, resolve } from "path";
|
|
18
|
-
function listNames(basePath) {
|
|
19
|
-
if (!existsSync(basePath)) {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
return readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isFile()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
23
|
-
}
|
|
24
|
-
function readSkillSummary(skillPath) {
|
|
25
|
-
try {
|
|
26
|
-
const skillFile = join(skillPath, "SKILL.md");
|
|
27
|
-
if (!existsSync(skillFile)) {
|
|
28
|
-
return "";
|
|
29
|
-
}
|
|
30
|
-
const contents = readFileSync(skillFile, "utf8");
|
|
31
|
-
const firstParagraph = contents.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => !line.startsWith("#"));
|
|
32
|
-
return firstParagraph ?? "";
|
|
33
|
-
} catch {
|
|
34
|
-
return "";
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function discoverIntegrations(workspaceRoot) {
|
|
38
|
-
const home = homedir();
|
|
39
|
-
const agentLocations = [
|
|
40
|
-
resolve(workspaceRoot, ".codex", "agents"),
|
|
41
|
-
resolve(workspaceRoot, "agents"),
|
|
42
|
-
join(home, ".codex", "agents"),
|
|
43
|
-
join(home, ".claude", "agents")
|
|
44
|
-
];
|
|
45
|
-
const skillLocations = [
|
|
46
|
-
resolve(workspaceRoot, ".codex", "skills"),
|
|
47
|
-
resolve(workspaceRoot, ".claude", "skills"),
|
|
48
|
-
join(home, ".codex", "skills"),
|
|
49
|
-
join(home, ".claude", "skills")
|
|
50
|
-
];
|
|
51
|
-
const agencyItems = agentLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(({ name }) => name.startsWith("agency-")).map(({ location, name }) => `${name} @ ${location}`);
|
|
52
|
-
const impeccableItems = skillLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(
|
|
53
|
-
({ name }) => name === "teach-impeccable" || name === "frontend-design" || name === "polish" || name === "audit" || name === "critique" || name.includes("impeccable")
|
|
54
|
-
).map(({ location, name }) => {
|
|
55
|
-
const summary = readSkillSummary(join(location, name));
|
|
56
|
-
return summary ? `${name} @ ${location} \u2014 ${summary}` : `${name} @ ${location}`;
|
|
57
|
-
});
|
|
58
|
-
return [
|
|
59
|
-
{
|
|
60
|
-
id: "agency-agents",
|
|
61
|
-
kind: "agents",
|
|
62
|
-
installed: agencyItems.length > 0,
|
|
63
|
-
locations: agentLocations.filter((location) => existsSync(location)),
|
|
64
|
-
items: agencyItems,
|
|
65
|
-
summary: agencyItems.length > 0 ? "Local specialized agent profiles are available for planner/executor/reviewer roles." : "No agency agent profiles were detected in the standard local locations."
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: "impeccable",
|
|
69
|
-
kind: "skills",
|
|
70
|
-
installed: impeccableItems.length > 0,
|
|
71
|
-
locations: skillLocations.filter((location) => existsSync(location)),
|
|
72
|
-
items: impeccableItems,
|
|
73
|
-
summary: impeccableItems.length > 0 ? "Frontend and design-oriented skills are available for review and polish workflows." : "No impeccable-related skills were detected in the standard local skill directories."
|
|
74
|
-
}
|
|
75
|
-
];
|
|
76
|
-
}
|
|
77
|
-
async function buildIntegrationSnippet(integrationId, workspaceRoot) {
|
|
78
|
-
if (integrationId === "agency-agents") {
|
|
79
|
-
return renderPrompt("integrations-agency-agents", { workspaceRoot });
|
|
80
|
-
}
|
|
81
|
-
if (integrationId === "impeccable") {
|
|
82
|
-
return renderPrompt("integrations-impeccable");
|
|
83
|
-
}
|
|
84
|
-
return "Unknown integration.";
|
|
85
|
-
}
|
|
9
|
+
import { env as env3, stdin } from "process";
|
|
86
10
|
|
|
87
11
|
// src/mcp/database.ts
|
|
88
|
-
import { existsSync
|
|
89
|
-
import { basename, join
|
|
12
|
+
import { existsSync, readFileSync } from "fs";
|
|
13
|
+
import { basename, join, resolve } from "path";
|
|
90
14
|
import { env } from "process";
|
|
91
|
-
import { homedir
|
|
15
|
+
import { homedir } from "os";
|
|
92
16
|
import { fileURLToPath } from "url";
|
|
93
17
|
var WORKSPACE_ROOT = env.FIFONY_WORKSPACE_ROOT ?? process.cwd();
|
|
94
18
|
var PERSISTENCE_ROOT = env.FIFONY_PERSISTENCE ?? WORKSPACE_ROOT;
|
|
95
19
|
var STATE_ROOT = resolvePersistenceRoot(PERSISTENCE_ROOT);
|
|
96
|
-
var DATABASE_PATH =
|
|
20
|
+
var DATABASE_PATH = join(STATE_ROOT, "s3db");
|
|
97
21
|
var STORAGE_BUCKET = env.FIFONY_STORAGE_BUCKET ?? "fifony";
|
|
98
22
|
var STORAGE_KEY_PREFIX = env.FIFONY_STORAGE_KEY_PREFIX ?? "state";
|
|
99
23
|
var DEBUG_BOOT = env.FIFONY_DEBUG_BOOT === "1";
|
|
100
24
|
function resolvePersistenceRoot(value) {
|
|
101
|
-
const resolved = value.startsWith("file://") ? fileURLToPath(value) : value.startsWith("~/") ?
|
|
102
|
-
return basename(resolved) === ".fifony" ? resolved :
|
|
25
|
+
const resolved = value.startsWith("file://") ? fileURLToPath(value) : value.startsWith("~/") ? resolve(homedir(), value.slice(2)) : resolve(value);
|
|
26
|
+
return basename(resolved) === ".fifony" ? resolved : join(resolved, ".fifony");
|
|
103
27
|
}
|
|
104
28
|
var RUNTIME_RESOURCE = "runtime_state";
|
|
105
29
|
var ISSUE_RESOURCE = "issues";
|
|
@@ -128,7 +52,7 @@ function nowIso() {
|
|
|
128
52
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
129
53
|
}
|
|
130
54
|
function safeRead(path) {
|
|
131
|
-
return
|
|
55
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
132
56
|
}
|
|
133
57
|
async function loadS3dbModule() {
|
|
134
58
|
try {
|
|
@@ -315,23 +239,134 @@ async function appendEvent(level, message, payload = {}, issueId) {
|
|
|
315
239
|
});
|
|
316
240
|
}
|
|
317
241
|
|
|
318
|
-
// src/mcp/
|
|
242
|
+
// src/mcp/jsonrpc-transport.ts
|
|
243
|
+
import { stdout } from "process";
|
|
244
|
+
|
|
245
|
+
// src/integrations/catalog.ts
|
|
246
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
247
|
+
import { homedir as homedir2 } from "os";
|
|
248
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
249
|
+
function listNames(basePath) {
|
|
250
|
+
if (!existsSync2(basePath)) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
return readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isFile()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
254
|
+
}
|
|
255
|
+
function readSkillSummary(skillPath) {
|
|
256
|
+
try {
|
|
257
|
+
const skillFile = join2(skillPath, "SKILL.md");
|
|
258
|
+
if (!existsSync2(skillFile)) {
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
const contents = readFileSync2(skillFile, "utf8");
|
|
262
|
+
const firstParagraph = contents.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => !line.startsWith("#"));
|
|
263
|
+
return firstParagraph ?? "";
|
|
264
|
+
} catch {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function discoverIntegrations(workspaceRoot) {
|
|
269
|
+
const home = homedir2();
|
|
270
|
+
const agentLocations = [
|
|
271
|
+
resolve2(workspaceRoot, ".codex", "agents"),
|
|
272
|
+
resolve2(workspaceRoot, "agents"),
|
|
273
|
+
join2(home, ".codex", "agents"),
|
|
274
|
+
join2(home, ".claude", "agents")
|
|
275
|
+
];
|
|
276
|
+
const skillLocations = [
|
|
277
|
+
resolve2(workspaceRoot, ".codex", "skills"),
|
|
278
|
+
resolve2(workspaceRoot, ".claude", "skills"),
|
|
279
|
+
join2(home, ".codex", "skills"),
|
|
280
|
+
join2(home, ".claude", "skills")
|
|
281
|
+
];
|
|
282
|
+
const agencyItems = agentLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(({ name }) => name.startsWith("agency-")).map(({ location, name }) => `${name} @ ${location}`);
|
|
283
|
+
const impeccableItems = skillLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(
|
|
284
|
+
({ name }) => name === "teach-impeccable" || name === "frontend-design" || name === "polish" || name === "audit" || name === "critique" || name.includes("impeccable")
|
|
285
|
+
).map(({ location, name }) => {
|
|
286
|
+
const summary = readSkillSummary(join2(location, name));
|
|
287
|
+
return summary ? `${name} @ ${location} \u2014 ${summary}` : `${name} @ ${location}`;
|
|
288
|
+
});
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
id: "agency-agents",
|
|
292
|
+
kind: "agents",
|
|
293
|
+
installed: agencyItems.length > 0,
|
|
294
|
+
locations: agentLocations.filter((location) => existsSync2(location)),
|
|
295
|
+
items: agencyItems,
|
|
296
|
+
summary: agencyItems.length > 0 ? "Local specialized agent profiles are available for planner/executor/reviewer roles." : "No agency agent profiles were detected in the standard local locations."
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "impeccable",
|
|
300
|
+
kind: "skills",
|
|
301
|
+
installed: impeccableItems.length > 0,
|
|
302
|
+
locations: skillLocations.filter((location) => existsSync2(location)),
|
|
303
|
+
items: impeccableItems,
|
|
304
|
+
summary: impeccableItems.length > 0 ? "Frontend and design-oriented skills are available for review and polish workflows." : "No impeccable-related skills were detected in the standard local skill directories."
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
async function buildIntegrationSnippet(integrationId, workspaceRoot) {
|
|
309
|
+
if (integrationId === "agency-agents") {
|
|
310
|
+
return renderPrompt("integrations-agency-agents", { workspaceRoot });
|
|
311
|
+
}
|
|
312
|
+
if (integrationId === "impeccable") {
|
|
313
|
+
return renderPrompt("integrations-impeccable");
|
|
314
|
+
}
|
|
315
|
+
return "Unknown integration.";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/mcp/api-client.ts
|
|
319
|
+
import { env as env2 } from "process";
|
|
320
|
+
async function resolveApiBaseUrl() {
|
|
321
|
+
const envPort = env2.FIFONY_API_PORT;
|
|
322
|
+
if (envPort) return `http://localhost:${envPort}`;
|
|
323
|
+
const runtime = await getRuntimeSnapshot();
|
|
324
|
+
const config = runtime.config;
|
|
325
|
+
const port = config?.dashboardPort;
|
|
326
|
+
if (port) return `http://localhost:${port}`;
|
|
327
|
+
for (const candidate of [4e3, 3e3, 8080]) {
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch(`http://localhost:${candidate}/health`, { signal: AbortSignal.timeout(1e3) });
|
|
330
|
+
if (res.ok) return `http://localhost:${candidate}`;
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
throw new Error("Fifony runtime API is not reachable. Start the runtime with --port to enable plan/refine/approve/analytics tools.");
|
|
335
|
+
}
|
|
336
|
+
async function apiPost(path, body = {}) {
|
|
337
|
+
const base = await resolveApiBaseUrl();
|
|
338
|
+
const res = await fetch(`${base}${path}`, {
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { "content-type": "application/json" },
|
|
341
|
+
body: JSON.stringify(body),
|
|
342
|
+
signal: AbortSignal.timeout(12e4)
|
|
343
|
+
});
|
|
344
|
+
const json = await res.json();
|
|
345
|
+
if (!res.ok || json.ok === false) {
|
|
346
|
+
throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
|
|
347
|
+
}
|
|
348
|
+
return json;
|
|
349
|
+
}
|
|
350
|
+
async function apiGet(path) {
|
|
351
|
+
const base = await resolveApiBaseUrl();
|
|
352
|
+
const res = await fetch(`${base}${path}`, {
|
|
353
|
+
signal: AbortSignal.timeout(3e4)
|
|
354
|
+
});
|
|
355
|
+
const json = await res.json();
|
|
356
|
+
if (!res.ok || json.ok === false) {
|
|
357
|
+
throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
|
|
358
|
+
}
|
|
359
|
+
return json;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/mcp/resources/resource-builder.ts
|
|
363
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
364
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
319
365
|
var __filename = fileURLToPath2(import.meta.url);
|
|
320
366
|
var __dirname = dirname(__filename);
|
|
321
|
-
var PACKAGE_ROOT = resolve3(__dirname, "
|
|
322
|
-
var WORKFLOW_PATH = join3(WORKSPACE_ROOT, "WORKFLOW.md");
|
|
367
|
+
var PACKAGE_ROOT = resolve3(__dirname, "../../..");
|
|
323
368
|
var README_PATH = join3(PACKAGE_ROOT, "README.md");
|
|
324
369
|
var FIFONY_GUIDE_PATH = join3(PACKAGE_ROOT, "FIFONY.md");
|
|
325
|
-
var DEBUG_BOOT2 = env2.FIFONY_DEBUG_BOOT === "1";
|
|
326
|
-
var incomingBuffer = Buffer.alloc(0);
|
|
327
|
-
function debugBoot2(message) {
|
|
328
|
-
if (!DEBUG_BOOT2) return;
|
|
329
|
-
process.stderr.write(`[FIFONY_DEBUG_BOOT] ${message}
|
|
330
|
-
`);
|
|
331
|
-
}
|
|
332
|
-
function hashInput(value) {
|
|
333
|
-
return createHash("sha1").update(value).digest("hex").slice(0, 10);
|
|
334
|
-
}
|
|
335
370
|
async function buildIntegrationGuide() {
|
|
336
371
|
return renderPrompt("mcp-integration-guide", {
|
|
337
372
|
workspaceRoot: WORKSPACE_ROOT,
|
|
@@ -363,7 +398,6 @@ async function buildStateSummary() {
|
|
|
363
398
|
workspaceRoot: WORKSPACE_ROOT,
|
|
364
399
|
persistenceRoot: PERSISTENCE_ROOT,
|
|
365
400
|
stateRoot: STATE_ROOT,
|
|
366
|
-
workflowPresent: existsSync3(WORKFLOW_PATH),
|
|
367
401
|
runtimeUpdatedAt: runtime.updatedAt ?? null,
|
|
368
402
|
issueCount: issues.length,
|
|
369
403
|
issuesByState: byState,
|
|
@@ -387,13 +421,15 @@ async function buildIssuePrompt(issue, provider, role) {
|
|
|
387
421
|
provider,
|
|
388
422
|
id: issue.id,
|
|
389
423
|
title: issue.title,
|
|
390
|
-
state: issue.state ?? "
|
|
424
|
+
state: issue.state ?? "Planning",
|
|
391
425
|
capabilityCategory: resolution.category,
|
|
392
426
|
overlays: resolution.overlays,
|
|
393
427
|
paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : [],
|
|
394
428
|
description: issue.description || "No description provided."
|
|
395
429
|
});
|
|
396
430
|
}
|
|
431
|
+
|
|
432
|
+
// src/mcp/resources/resource-handlers.ts
|
|
397
433
|
async function listResourcesMcp() {
|
|
398
434
|
const issues = await getIssues();
|
|
399
435
|
const resources = [
|
|
@@ -405,9 +441,6 @@ async function listResourcesMcp() {
|
|
|
405
441
|
{ uri: "fifony://integrations", name: "Fifony integrations", description: "Discovered local integrations such as agency-agents and impeccable skills.", mimeType: "application/json" },
|
|
406
442
|
{ uri: "fifony://capabilities", name: "Fifony capability routing", description: "How Fifony would route current issues to providers, profiles, and overlays.", mimeType: "application/json" }
|
|
407
443
|
];
|
|
408
|
-
if (existsSync3(WORKFLOW_PATH)) {
|
|
409
|
-
resources.push({ uri: "fifony://workspace/workflow", name: "Workspace workflow", description: "The active WORKFLOW.md from the target workspace.", mimeType: "text/markdown" });
|
|
410
|
-
}
|
|
411
444
|
resources.push(
|
|
412
445
|
{ uri: "fifony://analytics", name: "Token usage analytics", description: "Token usage analytics snapshot including totals, cost estimates, and per-model breakdown.", mimeType: "application/json" },
|
|
413
446
|
{ uri: "fifony://workflow/config", name: "Workflow config", description: "Current pipeline workflow configuration (plan/execute/review providers, models, and effort).", mimeType: "application/json" },
|
|
@@ -466,7 +499,6 @@ async function readResource(uri) {
|
|
|
466
499
|
)
|
|
467
500
|
}];
|
|
468
501
|
}
|
|
469
|
-
if (uri === "fifony://workspace/workflow") return [{ uri, mimeType: "text/markdown", text: safeRead(WORKFLOW_PATH) }];
|
|
470
502
|
if (uri === "fifony://analytics") {
|
|
471
503
|
try {
|
|
472
504
|
const result = await apiGet("/api/analytics/tokens");
|
|
@@ -548,407 +580,52 @@ async function readResource(uri) {
|
|
|
548
580
|
}
|
|
549
581
|
throw new Error(`Unknown resource: ${uri}`);
|
|
550
582
|
}
|
|
551
|
-
|
|
583
|
+
|
|
584
|
+
// src/mcp/tools/tool-list.ts
|
|
585
|
+
function listTools() {
|
|
552
586
|
return [
|
|
553
|
-
{ name: "fifony
|
|
554
|
-
{ name: "fifony
|
|
555
|
-
{ name: "fifony
|
|
556
|
-
{ name: "fifony
|
|
557
|
-
{ name: "fifony
|
|
558
|
-
{ name: "fifony
|
|
559
|
-
{ name: "fifony
|
|
560
|
-
{ name: "fifony
|
|
561
|
-
{ name: "fifony
|
|
587
|
+
{ name: "fifony.status", description: "Return a compact status summary for the current Fifony workspace.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
588
|
+
{ name: "fifony.list_issues", description: "List issues from the Fifony durable store.", inputSchema: { type: "object", properties: { state: { type: "string" }, capabilityCategory: { type: "string" }, category: { type: "string" } }, additionalProperties: false } },
|
|
589
|
+
{ name: "fifony.create_issue", description: "Create a new issue directly in the Fifony durable store.", inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, description: { type: "string" }, priority: { type: "number" }, state: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
|
|
590
|
+
{ name: "fifony.update_issue_state", description: "Update an issue state in the Fifony store and append an event.", inputSchema: { type: "object", properties: { issueId: { type: "string" }, state: { type: "string" }, note: { type: "string" } }, required: ["issueId", "state"], additionalProperties: false } },
|
|
591
|
+
{ name: "fifony.plan", description: "Generate an AI plan for an issue. The issue must be in Planning state. Returns the plan summary and step count.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to plan." }, fast: { type: "boolean", description: "Use fast planning mode (less thorough but quicker)." } }, required: ["issueId"], additionalProperties: false } },
|
|
592
|
+
{ name: "fifony.refine", description: "Refine an existing plan with feedback. The issue must already have a plan.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose plan to refine." }, feedback: { type: "string", description: "Feedback to guide the plan refinement." } }, required: ["issueId", "feedback"], additionalProperties: false } },
|
|
593
|
+
{ name: "fifony.approve", description: "Approve a plan and move the issue to Planned for execution.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to approve." } }, required: ["issueId"], additionalProperties: false } },
|
|
594
|
+
{ name: "fifony.merge", description: "Merge workspace changes back into the project root. Copies new/modified files from the issue workspace to TARGET_ROOT and removes files that were deleted. Skips fifony internal files, node_modules, .git, and dist.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose workspace to merge." } }, required: ["issueId"], additionalProperties: false } },
|
|
595
|
+
{ name: "fifony.analytics", description: "Get token usage analytics including overall totals, cost estimates, and top issues by token consumption.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
596
|
+
{ name: "fifony.integration_config", description: "Generate a ready-to-paste MCP client configuration snippet for this Fifony workspace.", inputSchema: { type: "object", properties: { client: { type: "string" } }, additionalProperties: false } },
|
|
597
|
+
{ name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
598
|
+
{ 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 } },
|
|
599
|
+
{ name: "fifony.resolve_capabilities", description: "Resolve which providers, roles, profiles, and overlays Fifony should use for a task.", inputSchema: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
|
|
600
|
+
{ 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 } },
|
|
601
|
+
{ 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 } },
|
|
602
|
+
{ name: "fifony.retry_issue", description: "Retry a failed or blocked issue, resetting it to Planned state.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to retry." } }, required: ["issueId"], additionalProperties: false } },
|
|
603
|
+
{ 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 } },
|
|
604
|
+
{ name: "fifony.get_diff", description: "Get git diff for an issue's workspace, including per-file summary and full diff text.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
|
|
605
|
+
{ name: "fifony.get_live", description: "Get live agent output for a running issue, including log tail, PID, elapsed time, and status.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
|
|
606
|
+
{ name: "fifony.get_events", description: "Get event feed, optionally filtered by issue, kind, or limited.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "Filter events by issue identifier." }, kind: { type: "string", description: "Filter events by kind (info, error, state, manual, progress)." }, limit: { type: "number", description: "Maximum number of events to return (default 50)." } }, additionalProperties: false } },
|
|
607
|
+
{ name: "fifony.get_workflow", description: "Get the current pipeline workflow configuration including providers, models, and effort for plan/execute/review stages.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
608
|
+
{ name: "fifony.set_workflow", description: "Update the pipeline workflow configuration. Each stage (plan, execute, review) needs provider, model, and effort.", inputSchema: { type: "object", properties: { plan: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, execute: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, review: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] } }, required: ["plan", "execute", "review"], additionalProperties: false } },
|
|
609
|
+
{ name: "fifony.scan_project", description: "Scan the target project structure, returning files, directories, and detected technologies.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
610
|
+
{ name: "fifony.install_agents", description: "Install agents from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { agents: { type: "array", items: { type: "string" }, description: "List of agent names to install." } }, required: ["agents"], additionalProperties: false } },
|
|
611
|
+
{ name: "fifony.install_skills", description: "Install skills from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { skills: { type: "array", items: { type: "string" }, description: "List of skill names to install." } }, required: ["skills"], additionalProperties: false } }
|
|
562
612
|
];
|
|
563
613
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
if (name === "fifony-plan-issue") {
|
|
581
|
-
const issueId = typeof args.issueId === "string" ? args.issueId : "";
|
|
582
|
-
const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "codex";
|
|
583
|
-
const issue = issueId ? await getIssue(issueId) : null;
|
|
584
|
-
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
585
|
-
return {
|
|
586
|
-
description: "Issue planning prompt grounded in the Fifony issue store.",
|
|
587
|
-
messages: [{
|
|
588
|
-
role: "user",
|
|
589
|
-
content: { type: "text", text: await buildIssuePrompt(issue, provider, "planner") }
|
|
590
|
-
}]
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
if (name === "fifony-review-workflow") {
|
|
594
|
-
const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "claude";
|
|
595
|
-
return {
|
|
596
|
-
description: "Workflow review prompt for Fifony orchestration.",
|
|
597
|
-
messages: [{
|
|
598
|
-
role: "user",
|
|
599
|
-
content: {
|
|
600
|
-
type: "text",
|
|
601
|
-
text: await renderPrompt("mcp-review-workflow", {
|
|
602
|
-
provider,
|
|
603
|
-
workspaceRoot: WORKSPACE_ROOT,
|
|
604
|
-
workflowPresent: existsSync3(WORKFLOW_PATH) ? "yes" : "no"
|
|
605
|
-
})
|
|
606
|
-
}
|
|
607
|
-
}]
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
if (name === "fifony-use-integration") {
|
|
611
|
-
const integration = typeof args.integration === "string" ? args.integration : "";
|
|
612
|
-
return {
|
|
613
|
-
description: "Integration guidance for a discovered Fifony extension.",
|
|
614
|
-
messages: [{
|
|
615
|
-
role: "user",
|
|
616
|
-
content: { type: "text", text: await buildIntegrationSnippet(integration, WORKSPACE_ROOT) }
|
|
617
|
-
}]
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
if (name === "fifony-route-task") {
|
|
621
|
-
const title = typeof args.title === "string" ? args.title : "";
|
|
622
|
-
const description = typeof args.description === "string" ? args.description : "";
|
|
623
|
-
const labels = typeof args.labels === "string" ? args.labels.split(",").map((label) => label.trim()).filter(Boolean) : [];
|
|
624
|
-
const paths = typeof args.paths === "string" ? args.paths.split(",").map((value) => value.trim()).filter(Boolean) : [];
|
|
625
|
-
const resolution = resolveTaskCapabilities({ title, description, labels, paths });
|
|
626
|
-
return {
|
|
627
|
-
description: "Task routing prompt produced by the Fifony capability resolver.",
|
|
628
|
-
messages: [{
|
|
629
|
-
role: "user",
|
|
630
|
-
content: {
|
|
631
|
-
type: "text",
|
|
632
|
-
text: await renderPrompt("mcp-route-task", {
|
|
633
|
-
resolutionJson: JSON.stringify(resolution, null, 2)
|
|
634
|
-
})
|
|
635
|
-
}
|
|
636
|
-
}]
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
if (name === "fifony-diagnose-blocked") {
|
|
640
|
-
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
641
|
-
if (!issueId) throw new Error("issueId is required");
|
|
642
|
-
const issue = await getIssue(issueId);
|
|
643
|
-
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
644
|
-
const issueData = issue;
|
|
645
|
-
let events = [];
|
|
646
|
-
try {
|
|
647
|
-
const evResult = await apiGet(`/api/events/feed?issueId=${encodeURIComponent(issueId)}`);
|
|
648
|
-
events = Array.isArray(evResult.events) ? evResult.events.slice(0, 30) : [];
|
|
649
|
-
} catch {
|
|
650
|
-
const localEvents = await listEvents({ limit: 100 });
|
|
651
|
-
events = localEvents.filter((event) => event.issueId === issueId).slice(0, 30);
|
|
652
|
-
}
|
|
653
|
-
const plan = issueData.plan ?? null;
|
|
654
|
-
const history = Array.isArray(issueData.history) ? issueData.history : [];
|
|
655
|
-
const lastError = issueData.lastError ?? null;
|
|
656
|
-
const state = issueData.state ?? "Unknown";
|
|
657
|
-
const attempts = issueData.attempts ?? 0;
|
|
658
|
-
const maxAttempts = issueData.maxAttempts ?? 3;
|
|
659
|
-
const diagnosticText = [
|
|
660
|
-
`# Diagnostic Report for Issue ${issueId}`,
|
|
661
|
-
``,
|
|
662
|
-
`## Issue Details`,
|
|
663
|
-
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
664
|
-
`- **State**: ${state}`,
|
|
665
|
-
`- **Attempts**: ${attempts} / ${maxAttempts}`,
|
|
666
|
-
`- **Last Error**: ${lastError ?? "None"}`,
|
|
667
|
-
`- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
|
|
668
|
-
``,
|
|
669
|
-
`## Plan`,
|
|
670
|
-
plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
|
|
671
|
-
plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
|
|
672
|
-
plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
|
|
673
|
-
``,
|
|
674
|
-
`## History`,
|
|
675
|
-
...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
|
|
676
|
-
``,
|
|
677
|
-
`## Recent Events`,
|
|
678
|
-
...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
|
|
679
|
-
``,
|
|
680
|
-
`## Diagnostic Questions`,
|
|
681
|
-
`Based on the information above, please analyze:`,
|
|
682
|
-
`1. What is the root cause of the issue being in "${state}" state?`,
|
|
683
|
-
`2. Is the error recoverable? If so, what steps should be taken?`,
|
|
684
|
-
`3. Does the plan need modification before retrying?`,
|
|
685
|
-
`4. Are there any dependency or configuration issues that need resolution?`,
|
|
686
|
-
`5. What is the recommended next action?`
|
|
687
|
-
].filter((line) => line !== void 0).join("\n");
|
|
688
|
-
return {
|
|
689
|
-
description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
|
|
690
|
-
messages: [{
|
|
691
|
-
role: "user",
|
|
692
|
-
content: { type: "text", text: diagnosticText }
|
|
693
|
-
}]
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
if (name === "fifony-weekly-summary") {
|
|
697
|
-
const issues = await getIssues();
|
|
698
|
-
let analytics = {};
|
|
699
|
-
try {
|
|
700
|
-
analytics = await apiGet("/api/analytics/tokens");
|
|
701
|
-
} catch {
|
|
702
|
-
}
|
|
703
|
-
const overall = analytics.overall ?? {};
|
|
704
|
-
const byState = issues.reduce((accumulator, issue) => {
|
|
705
|
-
const key = issue.state ?? "Unknown";
|
|
706
|
-
accumulator[key] = (accumulator[key] ?? 0) + 1;
|
|
707
|
-
return accumulator;
|
|
708
|
-
}, {});
|
|
709
|
-
const totalIssues = issues.length;
|
|
710
|
-
const completed = byState["Done"] ?? 0;
|
|
711
|
-
const blocked = (byState["Blocked"] ?? 0) + (byState["Failed"] ?? 0);
|
|
712
|
-
const inProgress = (byState["Running"] ?? 0) + (byState["In Review"] ?? 0) + (byState["Queued"] ?? 0);
|
|
713
|
-
const todo = byState["Todo"] ?? 0;
|
|
714
|
-
const planning = byState["Planning"] ?? 0;
|
|
715
|
-
const cancelled = byState["Cancelled"] ?? 0;
|
|
716
|
-
const inputTokens = typeof overall.inputTokens === "number" ? overall.inputTokens : 0;
|
|
717
|
-
const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
|
|
718
|
-
const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
|
|
719
|
-
const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
|
|
720
|
-
const summaryText = [
|
|
721
|
-
`# Fifony Weekly Progress Summary`,
|
|
722
|
-
``,
|
|
723
|
-
`## Issue Statistics`,
|
|
724
|
-
`| Status | Count |`,
|
|
725
|
-
`|--------|-------|`,
|
|
726
|
-
`| Total Issues | ${totalIssues} |`,
|
|
727
|
-
`| Completed (Done) | ${completed} |`,
|
|
728
|
-
`| In Progress | ${inProgress} |`,
|
|
729
|
-
`| Todo | ${todo} |`,
|
|
730
|
-
`| Planning | ${planning} |`,
|
|
731
|
-
`| Blocked/Failed | ${blocked} |`,
|
|
732
|
-
`| Cancelled | ${cancelled} |`,
|
|
733
|
-
``,
|
|
734
|
-
`## Token Usage`,
|
|
735
|
-
`- **Total Tokens**: ${totalTokens.toLocaleString()}`,
|
|
736
|
-
`- **Input Tokens**: ${inputTokens.toLocaleString()}`,
|
|
737
|
-
`- **Output Tokens**: ${outputTokens.toLocaleString()}`,
|
|
738
|
-
`- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
|
|
739
|
-
``,
|
|
740
|
-
`## Analysis Request`,
|
|
741
|
-
`Based on these metrics, please provide:`,
|
|
742
|
-
`1. A brief summary of overall progress this week`,
|
|
743
|
-
`2. Identification of any bottlenecks (blocked/failed issues)`,
|
|
744
|
-
`3. Token usage efficiency assessment`,
|
|
745
|
-
`4. Recommendations for improving throughput`,
|
|
746
|
-
`5. Priority items for next week`
|
|
747
|
-
].join("\n");
|
|
748
|
-
return {
|
|
749
|
-
description: "Weekly progress summary prompt for the Fifony workspace.",
|
|
750
|
-
messages: [{
|
|
751
|
-
role: "user",
|
|
752
|
-
content: { type: "text", text: summaryText }
|
|
753
|
-
}]
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
if (name === "fifony-refine-plan") {
|
|
757
|
-
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
758
|
-
const concern = typeof args.concern === "string" ? args.concern.trim() : "";
|
|
759
|
-
if (!issueId) throw new Error("issueId is required");
|
|
760
|
-
const issue = await getIssue(issueId);
|
|
761
|
-
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
762
|
-
const issueData = issue;
|
|
763
|
-
const plan = issueData.plan ?? null;
|
|
764
|
-
const steps = plan?.steps ?? [];
|
|
765
|
-
const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
|
|
766
|
-
${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
|
|
767
|
-
const refinementText = [
|
|
768
|
-
`# Plan Refinement for Issue ${issueId}`,
|
|
769
|
-
``,
|
|
770
|
-
`## Issue`,
|
|
771
|
-
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
772
|
-
`- **Description**: ${issueData.description ?? "No description"}`,
|
|
773
|
-
``,
|
|
774
|
-
`## Current Plan`,
|
|
775
|
-
plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
|
|
776
|
-
plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
|
|
777
|
-
``,
|
|
778
|
-
`### Steps`,
|
|
779
|
-
stepsText,
|
|
780
|
-
``,
|
|
781
|
-
concern ? `## Specific Concern
|
|
782
|
-
${concern}
|
|
783
|
-
` : "",
|
|
784
|
-
`## Refinement Guidance`,
|
|
785
|
-
`Please review the current plan and provide specific, actionable feedback:`,
|
|
786
|
-
`1. Are the steps correctly ordered and complete?`,
|
|
787
|
-
`2. Are there missing edge cases or error handling steps?`,
|
|
788
|
-
`3. Is the complexity estimate accurate?`,
|
|
789
|
-
`4. Are the file paths and affected areas correct?`,
|
|
790
|
-
`5. Should any steps be split, merged, or removed?`,
|
|
791
|
-
``,
|
|
792
|
-
`Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
|
|
793
|
-
].filter((line) => line !== void 0).join("\n");
|
|
794
|
-
return {
|
|
795
|
-
description: `Plan refinement prompt for issue ${issueId}.`,
|
|
796
|
-
messages: [{
|
|
797
|
-
role: "user",
|
|
798
|
-
content: { type: "text", text: refinementText }
|
|
799
|
-
}]
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
if (name === "fifony-code-review") {
|
|
803
|
-
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
804
|
-
if (!issueId) throw new Error("issueId is required");
|
|
805
|
-
const issue = await getIssue(issueId);
|
|
806
|
-
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
807
|
-
const issueData = issue;
|
|
808
|
-
let diffData = {};
|
|
809
|
-
try {
|
|
810
|
-
diffData = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
|
|
811
|
-
} catch (error) {
|
|
812
|
-
throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
|
|
813
|
-
}
|
|
814
|
-
const files = Array.isArray(diffData.files) ? diffData.files : [];
|
|
815
|
-
const diff = typeof diffData.diff === "string" ? diffData.diff : "";
|
|
816
|
-
const totalAdditions = typeof diffData.totalAdditions === "number" ? diffData.totalAdditions : 0;
|
|
817
|
-
const totalDeletions = typeof diffData.totalDeletions === "number" ? diffData.totalDeletions : 0;
|
|
818
|
-
if (!diff.trim()) {
|
|
819
|
-
return {
|
|
820
|
-
description: `Code review prompt for issue ${issueId} (no changes).`,
|
|
821
|
-
messages: [{
|
|
822
|
-
role: "user",
|
|
823
|
-
content: { type: "text", text: `# Code Review for ${issueId}
|
|
824
|
-
|
|
825
|
-
No code changes found for this issue. The workspace may not have been created yet or no modifications were made.` }
|
|
826
|
-
}]
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
|
|
830
|
-
const reviewText = [
|
|
831
|
-
`# Code Review for Issue ${issueId}`,
|
|
832
|
-
``,
|
|
833
|
-
`## Issue Context`,
|
|
834
|
-
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
835
|
-
`- **Description**: ${issueData.description ?? "No description"}`,
|
|
836
|
-
`- **State**: ${issueData.state ?? "Unknown"}`,
|
|
837
|
-
``,
|
|
838
|
-
`## Change Summary`,
|
|
839
|
-
`- **Files Changed**: ${files.length}`,
|
|
840
|
-
`- **Total Additions**: +${totalAdditions}`,
|
|
841
|
-
`- **Total Deletions**: -${totalDeletions}`,
|
|
842
|
-
``,
|
|
843
|
-
`### Files`,
|
|
844
|
-
`| Path | Status | Additions | Deletions |`,
|
|
845
|
-
`|------|--------|-----------|-----------|`,
|
|
846
|
-
filesTable,
|
|
847
|
-
``,
|
|
848
|
-
`## Diff`,
|
|
849
|
-
"```diff",
|
|
850
|
-
diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
|
|
851
|
-
"```",
|
|
852
|
-
``,
|
|
853
|
-
`## Review Checklist`,
|
|
854
|
-
`Please review the changes and evaluate:`,
|
|
855
|
-
`1. **Correctness**: Do the changes correctly implement what the issue describes?`,
|
|
856
|
-
`2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
|
|
857
|
-
`3. **Error Handling**: Are edge cases and errors properly handled?`,
|
|
858
|
-
`4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
|
|
859
|
-
`5. **Performance**: Are there any performance concerns or inefficiencies?`,
|
|
860
|
-
`6. **Tests**: Are changes adequately covered by tests?`,
|
|
861
|
-
`7. **Breaking Changes**: Do any changes break backward compatibility?`
|
|
862
|
-
].join("\n");
|
|
863
|
-
return {
|
|
864
|
-
description: `Code review prompt for issue ${issueId}.`,
|
|
865
|
-
messages: [{
|
|
866
|
-
role: "user",
|
|
867
|
-
content: { type: "text", text: reviewText }
|
|
868
|
-
}]
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
throw new Error(`Unknown prompt: ${name}`);
|
|
872
|
-
}
|
|
873
|
-
function listTools() {
|
|
874
|
-
return [
|
|
875
|
-
{ name: "fifony.status", description: "Return a compact status summary for the current Fifony workspace.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
876
|
-
{ name: "fifony.list_issues", description: "List issues from the Fifony durable store.", inputSchema: { type: "object", properties: { state: { type: "string" }, capabilityCategory: { type: "string" }, category: { type: "string" } }, additionalProperties: false } },
|
|
877
|
-
{ name: "fifony.create_issue", description: "Create a new issue directly in the Fifony durable store.", inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, description: { type: "string" }, priority: { type: "number" }, state: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
|
|
878
|
-
{ name: "fifony.update_issue_state", description: "Update an issue state in the Fifony store and append an event.", inputSchema: { type: "object", properties: { issueId: { type: "string" }, state: { type: "string" }, note: { type: "string" } }, required: ["issueId", "state"], additionalProperties: false } },
|
|
879
|
-
{ name: "fifony.plan", description: "Generate an AI plan for an issue. The issue must be in Planning state. Returns the plan summary and step count.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to plan." }, fast: { type: "boolean", description: "Use fast planning mode (less thorough but quicker)." } }, required: ["issueId"], additionalProperties: false } },
|
|
880
|
-
{ name: "fifony.refine", description: "Refine an existing plan with feedback. The issue must already have a plan.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose plan to refine." }, feedback: { type: "string", description: "Feedback to guide the plan refinement." } }, required: ["issueId", "feedback"], additionalProperties: false } },
|
|
881
|
-
{ name: "fifony.approve", description: "Approve a plan and move the issue to Todo for execution.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to approve." } }, required: ["issueId"], additionalProperties: false } },
|
|
882
|
-
{ name: "fifony.merge", description: "Merge workspace changes back into the project root. Copies new/modified files from the issue workspace to TARGET_ROOT and removes files that were deleted. Skips fifony internal files, node_modules, .git, and dist.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose workspace to merge." } }, required: ["issueId"], additionalProperties: false } },
|
|
883
|
-
{ name: "fifony.analytics", description: "Get token usage analytics including overall totals, cost estimates, and top issues by token consumption.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
884
|
-
{ name: "fifony.integration_config", description: "Generate a ready-to-paste MCP client configuration snippet for this Fifony workspace.", inputSchema: { type: "object", properties: { client: { type: "string" } }, additionalProperties: false } },
|
|
885
|
-
{ name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
886
|
-
{ 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 } },
|
|
887
|
-
{ name: "fifony.resolve_capabilities", description: "Resolve which providers, roles, profiles, and overlays Fifony should use for a task.", inputSchema: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
|
|
888
|
-
{ 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 } },
|
|
889
|
-
{ 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 } },
|
|
890
|
-
{ name: "fifony.retry_issue", description: "Retry a failed or blocked issue, resetting it to Todo state.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to retry." } }, required: ["issueId"], additionalProperties: false } },
|
|
891
|
-
{ 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 } },
|
|
892
|
-
{ name: "fifony.get_diff", description: "Get git diff for an issue's workspace, including per-file summary and full diff text.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
|
|
893
|
-
{ name: "fifony.get_live", description: "Get live agent output for a running issue, including log tail, PID, elapsed time, and status.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
|
|
894
|
-
{ name: "fifony.get_events", description: "Get event feed, optionally filtered by issue, kind, or limited.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "Filter events by issue identifier." }, kind: { type: "string", description: "Filter events by kind (info, error, state, manual, progress)." }, limit: { type: "number", description: "Maximum number of events to return (default 50)." } }, additionalProperties: false } },
|
|
895
|
-
{ name: "fifony.get_workflow", description: "Get the current pipeline workflow configuration including providers, models, and effort for plan/execute/review stages.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
896
|
-
{ name: "fifony.set_workflow", description: "Update the pipeline workflow configuration. Each stage (plan, execute, review) needs provider, model, and effort.", inputSchema: { type: "object", properties: { plan: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, execute: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, review: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] } }, required: ["plan", "execute", "review"], additionalProperties: false } },
|
|
897
|
-
{ name: "fifony.scan_project", description: "Scan the target project structure, returning files, directories, and detected technologies.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
898
|
-
{ name: "fifony.install_agents", description: "Install agents from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { agents: { type: "array", items: { type: "string" }, description: "List of agent names to install." } }, required: ["agents"], additionalProperties: false } },
|
|
899
|
-
{ name: "fifony.install_skills", description: "Install skills from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { skills: { type: "array", items: { type: "string" }, description: "List of skill names to install." } }, required: ["skills"], additionalProperties: false } }
|
|
900
|
-
];
|
|
901
|
-
}
|
|
902
|
-
function toolText(text) {
|
|
903
|
-
return { content: [{ type: "text", text }] };
|
|
904
|
-
}
|
|
905
|
-
async function resolveApiBaseUrl() {
|
|
906
|
-
const envPort = env2.FIFONY_API_PORT;
|
|
907
|
-
if (envPort) return `http://localhost:${envPort}`;
|
|
908
|
-
const runtime = await getRuntimeSnapshot();
|
|
909
|
-
const config = runtime.config;
|
|
910
|
-
const port = config?.dashboardPort;
|
|
911
|
-
if (port) return `http://localhost:${port}`;
|
|
912
|
-
for (const candidate of [4e3, 3e3, 8080]) {
|
|
913
|
-
try {
|
|
914
|
-
const res = await fetch(`http://localhost:${candidate}/health`, { signal: AbortSignal.timeout(1e3) });
|
|
915
|
-
if (res.ok) return `http://localhost:${candidate}`;
|
|
916
|
-
} catch {
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
throw new Error("Fifony runtime API is not reachable. Start the runtime with --port to enable plan/refine/approve/analytics tools.");
|
|
920
|
-
}
|
|
921
|
-
async function apiPost(path, body = {}) {
|
|
922
|
-
const base = await resolveApiBaseUrl();
|
|
923
|
-
const res = await fetch(`${base}${path}`, {
|
|
924
|
-
method: "POST",
|
|
925
|
-
headers: { "content-type": "application/json" },
|
|
926
|
-
body: JSON.stringify(body),
|
|
927
|
-
signal: AbortSignal.timeout(12e4)
|
|
928
|
-
});
|
|
929
|
-
const json = await res.json();
|
|
930
|
-
if (!res.ok || json.ok === false) {
|
|
931
|
-
throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
|
|
932
|
-
}
|
|
933
|
-
return json;
|
|
934
|
-
}
|
|
935
|
-
async function apiGet(path) {
|
|
936
|
-
const base = await resolveApiBaseUrl();
|
|
937
|
-
const res = await fetch(`${base}${path}`, {
|
|
938
|
-
signal: AbortSignal.timeout(3e4)
|
|
939
|
-
});
|
|
940
|
-
const json = await res.json();
|
|
941
|
-
if (!res.ok || json.ok === false) {
|
|
942
|
-
throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
|
|
943
|
-
}
|
|
944
|
-
return json;
|
|
945
|
-
}
|
|
946
|
-
async function callTool(name, args = {}) {
|
|
947
|
-
if (name === "fifony.status") return toolText(await buildStateSummary());
|
|
948
|
-
if (name === "fifony.list_issues") {
|
|
949
|
-
const stateFilter = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "";
|
|
950
|
-
const capabilityCategory = typeof args.capabilityCategory === "string" && args.capabilityCategory.trim() ? args.capabilityCategory.trim() : typeof args.category === "string" && args.category.trim() ? args.category.trim() : "";
|
|
951
|
-
return toolText(JSON.stringify(await listIssues({ state: stateFilter || void 0, capabilityCategory: capabilityCategory || void 0 }), null, 2));
|
|
614
|
+
|
|
615
|
+
// src/mcp/tools/tool-executor.ts
|
|
616
|
+
import { createHash } from "crypto";
|
|
617
|
+
function hashInput(value) {
|
|
618
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 10);
|
|
619
|
+
}
|
|
620
|
+
function toolText(text) {
|
|
621
|
+
return { content: [{ type: "text", text }] };
|
|
622
|
+
}
|
|
623
|
+
async function callTool(name, args = {}) {
|
|
624
|
+
if (name === "fifony.status") return toolText(await buildStateSummary());
|
|
625
|
+
if (name === "fifony.list_issues") {
|
|
626
|
+
const stateFilter = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "";
|
|
627
|
+
const capabilityCategory = typeof args.capabilityCategory === "string" && args.capabilityCategory.trim() ? args.capabilityCategory.trim() : typeof args.category === "string" && args.category.trim() ? args.category.trim() : "";
|
|
628
|
+
return toolText(JSON.stringify(await listIssues({ state: stateFilter || void 0, capabilityCategory: capabilityCategory || void 0 }), null, 2));
|
|
952
629
|
}
|
|
953
630
|
if (name === "fifony.create_issue") {
|
|
954
631
|
await initDatabase();
|
|
@@ -959,7 +636,7 @@ async function callTool(name, args = {}) {
|
|
|
959
636
|
const issueId = explicitId || `LOCAL-${hashInput(`${title}:${nowIso()}`)}`.toUpperCase();
|
|
960
637
|
const description = typeof args.description === "string" ? args.description : "";
|
|
961
638
|
const priority = typeof args.priority === "number" ? args.priority : 2;
|
|
962
|
-
const state =
|
|
639
|
+
const state = parseIssueState(args.state) ?? "Planning";
|
|
963
640
|
const baseLabels = Array.isArray(args.labels) ? args.labels.filter((value) => typeof value === "string") : ["fifony", "mcp"];
|
|
964
641
|
const paths = Array.isArray(args.paths) ? args.paths.filter((value) => typeof value === "string") : [];
|
|
965
642
|
const inferredPaths = inferCapabilityPaths({ id: issueId, identifier: issueId, title, description, labels: baseLabels, paths });
|
|
@@ -994,9 +671,9 @@ async function callTool(name, args = {}) {
|
|
|
994
671
|
await initDatabase();
|
|
995
672
|
const { issueResource: issueResource2 } = getResources();
|
|
996
673
|
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
997
|
-
const state =
|
|
674
|
+
const state = parseIssueState(args.state);
|
|
998
675
|
const note = typeof args.note === "string" ? args.note : "";
|
|
999
|
-
if (!issueId || !state) throw new Error("issueId and state are required");
|
|
676
|
+
if (!issueId || !state) throw new Error("issueId and a valid canonical state are required");
|
|
1000
677
|
const current = await issueResource2?.get(issueId);
|
|
1001
678
|
if (!current) throw new Error(`Issue not found: ${issueId}`);
|
|
1002
679
|
const updated = await issueResource2?.update(issueId, { state, updatedAt: nowIso() });
|
|
@@ -1044,8 +721,8 @@ async function callTool(name, args = {}) {
|
|
|
1044
721
|
const issue = result.issue;
|
|
1045
722
|
return toolText(JSON.stringify({
|
|
1046
723
|
issueId,
|
|
1047
|
-
state: issue?.state ?? "
|
|
1048
|
-
message: `Plan approved for ${issueId}. Issue moved to
|
|
724
|
+
state: issue?.state ?? "Planned",
|
|
725
|
+
message: `Plan approved for ${issueId}. Issue moved to Planned and is ready for execution.`
|
|
1049
726
|
}, null, 2));
|
|
1050
727
|
}
|
|
1051
728
|
if (name === "fifony.merge") {
|
|
@@ -1073,12 +750,7 @@ async function callTool(name, args = {}) {
|
|
|
1073
750
|
overall: { inputTokens, outputTokens, totalTokens },
|
|
1074
751
|
estimatedCostUsd: Math.round(estimatedCost * 100) / 100,
|
|
1075
752
|
modelBreakdown: byModel ?? {},
|
|
1076
|
-
topIssues: (topIssues ?? []).slice(0, 10).map((issue) => ({
|
|
1077
|
-
id: issue.id,
|
|
1078
|
-
totalTokens: issue.totalTokens,
|
|
1079
|
-
inputTokens: issue.inputTokens,
|
|
1080
|
-
outputTokens: issue.outputTokens
|
|
1081
|
-
}))
|
|
753
|
+
topIssues: (topIssues ?? []).slice(0, 10).map((issue) => ({ id: issue.id, totalTokens: issue.totalTokens, inputTokens: issue.inputTokens, outputTokens: issue.outputTokens }))
|
|
1082
754
|
}, null, 2));
|
|
1083
755
|
}
|
|
1084
756
|
if (name === "fifony.integration_config") {
|
|
@@ -1139,7 +811,7 @@ async function callTool(name, args = {}) {
|
|
|
1139
811
|
try {
|
|
1140
812
|
const result = await apiPost(`/api/issues/${encodeURIComponent(issueId)}/retry`);
|
|
1141
813
|
const issue = result.issue;
|
|
1142
|
-
return toolText(JSON.stringify({ issueId, state: issue?.state ?? "
|
|
814
|
+
return toolText(JSON.stringify({ issueId, state: issue?.state ?? "Planned", message: `Issue ${issueId} has been retried and reset to Planned.` }, null, 2));
|
|
1143
815
|
} catch (error) {
|
|
1144
816
|
throw new Error(`Failed to retry issue ${issueId}: ${String(error)}`);
|
|
1145
817
|
}
|
|
@@ -1163,97 +835,411 @@ async function callTool(name, args = {}) {
|
|
|
1163
835
|
throw new Error(`Failed to enhance ${field}: ${String(error)}`);
|
|
1164
836
|
}
|
|
1165
837
|
}
|
|
1166
|
-
if (name === "fifony.get_diff") {
|
|
1167
|
-
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
1168
|
-
if (!issueId) throw new Error("issueId is required");
|
|
838
|
+
if (name === "fifony.get_diff") {
|
|
839
|
+
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
840
|
+
if (!issueId) throw new Error("issueId is required");
|
|
841
|
+
try {
|
|
842
|
+
const result = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
|
|
843
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
844
|
+
} catch (error) {
|
|
845
|
+
throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (name === "fifony.get_live") {
|
|
849
|
+
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
850
|
+
if (!issueId) throw new Error("issueId is required");
|
|
851
|
+
try {
|
|
852
|
+
const result = await apiGet(`/api/live/${encodeURIComponent(issueId)}`);
|
|
853
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw new Error(`Failed to get live output for ${issueId}: ${String(error)}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (name === "fifony.get_events") {
|
|
859
|
+
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
860
|
+
const kind = typeof args.kind === "string" ? args.kind.trim() : "";
|
|
861
|
+
const limit = typeof args.limit === "number" ? args.limit : 50;
|
|
862
|
+
const params = new URLSearchParams();
|
|
863
|
+
if (issueId) params.set("issueId", issueId);
|
|
864
|
+
if (kind) params.set("kind", kind);
|
|
865
|
+
const query = params.toString();
|
|
866
|
+
try {
|
|
867
|
+
const result = await apiGet(`/api/events/feed${query ? `?${query}` : ""}`);
|
|
868
|
+
const events = Array.isArray(result.events) ? result.events.slice(0, limit) : [];
|
|
869
|
+
return toolText(JSON.stringify({ events, count: events.length }, null, 2));
|
|
870
|
+
} catch (error) {
|
|
871
|
+
const events = await listEvents({ limit });
|
|
872
|
+
const filtered = events.filter((event) => {
|
|
873
|
+
if (issueId && event.issueId !== issueId) return false;
|
|
874
|
+
if (kind && event.kind !== kind) return false;
|
|
875
|
+
return true;
|
|
876
|
+
}).slice(0, limit);
|
|
877
|
+
return toolText(JSON.stringify({ events: filtered, count: filtered.length }, null, 2));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (name === "fifony.get_workflow") {
|
|
881
|
+
try {
|
|
882
|
+
const result = await apiGet("/api/config/workflow");
|
|
883
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
884
|
+
} catch (error) {
|
|
885
|
+
throw new Error(`Failed to get workflow config: ${String(error)}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (name === "fifony.set_workflow") {
|
|
889
|
+
const plan = args.plan;
|
|
890
|
+
const execute = args.execute;
|
|
891
|
+
const review = args.review;
|
|
892
|
+
if (!plan || !execute || !review) throw new Error("plan, execute, and review are all required");
|
|
893
|
+
try {
|
|
894
|
+
const result = await apiPost("/api/config/workflow", { workflow: { plan, execute, review } });
|
|
895
|
+
return toolText(JSON.stringify({ message: "Workflow configuration updated successfully.", workflow: result.workflow }, null, 2));
|
|
896
|
+
} catch (error) {
|
|
897
|
+
throw new Error(`Failed to set workflow config: ${String(error)}`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (name === "fifony.scan_project") {
|
|
901
|
+
try {
|
|
902
|
+
const result = await apiGet("/api/scan/project");
|
|
903
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
904
|
+
} catch (error) {
|
|
905
|
+
throw new Error(`Failed to scan project: ${String(error)}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (name === "fifony.install_agents") {
|
|
909
|
+
const agents = Array.isArray(args.agents) ? args.agents.filter((value) => typeof value === "string") : [];
|
|
910
|
+
if (agents.length === 0) throw new Error("At least one agent name is required");
|
|
911
|
+
try {
|
|
912
|
+
const result = await apiPost("/api/install/agents", { agents });
|
|
913
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
914
|
+
} catch (error) {
|
|
915
|
+
throw new Error(`Failed to install agents: ${String(error)}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (name === "fifony.install_skills") {
|
|
919
|
+
const skills = Array.isArray(args.skills) ? args.skills.filter((value) => typeof value === "string") : [];
|
|
920
|
+
if (skills.length === 0) throw new Error("At least one skill name is required");
|
|
921
|
+
try {
|
|
922
|
+
const result = await apiPost("/api/install/skills", { skills });
|
|
923
|
+
return toolText(JSON.stringify(result, null, 2));
|
|
924
|
+
} catch (error) {
|
|
925
|
+
throw new Error(`Failed to install skills: ${String(error)}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/mcp/prompts/prompt-list.ts
|
|
932
|
+
function listPrompts() {
|
|
933
|
+
return [
|
|
934
|
+
{ name: "fifony-integrate-client", description: "Generate setup instructions for connecting an MCP-capable client to Fifony.", arguments: [{ name: "client", description: "Client name, e.g. codex or claude.", required: true }, { name: "goal", description: "What the client should do with Fifony.", required: false }] },
|
|
935
|
+
{ name: "fifony-plan-issue", description: "Generate a planning prompt for a specific issue in the Fifony store.", arguments: [{ name: "issueId", description: "Issue identifier.", required: true }, { name: "provider", description: "Agent provider name.", required: false }] },
|
|
936
|
+
{ name: "fifony-use-integration", description: "Generate a concrete integration prompt for agency-agents or impeccable.", arguments: [{ name: "integration", description: "Integration id: agency-agents or impeccable.", required: true }] },
|
|
937
|
+
{ name: "fifony-route-task", description: "Explain which providers, profiles, and overlays Fifony would choose for a task.", arguments: [{ name: "title", description: "Task title.", required: true }, { name: "description", description: "Task description.", required: false }, { name: "labels", description: "Comma-separated labels.", required: false }, { name: "paths", description: "Comma-separated target paths or files.", required: false }] },
|
|
938
|
+
{ name: "fifony-diagnose-blocked", description: "Help diagnose why an issue is blocked or failing, analyzing the issue plan, last error, history, and events.", arguments: [{ name: "issueId", description: "Issue identifier to diagnose.", required: true }] },
|
|
939
|
+
{ name: "fifony-weekly-summary", description: "Generate a weekly progress summary including issues created, completed, blocked, and token usage.", arguments: [] },
|
|
940
|
+
{ name: "fifony-refine-plan", description: "Guided plan refinement prompt that shows the current plan and helps provide specific feedback.", arguments: [{ name: "issueId", description: "Issue identifier whose plan to refine.", required: true }, { name: "concern", description: "Optional specific concern to address in refinement.", required: false }] },
|
|
941
|
+
{ name: "fifony-code-review", description: "Review code changes for an issue by analyzing its git diff.", arguments: [{ name: "issueId", description: "Issue identifier to review.", required: true }] }
|
|
942
|
+
];
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/mcp/prompts/prompt-handler.ts
|
|
946
|
+
async function getPrompt(name, args = {}) {
|
|
947
|
+
if (name === "fifony-integrate-client") {
|
|
948
|
+
const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
|
|
949
|
+
const goal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : "integrate with the local Fifony workspace";
|
|
950
|
+
const integrationGuide = await buildIntegrationGuide();
|
|
951
|
+
return {
|
|
952
|
+
description: "Client integration prompt for Fifony.",
|
|
953
|
+
messages: [{
|
|
954
|
+
role: "user",
|
|
955
|
+
content: {
|
|
956
|
+
type: "text",
|
|
957
|
+
text: await renderPrompt("mcp-integrate-client", { client, goal, integrationGuide })
|
|
958
|
+
}
|
|
959
|
+
}]
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
if (name === "fifony-plan-issue") {
|
|
963
|
+
const issueId = typeof args.issueId === "string" ? args.issueId : "";
|
|
964
|
+
const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "codex";
|
|
965
|
+
const issue = issueId ? await getIssue(issueId) : null;
|
|
966
|
+
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
967
|
+
return {
|
|
968
|
+
description: "Issue planning prompt grounded in the Fifony issue store.",
|
|
969
|
+
messages: [{
|
|
970
|
+
role: "user",
|
|
971
|
+
content: { type: "text", text: await buildIssuePrompt(issue, provider, "planner") }
|
|
972
|
+
}]
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
if (name === "fifony-use-integration") {
|
|
976
|
+
const integration = typeof args.integration === "string" ? args.integration : "";
|
|
977
|
+
return {
|
|
978
|
+
description: "Integration guidance for a discovered Fifony extension.",
|
|
979
|
+
messages: [{
|
|
980
|
+
role: "user",
|
|
981
|
+
content: { type: "text", text: await buildIntegrationSnippet(integration, WORKSPACE_ROOT) }
|
|
982
|
+
}]
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (name === "fifony-route-task") {
|
|
986
|
+
const title = typeof args.title === "string" ? args.title : "";
|
|
987
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
988
|
+
const labels = typeof args.labels === "string" ? args.labels.split(",").map((label) => label.trim()).filter(Boolean) : [];
|
|
989
|
+
const paths = typeof args.paths === "string" ? args.paths.split(",").map((value) => value.trim()).filter(Boolean) : [];
|
|
990
|
+
const resolution = resolveTaskCapabilities({ title, description, labels, paths });
|
|
991
|
+
return {
|
|
992
|
+
description: "Task routing prompt produced by the Fifony capability resolver.",
|
|
993
|
+
messages: [{
|
|
994
|
+
role: "user",
|
|
995
|
+
content: {
|
|
996
|
+
type: "text",
|
|
997
|
+
text: await renderPrompt("mcp-route-task", {
|
|
998
|
+
resolutionJson: JSON.stringify(resolution, null, 2)
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
}]
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (name === "fifony-diagnose-blocked") {
|
|
1005
|
+
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
1006
|
+
if (!issueId) throw new Error("issueId is required");
|
|
1007
|
+
const issue = await getIssue(issueId);
|
|
1008
|
+
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
1009
|
+
const issueData = issue;
|
|
1010
|
+
let events = [];
|
|
1011
|
+
try {
|
|
1012
|
+
const evResult = await apiGet(`/api/events/feed?issueId=${encodeURIComponent(issueId)}`);
|
|
1013
|
+
events = Array.isArray(evResult.events) ? evResult.events.slice(0, 30) : [];
|
|
1014
|
+
} catch {
|
|
1015
|
+
const localEvents = await listEvents({ limit: 100 });
|
|
1016
|
+
events = localEvents.filter((event) => event.issueId === issueId).slice(0, 30);
|
|
1017
|
+
}
|
|
1018
|
+
const plan = issueData.plan ?? null;
|
|
1019
|
+
const history = Array.isArray(issueData.history) ? issueData.history : [];
|
|
1020
|
+
const lastError = issueData.lastError ?? null;
|
|
1021
|
+
const state = issueData.state ?? "Unknown";
|
|
1022
|
+
const attempts = issueData.attempts ?? 0;
|
|
1023
|
+
const maxAttempts = issueData.maxAttempts ?? 3;
|
|
1024
|
+
const diagnosticText = [
|
|
1025
|
+
`# Diagnostic Report for Issue ${issueId}`,
|
|
1026
|
+
``,
|
|
1027
|
+
`## Issue Details`,
|
|
1028
|
+
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
1029
|
+
`- **State**: ${state}`,
|
|
1030
|
+
`- **Attempts**: ${attempts} / ${maxAttempts}`,
|
|
1031
|
+
`- **Last Error**: ${lastError ?? "None"}`,
|
|
1032
|
+
`- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
|
|
1033
|
+
``,
|
|
1034
|
+
`## Plan`,
|
|
1035
|
+
plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
|
|
1036
|
+
plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
|
|
1037
|
+
plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
|
|
1038
|
+
``,
|
|
1039
|
+
`## History`,
|
|
1040
|
+
...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
|
|
1041
|
+
``,
|
|
1042
|
+
`## Recent Events`,
|
|
1043
|
+
...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
|
|
1044
|
+
``,
|
|
1045
|
+
`## Diagnostic Questions`,
|
|
1046
|
+
`Based on the information above, please analyze:`,
|
|
1047
|
+
`1. What is the root cause of the issue being in "${state}" state?`,
|
|
1048
|
+
`2. Is the error recoverable? If so, what steps should be taken?`,
|
|
1049
|
+
`3. Does the plan need modification before retrying?`,
|
|
1050
|
+
`4. Are there any dependency or configuration issues that need resolution?`,
|
|
1051
|
+
`5. What is the recommended next action?`
|
|
1052
|
+
].filter((line) => line !== void 0).join("\n");
|
|
1053
|
+
return {
|
|
1054
|
+
description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
|
|
1055
|
+
messages: [{
|
|
1056
|
+
role: "user",
|
|
1057
|
+
content: { type: "text", text: diagnosticText }
|
|
1058
|
+
}]
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
if (name === "fifony-weekly-summary") {
|
|
1062
|
+
const issues = await getIssues();
|
|
1063
|
+
let analytics = {};
|
|
1169
1064
|
try {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
} catch (error) {
|
|
1173
|
-
throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
|
|
1065
|
+
analytics = await apiGet("/api/analytics/tokens");
|
|
1066
|
+
} catch {
|
|
1174
1067
|
}
|
|
1068
|
+
const overall = analytics.overall ?? {};
|
|
1069
|
+
const byState = issues.reduce((accumulator, issue) => {
|
|
1070
|
+
const key = issue.state ?? "Unknown";
|
|
1071
|
+
accumulator[key] = (accumulator[key] ?? 0) + 1;
|
|
1072
|
+
return accumulator;
|
|
1073
|
+
}, {});
|
|
1074
|
+
const totalIssues = issues.length;
|
|
1075
|
+
const completed = byState["Done"] ?? 0;
|
|
1076
|
+
const blocked = (byState["Blocked"] ?? 0) + (byState["Failed"] ?? 0);
|
|
1077
|
+
const inProgress = (byState["Running"] ?? 0) + (byState["Reviewing"] ?? 0) + (byState["Reviewed"] ?? 0) + (byState["Queued"] ?? 0);
|
|
1078
|
+
const planned = byState["Planned"] ?? 0;
|
|
1079
|
+
const planning = byState["Planning"] ?? 0;
|
|
1080
|
+
const cancelled = byState["Cancelled"] ?? 0;
|
|
1081
|
+
const inputTokens = typeof overall.inputTokens === "number" ? overall.inputTokens : 0;
|
|
1082
|
+
const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
|
|
1083
|
+
const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
|
|
1084
|
+
const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
|
|
1085
|
+
const summaryText = [
|
|
1086
|
+
`# Fifony Weekly Progress Summary`,
|
|
1087
|
+
``,
|
|
1088
|
+
`## Issue Statistics`,
|
|
1089
|
+
`| Status | Count |`,
|
|
1090
|
+
`|--------|-------|`,
|
|
1091
|
+
`| Total Issues | ${totalIssues} |`,
|
|
1092
|
+
`| Completed (Done) | ${completed} |`,
|
|
1093
|
+
`| In Progress | ${inProgress} |`,
|
|
1094
|
+
`| Planned | ${planned} |`,
|
|
1095
|
+
`| Planning | ${planning} |`,
|
|
1096
|
+
`| Blocked/Failed | ${blocked} |`,
|
|
1097
|
+
`| Cancelled | ${cancelled} |`,
|
|
1098
|
+
``,
|
|
1099
|
+
`## Token Usage`,
|
|
1100
|
+
`- **Total Tokens**: ${totalTokens.toLocaleString()}`,
|
|
1101
|
+
`- **Input Tokens**: ${inputTokens.toLocaleString()}`,
|
|
1102
|
+
`- **Output Tokens**: ${outputTokens.toLocaleString()}`,
|
|
1103
|
+
`- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
|
|
1104
|
+
``,
|
|
1105
|
+
`## Analysis Request`,
|
|
1106
|
+
`Based on these metrics, please provide:`,
|
|
1107
|
+
`1. A brief summary of overall progress this week`,
|
|
1108
|
+
`2. Identification of any bottlenecks (blocked/failed issues)`,
|
|
1109
|
+
`3. Token usage efficiency assessment`,
|
|
1110
|
+
`4. Recommendations for improving throughput`,
|
|
1111
|
+
`5. Priority items for next week`
|
|
1112
|
+
].join("\n");
|
|
1113
|
+
return {
|
|
1114
|
+
description: "Weekly progress summary prompt for the Fifony workspace.",
|
|
1115
|
+
messages: [{
|
|
1116
|
+
role: "user",
|
|
1117
|
+
content: { type: "text", text: summaryText }
|
|
1118
|
+
}]
|
|
1119
|
+
};
|
|
1175
1120
|
}
|
|
1176
|
-
if (name === "fifony
|
|
1121
|
+
if (name === "fifony-refine-plan") {
|
|
1177
1122
|
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
1123
|
+
const concern = typeof args.concern === "string" ? args.concern.trim() : "";
|
|
1178
1124
|
if (!issueId) throw new Error("issueId is required");
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1125
|
+
const issue = await getIssue(issueId);
|
|
1126
|
+
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
1127
|
+
const issueData = issue;
|
|
1128
|
+
const plan = issueData.plan ?? null;
|
|
1129
|
+
const steps = plan?.steps ?? [];
|
|
1130
|
+
const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
|
|
1131
|
+
${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
|
|
1132
|
+
const refinementText = [
|
|
1133
|
+
`# Plan Refinement for Issue ${issueId}`,
|
|
1134
|
+
``,
|
|
1135
|
+
`## Issue`,
|
|
1136
|
+
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
1137
|
+
`- **Description**: ${issueData.description ?? "No description"}`,
|
|
1138
|
+
``,
|
|
1139
|
+
`## Current Plan`,
|
|
1140
|
+
plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
|
|
1141
|
+
plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
|
|
1142
|
+
``,
|
|
1143
|
+
`### Steps`,
|
|
1144
|
+
stepsText,
|
|
1145
|
+
``,
|
|
1146
|
+
concern ? `## Specific Concern
|
|
1147
|
+
${concern}
|
|
1148
|
+
` : "",
|
|
1149
|
+
`## Refinement Guidance`,
|
|
1150
|
+
`Please review the current plan and provide specific, actionable feedback:`,
|
|
1151
|
+
`1. Are the steps correctly ordered and complete?`,
|
|
1152
|
+
`2. Are there missing edge cases or error handling steps?`,
|
|
1153
|
+
`3. Is the complexity estimate accurate?`,
|
|
1154
|
+
`4. Are the file paths and affected areas correct?`,
|
|
1155
|
+
`5. Should any steps be split, merged, or removed?`,
|
|
1156
|
+
``,
|
|
1157
|
+
`Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
|
|
1158
|
+
].filter((line) => line !== void 0).join("\n");
|
|
1159
|
+
return {
|
|
1160
|
+
description: `Plan refinement prompt for issue ${issueId}.`,
|
|
1161
|
+
messages: [{
|
|
1162
|
+
role: "user",
|
|
1163
|
+
content: { type: "text", text: refinementText }
|
|
1164
|
+
}]
|
|
1165
|
+
};
|
|
1185
1166
|
}
|
|
1186
|
-
if (name === "fifony
|
|
1167
|
+
if (name === "fifony-code-review") {
|
|
1187
1168
|
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const query = params.toString();
|
|
1194
|
-
try {
|
|
1195
|
-
const result = await apiGet(`/api/events/feed${query ? `?${query}` : ""}`);
|
|
1196
|
-
const events = Array.isArray(result.events) ? result.events.slice(0, limit) : [];
|
|
1197
|
-
return toolText(JSON.stringify({ events, count: events.length }, null, 2));
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
const events = await listEvents({ limit });
|
|
1200
|
-
const filtered = events.filter((event) => {
|
|
1201
|
-
if (issueId && event.issueId !== issueId) return false;
|
|
1202
|
-
if (kind && event.kind !== kind) return false;
|
|
1203
|
-
return true;
|
|
1204
|
-
}).slice(0, limit);
|
|
1205
|
-
return toolText(JSON.stringify({ events: filtered, count: filtered.length }, null, 2));
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
if (name === "fifony.get_workflow") {
|
|
1209
|
-
try {
|
|
1210
|
-
const result = await apiGet("/api/config/workflow");
|
|
1211
|
-
return toolText(JSON.stringify(result, null, 2));
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
throw new Error(`Failed to get workflow config: ${String(error)}`);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
if (name === "fifony.set_workflow") {
|
|
1217
|
-
const plan = args.plan;
|
|
1218
|
-
const execute = args.execute;
|
|
1219
|
-
const review = args.review;
|
|
1220
|
-
if (!plan || !execute || !review) throw new Error("plan, execute, and review are all required");
|
|
1221
|
-
try {
|
|
1222
|
-
const result = await apiPost("/api/config/workflow", { workflow: { plan, execute, review } });
|
|
1223
|
-
return toolText(JSON.stringify({ message: "Workflow configuration updated successfully.", workflow: result.workflow }, null, 2));
|
|
1224
|
-
} catch (error) {
|
|
1225
|
-
throw new Error(`Failed to set workflow config: ${String(error)}`);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
if (name === "fifony.scan_project") {
|
|
1229
|
-
try {
|
|
1230
|
-
const result = await apiGet("/api/scan/project");
|
|
1231
|
-
return toolText(JSON.stringify(result, null, 2));
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
throw new Error(`Failed to scan project: ${String(error)}`);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
if (name === "fifony.install_agents") {
|
|
1237
|
-
const agents = Array.isArray(args.agents) ? args.agents.filter((value) => typeof value === "string") : [];
|
|
1238
|
-
if (agents.length === 0) throw new Error("At least one agent name is required");
|
|
1169
|
+
if (!issueId) throw new Error("issueId is required");
|
|
1170
|
+
const issue = await getIssue(issueId);
|
|
1171
|
+
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
1172
|
+
const issueData = issue;
|
|
1173
|
+
let diffData = {};
|
|
1239
1174
|
try {
|
|
1240
|
-
|
|
1241
|
-
return toolText(JSON.stringify(result, null, 2));
|
|
1175
|
+
diffData = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
|
|
1242
1176
|
} catch (error) {
|
|
1243
|
-
throw new Error(`
|
|
1177
|
+
throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
|
|
1244
1178
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1179
|
+
const files = Array.isArray(diffData.files) ? diffData.files : [];
|
|
1180
|
+
const diff = typeof diffData.diff === "string" ? diffData.diff : "";
|
|
1181
|
+
const totalAdditions = typeof diffData.totalAdditions === "number" ? diffData.totalAdditions : 0;
|
|
1182
|
+
const totalDeletions = typeof diffData.totalDeletions === "number" ? diffData.totalDeletions : 0;
|
|
1183
|
+
if (!diff.trim()) {
|
|
1184
|
+
return {
|
|
1185
|
+
description: `Code review prompt for issue ${issueId} (no changes).`,
|
|
1186
|
+
messages: [{
|
|
1187
|
+
role: "user",
|
|
1188
|
+
content: { type: "text", text: `# Code Review for ${issueId}
|
|
1189
|
+
|
|
1190
|
+
No code changes found for this issue. The workspace may not have been created yet or no modifications were made.` }
|
|
1191
|
+
}]
|
|
1192
|
+
};
|
|
1254
1193
|
}
|
|
1194
|
+
const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
|
|
1195
|
+
const reviewText = [
|
|
1196
|
+
`# Code Review for Issue ${issueId}`,
|
|
1197
|
+
``,
|
|
1198
|
+
`## Issue Context`,
|
|
1199
|
+
`- **Title**: ${issueData.title ?? "Unknown"}`,
|
|
1200
|
+
`- **Description**: ${issueData.description ?? "No description"}`,
|
|
1201
|
+
`- **State**: ${issueData.state ?? "Unknown"}`,
|
|
1202
|
+
``,
|
|
1203
|
+
`## Change Summary`,
|
|
1204
|
+
`- **Files Changed**: ${files.length}`,
|
|
1205
|
+
`- **Total Additions**: +${totalAdditions}`,
|
|
1206
|
+
`- **Total Deletions**: -${totalDeletions}`,
|
|
1207
|
+
``,
|
|
1208
|
+
`### Files`,
|
|
1209
|
+
`| Path | Status | Additions | Deletions |`,
|
|
1210
|
+
`|------|--------|-----------|-----------|`,
|
|
1211
|
+
filesTable,
|
|
1212
|
+
``,
|
|
1213
|
+
`## Diff`,
|
|
1214
|
+
"```diff",
|
|
1215
|
+
diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
|
|
1216
|
+
"```",
|
|
1217
|
+
``,
|
|
1218
|
+
`## Review Checklist`,
|
|
1219
|
+
`Please review the changes and evaluate:`,
|
|
1220
|
+
`1. **Correctness**: Do the changes correctly implement what the issue describes?`,
|
|
1221
|
+
`2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
|
|
1222
|
+
`3. **Error Handling**: Are edge cases and errors properly handled?`,
|
|
1223
|
+
`4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
|
|
1224
|
+
`5. **Performance**: Are there any performance concerns or inefficiencies?`,
|
|
1225
|
+
`6. **Tests**: Are changes adequately covered by tests?`,
|
|
1226
|
+
`7. **Breaking Changes**: Do any changes break backward compatibility?`
|
|
1227
|
+
].join("\n");
|
|
1228
|
+
return {
|
|
1229
|
+
description: `Code review prompt for issue ${issueId}.`,
|
|
1230
|
+
messages: [{
|
|
1231
|
+
role: "user",
|
|
1232
|
+
content: { type: "text", text: reviewText }
|
|
1233
|
+
}]
|
|
1234
|
+
};
|
|
1255
1235
|
}
|
|
1256
|
-
throw new Error(`Unknown
|
|
1236
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/mcp/jsonrpc-transport.ts
|
|
1240
|
+
var incomingBuffer = Buffer.alloc(0);
|
|
1241
|
+
function setIncomingBuffer(buffer) {
|
|
1242
|
+
incomingBuffer = buffer;
|
|
1257
1243
|
}
|
|
1258
1244
|
function sendMessage(message) {
|
|
1259
1245
|
const payload = Buffer.from(JSON.stringify(message), "utf8");
|
|
@@ -1331,13 +1317,21 @@ function processIncomingBuffer() {
|
|
|
1331
1317
|
void handleRequest(request);
|
|
1332
1318
|
}
|
|
1333
1319
|
}
|
|
1320
|
+
|
|
1321
|
+
// src/mcp/server.ts
|
|
1322
|
+
var DEBUG_BOOT2 = env3.FIFONY_DEBUG_BOOT === "1";
|
|
1323
|
+
function debugBoot2(message) {
|
|
1324
|
+
if (!DEBUG_BOOT2) return;
|
|
1325
|
+
process.stderr.write(`[FIFONY_DEBUG_BOOT] ${message}
|
|
1326
|
+
`);
|
|
1327
|
+
}
|
|
1334
1328
|
async function bootstrap() {
|
|
1335
1329
|
debugBoot2("mcp:bootstrap:start");
|
|
1336
1330
|
await initDatabase();
|
|
1337
1331
|
debugBoot2("mcp:bootstrap:database-ready");
|
|
1338
1332
|
await appendEvent("info", "Fifony MCP server started.", { workspaceRoot: WORKSPACE_ROOT, persistenceRoot: PERSISTENCE_ROOT });
|
|
1339
1333
|
stdin.on("data", (chunk) => {
|
|
1340
|
-
|
|
1334
|
+
setIncomingBuffer(Buffer.concat([incomingBuffer, chunk]));
|
|
1341
1335
|
processIncomingBuffer();
|
|
1342
1336
|
});
|
|
1343
1337
|
stdin.resume();
|