fifony 0.1.11
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/FIFONY.md +173 -0
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +175 -0
- package/app/dist/assets/index-BE3a-eEo.js +13 -0
- package/app/dist/icon-maskable.svg +8 -0
- package/app/dist/icon.svg +7 -0
- package/app/dist/index.html +24 -0
- package/app/dist/manifest.webmanifest +49 -0
- package/app/dist/offline.html +86 -0
- package/app/dist/service-worker.js +100 -0
- package/app/public/icon-maskable.svg +8 -0
- package/app/public/icon.svg +7 -0
- package/app/public/manifest.webmanifest +49 -0
- package/app/public/offline.html +86 -0
- package/app/public/service-worker.js +100 -0
- package/bin/fifony.js +54 -0
- package/dist/chunk-LH5V2WV2.js +389 -0
- package/dist/chunk-LH5V2WV2.js.map +1 -0
- package/dist/cli.js +204 -0
- package/dist/cli.js.map +1 -0
- package/dist/mcp/server.js +747 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/runtime/run-local.js +6569 -0
- package/dist/runtime/run-local.js.map +1 -0
- package/package.json +69 -0
- package/src/fixtures/agent-catalog.json +208 -0
- package/src/fixtures/skill-catalog.json +67 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inferCapabilityPaths,
|
|
3
|
+
renderPrompt,
|
|
4
|
+
resolveTaskCapabilities
|
|
5
|
+
} from "../chunk-LH5V2WV2.js";
|
|
6
|
+
|
|
7
|
+
// src/mcp/server.ts
|
|
8
|
+
import { createHash } from "crypto";
|
|
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
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/mcp/database.ts
|
|
88
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
89
|
+
import { basename, join as join2, resolve as resolve2 } from "path";
|
|
90
|
+
import { env } from "process";
|
|
91
|
+
import { homedir as homedir2 } from "os";
|
|
92
|
+
import { fileURLToPath } from "url";
|
|
93
|
+
var WORKSPACE_ROOT = env.FIFONY_WORKSPACE_ROOT ?? process.cwd();
|
|
94
|
+
var PERSISTENCE_ROOT = env.FIFONY_PERSISTENCE ?? WORKSPACE_ROOT;
|
|
95
|
+
var STATE_ROOT = resolvePersistenceRoot(PERSISTENCE_ROOT);
|
|
96
|
+
var DATABASE_PATH = join2(STATE_ROOT, "s3db");
|
|
97
|
+
var STORAGE_BUCKET = env.FIFONY_STORAGE_BUCKET ?? "fifony";
|
|
98
|
+
var STORAGE_KEY_PREFIX = env.FIFONY_STORAGE_KEY_PREFIX ?? "state";
|
|
99
|
+
var DEBUG_BOOT = env.FIFONY_DEBUG_BOOT === "1";
|
|
100
|
+
function resolvePersistenceRoot(value) {
|
|
101
|
+
const resolved = value.startsWith("file://") ? fileURLToPath(value) : value.startsWith("~/") ? resolve2(homedir2(), value.slice(2)) : resolve2(value);
|
|
102
|
+
return basename(resolved) === ".fifony" ? resolved : join2(resolved, ".fifony");
|
|
103
|
+
}
|
|
104
|
+
var RUNTIME_RESOURCE = "runtime_state";
|
|
105
|
+
var ISSUE_RESOURCE = "issues";
|
|
106
|
+
var EVENT_RESOURCE = "events";
|
|
107
|
+
var SESSION_RESOURCE = "agent_sessions";
|
|
108
|
+
var PIPELINE_RESOURCE = "agent_pipelines";
|
|
109
|
+
var RUNTIME_RECORD_ID = "current";
|
|
110
|
+
var database = null;
|
|
111
|
+
var runtimeResource = null;
|
|
112
|
+
var issueResource = null;
|
|
113
|
+
var eventResource = null;
|
|
114
|
+
var sessionResource = null;
|
|
115
|
+
var pipelineResource = null;
|
|
116
|
+
function getResources() {
|
|
117
|
+
return { runtimeResource, issueResource, eventResource, sessionResource, pipelineResource };
|
|
118
|
+
}
|
|
119
|
+
function getDatabase() {
|
|
120
|
+
return database;
|
|
121
|
+
}
|
|
122
|
+
function debugBoot(message) {
|
|
123
|
+
if (!DEBUG_BOOT) return;
|
|
124
|
+
process.stderr.write(`[FIFONY_DEBUG_BOOT] ${message}
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
function nowIso() {
|
|
128
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
129
|
+
}
|
|
130
|
+
function safeRead(path) {
|
|
131
|
+
return existsSync2(path) ? readFileSync2(path, "utf8") : "";
|
|
132
|
+
}
|
|
133
|
+
async function loadS3dbModule() {
|
|
134
|
+
try {
|
|
135
|
+
const imported = await import("s3db.js/lite");
|
|
136
|
+
return {
|
|
137
|
+
default: imported.default,
|
|
138
|
+
FileSystemClient: imported.FileSystemClient
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error(`Unable to load s3db.js: ${String(error)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function initDatabase() {
|
|
145
|
+
if (database) return database;
|
|
146
|
+
debugBoot("mcp:getDatabase:start");
|
|
147
|
+
const s3db = await loadS3dbModule();
|
|
148
|
+
debugBoot("mcp:getDatabase:module-loaded");
|
|
149
|
+
const client = new s3db.FileSystemClient({
|
|
150
|
+
basePath: DATABASE_PATH,
|
|
151
|
+
bucket: STORAGE_BUCKET,
|
|
152
|
+
keyPrefix: STORAGE_KEY_PREFIX,
|
|
153
|
+
verbose: false
|
|
154
|
+
});
|
|
155
|
+
database = new s3db.default({ client, verbose: false });
|
|
156
|
+
await database.connect();
|
|
157
|
+
debugBoot("mcp:getDatabase:connected");
|
|
158
|
+
runtimeResource = database.resources[RUNTIME_RESOURCE] ?? await database.createResource({
|
|
159
|
+
name: RUNTIME_RESOURCE,
|
|
160
|
+
behavior: "body-overflow",
|
|
161
|
+
attributes: {
|
|
162
|
+
updatedAt: "datetime|required",
|
|
163
|
+
state: "json|required"
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
issueResource = database.resources[ISSUE_RESOURCE] ?? await database.createResource({
|
|
167
|
+
name: ISSUE_RESOURCE,
|
|
168
|
+
behavior: "body-overflow",
|
|
169
|
+
attributes: {
|
|
170
|
+
id: "string|required",
|
|
171
|
+
identifier: "string|required",
|
|
172
|
+
title: "string|required",
|
|
173
|
+
description: "string|optional",
|
|
174
|
+
priority: "number|required",
|
|
175
|
+
state: "string|required",
|
|
176
|
+
branchName: "string|optional",
|
|
177
|
+
labels: "json|required",
|
|
178
|
+
paths: "json|optional",
|
|
179
|
+
inferredPaths: "json|optional",
|
|
180
|
+
capabilityCategory: "string|optional",
|
|
181
|
+
capabilityOverlays: "json|optional",
|
|
182
|
+
capabilityRationale: "json|optional",
|
|
183
|
+
blockedBy: "json|required",
|
|
184
|
+
assignedToWorker: "boolean|required",
|
|
185
|
+
createdAt: "datetime|required",
|
|
186
|
+
updatedAt: "datetime|required",
|
|
187
|
+
history: "json|required",
|
|
188
|
+
attempts: "number|required",
|
|
189
|
+
maxAttempts: "number|required",
|
|
190
|
+
url: "string|optional",
|
|
191
|
+
assigneeId: "string|optional",
|
|
192
|
+
startedAt: "datetime|optional",
|
|
193
|
+
completedAt: "datetime|optional",
|
|
194
|
+
nextRetryAt: "datetime|optional",
|
|
195
|
+
workspacePath: "string|optional",
|
|
196
|
+
workspacePreparedAt: "datetime|optional",
|
|
197
|
+
lastError: "string|optional",
|
|
198
|
+
durationMs: "number|optional",
|
|
199
|
+
commandExitCode: "number|optional",
|
|
200
|
+
commandOutputTail: "string|optional"
|
|
201
|
+
},
|
|
202
|
+
partitions: {
|
|
203
|
+
byState: { fields: { state: "string" } },
|
|
204
|
+
byCapabilityCategory: { fields: { capabilityCategory: "string" } },
|
|
205
|
+
byStateAndCapability: {
|
|
206
|
+
fields: { state: "string", capabilityCategory: "string" }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
asyncPartitions: true
|
|
210
|
+
});
|
|
211
|
+
eventResource = database.resources[EVENT_RESOURCE] ?? await database.createResource({
|
|
212
|
+
name: EVENT_RESOURCE,
|
|
213
|
+
behavior: "body-overflow",
|
|
214
|
+
attributes: {
|
|
215
|
+
id: "string|required",
|
|
216
|
+
issueId: "string|optional",
|
|
217
|
+
kind: "string|required",
|
|
218
|
+
message: "string|required",
|
|
219
|
+
at: "datetime|required"
|
|
220
|
+
},
|
|
221
|
+
partitions: {
|
|
222
|
+
byIssueId: { fields: { issueId: "string" } },
|
|
223
|
+
byKind: { fields: { kind: "string" } },
|
|
224
|
+
byIssueIdAndKind: { fields: { issueId: "string", kind: "string" } }
|
|
225
|
+
},
|
|
226
|
+
asyncPartitions: true
|
|
227
|
+
});
|
|
228
|
+
sessionResource = database.resources[SESSION_RESOURCE] ?? await database.createResource({
|
|
229
|
+
name: SESSION_RESOURCE,
|
|
230
|
+
behavior: "body-overflow",
|
|
231
|
+
attributes: {
|
|
232
|
+
id: "string|required",
|
|
233
|
+
issueId: "string|required",
|
|
234
|
+
issueIdentifier: "string|required",
|
|
235
|
+
attempt: "number|required",
|
|
236
|
+
provider: "string|required",
|
|
237
|
+
role: "string|required",
|
|
238
|
+
cycle: "number|required",
|
|
239
|
+
session: "json|required",
|
|
240
|
+
updatedAt: "datetime|required"
|
|
241
|
+
},
|
|
242
|
+
partitions: {
|
|
243
|
+
byIssueId: { fields: { issueId: "string" } },
|
|
244
|
+
byIssueAttempt: { fields: { issueId: "string", attempt: "number" } },
|
|
245
|
+
byProviderRole: { fields: { provider: "string", role: "string" } }
|
|
246
|
+
},
|
|
247
|
+
asyncPartitions: true
|
|
248
|
+
});
|
|
249
|
+
pipelineResource = database.resources[PIPELINE_RESOURCE] ?? await database.createResource({
|
|
250
|
+
name: PIPELINE_RESOURCE,
|
|
251
|
+
behavior: "body-overflow",
|
|
252
|
+
attributes: {
|
|
253
|
+
id: "string|required",
|
|
254
|
+
issueId: "string|required",
|
|
255
|
+
issueIdentifier: "string|required",
|
|
256
|
+
attempt: "number|required",
|
|
257
|
+
pipeline: "json|required",
|
|
258
|
+
updatedAt: "datetime|required"
|
|
259
|
+
},
|
|
260
|
+
partitions: {
|
|
261
|
+
byIssueId: { fields: { issueId: "string" } },
|
|
262
|
+
byIssueAttempt: { fields: { issueId: "string", attempt: "number" } }
|
|
263
|
+
},
|
|
264
|
+
asyncPartitions: true
|
|
265
|
+
});
|
|
266
|
+
debugBoot("mcp:getDatabase:resources-ready");
|
|
267
|
+
return database;
|
|
268
|
+
}
|
|
269
|
+
async function listRecords(resource, limit = 100) {
|
|
270
|
+
if (!resource) return [];
|
|
271
|
+
if (typeof resource.query === "function") return await resource.query({});
|
|
272
|
+
return await resource.list({ limit });
|
|
273
|
+
}
|
|
274
|
+
async function listIssues(filters = {}) {
|
|
275
|
+
await initDatabase();
|
|
276
|
+
const { state, capabilityCategory } = filters;
|
|
277
|
+
if (!issueResource) return [];
|
|
278
|
+
const partition = state && capabilityCategory ? "byStateAndCapability" : state ? "byState" : capabilityCategory ? "byCapabilityCategory" : null;
|
|
279
|
+
const partitionValues = state && capabilityCategory ? { state, capabilityCategory } : state ? { state } : capabilityCategory ? { capabilityCategory } : {};
|
|
280
|
+
const records = await issueResource.list({ partition, partitionValues, limit: 500 });
|
|
281
|
+
return records.map((record) => record);
|
|
282
|
+
}
|
|
283
|
+
async function listEvents(filters = {}) {
|
|
284
|
+
await initDatabase();
|
|
285
|
+
const { issueId, kind, limit = 100 } = filters;
|
|
286
|
+
if (!eventResource) return [];
|
|
287
|
+
const partition = issueId && kind ? "byIssueIdAndKind" : issueId ? "byIssueId" : kind ? "byKind" : null;
|
|
288
|
+
const partitionValues = issueId && kind ? { issueId, kind } : issueId ? { issueId } : kind ? { kind } : {};
|
|
289
|
+
return await eventResource.list({ partition, partitionValues, limit });
|
|
290
|
+
}
|
|
291
|
+
async function getRuntimeSnapshot() {
|
|
292
|
+
await initDatabase();
|
|
293
|
+
const record = await runtimeResource?.get(RUNTIME_RECORD_ID);
|
|
294
|
+
const state = record?.state;
|
|
295
|
+
if (state && typeof state === "object") return state;
|
|
296
|
+
return {};
|
|
297
|
+
}
|
|
298
|
+
async function getIssues() {
|
|
299
|
+
return await listIssues();
|
|
300
|
+
}
|
|
301
|
+
async function getIssue(issueId) {
|
|
302
|
+
await initDatabase();
|
|
303
|
+
const issue = await issueResource?.get(issueId);
|
|
304
|
+
return issue ?? null;
|
|
305
|
+
}
|
|
306
|
+
async function appendEvent(level, message, payload = {}, issueId) {
|
|
307
|
+
await initDatabase();
|
|
308
|
+
const { randomUUID } = await import("crypto");
|
|
309
|
+
await eventResource?.insert({
|
|
310
|
+
id: randomUUID(),
|
|
311
|
+
issueId,
|
|
312
|
+
kind: level,
|
|
313
|
+
message,
|
|
314
|
+
at: nowIso()
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/mcp/server.ts
|
|
319
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
320
|
+
var __dirname = dirname(__filename);
|
|
321
|
+
var PACKAGE_ROOT = resolve3(__dirname, "../..");
|
|
322
|
+
var WORKFLOW_PATH = join3(WORKSPACE_ROOT, "WORKFLOW.md");
|
|
323
|
+
var README_PATH = join3(PACKAGE_ROOT, "README.md");
|
|
324
|
+
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
|
+
async function buildIntegrationGuide() {
|
|
336
|
+
return renderPrompt("mcp-integration-guide", {
|
|
337
|
+
workspaceRoot: WORKSPACE_ROOT,
|
|
338
|
+
persistenceRoot: PERSISTENCE_ROOT,
|
|
339
|
+
stateRoot: STATE_ROOT
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function computeCapabilityCounts(issues) {
|
|
343
|
+
return issues.reduce((accumulator, issue) => {
|
|
344
|
+
const key = typeof issue.capabilityCategory === "string" && issue.capabilityCategory.trim() ? issue.capabilityCategory.trim() : "default";
|
|
345
|
+
accumulator[key] = (accumulator[key] ?? 0) + 1;
|
|
346
|
+
return accumulator;
|
|
347
|
+
}, {});
|
|
348
|
+
}
|
|
349
|
+
async function buildStateSummary() {
|
|
350
|
+
const runtime = await getRuntimeSnapshot();
|
|
351
|
+
const issues = await getIssues();
|
|
352
|
+
const { sessionResource: sessionResource2, pipelineResource: pipelineResource2 } = getResources();
|
|
353
|
+
const sessions = await listRecords(sessionResource2, 500);
|
|
354
|
+
const pipelines = await listRecords(pipelineResource2, 500);
|
|
355
|
+
const events = await listEvents({ limit: 100 });
|
|
356
|
+
const byState = issues.reduce((accumulator, issue) => {
|
|
357
|
+
const key = issue.state ?? "Unknown";
|
|
358
|
+
accumulator[key] = (accumulator[key] ?? 0) + 1;
|
|
359
|
+
return accumulator;
|
|
360
|
+
}, {});
|
|
361
|
+
const byCapability = computeCapabilityCounts(issues);
|
|
362
|
+
return JSON.stringify({
|
|
363
|
+
workspaceRoot: WORKSPACE_ROOT,
|
|
364
|
+
persistenceRoot: PERSISTENCE_ROOT,
|
|
365
|
+
stateRoot: STATE_ROOT,
|
|
366
|
+
workflowPresent: existsSync3(WORKFLOW_PATH),
|
|
367
|
+
runtimeUpdatedAt: runtime.updatedAt ?? null,
|
|
368
|
+
issueCount: issues.length,
|
|
369
|
+
issuesByState: byState,
|
|
370
|
+
issuesByCapability: byCapability,
|
|
371
|
+
sessionCount: sessions.length,
|
|
372
|
+
pipelineCount: pipelines.length,
|
|
373
|
+
recentEventCount: events.length
|
|
374
|
+
}, null, 2);
|
|
375
|
+
}
|
|
376
|
+
async function buildIssuePrompt(issue, provider, role) {
|
|
377
|
+
const resolution = resolveTaskCapabilities({
|
|
378
|
+
id: issue.id,
|
|
379
|
+
identifier: issue.identifier,
|
|
380
|
+
title: issue.title,
|
|
381
|
+
description: typeof issue.description === "string" ? issue.description : "",
|
|
382
|
+
labels: Array.isArray(issue.labels) ? issue.labels.filter((value) => typeof value === "string") : [],
|
|
383
|
+
paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : []
|
|
384
|
+
});
|
|
385
|
+
return renderPrompt("mcp-issue", {
|
|
386
|
+
role,
|
|
387
|
+
provider,
|
|
388
|
+
id: issue.id,
|
|
389
|
+
title: issue.title,
|
|
390
|
+
state: issue.state ?? "Todo",
|
|
391
|
+
capabilityCategory: resolution.category,
|
|
392
|
+
overlays: resolution.overlays,
|
|
393
|
+
paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : [],
|
|
394
|
+
description: issue.description || "No description provided."
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async function listResourcesMcp() {
|
|
398
|
+
const issues = await getIssues();
|
|
399
|
+
const resources = [
|
|
400
|
+
{ uri: "fifony://guide/overview", name: "Fifony overview", description: "High-level overview and local integration guide.", mimeType: "text/markdown" },
|
|
401
|
+
{ uri: "fifony://guide/runtime", name: "Fifony runtime guide", description: "Detailed local runtime reference for the package.", mimeType: "text/markdown" },
|
|
402
|
+
{ uri: "fifony://guide/integration", name: "Fifony MCP integration guide", description: "How to wire an MCP client to this Fifony workspace.", mimeType: "text/markdown" },
|
|
403
|
+
{ uri: "fifony://state/summary", name: "Fifony state summary", description: "Compact summary of the current runtime, issue, and pipeline state.", mimeType: "application/json" },
|
|
404
|
+
{ uri: "fifony://issues", name: "Fifony issues", description: "Full issue list from the durable Fifony store.", mimeType: "application/json" },
|
|
405
|
+
{ uri: "fifony://integrations", name: "Fifony integrations", description: "Discovered local integrations such as agency-agents and impeccable skills.", mimeType: "application/json" },
|
|
406
|
+
{ uri: "fifony://capabilities", name: "Fifony capability routing", description: "How Fifony would route current issues to providers, profiles, and overlays.", mimeType: "application/json" }
|
|
407
|
+
];
|
|
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
|
+
for (const issue of issues.slice(0, 100)) {
|
|
412
|
+
resources.push({ uri: `fifony://issue/${encodeURIComponent(issue.id)}`, name: `Issue ${issue.id}`, description: issue.title, mimeType: "application/json" });
|
|
413
|
+
}
|
|
414
|
+
return resources;
|
|
415
|
+
}
|
|
416
|
+
async function readResource(uri) {
|
|
417
|
+
if (uri === "fifony://guide/overview") return [{ uri, mimeType: "text/markdown", text: safeRead(README_PATH) }];
|
|
418
|
+
if (uri === "fifony://guide/runtime") return [{ uri, mimeType: "text/markdown", text: safeRead(FIFONY_GUIDE_PATH) }];
|
|
419
|
+
if (uri === "fifony://guide/integration") return [{ uri, mimeType: "text/markdown", text: await buildIntegrationGuide() }];
|
|
420
|
+
if (uri === "fifony://state/summary") return [{ uri, mimeType: "application/json", text: await buildStateSummary() }];
|
|
421
|
+
if (uri === "fifony://issues") return [{ uri, mimeType: "application/json", text: JSON.stringify(await getIssues(), null, 2) }];
|
|
422
|
+
if (uri === "fifony://integrations") {
|
|
423
|
+
return [{ uri, mimeType: "application/json", text: JSON.stringify(discoverIntegrations(WORKSPACE_ROOT), null, 2) }];
|
|
424
|
+
}
|
|
425
|
+
if (uri === "fifony://capabilities") {
|
|
426
|
+
const issues = await getIssues();
|
|
427
|
+
return [{
|
|
428
|
+
uri,
|
|
429
|
+
mimeType: "application/json",
|
|
430
|
+
text: JSON.stringify(
|
|
431
|
+
issues.map((issue) => ({
|
|
432
|
+
issueId: issue.id,
|
|
433
|
+
title: issue.title,
|
|
434
|
+
paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : [],
|
|
435
|
+
inferredPaths: inferCapabilityPaths({
|
|
436
|
+
id: issue.id,
|
|
437
|
+
identifier: issue.identifier,
|
|
438
|
+
title: issue.title,
|
|
439
|
+
description: issue.description,
|
|
440
|
+
labels: Array.isArray(issue.labels) ? issue.labels.filter((value) => typeof value === "string") : [],
|
|
441
|
+
paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : []
|
|
442
|
+
}),
|
|
443
|
+
resolution: resolveTaskCapabilities({
|
|
444
|
+
id: issue.id,
|
|
445
|
+
identifier: issue.identifier,
|
|
446
|
+
title: issue.title,
|
|
447
|
+
description: issue.description,
|
|
448
|
+
labels: Array.isArray(issue.labels) ? issue.labels.filter((value) => typeof value === "string") : [],
|
|
449
|
+
paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : []
|
|
450
|
+
})
|
|
451
|
+
})),
|
|
452
|
+
null,
|
|
453
|
+
2
|
|
454
|
+
)
|
|
455
|
+
}];
|
|
456
|
+
}
|
|
457
|
+
if (uri === "fifony://workspace/workflow") return [{ uri, mimeType: "text/markdown", text: safeRead(WORKFLOW_PATH) }];
|
|
458
|
+
if (uri.startsWith("fifony://issue/")) {
|
|
459
|
+
const issueId = decodeURIComponent(uri.substring("fifony://issue/".length));
|
|
460
|
+
const issue = await getIssue(issueId);
|
|
461
|
+
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
462
|
+
return [{ uri, mimeType: "application/json", text: JSON.stringify(issue, null, 2) }];
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
465
|
+
}
|
|
466
|
+
function listPrompts() {
|
|
467
|
+
return [
|
|
468
|
+
{ 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 }] },
|
|
469
|
+
{ 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 }] },
|
|
470
|
+
{ name: "fifony-review-workflow", description: "Review the current WORKFLOW.md and propose improvements for orchestration quality.", arguments: [{ name: "provider", description: "Reviewing model or client.", required: false }] },
|
|
471
|
+
{ 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 }] },
|
|
472
|
+
{ 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 }] }
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
async function getPrompt(name, args = {}) {
|
|
476
|
+
if (name === "fifony-integrate-client") {
|
|
477
|
+
const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
|
|
478
|
+
const goal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : "integrate with the local Fifony workspace";
|
|
479
|
+
const integrationGuide = await buildIntegrationGuide();
|
|
480
|
+
return {
|
|
481
|
+
description: "Client integration prompt for Fifony.",
|
|
482
|
+
messages: [{
|
|
483
|
+
role: "user",
|
|
484
|
+
content: {
|
|
485
|
+
type: "text",
|
|
486
|
+
text: await renderPrompt("mcp-integrate-client", { client, goal, integrationGuide })
|
|
487
|
+
}
|
|
488
|
+
}]
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (name === "fifony-plan-issue") {
|
|
492
|
+
const issueId = typeof args.issueId === "string" ? args.issueId : "";
|
|
493
|
+
const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "codex";
|
|
494
|
+
const issue = issueId ? await getIssue(issueId) : null;
|
|
495
|
+
if (!issue) throw new Error(`Issue not found: ${issueId}`);
|
|
496
|
+
return {
|
|
497
|
+
description: "Issue planning prompt grounded in the Fifony issue store.",
|
|
498
|
+
messages: [{
|
|
499
|
+
role: "user",
|
|
500
|
+
content: { type: "text", text: await buildIssuePrompt(issue, provider, "planner") }
|
|
501
|
+
}]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (name === "fifony-review-workflow") {
|
|
505
|
+
const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "claude";
|
|
506
|
+
return {
|
|
507
|
+
description: "Workflow review prompt for Fifony orchestration.",
|
|
508
|
+
messages: [{
|
|
509
|
+
role: "user",
|
|
510
|
+
content: {
|
|
511
|
+
type: "text",
|
|
512
|
+
text: await renderPrompt("mcp-review-workflow", {
|
|
513
|
+
provider,
|
|
514
|
+
workspaceRoot: WORKSPACE_ROOT,
|
|
515
|
+
workflowPresent: existsSync3(WORKFLOW_PATH) ? "yes" : "no"
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
}]
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (name === "fifony-use-integration") {
|
|
522
|
+
const integration = typeof args.integration === "string" ? args.integration : "";
|
|
523
|
+
return {
|
|
524
|
+
description: "Integration guidance for a discovered Fifony extension.",
|
|
525
|
+
messages: [{
|
|
526
|
+
role: "user",
|
|
527
|
+
content: { type: "text", text: await buildIntegrationSnippet(integration, WORKSPACE_ROOT) }
|
|
528
|
+
}]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (name === "fifony-route-task") {
|
|
532
|
+
const title = typeof args.title === "string" ? args.title : "";
|
|
533
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
534
|
+
const labels = typeof args.labels === "string" ? args.labels.split(",").map((label) => label.trim()).filter(Boolean) : [];
|
|
535
|
+
const paths = typeof args.paths === "string" ? args.paths.split(",").map((value) => value.trim()).filter(Boolean) : [];
|
|
536
|
+
const resolution = resolveTaskCapabilities({ title, description, labels, paths });
|
|
537
|
+
return {
|
|
538
|
+
description: "Task routing prompt produced by the Fifony capability resolver.",
|
|
539
|
+
messages: [{
|
|
540
|
+
role: "user",
|
|
541
|
+
content: {
|
|
542
|
+
type: "text",
|
|
543
|
+
text: await renderPrompt("mcp-route-task", {
|
|
544
|
+
resolutionJson: JSON.stringify(resolution, null, 2)
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
}]
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
551
|
+
}
|
|
552
|
+
function listTools() {
|
|
553
|
+
return [
|
|
554
|
+
{ name: "fifony.status", description: "Return a compact status summary for the current Fifony workspace.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
555
|
+
{ 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 } },
|
|
556
|
+
{ 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 } },
|
|
557
|
+
{ 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 } },
|
|
558
|
+
{ 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 } },
|
|
559
|
+
{ name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
560
|
+
{ 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 } },
|
|
561
|
+
{ 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 } }
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
function toolText(text) {
|
|
565
|
+
return { content: [{ type: "text", text }] };
|
|
566
|
+
}
|
|
567
|
+
async function callTool(name, args = {}) {
|
|
568
|
+
if (name === "fifony.status") return toolText(await buildStateSummary());
|
|
569
|
+
if (name === "fifony.list_issues") {
|
|
570
|
+
const stateFilter = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "";
|
|
571
|
+
const capabilityCategory = typeof args.capabilityCategory === "string" && args.capabilityCategory.trim() ? args.capabilityCategory.trim() : typeof args.category === "string" && args.category.trim() ? args.category.trim() : "";
|
|
572
|
+
return toolText(JSON.stringify(await listIssues({ state: stateFilter || void 0, capabilityCategory: capabilityCategory || void 0 }), null, 2));
|
|
573
|
+
}
|
|
574
|
+
if (name === "fifony.create_issue") {
|
|
575
|
+
await initDatabase();
|
|
576
|
+
const { issueResource: issueResource2 } = getResources();
|
|
577
|
+
const title = typeof args.title === "string" ? args.title.trim() : "";
|
|
578
|
+
if (!title) throw new Error("title is required");
|
|
579
|
+
const explicitId = typeof args.id === "string" && args.id.trim() ? args.id.trim() : "";
|
|
580
|
+
const issueId = explicitId || `LOCAL-${hashInput(`${title}:${nowIso()}`)}`.toUpperCase();
|
|
581
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
582
|
+
const priority = typeof args.priority === "number" ? args.priority : 2;
|
|
583
|
+
const state = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "Todo";
|
|
584
|
+
const baseLabels = Array.isArray(args.labels) ? args.labels.filter((value) => typeof value === "string") : ["fifony", "mcp"];
|
|
585
|
+
const paths = Array.isArray(args.paths) ? args.paths.filter((value) => typeof value === "string") : [];
|
|
586
|
+
const inferredPaths = inferCapabilityPaths({ id: issueId, identifier: issueId, title, description, labels: baseLabels, paths });
|
|
587
|
+
const resolution = resolveTaskCapabilities({ id: issueId, identifier: issueId, title, description, labels: baseLabels, paths });
|
|
588
|
+
const labels = [...new Set([...baseLabels, resolution.category ? `capability:${resolution.category}` : "", ...resolution.overlays.map((overlay) => `overlay:${overlay}`)].filter(Boolean))];
|
|
589
|
+
const record = await issueResource2?.insert({
|
|
590
|
+
id: issueId,
|
|
591
|
+
identifier: issueId,
|
|
592
|
+
title,
|
|
593
|
+
description,
|
|
594
|
+
priority,
|
|
595
|
+
state,
|
|
596
|
+
labels,
|
|
597
|
+
paths,
|
|
598
|
+
inferredPaths,
|
|
599
|
+
capabilityCategory: resolution.category,
|
|
600
|
+
capabilityOverlays: resolution.overlays,
|
|
601
|
+
capabilityRationale: resolution.rationale,
|
|
602
|
+
blockedBy: [],
|
|
603
|
+
assignedToWorker: false,
|
|
604
|
+
createdAt: nowIso(),
|
|
605
|
+
url: `fifony://local/${issueId}`,
|
|
606
|
+
updatedAt: nowIso(),
|
|
607
|
+
history: [`[${nowIso()}] Issue created via MCP.`],
|
|
608
|
+
attempts: 0,
|
|
609
|
+
maxAttempts: 3
|
|
610
|
+
});
|
|
611
|
+
await appendEvent("info", `Issue ${issueId} created through MCP.`, { title, state, labels, paths, inferredPaths, capabilityCategory: resolution.category }, issueId);
|
|
612
|
+
return toolText(JSON.stringify(record ?? { id: issueId }, null, 2));
|
|
613
|
+
}
|
|
614
|
+
if (name === "fifony.update_issue_state") {
|
|
615
|
+
await initDatabase();
|
|
616
|
+
const { issueResource: issueResource2 } = getResources();
|
|
617
|
+
const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
|
|
618
|
+
const state = typeof args.state === "string" ? args.state.trim() : "";
|
|
619
|
+
const note = typeof args.note === "string" ? args.note : "";
|
|
620
|
+
if (!issueId || !state) throw new Error("issueId and state are required");
|
|
621
|
+
const current = await issueResource2?.get(issueId);
|
|
622
|
+
if (!current) throw new Error(`Issue not found: ${issueId}`);
|
|
623
|
+
const updated = await issueResource2?.update(issueId, { state, updatedAt: nowIso() });
|
|
624
|
+
await appendEvent("info", note || `Issue ${issueId} moved to ${state} through MCP.`, { state }, issueId);
|
|
625
|
+
return toolText(JSON.stringify(updated ?? { id: issueId, state }, null, 2));
|
|
626
|
+
}
|
|
627
|
+
if (name === "fifony.integration_config") {
|
|
628
|
+
const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "client";
|
|
629
|
+
return toolText(JSON.stringify({ client, mcpServers: { fifony: { command: "npx", args: ["fifony", "mcp", "--workspace", WORKSPACE_ROOT, "--persistence", PERSISTENCE_ROOT] } } }, null, 2));
|
|
630
|
+
}
|
|
631
|
+
if (name === "fifony.list_integrations") return toolText(JSON.stringify(discoverIntegrations(WORKSPACE_ROOT), null, 2));
|
|
632
|
+
if (name === "fifony.integration_snippet") {
|
|
633
|
+
const integration = typeof args.integration === "string" ? args.integration : "";
|
|
634
|
+
return toolText(await buildIntegrationSnippet(integration, WORKSPACE_ROOT));
|
|
635
|
+
}
|
|
636
|
+
if (name === "fifony.resolve_capabilities") {
|
|
637
|
+
const title = typeof args.title === "string" ? args.title : "";
|
|
638
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
639
|
+
const labels = Array.isArray(args.labels) ? args.labels.filter((value) => typeof value === "string") : [];
|
|
640
|
+
const paths = Array.isArray(args.paths) ? args.paths.filter((value) => typeof value === "string") : [];
|
|
641
|
+
return toolText(JSON.stringify({ inferredPaths: inferCapabilityPaths({ title, description, labels, paths }), resolution: resolveTaskCapabilities({ title, description, labels, paths }) }, null, 2));
|
|
642
|
+
}
|
|
643
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
644
|
+
}
|
|
645
|
+
function sendMessage(message) {
|
|
646
|
+
const payload = Buffer.from(JSON.stringify(message), "utf8");
|
|
647
|
+
stdout.write(`Content-Length: ${payload.length}\r
|
|
648
|
+
\r
|
|
649
|
+
`);
|
|
650
|
+
stdout.write(payload);
|
|
651
|
+
}
|
|
652
|
+
function sendResult(id, result) {
|
|
653
|
+
sendMessage({ jsonrpc: "2.0", id, result });
|
|
654
|
+
}
|
|
655
|
+
function sendError(id, code, message, data) {
|
|
656
|
+
sendMessage({ jsonrpc: "2.0", id, error: { code, message, data } });
|
|
657
|
+
}
|
|
658
|
+
async function handleRequest(request) {
|
|
659
|
+
const id = request.id ?? null;
|
|
660
|
+
try {
|
|
661
|
+
switch (request.method) {
|
|
662
|
+
case "initialize":
|
|
663
|
+
sendResult(id, { protocolVersion: "2024-11-05", capabilities: { resources: {}, tools: {}, prompts: {} }, serverInfo: { name: "fifony", version: "0.1.0" } });
|
|
664
|
+
return;
|
|
665
|
+
case "notifications/initialized":
|
|
666
|
+
return;
|
|
667
|
+
case "ping":
|
|
668
|
+
sendResult(id, {});
|
|
669
|
+
return;
|
|
670
|
+
case "resources/list":
|
|
671
|
+
sendResult(id, { resources: await listResourcesMcp() });
|
|
672
|
+
return;
|
|
673
|
+
case "resources/read":
|
|
674
|
+
sendResult(id, { contents: await readResource(String(request.params?.uri ?? "")) });
|
|
675
|
+
return;
|
|
676
|
+
case "tools/list":
|
|
677
|
+
sendResult(id, { tools: listTools() });
|
|
678
|
+
return;
|
|
679
|
+
case "tools/call":
|
|
680
|
+
sendResult(id, await callTool(String(request.params?.name ?? ""), request.params?.arguments ?? {}));
|
|
681
|
+
return;
|
|
682
|
+
case "prompts/list":
|
|
683
|
+
sendResult(id, { prompts: listPrompts() });
|
|
684
|
+
return;
|
|
685
|
+
case "prompts/get":
|
|
686
|
+
sendResult(id, await getPrompt(String(request.params?.name ?? ""), request.params?.arguments ?? {}));
|
|
687
|
+
return;
|
|
688
|
+
default:
|
|
689
|
+
sendError(id, -32601, `Method not found: ${request.method}`);
|
|
690
|
+
}
|
|
691
|
+
} catch (error) {
|
|
692
|
+
sendError(id, -32e3, String(error));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function processIncomingBuffer() {
|
|
696
|
+
while (true) {
|
|
697
|
+
const separatorIndex = incomingBuffer.indexOf("\r\n\r\n");
|
|
698
|
+
if (separatorIndex === -1) return;
|
|
699
|
+
const headerText = incomingBuffer.subarray(0, separatorIndex).toString("utf8");
|
|
700
|
+
const contentLengthHeader = headerText.split("\r\n").find((line) => line.toLowerCase().startsWith("content-length:"));
|
|
701
|
+
if (!contentLengthHeader) {
|
|
702
|
+
incomingBuffer = Buffer.alloc(0);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const contentLength = Number.parseInt(contentLengthHeader.split(":")[1]?.trim() ?? "0", 10);
|
|
706
|
+
const messageStart = separatorIndex + 4;
|
|
707
|
+
const messageEnd = messageStart + contentLength;
|
|
708
|
+
if (incomingBuffer.length < messageEnd) return;
|
|
709
|
+
const messageBody = incomingBuffer.subarray(messageStart, messageEnd).toString("utf8");
|
|
710
|
+
incomingBuffer = incomingBuffer.subarray(messageEnd);
|
|
711
|
+
let request;
|
|
712
|
+
try {
|
|
713
|
+
request = JSON.parse(messageBody);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
sendError(null, -32700, `Invalid JSON: ${String(error)}`);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
void handleRequest(request);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function bootstrap() {
|
|
722
|
+
debugBoot2("mcp:bootstrap:start");
|
|
723
|
+
await initDatabase();
|
|
724
|
+
debugBoot2("mcp:bootstrap:database-ready");
|
|
725
|
+
await appendEvent("info", "Fifony MCP server started.", { workspaceRoot: WORKSPACE_ROOT, persistenceRoot: PERSISTENCE_ROOT });
|
|
726
|
+
stdin.on("data", (chunk) => {
|
|
727
|
+
incomingBuffer = Buffer.concat([incomingBuffer, chunk]);
|
|
728
|
+
processIncomingBuffer();
|
|
729
|
+
});
|
|
730
|
+
stdin.resume();
|
|
731
|
+
debugBoot2("mcp:bootstrap:stdin-ready");
|
|
732
|
+
}
|
|
733
|
+
bootstrap().catch((error) => {
|
|
734
|
+
sendError(null, -32001, `Failed to start Fifony MCP server: ${String(error)}`);
|
|
735
|
+
process.exit(1);
|
|
736
|
+
});
|
|
737
|
+
process.on("SIGINT", async () => {
|
|
738
|
+
const db = getDatabase();
|
|
739
|
+
if (db) await db.disconnect();
|
|
740
|
+
process.exit(0);
|
|
741
|
+
});
|
|
742
|
+
process.on("SIGTERM", async () => {
|
|
743
|
+
const db = getDatabase();
|
|
744
|
+
if (db) await db.disconnect();
|
|
745
|
+
process.exit(0);
|
|
746
|
+
});
|
|
747
|
+
//# sourceMappingURL=server.js.map
|