midsummer-sol 0.1.3 → 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/index.js +818 -51
- package/package.json +9 -33
- package/sol-mcp.js +373 -28
- package/sol-secret-mcp.js +3250 -0
- package/sol.js +9210 -1957
package/package.json
CHANGED
|
@@ -1,39 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "midsummer-sol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
|
-
},
|
|
5
|
+
"bin": { "sol": "./sol.js", "sol-mcp": "./sol-mcp.js", "sol-secret-mcp": "./sol-secret-mcp.js" },
|
|
9
6
|
"main": "./index.js",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": "./index.js"
|
|
12
|
-
},
|
|
7
|
+
"exports": { ".": "./index.js" },
|
|
13
8
|
"type": "module",
|
|
14
|
-
"files": [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"index.js",
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
23
|
-
},
|
|
24
|
-
"engines": {
|
|
25
|
-
"node": ">=18"
|
|
26
|
-
},
|
|
9
|
+
"files": ["sol.js", "sol-mcp.js", "sol-secret-mcp.js", "index.js", "README.md", "LICENSE"],
|
|
10
|
+
"dependencies": { "@modelcontextprotocol/sdk": "^1.29.0" },
|
|
11
|
+
"engines": { "node": ">=18" },
|
|
27
12
|
"license": "Apache-2.0",
|
|
28
|
-
"keywords": [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"version-control",
|
|
32
|
-
"agent",
|
|
33
|
-
"mcp",
|
|
34
|
-
"no-filesystem"
|
|
35
|
-
],
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
13
|
+
"keywords": ["vcs", "git", "version-control", "agent", "mcp", "no-filesystem"],
|
|
14
|
+
"publishConfig": { "access": "public" }
|
|
15
|
+
}
|
package/sol-mcp.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
5
|
|
|
4
6
|
// 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
7
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
9
8
|
import { join as join2 } from "path";
|
|
10
9
|
|
|
@@ -21,7 +20,9 @@ function canonicalOp(op) {
|
|
|
21
20
|
path: op.path,
|
|
22
21
|
rootAfter: op.rootAfter,
|
|
23
22
|
by: op.by ?? null,
|
|
24
|
-
message: op.message ?? null
|
|
23
|
+
message: op.message ?? null,
|
|
24
|
+
...op.kind !== undefined ? { kind: op.kind } : {},
|
|
25
|
+
...op.prov !== undefined ? { prov: op.prov } : {}
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -53,7 +54,10 @@ function hashNode(node) {
|
|
|
53
54
|
canon = node.encoding ? `blob\x00${node.encoding}\x00${node.content}` : `blob\x00${node.content}`;
|
|
54
55
|
} else if (node.kind === "sealed")
|
|
55
56
|
canon = `sealed\x00${node.box}`;
|
|
56
|
-
else
|
|
57
|
+
else if (node.kind === "prov") {
|
|
58
|
+
const f = (k, v) => v === undefined ? "" : `\x00${k}\x00${v}`;
|
|
59
|
+
canon = "prov" + f("agentId", node.agentId) + f("sessionId", node.sessionId) + f("model", node.model) + f("intent", node.intent) + f("tool", node.tool) + f("rationale", node.rationale) + f("parent", node.parent);
|
|
60
|
+
} else
|
|
57
61
|
canon = "tree\x00" + Object.keys(node.entries).sort().map((k) => {
|
|
58
62
|
const e = node.entries[k];
|
|
59
63
|
return e.mode === undefined ? `${k}\x00${e.kind}\x00${e.hash}` : `${k}\x00${e.kind}\x00${e.hash}\x00${e.mode}`;
|
|
@@ -122,9 +126,128 @@ class MemoryAsyncStore {
|
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
// src/struct.ts
|
|
130
|
+
import { randomBytes } from "node:crypto";
|
|
131
|
+
var SLOT_PREFIX = "\x00slot\x00";
|
|
132
|
+
var HIDDEN_KEY = "\x00hidden";
|
|
133
|
+
function isReservedKey(key) {
|
|
134
|
+
return key.startsWith("\x00");
|
|
135
|
+
}
|
|
136
|
+
function slotKey(slotId) {
|
|
137
|
+
return SLOT_PREFIX + slotId;
|
|
138
|
+
}
|
|
139
|
+
function slotIdFromKey(key) {
|
|
140
|
+
return key.startsWith(SLOT_PREFIX) ? key.slice(SLOT_PREFIX.length) : undefined;
|
|
141
|
+
}
|
|
142
|
+
function mintSlotId() {
|
|
143
|
+
return randomBytes(16).toString("base64url");
|
|
144
|
+
}
|
|
145
|
+
var STRUCT_HEADER = "__solStruct";
|
|
146
|
+
function parseStruct(plaintext) {
|
|
147
|
+
if (!plaintext.startsWith("{"))
|
|
148
|
+
return;
|
|
149
|
+
let v;
|
|
150
|
+
try {
|
|
151
|
+
v = JSON.parse(plaintext);
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!v || typeof v !== "object")
|
|
156
|
+
return;
|
|
157
|
+
const tag = v[STRUCT_HEADER];
|
|
158
|
+
if (tag === "subtree" || tag === "name" || tag === "entries")
|
|
159
|
+
return v;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
function nextBucket(n) {
|
|
163
|
+
let b = 256;
|
|
164
|
+
while (b < n)
|
|
165
|
+
b *= 2;
|
|
166
|
+
return b;
|
|
167
|
+
}
|
|
168
|
+
function padPlaintext(p) {
|
|
169
|
+
const bare = JSON.stringify(p);
|
|
170
|
+
const target = nextBucket(bare.length);
|
|
171
|
+
if (bare.length >= target)
|
|
172
|
+
return bare;
|
|
173
|
+
const overhead = `,"${"__pad"}":""`.length;
|
|
174
|
+
const fill = Math.max(0, target - bare.length - overhead);
|
|
175
|
+
return JSON.stringify({ ...p, __pad: " ".repeat(fill) });
|
|
176
|
+
}
|
|
177
|
+
async function collectAsync(store, hash, into) {
|
|
178
|
+
if (into[hash])
|
|
179
|
+
return;
|
|
180
|
+
const node = await store.get(hash);
|
|
181
|
+
if (!node)
|
|
182
|
+
throw new Error(`sealSubtree: node missing from store: ${hash}`);
|
|
183
|
+
into[hash] = node;
|
|
184
|
+
if (node.kind === "tree")
|
|
185
|
+
for (const e of Object.values(node.entries))
|
|
186
|
+
await collectAsync(store, e.hash, into);
|
|
187
|
+
}
|
|
188
|
+
async function serializeSubtreeAsync(store, dirRoot) {
|
|
189
|
+
const nodes = {};
|
|
190
|
+
await collectAsync(store, dirRoot, nodes);
|
|
191
|
+
return padPlaintext({ __solStruct: "subtree", bundle: { root: dirRoot, nodes } });
|
|
192
|
+
}
|
|
193
|
+
async function subtreeRootAt(store, root, dirPath) {
|
|
194
|
+
const segs = dirPath.split("/").filter(Boolean);
|
|
195
|
+
let cur = root;
|
|
196
|
+
for (let i = 0;i < segs.length; i++) {
|
|
197
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
198
|
+
if (!entry || entry.kind !== "tree")
|
|
199
|
+
return;
|
|
200
|
+
cur = entry.hash;
|
|
201
|
+
}
|
|
202
|
+
return cur;
|
|
203
|
+
}
|
|
204
|
+
async function entryAt(store, root, path) {
|
|
205
|
+
const segs = path.split("/").filter(Boolean);
|
|
206
|
+
let cur = root;
|
|
207
|
+
for (let i = 0;i < segs.length; i++) {
|
|
208
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
209
|
+
if (!entry)
|
|
210
|
+
return;
|
|
211
|
+
if (i === segs.length - 1)
|
|
212
|
+
return entry;
|
|
213
|
+
if (entry.kind !== "tree")
|
|
214
|
+
return;
|
|
215
|
+
cur = entry.hash;
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
async function serializeNameAsync(store, name, entry) {
|
|
220
|
+
const nodes = {};
|
|
221
|
+
await collectAsync(store, entry.hash, nodes);
|
|
222
|
+
const payload = {
|
|
223
|
+
__solStruct: "name",
|
|
224
|
+
name,
|
|
225
|
+
entryKind: entry.kind,
|
|
226
|
+
...entry.mode === undefined ? {} : { entryMode: entry.mode },
|
|
227
|
+
bundle: { root: entry.hash, nodes }
|
|
228
|
+
};
|
|
229
|
+
return padPlaintext(payload);
|
|
230
|
+
}
|
|
231
|
+
async function hiddenEntryAsync(store, entry) {
|
|
232
|
+
const nodes = {};
|
|
233
|
+
await collectAsync(store, entry.hash, nodes);
|
|
234
|
+
return { entryKind: entry.kind, ...entry.mode === undefined ? {} : { entryMode: entry.mode }, bundle: { root: entry.hash, nodes } };
|
|
235
|
+
}
|
|
236
|
+
function serializeHiddenEntries(slots) {
|
|
237
|
+
return padPlaintext({ __solStruct: "entries", slots });
|
|
238
|
+
}
|
|
239
|
+
|
|
125
240
|
// src/async-tree.ts
|
|
126
241
|
function segments(path) {
|
|
127
|
-
|
|
242
|
+
const segs = path.split("/").filter(Boolean);
|
|
243
|
+
for (const s of segs) {
|
|
244
|
+
if (!s.includes("\x00"))
|
|
245
|
+
continue;
|
|
246
|
+
const reserved = s.startsWith("\x00slot\x00") ? s.slice("\x00slot\x00".length) : s === "\x00hidden" ? "" : undefined;
|
|
247
|
+
if (reserved === undefined || reserved.includes("\x00"))
|
|
248
|
+
throw new Error("illegal NUL byte in path");
|
|
249
|
+
}
|
|
250
|
+
return segs;
|
|
128
251
|
}
|
|
129
252
|
async function emptyRoot(store) {
|
|
130
253
|
return store.put({ kind: "tree", entries: {} });
|
|
@@ -138,6 +261,70 @@ async function writeSealed(store, root, path, box) {
|
|
|
138
261
|
const hash = await store.put({ kind: "sealed", box });
|
|
139
262
|
return writeInto(store, root, segments(path), hash, "sealed");
|
|
140
263
|
}
|
|
264
|
+
async function hideName(store, root, path, box, slotId) {
|
|
265
|
+
const hash = await store.put({ kind: "sealed", box });
|
|
266
|
+
return hideInto(store, root, segments(path), hash, slotKey(slotId));
|
|
267
|
+
}
|
|
268
|
+
async function reHideName(store, root, dirSegs, key, box) {
|
|
269
|
+
const hash = await store.put({ kind: "sealed", box });
|
|
270
|
+
if (dirSegs.length === 0) {
|
|
271
|
+
const tree2 = await getTree(store, root);
|
|
272
|
+
return store.put({ kind: "tree", entries: { ...tree2.entries, [key]: { kind: "sealed", hash } } });
|
|
273
|
+
}
|
|
274
|
+
const tree = await getTree(store, root);
|
|
275
|
+
const entries = { ...tree.entries };
|
|
276
|
+
const [head, ...rest] = dirSegs;
|
|
277
|
+
const child = entries[head];
|
|
278
|
+
if (!child || child.kind !== "tree")
|
|
279
|
+
return root;
|
|
280
|
+
entries[head] = { kind: "tree", hash: await reHideName(store, child.hash, rest, key, box) };
|
|
281
|
+
return store.put({ kind: "tree", entries });
|
|
282
|
+
}
|
|
283
|
+
async function hideInto(store, treeHash, segs, sealedHash, key) {
|
|
284
|
+
const tree = await getTree(store, treeHash);
|
|
285
|
+
const entries = { ...tree.entries };
|
|
286
|
+
const [head, ...rest] = segs;
|
|
287
|
+
if (rest.length === 0) {
|
|
288
|
+
delete entries[head];
|
|
289
|
+
entries[key] = { kind: "sealed", hash: sealedHash };
|
|
290
|
+
} else {
|
|
291
|
+
const child = entries[head];
|
|
292
|
+
if (!child || child.kind !== "tree")
|
|
293
|
+
return treeHash;
|
|
294
|
+
entries[head] = { kind: "tree", hash: await hideInto(store, child.hash, rest, sealedHash, key) };
|
|
295
|
+
}
|
|
296
|
+
return store.put({ kind: "tree", entries });
|
|
297
|
+
}
|
|
298
|
+
async function hideExistence(store, root, path, hiddenBox) {
|
|
299
|
+
const hash = await store.put({ kind: "sealed", box: hiddenBox });
|
|
300
|
+
return hideExistenceInto(store, root, segments(path), hash);
|
|
301
|
+
}
|
|
302
|
+
async function hideExistenceInto(store, treeHash, segs, sidecarHash) {
|
|
303
|
+
const tree = await getTree(store, treeHash);
|
|
304
|
+
const entries = { ...tree.entries };
|
|
305
|
+
const [head, ...rest] = segs;
|
|
306
|
+
if (rest.length === 0) {
|
|
307
|
+
delete entries[head];
|
|
308
|
+
entries[HIDDEN_KEY] = { kind: "sealed", hash: sidecarHash };
|
|
309
|
+
} else {
|
|
310
|
+
const child = entries[head];
|
|
311
|
+
if (!child || child.kind !== "tree")
|
|
312
|
+
return treeHash;
|
|
313
|
+
entries[head] = { kind: "tree", hash: await hideExistenceInto(store, child.hash, rest, sidecarHash) };
|
|
314
|
+
}
|
|
315
|
+
return store.put({ kind: "tree", entries });
|
|
316
|
+
}
|
|
317
|
+
async function dirEntriesAt(store, root, dirPath) {
|
|
318
|
+
const segs = segments(dirPath);
|
|
319
|
+
let cur = root;
|
|
320
|
+
for (const seg of segs) {
|
|
321
|
+
const entry = (await getTree(store, cur)).entries[seg];
|
|
322
|
+
if (!entry || entry.kind !== "tree")
|
|
323
|
+
return;
|
|
324
|
+
cur = entry.hash;
|
|
325
|
+
}
|
|
326
|
+
return (await getTree(store, cur)).entries;
|
|
327
|
+
}
|
|
141
328
|
async function writeInto(store, treeHash, segs, hash, leafKind, mode) {
|
|
142
329
|
const tree = await getTree(store, treeHash);
|
|
143
330
|
const entries = { ...tree.entries };
|
|
@@ -265,6 +452,10 @@ async function listAll(store, root, prefix = "") {
|
|
|
265
452
|
const tree = await getTree(store, root);
|
|
266
453
|
const out = [];
|
|
267
454
|
for (const [name, entry] of Object.entries(tree.entries)) {
|
|
455
|
+
if (isReservedKey(name)) {
|
|
456
|
+
out.push(prefix ? `${prefix}/${name}` : name);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
268
459
|
const p = prefix ? `${prefix}/${name}` : name;
|
|
269
460
|
if (entry.kind === "tree")
|
|
270
461
|
out.push(...await listAll(store, entry.hash, p));
|
|
@@ -274,6 +465,43 @@ async function listAll(store, root, prefix = "") {
|
|
|
274
465
|
return out.sort();
|
|
275
466
|
}
|
|
276
467
|
|
|
468
|
+
// src/sign.ts
|
|
469
|
+
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
470
|
+
|
|
471
|
+
// src/prov.ts
|
|
472
|
+
function provenanceFromEnv(env = process.env) {
|
|
473
|
+
const kind = env.SOL_KIND === "agent" ? "agent" : "human";
|
|
474
|
+
const node = buildProvNode({
|
|
475
|
+
agentId: env.SOL_AGENT_ID || (kind === "agent" ? env.SOL_ACTOR : undefined),
|
|
476
|
+
sessionId: env.SOL_SESSION,
|
|
477
|
+
model: env.SOL_MODEL,
|
|
478
|
+
intent: env.SOL_INTENT,
|
|
479
|
+
tool: env.SOL_TOOL
|
|
480
|
+
});
|
|
481
|
+
return node ? { kind, node } : { kind };
|
|
482
|
+
}
|
|
483
|
+
function buildProvNode(fields) {
|
|
484
|
+
const clean = { kind: "prov" };
|
|
485
|
+
let any = false;
|
|
486
|
+
for (const k of ["agentId", "sessionId", "model", "intent", "tool", "rationale", "parent"]) {
|
|
487
|
+
const v = fields[k];
|
|
488
|
+
if (v) {
|
|
489
|
+
clean[k] = v;
|
|
490
|
+
any = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return any ? clean : undefined;
|
|
494
|
+
}
|
|
495
|
+
async function captureProv(sink, env = process.env) {
|
|
496
|
+
const { kind, node } = provenanceFromEnv(env);
|
|
497
|
+
const out = {};
|
|
498
|
+
if (kind === "agent")
|
|
499
|
+
out.kind = "agent";
|
|
500
|
+
if (node)
|
|
501
|
+
out.prov = await sink.put(node);
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
277
505
|
// src/types.ts
|
|
278
506
|
var SEALED = "<<sealed>>";
|
|
279
507
|
|
|
@@ -284,14 +512,27 @@ function pageOps(ops, opts = {}) {
|
|
|
284
512
|
out = out.slice(0, opts.limit);
|
|
285
513
|
return out;
|
|
286
514
|
}
|
|
515
|
+
var noSign = (op) => op;
|
|
516
|
+
|
|
287
517
|
class AsyncRepo {
|
|
288
518
|
store;
|
|
289
519
|
log;
|
|
290
520
|
actor;
|
|
291
|
-
|
|
521
|
+
sign;
|
|
522
|
+
constructor(store, log, actor = "anon", sign = noSign) {
|
|
292
523
|
this.store = store;
|
|
293
524
|
this.log = log;
|
|
294
525
|
this.actor = actor;
|
|
526
|
+
this.sign = sign;
|
|
527
|
+
}
|
|
528
|
+
async appendAuthored(entry, by) {
|
|
529
|
+
await this.log.append(by === this.actor ? this.sign(entry) : entry);
|
|
530
|
+
}
|
|
531
|
+
provFor;
|
|
532
|
+
async provenance(by) {
|
|
533
|
+
if (by !== this.actor)
|
|
534
|
+
return {};
|
|
535
|
+
return this.provFor ??= captureProv(this.store);
|
|
295
536
|
}
|
|
296
537
|
async currentRoot() {
|
|
297
538
|
const head = await this.log.head();
|
|
@@ -308,7 +549,7 @@ class AsyncRepo {
|
|
|
308
549
|
}
|
|
309
550
|
async writeFile(path, content, at = Date.now(), by = this.actor) {
|
|
310
551
|
const rootAfter = await writeFile(this.store, await this.currentRoot(), path, content);
|
|
311
|
-
await this.
|
|
552
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by, ...await this.provenance(by) }, by);
|
|
312
553
|
return rootAfter;
|
|
313
554
|
}
|
|
314
555
|
async applyBatch(changes) {
|
|
@@ -327,7 +568,7 @@ class AsyncRepo {
|
|
|
327
568
|
}
|
|
328
569
|
async writeBytes(path, data, at = Date.now(), by = this.actor) {
|
|
329
570
|
const rootAfter = await writeFile(this.store, await this.currentRoot(), path, Buffer.from(data).toString("base64"), { encoding: "base64" });
|
|
330
|
-
await this.
|
|
571
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by, ...await this.provenance(by) }, by);
|
|
331
572
|
return rootAfter;
|
|
332
573
|
}
|
|
333
574
|
async readBytes(path) {
|
|
@@ -346,7 +587,7 @@ class AsyncRepo {
|
|
|
346
587
|
if (!f)
|
|
347
588
|
return this.currentRoot();
|
|
348
589
|
const rootAfter = await writeFile(this.store, await this.currentRoot(), path, f.content, { encoding: f.encoding, mode });
|
|
349
|
-
await this.
|
|
590
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
350
591
|
return rootAfter;
|
|
351
592
|
}
|
|
352
593
|
async mode(path) {
|
|
@@ -354,7 +595,96 @@ class AsyncRepo {
|
|
|
354
595
|
}
|
|
355
596
|
async writeSealed(path, box, at = Date.now()) {
|
|
356
597
|
const rootAfter = await writeSealed(this.store, await this.currentRoot(), path, box);
|
|
357
|
-
await this.
|
|
598
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
599
|
+
return rootAfter;
|
|
600
|
+
}
|
|
601
|
+
async sealSubtree(dirPath, seal, at = Date.now()) {
|
|
602
|
+
const subRoot = await subtreeRootAt(this.store, await this.currentRoot(), dirPath);
|
|
603
|
+
if (subRoot === undefined)
|
|
604
|
+
return;
|
|
605
|
+
const plaintext = await serializeSubtreeAsync(this.store, subRoot);
|
|
606
|
+
return this.writeSealed(dirPath, seal(plaintext), at);
|
|
607
|
+
}
|
|
608
|
+
async hideName(path, seal, opts = {}, at = Date.now()) {
|
|
609
|
+
const root = await this.currentRoot();
|
|
610
|
+
const segs = path.split("/").filter(Boolean);
|
|
611
|
+
const name = segs[segs.length - 1];
|
|
612
|
+
let entry = await entryAt(this.store, root, path);
|
|
613
|
+
if (!entry) {
|
|
614
|
+
if (opts.contentSeal && opts.absentContent !== undefined) {
|
|
615
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(opts.absentContent) });
|
|
616
|
+
entry = { kind: "sealed", hash: sealedHash };
|
|
617
|
+
} else {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (slotIdFromKey(name) !== undefined)
|
|
622
|
+
return slotIdFromKey(name);
|
|
623
|
+
if (opts.contentSeal && entry.kind === "blob") {
|
|
624
|
+
const blob = await this.store.get(entry.hash);
|
|
625
|
+
if (blob && blob.kind === "blob") {
|
|
626
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(blob.content) });
|
|
627
|
+
entry = entry.mode === undefined ? { kind: "sealed", hash: sealedHash } : { kind: "sealed", hash: sealedHash, mode: entry.mode };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const slotId = opts.slotId ?? mintSlotId();
|
|
631
|
+
const plaintext = await serializeNameAsync(this.store, name, entry);
|
|
632
|
+
const rootAfter = await hideName(this.store, root, path, seal(plaintext), slotId);
|
|
633
|
+
const slotPath = segs.length > 1 ? `${segs.slice(0, -1).join("/")}/${slotKey(slotId)}` : slotKey(slotId);
|
|
634
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path: slotPath, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
635
|
+
return slotId;
|
|
636
|
+
}
|
|
637
|
+
async reHideName(realDir, name, slotId, newContent, seal, contentSeal, at = Date.now()) {
|
|
638
|
+
const root = await this.currentRoot();
|
|
639
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: contentSeal(newContent) });
|
|
640
|
+
const childEntry = { kind: "sealed", hash: sealedHash };
|
|
641
|
+
const plaintext = await serializeNameAsync(this.store, name, childEntry);
|
|
642
|
+
const dirSegs = realDir.split("/").filter(Boolean);
|
|
643
|
+
const rootAfter = await reHideName(this.store, root, dirSegs, slotKey(slotId), seal(plaintext));
|
|
644
|
+
const slotPath = dirSegs.length ? `${dirSegs.join("/")}/${slotKey(slotId)}` : slotKey(slotId);
|
|
645
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path: slotPath, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
646
|
+
return rootAfter;
|
|
647
|
+
}
|
|
648
|
+
async hideExistence(path, seal, opts = {}, at = Date.now()) {
|
|
649
|
+
const root = await this.currentRoot();
|
|
650
|
+
const segs = path.split("/").filter(Boolean);
|
|
651
|
+
const name = segs[segs.length - 1];
|
|
652
|
+
const dirPath = segs.slice(0, -1).join("/");
|
|
653
|
+
let entry = await entryAt(this.store, root, path);
|
|
654
|
+
if (name === HIDDEN_KEY)
|
|
655
|
+
return root;
|
|
656
|
+
if (!entry) {
|
|
657
|
+
if (opts.contentSeal && opts.absentContent !== undefined) {
|
|
658
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(opts.absentContent) });
|
|
659
|
+
entry = { kind: "sealed", hash: sealedHash };
|
|
660
|
+
} else {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (opts.contentSeal && entry.kind === "blob") {
|
|
665
|
+
const blob = await this.store.get(entry.hash);
|
|
666
|
+
if (blob && blob.kind === "blob") {
|
|
667
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(blob.content) });
|
|
668
|
+
entry = entry.mode === undefined ? { kind: "sealed", hash: sealedHash } : { kind: "sealed", hash: sealedHash, mode: entry.mode };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const slots = {};
|
|
672
|
+
const dirEntries = await dirEntriesAt(this.store, root, dirPath);
|
|
673
|
+
const prior = dirEntries?.[HIDDEN_KEY];
|
|
674
|
+
if (prior && prior.kind === "sealed" && opts.openPrior) {
|
|
675
|
+
const priorNode = await this.store.get(prior.hash);
|
|
676
|
+
if (priorNode && priorNode.kind === "sealed") {
|
|
677
|
+
const opened = opts.openPrior(priorNode.box);
|
|
678
|
+
const payload = opened === undefined ? undefined : parseStruct(opened);
|
|
679
|
+
if (payload?.__solStruct === "entries")
|
|
680
|
+
Object.assign(slots, payload.slots);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
slots[name] = await hiddenEntryAsync(this.store, entry);
|
|
684
|
+
const plaintext = serializeHiddenEntries(slots);
|
|
685
|
+
const rootAfter = await hideExistence(this.store, root, path, seal(plaintext));
|
|
686
|
+
const hiddenPath = dirPath ? `${dirPath}/${HIDDEN_KEY}` : HIDDEN_KEY;
|
|
687
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path: hiddenPath, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
358
688
|
return rootAfter;
|
|
359
689
|
}
|
|
360
690
|
async read(path) {
|
|
@@ -362,7 +692,7 @@ class AsyncRepo {
|
|
|
362
692
|
}
|
|
363
693
|
async deleteFile(path, at = Date.now(), by = this.actor) {
|
|
364
694
|
const rootAfter = await deleteFile(this.store, await this.currentRoot(), path);
|
|
365
|
-
await this.
|
|
695
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "delete", path, rootAfter, at, by, ...await this.provenance(by) }, by);
|
|
366
696
|
return rootAfter;
|
|
367
697
|
}
|
|
368
698
|
async readFile(path) {
|
|
@@ -393,8 +723,8 @@ function decodeObject(raw) {
|
|
|
393
723
|
|
|
394
724
|
class FileStore {
|
|
395
725
|
objects;
|
|
396
|
-
constructor(solDir) {
|
|
397
|
-
this.objects = join(solDir, "objects");
|
|
726
|
+
constructor(solDir, objectsDir) {
|
|
727
|
+
this.objects = objectsDir ?? join(solDir, "objects");
|
|
398
728
|
mkdirSync(this.objects, { recursive: true });
|
|
399
729
|
}
|
|
400
730
|
path(hash) {
|
|
@@ -575,9 +905,6 @@ class SolWorkspace {
|
|
|
575
905
|
}
|
|
576
906
|
|
|
577
907
|
// 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
908
|
var tools = [
|
|
582
909
|
{ 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
910
|
{ name: "sol_read", description: "Read a text file from the sol workspace.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
|
@@ -589,7 +916,7 @@ var tools = [
|
|
|
589
916
|
{ name: "sol_history", description: "Show the authoring history (the op-log).", inputSchema: { type: "object", properties: {} } }
|
|
590
917
|
];
|
|
591
918
|
var text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
592
|
-
async function handle(name, a) {
|
|
919
|
+
async function handle(ws, name, a) {
|
|
593
920
|
switch (name) {
|
|
594
921
|
case "sol_write":
|
|
595
922
|
await ws.write(a.path, a.content);
|
|
@@ -626,13 +953,31 @@ async function handle(name, a) {
|
|
|
626
953
|
throw new Error("unknown tool: " + name);
|
|
627
954
|
}
|
|
628
955
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
|
|
956
|
+
async function startWorkspaceMcp(opts = {}) {
|
|
957
|
+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
958
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
959
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
960
|
+
const solDir = opts.solDir || process.env.SOL_DIR || join2(process.cwd(), ".sol");
|
|
961
|
+
mkdirSync2(solDir, { recursive: true });
|
|
962
|
+
const ws = new SolWorkspace(new FileStore(solDir), new FileOpLog(solDir), process.env.SOL_ACTOR || "agent");
|
|
963
|
+
const server = new Server({ name: "sol", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
964
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
965
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
966
|
+
try {
|
|
967
|
+
return await handle(ws, req.params.name, req.params.arguments ?? {});
|
|
968
|
+
} catch (e) {
|
|
969
|
+
return { content: [{ type: "text", text: "sol error: " + (e?.message ?? e) }], isError: true };
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
await server.connect(new StdioServerTransport);
|
|
973
|
+
}
|
|
974
|
+
if (__require.main == __require.module) {
|
|
975
|
+
startWorkspaceMcp().catch((e) => {
|
|
976
|
+
process.stderr.write(`sol-mcp: ${e?.message ?? e}
|
|
977
|
+
`);
|
|
978
|
+
process.exit(1);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
export {
|
|
982
|
+
startWorkspaceMcp
|
|
983
|
+
};
|