@agentshouse/kit 0.0.1 → 0.1.0-alpha.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
package/src/sync.js ADDED
@@ -0,0 +1,477 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import {
3
+ EXPLICIT_SYNC_VERSION,
4
+ KIT_DOOR_BATCH_PATH,
5
+ KIT_DOOR_BOOTSTRAP_PATH,
6
+ KIT_DOOR_DISCOVER_PATH,
7
+ KIT_DOOR_ENUMERATE_PATH,
8
+ } from "./contract.js";
9
+ import { heldControl, houseGetJson, houseRequest, maintainControl, openSession } from "./house.js";
10
+ import { operationId } from "./operation.js";
11
+ import { houseFailed, KitRefusal } from "./refusal.js";
12
+ import {
13
+ assertWritableChanges,
14
+ authorityFor,
15
+ deriveChanges,
16
+ OPERATIONS,
17
+ relativeTo,
18
+ resultingFiles,
19
+ sidecarPath,
20
+ unknownPathRefusal,
21
+ VERSION,
22
+ } from "./sync-library.js";
23
+ import {
24
+ canonicalReplica,
25
+ clearPending,
26
+ locate,
27
+ listFiles,
28
+ readLocalMap,
29
+ readPending,
30
+ readReplicaState,
31
+ removeReplicaFile,
32
+ writePending,
33
+ writeReplicaFile,
34
+ writeReplicaState,
35
+ } from "./sync-state.js";
36
+
37
+ function sessionDependencies(options, dependencies) {
38
+ return {
39
+ fetch: dependencies.fetch ?? options.fetch,
40
+ now: dependencies.now ?? options.now,
41
+ };
42
+ }
43
+
44
+ async function withSession(options, dependencies, operation) {
45
+ const session = await openSession(options, sessionDependencies(options, dependencies));
46
+ return maintainControl(session, (signal) => operation(session, signal), options.signal);
47
+ }
48
+
49
+ async function door(session, path, body, signal) {
50
+ return houseRequest(
51
+ session.house,
52
+ await heldControl(session, signal),
53
+ path,
54
+ body,
55
+ session.dependencies,
56
+ signal,
57
+ );
58
+ }
59
+
60
+ async function replicaDoor(session, options, state, path, body, signal) {
61
+ try {
62
+ return await door(session, path, body, signal);
63
+ } catch (error) {
64
+ if (error instanceof KitRefusal && error.code === "position_expired") {
65
+ await writeReplicaState(options.directory, { ...state, expired: true }, signal);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ function positionsOf(state) {
72
+ return state.authorities.map((entry) => ({
73
+ authority: entry.authority,
74
+ position: entry.position,
75
+ log_epoch: entry.log_epoch,
76
+ }));
77
+ }
78
+
79
+ function changePosition(value, declaration) {
80
+ if (typeof value?.position !== "string" || typeof value.log_epoch !== "string") {
81
+ throw new KitRefusal("kit_sync_rejected", declaration);
82
+ }
83
+ return { position: value.position, log_epoch: value.log_epoch };
84
+ }
85
+
86
+ function discoveryOf(value) {
87
+ if (typeof value?.md_model_version !== "string" || !Array.isArray(value.authorities)) {
88
+ throw new KitRefusal("kit_sync_rejected", "discover");
89
+ }
90
+ if (value.md_model_version !== VERSION) {
91
+ throw new KitRefusal("kit_md_model_version_mismatch", value.md_model_version);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ async function bootstrapBundle(session, prefixes, signal) {
97
+ const answer = await door(session, KIT_DOOR_BOOTSTRAP_PATH, { prefixes }, signal);
98
+ if (typeof answer?.transfer?.url === "string") {
99
+ const granted = await houseGetJson(answer.transfer.url, session.dependencies, signal);
100
+ if (!Array.isArray(granted?.scopes)) throw new KitRefusal("kit_bootstrap_rejected");
101
+ return granted;
102
+ }
103
+ if (!Array.isArray(answer?.scopes)) throw new KitRefusal("kit_bootstrap_rejected");
104
+ return answer;
105
+ }
106
+
107
+ function matchedDiscovery(discovery, scope) {
108
+ if (scope.manifest?.scope === "private") {
109
+ return discovery.authorities.find((entry) => entry.kind === "private");
110
+ }
111
+ return discovery.authorities.find(
112
+ (entry) => entry.kind === "room" && entry.room_ref === scope.manifest?.anchor,
113
+ );
114
+ }
115
+
116
+ async function writeScope(replica, scope, signal) {
117
+ if (
118
+ typeof scope?.manifest?.path !== "string" ||
119
+ typeof scope.manifest.position !== "string" ||
120
+ typeof scope.manifest.log_epoch !== "string"
121
+ ) {
122
+ throw new KitRefusal("kit_bootstrap_rejected");
123
+ }
124
+ if (!Array.isArray(scope.contents) || !Array.isArray(scope.manifest.files)) {
125
+ throw new KitRefusal("kit_bootstrap_rejected");
126
+ }
127
+ const files = [];
128
+ for (const entry of scope.contents) {
129
+ if (typeof entry?.path !== "string" || typeof entry.content !== "string") {
130
+ throw new KitRefusal("kit_bootstrap_rejected");
131
+ }
132
+ const meta = scope.manifest.files.find((file) => file.path === entry.path);
133
+ await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
134
+ files.push({
135
+ path: entry.path,
136
+ revision: typeof meta?.revision === "string" ? meta.revision : "",
137
+ sha256: typeof meta?.sha256 === "string" ? meta.sha256 : "",
138
+ content: entry.content,
139
+ });
140
+ }
141
+ return files;
142
+ }
143
+
144
+ async function replaceScope(replica, previous, scope, signal) {
145
+ const next = await writeScope(replica, scope, signal);
146
+ const kept = new Set(next.map((file) => file.path));
147
+ const root = locate(replica, scope.manifest.path);
148
+ for (const path of await listFiles(root)) {
149
+ if (!kept.has(path)) await removeReplicaFile(locate(replica, scope.manifest.path, path));
150
+ }
151
+ if (previous !== undefined) {
152
+ for (const file of previous.files) {
153
+ if (!kept.has(file.path)) await removeReplicaFile(locate(replica, previous.path, file.path));
154
+ }
155
+ }
156
+ return next;
157
+ }
158
+
159
+ function stringList(value) {
160
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
161
+ }
162
+
163
+ function operationsOf(value) {
164
+ if (value === null || typeof value !== "object") return null;
165
+ const operations = {};
166
+ for (const operation of OPERATIONS) {
167
+ if (!stringList(value[operation])) return null;
168
+ operations[operation] = [...value[operation]];
169
+ }
170
+ return operations;
171
+ }
172
+
173
+ function authorityRecord(scope, discovery, files) {
174
+ const found = matchedDiscovery(discovery, scope);
175
+ const operations = found === undefined ? null : operationsOf(found.operations);
176
+ if (found === undefined || !stringList(found.protected_paths) || operations === null) {
177
+ throw new KitRefusal("kit_bootstrap_rejected");
178
+ }
179
+ return {
180
+ kind: scope.manifest.scope === "private" ? "private" : "room",
181
+ authority: scope.manifest.anchor,
182
+ room_handle: scope.manifest.room_handle,
183
+ path: scope.manifest.path,
184
+ position: scope.manifest.position,
185
+ log_epoch: scope.manifest.log_epoch,
186
+ protected_paths: found.protected_paths,
187
+ operations,
188
+ files,
189
+ };
190
+ }
191
+
192
+ async function materialize(options, discovery, bundle, signal, previous = null) {
193
+ const authorities = [];
194
+ for (const scope of bundle.scopes) {
195
+ const held = previous?.authorities.find((entry) => entry.authority === scope.manifest?.anchor);
196
+ const files = await replaceScope(options.replica, held, scope, signal);
197
+ authorities.push(authorityRecord(scope, discovery, files));
198
+ }
199
+ const state = {
200
+ version: EXPLICIT_SYNC_VERSION,
201
+ directory: options.canonical,
202
+ prefixes: options.prefixes,
203
+ md_model_version: discovery.md_model_version,
204
+ bounds: discovery.bounds ?? null,
205
+ authorities,
206
+ };
207
+ await writeReplicaState(options.directory, state, signal);
208
+ return state;
209
+ }
210
+
211
+ async function loadState(options) {
212
+ const canonical = await canonicalReplica(options.replica);
213
+ const state = await readReplicaState(options.directory, canonical);
214
+ if (state === null || state.version !== EXPLICIT_SYNC_VERSION) {
215
+ throw new KitRefusal("kit_sync_not_bootstrapped", canonical);
216
+ }
217
+ return { ...state, directory: canonical };
218
+ }
219
+
220
+ function enumeratedRevision(change) {
221
+ return typeof change.revision === "string" ? change.revision : "";
222
+ }
223
+
224
+ function enumeratedDigest(change) {
225
+ return typeof change.sha256 === "string" ? change.sha256 : "";
226
+ }
227
+
228
+ function enumeratedRecord(path, content, change) {
229
+ return {
230
+ path,
231
+ revision: enumeratedRevision(change),
232
+ sha256: enumeratedDigest(change),
233
+ content,
234
+ };
235
+ }
236
+
237
+ async function applyEnumeration(options, state, enumeration, signal) {
238
+ if (!Array.isArray(enumeration?.authorities)) throw new KitRefusal("kit_sync_rejected", "enumerate");
239
+ for (const view of enumeration.authorities) {
240
+ const authority = state.authorities.find((entry) => entry.authority === view.authority);
241
+ if (authority === undefined) throw new KitRefusal("kit_sync_rejected", view.authority);
242
+ const reached = changePosition(view, "enumerate");
243
+ const recorded = new Map(authority.files.map((file) => [file.path, file]));
244
+ for (const change of view.changes ?? []) {
245
+ if (typeof change?.path !== "string") throw new KitRefusal("kit_sync_rejected", "enumerate");
246
+ if (change.removed === true) {
247
+ await removeReplicaFile(locate(options.replica, authority.path, change.path));
248
+ recorded.delete(change.path);
249
+ continue;
250
+ }
251
+ if (typeof change.content === "string") {
252
+ await writeReplicaFile(
253
+ locate(options.replica, authority.path, change.path),
254
+ change.content,
255
+ signal,
256
+ );
257
+ recorded.set(change.path, enumeratedRecord(change.path, change.content, change));
258
+ }
259
+ if (typeof change.provenance === "string") {
260
+ const sidecar = sidecarPath(change.path);
261
+ await writeReplicaFile(
262
+ locate(options.replica, authority.path, sidecar),
263
+ change.provenance,
264
+ signal,
265
+ );
266
+ recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
267
+ }
268
+ }
269
+ authority.files = [...recorded.values()];
270
+ Object.assign(authority, reached);
271
+ }
272
+ await writeReplicaState(options.directory, state, signal);
273
+ return state;
274
+ }
275
+
276
+ async function replicaText(path) {
277
+ try {
278
+ return await readFile(path, "utf8");
279
+ } catch (error) {
280
+ if (error.code === "ENOENT") return null;
281
+ throw error;
282
+ }
283
+ }
284
+
285
+ function predictedReplica(authority, pending) {
286
+ if (!Array.isArray(pending?.body?.changes)) return null;
287
+ return resultingFiles(authority, pending.body.changes);
288
+ }
289
+
290
+ function uncertainTransport(error) {
291
+ return (
292
+ !(error instanceof KitRefusal) ||
293
+ houseFailed(error) ||
294
+ error.code === "kit_command_cancelled"
295
+ );
296
+ }
297
+
298
+ async function acceptBatch(options, state, outcome, signal, pending = null) {
299
+ const authority = state.authorities.find((entry) => entry.authority === outcome.authority);
300
+ if (authority === undefined) throw new KitRefusal("kit_sync_rejected", outcome.authority);
301
+ const reached = changePosition(outcome, "batch");
302
+ const recorded = new Map(authority.files.map((file) => [file.path, file]));
303
+ const predicted = predictedReplica(authority, pending);
304
+ for (const file of outcome.contents ?? []) {
305
+ const relative = relativeTo(authority.path, file.path);
306
+ const destination = locate(options.replica, authority.path, relative);
307
+ const local = await replicaText(destination);
308
+ const held = recorded.get(relative)?.content ?? null;
309
+ const accepted = file.content;
310
+ const expected = predicted === null ? null : (predicted[relative] ?? null);
311
+ const preserve =
312
+ pending !== null &&
313
+ local !== null &&
314
+ local !== held &&
315
+ local !== accepted &&
316
+ local !== expected;
317
+ if (accepted === null) {
318
+ if (!preserve) await removeReplicaFile(destination);
319
+ recorded.delete(relative);
320
+ continue;
321
+ }
322
+ if (!preserve) await writeReplicaFile(destination, accepted, signal);
323
+ recorded.set(relative, {
324
+ path: relative,
325
+ revision: typeof file.revision === "string" ? file.revision : "",
326
+ sha256: "",
327
+ content: accepted,
328
+ });
329
+ }
330
+ authority.files = [...recorded.values()];
331
+ Object.assign(authority, reached);
332
+ await writeReplicaState(options.directory, state, signal);
333
+ return { authority: outcome.authority, ...reached, contents: outcome.contents };
334
+ }
335
+
336
+ function sameBody(left, right) {
337
+ return JSON.stringify(left) === JSON.stringify(right);
338
+ }
339
+
340
+ async function recoverPending(session, options, state, signal) {
341
+ const pending = await readPending(options.directory, state.directory);
342
+ if (pending === null) return null;
343
+ try {
344
+ const outcome = await replicaDoor(session, options, state, pending.path, pending.body, signal);
345
+ const accepted = await acceptBatch(options, state, outcome, signal, pending);
346
+ await clearPending(options.directory, state.directory);
347
+ return { pending, accepted };
348
+ } catch (error) {
349
+ if (uncertainTransport(error)) throw error;
350
+ await clearPending(options.directory, state.directory);
351
+ throw error;
352
+ }
353
+ }
354
+
355
+ function groupChanges(state, changes) {
356
+ const groups = [];
357
+ const index = new Map();
358
+ for (const change of changes) {
359
+ const authority = authorityFor(state, change.path);
360
+ if (authority === undefined) throw unknownPathRefusal(change.path);
361
+ const key = authority.authority;
362
+ if (!index.has(key)) {
363
+ index.set(key, groups.length);
364
+ groups.push({ authority, changes: [] });
365
+ }
366
+ groups[index.get(key)].changes.push(change);
367
+ }
368
+ return groups;
369
+ }
370
+
371
+ async function sendBatch(session, options, state, changes, signal) {
372
+ assertWritableChanges(state, changes);
373
+ const groups = groupChanges(state, changes);
374
+ let last = null;
375
+ for (const group of groups) {
376
+ const body = { operation_id: operationId(), changes: group.changes };
377
+ await writePending(
378
+ options.directory,
379
+ state.directory,
380
+ { operation_id: body.operation_id, path: KIT_DOOR_BATCH_PATH, body },
381
+ signal,
382
+ );
383
+ try {
384
+ const outcome = await replicaDoor(session, options, state, KIT_DOOR_BATCH_PATH, body, signal);
385
+ last = await acceptBatch(options, state, outcome, signal);
386
+ await clearPending(options.directory, state.directory);
387
+ } catch (error) {
388
+ if (uncertainTransport(error)) throw error;
389
+ await clearPending(options.directory, state.directory);
390
+ throw error;
391
+ }
392
+ }
393
+ return last;
394
+ }
395
+
396
+ async function derivedChanges(options, state) {
397
+ const changes = [];
398
+ for (const authority of state.authorities) {
399
+ const local = await readLocalMap(options.replica, authority.path);
400
+ changes.push(...deriveChanges(state, authority, local));
401
+ }
402
+ return changes;
403
+ }
404
+
405
+ export async function bootstrap(options, dependencies = {}) {
406
+ if (!Array.isArray(options.prefixes) || options.prefixes.length === 0) {
407
+ throw new KitRefusal("kit_sync_prefixes_required");
408
+ }
409
+ const canonical = await canonicalReplica(options.replica);
410
+ const existing = await readReplicaState(options.directory, canonical);
411
+ if (existing !== null) throw new KitRefusal("kit_sync_already_bootstrapped", canonical);
412
+ return withSession(options, dependencies, async (session, signal) => {
413
+ const discovery = discoveryOf(
414
+ await door(session, KIT_DOOR_DISCOVER_PATH, { prefixes: options.prefixes }, signal),
415
+ );
416
+ const bundle = await bootstrapBundle(session, options.prefixes, signal);
417
+ const state = await materialize(
418
+ { ...options, canonical, prefixes: options.prefixes },
419
+ discovery,
420
+ bundle,
421
+ signal,
422
+ );
423
+ return { positions: positionsOf(state) };
424
+ });
425
+ }
426
+
427
+ export async function pull(options, dependencies = {}) {
428
+ return withSession(options, dependencies, async (session, signal) => {
429
+ const state = await loadState(options);
430
+ const enumeration = await replicaDoor(
431
+ session,
432
+ options,
433
+ state,
434
+ KIT_DOOR_ENUMERATE_PATH,
435
+ { prefixes: state.prefixes, positions: positionsOf(state) },
436
+ signal,
437
+ );
438
+ const next = await applyEnumeration(options, state, enumeration, signal);
439
+ return { positions: positionsOf(next) };
440
+ });
441
+ }
442
+
443
+ export async function rebootstrap(options, dependencies = {}) {
444
+ return withSession(options, dependencies, async (session, signal) => {
445
+ const state = await loadState(options);
446
+ const discovery = discoveryOf(
447
+ await door(session, KIT_DOOR_DISCOVER_PATH, { prefixes: state.prefixes }, signal),
448
+ );
449
+ const bundle = await bootstrapBundle(session, state.prefixes, signal);
450
+ const next = await materialize(
451
+ { ...options, canonical: state.directory, prefixes: state.prefixes },
452
+ discovery,
453
+ bundle,
454
+ signal,
455
+ state,
456
+ );
457
+ await clearPending(options.directory, state.directory);
458
+ return { positions: positionsOf(next) };
459
+ });
460
+ }
461
+
462
+ export async function submit(options, dependencies = {}) {
463
+ return withSession(options, dependencies, async (session, signal) => {
464
+ const state = await loadState(options);
465
+ if (state.expired === true) throw new KitRefusal("position_expired", state.directory);
466
+ const recovered = await recoverPending(session, options, state, signal);
467
+ const requested = Array.isArray(options.changes) ? options.changes : null;
468
+ if (recovered !== null && requested !== null && sameBody(recovered.pending.body.changes, requested)) {
469
+ return recovered.accepted;
470
+ }
471
+ const changes = requested ?? (await derivedChanges(options, state));
472
+ if (changes.length === 0) {
473
+ return recovered !== null ? recovered.accepted : { submitted: false };
474
+ }
475
+ return sendBatch(session, options, state, changes, signal);
476
+ });
477
+ }