@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.
Files changed (3) hide show
  1. package/README.md +3 -14
  2. package/dist/index.js +357 -159
  3. 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 Premise
8
+ bunx @difflab/difflab-cli
9
9
  ```
10
10
 
11
- ## MCP todo tools
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
- `mcp setup` does **not** create a todo file. Use the `todo_init` MCP tool to create a list. 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.
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 (input) => {
27347
+ callback: async (untrustedInput) => {
27351
27348
  try {
27352
- return result(await handler(config.inputSchema.parse(input)));
27349
+ return serializeToolResult(await handler(config.inputSchema.parse(untrustedInput)));
27353
27350
  } catch (error) {
27354
27351
  return {
27355
- ...result({ error: error instanceof Error ? error.message : String(error) }),
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/todos/path.ts
27364
- import { lstat, realpath, stat } from "fs/promises";
27365
- import { isAbsolute, join, relative, resolve, sep } from "path";
27366
- function isFileError(error, code) {
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
- async function resolveTodoPath(cwd, path) {
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 todo files: ${current}`);
27394
+ throw new Error(`Symlink paths are not allowed for files: ${current}`);
27388
27395
  }
27389
27396
  } catch (error) {
27390
- if (isFileError(error, "ENOENT"))
27397
+ if (hasErrorCode(error, "ENOENT"))
27391
27398
  break;
27392
27399
  throw error;
27393
27400
  }
27394
27401
  }
27395
27402
  return file;
27396
27403
  }
27397
- var init_path = () => {};
27404
+ var init_pathx = __esm(() => {
27405
+ init_osx();
27406
+ });
27398
27407
 
27399
- // src/todos/store.ts
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 TodoStore {
27413
+ class JsonFileStore {
27414
+ schema;
27408
27415
  file;
27409
- constructor(file) {
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 initialize() {
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 list = { version: 1, tasks: [] };
27470
+ const validated = items.map((item) => this.parse(item));
27471
+ this.validateUniqueIds(validated);
27415
27472
  try {
27416
- await writeFile(this.file, `${JSON.stringify(list, null, 2)}
27417
- `, { flag: "wx", mode: 384 });
27418
- return { created: true, list };
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
- if (!isFileError2(error, "EEXIST"))
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
- return { created: false, list: await this.read() };
27506
+ throw new JsonFileStoreValidationError(`Invalid JSON store at ${this.file}`, { cause: error });
27423
27507
  }
27424
27508
  }
27425
- async list() {
27426
- return this.read();
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
- async add(text) {
27429
- const value = string2().trim().min(1).parse(text);
27430
- const list = await this.read();
27431
- const task = { id: randomUUID(), text: value, done: false };
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
- async complete(id) {
27437
- const list = await this.read();
27438
- const task = list.tasks.find((item) => item.id === id);
27439
- if (!task)
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 remove(id) {
27446
- const list = await this.read();
27447
- const index = list.tasks.findIndex((item) => item.id === id);
27448
- if (index === -1)
27449
- throw new Error(`Unknown task ID: ${id}`);
27450
- const [task] = list.tasks.splice(index, 1);
27451
- await this.write(list);
27452
- return task;
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
- async read() {
27455
- let content;
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
- content = await readFile(this.file, "utf8");
27580
+ return { version: 1, tasks: await this.tasks.list() };
27458
27581
  } catch (error) {
27459
- if (isFileError2(error, "ENOENT")) {
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
- const list = listSchema.parse(JSON.parse(content));
27468
- if (new Set(list.tasks.map((task) => task.id)).size !== list.tasks.length) {
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
- throw new Error(`Invalid todo list at ${this.file}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
27599
+ this.rethrowDomainError(error);
27474
27600
  }
27475
27601
  }
27476
- async write(list) {
27477
- const temp = `${this.file}.${randomUUID()}.tmp`;
27602
+ async complete(id) {
27478
27603
  try {
27479
- await writeFile(temp, `${JSON.stringify(list, null, 2)}
27480
- `, { flag: "wx", mode: 384 });
27481
- await rename(temp, this.file);
27482
- } finally {
27483
- await rm(temp, { force: true });
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, listSchema;
27642
+ var taskSchema;
27488
27643
  var init_store = __esm(() => {
27489
27644
  init_zod();
27490
- taskSchema = strictObject({
27491
- id: uuid2(),
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 resolveTodoPath(cwd, path));
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
- init_path();
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 createDifflabServer(version = "0.1.0") {
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
- return server;
27551
- }
27552
- async function serveMcp(version) {
27553
- await createDifflabServer(version).connect(new StdioServerTransport);
27701
+ await server.connect(new StdioServerTransport);
27554
27702
  }
27555
- var init_mcp2 = __esm(() => {
27703
+ var init_server3 = __esm(() => {
27556
27704
  init_mcp();
27557
27705
  init_stdio2();
27558
27706
  init_tools();
27559
27707
  });
27560
27708
 
27561
- // src/agents/adapter.ts
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 Error(`Could not configure ${command}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
27738
+ throw new CommandExecutionError(command, error instanceof Error ? error.message : String(error), { cause: error });
27572
27739
  }
27573
27740
  }
27574
- function isFileError3(error, code) {
27575
- return error instanceof Error && "code" in error && error.code === code;
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/agents/json-config.ts
27586
- import { randomUUID as randomUUID2 } from "crypto";
27587
- import { lstat as lstat2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
27588
- import { dirname as dirname2 } from "path";
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 readJsonConfig(file) {
27766
+ async function readJsonFile(file) {
27593
27767
  try {
27594
- if ((await lstat2(file)).isSymbolicLink()) {
27595
- throw new Error(`MCP config at ${file} is a symlink; refusing to replace it`);
27596
- }
27597
- const content = await readFile2(file, "utf8");
27598
- try {
27599
- const parsed = JSON.parse(content);
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 (isFileError3(error, "ENOENT"))
27775
+ if (hasErrorCode(error, "ENOENT"))
27608
27776
  return {};
27609
27777
  throw error;
27610
27778
  }
27611
27779
  }
27612
- function serverStatus(config, file) {
27613
- if (config.mcpServers !== undefined && !isObject2(config.mcpServers)) {
27614
- throw new Error(`Invalid MCP config at ${file}; mcpServers must be an object`);
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
- return { file, status: "added" };
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 Error(`MCP server "difflab" already exists in ${file} with a different configuration; not modified`);
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 (!isFileError3(error, "ENOENT"))
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 Error(`Codex already has a different difflab entry in ${file}; not modified`);
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).argument("[name]", "name to greet", "world").action((name) => write(greeting(name)));
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(() => init_agents());
29814
- if (client.length === 0) {
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-rc.6";
30031
+ var version2 = "0.1.0";
29834
30032
  await createProgram(console.log, version2).parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@difflab/difflab-cli",
3
- "version": "0.1.0-rc.6",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "description": "A minimal Bun CLI starter built with Commander",
6
6
  "license": "MIT",