@sanurb/ringi 0.1.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.
package/dist/mcp.js ADDED
@@ -0,0 +1,1228 @@
1
+ import { ReviewSourceType, ReviewId, TodoId, CoreLive, TodoService, ReviewService, CommentService, ExportService, parseDiff, getDiffSummary, GitService } from './chunk-3JLVANJR.js';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { execFileSync } from 'node:child_process';
4
+ import * as Context from 'effect/Context';
5
+ import * as Layer2 from 'effect/Layer';
6
+ import * as vm from 'node:vm';
7
+ import * as Duration from 'effect/Duration';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as ParseResult from 'effect/ParseResult';
10
+ import * as Schema2 from 'effect/Schema';
11
+ import * as ConfigProvider from 'effect/ConfigProvider';
12
+ import * as ManagedRuntime from 'effect/ManagedRuntime';
13
+
14
+ var DEFAULT_DB_PATH = ".ringi/reviews.db";
15
+ var DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024;
16
+ var DEFAULT_TIMEOUT_MS = 3e4;
17
+ var MAX_TIMEOUT_MS = 12e4;
18
+ var McpConfig = class extends Context.Tag("McpConfig")() {
19
+ };
20
+ var McpConfigLive = (config) => Layer2.succeed(McpConfig, config);
21
+ var resolveRepositoryRoot = (repoOverride) => {
22
+ const cwd = repoOverride ?? process.cwd();
23
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
24
+ cwd,
25
+ encoding: "utf8"
26
+ }).trim();
27
+ };
28
+ var resolveDbPath = (repoRoot, dbPathOverride) => dbPathOverride ?? `${repoRoot}/${DEFAULT_DB_PATH}`;
29
+ var parseNumberFlag = (flagValue, fallback, name) => {
30
+ if (flagValue === void 0) {
31
+ return fallback;
32
+ }
33
+ const parsed = Number.parseInt(flagValue, 10);
34
+ if (!Number.isFinite(parsed) || parsed <= 0) {
35
+ throw new Error(
36
+ `Invalid ${name}: expected a positive integer, received ${flagValue}`
37
+ );
38
+ }
39
+ return parsed;
40
+ };
41
+ var resolveMcpConfig = (argv) => {
42
+ const args = [...argv];
43
+ const readonly = args.includes("--readonly");
44
+ const dbIndex = args.indexOf("--db-path");
45
+ const repoIndex = args.indexOf("--repo");
46
+ const timeoutIndex = args.indexOf("--timeout-ms");
47
+ const maxOutputIndex = args.indexOf("--max-output-bytes");
48
+ const repoRoot = resolveRepositoryRoot(
49
+ repoIndex === -1 ? void 0 : args[repoIndex + 1]
50
+ );
51
+ return {
52
+ cwd: process.cwd(),
53
+ dbPath: resolveDbPath(
54
+ repoRoot,
55
+ dbIndex === -1 ? void 0 : args[dbIndex + 1]
56
+ ),
57
+ defaultTimeoutMs: parseNumberFlag(
58
+ timeoutIndex === -1 ? void 0 : args[timeoutIndex + 1],
59
+ DEFAULT_TIMEOUT_MS,
60
+ "timeout"
61
+ ),
62
+ maxOutputBytes: parseNumberFlag(
63
+ maxOutputIndex === -1 ? void 0 : args[maxOutputIndex + 1],
64
+ DEFAULT_MAX_OUTPUT_BYTES,
65
+ "max output bytes"
66
+ ),
67
+ maxTimeoutMs: MAX_TIMEOUT_MS,
68
+ readonly,
69
+ repoRoot
70
+ };
71
+ };
72
+ var InvalidCodeError = class extends Schema2.TaggedError()(
73
+ "InvalidCodeError",
74
+ { message: Schema2.String }
75
+ ) {
76
+ };
77
+ var InvalidTimeoutError = class extends Schema2.TaggedError()(
78
+ "InvalidTimeoutError",
79
+ { message: Schema2.String, received: Schema2.Unknown }
80
+ ) {
81
+ };
82
+ (class extends Schema2.TaggedError()(
83
+ "InputDecodeError",
84
+ { message: Schema2.String, operation: Schema2.String }
85
+ ) {
86
+ });
87
+ (class extends Schema2.TaggedError()(
88
+ "ReadonlyViolationError",
89
+ { message: Schema2.String }
90
+ ) {
91
+ });
92
+ var ExecutionTimeoutError = class extends Schema2.TaggedError()(
93
+ "ExecutionTimeoutError",
94
+ { message: Schema2.String, timeoutMs: Schema2.Number }
95
+ ) {
96
+ };
97
+ var SandboxExecutionError = class extends Schema2.TaggedError()(
98
+ "SandboxExecutionError",
99
+ { error: Schema2.Defect, message: Schema2.String }
100
+ ) {
101
+ };
102
+
103
+ // src/mcp/namespaces.ts
104
+ var PHASE_UNAVAILABLE_MESSAGE = "This capability is not available in the current server phase. Intelligence features require Phase 2.";
105
+ var createIntelligenceNamespace = () => Object.freeze({
106
+ getConfidence: (_reviewId) => Promise.reject(new Error(PHASE_UNAVAILABLE_MESSAGE)),
107
+ getImpacts: (_reviewId) => Promise.reject(new Error(PHASE_UNAVAILABLE_MESSAGE)),
108
+ getRelationships: (_reviewId) => Promise.reject(new Error(PHASE_UNAVAILABLE_MESSAGE)),
109
+ validate: (_options) => Promise.reject(new Error(PHASE_UNAVAILABLE_MESSAGE))
110
+ });
111
+ var createSessionNamespace = (deps) => Object.freeze({
112
+ context: async () => {
113
+ const repo = await deps.getRepositoryInfo();
114
+ const activeReviewId = await deps.getLatestReviewId();
115
+ return {
116
+ activeReviewId,
117
+ activeSnapshotId: null,
118
+ readonly: deps.readonly,
119
+ repository: repo,
120
+ serverMode: "stdio"
121
+ };
122
+ },
123
+ status: async () => ({
124
+ activeSubscriptions: 0,
125
+ currentPhase: "phase1",
126
+ ok: true,
127
+ readonly: deps.readonly
128
+ })
129
+ });
130
+ var buildPreview = (diffText, source) => {
131
+ const files = parseDiff(diffText);
132
+ const summary = getDiffSummary(files);
133
+ return {
134
+ files: files.map((f) => ({
135
+ additions: f.additions,
136
+ deletions: f.deletions,
137
+ path: f.newPath,
138
+ status: f.status
139
+ })),
140
+ source,
141
+ summary: {
142
+ totalAdditions: summary.totalAdditions,
143
+ totalDeletions: summary.totalDeletions,
144
+ totalFiles: summary.totalFiles
145
+ }
146
+ };
147
+ };
148
+ var createSourcesNamespace = (deps) => Object.freeze({
149
+ list: async () => {
150
+ const [stagedFiles, branches, commits] = await Promise.all([
151
+ deps.getStagedFiles(),
152
+ deps.getBranches(),
153
+ deps.getRecentCommits()
154
+ ]);
155
+ return {
156
+ branches: branches.map((b) => ({ current: b.current, name: b.name })),
157
+ recentCommits: commits.map((c) => ({
158
+ author: c.author,
159
+ date: c.date,
160
+ hash: c.hash,
161
+ message: c.message
162
+ })),
163
+ staged: { available: stagedFiles.length > 0 }
164
+ };
165
+ },
166
+ previewDiff: async (source) => {
167
+ let diffText;
168
+ switch (source.type) {
169
+ case "staged": {
170
+ diffText = await deps.getStagedDiff();
171
+ break;
172
+ }
173
+ case "branch": {
174
+ diffText = await deps.getBranchDiff(source.baseRef);
175
+ break;
176
+ }
177
+ case "commits": {
178
+ diffText = await deps.getCommitDiff(source.commits);
179
+ break;
180
+ }
181
+ default: {
182
+ throw new Error(
183
+ `Unsupported source type: ${source.type}`
184
+ );
185
+ }
186
+ }
187
+ return buildPreview(diffText, source);
188
+ }
189
+ });
190
+ var createEventsNamespace = () => {
191
+ let subscriptionCounter = 0;
192
+ return Object.freeze({
193
+ listRecent: async (_filter) => [],
194
+ subscribe: async (filter) => {
195
+ subscriptionCounter += 1;
196
+ return {
197
+ eventTypes: filter?.eventTypes ?? [
198
+ "reviews.updated",
199
+ "comments.updated",
200
+ "todos.updated",
201
+ "files.changed"
202
+ ],
203
+ id: `sub_${subscriptionCounter}`,
204
+ reviewId: filter?.reviewId
205
+ };
206
+ }
207
+ });
208
+ };
209
+
210
+ // src/mcp/sandbox.ts
211
+ var parseId = (value, fieldName) => {
212
+ if (typeof value !== "string" || value.length === 0) {
213
+ throw new Error(`Invalid ${fieldName}: expected a non-empty string`);
214
+ }
215
+ return value;
216
+ };
217
+ var createSandboxGlobals = (deps) => {
218
+ const reviews = Object.freeze({
219
+ create: async (_input) => {
220
+ deps.requireWritable();
221
+ return deps.call("reviews.create", async () => {
222
+ throw new Error("reviews.create: not wired to runtime");
223
+ });
224
+ },
225
+ export: (_options) => deps.call("reviews.export", async () => {
226
+ throw new Error("reviews.export: not wired to runtime");
227
+ }),
228
+ get: (reviewIdValue) => {
229
+ parseId(reviewIdValue, "reviewId");
230
+ return deps.call("reviews.get", async () => {
231
+ throw new Error("reviews.get: not wired to runtime");
232
+ });
233
+ },
234
+ getComments: (reviewIdValue, _filePath) => {
235
+ parseId(reviewIdValue, "reviewId");
236
+ return deps.call("reviews.getComments", async () => {
237
+ throw new Error("reviews.getComments: not wired to runtime");
238
+ });
239
+ },
240
+ getDiff: (_query) => deps.call("reviews.getDiff", async () => {
241
+ throw new Error("reviews.getDiff: not wired to runtime");
242
+ }),
243
+ getFiles: (reviewIdValue) => {
244
+ parseId(reviewIdValue, "reviewId");
245
+ return deps.call("reviews.getFiles", async () => {
246
+ throw new Error("reviews.getFiles: not wired to runtime");
247
+ });
248
+ },
249
+ getStatus: (reviewIdValue) => {
250
+ parseId(reviewIdValue, "reviewId");
251
+ return deps.call("reviews.getStatus", async () => {
252
+ throw new Error("reviews.getStatus: not wired to runtime");
253
+ });
254
+ },
255
+ getSuggestions: (reviewIdValue) => {
256
+ parseId(reviewIdValue, "reviewId");
257
+ return deps.call("reviews.getSuggestions", async () => {
258
+ throw new Error("reviews.getSuggestions: not wired to runtime");
259
+ });
260
+ },
261
+ list: (_filters) => deps.call("reviews.list", async () => {
262
+ throw new Error("reviews.list: not wired to runtime");
263
+ })
264
+ });
265
+ const todos = Object.freeze({
266
+ add: async (_input) => {
267
+ deps.requireWritable();
268
+ return deps.call("todos.add", async () => {
269
+ throw new Error("todos.add: not wired to runtime");
270
+ });
271
+ },
272
+ clear: async (_reviewIdValue) => {
273
+ deps.requireWritable();
274
+ return deps.call("todos.clear", async () => {
275
+ throw new Error("todos.clear: not wired to runtime");
276
+ });
277
+ },
278
+ done: async (todoIdValue) => {
279
+ deps.requireWritable();
280
+ parseId(todoIdValue, "todoId");
281
+ return deps.call("todos.done", async () => {
282
+ throw new Error("todos.done: not wired to runtime");
283
+ });
284
+ },
285
+ list: (_filter) => deps.call("todos.list", async () => {
286
+ throw new Error("todos.list: not wired to runtime");
287
+ }),
288
+ move: async (todoIdValue, _positionValue) => {
289
+ deps.requireWritable();
290
+ parseId(todoIdValue, "todoId");
291
+ return deps.call("todos.move", async () => {
292
+ throw new Error("todos.move: not wired to runtime");
293
+ });
294
+ },
295
+ remove: async (todoIdValue) => {
296
+ deps.requireWritable();
297
+ parseId(todoIdValue, "todoId");
298
+ return deps.call("todos.remove", async () => {
299
+ throw new Error("todos.remove: not wired to runtime");
300
+ });
301
+ },
302
+ undone: async (todoIdValue) => {
303
+ deps.requireWritable();
304
+ parseId(todoIdValue, "todoId");
305
+ return deps.call("todos.undone", async () => {
306
+ throw new Error("todos.undone: not wired to runtime");
307
+ });
308
+ }
309
+ });
310
+ const sources = createSourcesNamespace({
311
+ getBranchDiff: deps.getBranchDiff,
312
+ getBranches: deps.getBranches,
313
+ getCommitDiff: deps.getCommitDiff,
314
+ getRecentCommits: deps.getRecentCommits,
315
+ getRepositoryInfo: deps.getRepositoryInfo,
316
+ getStagedDiff: deps.getStagedDiff,
317
+ getStagedFiles: deps.getStagedFiles
318
+ });
319
+ const intelligence = createIntelligenceNamespace();
320
+ const events = createEventsNamespace();
321
+ const session = createSessionNamespace({
322
+ getLatestReviewId: deps.getLatestReviewId,
323
+ getRepositoryInfo: deps.getRepositoryInfo,
324
+ readonly: deps.readonly
325
+ });
326
+ return { events, intelligence, reviews, session, sources, todos };
327
+ };
328
+ var NonEmptyString = Schema2.String.pipe(Schema2.minLength(1));
329
+ var ReviewCreateFromSpec = Schema2.Struct({
330
+ source: Schema2.Struct({
331
+ baseRef: Schema2.optionalWith(Schema2.NullOr(Schema2.String), {
332
+ default: () => null
333
+ }),
334
+ type: Schema2.optionalWith(ReviewSourceType, {
335
+ default: () => "staged"
336
+ })
337
+ })
338
+ });
339
+ var ReviewCreateFromLegacy = Schema2.Struct({
340
+ sourceRef: Schema2.optionalWith(Schema2.NullOr(Schema2.String), {
341
+ default: () => null
342
+ }),
343
+ sourceType: Schema2.optionalWith(ReviewSourceType, {
344
+ default: () => "staged"
345
+ })
346
+ });
347
+ var decodeReviewCreateInput = (input) => {
348
+ if (typeof input === "object" && input !== null && "source" in input) {
349
+ const parsed2 = Schema2.decodeUnknownSync(ReviewCreateFromSpec)(input);
350
+ return {
351
+ sourceRef: parsed2.source.baseRef,
352
+ sourceType: parsed2.source.type
353
+ };
354
+ }
355
+ const parsed = Schema2.decodeUnknownSync(ReviewCreateFromLegacy)(input);
356
+ return {
357
+ sourceRef: parsed.sourceRef,
358
+ sourceType: parsed.sourceType
359
+ };
360
+ };
361
+ var ReviewExportInput = Schema2.Struct({
362
+ reviewId: ReviewId
363
+ });
364
+ var ReviewDiffQuery = Schema2.Struct({
365
+ filePath: NonEmptyString,
366
+ reviewId: ReviewId
367
+ });
368
+ var ReviewListFilters = Schema2.Struct({
369
+ limit: Schema2.optionalWith(Schema2.Number, { default: () => 20 }),
370
+ page: Schema2.optionalWith(Schema2.Number, { default: () => 1 }),
371
+ pageSize: Schema2.optionalWith(Schema2.Number, { default: () => 20 }),
372
+ sourceType: Schema2.optional(Schema2.String),
373
+ status: Schema2.optional(Schema2.String)
374
+ });
375
+ var TodoInputFromSpec = Schema2.Struct({
376
+ reviewId: Schema2.optionalWith(Schema2.NullOr(ReviewId), {
377
+ default: () => null
378
+ }),
379
+ text: NonEmptyString
380
+ });
381
+ var TodoInputFromLegacy = Schema2.Struct({
382
+ content: NonEmptyString,
383
+ reviewId: Schema2.optionalWith(Schema2.NullOr(ReviewId), {
384
+ default: () => null
385
+ })
386
+ });
387
+ var decodeCreateTodoInput = (input) => {
388
+ if (typeof input === "object" && input !== null && "text" in input) {
389
+ const parsed2 = Schema2.decodeUnknownSync(TodoInputFromSpec)(input);
390
+ return { content: parsed2.text, reviewId: parsed2.reviewId };
391
+ }
392
+ const parsed = Schema2.decodeUnknownSync(TodoInputFromLegacy)(input);
393
+ return { content: parsed.content, reviewId: parsed.reviewId };
394
+ };
395
+ var TodoListFilter = Schema2.Struct({
396
+ reviewId: Schema2.optional(ReviewId)
397
+ });
398
+ var TodoMoveInput = Schema2.Struct({
399
+ position: Schema2.Number,
400
+ todoId: TodoId
401
+ });
402
+
403
+ // src/mcp/execute.ts
404
+ var MAX_CODE_LENGTH = 5e4;
405
+ var MIN_PREVIEW_BYTES = 256;
406
+ var clampTimeout = (requestedTimeout, config) => {
407
+ if (requestedTimeout === void 0) {
408
+ return config.defaultTimeoutMs;
409
+ }
410
+ if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) {
411
+ throw new Error(
412
+ `Invalid timeout: expected a positive integer, received ${requestedTimeout}`
413
+ );
414
+ }
415
+ return Math.min(Math.trunc(requestedTimeout), config.maxTimeoutMs);
416
+ };
417
+ var formatError = (error) => error instanceof Error ? error.message : String(error);
418
+ var truncateUtf8 = (text, maxBytes) => {
419
+ const buffer = Buffer.from(text, "utf8");
420
+ if (buffer.byteLength <= maxBytes) {
421
+ return text;
422
+ }
423
+ return buffer.subarray(0, maxBytes).toString("utf8");
424
+ };
425
+ var summarizeForJournal = (value) => {
426
+ if (typeof value === "string") {
427
+ return value.length > 200 ? `${value.slice(0, 200)}\u2026` : value;
428
+ }
429
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) {
430
+ return value;
431
+ }
432
+ if (Array.isArray(value)) {
433
+ return { kind: "array", length: value.length };
434
+ }
435
+ if (typeof value === "object") {
436
+ return {
437
+ keys: Object.keys(value).slice(0, 10),
438
+ kind: "object"
439
+ };
440
+ }
441
+ return typeof value;
442
+ };
443
+ var finalizeOutput = (output, maxOutputBytes) => {
444
+ const serialized = JSON.stringify(output);
445
+ if (Buffer.byteLength(serialized, "utf8") <= maxOutputBytes) {
446
+ return output;
447
+ }
448
+ const previewBudget = Math.max(
449
+ MIN_PREVIEW_BYTES,
450
+ maxOutputBytes - Math.min(1024, Math.floor(maxOutputBytes / 4))
451
+ );
452
+ return {
453
+ ...output,
454
+ result: {
455
+ note: "Result truncated to fit MCP output budget",
456
+ preview: truncateUtf8(JSON.stringify(output.result), previewBudget)
457
+ },
458
+ truncated: true
459
+ };
460
+ };
461
+ var ensureCode = (code) => {
462
+ if (typeof code !== "string") {
463
+ throw new TypeError("Invalid code: expected a string");
464
+ }
465
+ const trimmed = code.trim();
466
+ if (trimmed.length === 0) {
467
+ throw new Error("Invalid code: expected a non-empty string");
468
+ }
469
+ if (trimmed.length > MAX_CODE_LENGTH) {
470
+ throw new Error(
471
+ `Invalid code: maximum length is ${MAX_CODE_LENGTH} characters`
472
+ );
473
+ }
474
+ return trimmed;
475
+ };
476
+ var decodeInputSync = (schema, input, operation) => {
477
+ try {
478
+ return Schema2.decodeUnknownSync(schema)(input);
479
+ } catch (error) {
480
+ if (error instanceof ParseResult.ParseError) {
481
+ throw new TypeError(
482
+ `${operation}: ${ParseResult.TreeFormatter.formatErrorSync(error)}`,
483
+ { cause: error }
484
+ );
485
+ }
486
+ throw error;
487
+ }
488
+ };
489
+ var validateCode = (code) => Effect.try({
490
+ catch: (e) => new InvalidCodeError({ message: formatError(e) }),
491
+ try: () => ensureCode(code)
492
+ });
493
+ var validateTimeout = (requested, config) => Effect.try({
494
+ catch: () => new InvalidTimeoutError({
495
+ message: `Invalid timeout: expected a positive integer, received ${requested}`,
496
+ received: requested
497
+ }),
498
+ try: () => clampTimeout(requested, config)
499
+ });
500
+ var writeSandboxLog = (level, args) => {
501
+ const line = args.map((arg) => {
502
+ if (typeof arg === "string") {
503
+ return arg;
504
+ }
505
+ try {
506
+ return JSON.stringify(arg);
507
+ } catch {
508
+ return String(arg);
509
+ }
510
+ }).join(" ");
511
+ process.stderr.write(`[ringi:mcp:${level}] ${line}
512
+ `);
513
+ };
514
+ var createSandboxConsole = () => Object.freeze({
515
+ error: (...args) => writeSandboxLog("error", args),
516
+ info: (...args) => writeSandboxLog("info", args),
517
+ log: (...args) => writeSandboxLog("log", args),
518
+ warn: (...args) => writeSandboxLog("warn", args)
519
+ });
520
+ var runSandbox = (globals, code, timeoutMs) => {
521
+ const execute = Effect.tryPromise({
522
+ catch: (error) => new SandboxExecutionError({
523
+ error,
524
+ message: formatError(error)
525
+ }),
526
+ try: () => {
527
+ const context = vm.createContext({
528
+ ...globals,
529
+ Buffer: void 0,
530
+ clearImmediate: void 0,
531
+ clearInterval: void 0,
532
+ clearTimeout: void 0,
533
+ console: createSandboxConsole(),
534
+ fetch: void 0,
535
+ process: void 0,
536
+ queueMicrotask,
537
+ require: void 0,
538
+ setImmediate: void 0,
539
+ setInterval: void 0,
540
+ setTimeout: void 0
541
+ });
542
+ const script = new vm.Script(
543
+ `"use strict"; (async () => {
544
+ ${code}
545
+ })()`,
546
+ { filename: "ringi-mcp-execute.js" }
547
+ );
548
+ return Promise.resolve(
549
+ script.runInContext(context, { timeout: timeoutMs })
550
+ );
551
+ }
552
+ });
553
+ return execute.pipe(
554
+ Effect.timeoutFail({
555
+ duration: Duration.millis(timeoutMs),
556
+ onTimeout: () => new ExecutionTimeoutError({
557
+ message: `Execution timed out after ${timeoutMs}ms`,
558
+ timeoutMs
559
+ })
560
+ })
561
+ );
562
+ };
563
+ var createJournal = () => {
564
+ const entries = [];
565
+ const recordSuccess = (name, result) => {
566
+ entries.push({ name, ok: true, result: summarizeForJournal(result) });
567
+ };
568
+ const recordFailure = (name, error) => {
569
+ entries.push({ error: formatError(error), name, ok: false });
570
+ };
571
+ const tracked = (runtime, name, effect) => runtime.runPromise(
572
+ effect
573
+ ).then(
574
+ (result) => {
575
+ recordSuccess(name, result);
576
+ return result;
577
+ },
578
+ (error) => {
579
+ recordFailure(name, error);
580
+ throw error;
581
+ }
582
+ );
583
+ const trackedAsync = async (name, fn) => {
584
+ try {
585
+ const result = await fn();
586
+ recordSuccess(name, result);
587
+ return result;
588
+ } catch (error) {
589
+ recordFailure(name, error);
590
+ throw error;
591
+ }
592
+ };
593
+ return { entries, recordFailure, recordSuccess, tracked, trackedAsync };
594
+ };
595
+ var buildSandboxGlobals = (runtime, config, journal) => {
596
+ const { tracked, trackedAsync } = journal;
597
+ const throwIfReadonly = () => {
598
+ if (config.readonly) {
599
+ throw new Error(
600
+ "Mutation rejected: MCP server is running in readonly mode"
601
+ );
602
+ }
603
+ };
604
+ const run = (name, effect) => tracked(runtime, name, effect);
605
+ const deps = {
606
+ call: trackedAsync,
607
+ getBranchDiff: (branch) => run(
608
+ "git.getBranchDiff",
609
+ Effect.gen(function* getBranchDiff() {
610
+ const git = yield* GitService;
611
+ return yield* git.getBranchDiff(branch);
612
+ })
613
+ ),
614
+ getBranches: () => run(
615
+ "git.getBranches",
616
+ Effect.gen(function* getBranches() {
617
+ const git = yield* GitService;
618
+ return yield* git.getBranches;
619
+ })
620
+ ),
621
+ getCommitDiff: (shas) => run(
622
+ "git.getCommitDiff",
623
+ Effect.gen(function* getCommitDiff() {
624
+ const git = yield* GitService;
625
+ return yield* git.getCommitDiff(shas);
626
+ })
627
+ ),
628
+ getLatestReviewId: async () => {
629
+ const result = await run(
630
+ "reviews.latestId",
631
+ Effect.gen(function* result2() {
632
+ const svc = yield* ReviewService;
633
+ return yield* svc.list({
634
+ page: 1,
635
+ pageSize: 1,
636
+ repositoryPath: config.repoRoot
637
+ });
638
+ })
639
+ );
640
+ return result.reviews[0]?.id ?? null;
641
+ },
642
+ getRecentCommits: async () => {
643
+ const result = await run(
644
+ "git.getCommits",
645
+ Effect.gen(function* result2() {
646
+ const git = yield* GitService;
647
+ return yield* git.getCommits({ limit: 10, offset: 0 });
648
+ })
649
+ );
650
+ return result.commits;
651
+ },
652
+ getRepositoryInfo: () => run(
653
+ "git.getRepositoryInfo",
654
+ Effect.gen(function* getRepositoryInfo() {
655
+ const git = yield* GitService;
656
+ return yield* git.getRepositoryInfo;
657
+ })
658
+ ),
659
+ getStagedDiff: () => run(
660
+ "git.getStagedDiff",
661
+ Effect.gen(function* getStagedDiff() {
662
+ const git = yield* GitService;
663
+ return yield* git.getStagedDiff;
664
+ })
665
+ ),
666
+ getStagedFiles: () => run(
667
+ "git.getStagedFiles",
668
+ Effect.gen(function* getStagedFiles() {
669
+ const git = yield* GitService;
670
+ return yield* git.getStagedFiles;
671
+ })
672
+ ),
673
+ readonly: config.readonly,
674
+ repoRoot: config.repoRoot,
675
+ requireWritable: throwIfReadonly
676
+ };
677
+ const baseGlobals = createSandboxGlobals(deps);
678
+ const reviews = Object.freeze({
679
+ create: async (inputValue) => {
680
+ throwIfReadonly();
681
+ const parsed = decodeReviewCreateInput(inputValue);
682
+ return run(
683
+ "reviews.create",
684
+ Effect.gen(function* create() {
685
+ const svc = yield* ReviewService;
686
+ return yield* svc.create(parsed);
687
+ })
688
+ );
689
+ },
690
+ export: async (options) => {
691
+ const opts = decodeInputSync(
692
+ ReviewExportInput,
693
+ options,
694
+ "reviews.export"
695
+ );
696
+ const markdown = await run(
697
+ "reviews.export",
698
+ Effect.gen(function* markdown2() {
699
+ const svc = yield* ExportService;
700
+ return yield* svc.exportReview(opts.reviewId);
701
+ })
702
+ );
703
+ return { markdown, reviewId: opts.reviewId };
704
+ },
705
+ get: (reviewIdValue) => {
706
+ const reviewId = reviewIdValue;
707
+ return run(
708
+ "reviews.get",
709
+ Effect.gen(function* get() {
710
+ const svc = yield* ReviewService;
711
+ return yield* svc.getById(reviewId);
712
+ })
713
+ );
714
+ },
715
+ getComments: (reviewIdValue, filePath) => {
716
+ const reviewId = reviewIdValue;
717
+ if (filePath !== null && filePath !== void 0 && typeof filePath !== "string") {
718
+ return Promise.reject(new Error("Invalid filePath: expected a string"));
719
+ }
720
+ return run(
721
+ "reviews.getComments",
722
+ Effect.gen(function* getComments() {
723
+ const svc = yield* CommentService;
724
+ return filePath ? yield* svc.getByFile(reviewId, filePath) : yield* svc.getByReview(reviewId);
725
+ })
726
+ );
727
+ },
728
+ getDiff: async (query) => {
729
+ const q = decodeInputSync(ReviewDiffQuery, query, "reviews.getDiff");
730
+ const hunks = await run(
731
+ `reviews.getDiff:${q.filePath}`,
732
+ Effect.gen(function* hunks2() {
733
+ const svc = yield* ReviewService;
734
+ return yield* svc.getFileHunks(q.reviewId, q.filePath);
735
+ })
736
+ );
737
+ return { filePath: q.filePath, hunks, reviewId: q.reviewId };
738
+ },
739
+ getFiles: async (reviewIdValue) => {
740
+ const reviewId = reviewIdValue;
741
+ const review = await run(
742
+ "reviews.getFiles",
743
+ Effect.gen(function* review2() {
744
+ const svc = yield* ReviewService;
745
+ return yield* svc.getById(reviewId);
746
+ })
747
+ );
748
+ return review.files;
749
+ },
750
+ getStatus: async (reviewIdValue) => {
751
+ const reviewId = reviewIdValue;
752
+ const [review, stats] = await Promise.all([
753
+ run(
754
+ "reviews.getStatus.review",
755
+ Effect.gen(function* () {
756
+ const svc = yield* ReviewService;
757
+ return yield* svc.getById(reviewId);
758
+ })
759
+ ),
760
+ run(
761
+ "reviews.getStatus.comments",
762
+ Effect.gen(function* () {
763
+ const svc = yield* CommentService;
764
+ return yield* svc.getStats(reviewId);
765
+ })
766
+ )
767
+ ]);
768
+ return {
769
+ resolvedComments: stats.resolved,
770
+ reviewId,
771
+ status: review.status,
772
+ totalComments: stats.total,
773
+ unresolvedComments: stats.unresolved,
774
+ withSuggestions: 0
775
+ };
776
+ },
777
+ getSuggestions: async (reviewIdValue) => {
778
+ const reviewId = reviewIdValue;
779
+ const comments = await run(
780
+ "reviews.getSuggestions",
781
+ Effect.gen(function* comments2() {
782
+ const svc = yield* CommentService;
783
+ return yield* svc.getByReview(reviewId);
784
+ })
785
+ );
786
+ return comments.filter((c) => c.suggestion != null).map((c) => ({
787
+ commentId: c.id,
788
+ id: c.id,
789
+ originalCode: "",
790
+ suggestedCode: c.suggestion ?? ""
791
+ }));
792
+ },
793
+ list: async (filters) => {
794
+ const parsed = decodeInputSync(
795
+ ReviewListFilters,
796
+ filters ?? {},
797
+ "reviews.list"
798
+ );
799
+ return run(
800
+ "reviews.list",
801
+ Effect.gen(function* list() {
802
+ const svc = yield* ReviewService;
803
+ return yield* svc.list({
804
+ page: parsed.page,
805
+ pageSize: parsed.limit,
806
+ repositoryPath: config.repoRoot,
807
+ sourceType: parsed.sourceType,
808
+ status: parsed.status
809
+ });
810
+ })
811
+ );
812
+ }
813
+ });
814
+ const todos = Object.freeze({
815
+ add: async (inputValue) => {
816
+ throwIfReadonly();
817
+ const parsed = decodeCreateTodoInput(inputValue);
818
+ return run(
819
+ "todos.add",
820
+ Effect.gen(function* add() {
821
+ const svc = yield* TodoService;
822
+ return yield* svc.create(parsed);
823
+ })
824
+ );
825
+ },
826
+ clear: async (_reviewIdValue) => {
827
+ throwIfReadonly();
828
+ return run(
829
+ "todos.clear",
830
+ Effect.gen(function* clear() {
831
+ const svc = yield* TodoService;
832
+ const result = yield* svc.removeCompleted();
833
+ return { removed: result.deleted, success: true };
834
+ })
835
+ );
836
+ },
837
+ done: async (todoIdValue) => {
838
+ throwIfReadonly();
839
+ const todoId = todoIdValue;
840
+ return run(
841
+ "todos.done",
842
+ Effect.gen(function* done() {
843
+ const svc = yield* TodoService;
844
+ const todo = yield* svc.getById(todoId);
845
+ if (todo.completed) {
846
+ return todo;
847
+ }
848
+ return yield* svc.toggle(todoId);
849
+ })
850
+ );
851
+ },
852
+ list: async (filter) => {
853
+ const parsed = decodeInputSync(
854
+ TodoListFilter,
855
+ filter ?? {},
856
+ "todos.list"
857
+ );
858
+ const result = await run(
859
+ "todos.list",
860
+ Effect.gen(function* result2() {
861
+ const svc = yield* TodoService;
862
+ return yield* svc.list({ reviewId: parsed.reviewId });
863
+ })
864
+ );
865
+ return result.data;
866
+ },
867
+ move: async (todoIdValue, positionValue) => {
868
+ throwIfReadonly();
869
+ const parsed = decodeInputSync(
870
+ TodoMoveInput,
871
+ { position: positionValue, todoId: todoIdValue },
872
+ "todos.move"
873
+ );
874
+ return run(
875
+ "todos.move",
876
+ Effect.gen(function* move() {
877
+ const svc = yield* TodoService;
878
+ return yield* svc.move(parsed.todoId, parsed.position);
879
+ })
880
+ );
881
+ },
882
+ remove: async (todoIdValue) => {
883
+ throwIfReadonly();
884
+ const todoId = todoIdValue;
885
+ return run(
886
+ "todos.remove",
887
+ Effect.gen(function* remove() {
888
+ const svc = yield* TodoService;
889
+ return yield* svc.remove(todoId);
890
+ })
891
+ );
892
+ },
893
+ undone: async (todoIdValue) => {
894
+ throwIfReadonly();
895
+ const todoId = todoIdValue;
896
+ return run(
897
+ "todos.undone",
898
+ Effect.gen(function* undone() {
899
+ const svc = yield* TodoService;
900
+ const todo = yield* svc.getById(todoId);
901
+ if (!todo.completed) {
902
+ return todo;
903
+ }
904
+ return yield* svc.toggle(todoId);
905
+ })
906
+ );
907
+ }
908
+ });
909
+ return {
910
+ events: baseGlobals.events,
911
+ intelligence: baseGlobals.intelligence,
912
+ reviews,
913
+ session: baseGlobals.session,
914
+ sources: baseGlobals.sources,
915
+ todos
916
+ };
917
+ };
918
+ var executeCode = (runtime, config, input) => Effect.gen(function* executeCode2() {
919
+ const timeoutMs = yield* validateTimeout(input.timeout, config);
920
+ const code = yield* validateCode(input.code);
921
+ const journal = createJournal();
922
+ const globals = buildSandboxGlobals(runtime, config, journal);
923
+ const sandboxResult = yield* runSandbox(globals, code, timeoutMs).pipe(
924
+ Effect.catchTags({
925
+ ExecutionTimeoutError: (e) => Effect.succeed({
926
+ error: e.message,
927
+ ok: false,
928
+ result: journal.entries.length === 0 ? null : { operations: journal.entries }
929
+ }),
930
+ SandboxExecutionError: (e) => Effect.succeed({
931
+ error: e.message,
932
+ ok: false,
933
+ result: journal.entries.length === 0 ? null : { operations: journal.entries }
934
+ })
935
+ }),
936
+ Effect.map((result) => {
937
+ if (typeof result === "object" && result !== null && "ok" in result && result.ok === false) {
938
+ return result;
939
+ }
940
+ return { ok: true, result };
941
+ })
942
+ );
943
+ return finalizeOutput(sandboxResult, config.maxOutputBytes);
944
+ });
945
+ var executeCodeToPromise = async (runtime, config, input) => {
946
+ const program = executeCode(runtime, config, input).pipe(
947
+ Effect.catchTags({
948
+ InvalidCodeError: (e) => Effect.succeed({
949
+ error: e.message,
950
+ ok: false,
951
+ result: null
952
+ }),
953
+ InvalidTimeoutError: (e) => Effect.succeed({
954
+ error: e.message,
955
+ ok: false,
956
+ result: null
957
+ })
958
+ })
959
+ );
960
+ try {
961
+ return await Effect.runPromise(program);
962
+ } catch (error) {
963
+ return finalizeOutput(
964
+ {
965
+ error: formatError(error),
966
+ ok: false,
967
+ result: null
968
+ },
969
+ config.maxOutputBytes
970
+ );
971
+ }
972
+ };
973
+ var makeConfigLayer = (config) => Layer2.setConfigProvider(
974
+ ConfigProvider.fromMap(
975
+ /* @__PURE__ */ new Map([
976
+ ["DB_PATH", config.dbPath],
977
+ ["REPOSITORY_PATH", config.repoRoot]
978
+ ])
979
+ )
980
+ );
981
+ var makeMcpLayer = (config) => Layer2.mergeAll(CoreLive, McpConfigLive(config)).pipe(
982
+ Layer2.provideMerge(makeConfigLayer(config))
983
+ );
984
+ var createMcpRuntime = (config) => ManagedRuntime.make(makeMcpLayer(config));
985
+
986
+ // src/mcp/server.ts
987
+ var JSON_RPC_VERSION = "2.0";
988
+ var LATEST_PROTOCOL_VERSION = "2025-11-25";
989
+ var MCP_SERVER_NAME = "ringi";
990
+ var MCP_SERVER_VERSION = process.env.npm_package_version ?? "0.0.0-dev";
991
+ var EXECUTE_TOOL = {
992
+ description: "Run constrained JavaScript against Ringi review namespaces: review, todo, comment, diff, export, and session.",
993
+ inputSchema: {
994
+ additionalProperties: false,
995
+ properties: {
996
+ code: {
997
+ description: "JavaScript snippet to evaluate inside the Ringi MCP sandbox.",
998
+ type: "string"
999
+ },
1000
+ timeout: {
1001
+ description: "Optional timeout in milliseconds. Defaults to 30000 and clamps at 120000.",
1002
+ type: "number"
1003
+ }
1004
+ },
1005
+ required: ["code"],
1006
+ type: "object"
1007
+ },
1008
+ name: "execute",
1009
+ outputSchema: {
1010
+ additionalProperties: false,
1011
+ properties: {
1012
+ error: { type: "string" },
1013
+ ok: { type: "boolean" },
1014
+ result: {},
1015
+ truncated: { type: "boolean" }
1016
+ },
1017
+ required: ["ok", "result"],
1018
+ type: "object"
1019
+ }
1020
+ };
1021
+ var writeStderr = (message) => {
1022
+ process.stderr.write(`[ringi:mcp] ${message}
1023
+ `);
1024
+ };
1025
+ var writeMessage = (message) => {
1026
+ const body = JSON.stringify(message);
1027
+ const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
1028
+ \r
1029
+ ${body}`;
1030
+ stdout.write(payload);
1031
+ };
1032
+ var parseContentLength = (headerText) => {
1033
+ const headerLine = headerText.split("\r\n").find((line) => line.toLowerCase().startsWith("content-length:"));
1034
+ if (!headerLine) {
1035
+ throw new Error("Missing Content-Length header");
1036
+ }
1037
+ const rawValue = headerLine.slice(headerLine.indexOf(":") + 1).trim();
1038
+ const parsed = Number.parseInt(rawValue, 10);
1039
+ if (!Number.isFinite(parsed) || parsed < 0) {
1040
+ throw new Error(`Invalid Content-Length header: ${rawValue}`);
1041
+ }
1042
+ return parsed;
1043
+ };
1044
+ var formatError2 = (error) => {
1045
+ if (error instanceof Error) {
1046
+ return error.message;
1047
+ }
1048
+ return String(error);
1049
+ };
1050
+ var sendError = (id, code, message) => {
1051
+ writeMessage({
1052
+ error: {
1053
+ code,
1054
+ message
1055
+ },
1056
+ id,
1057
+ jsonrpc: JSON_RPC_VERSION
1058
+ });
1059
+ };
1060
+ var sendResult = (id, result) => {
1061
+ writeMessage({
1062
+ id,
1063
+ jsonrpc: JSON_RPC_VERSION,
1064
+ result
1065
+ });
1066
+ };
1067
+ var createInitializeResult = () => ({
1068
+ capabilities: {
1069
+ tools: {
1070
+ listChanged: false
1071
+ }
1072
+ },
1073
+ protocolVersion: LATEST_PROTOCOL_VERSION,
1074
+ serverInfo: {
1075
+ description: "Local-first MCP codemode adapter over the Ringi core runtime.",
1076
+ name: MCP_SERVER_NAME,
1077
+ version: MCP_SERVER_VERSION
1078
+ }
1079
+ });
1080
+ var StdioJsonRpcServer = class {
1081
+ config = resolveMcpConfig(process.argv.slice(2));
1082
+ runtime = createMcpRuntime(this.config);
1083
+ buffer = Buffer.alloc(0);
1084
+ initialized = false;
1085
+ shuttingDown = false;
1086
+ start() {
1087
+ stdin.on("data", async (chunk) => {
1088
+ this.buffer = Buffer.concat([this.buffer, chunk]);
1089
+ try {
1090
+ await this.drainBuffer();
1091
+ } catch (error) {
1092
+ writeStderr(`fatal buffer drain error: ${formatError2(error)}`);
1093
+ }
1094
+ });
1095
+ stdin.on("end", async () => {
1096
+ await this.runtime.dispose();
1097
+ });
1098
+ process.on("SIGINT", async () => {
1099
+ await this.close(0);
1100
+ });
1101
+ process.on("SIGTERM", async () => {
1102
+ await this.close(0);
1103
+ });
1104
+ writeStderr(
1105
+ `server started readonly=${String(this.config.readonly)} repo=${this.config.repoRoot}`
1106
+ );
1107
+ }
1108
+ async close(code) {
1109
+ await this.runtime.dispose();
1110
+ process.exit(code);
1111
+ }
1112
+ async drainBuffer() {
1113
+ while (true) {
1114
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
1115
+ if (headerEnd === -1) {
1116
+ return;
1117
+ }
1118
+ const headerText = this.buffer.subarray(0, headerEnd).toString("utf8");
1119
+ const contentLength = parseContentLength(headerText);
1120
+ const messageEnd = headerEnd + 4 + contentLength;
1121
+ if (this.buffer.byteLength < messageEnd) {
1122
+ return;
1123
+ }
1124
+ const payload = this.buffer.subarray(headerEnd + 4, messageEnd).toString("utf8");
1125
+ this.buffer = this.buffer.subarray(messageEnd);
1126
+ let message;
1127
+ try {
1128
+ message = JSON.parse(payload);
1129
+ } catch (error) {
1130
+ sendError(null, -32700, `Parse error: ${formatError2(error)}`);
1131
+ continue;
1132
+ }
1133
+ await this.handleMessage(message);
1134
+ }
1135
+ }
1136
+ async handleMessage(message) {
1137
+ if (typeof message.method !== "string") {
1138
+ sendError(message.id ?? null, -32600, "Invalid JSON-RPC request");
1139
+ return;
1140
+ }
1141
+ try {
1142
+ if (await this.handleLifecycleMessage(message)) {
1143
+ return;
1144
+ }
1145
+ this.assertInitialized();
1146
+ if (message.method === "tools/list") {
1147
+ sendResult(message.id ?? null, { tools: [EXECUTE_TOOL] });
1148
+ return;
1149
+ }
1150
+ if (message.method === "tools/call") {
1151
+ await this.handleToolCall(message.id ?? null, message.params);
1152
+ return;
1153
+ }
1154
+ sendError(
1155
+ message.id ?? null,
1156
+ -32601,
1157
+ `Method not found: ${message.method}`
1158
+ );
1159
+ } catch (error) {
1160
+ sendError(message.id ?? null, -32603, formatError2(error));
1161
+ }
1162
+ }
1163
+ async handleLifecycleMessage(message) {
1164
+ if (message.method === "initialize") {
1165
+ this.initialized = true;
1166
+ sendResult(message.id ?? null, createInitializeResult());
1167
+ return true;
1168
+ }
1169
+ if (message.method === "notifications/initialized") {
1170
+ return true;
1171
+ }
1172
+ if (message.method === "ping") {
1173
+ sendResult(message.id ?? null, {});
1174
+ return true;
1175
+ }
1176
+ if (message.method === "shutdown") {
1177
+ this.shuttingDown = true;
1178
+ sendResult(message.id ?? null, {});
1179
+ return true;
1180
+ }
1181
+ if (message.method === "exit") {
1182
+ await this.close(this.shuttingDown ? 0 : 1);
1183
+ return true;
1184
+ }
1185
+ return false;
1186
+ }
1187
+ assertInitialized() {
1188
+ if (!this.initialized) {
1189
+ throw new Error("Server is not initialized");
1190
+ }
1191
+ if (this.shuttingDown) {
1192
+ throw new Error("Server is shutting down");
1193
+ }
1194
+ }
1195
+ async handleToolCall(id, params) {
1196
+ if (!params || typeof params.name !== "string") {
1197
+ sendError(id, -32602, "Invalid tools/call params");
1198
+ return;
1199
+ }
1200
+ if (params.name !== EXECUTE_TOOL.name) {
1201
+ sendError(id, -32602, `Tool not found: ${String(params.name)}`);
1202
+ return;
1203
+ }
1204
+ const argumentsValue = params.arguments;
1205
+ if (typeof argumentsValue !== "object" || argumentsValue === null) {
1206
+ sendError(id, -32602, "Invalid tools/call arguments");
1207
+ return;
1208
+ }
1209
+ const executeResult = await executeCodeToPromise(
1210
+ this.runtime,
1211
+ this.config,
1212
+ argumentsValue
1213
+ );
1214
+ sendResult(id, {
1215
+ content: [
1216
+ {
1217
+ text: JSON.stringify(executeResult),
1218
+ type: "text"
1219
+ }
1220
+ ],
1221
+ isError: !executeResult.ok,
1222
+ structuredContent: executeResult
1223
+ });
1224
+ }
1225
+ };
1226
+ new StdioJsonRpcServer().start();
1227
+ //# sourceMappingURL=mcp.js.map
1228
+ //# sourceMappingURL=mcp.js.map