fold-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +14 -0
  2. package/bin/fold-agent.js +2 -0
  3. package/dist/cli/app.d.ts +4 -0
  4. package/dist/cli/app.js +1113 -0
  5. package/dist/cli/app.js.map +1 -0
  6. package/dist/cli/bin.d.ts +2 -0
  7. package/dist/cli/bin.js +8 -0
  8. package/dist/cli/bin.js.map +1 -0
  9. package/dist/cli/context.d.ts +5 -0
  10. package/dist/cli/context.js +2 -0
  11. package/dist/cli/context.js.map +1 -0
  12. package/dist/cli/operations.d.ts +126 -0
  13. package/dist/cli/operations.js +1159 -0
  14. package/dist/cli/operations.js.map +1 -0
  15. package/dist/cli/output.d.ts +22 -0
  16. package/dist/cli/output.js +245 -0
  17. package/dist/cli/output.js.map +1 -0
  18. package/dist/cli/package-info.d.ts +5 -0
  19. package/dist/cli/package-info.js +12 -0
  20. package/dist/cli/package-info.js.map +1 -0
  21. package/dist/cli/results.d.ts +393 -0
  22. package/dist/cli/results.js +2 -0
  23. package/dist/cli/results.js.map +1 -0
  24. package/dist/cli/skill-install.d.ts +7 -0
  25. package/dist/cli/skill-install.js +211 -0
  26. package/dist/cli/skill-install.js.map +1 -0
  27. package/dist/deploy/public-origin.d.ts +15 -0
  28. package/dist/deploy/public-origin.js +59 -0
  29. package/dist/deploy/public-origin.js.map +1 -0
  30. package/dist/rooms/append-log-api.d.ts +16 -0
  31. package/dist/rooms/append-log-api.js +72 -0
  32. package/dist/rooms/append-log-api.js.map +1 -0
  33. package/dist/rooms/append-log-validation.d.ts +2 -0
  34. package/dist/rooms/append-log-validation.js +16 -0
  35. package/dist/rooms/append-log-validation.js.map +1 -0
  36. package/dist/rooms/comments.d.ts +63 -0
  37. package/dist/rooms/comments.js +136 -0
  38. package/dist/rooms/comments.js.map +1 -0
  39. package/dist/rooms/crypto.d.ts +11 -0
  40. package/dist/rooms/crypto.js +44 -0
  41. package/dist/rooms/crypto.js.map +1 -0
  42. package/dist/rooms/encrypted-records.d.ts +5 -0
  43. package/dist/rooms/encrypted-records.js +21 -0
  44. package/dist/rooms/encrypted-records.js.map +1 -0
  45. package/dist/rooms/markdown-snapshot.d.ts +23 -0
  46. package/dist/rooms/markdown-snapshot.js +126 -0
  47. package/dist/rooms/markdown-snapshot.js.map +1 -0
  48. package/dist/rooms/metadata.d.ts +32 -0
  49. package/dist/rooms/metadata.js +118 -0
  50. package/dist/rooms/metadata.js.map +1 -0
  51. package/dist/rooms/personas.d.ts +16 -0
  52. package/dist/rooms/personas.js +78 -0
  53. package/dist/rooms/personas.js.map +1 -0
  54. package/dist/rooms/project-state.d.ts +41 -0
  55. package/dist/rooms/project-state.js +249 -0
  56. package/dist/rooms/project-state.js.map +1 -0
  57. package/dist/rooms/proposals.d.ts +63 -0
  58. package/dist/rooms/proposals.js +254 -0
  59. package/dist/rooms/proposals.js.map +1 -0
  60. package/dist/rooms/replay.d.ts +13 -0
  61. package/dist/rooms/replay.js +19 -0
  62. package/dist/rooms/replay.js.map +1 -0
  63. package/dist/rooms/room-reference.d.ts +21 -0
  64. package/dist/rooms/room-reference.js +142 -0
  65. package/dist/rooms/room-reference.js.map +1 -0
  66. package/dist/rooms/timeline.d.ts +26 -0
  67. package/dist/rooms/timeline.js +68 -0
  68. package/dist/rooms/timeline.js.map +1 -0
  69. package/package.json +35 -0
  70. package/skills/fold/SKILL.md +81 -0
  71. package/skills/fold/agents/openai.yaml +4 -0
  72. package/skills/fold/references/cli.md +33 -0
  73. package/skills/fold/references/security.md +14 -0
  74. package/skills/fold/references/workflow.md +48 -0
