open-research-protocol 0.4.14 → 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 +4 -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,188 @@
1
+ import process from "node:process";
2
+
3
+ import { buildWorkspaceInventory, applyWorkspaceSlotsToInventory } from "./list.js";
4
+ import { buildWorkspaceSlotAssignment, fetchHostedWorkspacesPayload, fetchIdeasPayload, resolveWorkspaceSelectorFromCollections } from "./orp.js";
5
+ import { clearWorkspaceSlot, listTrackedWorkspaces, loadWorkspaceSlots, normalizeWorkspaceSlotName, setWorkspaceSlot } from "./registry.js";
6
+
7
+ function normalizeOptionalString(value) {
8
+ if (value == null) {
9
+ return null;
10
+ }
11
+ const trimmed = String(value).trim();
12
+ return trimmed.length > 0 ? trimmed : null;
13
+ }
14
+
15
+ function printWorkspaceSlotHelp() {
16
+ console.log(`ORP workspace slot
17
+
18
+ Usage:
19
+ orp workspace slot list [--json]
20
+ orp workspace slot set <main|offhand> <name-or-id> [--json]
21
+ orp workspace slot clear <main|offhand> [--json]
22
+
23
+ Examples:
24
+ orp workspace slot list
25
+ orp workspace slot set main main-cody-1
26
+ orp workspace slot set offhand research-lab
27
+ orp workspace slot clear offhand
28
+ `);
29
+ }
30
+
31
+ function parseWorkspaceSlotArgs(argv = []) {
32
+ const options = {
33
+ json: false,
34
+ };
35
+
36
+ const positionals = [];
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const arg = argv[index];
39
+ if (arg === "-h" || arg === "--help") {
40
+ options.help = true;
41
+ continue;
42
+ }
43
+ if (arg === "--json") {
44
+ options.json = true;
45
+ continue;
46
+ }
47
+ positionals.push(arg);
48
+ }
49
+
50
+ options.subcommand = positionals[0] || null;
51
+ options.slotName = positionals[1] || null;
52
+ options.selector = positionals[2] || null;
53
+ if (positionals.length > 3) {
54
+ throw new Error(`unexpected argument: ${positionals[3]}`);
55
+ }
56
+ return options;
57
+ }
58
+
59
+ function summarizeSlotInventory(payload) {
60
+ const lines = ["Workspace slots"];
61
+ for (const slotName of ["main", "offhand"]) {
62
+ const slot = payload?.slots?.[slotName] || null;
63
+ if (!slot) {
64
+ lines.push(`${slotName}: unset`);
65
+ continue;
66
+ }
67
+ const label = normalizeOptionalString(slot.title) || normalizeOptionalString(slot.workspaceId) || normalizeOptionalString(slot.selector) || "unknown";
68
+ const extra =
69
+ normalizeOptionalString(slot.kind) === "workspace-file"
70
+ ? slot.manifestPath
71
+ : normalizeOptionalString(slot.ideaId) || normalizeOptionalString(slot.hostedWorkspaceId) || normalizeOptionalString(slot.workspaceId);
72
+ lines.push(`${slotName}: ${label}${extra ? ` [${extra}]` : ""}`);
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+
77
+ async function buildSlotInventory(options = {}) {
78
+ const [localResult, slotsResult] = await Promise.all([listTrackedWorkspaces(options), loadWorkspaceSlots(options)]);
79
+ let hostedResult = { source: null, workspaces: [] };
80
+ try {
81
+ hostedResult = await fetchHostedWorkspacesPayload(options);
82
+ } catch {
83
+ hostedResult = { source: null, workspaces: [] };
84
+ }
85
+ return applyWorkspaceSlotsToInventory(
86
+ buildWorkspaceInventory({
87
+ localResult,
88
+ hostedResult,
89
+ hostedError: null,
90
+ }),
91
+ slotsResult.slots,
92
+ );
93
+ }
94
+
95
+ async function resolveWorkspaceSelection(selector, options = {}) {
96
+ const [ideasPayload, hostedResult, localRegistry] = await Promise.all([
97
+ fetchIdeasPayload(options).catch(() => ({ ideas: [] })),
98
+ fetchHostedWorkspacesPayload(options).catch(() => ({ workspaces: [] })),
99
+ listTrackedWorkspaces(options).catch(() => ({ workspaces: [] })),
100
+ ]);
101
+ return resolveWorkspaceSelectorFromCollections(selector, {
102
+ ideas: ideasPayload.ideas,
103
+ hostedWorkspaces: hostedResult.workspaces,
104
+ localWorkspaces: localRegistry.workspaces,
105
+ });
106
+ }
107
+
108
+ export async function runWorkspaceSlot(argv = process.argv.slice(2)) {
109
+ const options = parseWorkspaceSlotArgs(argv);
110
+ if (options.help || !options.subcommand) {
111
+ printWorkspaceSlotHelp();
112
+ return 0;
113
+ }
114
+
115
+ if (options.subcommand === "list") {
116
+ const inventory = await buildSlotInventory(options);
117
+ const payload = {
118
+ ok: true,
119
+ slots: inventory.slots || {},
120
+ workspaces: inventory.workspaces.filter((workspace) => Array.isArray(workspace.slots) && workspace.slots.length > 0),
121
+ };
122
+ if (options.json) {
123
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
124
+ return 0;
125
+ }
126
+ process.stdout.write(`${summarizeSlotInventory(payload)}\n`);
127
+ return 0;
128
+ }
129
+
130
+ const slotName = normalizeWorkspaceSlotName(options.slotName);
131
+ if (!slotName) {
132
+ throw new Error("Provide a supported slot name: main or offhand.");
133
+ }
134
+
135
+ if (options.subcommand === "clear") {
136
+ const result = await clearWorkspaceSlot(slotName, options);
137
+ const payload = {
138
+ ok: true,
139
+ slotName,
140
+ cleared: result.cleared,
141
+ slotsPath: result.slotsPath,
142
+ };
143
+ if (options.json) {
144
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
145
+ return 0;
146
+ }
147
+ process.stdout.write(
148
+ `${result.cleared ? "Cleared" : "Left unchanged"} workspace slot '${slotName}'.\n`,
149
+ );
150
+ return 0;
151
+ }
152
+
153
+ if (options.subcommand === "set") {
154
+ const selector = normalizeOptionalString(options.selector);
155
+ if (!selector) {
156
+ throw new Error(`Provide a workspace name or id to assign to '${slotName}'.`);
157
+ }
158
+ if (normalizeWorkspaceSlotName(selector)) {
159
+ throw new Error(`Use a real workspace title or id when assigning '${slotName}', not another slot name.`);
160
+ }
161
+
162
+ const candidate = await resolveWorkspaceSelection(selector, options);
163
+ if (!candidate) {
164
+ throw new Error(`Workspace not found: ${selector}`);
165
+ }
166
+ const assignment = buildWorkspaceSlotAssignment(candidate);
167
+ const result = await setWorkspaceSlot(slotName, assignment, options);
168
+ const payload = {
169
+ ok: true,
170
+ slotName,
171
+ slot: result.slot,
172
+ slotsPath: result.slotsPath,
173
+ };
174
+ if (options.json) {
175
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
176
+ return 0;
177
+ }
178
+ const label =
179
+ normalizeOptionalString(result.slot.title) ||
180
+ normalizeOptionalString(result.slot.workspaceId) ||
181
+ normalizeOptionalString(result.slot.selector) ||
182
+ "workspace";
183
+ process.stdout.write(`Assigned workspace slot '${slotName}' to '${label}'.\n`);
184
+ return 0;
185
+ }
186
+
187
+ throw new Error(`unknown workspace slot subcommand: ${options.subcommand}`);
188
+ }
@@ -0,0 +1,363 @@
1
+ import process from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
+
4
+ import {
5
+ deriveBaseTitle,
6
+ deriveWorkspaceId,
7
+ getResumeCommand,
8
+ parseWorkspaceSource,
9
+ resolveResumeMetadata,
10
+ WORKSPACE_SCHEMA_VERSION,
11
+ } from "./core-plan.js";
12
+ import { fetchIdeaPayload, loadWorkspaceSource, updateIdeaPayload } from "./orp.js";
13
+ import { cacheManagedWorkspaceManifest } from "./registry.js";
14
+
15
+ const STRUCTURED_WORKSPACE_BLOCK_PATTERN = /```orp-workspace\s*[\s\S]*?```/i;
16
+ const WORKSPACE_TITLE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+
18
+ function printSyncHelp() {
19
+ console.log(`ORP workspace sync
20
+
21
+ Usage:
22
+ orp workspace sync <name-or-id> [--workspace-file <path> | --notes-file <path>] [--title <slug>] [--dry-run] [--json]
23
+
24
+ Options:
25
+ --workspace-file <path> Read a structured workspace manifest JSON file
26
+ --notes-file <path> Read a local notes file and normalize launchable paths into a manifest
27
+ --title <slug> Required when the source workspace does not already have a saved title
28
+ --dry-run Print the sync preview without updating the hosted idea
29
+ --json Print the sync preview as JSON
30
+ --base-url <url> Override the ORP hosted base URL
31
+ --orp-command <cmd> Override the ORP CLI executable used for hosted fetches/updates
32
+ -h, --help Show this help text
33
+
34
+ Examples:
35
+ orp workspace sync main
36
+ orp workspace sync main --workspace-file ./workspace.json --title main-cody-1
37
+ orp workspace sync main --notes-file ./workspace-notes.txt --title main-cody-1 --dry-run
38
+ `);
39
+ }
40
+
41
+ function normalizeOptionalString(value) {
42
+ if (value == null) {
43
+ return null;
44
+ }
45
+ const trimmed = String(value).trim();
46
+ return trimmed.length > 0 ? trimmed : null;
47
+ }
48
+
49
+ function parseWorkspaceSyncArgs(argv = []) {
50
+ const options = {
51
+ dryRun: false,
52
+ json: false,
53
+ };
54
+
55
+ for (let index = 0; index < argv.length; index += 1) {
56
+ const arg = argv[index];
57
+ if (arg === "-h" || arg === "--help") {
58
+ options.help = true;
59
+ continue;
60
+ }
61
+ if (arg === "--dry-run") {
62
+ options.dryRun = true;
63
+ continue;
64
+ }
65
+ if (arg === "--json") {
66
+ options.json = true;
67
+ continue;
68
+ }
69
+ if (arg.startsWith("--")) {
70
+ const next = argv[index + 1];
71
+ if (next == null || next.startsWith("--")) {
72
+ throw new Error(`missing value for ${arg}`);
73
+ }
74
+ if (arg === "--workspace-file") {
75
+ options.workspaceFile = next;
76
+ } else if (arg === "--notes-file") {
77
+ options.notesFile = next;
78
+ } else if (arg === "--title") {
79
+ options.title = next;
80
+ } else if (arg === "--base-url") {
81
+ options.baseUrl = next;
82
+ } else if (arg === "--orp-command") {
83
+ options.orpCommand = next;
84
+ } else {
85
+ throw new Error(`unknown option: ${arg}`);
86
+ }
87
+ index += 1;
88
+ continue;
89
+ }
90
+
91
+ if (options.ideaId) {
92
+ throw new Error(`unexpected argument: ${arg}`);
93
+ }
94
+ options.ideaId = arg;
95
+ }
96
+
97
+ return options;
98
+ }
99
+
100
+ function getLinkedIdeaIdFromWorkspaceRecord(workspace) {
101
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
102
+ return null;
103
+ }
104
+ const linkedIdea =
105
+ workspace.linked_idea && typeof workspace.linked_idea === "object" && !Array.isArray(workspace.linked_idea)
106
+ ? workspace.linked_idea
107
+ : workspace.linkedIdea && typeof workspace.linkedIdea === "object" && !Array.isArray(workspace.linkedIdea)
108
+ ? workspace.linkedIdea
109
+ : null;
110
+ return normalizeOptionalString(linkedIdea?.idea_id ?? linkedIdea?.ideaId);
111
+ }
112
+
113
+ export function resolveWorkspaceSyncTargetIdeaId(source) {
114
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
115
+ return null;
116
+ }
117
+ if (source.sourceType === "hosted-idea") {
118
+ return normalizeOptionalString(source.idea?.id);
119
+ }
120
+ if (source.sourceType === "hosted-workspace") {
121
+ return getLinkedIdeaIdFromWorkspaceRecord(source.hostedWorkspace);
122
+ }
123
+ return null;
124
+ }
125
+
126
+ async function resolveWorkspaceSyncTargetSource(source, options) {
127
+ if (!options.workspaceFile && !options.notesFile && (source.sourceType === "hosted-idea" || source.sourceType === "hosted-workspace")) {
128
+ return source;
129
+ }
130
+ return loadWorkspaceSource({
131
+ ideaId: options.ideaId,
132
+ baseUrl: options.baseUrl,
133
+ orpCommand: options.orpCommand,
134
+ });
135
+ }
136
+
137
+ export function validateWorkspaceTitle(value, label = "--title") {
138
+ const normalized = normalizeOptionalString(value);
139
+ if (!normalized) {
140
+ throw new Error(`${label} is required and must use lowercase letters, numbers, and dashes only.`);
141
+ }
142
+ if (!WORKSPACE_TITLE_PATTERN.test(normalized)) {
143
+ throw new Error(`${label} must use lowercase letters, numbers, and single dashes only, like main-cody-1.`);
144
+ }
145
+ return normalized;
146
+ }
147
+
148
+ async function promptForWorkspaceTitle() {
149
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
150
+ throw new Error("Workspace title is required. Provide --title <slug> with lowercase letters, numbers, and dashes only.");
151
+ }
152
+ const rl = createInterface({
153
+ input: process.stdin,
154
+ output: process.stdout,
155
+ });
156
+ try {
157
+ const answer = await rl.question("Workspace title (lowercase-dash format, example: main-cody-1): ");
158
+ return validateWorkspaceTitle(answer, "workspace title");
159
+ } finally {
160
+ rl.close();
161
+ }
162
+ }
163
+
164
+ function normalizeNotesBody(value) {
165
+ return String(value || "")
166
+ .replace(/\r\n/g, "\n")
167
+ .replace(/[ \t]+\n/g, "\n")
168
+ .replace(/\n{3,}/g, "\n\n")
169
+ .trim();
170
+ }
171
+
172
+ function combineNoteSections(sections) {
173
+ return sections
174
+ .map((section) => normalizeNotesBody(section))
175
+ .filter((section) => section.length > 0)
176
+ .join("\n\n");
177
+ }
178
+
179
+ export function extractWorkspaceNarrativeNotes(notes, options = {}) {
180
+ const withoutStructuredBlock = String(notes || "").replace(STRUCTURED_WORKSPACE_BLOCK_PATTERN, "");
181
+ const lines = withoutStructuredBlock.split(/\r?\n/);
182
+ const filteredLines = options.stripLegacyWorkspaceLines
183
+ ? lines.filter((line) => !line.trim().startsWith("/"))
184
+ : lines;
185
+ return normalizeNotesBody(filteredLines.join("\n"));
186
+ }
187
+
188
+ function serializeWorkspaceManifest(manifest) {
189
+ const tabs = manifest.tabs.map((entry) =>
190
+ Object.fromEntries(
191
+ Object.entries({
192
+ title: normalizeOptionalString(entry.title) ?? undefined,
193
+ path: String(entry.path).trim(),
194
+ resumeCommand: normalizeOptionalString(entry.resumeCommand) ?? undefined,
195
+ resumeTool: normalizeOptionalString(entry.resumeTool) ?? undefined,
196
+ resumeSessionId: normalizeOptionalString(entry.resumeSessionId ?? entry.sessionId) ?? undefined,
197
+ codexSessionId:
198
+ normalizeOptionalString(entry.resumeTool) === "codex"
199
+ ? normalizeOptionalString(entry.codexSessionId ?? entry.resumeSessionId ?? entry.sessionId) ?? undefined
200
+ : undefined,
201
+ claudeSessionId:
202
+ normalizeOptionalString(entry.resumeTool) === "claude"
203
+ ? normalizeOptionalString(entry.claudeSessionId ?? entry.resumeSessionId ?? entry.sessionId) ?? undefined
204
+ : undefined,
205
+ }).filter(([, value]) => value !== undefined),
206
+ ),
207
+ );
208
+
209
+ const normalized = Object.fromEntries(
210
+ Object.entries({
211
+ version: WORKSPACE_SCHEMA_VERSION,
212
+ workspaceId: normalizeOptionalString(manifest.workspaceId) ?? undefined,
213
+ title: normalizeOptionalString(manifest.title) ?? undefined,
214
+ tabs,
215
+ }).filter(([, value]) => value !== undefined),
216
+ );
217
+
218
+ return JSON.stringify(normalized, null, 2);
219
+ }
220
+
221
+ function composeWorkspaceNotes({ narrativeNotes, manifest }) {
222
+ return combineNoteSections([
223
+ narrativeNotes,
224
+ `\`\`\`orp-workspace\n${serializeWorkspaceManifest(manifest)}\n\`\`\``,
225
+ ]);
226
+ }
227
+
228
+ export function buildWorkspaceSyncPreview({ source, parsed, targetIdea, workspaceTitle = null }) {
229
+ const manifest = parsed.manifest
230
+ ? {
231
+ version: parsed.manifest.version || WORKSPACE_SCHEMA_VERSION,
232
+ workspaceId: parsed.manifest.workspaceId || workspaceTitle || deriveWorkspaceId(source, parsed),
233
+ title: workspaceTitle || parsed.manifest.title || null,
234
+ tabs: parsed.manifest.tabs.map((entry) => ({
235
+ title: entry.title || deriveBaseTitle(entry),
236
+ path: entry.path,
237
+ resumeCommand: entry.resumeCommand || null,
238
+ resumeTool: entry.resumeTool || null,
239
+ resumeSessionId: entry.sessionId || null,
240
+ codexSessionId: entry.resumeTool === "codex" ? entry.sessionId || null : null,
241
+ claudeSessionId: entry.resumeTool === "claude" ? entry.sessionId || null : null,
242
+ })),
243
+ }
244
+ : {
245
+ version: WORKSPACE_SCHEMA_VERSION,
246
+ workspaceId: workspaceTitle || deriveWorkspaceId(source, parsed),
247
+ title: workspaceTitle || null,
248
+ tabs: parsed.entries.map((entry) => ({
249
+ title: deriveBaseTitle(entry),
250
+ path: entry.path,
251
+ resumeCommand: getResumeCommand(entry),
252
+ resumeTool: resolveResumeMetadata(entry).resumeTool,
253
+ resumeSessionId: resolveResumeMetadata(entry).resumeSessionId,
254
+ codexSessionId:
255
+ resolveResumeMetadata(entry).resumeTool === "codex" ? resolveResumeMetadata(entry).resumeSessionId : null,
256
+ claudeSessionId:
257
+ resolveResumeMetadata(entry).resumeTool === "claude" ? resolveResumeMetadata(entry).resumeSessionId : null,
258
+ })),
259
+ };
260
+
261
+ const narrativeSourceNotes =
262
+ source.sourceType === "workspace-file" ? targetIdea.notes || "" : source.notes || targetIdea.notes || "";
263
+ const narrativeNotes = extractWorkspaceNarrativeNotes(narrativeSourceNotes, {
264
+ stripLegacyWorkspaceLines: source.sourceType === "workspace-file",
265
+ });
266
+ const nextNotes = composeWorkspaceNotes({
267
+ narrativeNotes,
268
+ manifest,
269
+ });
270
+
271
+ return {
272
+ targetIdeaId: targetIdea.id,
273
+ targetIdeaTitle: targetIdea.title,
274
+ sourceType: source.sourceType,
275
+ sourceLabel: source.sourceLabel,
276
+ parseMode: parsed.parseMode,
277
+ workspaceId: manifest.workspaceId,
278
+ manifest,
279
+ nextNotes,
280
+ nextNotesLength: nextNotes.length,
281
+ tabs: manifest.tabs,
282
+ skipped: parsed.skipped,
283
+ };
284
+ }
285
+
286
+ function summarizeSyncPreview(preview) {
287
+ const lines = [
288
+ `Workspace sync preview`,
289
+ ` target: ${preview.targetIdeaTitle} (${preview.targetIdeaId})`,
290
+ ` source: ${preview.sourceType} (${preview.sourceLabel})`,
291
+ ` parse mode: ${preview.parseMode}`,
292
+ ` workspace id: ${preview.workspaceId}`,
293
+ ` tabs: ${preview.tabs.length}`,
294
+ ` stored notes: ${preview.nextNotesLength} chars`,
295
+ ];
296
+
297
+ if (preview.skipped.length > 0) {
298
+ lines.push(` skipped non-path lines: ${preview.skipped.length}`);
299
+ }
300
+ return lines.join("\n");
301
+ }
302
+
303
+ export async function runWorkspaceSync(argv = process.argv.slice(2)) {
304
+ const options = parseWorkspaceSyncArgs(argv);
305
+ if (options.help) {
306
+ printSyncHelp();
307
+ return 0;
308
+ }
309
+ if (!options.ideaId) {
310
+ throw new Error("Provide the hosted workspace selector that should receive the synced workspace.");
311
+ }
312
+
313
+ const source = await loadWorkspaceSource(options);
314
+ const parsed = parseWorkspaceSource(source);
315
+ if (parsed.entries.length === 0) {
316
+ throw new Error("No launchable workspace lines were found in the provided source.");
317
+ }
318
+ const resolvedWorkspaceTitle = options.title
319
+ ? validateWorkspaceTitle(options.title)
320
+ : normalizeOptionalString(parsed.manifest?.title) || (await promptForWorkspaceTitle());
321
+ const targetSource = await resolveWorkspaceSyncTargetSource(source, options);
322
+ const targetIdeaId = resolveWorkspaceSyncTargetIdeaId(targetSource);
323
+ if (!targetIdeaId) {
324
+ throw new Error(
325
+ `Workspace sync target '${options.ideaId}' does not resolve to a hosted idea-backed workspace. Use a synced hosted selector like main, an idea id, or a workspace linked to a hosted idea.`,
326
+ );
327
+ }
328
+
329
+ const targetPayload =
330
+ targetSource.sourceType === "hosted-idea" && targetSource.idea?.id === targetIdeaId
331
+ ? targetSource.payload
332
+ : await fetchIdeaPayload(targetIdeaId, options);
333
+
334
+ if (!targetPayload?.idea) {
335
+ throw new Error("Unable to resolve the hosted ORP idea for workspace sync.");
336
+ }
337
+
338
+ const preview = buildWorkspaceSyncPreview({
339
+ source,
340
+ parsed,
341
+ targetIdea: targetPayload.idea,
342
+ workspaceTitle: resolvedWorkspaceTitle,
343
+ });
344
+
345
+ if (options.json) {
346
+ process.stdout.write(`${JSON.stringify(preview, null, 2)}\n`);
347
+ return 0;
348
+ }
349
+
350
+ if (options.dryRun) {
351
+ process.stdout.write(`${summarizeSyncPreview(preview)}\n`);
352
+ return 0;
353
+ }
354
+
355
+ const updated = await updateIdeaPayload(targetIdeaId, { notes: preview.nextNotes }, options);
356
+ const managedCache = await cacheManagedWorkspaceManifest(preview.manifest);
357
+ process.stdout.write(
358
+ `Synced workspace '${preview.workspaceId}' to idea '${updated.title || preview.targetIdeaTitle}'.\n`,
359
+ );
360
+ process.stdout.write(`Tabs: ${preview.tabs.length}. Stored notes: ${preview.nextNotesLength} chars.\n`);
361
+ process.stdout.write(`Updated local workspace cache at ${managedCache.manifestPath}.\n`);
362
+ return 0;
363
+ }