@musashishao/folderforge 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +181 -0
  2. package/dist/adapters/child-mcp/client.js +114 -0
  3. package/dist/adapters/child-mcp/registry.js +66 -0
  4. package/dist/audit/audit-log.js +45 -0
  5. package/dist/audit/event-types.js +1 -0
  6. package/dist/core/config.js +211 -0
  7. package/dist/core/container.js +51 -0
  8. package/dist/core/errors.js +37 -0
  9. package/dist/core/logger.js +8 -0
  10. package/dist/core/types.js +4 -0
  11. package/dist/dashboard/server.js +191 -0
  12. package/dist/lsp/protocol.js +116 -0
  13. package/dist/main.js +190 -0
  14. package/dist/managers/db-manager.js +161 -0
  15. package/dist/managers/lsp-manager.js +269 -0
  16. package/dist/managers/process-manager.js +140 -0
  17. package/dist/policy/approvals.js +143 -0
  18. package/dist/policy/command-policy.js +99 -0
  19. package/dist/policy/glob-match.js +61 -0
  20. package/dist/policy/path-policy.js +73 -0
  21. package/dist/policy/policy-engine.js +156 -0
  22. package/dist/policy/rate-limiter.js +96 -0
  23. package/dist/policy/risk.js +112 -0
  24. package/dist/policy/secret-policy.js +132 -0
  25. package/dist/server/mcp-server.js +144 -0
  26. package/dist/server/transports/http.js +133 -0
  27. package/dist/server/transports/stdio.js +14 -0
  28. package/dist/tools/adapter-tools.js +62 -0
  29. package/dist/tools/browser-tools.js +76 -0
  30. package/dist/tools/build-tools.js +78 -0
  31. package/dist/tools/code-tools.js +250 -0
  32. package/dist/tools/coverage-tools.js +135 -0
  33. package/dist/tools/db-tools.js +130 -0
  34. package/dist/tools/diff-util.js +45 -0
  35. package/dist/tools/error-parser.js +57 -0
  36. package/dist/tools/file-tools.js +319 -0
  37. package/dist/tools/format-tools.js +118 -0
  38. package/dist/tools/git-tools.js +371 -0
  39. package/dist/tools/index.js +63 -0
  40. package/dist/tools/memory-tools.js +54 -0
  41. package/dist/tools/output-schemas.js +100 -0
  42. package/dist/tools/pagination.js +92 -0
  43. package/dist/tools/pkg-tools.js +260 -0
  44. package/dist/tools/process-tools.js +128 -0
  45. package/dist/tools/registry.js +194 -0
  46. package/dist/tools/schema-lock.js +152 -0
  47. package/dist/tools/search-tools.js +176 -0
  48. package/dist/tools/security-tools.js +147 -0
  49. package/dist/tools/terminal-tools.js +57 -0
  50. package/dist/tools/workspace-tools.js +186 -0
  51. package/dist/workspace/memory-store.js +67 -0
  52. package/dist/workspace/onboarding.js +46 -0
  53. package/dist/workspace/project-detector.js +95 -0
  54. package/dist/workspace/workspace-manager.js +106 -0
  55. package/docs/adapters.md +76 -0
  56. package/docs/architecture.md +66 -0
  57. package/docs/roadmap.md +172 -0
  58. package/docs/security.md +94 -0
  59. package/docs/tools.md +129 -0
  60. package/examples/claude-desktop.json +18 -0
  61. package/examples/codex.toml +18 -0
  62. package/examples/config.basic.yaml +37 -0
  63. package/examples/config.full.yaml +120 -0
  64. package/package.json +74 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Structured error types for FolderForge.
