@difflab/difflab-cli 0.1.0-rc.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -21
- package/NOTICE +2 -0
- package/README.md +3 -14
- package/dist/index.js +358 -159
- package/package.json +4 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,202 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
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,271 @@ 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 metadata = { ...envelope };
|
|
27501
|
+
delete metadata[this.itemsKey];
|
|
27502
|
+
this.envelopeMetadata = metadata;
|
|
27503
|
+
return envelope;
|
|
27504
|
+
} catch (error) {
|
|
27505
|
+
if (error instanceof JsonFileStoreValidationError)
|
|
27421
27506
|
throw error;
|
|
27422
|
-
|
|
27507
|
+
throw new JsonFileStoreValidationError(`Invalid JSON store at ${this.file}`, { cause: error });
|
|
27423
27508
|
}
|
|
27424
27509
|
}
|
|
27425
|
-
|
|
27426
|
-
|
|
27510
|
+
parse(value) {
|
|
27511
|
+
try {
|
|
27512
|
+
return this.schema.parse(value);
|
|
27513
|
+
} catch (error) {
|
|
27514
|
+
throw new JsonFileStoreValidationError(`Invalid item for ${this.file}`, { cause: error });
|
|
27515
|
+
}
|
|
27427
27516
|
}
|
|
27428
|
-
|
|
27429
|
-
|
|
27430
|
-
|
|
27431
|
-
|
|
27432
|
-
list.tasks.push(task);
|
|
27433
|
-
await this.write(list);
|
|
27434
|
-
return task;
|
|
27517
|
+
validateUniqueIds(items) {
|
|
27518
|
+
if (new Set(items.map((item) => item.id)).size !== items.length) {
|
|
27519
|
+
throw new JsonFileStoreValidationError(`Invalid item IDs for ${this.file}`);
|
|
27520
|
+
}
|
|
27435
27521
|
}
|
|
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;
|
|
27522
|
+
serialize(items) {
|
|
27523
|
+
const payload = this.itemsKey ? { ...this.envelopeMetadata, ...this.metadata, [this.itemsKey]: items } : items;
|
|
27524
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
27525
|
+
`;
|
|
27444
27526
|
}
|
|
27445
|
-
async
|
|
27446
|
-
const
|
|
27447
|
-
|
|
27448
|
-
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27527
|
+
async persist(items) {
|
|
27528
|
+
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
27529
|
+
try {
|
|
27530
|
+
await writeFile(temp, this.serialize(items), { mode: 384 });
|
|
27531
|
+
await rename(temp, this.file);
|
|
27532
|
+
} catch (error) {
|
|
27533
|
+
throw new JsonFileStoreWriteError(`Could not write JSON file: ${this.file}`, { cause: error });
|
|
27534
|
+
} finally {
|
|
27535
|
+
await rm(temp, { force: true });
|
|
27536
|
+
}
|
|
27453
27537
|
}
|
|
27454
|
-
|
|
27455
|
-
|
|
27538
|
+
}
|
|
27539
|
+
var JsonFileStoreError, JsonFileStoreReadError, JsonFileStoreNotFoundError, JsonFileStoreWriteError, JsonFileStoreValidationError, JsonFileStoreMissingItemError;
|
|
27540
|
+
var init_json_file_store = __esm(() => {
|
|
27541
|
+
JsonFileStoreError = class JsonFileStoreError extends Error {
|
|
27542
|
+
constructor(message, options) {
|
|
27543
|
+
super(message, options);
|
|
27544
|
+
this.name = new.target.name;
|
|
27545
|
+
}
|
|
27546
|
+
};
|
|
27547
|
+
JsonFileStoreReadError = class JsonFileStoreReadError extends JsonFileStoreError {
|
|
27548
|
+
};
|
|
27549
|
+
JsonFileStoreNotFoundError = class JsonFileStoreNotFoundError extends JsonFileStoreReadError {
|
|
27550
|
+
};
|
|
27551
|
+
JsonFileStoreWriteError = class JsonFileStoreWriteError extends JsonFileStoreError {
|
|
27552
|
+
};
|
|
27553
|
+
JsonFileStoreValidationError = class JsonFileStoreValidationError extends JsonFileStoreError {
|
|
27554
|
+
};
|
|
27555
|
+
JsonFileStoreMissingItemError = class JsonFileStoreMissingItemError extends JsonFileStoreReadError {
|
|
27556
|
+
};
|
|
27557
|
+
});
|
|
27558
|
+
|
|
27559
|
+
// src/todos/store.ts
|
|
27560
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
27561
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
27562
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
27563
|
+
|
|
27564
|
+
class TodoStore {
|
|
27565
|
+
file;
|
|
27566
|
+
tasks;
|
|
27567
|
+
constructor(file) {
|
|
27568
|
+
this.file = resolve3(file);
|
|
27569
|
+
this.tasks = new JsonFileStore(this.file, taskSchema, {
|
|
27570
|
+
itemsKey: "tasks",
|
|
27571
|
+
metadata: { version: 1 }
|
|
27572
|
+
});
|
|
27573
|
+
}
|
|
27574
|
+
async initialize() {
|
|
27575
|
+
await mkdir2(dirname2(this.file), { recursive: true });
|
|
27576
|
+
const created = await this.tasks.initialize();
|
|
27577
|
+
return { created, list: created ? { version: 1, tasks: [] } : await this.list() };
|
|
27578
|
+
}
|
|
27579
|
+
async list() {
|
|
27456
27580
|
try {
|
|
27457
|
-
|
|
27581
|
+
return { version: 1, tasks: await this.tasks.list() };
|
|
27458
27582
|
} catch (error) {
|
|
27459
|
-
if (
|
|
27583
|
+
if (error instanceof JsonFileStoreNotFoundError) {
|
|
27460
27584
|
throw new Error(`Todo list does not exist: ${this.file}. Call todo_init first.`, {
|
|
27461
27585
|
cause: error
|
|
27462
27586
|
});
|
|
27463
27587
|
}
|
|
27588
|
+
if (error instanceof JsonFileStoreValidationError) {
|
|
27589
|
+
throw new Error(`Invalid todo list at ${this.file}`, { cause: error });
|
|
27590
|
+
}
|
|
27464
27591
|
throw error;
|
|
27465
27592
|
}
|
|
27593
|
+
}
|
|
27594
|
+
async add(text) {
|
|
27595
|
+
const task = { id: randomUUID2(), text: string2().trim().min(1).parse(text), done: false };
|
|
27466
27596
|
try {
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
throw new Error("duplicate task IDs");
|
|
27470
|
-
}
|
|
27471
|
-
return list;
|
|
27597
|
+
await this.tasks.write(task);
|
|
27598
|
+
return task;
|
|
27472
27599
|
} catch (error) {
|
|
27473
|
-
|
|
27600
|
+
this.rethrowDomainError(error);
|
|
27474
27601
|
}
|
|
27475
27602
|
}
|
|
27476
|
-
async
|
|
27477
|
-
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
27603
|
+
async complete(id) {
|
|
27478
27604
|
try {
|
|
27479
|
-
await
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
}
|
|
27483
|
-
|
|
27605
|
+
return await this.tasks.update(id, (task) => {
|
|
27606
|
+
task.done = true;
|
|
27607
|
+
});
|
|
27608
|
+
} catch (error) {
|
|
27609
|
+
if (error instanceof JsonFileStoreMissingItemError)
|
|
27610
|
+
throw new Error(`Unknown task ID: ${id}`, { cause: error });
|
|
27611
|
+
this.rethrowDomainError(error);
|
|
27484
27612
|
}
|
|
27485
27613
|
}
|
|
27614
|
+
async remove(id) {
|
|
27615
|
+
let list;
|
|
27616
|
+
try {
|
|
27617
|
+
list = await this.tasks.list();
|
|
27618
|
+
} catch (error) {
|
|
27619
|
+
this.rethrowDomainError(error);
|
|
27620
|
+
}
|
|
27621
|
+
const task = list.find((item) => item.id === id);
|
|
27622
|
+
if (!task)
|
|
27623
|
+
throw new Error(`Unknown task ID: ${id}`);
|
|
27624
|
+
try {
|
|
27625
|
+
await this.tasks.replace(list.filter((item) => item.id !== id));
|
|
27626
|
+
return task;
|
|
27627
|
+
} catch (error) {
|
|
27628
|
+
this.rethrowDomainError(error);
|
|
27629
|
+
}
|
|
27630
|
+
}
|
|
27631
|
+
rethrowDomainError(error) {
|
|
27632
|
+
if (error instanceof JsonFileStoreNotFoundError) {
|
|
27633
|
+
throw new Error(`Todo list does not exist: ${this.file}. Call todo_init first.`, {
|
|
27634
|
+
cause: error
|
|
27635
|
+
});
|
|
27636
|
+
}
|
|
27637
|
+
if (error instanceof JsonFileStoreValidationError) {
|
|
27638
|
+
throw new Error(`Invalid todo list at ${this.file}`, { cause: error });
|
|
27639
|
+
}
|
|
27640
|
+
throw error;
|
|
27641
|
+
}
|
|
27486
27642
|
}
|
|
27487
|
-
var taskSchema
|
|
27643
|
+
var taskSchema;
|
|
27488
27644
|
var init_store = __esm(() => {
|
|
27489
27645
|
init_zod();
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
text: string2().min(1),
|
|
27493
|
-
done: boolean2()
|
|
27494
|
-
});
|
|
27495
|
-
listSchema = strictObject({ version: literal(1), tasks: array(taskSchema) });
|
|
27646
|
+
init_json_file_store();
|
|
27647
|
+
taskSchema = strictObject({ id: uuid2(), text: string2().min(1), done: boolean2() });
|
|
27496
27648
|
});
|
|
27497
27649
|
|
|
27498
27650
|
// src/todos/tools.ts
|
|
27499
27651
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
27500
27652
|
async function store(cwd, path) {
|
|
27501
|
-
return new TodoStore(await
|
|
27653
|
+
return new TodoStore(await resolvePath(cwd, path));
|
|
27502
27654
|
}
|
|
27503
27655
|
var cwd, path2, id, fileInput, todoTools;
|
|
27504
27656
|
var init_tools = __esm(() => {
|
|
27505
27657
|
init_zod();
|
|
27506
|
-
|
|
27658
|
+
init_pathx();
|
|
27507
27659
|
init_store();
|
|
27508
27660
|
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
27661
|
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 +27693,95 @@ var init_tools = __esm(() => {
|
|
|
27541
27693
|
];
|
|
27542
27694
|
});
|
|
27543
27695
|
|
|
27544
|
-
// src/mcp.ts
|
|
27545
|
-
function
|
|
27696
|
+
// src/mcp/server.ts
|
|
27697
|
+
async function serveMcp(version = "0.1.0") {
|
|
27546
27698
|
const server = new McpServer({ name: "difflab", version });
|
|
27547
27699
|
for (const tool of todoTools) {
|
|
27548
27700
|
server.registerTool(tool.name, tool.config, tool.callback);
|
|
27549
27701
|
}
|
|
27550
|
-
|
|
27551
|
-
}
|
|
27552
|
-
async function serveMcp(version) {
|
|
27553
|
-
await createDifflabServer(version).connect(new StdioServerTransport);
|
|
27702
|
+
await server.connect(new StdioServerTransport);
|
|
27554
27703
|
}
|
|
27555
|
-
var
|
|
27704
|
+
var init_server3 = __esm(() => {
|
|
27556
27705
|
init_mcp();
|
|
27557
27706
|
init_stdio2();
|
|
27558
27707
|
init_tools();
|
|
27559
27708
|
});
|
|
27560
27709
|
|
|
27561
|
-
// src/
|
|
27710
|
+
// src/errors.ts
|
|
27711
|
+
var DifflabError, ConfigurationError, CommandExecutionError;
|
|
27712
|
+
var init_errors4 = __esm(() => {
|
|
27713
|
+
DifflabError = class DifflabError extends Error {
|
|
27714
|
+
constructor(message, options) {
|
|
27715
|
+
super(message, options);
|
|
27716
|
+
this.name = "DifflabError";
|
|
27717
|
+
}
|
|
27718
|
+
};
|
|
27719
|
+
ConfigurationError = class ConfigurationError extends DifflabError {
|
|
27720
|
+
constructor(message, options) {
|
|
27721
|
+
super(message, options);
|
|
27722
|
+
this.name = "ConfigurationError";
|
|
27723
|
+
}
|
|
27724
|
+
};
|
|
27725
|
+
CommandExecutionError = class CommandExecutionError extends DifflabError {
|
|
27726
|
+
constructor(command, message, options) {
|
|
27727
|
+
super(`Could not configure ${command}: ${message}`, options);
|
|
27728
|
+
this.name = "CommandExecutionError";
|
|
27729
|
+
}
|
|
27730
|
+
};
|
|
27731
|
+
});
|
|
27732
|
+
|
|
27733
|
+
// src/extensions/processx.ts
|
|
27562
27734
|
import { execFileSync } from "child_process";
|
|
27563
|
-
import { homedir } from "os";
|
|
27564
|
-
function setupHome(context) {
|
|
27565
|
-
return context.home ?? homedir();
|
|
27566
|
-
}
|
|
27567
27735
|
function runCommand(command, args, cwd) {
|
|
27568
27736
|
try {
|
|
27569
27737
|
execFileSync(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
27570
27738
|
} catch (error) {
|
|
27571
|
-
throw new
|
|
27739
|
+
throw new CommandExecutionError(command, error instanceof Error ? error.message : String(error), { cause: error });
|
|
27572
27740
|
}
|
|
27573
27741
|
}
|
|
27574
|
-
|
|
27575
|
-
|
|
27742
|
+
var init_processx = __esm(() => {
|
|
27743
|
+
init_errors4();
|
|
27744
|
+
});
|
|
27745
|
+
|
|
27746
|
+
// src/agents/adapter.ts
|
|
27747
|
+
function setupHome(context) {
|
|
27748
|
+
return resolveHomeDirectory(context.home);
|
|
27576
27749
|
}
|
|
27577
27750
|
var mcpServer;
|
|
27578
27751
|
var init_adapter = __esm(() => {
|
|
27752
|
+
init_osx();
|
|
27753
|
+
init_processx();
|
|
27579
27754
|
mcpServer = {
|
|
27580
27755
|
command: "npx",
|
|
27581
27756
|
args: ["-y", "@difflab/difflab-cli", "mcp", "serve"]
|
|
27582
27757
|
};
|
|
27583
27758
|
});
|
|
27584
27759
|
|
|
27585
|
-
// src/
|
|
27586
|
-
import { randomUUID as
|
|
27587
|
-
import { lstat as lstat2, mkdir as
|
|
27588
|
-
import { dirname as
|
|
27760
|
+
// src/extensions/fsx.ts
|
|
27761
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
27762
|
+
import { lstat as lstat2, mkdir as mkdir3, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
27763
|
+
import { dirname as dirname3 } from "path";
|
|
27589
27764
|
function isObject2(value) {
|
|
27590
27765
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
27591
27766
|
}
|
|
27592
|
-
async function
|
|
27767
|
+
async function readJsonFile(file) {
|
|
27593
27768
|
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
|
-
}
|
|
27769
|
+
if ((await lstat2(file)).isSymbolicLink())
|
|
27770
|
+
throw new Error(`JSON file at ${file} is a symlink`);
|
|
27771
|
+
const parsed = JSON.parse(await readFile2(file, "utf8"));
|
|
27772
|
+
if (!isObject2(parsed))
|
|
27773
|
+
throw new Error("root must be a JSON object");
|
|
27774
|
+
return parsed;
|
|
27606
27775
|
} catch (error) {
|
|
27607
|
-
if (
|
|
27776
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
27608
27777
|
return {};
|
|
27609
27778
|
throw error;
|
|
27610
27779
|
}
|
|
27611
27780
|
}
|
|
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`;
|
|
27781
|
+
async function patchJsonToFile(file, patch) {
|
|
27782
|
+
const updated = patch(await readJsonFile(file));
|
|
27783
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
27784
|
+
const temp = `${file}.${randomUUID3()}.tmp`;
|
|
27636
27785
|
try {
|
|
27637
27786
|
await writeFile2(temp, `${JSON.stringify(updated, null, 2)}
|
|
27638
27787
|
`, { flag: "wx", mode: 384 });
|
|
@@ -27640,7 +27789,27 @@ async function addJsonServer(file) {
|
|
|
27640
27789
|
} finally {
|
|
27641
27790
|
await rm2(temp, { force: true });
|
|
27642
27791
|
}
|
|
27643
|
-
|
|
27792
|
+
}
|
|
27793
|
+
var init_fsx = __esm(() => {
|
|
27794
|
+
init_osx();
|
|
27795
|
+
});
|
|
27796
|
+
|
|
27797
|
+
// src/mcp/json-config.ts
|
|
27798
|
+
async function readJsonConfig(file) {
|
|
27799
|
+
try {
|
|
27800
|
+
return await readJsonFile(file);
|
|
27801
|
+
} catch (error) {
|
|
27802
|
+
throw new ConfigurationError(`Invalid MCP config at ${file}; not modified: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
27803
|
+
}
|
|
27804
|
+
}
|
|
27805
|
+
function serverStatus(config, file) {
|
|
27806
|
+
if (config.mcpServers !== undefined && !isObject2(config.mcpServers)) {
|
|
27807
|
+
throw new ConfigurationError(`Invalid MCP config at ${file}; mcpServers must be an object`);
|
|
27808
|
+
}
|
|
27809
|
+
if (!isObject2(config.mcpServers) || !Object.hasOwn(config.mcpServers, "difflab"))
|
|
27810
|
+
return "missing";
|
|
27811
|
+
const entry = config.mcpServers.difflab;
|
|
27812
|
+
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
27813
|
}
|
|
27645
27814
|
function jsonAgentAdapter(id, configPath) {
|
|
27646
27815
|
return {
|
|
@@ -27650,8 +27819,23 @@ function jsonAgentAdapter(id, configPath) {
|
|
|
27650
27819
|
}
|
|
27651
27820
|
};
|
|
27652
27821
|
}
|
|
27822
|
+
async function addJsonServer(file) {
|
|
27823
|
+
const config = await readJsonConfig(file);
|
|
27824
|
+
const status = serverStatus(config, file);
|
|
27825
|
+
if (status === "matching")
|
|
27826
|
+
return { file, status: "existing" };
|
|
27827
|
+
if (status === "conflicting")
|
|
27828
|
+
throw new ConfigurationError(`MCP server "difflab" already exists in ${file} with a different configuration; not modified`);
|
|
27829
|
+
await patchJsonToFile(file, (current) => ({
|
|
27830
|
+
...current,
|
|
27831
|
+
mcpServers: { ...isObject2(current.mcpServers) ? current.mcpServers : {}, difflab: mcpServer }
|
|
27832
|
+
}));
|
|
27833
|
+
return { file, status: "added" };
|
|
27834
|
+
}
|
|
27653
27835
|
var init_json_config = __esm(() => {
|
|
27654
27836
|
init_adapter();
|
|
27837
|
+
init_errors4();
|
|
27838
|
+
init_fsx();
|
|
27655
27839
|
});
|
|
27656
27840
|
|
|
27657
27841
|
// src/agents/claude-code.ts
|
|
@@ -27659,6 +27843,7 @@ import { join as join2 } from "path";
|
|
|
27659
27843
|
var claudeCodeAdapter;
|
|
27660
27844
|
var init_claude_code = __esm(() => {
|
|
27661
27845
|
init_adapter();
|
|
27846
|
+
init_errors4();
|
|
27662
27847
|
init_json_config();
|
|
27663
27848
|
claudeCodeAdapter = {
|
|
27664
27849
|
id: "claude-code",
|
|
@@ -27669,7 +27854,7 @@ var init_claude_code = __esm(() => {
|
|
|
27669
27854
|
if (status === "matching")
|
|
27670
27855
|
return { file, status: "existing" };
|
|
27671
27856
|
if (status === "conflicting") {
|
|
27672
|
-
throw new
|
|
27857
|
+
throw new ConfigurationError(`MCP server "difflab" already exists in ${file} with a different configuration; not modified`);
|
|
27673
27858
|
}
|
|
27674
27859
|
const run = context.run ?? runCommand;
|
|
27675
27860
|
run("claude", ["mcp", "add", "--scope", "user", "difflab", "--", mcpServer.command, ...mcpServer.args], home);
|
|
@@ -27698,6 +27883,8 @@ function codexServerStatus(content) {
|
|
|
27698
27883
|
var codexAdapter;
|
|
27699
27884
|
var init_codex = __esm(() => {
|
|
27700
27885
|
init_adapter();
|
|
27886
|
+
init_errors4();
|
|
27887
|
+
init_osx();
|
|
27701
27888
|
codexAdapter = {
|
|
27702
27889
|
id: "codex",
|
|
27703
27890
|
async setupMcpConfig(context = {}) {
|
|
@@ -27707,14 +27894,14 @@ var init_codex = __esm(() => {
|
|
|
27707
27894
|
try {
|
|
27708
27895
|
content = await readFile3(file, "utf8");
|
|
27709
27896
|
} catch (error) {
|
|
27710
|
-
if (!
|
|
27897
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
27711
27898
|
throw error;
|
|
27712
27899
|
}
|
|
27713
27900
|
const status = codexServerStatus(content);
|
|
27714
27901
|
if (status === "matching")
|
|
27715
27902
|
return { file, status: "existing" };
|
|
27716
27903
|
if (status === "conflicting") {
|
|
27717
|
-
throw new
|
|
27904
|
+
throw new ConfigurationError(`Codex already has a different difflab entry in ${file}; not modified`);
|
|
27718
27905
|
}
|
|
27719
27906
|
const run = context.run ?? runCommand;
|
|
27720
27907
|
run("codex", ["mcp", "add", "difflab", "--", mcpServer.command, ...mcpServer.args], home);
|
|
@@ -27754,6 +27941,33 @@ var init_agents = __esm(() => {
|
|
|
27754
27941
|
agentNames = Object.keys(agentAdapters);
|
|
27755
27942
|
});
|
|
27756
27943
|
|
|
27944
|
+
// src/mcp/setup.ts
|
|
27945
|
+
async function setupMcpClients(clients, write) {
|
|
27946
|
+
if (clients.length === 0) {
|
|
27947
|
+
throw new Error(`Select at least one client with --client (${agentNames.join(", ")})`);
|
|
27948
|
+
}
|
|
27949
|
+
const adapters = [...new Set(clients)].map((name) => {
|
|
27950
|
+
const adapter = agentAdapter(name);
|
|
27951
|
+
if (!adapter)
|
|
27952
|
+
throw new Error(`Unknown MCP client: ${name}. Choose ${agentNames.join(", ")}`);
|
|
27953
|
+
return adapter;
|
|
27954
|
+
});
|
|
27955
|
+
for (const adapter of adapters) {
|
|
27956
|
+
const { file, status } = await adapter.setupMcpConfig();
|
|
27957
|
+
write(`${status === "added" ? "Added" : "Already configured"} ${adapter.id}: ${file}`);
|
|
27958
|
+
}
|
|
27959
|
+
write("Restart or reload the selected client to use the Difflab MCP tools.");
|
|
27960
|
+
}
|
|
27961
|
+
var init_setup = __esm(() => {
|
|
27962
|
+
init_agents();
|
|
27963
|
+
});
|
|
27964
|
+
|
|
27965
|
+
// src/mcp/index.ts
|
|
27966
|
+
var init_mcp2 = __esm(() => {
|
|
27967
|
+
init_server3();
|
|
27968
|
+
init_setup();
|
|
27969
|
+
});
|
|
27970
|
+
|
|
27757
27971
|
// ../../node_modules/commander/lib/error.js
|
|
27758
27972
|
class CommanderError extends Error {
|
|
27759
27973
|
constructor(exitCode, code, message) {
|
|
@@ -29799,36 +30013,21 @@ function useColor() {
|
|
|
29799
30013
|
var program = new Command;
|
|
29800
30014
|
|
|
29801
30015
|
// src/cli.ts
|
|
29802
|
-
function greeting(name) {
|
|
29803
|
-
return `Hello, ${name}!`;
|
|
29804
|
-
}
|
|
29805
30016
|
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)
|
|
30017
|
+
const program = new Command().name("difflab-cli").description("Difflab CLI and local MCP server").version(version);
|
|
30018
|
+
program.action(() => program.outputHelp());
|
|
29807
30019
|
const mcp = program.command("mcp").description("Difflab MCP server commands");
|
|
29808
30020
|
mcp.command("serve").description("Serve Difflab tools over MCP stdio").action(async () => {
|
|
29809
30021
|
await Promise.resolve().then(() => init_mcp2());
|
|
29810
30022
|
await serveMcp(version);
|
|
29811
30023
|
});
|
|
29812
30024
|
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.");
|
|
30025
|
+
await Promise.resolve().then(() => init_mcp2());
|
|
30026
|
+
await setupMcpClients(client, write);
|
|
29828
30027
|
});
|
|
29829
30028
|
return program;
|
|
29830
30029
|
}
|
|
29831
30030
|
|
|
29832
30031
|
// src/index.ts
|
|
29833
|
-
var version2 = "0.
|
|
30032
|
+
var version2 = "0.2.0";
|
|
29834
30033
|
await createProgram(console.log, version2).parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@difflab/difflab-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A minimal Bun CLI starter built with Commander",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/difflab-io/difflab.git",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"README.md",
|
|
23
|
-
"LICENSE"
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"NOTICE"
|
|
24
25
|
],
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public",
|