midsummer-sol 0.1.1
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 +99 -0
- package/index.js +2035 -0
- package/package.json +39 -0
- package/sol-mcp.js +638 -0
- package/sol.js +3518 -0
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "midsummer-sol",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Sol — agent-native version control (a new git). CLI, MCP server, and no-filesystem SDK.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sol": "./sol.js",
|
|
7
|
+
"sol-mcp": "./sol-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"sol.js",
|
|
16
|
+
"sol-mcp.js",
|
|
17
|
+
"index.js",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"vcs",
|
|
30
|
+
"git",
|
|
31
|
+
"version-control",
|
|
32
|
+
"agent",
|
|
33
|
+
"mcp",
|
|
34
|
+
"no-filesystem"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/sol-mcp.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/bin/sol-mcp.ts
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
9
|
+
import { join as join2 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/file-store.ts
|
|
12
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
15
|
+
|
|
16
|
+
// src/attest.ts
|
|
17
|
+
function canonicalOp(op) {
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
seq: op.seq,
|
|
20
|
+
type: op.type,
|
|
21
|
+
path: op.path,
|
|
22
|
+
rootAfter: op.rootAfter,
|
|
23
|
+
by: op.by ?? null,
|
|
24
|
+
message: op.message ?? null
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/errors.ts
|
|
29
|
+
class CorruptRepoError extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(`corrupt repo: ${message}`);
|
|
32
|
+
this.name = "CorruptRepoError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class CorruptObjectError extends Error {
|
|
37
|
+
hash;
|
|
38
|
+
constructor(hash, message = "stored bytes do not hash to their content address") {
|
|
39
|
+
super(`corrupt object ${hash}: ${message}`);
|
|
40
|
+
this.hash = hash;
|
|
41
|
+
this.name = "CorruptObjectError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/store.ts
|
|
46
|
+
import { createHash } from "node:crypto";
|
|
47
|
+
function hashString(s) {
|
|
48
|
+
return "h_" + createHash("sha256").update(s, "utf8").digest("hex");
|
|
49
|
+
}
|
|
50
|
+
function hashNode(node) {
|
|
51
|
+
let canon;
|
|
52
|
+
if (node.kind === "blob") {
|
|
53
|
+
canon = node.encoding ? `blob\x00${node.encoding}\x00${node.content}` : `blob\x00${node.content}`;
|
|
54
|
+
} else if (node.kind === "sealed")
|
|
55
|
+
canon = `sealed\x00${node.box}`;
|
|
56
|
+
else
|
|
57
|
+
canon = "tree\x00" + Object.keys(node.entries).sort().map((k) => {
|
|
58
|
+
const e = node.entries[k];
|
|
59
|
+
return e.mode === undefined ? `${k}\x00${e.kind}\x00${e.hash}` : `${k}\x00${e.kind}\x00${e.hash}\x00${e.mode}`;
|
|
60
|
+
}).join(`
|
|
61
|
+
`);
|
|
62
|
+
return hashString(canon);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class Store {
|
|
66
|
+
objects = new Map;
|
|
67
|
+
put(node) {
|
|
68
|
+
const h = hashNode(node);
|
|
69
|
+
if (!this.objects.has(h))
|
|
70
|
+
this.objects.set(h, node);
|
|
71
|
+
return h;
|
|
72
|
+
}
|
|
73
|
+
get(h) {
|
|
74
|
+
return this.objects.get(h);
|
|
75
|
+
}
|
|
76
|
+
getTree(h) {
|
|
77
|
+
const n = this.get(h);
|
|
78
|
+
if (!n || n.kind !== "tree")
|
|
79
|
+
throw new Error(`not a tree: ${h}`);
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
has(h) {
|
|
83
|
+
return this.get(h) !== undefined;
|
|
84
|
+
}
|
|
85
|
+
size() {
|
|
86
|
+
return this.objects.size;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/chain.ts
|
|
91
|
+
function hashEntry(prevHash, op) {
|
|
92
|
+
return hashString(canonicalOp(op) + "\x00" + (prevHash ?? ""));
|
|
93
|
+
}
|
|
94
|
+
function chainOp(prevHash, op) {
|
|
95
|
+
return { ...op, prevHash, entryHash: hashEntry(prevHash, op) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/async-store.ts
|
|
99
|
+
async function getTree(store, hash) {
|
|
100
|
+
const node = await store.get(hash);
|
|
101
|
+
if (!node || node.kind !== "tree")
|
|
102
|
+
throw new Error(`not a tree: ${hash}`);
|
|
103
|
+
return node;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class MemoryAsyncStore {
|
|
107
|
+
nodes = new Map;
|
|
108
|
+
async put(node) {
|
|
109
|
+
const h = hashNode(node);
|
|
110
|
+
if (!this.nodes.has(h))
|
|
111
|
+
this.nodes.set(h, node);
|
|
112
|
+
return h;
|
|
113
|
+
}
|
|
114
|
+
async get(hash) {
|
|
115
|
+
return this.nodes.get(hash);
|
|
116
|
+
}
|
|
117
|
+
async has(hash) {
|
|
118
|
+
return this.nodes.has(hash);
|
|
119
|
+
}
|
|
120
|
+
get size() {
|
|
121
|
+
return this.nodes.size;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/async-tree.ts
|
|
126
|
+
function segments(path) {
|
|
127
|
+
return path.split("/").filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
async function emptyRoot(store) {
|
|
130
|
+
return store.put({ kind: "tree", entries: {} });
|
|
131
|
+
}
|
|
132
|
+
async function writeFile(store, root, path, content, opts = {}) {
|
|
133
|
+
const blob = opts.encoding ? { kind: "blob", content, encoding: opts.encoding } : { kind: "blob", content };
|
|
134
|
+
const hash = await store.put(blob);
|
|
135
|
+
return writeInto(store, root, segments(path), hash, "blob", opts.mode);
|
|
136
|
+
}
|
|
137
|
+
async function writeSealed(store, root, path, box) {
|
|
138
|
+
const hash = await store.put({ kind: "sealed", box });
|
|
139
|
+
return writeInto(store, root, segments(path), hash, "sealed");
|
|
140
|
+
}
|
|
141
|
+
async function writeInto(store, treeHash, segs, hash, leafKind, mode) {
|
|
142
|
+
const tree = await getTree(store, treeHash);
|
|
143
|
+
const entries = { ...tree.entries };
|
|
144
|
+
const [head, ...rest] = segs;
|
|
145
|
+
if (rest.length === 0) {
|
|
146
|
+
entries[head] = mode === undefined ? { kind: leafKind, hash } : { kind: leafKind, hash, mode };
|
|
147
|
+
} else {
|
|
148
|
+
const child = entries[head];
|
|
149
|
+
const childHash = child?.kind === "tree" ? child.hash : await emptyRoot(store);
|
|
150
|
+
entries[head] = { kind: "tree", hash: await writeInto(store, childHash, rest, hash, leafKind, mode) };
|
|
151
|
+
}
|
|
152
|
+
return store.put({ kind: "tree", entries });
|
|
153
|
+
}
|
|
154
|
+
async function applyBatch(store, root, changes) {
|
|
155
|
+
if (changes.size === 0)
|
|
156
|
+
return root;
|
|
157
|
+
const tree = await getTree(store, root);
|
|
158
|
+
const entries = { ...tree.entries };
|
|
159
|
+
const subdirs = new Map;
|
|
160
|
+
for (const [path, leaf] of changes) {
|
|
161
|
+
const i = path.indexOf("/");
|
|
162
|
+
if (i === -1) {
|
|
163
|
+
if (leaf === null)
|
|
164
|
+
delete entries[path];
|
|
165
|
+
else
|
|
166
|
+
entries[path] = leaf.mode === undefined ? { kind: leaf.kind, hash: leaf.hash } : { kind: leaf.kind, hash: leaf.hash, mode: leaf.mode };
|
|
167
|
+
} else {
|
|
168
|
+
const head = path.slice(0, i);
|
|
169
|
+
let m = subdirs.get(head);
|
|
170
|
+
if (!m) {
|
|
171
|
+
m = new Map;
|
|
172
|
+
subdirs.set(head, m);
|
|
173
|
+
}
|
|
174
|
+
m.set(path.slice(i + 1), leaf);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const [name, sub] of subdirs) {
|
|
178
|
+
const child = entries[name];
|
|
179
|
+
const childRoot = child && child.kind === "tree" ? child.hash : await emptyRoot(store);
|
|
180
|
+
const built = await applyBatch(store, childRoot, sub);
|
|
181
|
+
const builtTree = await getTree(store, built);
|
|
182
|
+
if (Object.keys(builtTree.entries).length === 0)
|
|
183
|
+
delete entries[name];
|
|
184
|
+
else
|
|
185
|
+
entries[name] = { kind: "tree", hash: built };
|
|
186
|
+
}
|
|
187
|
+
return store.put({ kind: "tree", entries });
|
|
188
|
+
}
|
|
189
|
+
async function deleteFile(store, root, path) {
|
|
190
|
+
return deleteInto(store, root, segments(path));
|
|
191
|
+
}
|
|
192
|
+
async function deleteInto(store, treeHash, segs) {
|
|
193
|
+
const tree = await getTree(store, treeHash);
|
|
194
|
+
const entries = { ...tree.entries };
|
|
195
|
+
const [head, ...rest] = segs;
|
|
196
|
+
if (!entries[head])
|
|
197
|
+
return treeHash;
|
|
198
|
+
if (rest.length === 0) {
|
|
199
|
+
delete entries[head];
|
|
200
|
+
} else if (entries[head].kind === "tree") {
|
|
201
|
+
entries[head] = { kind: "tree", hash: await deleteInto(store, entries[head].hash, rest) };
|
|
202
|
+
}
|
|
203
|
+
return store.put({ kind: "tree", entries });
|
|
204
|
+
}
|
|
205
|
+
async function nodeAt(store, root, path) {
|
|
206
|
+
const segs = segments(path);
|
|
207
|
+
let cur = root;
|
|
208
|
+
for (let i = 0;i < segs.length; i++) {
|
|
209
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
210
|
+
if (!entry)
|
|
211
|
+
return;
|
|
212
|
+
if (i === segs.length - 1) {
|
|
213
|
+
if (entry.kind === "tree")
|
|
214
|
+
return;
|
|
215
|
+
const node = await store.get(entry.hash);
|
|
216
|
+
if (!node)
|
|
217
|
+
return;
|
|
218
|
+
if (node.kind === "blob")
|
|
219
|
+
return { kind: "blob", content: node.content };
|
|
220
|
+
if (node.kind === "sealed")
|
|
221
|
+
return { kind: "sealed", box: node.box };
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (entry.kind !== "tree")
|
|
225
|
+
return;
|
|
226
|
+
cur = entry.hash;
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
async function fileAt(store, root, path) {
|
|
231
|
+
const segs = segments(path);
|
|
232
|
+
let cur = root;
|
|
233
|
+
for (let i = 0;i < segs.length; i++) {
|
|
234
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
235
|
+
if (!entry)
|
|
236
|
+
return;
|
|
237
|
+
if (i === segs.length - 1) {
|
|
238
|
+
if (entry.kind !== "blob")
|
|
239
|
+
return;
|
|
240
|
+
const node = await store.get(entry.hash);
|
|
241
|
+
return node && node.kind === "blob" ? node : undefined;
|
|
242
|
+
}
|
|
243
|
+
if (entry.kind !== "tree")
|
|
244
|
+
return;
|
|
245
|
+
cur = entry.hash;
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
async function modeAt(store, root, path) {
|
|
250
|
+
const segs = segments(path);
|
|
251
|
+
let cur = root;
|
|
252
|
+
for (let i = 0;i < segs.length; i++) {
|
|
253
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
254
|
+
if (!entry)
|
|
255
|
+
return;
|
|
256
|
+
if (i === segs.length - 1)
|
|
257
|
+
return entry.mode;
|
|
258
|
+
if (entry.kind !== "tree")
|
|
259
|
+
return;
|
|
260
|
+
cur = entry.hash;
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
async function listAll(store, root, prefix = "") {
|
|
265
|
+
const tree = await getTree(store, root);
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const [name, entry] of Object.entries(tree.entries)) {
|
|
268
|
+
const p = prefix ? `${prefix}/${name}` : name;
|
|
269
|
+
if (entry.kind === "tree")
|
|
270
|
+
out.push(...await listAll(store, entry.hash, p));
|
|
271
|
+
else
|
|
272
|
+
out.push(p);
|
|
273
|
+
}
|
|
274
|
+
return out.sort();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/types.ts
|
|
278
|
+
var SEALED = "<<sealed>>";
|
|
279
|
+
|
|
280
|
+
// src/async-repo.ts
|
|
281
|
+
function pageOps(ops, opts = {}) {
|
|
282
|
+
let out = opts.from !== undefined ? ops.filter((o) => o.seq >= opts.from) : ops.slice();
|
|
283
|
+
if (opts.limit !== undefined)
|
|
284
|
+
out = out.slice(0, opts.limit);
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
class AsyncRepo {
|
|
288
|
+
store;
|
|
289
|
+
log;
|
|
290
|
+
actor;
|
|
291
|
+
constructor(store, log, actor = "anon") {
|
|
292
|
+
this.store = store;
|
|
293
|
+
this.log = log;
|
|
294
|
+
this.actor = actor;
|
|
295
|
+
}
|
|
296
|
+
async currentRoot() {
|
|
297
|
+
const head = await this.log.head();
|
|
298
|
+
if (head === undefined) {
|
|
299
|
+
const seq = await this.log.seq();
|
|
300
|
+
if (seq > 0)
|
|
301
|
+
throw new CorruptRepoError(`op-log has ${seq} op(s) but no head pointer — head is lost`);
|
|
302
|
+
return emptyRoot(this.store);
|
|
303
|
+
}
|
|
304
|
+
if (!await this.store.has(head)) {
|
|
305
|
+
throw new CorruptRepoError(`head ${head} references a node missing from the store — dangling head`);
|
|
306
|
+
}
|
|
307
|
+
return head;
|
|
308
|
+
}
|
|
309
|
+
async writeFile(path, content, at = Date.now(), by = this.actor) {
|
|
310
|
+
const rootAfter = await writeFile(this.store, await this.currentRoot(), path, content);
|
|
311
|
+
await this.log.append({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by });
|
|
312
|
+
return rootAfter;
|
|
313
|
+
}
|
|
314
|
+
async applyBatch(changes) {
|
|
315
|
+
const root = await this.currentRoot();
|
|
316
|
+
const leaves = new Map;
|
|
317
|
+
for (const c of changes) {
|
|
318
|
+
if ("delete" in c) {
|
|
319
|
+
leaves.set(c.path, null);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const blob = c.encoding ? { kind: "blob", content: c.content, encoding: c.encoding } : { kind: "blob", content: c.content };
|
|
323
|
+
const hash = await this.store.put(blob);
|
|
324
|
+
leaves.set(c.path, { kind: "blob", hash, mode: c.mode });
|
|
325
|
+
}
|
|
326
|
+
return { root: await applyBatch(this.store, root, leaves), changed: changes.length };
|
|
327
|
+
}
|
|
328
|
+
async writeBytes(path, data, at = Date.now(), by = this.actor) {
|
|
329
|
+
const rootAfter = await writeFile(this.store, await this.currentRoot(), path, Buffer.from(data).toString("base64"), { encoding: "base64" });
|
|
330
|
+
await this.log.append({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by });
|
|
331
|
+
return rootAfter;
|
|
332
|
+
}
|
|
333
|
+
async readBytes(path) {
|
|
334
|
+
const leaf = await this.read(path);
|
|
335
|
+
if (!leaf)
|
|
336
|
+
return;
|
|
337
|
+
if (leaf.kind === "sealed")
|
|
338
|
+
return SEALED;
|
|
339
|
+
const f = await fileAt(this.store, await this.currentRoot(), path);
|
|
340
|
+
if (!f)
|
|
341
|
+
return;
|
|
342
|
+
return new Uint8Array(Buffer.from(f.content, f.encoding === "base64" ? "base64" : "utf8"));
|
|
343
|
+
}
|
|
344
|
+
async chmod(path, mode, at = Date.now()) {
|
|
345
|
+
const f = await fileAt(this.store, await this.currentRoot(), path);
|
|
346
|
+
if (!f)
|
|
347
|
+
return this.currentRoot();
|
|
348
|
+
const rootAfter = await writeFile(this.store, await this.currentRoot(), path, f.content, { encoding: f.encoding, mode });
|
|
349
|
+
await this.log.append({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by: this.actor });
|
|
350
|
+
return rootAfter;
|
|
351
|
+
}
|
|
352
|
+
async mode(path) {
|
|
353
|
+
return modeAt(this.store, await this.currentRoot(), path);
|
|
354
|
+
}
|
|
355
|
+
async writeSealed(path, box, at = Date.now()) {
|
|
356
|
+
const rootAfter = await writeSealed(this.store, await this.currentRoot(), path, box);
|
|
357
|
+
await this.log.append({ seq: await this.log.seq() + 1, type: "seal", path, rootAfter, at, by: this.actor });
|
|
358
|
+
return rootAfter;
|
|
359
|
+
}
|
|
360
|
+
async read(path) {
|
|
361
|
+
return nodeAt(this.store, await this.currentRoot(), path);
|
|
362
|
+
}
|
|
363
|
+
async deleteFile(path, at = Date.now(), by = this.actor) {
|
|
364
|
+
const rootAfter = await deleteFile(this.store, await this.currentRoot(), path);
|
|
365
|
+
await this.log.append({ seq: await this.log.seq() + 1, type: "delete", path, rootAfter, at, by });
|
|
366
|
+
return rootAfter;
|
|
367
|
+
}
|
|
368
|
+
async readFile(path) {
|
|
369
|
+
const leaf = await this.read(path);
|
|
370
|
+
if (!leaf)
|
|
371
|
+
return;
|
|
372
|
+
return leaf.kind === "sealed" ? SEALED : leaf.content;
|
|
373
|
+
}
|
|
374
|
+
async list() {
|
|
375
|
+
return listAll(this.store, await this.currentRoot());
|
|
376
|
+
}
|
|
377
|
+
async head() {
|
|
378
|
+
return this.currentRoot();
|
|
379
|
+
}
|
|
380
|
+
async history(opts) {
|
|
381
|
+
return this.log.history(opts);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/file-store.ts
|
|
386
|
+
function encodeObject(node) {
|
|
387
|
+
return gzipSync(JSON.stringify(node), { level: 1 });
|
|
388
|
+
}
|
|
389
|
+
function decodeObject(raw) {
|
|
390
|
+
const json = raw.length >= 2 && raw[0] === 31 && raw[1] === 139 ? gunzipSync(raw).toString("utf8") : raw.toString("utf8");
|
|
391
|
+
return JSON.parse(json);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
class FileStore {
|
|
395
|
+
objects;
|
|
396
|
+
constructor(solDir) {
|
|
397
|
+
this.objects = join(solDir, "objects");
|
|
398
|
+
mkdirSync(this.objects, { recursive: true });
|
|
399
|
+
}
|
|
400
|
+
path(hash) {
|
|
401
|
+
return join(this.objects, hash);
|
|
402
|
+
}
|
|
403
|
+
async put(node) {
|
|
404
|
+
const h = hashNode(node);
|
|
405
|
+
const p = this.path(h);
|
|
406
|
+
if (!existsSync(p)) {
|
|
407
|
+
const tmp = `${p}.tmp`;
|
|
408
|
+
writeFileSync(tmp, encodeObject(node));
|
|
409
|
+
renameSync(tmp, p);
|
|
410
|
+
}
|
|
411
|
+
return h;
|
|
412
|
+
}
|
|
413
|
+
async get(hash) {
|
|
414
|
+
const p = this.path(hash);
|
|
415
|
+
if (!existsSync(p))
|
|
416
|
+
return;
|
|
417
|
+
const node = decodeObject(readFileSync(p));
|
|
418
|
+
if (hashNode(node) !== hash)
|
|
419
|
+
throw new CorruptObjectError(hash);
|
|
420
|
+
return node;
|
|
421
|
+
}
|
|
422
|
+
async has(hash) {
|
|
423
|
+
return existsSync(this.path(hash));
|
|
424
|
+
}
|
|
425
|
+
count() {
|
|
426
|
+
return existsSync(this.objects) ? readdirSync(this.objects).length : 0;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
class FileOpLog {
|
|
431
|
+
opsFile;
|
|
432
|
+
headFile;
|
|
433
|
+
constructor(solDir) {
|
|
434
|
+
this.opsFile = join(solDir, "ops.jsonl");
|
|
435
|
+
this.headFile = join(solDir, "HEAD");
|
|
436
|
+
}
|
|
437
|
+
readHead() {
|
|
438
|
+
return existsSync(this.headFile) ? JSON.parse(readFileSync(this.headFile, "utf8")) : { seq: 0 };
|
|
439
|
+
}
|
|
440
|
+
async head() {
|
|
441
|
+
return this.readHead().head;
|
|
442
|
+
}
|
|
443
|
+
async seq() {
|
|
444
|
+
return this.readHead().seq ?? 0;
|
|
445
|
+
}
|
|
446
|
+
async logTip() {
|
|
447
|
+
return this.readHead().logTip;
|
|
448
|
+
}
|
|
449
|
+
async append(entry) {
|
|
450
|
+
const chained = chainOp(this.readHead().logTip, entry);
|
|
451
|
+
appendFileSync(this.opsFile, JSON.stringify(chained) + `
|
|
452
|
+
`);
|
|
453
|
+
writeFileSync(this.headFile, JSON.stringify({ head: chained.rootAfter, seq: chained.seq, logTip: chained.entryHash }));
|
|
454
|
+
}
|
|
455
|
+
async history(opts) {
|
|
456
|
+
if (!existsSync(this.opsFile))
|
|
457
|
+
return [];
|
|
458
|
+
const all = [];
|
|
459
|
+
for (const l of readFileSync(this.opsFile, "utf8").split(`
|
|
460
|
+
`)) {
|
|
461
|
+
if (!l)
|
|
462
|
+
continue;
|
|
463
|
+
try {
|
|
464
|
+
all.push(JSON.parse(l));
|
|
465
|
+
} catch {
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return pageOps(all, opts);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/workspace.ts
|
|
474
|
+
function safePath(path) {
|
|
475
|
+
const p = path.replace(/^\.[\\/]+/, "").trim();
|
|
476
|
+
if (!p)
|
|
477
|
+
throw new Error("path: empty");
|
|
478
|
+
if (p.startsWith("/") || p.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
479
|
+
throw new Error(`path: absolute paths are not allowed: ${path}`);
|
|
480
|
+
if (p.split(/[\\/]/).some((s) => s === ".."))
|
|
481
|
+
throw new Error(`path: '..' escapes the workspace: ${path}`);
|
|
482
|
+
return p;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
class SolWorkspace {
|
|
486
|
+
store;
|
|
487
|
+
log;
|
|
488
|
+
actor;
|
|
489
|
+
repo;
|
|
490
|
+
constructor(store, log, actor = "agent") {
|
|
491
|
+
this.store = store;
|
|
492
|
+
this.log = log;
|
|
493
|
+
this.actor = actor;
|
|
494
|
+
this.repo = new AsyncRepo(store, log, actor);
|
|
495
|
+
}
|
|
496
|
+
async write(path, content) {
|
|
497
|
+
return this.repo.writeFile(safePath(path), content);
|
|
498
|
+
}
|
|
499
|
+
async writeBytes(path, bytes) {
|
|
500
|
+
return this.repo.writeBytes(safePath(path), bytes);
|
|
501
|
+
}
|
|
502
|
+
async read(path) {
|
|
503
|
+
const c = await this.repo.readFile(path);
|
|
504
|
+
return c === undefined || c === SEALED ? undefined : c;
|
|
505
|
+
}
|
|
506
|
+
async readBytes(path) {
|
|
507
|
+
const b = await this.repo.readBytes(path);
|
|
508
|
+
return b === undefined || b === SEALED ? undefined : b;
|
|
509
|
+
}
|
|
510
|
+
async edit(path, oldStr, newStr, opts = {}) {
|
|
511
|
+
path = safePath(path);
|
|
512
|
+
const cur = await this.repo.readFile(path);
|
|
513
|
+
if (cur === undefined)
|
|
514
|
+
throw new Error(`edit: no such file: ${path}`);
|
|
515
|
+
if (cur === SEALED)
|
|
516
|
+
throw new Error(`edit: cannot text-edit a sealed file: ${path}`);
|
|
517
|
+
const occurrences = cur.split(oldStr).length - 1;
|
|
518
|
+
if (occurrences === 0)
|
|
519
|
+
throw new Error(`edit: text not found in ${path}`);
|
|
520
|
+
if (occurrences > 1 && !opts.all)
|
|
521
|
+
throw new Error(`edit: text appears ${occurrences}x in ${path} — add surrounding context or pass all:true`);
|
|
522
|
+
await this.repo.writeFile(path, opts.all ? cur.split(oldStr).join(newStr) : cur.replace(oldStr, newStr));
|
|
523
|
+
}
|
|
524
|
+
async ls(prefix) {
|
|
525
|
+
const all = await this.repo.list();
|
|
526
|
+
if (!prefix)
|
|
527
|
+
return all;
|
|
528
|
+
const dir = prefix.endsWith("/") ? prefix : prefix + "/";
|
|
529
|
+
return all.filter((p) => p === prefix || p.startsWith(dir));
|
|
530
|
+
}
|
|
531
|
+
async exists(path) {
|
|
532
|
+
return (await this.repo.list()).includes(path);
|
|
533
|
+
}
|
|
534
|
+
async isSealed(path) {
|
|
535
|
+
return await this.repo.readFile(path) === SEALED;
|
|
536
|
+
}
|
|
537
|
+
async move(from, to) {
|
|
538
|
+
const c = await this.repo.readFile(from);
|
|
539
|
+
if (c === undefined)
|
|
540
|
+
throw new Error(`move: no such file: ${from}`);
|
|
541
|
+
if (c === SEALED)
|
|
542
|
+
throw new Error(`move: cannot move a sealed file via the text API: ${from}`);
|
|
543
|
+
await this.repo.writeFile(safePath(to), c);
|
|
544
|
+
await this.repo.deleteFile(from);
|
|
545
|
+
}
|
|
546
|
+
remove(path) {
|
|
547
|
+
return this.repo.deleteFile(path);
|
|
548
|
+
}
|
|
549
|
+
async commit(message) {
|
|
550
|
+
const head = await this.repo.head();
|
|
551
|
+
const hist = await this.log.history();
|
|
552
|
+
let parent;
|
|
553
|
+
for (let i = hist.length - 1;i >= 0; i--) {
|
|
554
|
+
if (hist[i].type === "checkpoint") {
|
|
555
|
+
parent = hist[i].rootAfter;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const entry = { seq: await this.log.seq() + 1, type: "checkpoint", path: "", rootAfter: head, at: Date.now(), by: this.actor, message, ...parent && parent !== head ? { parent } : {} };
|
|
560
|
+
await this.log.append(entry);
|
|
561
|
+
return head;
|
|
562
|
+
}
|
|
563
|
+
checkpoint(message) {
|
|
564
|
+
return this.commit(message);
|
|
565
|
+
}
|
|
566
|
+
head() {
|
|
567
|
+
return this.repo.head();
|
|
568
|
+
}
|
|
569
|
+
history(opts) {
|
|
570
|
+
return this.repo.history(opts);
|
|
571
|
+
}
|
|
572
|
+
get raw() {
|
|
573
|
+
return this.repo;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/bin/sol-mcp.ts
|
|
578
|
+
var solDir = process.env.SOL_DIR || join2(process.cwd(), ".sol");
|
|
579
|
+
mkdirSync2(solDir, { recursive: true });
|
|
580
|
+
var ws = new SolWorkspace(new FileStore(solDir), new FileOpLog(solDir), process.env.SOL_ACTOR || "agent");
|
|
581
|
+
var tools = [
|
|
582
|
+
{ name: "sol_write", description: "Create or overwrite a text file in the sol workspace. Authoring goes here \u2014 not to a disk.", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
|
|
583
|
+
{ name: "sol_read", description: "Read a text file from the sol workspace.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
584
|
+
{ name: "sol_edit", description: "Replace a unique snippet in a file (set all=true to replace every occurrence).", inputSchema: { type: "object", properties: { path: { type: "string" }, old_str: { type: "string" }, new_str: { type: "string" }, all: { type: "boolean" } }, required: ["path", "old_str", "new_str"] } },
|
|
585
|
+
{ name: "sol_ls", description: "List tracked files, optionally scoped to a directory prefix.", inputSchema: { type: "object", properties: { prefix: { type: "string" } } } },
|
|
586
|
+
{ name: "sol_move", description: "Move or rename a file.", inputSchema: { type: "object", properties: { from: { type: "string" }, to: { type: "string" } }, required: ["from", "to"] } },
|
|
587
|
+
{ name: "sol_rm", description: "Delete (untrack) a file.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
588
|
+
{ name: "sol_commit", description: "Record a commit (a shareable milestone) over the current state.", inputSchema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] } },
|
|
589
|
+
{ name: "sol_history", description: "Show the authoring history (the op-log).", inputSchema: { type: "object", properties: {} } }
|
|
590
|
+
];
|
|
591
|
+
var text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
592
|
+
async function handle(name, a) {
|
|
593
|
+
switch (name) {
|
|
594
|
+
case "sol_write":
|
|
595
|
+
await ws.write(a.path, a.content);
|
|
596
|
+
return text(`wrote ${a.path} (${a.content.length} chars)`);
|
|
597
|
+
case "sol_read": {
|
|
598
|
+
const c = await ws.read(a.path);
|
|
599
|
+
return c === undefined ? text(`(absent: ${a.path})`) : text(c);
|
|
600
|
+
}
|
|
601
|
+
case "sol_edit":
|
|
602
|
+
await ws.edit(a.path, a.old_str, a.new_str, { all: a.all });
|
|
603
|
+
return text(`edited ${a.path}`);
|
|
604
|
+
case "sol_ls": {
|
|
605
|
+
const f = await ws.ls(a.prefix);
|
|
606
|
+
return text(f.length ? f.join(`
|
|
607
|
+
`) : "(no files yet)");
|
|
608
|
+
}
|
|
609
|
+
case "sol_move":
|
|
610
|
+
await ws.move(a.from, a.to);
|
|
611
|
+
return text(`moved ${a.from} -> ${a.to}`);
|
|
612
|
+
case "sol_rm":
|
|
613
|
+
await ws.remove(a.path);
|
|
614
|
+
return text(`removed ${a.path}`);
|
|
615
|
+
case "sol_commit":
|
|
616
|
+
case "sol_checkpoint": {
|
|
617
|
+
const h = await ws.commit(a.message);
|
|
618
|
+
return text(`commit ${h.slice(0, 14)} \u2014 ${a.message}`);
|
|
619
|
+
}
|
|
620
|
+
case "sol_history": {
|
|
621
|
+
const ops = await ws.history();
|
|
622
|
+
return text(ops.map((o) => `${o.seq} ${o.type} ${o.path || ""}${o.message ? " : " + o.message : ""}`).join(`
|
|
623
|
+
`) || "(empty)");
|
|
624
|
+
}
|
|
625
|
+
default:
|
|
626
|
+
throw new Error("unknown tool: " + name);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
var server = new Server({ name: "sol", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
630
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
631
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
632
|
+
try {
|
|
633
|
+
return await handle(req.params.name, req.params.arguments ?? {});
|
|
634
|
+
} catch (e) {
|
|
635
|
+
return { content: [{ type: "text", text: "sol error: " + (e?.message ?? e) }], isError: true };
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
await server.connect(new StdioServerTransport);
|