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.
- package/AGENT_INTEGRATION.md +50 -0
- package/README.md +273 -144
- package/bin/orp.js +14 -1
- package/cli/orp.py +14846 -9925
- package/docs/AGENT_LOOP.md +13 -0
- package/docs/AGENT_MODES.md +79 -0
- package/docs/CANONICAL_CLI_BOUNDARY.md +15 -0
- package/docs/EXCHANGE.md +94 -0
- package/docs/LAUNCH_KIT.md +107 -0
- package/docs/ORP_HOSTED_WORKSPACE_CONTRACT.md +295 -0
- package/docs/ORP_PUBLIC_LAUNCH_CHECKLIST.md +5 -0
- package/docs/START_HERE.md +567 -0
- package/package.json +5 -2
- package/packages/lifeops-orp/README.md +67 -0
- package/packages/lifeops-orp/package.json +48 -0
- package/packages/lifeops-orp/src/index.d.ts +106 -0
- package/packages/lifeops-orp/src/index.js +7 -0
- package/packages/lifeops-orp/src/mapping.js +309 -0
- package/packages/lifeops-orp/src/workspace.js +108 -0
- package/packages/lifeops-orp/test/orp.test.js +187 -0
- package/packages/orp-workspace-launcher/README.md +82 -0
- package/packages/orp-workspace-launcher/package.json +39 -0
- package/packages/orp-workspace-launcher/src/commands.js +77 -0
- package/packages/orp-workspace-launcher/src/core-plan.js +506 -0
- package/packages/orp-workspace-launcher/src/hosted-state.js +208 -0
- package/packages/orp-workspace-launcher/src/index.js +82 -0
- package/packages/orp-workspace-launcher/src/ledger.js +745 -0
- package/packages/orp-workspace-launcher/src/list.js +488 -0
- package/packages/orp-workspace-launcher/src/orp-command.js +126 -0
- package/packages/orp-workspace-launcher/src/orp.js +912 -0
- package/packages/orp-workspace-launcher/src/registry.js +558 -0
- package/packages/orp-workspace-launcher/src/slot.js +188 -0
- package/packages/orp-workspace-launcher/src/sync.js +363 -0
- package/packages/orp-workspace-launcher/src/tabs.js +166 -0
- package/packages/orp-workspace-launcher/test/commands.test.js +164 -0
- package/packages/orp-workspace-launcher/test/core-plan.test.js +253 -0
- package/packages/orp-workspace-launcher/test/fixtures/smoke-notes.txt +2 -0
- package/packages/orp-workspace-launcher/test/fixtures/workspace-manifest.json +17 -0
- package/packages/orp-workspace-launcher/test/ledger.test.js +244 -0
- package/packages/orp-workspace-launcher/test/list.test.js +299 -0
- package/packages/orp-workspace-launcher/test/orp-command.test.js +44 -0
- package/packages/orp-workspace-launcher/test/orp.test.js +224 -0
- package/packages/orp-workspace-launcher/test/tabs.test.js +168 -0
- package/scripts/orp-kernel-agent-pilot.py +10 -1
- package/scripts/orp-kernel-agent-replication.py +10 -1
- package/scripts/orp-kernel-canonical-continuation.py +10 -1
- package/scripts/orp-kernel-continuation-pilot.py +10 -1
- package/scripts/render-terminal-demo.py +416 -0
- package/spec/v1/exchange-report.schema.json +105 -0
- package/spec/v1/hosted-workspace-event.schema.json +102 -0
- package/spec/v1/hosted-workspace.schema.json +332 -0
- 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
|
+
}
|