3
+ */
4
+ export class FolderForgeError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = 'FolderForgeError';
9
+ this.code = code;
10
+ }
11
+ }
12
+ export class PolicyDeniedError extends FolderForgeError {
13
+ constructor(message) {
14
+ super('POLICY_DENIED', message);
15
+ this.name = 'PolicyDeniedError';
16
+ }
17
+ }
18
+ export class ApprovalRequiredError extends FolderForgeError {
19
+ approvalId;
20
+ constructor(message, approvalId) {
21
+ super('APPROVAL_REQUIRED', message);
22
+ this.name = 'ApprovalRequiredError';
23
+ this.approvalId = approvalId;
24
+ }
25
+ }
26
+ export class PathEscapeError extends FolderForgeError {
27
+ constructor(message) {
28
+ super('PATH_ESCAPE', message);
29
+ this.name = 'PathEscapeError';
30
+ }
31
+ }
32
+ export class WorkspaceNotActiveError extends FolderForgeError {
33
+ constructor() {
34
+ super('NO_WORKSPACE', 'No active workspace. Call workspace_activate first.');
35
+ this.name = 'WorkspaceNotActiveError';
36
+ }
37
+ }
@@ -0,0 +1,8 @@
1
+ import pino from 'pino';
2
+ /**
3
+ * Logger writes to stderr so it never corrupts the stdio MCP channel (stdout).
4
+ */
5
+ export const logger = pino({
6
+ level: process.env.FOLDERFORGE_LOG_LEVEL ?? 'info',
7
+ base: { service: 'folderforge' },
8
+ }, pino.destination(2));
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Core type definitions shared across FolderForge.
3
+ */
4
+ export {};
@@ -0,0 +1,191 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { timingSafeEqual } from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ import { logger } from '../core/logger.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /** True when the bind host is loopback-only and therefore safe without a token. */
9
+ export function isLoopbackHost(host) {
10
+ return host === '127.0.0.1' || host === '::1' || host === 'localhost';
11
+ }
12
+ /**
13
+ * Local control-plane dashboard. Read-only views plus approval actions.
14
+ *
15
+ * Endpoints:
16
+ * GET / -> static dashboard (dashboard/static/index.html)
17
+ * GET /status -> server + workspace + policy snapshot
18
+ * GET /audit -> recent audit events
19
+ * GET /processes -> managed long-running processes
20
+ * GET /approvals -> pending + resolved approval requests
21
+ * POST /approvals/:id/approve -> approve (body: { scope?: 'once'|'session' })
22
+ * POST /approvals/:id/deny -> deny
23
+ */
24
+ export function startDashboard(container, registry, opts) {
25
+ // Auth is enforced only when bound to a non-loopback address. A loopback bind
26
+ // is treated as trusted (same machine), matching the default 127.0.0.1.
27
+ const requireAuth = !isLoopbackHost(opts.host);
28
+ if (requireAuth && !opts.token) {
29
+ throw new Error('Dashboard bound to a non-loopback host requires a token');
30
+ }
31
+ const server = createServer((req, res) => {
32
+ if (requireAuth && !isAuthorized(req, opts.token)) {
33
+ res.writeHead(401, {
34
+ 'content-type': 'application/json; charset=utf-8',
35
+ 'www-authenticate': 'Bearer realm="folderforge-dashboard"',
36
+ });
37
+ res.end(JSON.stringify({ error: 'unauthorized', message: 'Valid bearer token required.' }));
38
+ return;
39
+ }
40
+ handle(req, res, container, registry).catch((err) => {
41
+ logger.error({ err: String(err) }, 'Dashboard request failed');
42
+ sendJson(res, 500, { error: 'internal_error', message: String(err) });
43
+ });
44
+ });
45
+ server.listen(opts.port, opts.host, () => {
46
+ logger.info({ host: opts.host, port: opts.port, authRequired: requireAuth }, 'Dashboard listening');
47
+ });
48
+ return server;
49
+ }
50
+ /**
51
+ * Accept the token via either `Authorization: Bearer <token>` or a `?token=`
52
+ * query parameter (handy for opening the dashboard in a browser). Comparison is
53
+ * constant-time to avoid leaking the token length/prefix via timing.
54
+ */
55
+ function isAuthorized(req, expected) {
56
+ const header = req.headers['authorization'];
57
+ let provided;
58
+ if (typeof header === 'string' && header.startsWith('Bearer ')) {
59
+ provided = header.slice('Bearer '.length).trim();
60
+ }
61
+ if (!provided) {
62
+ const url = new URL(req.url ?? '/', 'http://localhost');
63
+ provided = url.searchParams.get('token') ?? undefined;
64
+ }
65
+ if (!provided)
66
+ return false;
67
+ return timingSafeEqualStr(provided, expected);
68
+ }
69
+ function timingSafeEqualStr(a, b) {
70
+ const ab = Buffer.from(a);
71
+ const bb = Buffer.from(b);
72
+ if (ab.length !== bb.length)
73
+ return false;
74
+ return timingSafeEqual(ab, bb);
75
+ }
76
+ async function handle(req, res, container, registry) {
77
+ const method = req.method ?? 'GET';
78
+ const url = new URL(req.url ?? '/', 'http://localhost');
79
+ const path = url.pathname;
80
+ if (method === 'GET' && (path === '/' || path === '/index.html')) {
81
+ return sendStatic(res);
82
+ }
83
+ if (method === 'GET' && path === '/status') {
84
+ const active = container.workspace.getActive();
85
+ return sendJson(res, 200, {
86
+ server: {
87
+ name: container.config.server.name,
88
+ transport: container.config.server.transport,
89
+ },
90
+ workspace: {
91
+ active: Boolean(active),
92
+ projectRoot: container.projectRoot(),
93
+ name: active?.name ?? null,
94
+ languageHints: active?.languageHints ?? [],
95
+ allowedDirectories: container.config.workspace.allowedDirectories,
96
+ },
97
+ policy: container.policy.describe(),
98
+ tools: {
99
+ active: registry.listActive().length,
100
+ total: registry.listAll().length,
101
+ },
102
+ });
103
+ }
104
+ if (method === 'GET' && path === '/audit') {
105
+ const limit = clampInt(url.searchParams.get('limit'), 50, 1, 500);
106
+ return sendJson(res, 200, { entries: container.audit.recent(limit) });
107
+ }
108
+ if (method === 'GET' && path === '/processes') {
109
+ return sendJson(res, 200, { processes: container.processes.list() });
110
+ }
111
+ if (method === 'GET' && path === '/approvals') {
112
+ return sendJson(res, 200, {
113
+ pending: container.policy.approvals.pending(),
114
+ all: container.policy.approvals.all(),
115
+ });
116
+ }
117
+ // POST /approvals/:id/approve | /approvals/:id/deny
118
+ const approvalMatch = /^\/approvals\/([^/]+)\/(approve|deny)$/.exec(path);
119
+ if (method === 'POST' && approvalMatch) {
120
+ const id = approvalMatch[1];
121
+ const action = approvalMatch[2];
122
+ const body = await readJsonBody(req);
123
+ let result;
124
+ if (action === 'approve') {
125
+ const scope = body?.scope === 'session' ? 'session' : 'once';
126
+ result = container.policy.approvals.approve(id, scope);
127
+ }
128
+ else {
129
+ result = container.policy.approvals.deny(id);
130
+ }
131
+ if (!result) {
132
+ return sendJson(res, 404, { error: 'approval_not_found', id });
133
+ }
134
+ container.audit.record({
135
+ type: 'approval_resolved',
136
+ tool: result.tool,
137
+ risk: result.risk,
138
+ summary: `${action} (${result.state})`,
139
+ detail: { approvalId: id },
140
+ });
141
+ return sendJson(res, 200, { approval: result });
142
+ }
143
+ sendJson(res, 404, { error: 'not_found', path });
144
+ }
145
+ function sendStatic(res) {
146
+ const candidates = [
147
+ join(__dirname, 'static', 'index.html'),
148
+ join(process.cwd(), 'src', 'dashboard', 'static', 'index.html'),
149
+ ];
150
+ for (const file of candidates) {
151
+ if (existsSync(file)) {
152
+ const html = readFileSync(file, 'utf8');
153
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
154
+ res.end(html);
155
+ return;
156
+ }
157
+ }
158
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
159
+ res.end('Dashboard static asset not found');
160
+ }
161
+ function sendJson(res, status, body) {
162
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
163
+ res.end(JSON.stringify(body, null, 2));
164
+ }
165
+ async function readJsonBody(req) {
166
+ return new Promise((resolveBody) => {
167
+ let raw = '';
168
+ req.on('data', (chunk) => {
169
+ raw += chunk;
170
+ if (raw.length > 1_000_000)
171
+ req.destroy();
172
+ });
173
+ req.on('end', () => {
174
+ if (!raw.trim())
175
+ return resolveBody(null);
176
+ try {
177
+ resolveBody(JSON.parse(raw));
178
+ }
179
+ catch {
180
+ resolveBody(null);
181
+ }
182
+ });
183
+ req.on('error', () => resolveBody(null));
184
+ });
185
+ }
186
+ function clampInt(value, fallback, min, max) {
187
+ const n = value ? Number(value) : NaN;
188
+ if (!Number.isFinite(n))
189
+ return fallback;
190
+ return Math.min(max, Math.max(min, Math.floor(n)));
191
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Minimal Language Server Protocol (LSP) transport primitives.
3
+ *
4
+ * LSP runs JSON-RPC 2.0 over a stream framed with HTTP-style headers:
5
+ *
6
+ * Content-Length: <bytes>\r\n
7
+ * \r\n
8
+ * <utf8 json body>
9
+ *
10
+ * This module is intentionally transport-pure: it knows how to *encode* an
11
+ * outgoing message and how to *incrementally decode* a byte stream into
12
+ * complete messages. It spawns nothing and touches no filesystem, which keeps
13
+ * the framing logic unit-testable without a real language server (the spawn /
14
+ * lifecycle layer lives in `managers/lsp-manager.ts`).
15
+ */
16
+ /** Encode a JSON-RPC message into a framed LSP buffer (headers + body). */
17
+ export function encodeMessage(message) {
18
+ const body = Buffer.from(JSON.stringify(message), 'utf8');
19
+ const header = `Content-Length: ${body.length}\r\n\r\n`;
20
+ return Buffer.concat([Buffer.from(header, 'ascii'), body]);
21
+ }
22
+ /**
23
+ * Incremental decoder for the LSP framing. Feed it raw chunks as they arrive on
24
+ * the language server's stdout; it buffers partial frames and emits each
25
+ * complete JSON-RPC message via the `onMessage` callback. A single chunk may
26
+ * contain zero, one, or many messages, and a message may be split across
27
+ * chunks; both cases are handled.
28
+ */
29
+ export class MessageBuffer {
30
+ onMessage;
31
+ buffer = Buffer.alloc(0);
32
+ constructor(onMessage) {
33
+ this.onMessage = onMessage;
34
+ }
35
+ append(chunk) {
36
+ this.buffer = this.buffer.length
37
+ ? Buffer.concat([this.buffer, chunk])
38
+ : Buffer.from(chunk);
39
+ this.drain();
40
+ }
41
+ drain() {
42
+ // Loop because one append may complete several queued frames.
43
+ for (;;) {
44
+ const headerEnd = this.buffer.indexOf('\r\n\r\n');
45
+ if (headerEnd === -1)
46
+ return; // headers not fully received yet
47
+ const header = this.buffer.subarray(0, headerEnd).toString('ascii');
48
+ const length = parseContentLength(header);
49
+ if (length === null) {
50
+ // Malformed header block: drop it and resync past the separator so a
51
+ // single bad frame can't wedge the stream forever.
52
+ this.buffer = this.buffer.subarray(headerEnd + 4);
53
+ continue;
54
+ }
55
+ const bodyStart = headerEnd + 4;
56
+ const bodyEnd = bodyStart + length;
57
+ if (this.buffer.length < bodyEnd)
58
+ return; // body not fully received yet
59
+ const body = this.buffer.subarray(bodyStart, bodyEnd).toString('utf8');
60
+ this.buffer = this.buffer.subarray(bodyEnd);
61
+ let parsed = null;
62
+ try {
63
+ parsed = JSON.parse(body);
64
+ }
65
+ catch {
66
+ parsed = null; // skip a non-JSON body but keep draining
67
+ }
68
+ if (parsed)
69
+ this.onMessage(parsed);
70
+ }
71
+ }
72
+ }
73
+ /** Parse the `Content-Length` value out of an LSP header block. */
74
+ export function parseContentLength(header) {
75
+ for (const line of header.split(/\r\n/)) {
76
+ const m = /^Content-Length:\s*(\d+)\s*$/i.exec(line);
77
+ if (m) {
78
+ const n = Number(m[1]);
79
+ return Number.isFinite(n) && n >= 0 ? n : null;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ /** Type guard: a message is a response when it carries an `id` and no `method`. */
85
+ export function isResponse(msg) {
86
+ return 'id' in msg && !('method' in msg);
87
+ }
88
+ /**
89
+ * LSP SymbolKind enum (subset we surface). Maps numeric kinds returned by
90
+ * documentSymbol / workspaceSymbol to human-readable names.
91
+ */
92
+ export const SYMBOL_KIND = {
93
+ 1: 'file',
94
+ 2: 'module',
95
+ 3: 'namespace',
96
+ 4: 'package',
97
+ 5: 'class',
98
+ 6: 'method',
99
+ 7: 'property',
100
+ 8: 'field',
101
+ 9: 'constructor',
102
+ 10: 'enum',
103
+ 11: 'interface',
104
+ 12: 'function',
105
+ 13: 'variable',
106
+ 14: 'constant',
107
+ 23: 'struct',
108
+ };
109
+ /** LSP DiagnosticSeverity -> our severity vocabulary. */
110
+ export function lspSeverity(n) {
111
+ if (n === 1)
112
+ return 'error';
113
+ if (n === 2)
114
+ return 'warning';
115
+ return 'info';
116
+ }
package/dist/main.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from './core/config.js';
3
+ import { Container } from './core/container.js';
4
+ import { buildRegistry, registerAdapterTools } from './tools/index.js';
5
+ import { createMcpServer } from './server/mcp-server.js';
6
+ import { startStdioTransport } from './server/transports/stdio.js';
7
+ import { startHttpTransport } from './server/transports/http.js';
8
+ import { startDashboard, isLoopbackHost } from './dashboard/server.js';
9
+ import { logger } from './core/logger.js';
10
+ import { randomBytes } from 'node:crypto';
11
+ const VERSION = '1.0.0';
12
+ function parseArgs(argv) {
13
+ const args = { stdio: false, http: false, dashboard: true };
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i];
16
+ const next = () => argv[++i];
17
+ switch (a) {
18
+ case '--project':
19
+ case '-p': {
20
+ const v = next();
21
+ if (v !== undefined)
22
+ args.project = v;
23
+ break;
24
+ }
25
+ case '--config':
26
+ case '-c': {
27
+ const v = next();
28
+ if (v !== undefined)
29
+ args.config = v;
30
+ break;
31
+ }
32
+ case '--stdio':
33
+ args.stdio = true;
34
+ break;
35
+ case '--http':
36
+ args.http = true;
37
+ break;
38
+ case '--port':
39
+ args.port = Number(next());
40
+ break;
41
+ case '--host': {
42
+ const v = next();
43
+ if (v !== undefined)
44
+ args.host = v;
45
+ break;
46
+ }
47
+ case '--dashboard-port':
48
+ args.dashboardPort = Number(next());
49
+ break;
50
+ case '--no-dashboard':
51
+ args.dashboard = false;
52
+ break;
53
+ case '--version':
54
+ case '-v':
55
+ process.stdout.write(`folderforge ${VERSION}\n`);
56
+ process.exit(0);
57
+ break;
58
+ case '--help':
59
+ case '-h':
60
+ printHelp();
61
+ process.exit(0);
62
+ break;
63
+ default:
64
+ if (a && a.startsWith('-')) {
65
+ logger.warn({ arg: a }, 'Unknown argument ignored');
66
+ }
67
+ }
68
+ }
69
+ return args;
70
+ }
71
+ function printHelp() {
72
+ process.stdout.write([
73
+ 'FolderForge - local development control plane for AI coding agents',
74
+ '',
75
+ 'Usage: folderforge [options]',
76
+ '',
77
+ 'Options:',
78
+ ' -p, --project <dir> Project root to activate (default: cwd)',
79
+ ' -c, --config <file> Path to a YAML config file',
80
+ ' --stdio Serve MCP over stdio (default for agent clients)',
81
+ ' --http Serve MCP over Streamable HTTP',
82
+ ' --port <n> HTTP MCP port (default 7331)',
83
+ ' --host <addr> Bind address (default 127.0.0.1)',
84
+ ' --dashboard-port <n> Dashboard port (default 7332)',
85
+ ' --no-dashboard Disable the local dashboard',
86
+ ' -v, --version Print version and exit',
87
+ ' -h, --help Show this help',
88
+ '',
89
+ ].join('\n'));
90
+ }
91
+ async function main() {
92
+ const args = parseArgs(process.argv.slice(2));
93
+ const config = loadConfig({
94
+ ...(args.config !== undefined ? { configPath: args.config } : {}),
95
+ ...(args.project !== undefined ? { projectRoot: args.project } : {}),
96
+ });
97
+ // CLI overrides for transport/ports.
98
+ if (args.http)
99
+ config.server.transport = 'http';
100
+ if (args.stdio)
101
+ config.server.transport = 'stdio';
102
+ if (args.host !== undefined)
103
+ config.server.http.host = args.host;
104
+ if (args.port !== undefined)
105
+ config.server.http.port = args.port;
106
+ if (args.dashboardPort !== undefined)
107
+ config.server.dashboard.port = args.dashboardPort;
108
+ const container = new Container(config);
109
+ const registry = buildRegistry(container);
110
+ // Wire enabled child MCP adapters (Serena, Playwright, ...). Each child tool is
111
+ // exposed namespaced (e.g. serena__find_symbol). Discovery never blocks startup.
112
+ try {
113
+ const added = await registerAdapterTools(container, registry);
114
+ if (added > 0)
115
+ logger.info({ added }, 'Registered child MCP adapter tools');
116
+ }
117
+ catch (err) {
118
+ logger.warn({ err: String(err) }, 'Adapter tool registration failed; continuing without adapters');
119
+ }
120
+ const server = createMcpServer(registry, {
121
+ name: config.server.name,
122
+ version: VERSION,
123
+ roots: config.workspace.allowedDirectories,
124
+ });
125
+ container.audit.record({
126
+ type: 'server_start',
127
+ summary: `transport=${config.server.transport} tools=${registry.listAll().length}`,
128
+ detail: { version: VERSION, projectRoot: container.projectRoot() },
129
+ });
130
+ // Dashboard is independent of the MCP transport (and is safe to run alongside stdio).
131
+ if (args.dashboard) {
132
+ const dashHost = config.server.dashboard.host;
133
+ // A non-loopback bind exposes the control plane to the network, so it must be
134
+ // token-protected. Use the configured token or mint one and log it once.
135
+ let token = config.server.dashboard.token;
136
+ if (!isLoopbackHost(dashHost) && !token) {
137
+ token = randomBytes(24).toString('base64url');
138
+ logger.warn({ host: dashHost, token }, 'Dashboard bound to a non-loopback host; generated an auth token (set server.dashboard.token to pin it)');
139
+ }
140
+ startDashboard(container, registry, {
141
+ host: dashHost,
142
+ port: config.server.dashboard.port,
143
+ ...(token ? { token } : {}),
144
+ });
145
+ }
146
+ if (config.server.transport === 'http') {
147
+ const httpHost = config.server.http.host;
148
+ // Mirror the dashboard: a non-loopback bind must be token-protected. Use the
149
+ // configured token or mint one and log it once.
150
+ let httpToken = config.server.http.token;
151
+ if (!isLoopbackHost(httpHost) && !httpToken) {
152
+ httpToken = randomBytes(24).toString('base64url');
153
+ logger.warn({ host: httpHost, token: httpToken }, 'HTTP transport bound to a non-loopback host; generated an auth token (set server.http.token to pin it)');
154
+ }
155
+ await startHttpTransport(server, {
156
+ host: httpHost,
157
+ port: config.server.http.port,
158
+ ...(httpToken ? { token: httpToken } : {}),
159
+ ...(config.server.http.corsOrigins ? { corsOrigins: config.server.http.corsOrigins } : {}),
160
+ ...(config.server.http.sessionTtlMs !== undefined
161
+ ? { sessionTtlMs: config.server.http.sessionTtlMs }
162
+ : {}),
163
+ });
164
+ }
165
+ else {
166
+ await startStdioTransport(server);
167
+ }
168
+ const shutdown = async (signal) => {
169
+ logger.info({ signal }, 'Shutting down FolderForge');
170
+ try {
171
+ container.adapters.stopAll();
172
+ }
173
+ catch {
174
+ // ignore
175
+ }
176
+ try {
177
+ await server.close();
178
+ }
179
+ catch {
180
+ // ignore
181
+ }
182
+ process.exit(0);
183
+ };
184
+ process.on('SIGINT', () => void shutdown('SIGINT'));
185
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
186
+ }
187
+ main().catch((err) => {
188
+ logger.error({ err: err instanceof Error ? err.stack : String(err) }, 'Fatal startup error');
189
+ process.exit(1);
190
+ });