@difflab/difflab-cli 0.1.0-rc.6 → 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/README.md +3 -14
- package/dist/index.js +357 -159
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@ A Bun command-line application with a local Difflab MCP server.
|
|
|
5
5
|
## Run
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bunx @difflab/difflab-cli
|
|
8
|
+
bunx @difflab/difflab-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## MCP
|
|
11
|
+
## MCP setup
|
|
12
12
|
|
|
13
13
|
After installing the CLI so `difflab-cli` is on your PATH, register it with the MCP clients you use:
|
|
14
14
|
|
|
@@ -19,18 +19,7 @@ difflab-cli mcp setup --client cursor --client codex
|
|
|
19
19
|
|
|
20
20
|
Clients are selected explicitly. Supported names: `pi`, `cursor`, `codex`, `claude-code`, and `claude-desktop`. Pi uses the shared `~/.config/mcp/mcp.json` file (through the Pi MCP adapter), Cursor uses `~/.cursor/mcp.json`, Codex uses its CLI, Claude Code uses its CLI with user scope, and Claude Desktop uses its platform-specific user config. These clients do **not** all read one common file. Setup adds Difflab only when absent, leaves other servers in place, and refuses to replace a conflicting `difflab` entry. Codex and Claude Code must have their CLIs installed if selected. Restart or reload the selected client after setup.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Each todo call requires **`cwd` (an absolute path to an existing directory)** and **`path` (a relative path to a JSON todo file inside that directory)**. They are supplied per call, not in the MCP server configuration. For example, with `cwd` set to `/home/alex`:
|
|
25
|
-
|
|
26
|
-
```text
|
|
27
|
-
todo_init({"cwd":"/home/alex","path":"todos.json"})
|
|
28
|
-
todo_add({"cwd":"/home/alex","path":"todos.json","text":"Review PR"})
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`todo_list`, `todo_complete`, and `todo_remove` also require both fields; the latter two require a task `id` returned by `todo_list` or `todo_add`. Call `todo_init` before other operations on a new file. Parent traversal outside `cwd`, absolute `path` values, and symlinked paths are rejected. Keep personal todo files outside the repository or add their paths to `.gitignore`. Concurrent writes to the same file are not yet coordinated.
|
|
32
|
-
|
|
33
|
-
Agents in this repo can load `skills/difflab-todo/SKILL.md` for usage instructions.
|
|
22
|
+
MCP clients launch `npx -y @difflab/difflab-cli mcp serve` over stdio, so no daemon or port is needed. Bun must be on the client's PATH.
|
|
34
23
|
|
|
35
24
|
## Develop
|
|
36
25
|
|
package/dist/index.js
CHANGED
|
@@ -27336,9 +27336,6 @@ var init_stdio2 = __esm(() => {
|
|
|
27336
27336
|
});
|
|
27337
27337
|
|
|
27338
27338
|
// src/tools.ts
|
|
27339
|
-
function result(value) {
|
|
27340
|
-
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
27341
|
-
}
|
|
27342
27339
|
function defineTool(name, config, handler) {
|
|
27343
27340
|
return {
|
|
27344
27341
|
name,
|
|
@@ -27347,26 +27344,36 @@ function defineTool(name, config, handler) {
|
|
|
27347
27344
|
inputSchema: config.inputSchema,
|
|
27348
27345
|
...config.annotations ? { annotations: config.annotations } : {}
|
|
27349
27346
|
},
|
|
27350
|
-
callback: async (
|
|
27347
|
+
callback: async (untrustedInput) => {
|
|
27351
27348
|
try {
|
|
27352
|
-
return
|
|
27349
|
+
return serializeToolResult(await handler(config.inputSchema.parse(untrustedInput)));
|
|
27353
27350
|
} catch (error) {
|
|
27354
27351
|
return {
|
|
27355
|
-
...
|
|
27352
|
+
...serializeToolResult({ error: error instanceof Error ? error.message : String(error) }),
|
|
27356
27353
|
isError: true
|
|
27357
27354
|
};
|
|
27358
27355
|
}
|
|
27359
27356
|
}
|
|
27360
27357
|
};
|
|
27361
27358
|
}
|
|
27359
|
+
function serializeToolResult(value) {
|
|
27360
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
27361
|
+
}
|
|
27362
27362
|
|
|
27363
|
-
// src/
|
|
27364
|
-
import {
|
|
27365
|
-
|
|
27366
|
-
|
|
27363
|
+
// src/extensions/osx.ts
|
|
27364
|
+
import { homedir } from "os";
|
|
27365
|
+
function resolveHomeDirectory(home) {
|
|
27366
|
+
return home ?? homedir();
|
|
27367
|
+
}
|
|
27368
|
+
function hasErrorCode(error, code) {
|
|
27367
27369
|
return error instanceof Error && "code" in error && error.code === code;
|
|
27368
27370
|
}
|
|
27369
|
-
|
|
27371
|
+
var init_osx = () => {};
|
|
27372
|
+
|
|
27373
|
+
// src/extensions/pathx.ts
|
|
27374
|
+
import { lstat, realpath, stat } from "fs/promises";
|
|
27375
|
+
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
27376
|
+
async function resolvePath(cwd, path) {
|
|
27370
27377
|
if (!isAbsolute(cwd))
|
|
27371
27378
|
throw new Error("cwd must be an absolute path to an existing directory");
|
|
27372
27379
|
if (!path.trim() || isAbsolute(path))
|
|
@@ -27384,126 +27391,270 @@ async function resolveTodoPath(cwd, path) {
|
|
|
27384
27391
|
current = join(current, segment);
|
|
27385
27392
|
try {
|
|
27386
27393
|
if ((await lstat(current)).isSymbolicLink()) {
|
|
27387
|
-
throw new Error(`Symlink paths are not allowed for
|
|
27394
|
+
throw new Error(`Symlink paths are not allowed for files: ${current}`);
|
|
27388
27395
|
}
|
|
27389
27396
|
} catch (error) {
|
|
27390
|
-
if (
|
|
27397
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
27391
27398
|
break;
|
|
27392
27399
|
throw error;
|
|
27393
27400
|
}
|
|
27394
27401
|
}
|
|
27395
27402
|
return file;
|
|
27396
27403
|
}
|
|
27397
|
-
var
|
|
27404
|
+
var init_pathx = __esm(() => {
|
|
27405
|
+
init_osx();
|
|
27406
|
+
});
|
|
27398
27407
|
|
|
27399
|
-
// src/
|
|
27408
|
+
// src/stores/json-file-store.ts
|
|
27400
27409
|
import { randomUUID } from "crypto";
|
|
27401
27410
|
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
27402
27411
|
import { dirname, resolve as resolve2 } from "path";
|
|
27403
|
-
function isFileError2(error, code) {
|
|
27404
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
27405
|
-
}
|
|
27406
27412
|
|
|
27407
|
-
class
|
|
27413
|
+
class JsonFileStore {
|
|
27414
|
+
schema;
|
|
27408
27415
|
file;
|
|
27409
|
-
|
|
27416
|
+
itemsKey;
|
|
27417
|
+
metadata;
|
|
27418
|
+
envelopeMetadata = {};
|
|
27419
|
+
constructor(file, schema, options) {
|
|
27420
|
+
this.schema = schema;
|
|
27410
27421
|
this.file = resolve2(file);
|
|
27422
|
+
this.itemsKey = options?.itemsKey;
|
|
27423
|
+
this.metadata = options?.metadata ?? {};
|
|
27411
27424
|
}
|
|
27412
|
-
async
|
|
27425
|
+
async read(id) {
|
|
27426
|
+
const item = (await this.list()).find((candidate) => candidate.id === id);
|
|
27427
|
+
if (!item)
|
|
27428
|
+
throw new JsonFileStoreMissingItemError(`Item does not exist: ${id}`);
|
|
27429
|
+
return item;
|
|
27430
|
+
}
|
|
27431
|
+
async list() {
|
|
27432
|
+
const envelope = await this.load();
|
|
27433
|
+
const raw = this.itemsKey ? envelope[this.itemsKey] : envelope;
|
|
27434
|
+
if (!Array.isArray(raw)) {
|
|
27435
|
+
throw new JsonFileStoreValidationError(`Invalid JSON store at ${this.file}`);
|
|
27436
|
+
}
|
|
27437
|
+
const items = raw.map((item) => this.parse(item));
|
|
27438
|
+
this.validateUniqueIds(items);
|
|
27439
|
+
return items;
|
|
27440
|
+
}
|
|
27441
|
+
async write(item) {
|
|
27442
|
+
const value = this.parse(item);
|
|
27443
|
+
const items = await this.list();
|
|
27444
|
+
const index = items.findIndex((candidate) => candidate.id === value.id);
|
|
27445
|
+
if (index === -1)
|
|
27446
|
+
items.push(value);
|
|
27447
|
+
else
|
|
27448
|
+
items[index] = value;
|
|
27449
|
+
await this.persist(items);
|
|
27450
|
+
}
|
|
27451
|
+
async replace(items) {
|
|
27452
|
+
const validated = items.map((item) => this.parse(item));
|
|
27453
|
+
this.validateUniqueIds(validated);
|
|
27454
|
+
await this.persist(validated);
|
|
27455
|
+
}
|
|
27456
|
+
async update(id, mutation) {
|
|
27457
|
+
const items = await this.list();
|
|
27458
|
+
const index = items.findIndex((item) => item.id === id);
|
|
27459
|
+
if (index === -1)
|
|
27460
|
+
throw new JsonFileStoreMissingItemError(`Item does not exist: ${id}`);
|
|
27461
|
+
const current = items[index];
|
|
27462
|
+
const updated = this.parse(mutation(current) ?? current);
|
|
27463
|
+
items[index] = updated;
|
|
27464
|
+
this.validateUniqueIds(items);
|
|
27465
|
+
await this.persist(items);
|
|
27466
|
+
return updated;
|
|
27467
|
+
}
|
|
27468
|
+
async initialize(items = []) {
|
|
27413
27469
|
await mkdir(dirname(this.file), { recursive: true });
|
|
27414
|
-
const
|
|
27470
|
+
const validated = items.map((item) => this.parse(item));
|
|
27471
|
+
this.validateUniqueIds(validated);
|
|
27415
27472
|
try {
|
|
27416
|
-
await writeFile(this.file,
|
|
27417
|
-
|
|
27418
|
-
|
|
27473
|
+
await writeFile(this.file, this.serialize(validated), { flag: "wx", mode: 384 });
|
|
27474
|
+
return true;
|
|
27475
|
+
} catch (error) {
|
|
27476
|
+
if (error.code === "EEXIST")
|
|
27477
|
+
return false;
|
|
27478
|
+
throw new JsonFileStoreWriteError(`Could not write JSON file: ${this.file}`, { cause: error });
|
|
27479
|
+
}
|
|
27480
|
+
}
|
|
27481
|
+
async load() {
|
|
27482
|
+
let content;
|
|
27483
|
+
try {
|
|
27484
|
+
content = await readFile(this.file, "utf8");
|
|
27419
27485
|
} catch (error) {
|
|
27420
|
-
|
|
27486
|
+
const ErrorType = error.code === "ENOENT" ? JsonFileStoreNotFoundError : JsonFileStoreReadError;
|
|
27487
|
+
throw new ErrorType(`Could not read JSON file: ${this.file}`, { cause: error });
|
|
27488
|
+
}
|
|
27489
|
+
try {
|
|
27490
|
+
const parsed = JSON.parse(content);
|
|
27491
|
+
if (!this.itemsKey)
|
|
27492
|
+
return parsed;
|
|
27493
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
27494
|
+
throw new Error("Envelope must be an object");
|
|
27495
|
+
const envelope = parsed;
|
|
27496
|
+
for (const [key, value] of Object.entries(this.metadata)) {
|
|
27497
|
+
if (envelope[key] !== value)
|
|
27498
|
+
throw new Error(`Invalid metadata: ${key}`);
|
|
27499
|
+
}
|
|
27500
|
+
const { [this.itemsKey]: _, ...metadata } = envelope;
|
|
27501
|
+
this.envelopeMetadata = metadata;
|
|
27502
|
+
return envelope;
|
|
27503
|
+
} catch (error) {
|
|
27504
|
+
if (error instanceof JsonFileStoreValidationError)
|
|
27421
27505
|
throw error;
|
|
27422
|
-
|
|
27506
|
+
throw new JsonFileStoreValidationError(`Invalid JSON store at ${this.file}`, { cause: error });
|
|
27423
27507
|
}
|
|
27424
27508
|
}
|
|
27425
|
-
|
|
27426
|
-
|
|
27509
|
+
parse(value) {
|
|
27510
|
+
try {
|
|
27511
|
+
return this.schema.parse(value);
|
|
27512
|
+
} catch (error) {
|
|
27513
|
+
throw new JsonFileStoreValidationError(`Invalid item for ${this.file}`, { cause: error });
|
|
27514
|
+
}
|
|
27427
27515
|
}
|
|
27428
|
-
|
|
27429
|
-
|
|
27430
|
-
|
|
27431
|
-
|
|
27432
|
-
list.tasks.push(task);
|
|
27433
|
-
await this.write(list);
|
|
27434
|
-
return task;
|
|
27516
|
+
validateUniqueIds(items) {
|
|
27517
|
+
if (new Set(items.map((item) => item.id)).size !== items.length) {
|
|
27518
|
+
throw new JsonFileStoreValidationError(`Invalid item IDs for ${this.file}`);
|
|
27519
|
+
}
|
|
27435
27520
|
}
|
|
27436
|
-
|
|
27437
|
-
const
|
|
27438
|
-
|
|
27439
|
-
|
|
27440
|
-
throw new Error(`Unknown task ID: ${id}`);
|
|
27441
|
-
task.done = true;
|
|
27442
|
-
await this.write(list);
|
|
27443
|
-
return task;
|
|
27521
|
+
serialize(items) {
|
|
27522
|
+
const payload = this.itemsKey ? { ...this.envelopeMetadata, ...this.metadata, [this.itemsKey]: items } : items;
|
|
27523
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
27524
|
+
`;
|
|
27444
27525
|
}
|
|
27445
|
-
async
|
|
27446
|
-
const
|
|
27447
|
-
|
|
27448
|
-
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27526
|
+
async persist(items) {
|
|
27527
|
+
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
27528
|
+
try {
|
|
27529
|
+
await writeFile(temp, this.serialize(items), { mode: 384 });
|
|
27530
|
+
await rename(temp, this.file);
|
|
27531
|
+
} catch (error) {
|
|
27532
|
+
throw new JsonFileStoreWriteError(`Could not write JSON file: ${this.file}`, { cause: error });
|
|
27533
|
+
} finally {
|
|
27534
|
+
await rm(temp, { force: true });
|
|
27535
|
+
}
|
|
27453
27536
|
}
|
|
27454
|
-
|
|
27455
|
-
|
|
27537
|
+
}
|
|
27538
|
+
var JsonFileStoreError, JsonFileStoreReadError, JsonFileStoreNotFoundError, JsonFileStoreWriteError, JsonFileStoreValidationError, JsonFileStoreMissingItemError;
|
|
27539
|
+
var init_json_file_store = __esm(() => {
|
|
27540
|
+
JsonFileStoreError = class JsonFileStoreError extends Error {
|
|
27541
|
+
constructor(message, options) {
|
|
27542
|
+
super(message, options);
|
|
27543
|
+
this.name = new.target.name;
|
|
27544
|
+
}
|
|
27545
|
+
};
|
|
27546
|
+
JsonFileStoreReadError = class JsonFileStoreReadError extends JsonFileStoreError {
|
|
27547
|
+
};
|
|
27548
|
+
JsonFileStoreNotFoundError = class JsonFileStoreNotFoundError extends JsonFileStoreReadError {
|
|
27549
|
+
};
|
|
27550
|
+
JsonFileStoreWriteError = class JsonFileStoreWriteError extends JsonFileStoreError {
|
|
27551
|
+
};
|
|
27552
|
+
JsonFileStoreValidationError = class JsonFileStoreValidationError extends JsonFileStoreError {
|
|
27553
|
+
};
|
|
27554
|
+
JsonFileStoreMissingItemError = class JsonFileStoreMissingItemError extends JsonFileStoreReadError {
|
|
27555
|
+
};
|
|
27556
|
+
});
|
|
27557
|
+
|
|
27558
|
+
// src/todos/store.ts
|
|
27559
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
27560
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
27561
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
27562
|
+
|
|
27563
|
+
class TodoStore {
|
|
27564
|
+
file;
|
|
27565
|
+
tasks;
|
|
27566
|
+
constructor(file) {
|
|
27567
|
+
this.file = resolve3(file);
|
|
27568
|
+
this.tasks = new JsonFileStore(this.file, taskSchema, {
|
|
27569
|
+
itemsKey: "tasks",
|
|
27570
|
+
metadata: { version: 1 }
|
|
27571
|
+
});
|
|
27572
|
+
}
|
|
27573
|
+
async initialize() {
|
|
27574
|
+
await mkdir2(dirname2(this.file), { recursive: true });
|
|
27575
|
+
const created = await this.tasks.initialize();
|
|
27576
|
+
return { created, list: created ? { version: 1, tasks: [] } : await this.list() };
|
|
27577
|
+
}
|
|
27578
|
+
async list() {
|
|
27456
27579
|
try {
|
|
27457
|
-
|
|
27580
|
+
return { version: 1, tasks: await this.tasks.list() };
|
|
27458
27581
|
} catch (error) {
|
|
27459
|
-
if (
|
|
27582
|
+
if (error instanceof JsonFileStoreNotFoundError) {
|
|
27460
27583
|
throw new Error(`Todo list does not exist: ${this.file}. Call todo_init first.`, {
|
|
27461
27584
|
cause: error
|
|
27462
27585
|
});
|
|
27463
27586
|
}
|
|
27587
|
+
if (error instanceof JsonFileStoreValidationError) {
|
|
27588
|
+
throw new Error(`Invalid todo list at ${this.file}`, { cause: error });
|
|
27589
|
+
}
|
|
27464
27590
|
throw error;
|
|
27465
27591
|
}
|
|
27592
|
+
}
|
|
27593
|
+
async add(text) {
|
|
27594
|
+
const task = { id: randomUUID2(), text: string2().trim().min(1).parse(text), done: false };
|
|
27466
27595
|
try {
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
throw new Error("duplicate task IDs");
|
|
27470
|
-
}
|
|
27471
|
-
return list;
|
|
27596
|
+
await this.tasks.write(task);
|
|
27597
|
+
return task;
|
|
27472
27598
|
} catch (error) {
|
|
27473
|
-
|
|
27599
|
+
this.rethrowDomainError(error);
|
|
27474
27600
|
}
|
|
27475
27601
|
}
|
|
27476
|
-
async
|
|
27477
|
-
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
27602
|
+
async complete(id) {
|
|
27478
27603
|
try {
|
|
27479
|
-
await
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
}
|
|
27483
|
-
|
|
27604
|
+
return await this.tasks.update(id, (task) => {
|
|
27605
|
+
task.done = true;
|
|
27606
|
+
});
|
|
27607
|
+
} catch (error) {
|
|
27608
|
+
if (error instanceof JsonFileStoreMissingItemError)
|
|
27609
|
+
throw new Error(`Unknown task ID: ${id}`);
|
|
27610
|
+
this.rethrowDomainError(error);
|
|
27484
27611
|
}
|
|
27485
27612
|
}
|
|
27613
|
+
async remove(id) {
|
|
27614
|
+
let list;
|
|
27615
|
+
try {
|
|
27616
|
+
list = await this.tasks.list();
|
|
27617
|
+
} catch (error) {
|
|
27618
|
+
this.rethrowDomainError(error);
|
|
27619
|
+
}
|
|
27620
|
+
const task = list.find((item) => item.id === id);
|
|
27621
|
+
if (!task)
|
|
27622
|
+
throw new Error(`Unknown task ID: ${id}`);
|
|
27623
|
+
try {
|
|
27624
|
+
await this.tasks.replace(list.filter((item) => item.id !== id));
|
|
27625
|
+
return task;
|
|
27626
|
+
} catch (error) {
|
|
27627
|
+
this.rethrowDomainError(error);
|
|
27628
|
+
}
|
|
27629
|
+
}
|
|
27630
|
+
rethrowDomainError(error) {
|
|
27631
|
+
if (error instanceof JsonFileStoreNotFoundError) {
|
|
27632
|
+
throw new Error(`Todo list does not exist: ${this.file}. Call todo_init first.`, {
|
|
27633
|
+
cause: error
|
|
27634
|
+
});
|
|
27635
|
+
}
|
|
27636
|
+
if (error instanceof JsonFileStoreValidationError) {
|
|
27637
|
+
throw new Error(`Invalid todo list at ${this.file}`, { cause: error });
|
|
27638
|
+
}
|
|
27639
|
+
throw error;
|
|
27640
|
+
}
|
|
27486
27641
|
}
|
|
27487
|
-
var taskSchema
|
|
27642
|
+
var taskSchema;
|
|
27488
27643
|
var init_store = __esm(() => {
|
|
27489
27644
|
init_zod();
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
text: string2().min(1),
|
|
27493
|
-
done: boolean2()
|
|
27494
|
-
});
|
|
27495
|
-
listSchema = strictObject({ version: literal(1), tasks: array(taskSchema) });
|
|
27645
|
+
init_json_file_store();
|
|
27646
|
+
taskSchema = strictObject({ id: uuid2(), text: string2().min(1), done: boolean2() });
|
|
27496
27647
|
});
|
|
27497
27648
|
|
|
27498
27649
|
// src/todos/tools.ts
|
|
27499
27650
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
27500
27651
|
async function store(cwd, path) {
|
|
27501
|
-
return new TodoStore(await
|
|
27652
|
+
return new TodoStore(await resolvePath(cwd, path));
|
|
27502
27653
|
}
|
|
27503
27654
|
var cwd, path2, id, fileInput, todoTools;
|
|
27504
27655
|
var init_tools = __esm(() => {
|
|
27505
27656
|
init_zod();
|
|
27506
|
-
|
|
27657
|
+
init_pathx();
|
|
27507
27658
|
init_store();
|
|
27508
27659
|
cwd = string2().refine(isAbsolute2, "cwd must be an absolute path").describe("Absolute path to an existing base directory, such as the repository root. Never use the server process cwd implicitly.");
|
|
27509
27660
|
path2 = string2().min(1).refine((value) => value.trim().length > 0 && !isAbsolute2(value), "path must be relative to cwd").describe("Relative path to the JSON todo file inside cwd, such as todos.json or .local/todos.json. No escaping cwd or symlinks.");
|
|
@@ -27541,98 +27692,95 @@ var init_tools = __esm(() => {
|
|
|
27541
27692
|
];
|
|
27542
27693
|
});
|
|
27543
27694
|
|
|
27544
|
-
// src/mcp.ts
|
|
27545
|
-
function
|
|
27695
|
+
// src/mcp/server.ts
|
|
27696
|
+
async function serveMcp(version = "0.1.0") {
|
|
27546
27697
|
const server = new McpServer({ name: "difflab", version });
|
|
27547
27698
|
for (const tool of todoTools) {
|
|
27548
27699
|
server.registerTool(tool.name, tool.config, tool.callback);
|
|
27549
27700
|
}
|
|
27550
|
-
|
|
27551
|
-
}
|
|
27552
|
-
async function serveMcp(version) {
|
|
27553
|
-
await createDifflabServer(version).connect(new StdioServerTransport);
|
|
27701
|
+
await server.connect(new StdioServerTransport);
|
|
27554
27702
|
}
|
|
27555
|
-
var
|
|
27703
|
+
var init_server3 = __esm(() => {
|
|
27556
27704
|
init_mcp();
|
|
27557
27705
|
init_stdio2();
|
|
27558
27706
|
init_tools();
|
|
27559
27707
|
});
|
|
27560
27708
|
|
|
27561
|
-
// src/
|
|
27709
|
+
// src/errors.ts
|
|
27710
|
+
var DifflabError, ConfigurationError, CommandExecutionError;
|
|
27711
|
+
var init_errors4 = __esm(() => {
|
|
27712
|
+
DifflabError = class DifflabError extends Error {
|
|
27713
|
+
constructor(message, options) {
|
|
27714
|
+
super(message, options);
|
|
27715
|
+
this.name = "DifflabError";
|
|
27716
|
+
}
|
|
27717
|
+
};
|
|
27718
|
+
ConfigurationError = class ConfigurationError extends DifflabError {
|
|
27719
|
+
constructor(message, options) {
|
|
27720
|
+
super(message, options);
|
|
27721
|
+
this.name = "ConfigurationError";
|
|
27722
|
+
}
|
|
27723
|
+
};
|
|
27724
|
+
CommandExecutionError = class CommandExecutionError extends DifflabError {
|
|
27725
|
+
constructor(command, message, options) {
|
|
27726
|
+
super(`Could not configure ${command}: ${message}`, options);
|
|
27727
|
+
this.name = "CommandExecutionError";
|
|
27728
|
+
}
|
|
27729
|
+
};
|
|
27730
|
+
});
|
|
27731
|
+
|
|
27732
|
+
// src/extensions/processx.ts
|
|
27562
27733
|
import { execFileSync } from "child_process";
|
|
27563
|
-
import { homedir } from "os";
|
|
27564
|
-
function setupHome(context) {
|
|
27565
|
-
return context.home ?? homedir();
|
|
27566
|
-
}
|
|
27567
27734
|
function runCommand(command, args, cwd) {
|
|
27568
27735
|
try {
|
|
27569
27736
|
execFileSync(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
27570
27737
|
} catch (error) {
|
|
27571
|
-
throw new
|
|
27738
|
+
throw new CommandExecutionError(command, error instanceof Error ? error.message : String(error), { cause: error });
|
|
27572
27739
|
}
|
|
27573
27740
|
}
|
|
27574
|
-
|
|
27575
|
-
|
|
27741
|
+
var init_processx = __esm(() => {
|
|
27742
|
+
init_errors4();
|
|
27743
|
+
});
|
|
27744
|
+
|
|
27745
|
+
// src/agents/adapter.ts
|
|
27746
|
+
function setupHome(context) {
|
|
27747
|
+
return resolveHomeDirectory(context.home);
|
|
27576
27748
|
}
|
|
27577
27749
|
var mcpServer;
|
|
27578
27750
|
var init_adapter = __esm(() => {
|
|
27751
|
+
init_osx();
|
|
27752
|
+
init_processx();
|
|
27579
27753
|
mcpServer = {
|
|
27580
27754
|
command: "npx",
|
|
27581
27755
|
args: ["-y", "@difflab/difflab-cli", "mcp", "serve"]
|
|
27582
27756
|
};
|
|
27583
27757
|
});
|
|
27584
27758
|
|
|
27585
|
-
// src/
|
|
27586
|
-
import { randomUUID as
|
|
27587
|
-
import { lstat as lstat2, mkdir as
|
|
27588
|
-
import { dirname as
|
|
27759
|
+
// src/extensions/fsx.ts
|
|
27760
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
27761
|
+
import { lstat as lstat2, mkdir as mkdir3, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
27762
|
+
import { dirname as dirname3 } from "path";
|
|
27589
27763
|
function isObject2(value) {
|
|
27590
27764
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
27591
27765
|
}
|
|
27592
|
-
async function
|
|
27766
|
+
async function readJsonFile(file) {
|
|
27593
27767
|
try {
|
|
27594
|
-
if ((await lstat2(file)).isSymbolicLink())
|
|
27595
|
-
throw new Error(`
|
|
27596
|
-
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27600
|
-
if (!isObject2(parsed))
|
|
27601
|
-
throw new Error("root must be a JSON object");
|
|
27602
|
-
return parsed;
|
|
27603
|
-
} catch (error) {
|
|
27604
|
-
throw new Error(`Invalid MCP config at ${file}; not modified: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
27605
|
-
}
|
|
27768
|
+
if ((await lstat2(file)).isSymbolicLink())
|
|
27769
|
+
throw new Error(`JSON file at ${file} is a symlink`);
|
|
27770
|
+
const parsed = JSON.parse(await readFile2(file, "utf8"));
|
|
27771
|
+
if (!isObject2(parsed))
|
|
27772
|
+
throw new Error("root must be a JSON object");
|
|
27773
|
+
return parsed;
|
|
27606
27774
|
} catch (error) {
|
|
27607
|
-
if (
|
|
27775
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
27608
27776
|
return {};
|
|
27609
27777
|
throw error;
|
|
27610
27778
|
}
|
|
27611
27779
|
}
|
|
27612
|
-
function
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
}
|
|
27616
|
-
if (!isObject2(config.mcpServers) || !Object.hasOwn(config.mcpServers, "difflab")) {
|
|
27617
|
-
return "missing";
|
|
27618
|
-
}
|
|
27619
|
-
const entry = config.mcpServers.difflab;
|
|
27620
|
-
return isObject2(entry) && entry.command === mcpServer.command && Array.isArray(entry.args) && entry.args.length === mcpServer.args.length && entry.args.every((arg, index) => arg === mcpServer.args[index]) ? "matching" : "conflicting";
|
|
27621
|
-
}
|
|
27622
|
-
async function addJsonServer(file) {
|
|
27623
|
-
const config = await readJsonConfig(file);
|
|
27624
|
-
const status = serverStatus(config, file);
|
|
27625
|
-
if (status === "matching")
|
|
27626
|
-
return { file, status: "existing" };
|
|
27627
|
-
if (status === "conflicting") {
|
|
27628
|
-
throw new Error(`MCP server "difflab" already exists in ${file} with a different configuration; not modified`);
|
|
27629
|
-
}
|
|
27630
|
-
const updated = {
|
|
27631
|
-
...config,
|
|
27632
|
-
mcpServers: { ...isObject2(config.mcpServers) ? config.mcpServers : {}, difflab: mcpServer }
|
|
27633
|
-
};
|
|
27634
|
-
await mkdir2(dirname2(file), { recursive: true });
|
|
27635
|
-
const temp = `${file}.${randomUUID2()}.tmp`;
|
|
27780
|
+
async function patchJsonToFile(file, patch) {
|
|
27781
|
+
const updated = patch(await readJsonFile(file));
|
|
27782
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
27783
|
+
const temp = `${file}.${randomUUID3()}.tmp`;
|
|
27636
27784
|
try {
|
|
27637
27785
|
await writeFile2(temp, `${JSON.stringify(updated, null, 2)}
|
|
27638
27786
|
`, { flag: "wx", mode: 384 });
|
|
@@ -27640,7 +27788,27 @@ async function addJsonServer(file) {
|
|
|
27640
27788
|
} finally {
|
|
27641
27789
|
await rm2(temp, { force: true });
|
|
27642
27790
|
}
|
|
27643
|
-
|
|
27791
|
+
}
|
|
27792
|
+
var init_fsx = __esm(() => {
|
|
27793
|
+
init_osx();
|
|
27794
|
+
});
|
|
27795
|
+
|
|
27796
|
+
// src/mcp/json-config.ts
|
|
27797
|
+
async function readJsonConfig(file) {
|
|
27798
|
+
try {
|
|
27799
|
+
return await readJsonFile(file);
|
|
27800
|
+
} catch (error) {
|
|
27801
|
+
throw new ConfigurationError(`Invalid MCP config at ${file}; not modified: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
27802
|
+
}
|
|
27803
|
+
}
|
|
27804
|
+
function serverStatus(config, file) {
|
|
27805
|
+
if (config.mcpServers !== undefined && !isObject2(config.mcpServers)) {
|
|
27806
|
+
throw new ConfigurationError(`Invalid MCP config at ${file}; mcpServers must be an object`);
|
|
27807
|
+
}
|
|
27808
|
+
if (!isObject2(config.mcpServers) || !Object.hasOwn(config.mcpServers, "difflab"))
|
|
27809
|
+
return "missing";
|
|
27810
|
+
const entry = config.mcpServers.difflab;
|
|
27811
|
+
return isObject2(entry) && entry.command === mcpServer.command && Array.isArray(entry.args) && entry.args.length === mcpServer.args.length && entry.args.every((arg, index) => arg === mcpServer.args[index]) ? "matching" : "conflicting";
|
|
27644
27812
|
}
|
|
27645
27813
|
function jsonAgentAdapter(id, configPath) {
|
|
27646
27814
|
return {
|
|
@@ -27650,8 +27818,23 @@ function jsonAgentAdapter(id, configPath) {
|
|
|
27650
27818
|
}
|
|
27651
27819
|
};
|
|
27652
27820
|
}
|
|
27821
|
+
async function addJsonServer(file) {
|
|
27822
|
+
const config = await readJsonConfig(file);
|
|
27823
|
+
const status = serverStatus(config, file);
|
|
27824
|
+
if (status === "matching")
|
|
27825
|
+
return { file, status: "existing" };
|
|
27826
|
+
if (status === "conflicting")
|
|
27827
|
+
throw new ConfigurationError(`MCP server "difflab" already exists in ${file} with a different configuration; not modified`);
|
|
27828
|
+
await patchJsonToFile(file, (current) => ({
|
|
27829
|
+
...current,
|
|
27830
|
+
mcpServers: { ...isObject2(current.mcpServers) ? current.mcpServers : {}, difflab: mcpServer }
|
|
27831
|
+
}));
|
|
27832
|
+
return { file, status: "added" };
|
|
27833
|
+
}
|
|
27653
27834
|
var init_json_config = __esm(() => {
|
|
27654
27835
|
init_adapter();
|
|
27836
|
+
init_errors4();
|
|
27837
|
+
init_fsx();
|
|
27655
27838
|
});
|
|
27656
27839
|
|
|
27657
27840
|
// src/agents/claude-code.ts
|
|
@@ -27659,6 +27842,7 @@ import { join as join2 } from "path";
|
|
|
27659
27842
|
var claudeCodeAdapter;
|
|
27660
27843
|
var init_claude_code = __esm(() => {
|
|
27661
27844
|
init_adapter();
|
|
27845
|
+
init_errors4();
|
|
27662
27846
|
init_json_config();
|
|
27663
27847
|
claudeCodeAdapter = {
|
|
27664
27848
|
id: "claude-code",
|
|
@@ -27669,7 +27853,7 @@ var init_claude_code = __esm(() => {
|
|
|
27669
27853
|
if (status === "matching")
|
|
27670
27854
|
return { file, status: "existing" };
|
|
27671
27855
|
if (status === "conflicting") {
|
|
27672
|
-
throw new
|
|
27856
|
+
throw new ConfigurationError(`MCP server "difflab" already exists in ${file} with a different configuration; not modified`);
|
|
27673
27857
|
}
|
|
27674
27858
|
const run = context.run ?? runCommand;
|
|
27675
27859
|
run("claude", ["mcp", "add", "--scope", "user", "difflab", "--", mcpServer.command, ...mcpServer.args], home);
|
|
@@ -27698,6 +27882,8 @@ function codexServerStatus(content) {
|
|
|
27698
27882
|
var codexAdapter;
|
|
27699
27883
|
var init_codex = __esm(() => {
|
|
27700
27884
|
init_adapter();
|
|
27885
|
+
init_errors4();
|
|
27886
|
+
init_osx();
|
|
27701
27887
|
codexAdapter = {
|
|
27702
27888
|
id: "codex",
|
|
27703
27889
|
async setupMcpConfig(context = {}) {
|
|
@@ -27707,14 +27893,14 @@ var init_codex = __esm(() => {
|
|
|
27707
27893
|
try {
|
|
27708
27894
|
content = await readFile3(file, "utf8");
|
|
27709
27895
|
} catch (error) {
|
|
27710
|
-
if (!
|
|
27896
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
27711
27897
|
throw error;
|
|
27712
27898
|
}
|
|
27713
27899
|
const status = codexServerStatus(content);
|
|
27714
27900
|
if (status === "matching")
|
|
27715
27901
|
return { file, status: "existing" };
|
|
27716
27902
|
if (status === "conflicting") {
|
|
27717
|
-
throw new
|
|
27903
|
+
throw new ConfigurationError(`Codex already has a different difflab entry in ${file}; not modified`);
|
|
27718
27904
|
}
|
|
27719
27905
|
const run = context.run ?? runCommand;
|
|
27720
27906
|
run("codex", ["mcp", "add", "difflab", "--", mcpServer.command, ...mcpServer.args], home);
|
|
@@ -27754,6 +27940,33 @@ var init_agents = __esm(() => {
|
|
|
27754
27940
|
agentNames = Object.keys(agentAdapters);
|
|
27755
27941
|
});
|
|
27756
27942
|
|
|
27943
|
+
// src/mcp/setup.ts
|
|
27944
|
+
async function setupMcpClients(clients, write) {
|
|
27945
|
+
if (clients.length === 0) {
|
|
27946
|
+
throw new Error(`Select at least one client with --client (${agentNames.join(", ")})`);
|
|
27947
|
+
}
|
|
27948
|
+
const adapters = [...new Set(clients)].map((name) => {
|
|
27949
|
+
const adapter = agentAdapter(name);
|
|
27950
|
+
if (!adapter)
|
|
27951
|
+
throw new Error(`Unknown MCP client: ${name}. Choose ${agentNames.join(", ")}`);
|
|
27952
|
+
return adapter;
|
|
27953
|
+
});
|
|
27954
|
+
for (const adapter of adapters) {
|
|
27955
|
+
const { file, status } = await adapter.setupMcpConfig();
|
|
27956
|
+
write(`${status === "added" ? "Added" : "Already configured"} ${adapter.id}: ${file}`);
|
|
27957
|
+
}
|
|
27958
|
+
write("Restart or reload the selected client to use the Difflab MCP tools.");
|
|
27959
|
+
}
|
|
27960
|
+
var init_setup = __esm(() => {
|
|
27961
|
+
init_agents();
|
|
27962
|
+
});
|
|
27963
|
+
|
|
27964
|
+
// src/mcp/index.ts
|
|
27965
|
+
var init_mcp2 = __esm(() => {
|
|
27966
|
+
init_server3();
|
|
27967
|
+
init_setup();
|
|
27968
|
+
});
|
|
27969
|
+
|
|
27757
27970
|
// ../../node_modules/commander/lib/error.js
|
|
27758
27971
|
class CommanderError extends Error {
|
|
27759
27972
|
constructor(exitCode, code, message) {
|
|
@@ -29799,36 +30012,21 @@ function useColor() {
|
|
|
29799
30012
|
var program = new Command;
|
|
29800
30013
|
|
|
29801
30014
|
// src/cli.ts
|
|
29802
|
-
function greeting(name) {
|
|
29803
|
-
return `Hello, ${name}!`;
|
|
29804
|
-
}
|
|
29805
30015
|
function createProgram(write = console.log, version = "0.1.0") {
|
|
29806
|
-
const program = new Command().name("difflab-cli").description("Difflab CLI and local MCP server").version(version)
|
|
30016
|
+
const program = new Command().name("difflab-cli").description("Difflab CLI and local MCP server").version(version);
|
|
30017
|
+
program.action(() => program.outputHelp());
|
|
29807
30018
|
const mcp = program.command("mcp").description("Difflab MCP server commands");
|
|
29808
30019
|
mcp.command("serve").description("Serve Difflab tools over MCP stdio").action(async () => {
|
|
29809
30020
|
await Promise.resolve().then(() => init_mcp2());
|
|
29810
30021
|
await serveMcp(version);
|
|
29811
30022
|
});
|
|
29812
30023
|
mcp.command("setup").description("Register the installed Difflab MCP server in selected user-level clients").option("--client <name>", "pi, cursor, codex, claude-code, or claude-desktop (repeatable)", (name, selected) => [...selected, name], []).action(async ({ client }) => {
|
|
29813
|
-
await Promise.resolve().then(() =>
|
|
29814
|
-
|
|
29815
|
-
throw new Error(`Select at least one client with --client (${agentNames.join(", ")})`);
|
|
29816
|
-
}
|
|
29817
|
-
const adapters = [...new Set(client)].map((name) => {
|
|
29818
|
-
const adapter = agentAdapter(name);
|
|
29819
|
-
if (!adapter)
|
|
29820
|
-
throw new Error(`Unknown MCP client: ${name}. Choose ${agentNames.join(", ")}`);
|
|
29821
|
-
return adapter;
|
|
29822
|
-
});
|
|
29823
|
-
for (const adapter of adapters) {
|
|
29824
|
-
const { file, status } = await adapter.setupMcpConfig();
|
|
29825
|
-
write(`${status === "added" ? "Added" : "Already configured"} ${adapter.id}: ${file}`);
|
|
29826
|
-
}
|
|
29827
|
-
write("Restart or reload the selected client to use the Difflab MCP tools.");
|
|
30024
|
+
await Promise.resolve().then(() => init_mcp2());
|
|
30025
|
+
await setupMcpClients(client, write);
|
|
29828
30026
|
});
|
|
29829
30027
|
return program;
|
|
29830
30028
|
}
|
|
29831
30029
|
|
|
29832
30030
|
// src/index.ts
|
|
29833
|
-
var version2 = "0.1.0
|
|
30031
|
+
var version2 = "0.1.0";
|
|
29834
30032
|
await createProgram(console.log, version2).parseAsync(process.argv);
|