open-research-protocol 0.4.13 → 0.4.15

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 (52) hide show
  1. package/AGENT_INTEGRATION.md +50 -0
  2. package/README.md +273 -144
  3. package/bin/orp.js +14 -1
  4. package/cli/orp.py +14846 -9925
  5. package/docs/AGENT_LOOP.md +13 -0
  6. package/docs/AGENT_MODES.md +79 -0
  7. package/docs/CANONICAL_CLI_BOUNDARY.md +15 -0
  8. package/docs/EXCHANGE.md +94 -0
  9. package/docs/LAUNCH_KIT.md +107 -0
  10. package/docs/ORP_HOSTED_WORKSPACE_CONTRACT.md +295 -0
  11. package/docs/ORP_PUBLIC_LAUNCH_CHECKLIST.md +5 -0
  12. package/docs/START_HERE.md +567 -0
  13. package/package.json +5 -2
  14. package/packages/lifeops-orp/README.md +67 -0
  15. package/packages/lifeops-orp/package.json +48 -0
  16. package/packages/lifeops-orp/src/index.d.ts +106 -0
  17. package/packages/lifeops-orp/src/index.js +7 -0
  18. package/packages/lifeops-orp/src/mapping.js +309 -0
  19. package/packages/lifeops-orp/src/workspace.js +108 -0
  20. package/packages/lifeops-orp/test/orp.test.js +187 -0
  21. package/packages/orp-workspace-launcher/README.md +82 -0
  22. package/packages/orp-workspace-launcher/package.json +39 -0
  23. package/packages/orp-workspace-launcher/src/commands.js +77 -0
  24. package/packages/orp-workspace-launcher/src/core-plan.js +506 -0
  25. package/packages/orp-workspace-launcher/src/hosted-state.js +208 -0
  26. package/packages/orp-workspace-launcher/src/index.js +82 -0
  27. package/packages/orp-workspace-launcher/src/ledger.js +745 -0
  28. package/packages/orp-workspace-launcher/src/list.js +488 -0
  29. package/packages/orp-workspace-launcher/src/orp-command.js +126 -0
  30. package/packages/orp-workspace-launcher/src/orp.js +912 -0
  31. package/packages/orp-workspace-launcher/src/registry.js +558 -0
  32. package/packages/orp-workspace-launcher/src/slot.js +188 -0
  33. package/packages/orp-workspace-launcher/src/sync.js +363 -0
  34. package/packages/orp-workspace-launcher/src/tabs.js +166 -0
  35. package/packages/orp-workspace-launcher/test/commands.test.js +164 -0
  36. package/packages/orp-workspace-launcher/test/core-plan.test.js +253 -0
  37. package/packages/orp-workspace-launcher/test/fixtures/smoke-notes.txt +2 -0
  38. package/packages/orp-workspace-launcher/test/fixtures/workspace-manifest.json +17 -0
  39. package/packages/orp-workspace-launcher/test/ledger.test.js +244 -0
  40. package/packages/orp-workspace-launcher/test/list.test.js +299 -0
  41. package/packages/orp-workspace-launcher/test/orp-command.test.js +44 -0
  42. package/packages/orp-workspace-launcher/test/orp.test.js +224 -0
  43. package/packages/orp-workspace-launcher/test/tabs.test.js +168 -0
  44. package/scripts/orp-kernel-agent-pilot.py +10 -1
  45. package/scripts/orp-kernel-agent-replication.py +10 -1
  46. package/scripts/orp-kernel-canonical-continuation.py +10 -1
  47. package/scripts/orp-kernel-continuation-pilot.py +10 -1
  48. package/scripts/render-terminal-demo.py +416 -0
  49. package/spec/v1/exchange-report.schema.json +105 -0
  50. package/spec/v1/hosted-workspace-event.schema.json +102 -0
  51. package/spec/v1/hosted-workspace.schema.json +332 -0
  52. package/spec/v1/workspace.schema.json +108 -0