@@ -0,0 +1,1159 @@
1
+ import { basename, resolve } from 'node:path';
2
+ import { createEncryptedMarkdownSnapshot, createEncryptedMarkdownUpdate, createEncryptedMarkdownReplacementUpdateFromRecords, decryptMarkdownFromRecords, decryptMarkdownSnapshot, summarizeMarkdown, } from '../rooms/markdown-snapshot.js';
3
+ import { createRoomAccess, createRoomToken, DEFAULT_SERVER_URL, parseRoomReference, roomUrlForAccess, serverRoomUrlForAccess, } from '../rooms/room-reference.js';
4
+ import { appendEncryptedUpdate, fetchRoomStatus, listEncryptedUpdates, } from '../rooms/append-log-api.js';
5
+ import { defaultMetadataPath, findRoomMetadata, findRoomMetadataByAlias, listRoomMetadata, removeRoomMetadataByAlias, resolveSourcePath, upsertRoomMetadata, } from '../rooms/metadata.js';
6
+ import { addProjectFile, createEncryptedProjectSnapshot, normalizeProjectSnapshot, projectFileOrThrow, PROJECT_UPDATE_SENDER_ID_PREFIX, readMarkdownProject, replaceProjectFile, singleFileProject, summarizeProject, isStaleProjectFileSnapshotSeq, WEB_PROJECT_FILE_SENDER_ID_PREFIX, writeMarkdownProject, } from '../rooms/project-state.js';
7
+ import { createEncryptedProposalRecord, createProposalAcceptedEvent, createProposalRejectedEvent, replayProposalsFromRecords, } from '../rooms/proposals.js';
8
+ import { createComment, createCommentReplyEvent, createEncryptedCommentEvent, createEncryptedCommentRecord, replayCommentsFromRecords, } from '../rooms/comments.js';
9
+ import { assignPersona } from '../rooms/personas.js';
10
+ import { resolvePublicOrigin } from '../deploy/public-origin.js';
11
+ import { createEncryptedTimelineEvent, createTimelineEvent, decryptTimelineEvent, TIMELINE_EVENT_SENDER_ID_PREFIX, } from '../rooms/timeline.js';
12
+ import { decryptJsonRecord } from '../rooms/encrypted-records.js';
13
+ import { installFoldSkill } from './skill-install.js';
14
+ import { DEFAULT_FOLD_AGENT_COMMAND_PREFIX, FOLD_AGENT_PACKAGE_NAME, FOLD_AGENT_VERSION, } from './package-info.js';
15
+ const CLI_SENDER_ID = 'fold-cli:document';
16
+ const CLI_REVIEWER_FINGERPRINT = 'fold-cli:review';
17
+ const CLI_COMMENTER_FINGERPRINT = 'fold-cli:comment';
18
+ export async function publishMarkdown(options) {
19
+ const sourcePath = resolveSourcePath(options.cwd, options.filePath);
20
+ const project = await readMarkdownProject(options.cwd, options.filePath, options.path);
21
+ const primary = projectFileOrThrow(project, project.primaryPath);
22
+ const markdown = primary.markdown;
23
+ const savedAlias = options.alias ?? defaultAliasForSource(options.filePath);
24
+ const urls = resolvePublicOrigin({
25
+ serverUrl: options.serverUrl,
26
+ appUrl: options.appUrl,
27
+ syncUrl: options.syncUrl,
28
+ defaultUrl: DEFAULT_SERVER_URL,
29
+ });
30
+ const access = createRoomAccess(urls.syncUrl, urls.appUrl, urls.syncUrl);
31
+ const document = summarizeMarkdown(markdown);
32
+ const projectSummary = summarizeProject(project);
33
+ const encryptedUpdate = await createEncryptedMarkdownUpdate(markdown, access, CLI_SENDER_ID);
34
+ await appendEncryptedUpdate(access, encryptedUpdate);
35
+ await appendEncryptedUpdate(access, await createEncryptedProjectSnapshot(access, project));
36
+ const publishPersona = assignPersona({
37
+ roomId: access.roomId,
38
+ participantKind: 'human',
39
+ participantFingerprint: CLI_SENDER_ID,
40
+ });
41
+ const publishEvent = createTimelineEvent({
42
+ idSeed: `publish:${document.sha256}`,
43
+ type: 'publish',
44
+ actorPersonaId: publishPersona.id,
45
+ proposalId: null,
46
+ documentSha256: projectSummary.sha256,
47
+ message: 'Published Markdown project',
48
+ });
49
+ const eventRecord = await appendEncryptedUpdate(access, await createEncryptedTimelineEvent(access, publishEvent));
50
+ const encryptedSnapshot = await createEncryptedMarkdownSnapshot(markdown, access, CLI_SENDER_ID);
51
+ const token = createRoomToken(access);
52
+ const metadataPath = defaultMetadataPath(options.cwd);
53
+ const now = new Date().toISOString();
54
+ if (options.save) {
55
+ const entry = {
56
+ alias: savedAlias,
57
+ roomId: access.roomId,
58
+ appUrl: access.appUrl,
59
+ syncUrl: access.syncUrl,
60
+ serverUrl: access.serverUrl,
61
+ roomUrl: roomUrlForAccess(access),
62
+ token,
63
+ sourcePath,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ lastUsedAt: now,
67
+ document,
68
+ encryptedSnapshot,
69
+ };
70
+ await upsertRoomMetadata(metadataPath, entry);
71
+ }
72
+ return {
73
+ schema: 'fold.publish.result.v1',
74
+ ok: true,
75
+ mode: 'server-backed',
76
+ room: publicRoomResult(options.save ? aliasAccess(access, savedAlias) : access, token),
77
+ metadata: {
78
+ path: metadataPath,
79
+ saved: options.save,
80
+ },
81
+ document,
82
+ project: projectSummary,
83
+ server: {
84
+ recordCount: eventRecord.seq,
85
+ latestSeq: eventRecord.seq,
86
+ },
87
+ };
88
+ }
89
+ export async function createRoomProfile(options) {
90
+ const markdown = '';
91
+ const project = singleFileProject('document.md', markdown);
92
+ const urls = resolvePublicOrigin({
93
+ serverUrl: options.serverUrl,
94
+ appUrl: options.appUrl,
95
+ syncUrl: options.syncUrl,
96
+ defaultUrl: DEFAULT_SERVER_URL,
97
+ });
98
+ const access = createRoomAccess(urls.syncUrl, urls.appUrl, urls.syncUrl);
99
+ const document = summarizeMarkdown(markdown);
100
+ const projectSummary = summarizeProject(project);
101
+ await appendEncryptedUpdate(access, await createEncryptedMarkdownUpdate(markdown, access, CLI_SENDER_ID));
102
+ await appendEncryptedUpdate(access, await createEncryptedProjectSnapshot(access, project));
103
+ const publishPersona = assignPersona({
104
+ roomId: access.roomId,
105
+ participantKind: 'human',
106
+ participantFingerprint: CLI_SENDER_ID,
107
+ });
108
+ const publishEvent = createTimelineEvent({
109
+ idSeed: `room-create:${access.roomId}`,
110
+ type: 'publish',
111
+ actorPersonaId: publishPersona.id,
112
+ proposalId: null,
113
+ documentSha256: projectSummary.sha256,
114
+ message: 'Created empty Markdown project room',
115
+ });
116
+ const eventRecord = await appendEncryptedUpdate(access, await createEncryptedTimelineEvent(access, publishEvent));
117
+ const encryptedSnapshot = await createEncryptedMarkdownSnapshot(markdown, access, CLI_SENDER_ID);
118
+ const token = createRoomToken(access);
119
+ const metadataPath = defaultMetadataPath(options.cwd);
120
+ const now = new Date().toISOString();
121
+ const entry = {
122
+ alias: options.alias,
123
+ roomId: access.roomId,
124
+ appUrl: access.appUrl,
125
+ syncUrl: access.syncUrl,
126
+ serverUrl: access.serverUrl,
127
+ roomUrl: roomUrlForAccess(access),
128
+ token,
129
+ createdAt: now,
130
+ updatedAt: now,
131
+ lastUsedAt: now,
132
+ document,
133
+ encryptedSnapshot,
134
+ };
135
+ await upsertRoomMetadata(metadataPath, entry);
136
+ return {
137
+ schema: 'fold.room.create.result.v1',
138
+ ok: true,
139
+ mode: 'server-backed',
140
+ room: publicRoomResult(aliasAccess(access, options.alias), token),
141
+ metadata: {
142
+ path: metadataPath,
143
+ alias: options.alias,
144
+ saved: true,
145
+ },
146
+ document,
147
+ project: projectSummary,
148
+ server: {
149
+ recordCount: eventRecord.seq,
150
+ latestSeq: eventRecord.seq,
151
+ },
152
+ };
153
+ }
154
+ export async function exportMarkdown(options) {
155
+ const reference = await resolveRoomReference(options.cwd, options.room);
156
+ const metadataPath = defaultMetadataPath(options.cwd);
157
+ const entry = await findRoomMetadata(metadataPath, reference.roomId, reference.serverUrl);
158
+ const records = await listEncryptedUpdates(reference);
159
+ const project = records.length > 0
160
+ ? await currentProjectFromRecords(records, reference, entry)
161
+ : singleFileProject(entry?.sourcePath ? basename(entry.sourcePath) : 'document.md', await decryptLocalSnapshotOrThrow(entry, reference));
162
+ const selected = options.path ? projectFileOrThrow(project, options.path) : projectFileOrThrow(project, project.primaryPath);
163
+ const markdown = selected.markdown;
164
+ const document = summarizeMarkdown(markdown);
165
+ const projectSummary = summarizeProject(project);
166
+ const outputPath = options.outputPath ? resolve(options.cwd, options.outputPath) : null;
167
+ let writtenPaths = [];
168
+ if (options.outputPath) {
169
+ writtenPaths = await writeMarkdownProject(options.cwd, options.outputPath, project, options.path, {
170
+ forceDirectory: options.forceProjectDirectory,
171
+ });
172
+ }
173
+ return {
174
+ schema: 'fold.export.result.v1',
175
+ ok: true,
176
+ mode: 'server-backed',
177
+ room: safeRoomResult(reference),
178
+ metadata: {
179
+ path: metadataPath,
180
+ found: Boolean(entry),
181
+ },
182
+ output: {
183
+ path: outputPath,
184
+ written: Boolean(outputPath),
185
+ bytes: document.bytes,
186
+ sha256: document.sha256,
187
+ paths: writtenPaths,
188
+ },
189
+ document: {
190
+ ...document,
191
+ markdown,
192
+ },
193
+ project: projectSummary,
194
+ server: {
195
+ recordCount: records.length,
196
+ latestSeq: records.at(-1)?.seq ?? null,
197
+ },
198
+ };
199
+ }
200
+ export async function postMarkdown(options) {
201
+ const reference = await resolveRoomReference(options.cwd, options.room);
202
+ const metadataPath = defaultMetadataPath(options.cwd);
203
+ const entry = await findRoomMetadata(metadataPath, reference.roomId, reference.serverUrl);
204
+ const records = await listEncryptedUpdates(reference);
205
+ const baseProject = records.length > 0
206
+ ? await currentProjectFromRecords(records, reference, entry)
207
+ : singleFileProject(entry?.sourcePath ? basename(entry.sourcePath) : 'document.md', await decryptLocalSnapshotOrThrow(entry, reference));
208
+ const inputProject = await readMarkdownProject(options.cwd, options.filePath, options.path);
209
+ if (inputProject.files.length !== 1) {
210
+ throw new Error('fold post accepts one Markdown file; use one command per fresh file');
211
+ }
212
+ const inputFile = projectFileOrThrow(inputProject, inputProject.primaryPath);
213
+ if (baseProject.files.some((file) => file.path === inputFile.path)) {
214
+ throw new Error(`Project file already exists: ${inputFile.path}. Use fold propose to change existing files.`);
215
+ }
216
+ const postedProject = addProjectFile(baseProject, inputFile.path, inputFile.markdown);
217
+ const projectSummary = summarizeProject(postedProject);
218
+ const fileSummary = summarizeMarkdown(inputFile.markdown);
219
+ await appendEncryptedUpdate(reference, await createEncryptedProjectSnapshot(reference, postedProject));
220
+ const persona = assignPersona({
221
+ roomId: reference.roomId,
222
+ participantKind: 'agent',
223
+ participantFingerprint: CLI_SENDER_ID,
224
+ });
225
+ const event = createTimelineEvent({
226
+ type: 'file_posted',
227
+ actorPersonaId: persona.id,
228
+ proposalId: null,
229
+ documentSha256: projectSummary.sha256,
230
+ message: `Posted ${inputFile.path}`,
231
+ acceptedProject: postedProject,
232
+ });
233
+ const eventRecord = await appendEncryptedUpdate(reference, await createEncryptedTimelineEvent(reference, event));
234
+ return {
235
+ schema: 'fold.post.result.v1',
236
+ ok: true,
237
+ mode: 'accepted-file',
238
+ room: safeRoomResult(reference),
239
+ metadata: {
240
+ path: metadataPath,
241
+ found: Boolean(entry),
242
+ },
243
+ file: {
244
+ path: inputFile.path,
245
+ ...fileSummary,
246
+ },
247
+ project: projectSummary,
248
+ timeline: event,
249
+ server: {
250
+ recordCount: eventRecord.seq,
251
+ latestSeq: eventRecord.seq,
252
+ },
253
+ };
254
+ }
255
+ export async function roomStatus(options) {
256
+ const reference = await resolveRoomReference(options.cwd, options.room);
257
+ const metadataPath = defaultMetadataPath(options.cwd);
258
+ const entry = await findRoomMetadata(metadataPath, reference.roomId, reference.serverUrl);
259
+ const status = await fetchRoomStatus(reference);
260
+ const records = await listEncryptedUpdates(reference);
261
+ const project = records.length > 0 ? await currentProjectFromRecords(records, reference, entry) : null;
262
+ const replayedDocument = project
263
+ ? summarizeMarkdown(projectFileOrThrow(project, project.primaryPath).markdown)
264
+ : null;
265
+ return {
266
+ schema: 'fold.status.result.v1',
267
+ ok: true,
268
+ mode: 'server-backed',
269
+ room: safeRoomResult(reference),
270
+ metadata: {
271
+ path: metadataPath,
272
+ found: Boolean(entry),
273
+ sourcePath: entry?.sourcePath ?? null,
274
+ createdAt: entry?.createdAt ?? null,
275
+ updatedAt: entry?.updatedAt ?? null,
276
+ },
277
+ document: replayedDocument ?? entry?.document ?? null,
278
+ project: project ? summarizeProject(project) : null,
279
+ server: {
280
+ checked: true,
281
+ recordCount: status.recordCount,
282
+ latestSeq: status.latestSeq,
283
+ },
284
+ };
285
+ }
286
+ export async function roomContext(options) {
287
+ const reference = await resolveRoomReference(options.cwd, options.room);
288
+ const metadataPath = defaultMetadataPath(options.cwd);
289
+ const entry = await findRoomMetadata(metadataPath, reference.roomId, reference.serverUrl);
290
+ const records = await listEncryptedUpdates(reference);
291
+ const project = await currentProjectFromRecords(records, reference, entry);
292
+ const projectSummary = summarizeProject(project);
293
+ const primary = projectFileOrThrow(project, project.primaryPath);
294
+ const document = summarizeMarkdown(primary.markdown);
295
+ const comments = await replayCommentsFromRecords(reference, records);
296
+ const proposalReplay = await replayProposalsFromRecords(reference, records);
297
+ const proposals = proposalReplay.proposals.map(publicProposalListItem);
298
+ return {
299
+ schema: 'fold.context.result.v1',
300
+ ok: true,
301
+ mode: 'agent-context',
302
+ room: safeRoomResult(reference),
303
+ document,
304
+ project: projectSummary,
305
+ files: project.files.map((file) => ({
306
+ path: file.path,
307
+ markdown: file.markdown,
308
+ ...summarizeMarkdown(file.markdown),
309
+ })),
310
+ comments: {
311
+ unresolved: comments.filter((comment) => !comment.resolvedAt),
312
+ },
313
+ proposals: {
314
+ pending: proposals.filter((proposal) => proposal.status === 'pending'),
315
+ accepted: proposals.filter((proposal) => proposal.status === 'accepted'),
316
+ rejected: proposals.filter((proposal) => proposal.status === 'rejected'),
317
+ },
318
+ server: {
319
+ recordCount: records.length,
320
+ latestSeq: records.at(-1)?.seq ?? null,
321
+ },
322
+ };
323
+ }
324
+ export async function resumeRoom(options) {
325
+ const parsedSecretReference = tryParseRoomReference(options.room);
326
+ if (!parsedSecretReference && options.alias) {
327
+ throw new Error('--alias is only valid when --room is a fold:v1 token or room URL; repeat agents should use --room <alias> without --alias');
328
+ }
329
+ if (parsedSecretReference && !options.alias) {
330
+ throw new Error('Resuming from a room URL or fold:v1 token requires --alias so follow-up commands do not echo secret room access material');
331
+ }
332
+ const imported = Boolean(parsedSecretReference && options.alias);
333
+ if (imported && options.alias) {
334
+ await addRoomProfile({
335
+ cwd: options.cwd,
336
+ room: options.room,
337
+ alias: options.alias,
338
+ });
339
+ }
340
+ const room = options.alias ?? options.room;
341
+ const status = await roomStatus({ cwd: options.cwd, room });
342
+ const exported = options.outputPath
343
+ ? await exportMarkdown({ cwd: options.cwd, room, outputPath: options.outputPath, forceProjectDirectory: true })
344
+ : null;
345
+ const context = await roomContext({ cwd: options.cwd, room });
346
+ const requests = await listComments({
347
+ cwd: options.cwd,
348
+ room,
349
+ type: 'request',
350
+ open: true,
351
+ });
352
+ const comments = await listComments({
353
+ cwd: options.cwd,
354
+ room,
355
+ type: 'comment',
356
+ open: true,
357
+ });
358
+ const proposals = await listProposals({ cwd: options.cwd, room });
359
+ const commandPrefix = options.commandPrefix ?? 'fold';
360
+ const roomArgument = JSON.stringify(room);
361
+ const outputArgument = options.outputPath ? JSON.stringify(options.outputPath) : null;
362
+ const skillUrl = `${context.room.appUrl.replace(/\/$/, '')}/.well-known/fold/agent-skill.md`;
363
+ return {
364
+ schema: 'fold.resume.result.v1',
365
+ ok: true,
366
+ mode: 'agent-resume',
367
+ room: context.room,
368
+ metadata: {
369
+ path: defaultMetadataPath(options.cwd),
370
+ alias: room,
371
+ imported,
372
+ },
373
+ skill: {
374
+ url: skillUrl,
375
+ install: {
376
+ required: false,
377
+ repeatAgents: `If the Fold skill is already installed, do not reinstall it; run ${commandPrefix} resume with the saved alias.`,
378
+ command: `${commandPrefix} skill`,
379
+ updateCommand: `${commandPrefix} skill update`,
380
+ },
381
+ },
382
+ status,
383
+ export: exported,
384
+ context,
385
+ requests,
386
+ comments,
387
+ proposals,
388
+ nextCommands: {
389
+ post: outputArgument
390
+ ? `${commandPrefix} post ${outputArgument}/NEW_FILE.md --room ${roomArgument} --path "NEW_FILE.md" --json`
391
+ : null,
392
+ propose: outputArgument
393
+ ? `${commandPrefix} propose ${outputArgument} --room ${roomArgument} --title "Describe the change" --comment "Summarize what changed." --json`
394
+ : null,
395
+ requests: `${commandPrefix} requests --room ${roomArgument} --json`,
396
+ comments: `${commandPrefix} comments --room ${roomArgument} --type comment --open --json`,
397
+ proposals: `${commandPrefix} proposals --room ${roomArgument} --json`,
398
+ reply: `${commandPrefix} reply "<thread-id>" --room ${roomArgument} --text "Short reply." --json`,
399
+ context: `${commandPrefix} context --room ${roomArgument} --json`,
400
+ },
401
+ };
402
+ }
403
+ export async function bootstrapRoom(options) {
404
+ const commandPrefix = options.nextCommandPrefix ?? DEFAULT_FOLD_AGENT_COMMAND_PREFIX;
405
+ const skill = options.skipSkill
406
+ ? null
407
+ : await installFoldSkill({
408
+ cwd: options.cwd,
409
+ scope: options.skillScope,
410
+ mode: 'update',
411
+ });
412
+ const resume = await resumeRoom({
413
+ cwd: options.cwd,
414
+ room: options.room,
415
+ alias: options.alias,
416
+ outputPath: options.outputPath,
417
+ commandPrefix,
418
+ });
419
+ return {
420
+ schema: 'fold.bootstrap.result.v1',
421
+ ok: true,
422
+ package: {
423
+ name: FOLD_AGENT_PACKAGE_NAME,
424
+ version: FOLD_AGENT_VERSION,
425
+ },
426
+ skill,
427
+ resume,
428
+ nextCommands: resume.nextCommands,
429
+ };
430
+ }
431
+ function tryParseRoomReference(input) {
432
+ try {
433
+ return parseRoomReference(input);
434
+ }
435
+ catch {
436
+ return null;
437
+ }
438
+ }
439
+ export async function patchMarkdown(options) {
440
+ const proposed = await proposeMarkdown({
441
+ cwd: options.cwd,
442
+ filePath: options.filePath,
443
+ room: options.room,
444
+ path: options.path,
445
+ title: options.summary,
446
+ comment: options.summary,
447
+ });
448
+ return {
449
+ schema: 'fold.patch.result.v1',
450
+ ok: true,
451
+ mode: 'suggestion',
452
+ room: proposed.room,
453
+ metadata: proposed.metadata,
454
+ base: proposed.base,
455
+ proposed: proposed.proposed,
456
+ suggestion: {
457
+ id: proposed.proposal.id,
458
+ kind: 'whole-document-replacement',
459
+ baseSha256: proposed.proposal.base.sha256,
460
+ proposedSha256: proposed.proposal.proposed.sha256,
461
+ },
462
+ server: proposed.server,
463
+ };
464
+ }
465
+ export async function proposeMarkdown(options) {
466
+ const reference = await resolveRoomReference(options.cwd, options.room);
467
+ const metadataPath = defaultMetadataPath(options.cwd);
468
+ const entry = await findRoomMetadata(metadataPath, reference.roomId, reference.serverUrl);
469
+ const records = await listEncryptedUpdates(reference);
470
+ const baseProject = records.length > 0
471
+ ? await currentProjectFromRecords(records, reference, entry)
472
+ : singleFileProject(entry?.sourcePath ? basename(entry.sourcePath) : 'document.md', await decryptLocalSnapshotOrThrow(entry, reference));
473
+ const inputProject = await readMarkdownProject(options.cwd, options.filePath, options.path);
474
+ const proposedPath = options.path ?? inferSingleFileProposalPath(baseProject, inputProject);
475
+ const inputPrimary = projectFileOrThrow(inputProject, inputProject.primaryPath);
476
+ const existingBaseFile = proposedPath
477
+ ? baseProject.files.find((file) => file.path === proposedPath)
478
+ : null;
479
+ if (proposedPath && !existingBaseFile) {
480
+ throw new Error(`Fresh project files must be posted directly with fold post: ${proposedPath}`);
481
+ }
482
+ const newInputPaths = proposedPath ? [] : projectNewFilePaths(baseProject, inputProject);
483
+ if (newInputPaths.length > 0) {
484
+ throw new Error(`Fresh project files must be posted directly with fold post: ${newInputPaths.join(', ')}`);
485
+ }
486
+ const proposedProject = proposedPath
487
+ ? replaceProjectFile(baseProject, proposedPath, inputPrimary.markdown)
488
+ : preserveBasePrimaryPath(baseProject, inputProject);
489
+ const baseMarkdown = existingBaseFile
490
+ ? existingBaseFile.markdown
491
+ : proposedPath
492
+ ? ''
493
+ : projectFileOrThrow(baseProject, baseProject.primaryPath).markdown;
494
+ const proposedMarkdown = proposedPath
495
+ ? projectFileOrThrow(proposedProject, proposedPath).markdown
496
+ : projectFileOrThrow(proposedProject, proposedProject.primaryPath).markdown;
497
+ const { update, proposal, timelineEvent } = await createEncryptedProposalRecord({
498
+ access: reference,
499
+ baseMarkdown,
500
+ proposedMarkdown,
501
+ baseProject,
502
+ proposedProject: proposedPath ? undefined : proposedProject,
503
+ path: proposedPath,
504
+ title: options.title,
505
+ comment: options.comment,
506
+ });
507
+ await appendEncryptedUpdate(reference, update);
508
+ const eventRecord = await appendEncryptedUpdate(reference, await createEncryptedTimelineEvent(reference, timelineEvent));
509
+ return {
510
+ schema: 'fold.propose.result.v1',
511
+ ok: true,
512
+ mode: 'proposal',
513
+ room: safeRoomResult(reference),
514
+ metadata: {
515
+ path: metadataPath,
516
+ found: Boolean(entry),
517
+ },
518
+ base: summarizeMarkdown(baseMarkdown),
519
+ proposed: summarizeMarkdown(proposedMarkdown),
520
+ project: {
521
+ base: summarizeProject(baseProject),
522
+ proposed: summarizeProject(proposedProject),
523
+ },
524
+ proposal: publicProposalSummary(proposal),
525
+ timeline: timelineEvent,
526
+ server: {
527
+ recordCount: eventRecord.seq,
528
+ latestSeq: eventRecord.seq,
529
+ },
530
+ };
531
+ }
532
+ export async function listProposals(options) {
533
+ const reference = await resolveRoomReference(options.cwd, options.room);
534
+ const records = await listEncryptedUpdates(reference);
535
+ const replay = await replayProposalsFromRecords(reference, records);
536
+ return {
537
+ schema: 'fold.proposals.result.v1',
538
+ ok: true,
539
+ mode: 'proposal-list',
540
+ room: safeRoomResult(reference),
541
+ proposals: replay.proposals.map(publicProposalListItem),
542
+ server: {
543
+ recordCount: records.length,
544
+ latestSeq: records.at(-1)?.seq ?? null,
545
+ },
546
+ };
547
+ }
548
+ export async function listComments(options) {
549
+ const reference = await resolveRoomReference(options.cwd, options.room);
550
+ const records = await listEncryptedUpdates(reference);
551
+ const comments = await replayCommentsFromRecords(reference, records);
552
+ const type = options.type ?? 'all';
553
+ const visibleComments = comments.filter((comment) => commentMatchesFilter(comment, {
554
+ path: options.path,
555
+ type,
556
+ open: options.open ?? false,
557
+ }));
558
+ return {
559
+ schema: 'fold.comments.result.v1',
560
+ ok: true,
561
+ mode: 'comment-list',
562
+ room: safeRoomResult(reference),
563
+ filters: {
564
+ type,
565
+ open: options.open ?? false,
566
+ path: options.path ?? null,
567
+ },
568
+ comments: visibleComments,
569
+ server: {
570
+ recordCount: records.length,
571
+ latestSeq: records.at(-1)?.seq ?? null,
572
+ },
573
+ };
574
+ }
575
+ export async function addComment(options) {
576
+ const text = options.text.trim();
577
+ if (!text)
578
+ throw new Error('Comment text is required');
579
+ const reference = await resolveRoomReference(options.cwd, options.room);
580
+ const records = await listEncryptedUpdates(reference);
581
+ const project = await currentProjectFromRecords(records, reference);
582
+ const filePath = options.path ?? project.primaryPath;
583
+ const file = projectFileOrThrow(project, filePath);
584
+ const persona = assignPersona({
585
+ roomId: reference.roomId,
586
+ participantKind: 'agent',
587
+ participantFingerprint: CLI_COMMENTER_FINGERPRINT,
588
+ });
589
+ const comment = createComment({
590
+ persona,
591
+ text,
592
+ markdown: file.markdown,
593
+ filePath: file.path,
594
+ selectedQuote: options.quote,
595
+ type: options.type,
596
+ });
597
+ const record = await appendEncryptedUpdate(reference, await createEncryptedCommentRecord(reference, comment));
598
+ return {
599
+ schema: 'fold.comment.result.v1',
600
+ ok: true,
601
+ mode: 'comment',
602
+ room: safeRoomResult(reference),
603
+ comment,
604
+ server: {
605
+ recordCount: record.seq,
606
+ latestSeq: record.seq,
607
+ },
608
+ };
609
+ }
610
+ export async function replyToComment(options) {
611
+ const text = options.text.trim();
612
+ if (!text)
613
+ throw new Error('Reply text is required');
614
+ const reference = await resolveRoomReference(options.cwd, options.room);
615
+ const records = await listEncryptedUpdates(reference);
616
+ const comments = await replayCommentsFromRecords(reference, records);
617
+ const comment = comments.find((candidate) => candidate.id === options.commentId);
618
+ if (!comment)
619
+ throw new Error(`Comment not found: ${options.commentId}`);
620
+ if (comment.resolvedAt)
621
+ throw new Error(`Comment ${options.commentId} is resolved; reopen it before replying`);
622
+ const persona = assignPersona({
623
+ roomId: reference.roomId,
624
+ participantKind: 'agent',
625
+ participantFingerprint: CLI_COMMENTER_FINGERPRINT,
626
+ });
627
+ const event = createCommentReplyEvent({ comment, persona, text });
628
+ const record = await appendEncryptedUpdate(reference, await createEncryptedCommentEvent(reference, event));
629
+ const updated = {
630
+ ...comment,
631
+ replies: [...(comment.replies || []), event.reply].sort((left, right) => left.createdAt.localeCompare(right.createdAt)),
632
+ };
633
+ return {
634
+ schema: 'fold.reply.result.v1',
635
+ ok: true,
636
+ mode: 'comment',
637
+ room: safeRoomResult(reference),
638
+ comment: updated,
639
+ server: {
640
+ recordCount: record.seq,
641
+ latestSeq: record.seq,
642
+ },
643
+ };
644
+ }
645
+ function commentMatchesFilter(comment, filter) {
646
+ if (filter.path && comment.filePath !== filter.path)
647
+ return false;
648
+ if (filter.open && comment.resolvedAt)
649
+ return false;
650
+ if (filter.type === 'request' && comment.type !== 'request')
651
+ return false;
652
+ if (filter.type === 'comment' && comment.type !== 'note')
653
+ return false;
654
+ return true;
655
+ }
656
+ export async function showProposal(options) {
657
+ const { reference, records, proposal, timeline } = await getProposalOrThrow(options);
658
+ return {
659
+ schema: 'fold.show-proposal.result.v1',
660
+ ok: true,
661
+ mode: 'proposal',
662
+ room: safeRoomResult(reference),
663
+ proposal,
664
+ timeline: timeline.filter((event) => event.proposalId === proposal.id),
665
+ server: {
666
+ recordCount: records.length,
667
+ latestSeq: records.at(-1)?.seq ?? null,
668
+ },
669
+ };
670
+ }
671
+ export async function acceptProposal(options) {
672
+ const { reference, records, proposal } = await getProposalOrThrow(options);
673
+ assertPendingProposal(proposal);
674
+ const currentProject = await currentProjectFromRecords(records, reference);
675
+ const currentDocument = proposal.baseProject ? summarizeProject(currentProject) : summarizeMarkdown(projectFileOrThrow(currentProject, currentProject.primaryPath).markdown);
676
+ if (proposal.baseProject && currentDocument.sha256 !== proposal.baseProject.sha256) {
677
+ throw new Error(`Proposal ${proposal.id} is based on project ${proposal.baseProject.sha256} but current document/project is ${currentDocument.sha256}`);
678
+ }
679
+ if (!proposal.baseProject && currentDocument.sha256 !== proposal.base.sha256) {
680
+ throw new Error(`Proposal ${proposal.id} is based on ${proposal.base.sha256} but current document is ${currentDocument.sha256}`);
681
+ }
682
+ const document = summarizeMarkdown(proposal.proposed.markdown);
683
+ const acceptedProject = proposal.proposedProject
684
+ ?? (proposal.path
685
+ ? replaceProjectFile(currentProject, proposal.path, proposal.proposed.markdown)
686
+ : singleFileProject(currentProject.primaryPath, proposal.proposed.markdown));
687
+ const primaryMarkdown = projectFileOrThrow(acceptedProject, acceptedProject.primaryPath).markdown;
688
+ const reviewerPersona = assignPersona({
689
+ roomId: reference.roomId,
690
+ participantKind: 'human',
691
+ participantFingerprint: CLI_REVIEWER_FINGERPRINT,
692
+ });
693
+ const eventUpdate = await createProposalAcceptedEvent(reference, proposal, document.sha256, reviewerPersona.id, acceptedProject);
694
+ const eventRecord = await appendEncryptedUpdate(reference, eventUpdate);
695
+ // The accepted event carries the accepted project so replay can recover even if
696
+ // these redundant compatibility snapshots fail after the decision is durable.
697
+ const documentUpdate = await createEncryptedMarkdownReplacementUpdateFromRecords(records, primaryMarkdown, reference);
698
+ await appendEncryptedUpdate(reference, documentUpdate);
699
+ await appendEncryptedUpdate(reference, await createEncryptedProjectSnapshot(reference, acceptedProject));
700
+ const replay = await replayProposalsFromRecords(reference, await listEncryptedUpdates(reference));
701
+ const updated = findProposalInReplay(replay.proposals, options.proposalId);
702
+ return {
703
+ schema: 'fold.accept.result.v1',
704
+ ok: true,
705
+ mode: 'proposal-decision',
706
+ room: safeRoomResult(reference),
707
+ proposal: publicProposalSummary(updated),
708
+ status: 'accepted',
709
+ document,
710
+ project: summarizeProject(acceptedProject),
711
+ timeline: replay.timeline.find((event) => event.proposalId === proposal.id && event.type === 'proposal_accepted'),
712
+ server: {
713
+ recordCount: eventRecord.seq,
714
+ latestSeq: eventRecord.seq,
715
+ },
716
+ };
717
+ }
718
+ export async function rejectProposal(options) {
719
+ const { reference, proposal } = await getProposalOrThrow(options);
720
+ assertPendingProposal(proposal);
721
+ const reviewerPersona = assignPersona({
722
+ roomId: reference.roomId,
723
+ participantKind: 'human',
724
+ participantFingerprint: CLI_REVIEWER_FINGERPRINT,
725
+ });
726
+ const eventUpdate = await createProposalRejectedEvent(reference, proposal, reviewerPersona.id);
727
+ const eventRecord = await appendEncryptedUpdate(reference, eventUpdate);
728
+ const replay = await replayProposalsFromRecords(reference, await listEncryptedUpdates(reference));
729
+ const updated = findProposalInReplay(replay.proposals, options.proposalId);
730
+ return {
731
+ schema: 'fold.reject.result.v1',
732
+ ok: true,
733
+ mode: 'proposal-decision',
734
+ room: safeRoomResult(reference),
735
+ proposal: publicProposalSummary(updated),
736
+ status: 'rejected',
737
+ document: null,
738
+ project: null,
739
+ timeline: replay.timeline.find((event) => event.proposalId === proposal.id && event.type === 'proposal_rejected'),
740
+ server: {
741
+ recordCount: eventRecord.seq,
742
+ latestSeq: eventRecord.seq,
743
+ },
744
+ };
745
+ }
746
+ export async function addRoomProfile(options) {
747
+ const reference = parseRoomReference(options.room);
748
+ const metadataPath = defaultMetadataPath(options.cwd);
749
+ const now = new Date().toISOString();
750
+ const entry = {
751
+ alias: options.alias,
752
+ roomId: reference.roomId,
753
+ appUrl: reference.appUrl,
754
+ syncUrl: reference.syncUrl,
755
+ serverUrl: reference.serverUrl,
756
+ roomUrl: roomUrlForAccess(reference),
757
+ token: createRoomToken(reference),
758
+ createdAt: now,
759
+ updatedAt: now,
760
+ lastUsedAt: now,
761
+ document: summarizeMarkdown(''),
762
+ encryptedSnapshot: await createEncryptedMarkdownSnapshot('', reference, CLI_SENDER_ID),
763
+ };
764
+ await upsertRoomMetadata(metadataPath, entry);
765
+ return {
766
+ schema: 'fold.room.add.result.v1',
767
+ ok: true,
768
+ room: publicRoomResult(aliasAccess(reference, options.alias), createRoomToken(reference)),
769
+ metadata: {
770
+ path: metadataPath,
771
+ alias: options.alias,
772
+ },
773
+ };
774
+ }
775
+ export async function listRoomProfiles(options) {
776
+ const metadataPath = defaultMetadataPath(options.cwd);
777
+ const rooms = await listRoomMetadata(metadataPath);
778
+ return {
779
+ schema: 'fold.room.list.result.v1',
780
+ ok: true,
781
+ metadata: { path: metadataPath },
782
+ rooms: rooms.map((room) => safeRoomResult(accessFromEntry(room))),
783
+ };
784
+ }
785
+ export async function showRoomProfile(options) {
786
+ const metadataPath = defaultMetadataPath(options.cwd);
787
+ const entry = await roomEntryByAliasOrThrow(metadataPath, options.alias);
788
+ return {
789
+ schema: 'fold.room.show.result.v1',
790
+ ok: true,
791
+ room: publicRoomResult(accessFromEntry(entry), entry.token),
792
+ metadata: {
793
+ path: metadataPath,
794
+ alias: entry.alias ?? options.alias,
795
+ },
796
+ };
797
+ }
798
+ export async function setRoomProfileUrls(options) {
799
+ const metadataPath = defaultMetadataPath(options.cwd);
800
+ const entry = await roomEntryByAliasOrThrow(metadataPath, options.alias);
801
+ const access = accessFromEntry(entry);
802
+ const nextAccess = {
803
+ ...access,
804
+ appUrl: options.appUrl ?? access.appUrl,
805
+ syncUrl: options.syncUrl ?? access.syncUrl,
806
+ serverUrl: options.syncUrl ?? access.syncUrl,
807
+ };
808
+ const now = new Date().toISOString();
809
+ const nextEntry = {
810
+ ...entry,
811
+ appUrl: nextAccess.appUrl,
812
+ syncUrl: nextAccess.syncUrl,
813
+ serverUrl: nextAccess.serverUrl,
814
+ roomUrl: roomUrlForAccess(nextAccess),
815
+ token: createRoomToken(nextAccess),
816
+ updatedAt: now,
817
+ lastUsedAt: now,
818
+ };
819
+ await upsertRoomMetadata(metadataPath, nextEntry);
820
+ return {
821
+ schema: 'fold.room.set-url.result.v1',
822
+ ok: true,
823
+ room: publicRoomResult(accessFromEntry(nextEntry), nextEntry.token),
824
+ metadata: {
825
+ path: metadataPath,
826
+ alias: options.alias,
827
+ },
828
+ };
829
+ }
830
+ export async function forgetRoomProfile(options) {
831
+ const metadataPath = defaultMetadataPath(options.cwd);
832
+ await removeRoomMetadataByAlias(metadataPath, options.alias);
833
+ return {
834
+ schema: 'fold.room.forget.result.v1',
835
+ ok: true,
836
+ metadata: {
837
+ path: metadataPath,
838
+ alias: options.alias,
839
+ },
840
+ };
841
+ }
842
+ export async function createRoomInvite(options) {
843
+ const metadataPath = defaultMetadataPath(options.cwd);
844
+ const entry = await roomEntryByAliasOrThrow(metadataPath, options.alias);
845
+ const access = accessFromEntry(entry);
846
+ const roomUrl = roomUrlForAccess(access);
847
+ const warnings = shareabilityWarnings(access);
848
+ const skillUrl = `${access.appUrl.replace(/\/$/, '')}/.well-known/fold/agent-skill.md`;
849
+ const bootstrapCommand = `${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} bootstrap --room ${JSON.stringify(entry.token)} --alias ${JSON.stringify(options.alias)} --output ./fold-project --json`;
850
+ const agentInviteText = [
851
+ 'Join this Fold project room:',
852
+ '',
853
+ '1. Run the pinned Fold agent CLI. It installs the Fold skill locally and resumes the encrypted project:',
854
+ ` ${bootstrapCommand}`,
855
+ '',
856
+ ' Do not use /usr/bin/fold. That is the Unix text wrapper, not Fold.',
857
+ ' The Fold skill teaches agent behavior; fold-agent performs encrypted room operations.',
858
+ '',
859
+ `2. Optional reference skill: ${skillUrl}`,
860
+ '',
861
+ ' Inside a cloned Fold repo during development, the equivalent local command is:',
862
+ ` npm run --silent cli -- bootstrap --room ${JSON.stringify(entry.token)} --alias ${JSON.stringify(options.alias)} --output ./fold-project --json`,
863
+ '',
864
+ '3. Post fresh Markdown files directly; propose changes to existing files:',
865
+ ` ${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} post ./fold-project/NEW_FILE.md --room ${JSON.stringify(options.alias)} --path "NEW_FILE.md" --json`,
866
+ ` ${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} propose ./fold-project --room ${JSON.stringify(options.alias)} --title "Describe the change" --comment "Summarize what changed." --json`,
867
+ '',
868
+ '4. Answer human requests and join comment threads when clarification is better than a proposal:',
869
+ ` ${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} requests --room ${JSON.stringify(options.alias)} --json`,
870
+ ` ${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} comments --room ${JSON.stringify(options.alias)} --type comment --open --json`,
871
+ ` ${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} reply "<thread-id>" --room ${JSON.stringify(options.alias)} --text "Short reply." --json`,
872
+ ` ${DEFAULT_FOLD_AGENT_COMMAND_PREFIX} comment --room ${JSON.stringify(options.alias)} --path "docs/PLAN.md" --text "Short note." --json`,
873
+ ].join('\n');
874
+ const text = options.audience === 'agent'
875
+ ? agentInviteText
876
+ : [
877
+ 'Open this Fold project:',
878
+ roomUrl,
879
+ '',
880
+ 'This link contains the room key. Anyone with it can decrypt the project.',
881
+ ].join('\n');
882
+ return {
883
+ schema: 'fold.room.invite.result.v1',
884
+ ok: true,
885
+ audience: options.audience,
886
+ room: options.audience === 'agent'
887
+ ? safeRoomResult(access)
888
+ : publicRoomResult(access, entry.token),
889
+ warnings,
890
+ invite: {
891
+ text,
892
+ skillUrl: options.audience === 'agent' ? skillUrl : null,
893
+ },
894
+ };
895
+ }
896
+ function publicRoomResult(access, token) {
897
+ return {
898
+ roomId: access.roomId,
899
+ alias: 'alias' in access && typeof access.alias === 'string'
900
+ ? access.alias
901
+ : null,
902
+ appUrl: access.appUrl,
903
+ syncUrl: access.syncUrl,
904
+ serverUrl: access.serverUrl,
905
+ serverRoomUrl: serverRoomUrlForAccess(access),
906
+ url: roomUrlForAccess(access),
907
+ token,
908
+ hasClientKey: true,
909
+ };
910
+ }
911
+ function safeRoomResult(access) {
912
+ return {
913
+ roomId: access.roomId,
914
+ alias: access.alias ?? null,
915
+ appUrl: access.appUrl,
916
+ syncUrl: access.syncUrl,
917
+ serverUrl: access.serverUrl,
918
+ serverRoomUrl: serverRoomUrlForAccess(access),
919
+ hasClientKey: true,
920
+ };
921
+ }
922
+ function publicProposalSummary(proposal) {
923
+ return {
924
+ id: proposal.id,
925
+ kind: proposal.kind,
926
+ title: proposal.title,
927
+ comment: proposal.comment,
928
+ status: proposal.status,
929
+ createdAt: proposal.createdAt,
930
+ updatedAt: proposal.statusUpdatedAt,
931
+ persona: proposal.persona,
932
+ base: proposal.base,
933
+ proposed: summarizeMarkdown(proposal.proposed.markdown),
934
+ path: proposal.path,
935
+ project: proposal.proposedProject ? summarizeProject(proposal.proposedProject) : undefined,
936
+ };
937
+ }
938
+ function publicProposalListItem(proposal) {
939
+ return {
940
+ id: proposal.id,
941
+ title: proposal.title,
942
+ comment: proposal.comment,
943
+ status: proposal.status,
944
+ createdAt: proposal.createdAt,
945
+ updatedAt: proposal.statusUpdatedAt,
946
+ persona: proposal.persona,
947
+ base: proposal.base,
948
+ proposed: summarizeMarkdown(proposal.proposed.markdown),
949
+ path: proposal.path,
950
+ project: proposal.proposedProject ? summarizeProject(proposal.proposedProject) : undefined,
951
+ };
952
+ }
953
+ async function resolveRoomReference(cwd, input) {
954
+ try {
955
+ return parseRoomReference(input);
956
+ }
957
+ catch (error) {
958
+ const metadataPath = defaultMetadataPath(cwd);
959
+ const entry = await findRoomMetadataByAlias(metadataPath, input);
960
+ if (!entry)
961
+ throw error;
962
+ return {
963
+ ...parseRoomReference(entry.token),
964
+ kind: 'token',
965
+ ...aliasAccess(accessFromEntry(entry), entry.alias ?? input),
966
+ roomUrl: roomUrlForAccess(accessFromEntry(entry)),
967
+ serverRoomUrl: serverRoomUrlForAccess(accessFromEntry(entry)),
968
+ };
969
+ }
970
+ }
971
+ function accessFromEntry(entry) {
972
+ return aliasAccess({
973
+ roomId: entry.roomId,
974
+ roomSecret: parseRoomReference(entry.token).roomSecret,
975
+ appUrl: entry.appUrl ?? entry.serverUrl,
976
+ syncUrl: entry.syncUrl ?? entry.serverUrl,
977
+ serverUrl: entry.syncUrl ?? entry.serverUrl,
978
+ }, entry.alias);
979
+ }
980
+ function aliasAccess(access, alias) {
981
+ return alias ? { ...access, alias } : access;
982
+ }
983
+ async function roomEntryByAliasOrThrow(metadataPath, alias) {
984
+ const entry = await findRoomMetadataByAlias(metadataPath, alias);
985
+ if (!entry)
986
+ throw new Error(`Room alias not found: ${alias}`);
987
+ return entry;
988
+ }
989
+ function defaultAliasForSource(filePath) {
990
+ return basename(filePath).replace(/\.md$/i, '') || 'room';
991
+ }
992
+ function shareabilityWarnings(access) {
993
+ const warnings = [];
994
+ for (const [label, value] of [['appUrl', access.appUrl], ['syncUrl', access.syncUrl]]) {
995
+ const host = new URL(value).hostname;
996
+ if (host === 'localhost' ||
997
+ host === '127.0.0.1' ||
998
+ host === '::1' ||
999
+ host.startsWith('10.') ||
1000
+ host.startsWith('192.168.') ||
1001
+ /^172\.(1[6-9]|2\d|3[0-1])\./.test(host)) {
1002
+ warnings.push(`${label} ${value} may only be reachable on the host machine or local network.`);
1003
+ }
1004
+ }
1005
+ return warnings;
1006
+ }
1007
+ function inferSingleFileProposalPath(baseProject, inputProject) {
1008
+ if (inputProject.files.length !== 1)
1009
+ return undefined;
1010
+ const inputPath = inputProject.files[0].path;
1011
+ if (baseProject.files.some((file) => file.path === inputPath))
1012
+ return inputPath;
1013
+ if (baseProject.files.length === 1)
1014
+ return baseProject.primaryPath;
1015
+ throw new Error(`Use --path to choose which room file ${inputPath} should replace`);
1016
+ }
1017
+ function preserveBasePrimaryPath(baseProject, inputProject) {
1018
+ return normalizeProjectSnapshot({
1019
+ ...inputProject,
1020
+ primaryPath: inputProject.files.some((file) => file.path === baseProject.primaryPath)
1021
+ ? baseProject.primaryPath
1022
+ : inputProject.primaryPath,
1023
+ });
1024
+ }
1025
+ function projectNewFilePaths(baseProject, inputProject) {
1026
+ const basePaths = new Set(baseProject.files.map((file) => file.path));
1027
+ return inputProject.files
1028
+ .map((file) => file.path)
1029
+ .filter((path) => !basePaths.has(path));
1030
+ }
1031
+ async function decryptLocalSnapshotOrThrow(entry, access) {
1032
+ if (!entry) {
1033
+ throw new Error('No server records or local metadata found for room');
1034
+ }
1035
+ return decryptMarkdownSnapshot(entry.encryptedSnapshot, access);
1036
+ }
1037
+ async function currentMarkdownFromRecords(records, access) {
1038
+ const project = await currentProjectFromRecords(records, access);
1039
+ return projectFileOrThrow(project, project.primaryPath).markdown;
1040
+ }
1041
+ async function currentProjectFromRecords(records, access, entry) {
1042
+ let proposalsById;
1043
+ const fileAppliedSeq = new Map();
1044
+ let project;
1045
+ for (const record of records) {
1046
+ if (record.senderId.startsWith(PROJECT_UPDATE_SENDER_ID_PREFIX)) {
1047
+ const value = await decryptJsonRecord(access, record, record.senderId);
1048
+ if (!isReplayProjectSnapshot(value)) {
1049
+ throw new Error('Invalid encrypted project snapshot payload');
1050
+ }
1051
+ project = normalizeProjectSnapshot(value);
1052
+ fileAppliedSeq.clear();
1053
+ for (const file of project.files)
1054
+ fileAppliedSeq.set(file.path, record.seq);
1055
+ continue;
1056
+ }
1057
+ if (record.senderId.startsWith(WEB_PROJECT_FILE_SENDER_ID_PREFIX)) {
1058
+ const value = await decryptJsonRecord(access, record, record.senderId);
1059
+ if (!isReplayWebProjectFileSnapshot(value)) {
1060
+ throw new Error('Invalid encrypted web project file snapshot payload');
1061
+ }
1062
+ if (isStaleProjectFileSnapshotSeq(fileAppliedSeq.get(value.path), record.seq))
1063
+ continue;
1064
+ project = replaceProjectFile(project ?? singleFileProject(value.path, value.markdown), value.path, value.markdown);
1065
+ project = normalizeProjectSnapshot({ ...project, updatedAt: value.updatedAt });
1066
+ fileAppliedSeq.set(projectFileOrThrow(project, value.path).path, record.seq);
1067
+ continue;
1068
+ }
1069
+ if (!record.senderId.startsWith(TIMELINE_EVENT_SENDER_ID_PREFIX))
1070
+ continue;
1071
+ const event = await decryptTimelineEvent(access, record, record.senderId);
1072
+ if (event.type !== 'proposal_accepted' || !event.proposalId)
1073
+ continue;
1074
+ if (event.acceptedProject) {
1075
+ project = normalizeProjectSnapshot(event.acceptedProject);
1076
+ }
1077
+ else {
1078
+ if (!proposalsById) {
1079
+ const replay = await replayProposalsFromRecords(access, records);
1080
+ proposalsById = new Map(replay.proposals.map((proposal) => [proposal.id, proposal]));
1081
+ }
1082
+ const accepted = proposalsById.get(event.proposalId);
1083
+ if (accepted?.proposedProject) {
1084
+ project = normalizeProjectSnapshot(accepted.proposedProject);
1085
+ }
1086
+ else if (accepted) {
1087
+ project = singleFileProject(accepted.path ?? project?.primaryPath ?? 'document.md', accepted.proposed.markdown);
1088
+ }
1089
+ }
1090
+ if (project) {
1091
+ fileAppliedSeq.clear();
1092
+ for (const file of project.files)
1093
+ fileAppliedSeq.set(file.path, record.seq);
1094
+ }
1095
+ }
1096
+ if (!project) {
1097
+ const markdown = records.length > 0
1098
+ ? await decryptMarkdownFromRecords(records, access)
1099
+ : await decryptLocalSnapshotOrThrow(entry, access);
1100
+ project = singleFileProject(entry?.sourcePath ? basename(entry.sourcePath) : 'document.md', markdown);
1101
+ }
1102
+ return project;
1103
+ }
1104
+ function isReplayProjectSnapshot(value) {
1105
+ if (!value || typeof value !== 'object')
1106
+ return false;
1107
+ const candidate = value;
1108
+ return candidate.schema === 'fold.project.v1'
1109
+ && typeof candidate.primaryPath === 'string'
1110
+ && typeof candidate.updatedAt === 'string'
1111
+ && Array.isArray(candidate.files)
1112
+ && candidate.files.every((file) => (file &&
1113
+ typeof file === 'object' &&
1114
+ typeof file.path === 'string' &&
1115
+ typeof file.markdown === 'string'));
1116
+ }
1117
+ function isReplayWebProjectFileSnapshot(value) {
1118
+ if (!value || typeof value !== 'object')
1119
+ return false;
1120
+ const candidate = value;
1121
+ return candidate.type === 'project_file_snapshot'
1122
+ && typeof candidate.path === 'string'
1123
+ && typeof candidate.markdown === 'string'
1124
+ && typeof candidate.updatedAt === 'string';
1125
+ }
1126
+ function isStaleProjectFileSnapshot(currentUpdatedAt, nextUpdatedAt) {
1127
+ if (!currentUpdatedAt)
1128
+ return false;
1129
+ const currentTime = Date.parse(currentUpdatedAt);
1130
+ const nextTime = Date.parse(nextUpdatedAt);
1131
+ if (!Number.isNaN(currentTime) && !Number.isNaN(nextTime)) {
1132
+ return nextTime < currentTime;
1133
+ }
1134
+ return nextUpdatedAt < currentUpdatedAt;
1135
+ }
1136
+ async function getProposalOrThrow(options) {
1137
+ const reference = await resolveRoomReference(options.cwd, options.room);
1138
+ const records = await listEncryptedUpdates(reference);
1139
+ const replay = await replayProposalsFromRecords(reference, records);
1140
+ return {
1141
+ reference,
1142
+ records,
1143
+ proposal: findProposalInReplay(replay.proposals, options.proposalId),
1144
+ timeline: replay.timeline,
1145
+ };
1146
+ }
1147
+ function findProposalInReplay(proposals, proposalId) {
1148
+ const proposal = proposals.find((candidate) => candidate.id === proposalId);
1149
+ if (!proposal) {
1150
+ throw new Error(`Proposal not found: ${proposalId}`);
1151
+ }
1152
+ return proposal;
1153
+ }
1154
+ function assertPendingProposal(proposal) {
1155
+ if (proposal.status !== 'pending') {
1156
+ throw new Error(`Proposal ${proposal.id} is already ${proposal.status}`);
1157
+ }
1158
+ }
1159
+ //# sourceMappingURL=operations.js.map