@@ -0,0 +1,745 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+
5
+ import {
6
+ buildCanonicalResumeCommand,
7
+ deriveBaseTitle,
8
+ normalizeWorkspaceManifest,
9
+ parseWorkspaceSource,
10
+ resolveResumeMetadata,
11
+ } from "./core-plan.js";
12
+ import { buildHostedWorkspaceState } from "./hosted-state.js";
13
+ import {
14
+ buildWorkspaceManifestFromHostedWorkspacePayload,
15
+ fetchIdeaPayload,
16
+ fetchHostedWorkspacePayload,
17
+ loadWorkspaceSource,
18
+ pushHostedWorkspaceState,
19
+ resolveWorkspaceWatchTargets,
20
+ updateIdeaPayload,
21
+ } from "./orp.js";
22
+ import {
23
+ cacheManagedWorkspaceManifest,
24
+ loadWorkspaceRegistry,
25
+ loadWorkspaceSlots,
26
+ registerWorkspaceManifest,
27
+ setWorkspaceSlot,
28
+ } from "./registry.js";
29
+ import { buildWorkspaceSyncPreview, resolveWorkspaceSyncTargetIdeaId, validateWorkspaceTitle } from "./sync.js";
30
+
31
+ function normalizeOptionalString(value) {
32
+ if (value == null) {
33
+ return null;
34
+ }
35
+ const trimmed = String(value).trim();
36
+ return trimmed.length > 0 ? trimmed : null;
37
+ }
38
+
39
+ function validateAbsolutePath(value, label) {
40
+ const normalized = normalizeOptionalString(value);
41
+ if (!normalized || !normalized.startsWith("/")) {
42
+ throw new Error(`${label} must be an absolute path`);
43
+ }
44
+ return normalized;
45
+ }
46
+
47
+ function serializeManifest(manifest) {
48
+ return `${JSON.stringify(materializeWorkspaceManifest(manifest), null, 2)}\n`;
49
+ }
50
+
51
+ function materializeWorkspaceTab(tab) {
52
+ const resume = resolveResumeMetadata(tab);
53
+ return Object.fromEntries(
54
+ Object.entries({
55
+ title: normalizeOptionalString(tab.title) || undefined,
56
+ path: tab.path,
57
+ resumeCommand: resume.resumeCommand || undefined,
58
+ resumeTool: resume.resumeTool || undefined,
59
+ resumeSessionId: resume.resumeSessionId || undefined,
60
+ codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
61
+ claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
62
+ }).filter(([, value]) => value !== undefined),
63
+ );
64
+ }
65
+
66
+ function materializeWorkspaceManifest(manifest) {
67
+ const normalized = normalizeWorkspaceManifest(manifest);
68
+ return Object.fromEntries(
69
+ Object.entries({
70
+ version: normalized.version,
71
+ workspaceId: normalized.workspaceId || undefined,
72
+ title: normalized.title || undefined,
73
+ capture: normalized.capture || undefined,
74
+ tabs: normalized.tabs.map((tab) => materializeWorkspaceTab(tab)),
75
+ }).filter(([, value]) => value !== undefined),
76
+ );
77
+ }
78
+
79
+ function normalizeEditableManifest(source, parsed) {
80
+ const baseManifest = parsed.manifest
81
+ ? {
82
+ version: parsed.manifest.version,
83
+ workspaceId: parsed.manifest.workspaceId,
84
+ title: parsed.manifest.title,
85
+ capture: parsed.manifest.capture,
86
+ tabs: parsed.manifest.tabs.map((entry) => {
87
+ const resume = resolveResumeMetadata(entry);
88
+ return Object.fromEntries(
89
+ Object.entries({
90
+ title: normalizeOptionalString(entry.title) || undefined,
91
+ path: entry.path,
92
+ resumeCommand: resume.resumeCommand || undefined,
93
+ resumeTool: resume.resumeTool || undefined,
94
+ resumeSessionId: resume.resumeSessionId || undefined,
95
+ codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
96
+ claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
97
+ }).filter(([, value]) => value !== undefined),
98
+ );
99
+ }),
100
+ }
101
+ : {
102
+ version: "1",
103
+ workspaceId: source.workspaceManifest?.workspaceId || source.title || "workspace",
104
+ title: source.workspaceManifest?.title || source.title || null,
105
+ capture: source.workspaceManifest?.capture || null,
106
+ tabs: parsed.entries.map((entry) => {
107
+ const resume = resolveResumeMetadata(entry);
108
+ return Object.fromEntries(
109
+ Object.entries({
110
+ title: normalizeOptionalString(entry.title) || deriveBaseTitle(entry),
111
+ path: entry.path,
112
+ resumeCommand: resume.resumeCommand || undefined,
113
+ resumeTool: resume.resumeTool || undefined,
114
+ resumeSessionId: resume.resumeSessionId || undefined,
115
+ codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
116
+ claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
117
+ }).filter(([, value]) => value !== undefined),
118
+ );
119
+ }),
120
+ };
121
+
122
+ return normalizeWorkspaceManifest(baseManifest);
123
+ }
124
+
125
+ function parseLedgerSelectorArgs(argv = [], { commandName, requirePath = false, requireSelector = true } = {}) {
126
+ const options = {
127
+ json: false,
128
+ all: false,
129
+ };
130
+
131
+ for (let index = 0; index < argv.length; index += 1) {
132
+ const arg = argv[index];
133
+
134
+ if (arg === "-h" || arg === "--help") {
135
+ options.help = true;
136
+ continue;
137
+ }
138
+ if (arg === "--json") {
139
+ options.json = true;
140
+ continue;
141
+ }
142
+ if (arg === "--all") {
143
+ options.all = true;
144
+ continue;
145
+ }
146
+ if (arg.startsWith("--")) {
147
+ const next = argv[index + 1];
148
+ if (next == null || next.startsWith("--")) {
149
+ throw new Error(`missing value for ${arg}`);
150
+ }
151
+
152
+ if (arg === "--workspace-file") {
153
+ options.workspaceFile = next;
154
+ } else if (arg === "--hosted-workspace-id") {
155
+ options.hostedWorkspaceId = next;
156
+ } else if (arg === "--base-url") {
157
+ options.baseUrl = next;
158
+ } else if (arg === "--orp-command") {
159
+ options.orpCommand = next;
160
+ } else if (arg === "--path") {
161
+ options.path = next;
162
+ } else if (arg === "--title") {
163
+ options.title = next;
164
+ } else if (arg === "--resume-command") {
165
+ options.resumeCommand = next;
166
+ } else if (arg === "--resume-tool") {
167
+ options.resumeTool = next;
168
+ } else if (arg === "--resume-session-id") {
169
+ options.resumeSessionId = next;
170
+ } else if (arg === "--index") {
171
+ options.index = next;
172
+ } else {
173
+ throw new Error(`unknown option: ${arg}`);
174
+ }
175
+
176
+ index += 1;
177
+ continue;
178
+ }
179
+
180
+ if (options.ideaId) {
181
+ throw new Error(`unexpected argument: ${arg}`);
182
+ }
183
+ options.ideaId = arg;
184
+ }
185
+
186
+ if (options.help) {
187
+ return options;
188
+ }
189
+
190
+ if (requireSelector && !options.ideaId && !options.workspaceFile && !options.hostedWorkspaceId) {
191
+ throw new Error(`Provide a workspace selector for \`${commandName}\`.`);
192
+ }
193
+ if (requirePath && !options.path) {
194
+ throw new Error(`--path is required for \`${commandName}\`.`);
195
+ }
196
+ if (options.path) {
197
+ options.path = validateAbsolutePath(options.path, "--path");
198
+ }
199
+ if (options.index != null) {
200
+ const parsed = Number.parseInt(String(options.index), 10);
201
+ if (!Number.isInteger(parsed) || parsed < 1) {
202
+ throw new Error("--index must be a positive integer");
203
+ }
204
+ options.index = parsed;
205
+ }
206
+
207
+ return options;
208
+ }
209
+
210
+ export function parseWorkspaceAddTabArgs(argv = []) {
211
+ return parseLedgerSelectorArgs(argv, {
212
+ commandName: "orp workspace add-tab",
213
+ requirePath: true,
214
+ requireSelector: true,
215
+ });
216
+ }
217
+
218
+ export function parseWorkspaceCreateArgs(argv = []) {
219
+ const options = {
220
+ json: false,
221
+ };
222
+
223
+ for (let index = 0; index < argv.length; index += 1) {
224
+ const arg = argv[index];
225
+
226
+ if (arg === "-h" || arg === "--help") {
227
+ options.help = true;
228
+ continue;
229
+ }
230
+ if (arg === "--json") {
231
+ options.json = true;
232
+ continue;
233
+ }
234
+ if (arg.startsWith("--")) {
235
+ const next = argv[index + 1];
236
+ if (next == null || next.startsWith("--")) {
237
+ throw new Error(`missing value for ${arg}`);
238
+ }
239
+ if (arg === "--workspace-file") {
240
+ options.workspaceFile = next;
241
+ } else if (arg === "--slot") {
242
+ options.slotName = next;
243
+ } else if (arg === "--path") {
244
+ options.path = next;
245
+ } else if (arg === "--resume-command") {
246
+ options.resumeCommand = next;
247
+ } else if (arg === "--resume-tool") {
248
+ options.resumeTool = next;
249
+ } else if (arg === "--resume-session-id") {
250
+ options.resumeSessionId = next;
251
+ } else {
252
+ throw new Error(`unknown option: ${arg}`);
253
+ }
254
+ index += 1;
255
+ continue;
256
+ }
257
+
258
+ if (options.title) {
259
+ throw new Error(`unexpected argument: ${arg}`);
260
+ }
261
+ options.title = arg;
262
+ }
263
+
264
+ if (options.help) {
265
+ return options;
266
+ }
267
+
268
+ options.title = validateWorkspaceTitle(options.title, "workspace title");
269
+ if (options.path) {
270
+ options.path = validateAbsolutePath(options.path, "--path");
271
+ }
272
+ if (options.slotName) {
273
+ const slot = String(options.slotName || "").trim().toLowerCase();
274
+ if (slot !== "main" && slot !== "offhand") {
275
+ throw new Error("--slot must be one of: main, offhand");
276
+ }
277
+ options.slotName = slot;
278
+ }
279
+
280
+ return options;
281
+ }
282
+
283
+ export function parseWorkspaceRemoveTabArgs(argv = []) {
284
+ const options = parseLedgerSelectorArgs(argv, {
285
+ commandName: "orp workspace remove-tab",
286
+ requirePath: false,
287
+ requireSelector: true,
288
+ });
289
+ if (options.help) {
290
+ return options;
291
+ }
292
+ if (
293
+ options.index == null &&
294
+ !options.path &&
295
+ !options.title &&
296
+ !options.resumeCommand &&
297
+ !options.resumeSessionId
298
+ ) {
299
+ throw new Error("Provide at least one selector like --index, --path, --title, --resume-command, or --resume-session-id.");
300
+ }
301
+ return options;
302
+ }
303
+
304
+ export function addTabToManifest(manifest, options = {}) {
305
+ const nextManifest = normalizeWorkspaceManifest({
306
+ ...manifest,
307
+ tabs: manifest.tabs.map((tab) => ({ ...tab })),
308
+ });
309
+ const resume = resolveResumeMetadata({
310
+ resumeCommand: options.resumeCommand,
311
+ resumeTool: options.resumeTool,
312
+ resumeSessionId: options.resumeSessionId,
313
+ });
314
+ const nextTab = Object.fromEntries(
315
+ Object.entries({
316
+ title: normalizeOptionalString(options.title) || undefined,
317
+ path: validateAbsolutePath(options.path, "--path"),
318
+ resumeCommand: resume.resumeCommand || undefined,
319
+ resumeTool: resume.resumeTool || undefined,
320
+ resumeSessionId: resume.resumeSessionId || undefined,
321
+ codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
322
+ claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
323
+ }).filter(([, value]) => value !== undefined),
324
+ );
325
+
326
+ const duplicate = nextManifest.tabs.find((tab) => {
327
+ const existingResume = resolveResumeMetadata(tab);
328
+ return (
329
+ tab.path === nextTab.path &&
330
+ normalizeOptionalString(tab.title) === normalizeOptionalString(nextTab.title) &&
331
+ existingResume.resumeCommand === (nextTab.resumeCommand || null)
332
+ );
333
+ });
334
+
335
+ if (duplicate) {
336
+ return {
337
+ manifest: nextManifest,
338
+ added: false,
339
+ tab: duplicate,
340
+ };
341
+ }
342
+
343
+ nextManifest.tabs.push(nextTab);
344
+ return {
345
+ manifest: normalizeWorkspaceManifest(nextManifest),
346
+ added: true,
347
+ tab: nextTab,
348
+ };
349
+ }
350
+
351
+ function tabMatchesRemoval(tab, filters = {}, index) {
352
+ const resume = resolveResumeMetadata(tab);
353
+ if (filters.index != null && filters.index !== index + 1) {
354
+ return false;
355
+ }
356
+ if (filters.path && tab.path !== filters.path) {
357
+ return false;
358
+ }
359
+ if (filters.title && normalizeOptionalString(tab.title) !== normalizeOptionalString(filters.title)) {
360
+ return false;
361
+ }
362
+ if (filters.resumeCommand && resume.resumeCommand !== normalizeOptionalString(filters.resumeCommand)) {
363
+ return false;
364
+ }
365
+ if (filters.resumeSessionId && resume.resumeSessionId !== normalizeOptionalString(filters.resumeSessionId)) {
366
+ return false;
367
+ }
368
+ if (filters.resumeTool && resume.resumeTool !== normalizeOptionalString(filters.resumeTool)?.toLowerCase()) {
369
+ return false;
370
+ }
371
+ return true;
372
+ }
373
+
374
+ export function removeTabsFromManifest(manifest, filters = {}) {
375
+ const nextManifest = normalizeWorkspaceManifest({
376
+ ...manifest,
377
+ tabs: manifest.tabs.map((tab) => ({ ...tab })),
378
+ });
379
+ const matchedIndexes = nextManifest.tabs
380
+ .map((tab, index) => (tabMatchesRemoval(tab, filters, index) ? index : -1))
381
+ .filter((index) => index >= 0);
382
+
383
+ if (matchedIndexes.length === 0) {
384
+ throw new Error("No saved tab matched the provided selectors.");
385
+ }
386
+ if (!filters.all && matchedIndexes.length > 1) {
387
+ throw new Error("Multiple saved tabs matched the provided selectors. Narrow it down or pass --all.");
388
+ }
389
+
390
+ const removalSet = new Set(filters.all ? matchedIndexes : [matchedIndexes[0]]);
391
+ const removedTabs = nextManifest.tabs.filter((_, index) => removalSet.has(index));
392
+ nextManifest.tabs = nextManifest.tabs.filter((_, index) => !removalSet.has(index));
393
+
394
+ if (nextManifest.tabs.length === 0) {
395
+ throw new Error("Refusing to remove every saved tab from the workspace.");
396
+ }
397
+
398
+ return {
399
+ manifest: normalizeWorkspaceManifest(nextManifest),
400
+ removedTabs,
401
+ };
402
+ }
403
+
404
+ async function persistWorkspaceManifest(source, manifest, options = {}) {
405
+ const watchTargets = resolveWorkspaceWatchTargets(source, options);
406
+
407
+ if (source.sourceType === "workspace-file" && source.sourcePath) {
408
+ await fs.writeFile(source.sourcePath, serializeManifest(manifest), "utf8");
409
+ const registration = await registerWorkspaceManifest(source.sourcePath, manifest, options);
410
+ return {
411
+ persistedTo: "workspace-file",
412
+ manifestPath: source.sourcePath,
413
+ registryPath: registration.registryPath,
414
+ manifest,
415
+ };
416
+ }
417
+
418
+ if (watchTargets.syncIdeaSelector) {
419
+ const targetSource = await loadWorkspaceSource({
420
+ ...options,
421
+ ideaId: watchTargets.syncIdeaSelector,
422
+ });
423
+ const targetIdeaId = resolveWorkspaceSyncTargetIdeaId(targetSource);
424
+ if (!targetIdeaId) {
425
+ throw new Error(`Workspace source does not resolve to a syncable hosted idea: ${watchTargets.syncIdeaSelector}`);
426
+ }
427
+ const targetPayload =
428
+ targetSource.sourceType === "hosted-idea" && targetSource.idea?.id === targetIdeaId
429
+ ? targetSource.payload
430
+ : await fetchIdeaPayload(targetIdeaId, options);
431
+ const liveSource = {
432
+ sourceType: "workspace-file",
433
+ sourceLabel: `edited-workspace:${watchTargets.syncIdeaSelector}`,
434
+ title: manifest.title || manifest.workspaceId || source.title || watchTargets.syncIdeaSelector,
435
+ workspaceManifest: manifest,
436
+ notes: "",
437
+ };
438
+ const parsed = parseWorkspaceSource(liveSource);
439
+ const preview = buildWorkspaceSyncPreview({
440
+ source: liveSource,
441
+ parsed,
442
+ targetIdea: targetPayload.idea,
443
+ workspaceTitle: manifest.title || manifest.workspaceId || undefined,
444
+ });
445
+ const updatedIdea = await updateIdeaPayload(targetIdeaId, { notes: preview.nextNotes }, options);
446
+ const managedCache = await cacheManagedWorkspaceManifest(preview.manifest, options);
447
+ return {
448
+ persistedTo: "hosted-idea",
449
+ ideaId: targetIdeaId,
450
+ updatedIdea,
451
+ managedCache,
452
+ manifest: preview.manifest,
453
+ };
454
+ }
455
+
456
+ if (watchTargets.hostedWorkspaceId) {
457
+ const previousWorkspace =
458
+ source.hostedWorkspace ||
459
+ (await fetchHostedWorkspacePayload(watchTargets.hostedWorkspaceId, options)).workspace;
460
+ const state = buildHostedWorkspaceState(manifest, {
461
+ previousWorkspace,
462
+ capturedAt: manifest.capture?.capturedAt,
463
+ updatedAt: new Date().toISOString(),
464
+ });
465
+ const pushResult = await pushHostedWorkspaceState(watchTargets.hostedWorkspaceId, state, options);
466
+ const cachedManifest = buildWorkspaceManifestFromHostedWorkspacePayload(pushResult);
467
+ const managedCache = await cacheManagedWorkspaceManifest(cachedManifest, options);
468
+ return {
469
+ persistedTo: "hosted-workspace",
470
+ workspaceId: watchTargets.hostedWorkspaceId,
471
+ pushResult,
472
+ managedCache,
473
+ manifest: cachedManifest,
474
+ };
475
+ }
476
+
477
+ throw new Error("This workspace source cannot be edited in place yet. Use a saved workspace selector or --workspace-file.");
478
+ }
479
+
480
+ function printWorkspaceAddTabHelp() {
481
+ console.log(`ORP workspace add-tab
482
+
483
+ Usage:
484
+ orp workspace add-tab <name-or-id> --path <absolute-path> [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
485
+ orp workspace add-tab --hosted-workspace-id <workspace-id> --path <absolute-path> [--json]
486
+ orp workspace add-tab --workspace-file <path> --path <absolute-path> [--json]
487
+
488
+ Options:
489
+ --path <absolute-path> Add this local project path to the saved workspace
490
+ --title <title> Optional saved tab title
491
+ --resume-command <text> Exact saved resume command, like \`codex resume ...\` or \`claude --resume ...\`
492
+ --resume-tool <tool> Build the resume command from \`codex\` or \`claude\`
493
+ --resume-session-id <id> Resume session id to save with the tab
494
+ --hosted-workspace-id <id> Edit a first-class hosted workspace directly
495
+ --workspace-file <path> Edit a local structured workspace manifest
496
+ --json Print the updated workspace edit result as JSON
497
+ -h, --help Show this help text
498
+
499
+ Examples:
500
+ orp workspace add-tab main --path /Volumes/Code_2TB/code/new-project
501
+ orp workspace add-tab main --path /Volumes/Code_2TB/code/new-project --resume-command "codex resume 019d..."
502
+ orp workspace add-tab main --path /Volumes/Code_2TB/code/new-project --resume-tool claude --resume-session-id claude-456
503
+ `);
504
+ }
505
+
506
+ function printWorkspaceCreateHelp() {
507
+ console.log(`ORP workspace create
508
+
509
+ Usage:
510
+ orp workspace create <title-slug> [--workspace-file <path>] [--slot <main|offhand>] [--path <absolute-path>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
511
+
512
+ Options:
513
+ <title-slug> Required local workspace title using lowercase letters, numbers, and dashes only
514
+ --workspace-file <path> Create the workspace manifest at an explicit local path instead of the managed ORP workspace directory
515
+ --slot <main|offhand> Optionally assign the created workspace to a named slot
516
+ --path <absolute-path> Optionally seed the workspace with one saved path immediately
517
+ --resume-command <text> Exact saved resume command, like \`codex resume ...\` or \`claude --resume ...\`
518
+ --resume-tool <tool> Build the resume command from \`codex\` or \`claude\`
519
+ --resume-session-id <id> Resume session id to save with the first tab
520
+ --json Print the created workspace result as JSON
521
+ -h, --help Show this help text
522
+
523
+ Examples:
524
+ orp workspace create main-cody-1
525
+ orp workspace create main-cody-1 --slot main
526
+ orp workspace create research-lab --path /Volumes/Code_2TB/code/research-lab
527
+ orp workspace create research-lab --path /Volumes/Code_2TB/code/research-lab --resume-tool claude --resume-session-id 469d99b2-2997-42bf-a8f5-3812c808ef29
528
+ `);
529
+ }
530
+
531
+ function printWorkspaceRemoveTabHelp() {
532
+ console.log(`ORP workspace remove-tab
533
+
534
+ Usage:
535
+ orp workspace remove-tab <name-or-id> (--index <n> | --path <absolute-path> | --title <title> | --resume-session-id <id> | --resume-command <text>) [--all] [--json]
536
+ orp workspace remove-tab --hosted-workspace-id <workspace-id> ... [--json]
537
+ orp workspace remove-tab --workspace-file <path> ... [--json]
538
+
539
+ Options:
540
+ --index <n> Remove the saved tab at 1-based index \`n\`
541
+ --path <absolute-path> Match saved tabs by absolute path
542
+ --title <title> Match saved tabs by title
543
+ --resume-command <text> Match saved tabs by exact resume command
544
+ --resume-session-id <id> Match saved tabs by resume session id
545
+ --resume-tool <tool> Narrow removal to \`codex\` or \`claude\`
546
+ --all Remove every matching tab instead of requiring one exact match
547
+ --hosted-workspace-id <id> Edit a first-class hosted workspace directly
548
+ --workspace-file <path> Edit a local structured workspace manifest
549
+ --json Print the updated workspace edit result as JSON
550
+ -h, --help Show this help text
551
+
552
+ Examples:
553
+ orp workspace remove-tab main --index 11
554
+ orp workspace remove-tab main --path /Volumes/Code_2TB/code/frg-site --resume-session-id 019d348d-5031-78e1-9840-a66deaac33ae
555
+ orp workspace remove-tab main --title frg-site
556
+ `);
557
+ }
558
+
559
+ function summarizeWorkspaceLedgerMutation(result) {
560
+ const lines = [
561
+ `Workspace: ${result.workspaceTitle || result.workspaceId || "workspace"}`,
562
+ `Action: ${result.action}`,
563
+ `Saved tabs: ${result.tabCount}`,
564
+ ];
565
+
566
+ if (result.action === "add-tab") {
567
+ lines.push(`Added: ${result.tab?.title || path.basename(result.tab?.path || "") || result.tab?.path}`);
568
+ lines.push(`Path: ${result.tab?.path}`);
569
+ if (result.tab?.resumeCommand) {
570
+ lines.push(`Resume: ${result.tab.resumeCommand}`);
571
+ }
572
+ } else if (result.action === "remove-tab") {
573
+ lines.push(`Removed: ${result.removedTabs.length}`);
574
+ for (const tab of result.removedTabs) {
575
+ lines.push(` - ${tab.title || path.basename(tab.path)} (${tab.path})`);
576
+ }
577
+ }
578
+
579
+ if (result.persistedTo === "hosted-idea") {
580
+ lines.push(`Canonical source: ORP idea ${result.ideaId}`);
581
+ } else if (result.persistedTo === "hosted-workspace") {
582
+ lines.push(`Canonical source: hosted workspace ${result.workspaceId}`);
583
+ } else if (result.persistedTo === "workspace-file") {
584
+ lines.push(`Saved file: ${result.manifestPath}`);
585
+ }
586
+
587
+ if (result.managedCachePath) {
588
+ lines.push(`Local cache: ${result.managedCachePath}`);
589
+ }
590
+
591
+ return lines.join("\n");
592
+ }
593
+
594
+ async function runWorkspaceLedgerMutation(options, mutate, action) {
595
+ const source = await loadWorkspaceSource(options);
596
+ const parsed = parseWorkspaceSource(source);
597
+ const manifest = normalizeEditableManifest(source, parsed);
598
+ const mutated = mutate(manifest, options);
599
+ const persisted = await persistWorkspaceManifest(source, mutated.manifest, options);
600
+ const finalManifest = materializeWorkspaceManifest(persisted.manifest || mutated.manifest);
601
+
602
+ const result = {
603
+ action,
604
+ workspaceId: finalManifest.workspaceId,
605
+ workspaceTitle: finalManifest.title || source.title || null,
606
+ tabCount: finalManifest.tabs.length,
607
+ tab: mutated.tab ? materializeWorkspaceTab(mutated.tab) : null,
608
+ removedTabs: (mutated.removedTabs || []).map((tab) => materializeWorkspaceTab(tab)),
609
+ persistedTo: persisted.persistedTo,
610
+ ideaId: persisted.ideaId || null,
611
+ workspaceSourceId: persisted.workspaceId || null,
612
+ manifestPath: persisted.manifestPath || null,
613
+ managedCachePath: persisted.managedCache?.manifestPath || null,
614
+ manifest: finalManifest,
615
+ };
616
+
617
+ if (options.json) {
618
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
619
+ return 0;
620
+ }
621
+
622
+ process.stdout.write(`${summarizeWorkspaceLedgerMutation(result)}\n`);
623
+ return 0;
624
+ }
625
+
626
+ export async function runWorkspaceAddTab(argv = process.argv.slice(2)) {
627
+ const options = parseWorkspaceAddTabArgs(argv);
628
+ if (options.help) {
629
+ printWorkspaceAddTabHelp();
630
+ return 0;
631
+ }
632
+ return runWorkspaceLedgerMutation(options, addTabToManifest, "add-tab");
633
+ }
634
+
635
+ export async function runWorkspaceRemoveTab(argv = process.argv.slice(2)) {
636
+ const options = parseWorkspaceRemoveTabArgs(argv);
637
+ if (options.help) {
638
+ printWorkspaceRemoveTabHelp();
639
+ return 0;
640
+ }
641
+ return runWorkspaceLedgerMutation(options, removeTabsFromManifest, "remove-tab");
642
+ }
643
+
644
+ export async function runWorkspaceCreate(argv = process.argv.slice(2)) {
645
+ const options = parseWorkspaceCreateArgs(argv);
646
+ if (options.help) {
647
+ printWorkspaceCreateHelp();
648
+ return 0;
649
+ }
650
+
651
+ const tabs = [];
652
+ if (options.path) {
653
+ const resume = resolveResumeMetadata({
654
+ resumeCommand: options.resumeCommand,
655
+ resumeTool: options.resumeTool,
656
+ resumeSessionId: options.resumeSessionId,
657
+ });
658
+ tabs.push(
659
+ Object.fromEntries(
660
+ Object.entries({
661
+ title: deriveBaseTitle({ path: options.path }),
662
+ path: options.path,
663
+ resumeCommand: resume.resumeCommand || undefined,
664
+ resumeTool: resume.resumeTool || undefined,
665
+ resumeSessionId: resume.resumeSessionId || undefined,
666
+ codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
667
+ claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
668
+ }).filter(([, value]) => value !== undefined),
669
+ ),
670
+ );
671
+ }
672
+
673
+ const manifest = normalizeWorkspaceManifest({
674
+ version: "1",
675
+ workspaceId: options.title,
676
+ title: options.title,
677
+ tabs,
678
+ });
679
+
680
+ let manifestPath = null;
681
+ let registryPath = null;
682
+ let managedCachePath = null;
683
+ if (options.workspaceFile) {
684
+ manifestPath = path.resolve(options.workspaceFile);
685
+ await fs.mkdir(path.dirname(manifestPath), { recursive: true });
686
+ await fs.writeFile(manifestPath, serializeManifest(manifest), "utf8");
687
+ const registration = await registerWorkspaceManifest(manifestPath, manifest, options);
688
+ registryPath = registration.registryPath;
689
+ } else {
690
+ const cached = await cacheManagedWorkspaceManifest(manifest, options);
691
+ manifestPath = cached.manifestPath;
692
+ registryPath = cached.registryPath;
693
+ managedCachePath = cached.manifestPath;
694
+ }
695
+
696
+ let assignedSlot = null;
697
+ const assignment = {
698
+ kind: "workspace-file",
699
+ selector: manifest.title,
700
+ workspaceId: manifest.workspaceId,
701
+ title: manifest.title,
702
+ manifestPath,
703
+ };
704
+ if (options.slotName) {
705
+ assignedSlot = (await setWorkspaceSlot(options.slotName, assignment, options)).slot;
706
+ } else {
707
+ const [registryResult, slotsResult] = await Promise.all([
708
+ loadWorkspaceRegistry(options),
709
+ loadWorkspaceSlots(options),
710
+ ]);
711
+ if (!slotsResult.slots?.main && Array.isArray(registryResult.registry?.workspaces) && registryResult.registry.workspaces.length === 1) {
712
+ assignedSlot = (await setWorkspaceSlot("main", assignment, options)).slot;
713
+ }
714
+ }
715
+
716
+ const result = {
717
+ ok: true,
718
+ action: "create",
719
+ workspaceId: manifest.workspaceId,
720
+ workspaceTitle: manifest.title,
721
+ tabCount: manifest.tabs.length,
722
+ manifestPath,
723
+ registryPath,
724
+ managedCachePath,
725
+ slot: assignedSlot,
726
+ manifest: materializeWorkspaceManifest(manifest),
727
+ };
728
+
729
+ if (options.json) {
730
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
731
+ return 0;
732
+ }
733
+
734
+ const lines = [
735
+ `Workspace: ${result.workspaceTitle}`,
736
+ "Action: create",
737
+ `Saved tabs: ${result.tabCount}`,
738
+ `Saved file: ${result.manifestPath}`,
739
+ ];
740
+ if (result.slot?.slot) {
741
+ lines.push(`Slot: ${result.slot.slot}`);
742
+ }
743
+ process.stdout.write(`${lines.join("\n")}\n`);
744
+ return 0;
745
+ }