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.
- 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 +4 -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,558 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import { normalizeWorkspaceManifest } from "./core-plan.js";
|
|
8
|
+
|
|
9
|
+
const WORKSPACE_REGISTRY_VERSION = "1";
|
|
10
|
+
const WORKSPACE_SLOTS_VERSION = "1";
|
|
11
|
+
const WORKSPACE_SLOT_NAMES = new Set(["main", "offhand"]);
|
|
12
|
+
|
|
13
|
+
function normalizeOptionalString(value) {
|
|
14
|
+
if (value == null) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const trimmed = String(value).trim();
|
|
18
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function slugify(value) {
|
|
22
|
+
const normalized = String(value || "")
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
26
|
+
.replace(/^-+|-+$/g, "");
|
|
27
|
+
return normalized || "workspace";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeWorkspaceSlotName(value) {
|
|
31
|
+
const normalized = normalizeOptionalString(value)?.toLowerCase() || null;
|
|
32
|
+
return normalized && WORKSPACE_SLOT_NAMES.has(normalized) ? normalized : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shortHash(value) {
|
|
36
|
+
return createHash("sha1").update(String(value || "")).digest("hex").slice(0, 8);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function countTabsWithValue(tabs, key) {
|
|
40
|
+
return tabs.reduce((count, tab) => (normalizeOptionalString(tab[key]) ? count + 1 : count), 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildResumeSessionRows(tabs) {
|
|
44
|
+
return tabs
|
|
45
|
+
.map((tab) => {
|
|
46
|
+
const resumeCommand = normalizeOptionalString(tab.resumeCommand);
|
|
47
|
+
const resumeTool = normalizeOptionalString(tab.resumeTool);
|
|
48
|
+
const resumeSessionId = normalizeOptionalString(tab.resumeSessionId ?? tab.sessionId);
|
|
49
|
+
if (!resumeCommand && !resumeSessionId) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
Object.entries({
|
|
54
|
+
title: normalizeOptionalString(tab.title) ?? undefined,
|
|
55
|
+
path: normalizeOptionalString(tab.path) ?? undefined,
|
|
56
|
+
resumeCommand: resumeCommand ?? undefined,
|
|
57
|
+
resumeTool: resumeTool ?? undefined,
|
|
58
|
+
resumeSessionId: resumeSessionId ?? undefined,
|
|
59
|
+
codexSessionId: resumeTool === "codex" ? resumeSessionId ?? undefined : undefined,
|
|
60
|
+
}).filter(([, value]) => value !== undefined),
|
|
61
|
+
);
|
|
62
|
+
})
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeRegistryEntry(rawEntry) {
|
|
67
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const manifestPath = normalizeOptionalString(rawEntry.manifestPath);
|
|
72
|
+
if (!manifestPath) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Object.fromEntries(
|
|
77
|
+
Object.entries({
|
|
78
|
+
manifestPath,
|
|
79
|
+
workspaceId: normalizeOptionalString(rawEntry.workspaceId) ?? undefined,
|
|
80
|
+
title: normalizeOptionalString(rawEntry.title) ?? undefined,
|
|
81
|
+
host: normalizeOptionalString(rawEntry.host) ?? undefined,
|
|
82
|
+
captureMode: normalizeOptionalString(rawEntry.captureMode) ?? undefined,
|
|
83
|
+
capturedAt: normalizeOptionalString(rawEntry.capturedAt) ?? undefined,
|
|
84
|
+
trackingStartedAt: normalizeOptionalString(rawEntry.trackingStartedAt) ?? undefined,
|
|
85
|
+
windowId: Number.isInteger(rawEntry.windowId) && rawEntry.windowId > 0 ? rawEntry.windowId : undefined,
|
|
86
|
+
windowIndex: Number.isInteger(rawEntry.windowIndex) && rawEntry.windowIndex > 0 ? rawEntry.windowIndex : undefined,
|
|
87
|
+
tabCount: Number.isInteger(rawEntry.tabCount) && rawEntry.tabCount >= 0 ? rawEntry.tabCount : undefined,
|
|
88
|
+
codexSessionCount:
|
|
89
|
+
Number.isInteger(rawEntry.codexSessionCount) && rawEntry.codexSessionCount >= 0
|
|
90
|
+
? rawEntry.codexSessionCount
|
|
91
|
+
: undefined,
|
|
92
|
+
tmuxSessionCount:
|
|
93
|
+
Number.isInteger(rawEntry.tmuxSessionCount) && rawEntry.tmuxSessionCount >= 0
|
|
94
|
+
? rawEntry.tmuxSessionCount
|
|
95
|
+
: undefined,
|
|
96
|
+
resumeSessions: Array.isArray(rawEntry.resumeSessions)
|
|
97
|
+
? rawEntry.resumeSessions
|
|
98
|
+
.map((session) => {
|
|
99
|
+
if (!session || typeof session !== "object" || Array.isArray(session)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const resumeCommand = normalizeOptionalString(session.resumeCommand);
|
|
103
|
+
const resumeSessionId = normalizeOptionalString(session.resumeSessionId);
|
|
104
|
+
if (!resumeCommand && !resumeSessionId) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return Object.fromEntries(
|
|
108
|
+
Object.entries({
|
|
109
|
+
title: normalizeOptionalString(session.title) ?? undefined,
|
|
110
|
+
path: normalizeOptionalString(session.path) ?? undefined,
|
|
111
|
+
resumeCommand: resumeCommand ?? undefined,
|
|
112
|
+
resumeTool: normalizeOptionalString(session.resumeTool) ?? undefined,
|
|
113
|
+
resumeSessionId: resumeSessionId ?? undefined,
|
|
114
|
+
codexSessionId: normalizeOptionalString(session.codexSessionId) ?? undefined,
|
|
115
|
+
}).filter(([, value]) => value !== undefined),
|
|
116
|
+
);
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
: Array.isArray(rawEntry.codexSessions)
|
|
120
|
+
? rawEntry.codexSessions
|
|
121
|
+
.map((session) => {
|
|
122
|
+
if (!session || typeof session !== "object" || Array.isArray(session)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const codexSessionId = normalizeOptionalString(session.codexSessionId);
|
|
126
|
+
if (!codexSessionId) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return Object.fromEntries(
|
|
130
|
+
Object.entries({
|
|
131
|
+
title: normalizeOptionalString(session.title) ?? undefined,
|
|
132
|
+
path: normalizeOptionalString(session.path) ?? undefined,
|
|
133
|
+
resumeCommand: codexSessionId ? `codex resume ${codexSessionId}` : undefined,
|
|
134
|
+
resumeTool: codexSessionId ? "codex" : undefined,
|
|
135
|
+
resumeSessionId: codexSessionId,
|
|
136
|
+
codexSessionId,
|
|
137
|
+
}).filter(([, value]) => value !== undefined),
|
|
138
|
+
);
|
|
139
|
+
})
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
: undefined,
|
|
142
|
+
codexSessions: undefined,
|
|
143
|
+
registeredAt: normalizeOptionalString(rawEntry.registeredAt) ?? undefined,
|
|
144
|
+
updatedAt: normalizeOptionalString(rawEntry.updatedAt) ?? undefined,
|
|
145
|
+
}).filter(([, value]) => value !== undefined),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeRegistry(payload) {
|
|
150
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
151
|
+
return {
|
|
152
|
+
version: WORKSPACE_REGISTRY_VERSION,
|
|
153
|
+
workspaces: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const version = normalizeOptionalString(payload.version) || WORKSPACE_REGISTRY_VERSION;
|
|
158
|
+
if (version !== WORKSPACE_REGISTRY_VERSION) {
|
|
159
|
+
throw new Error(`unsupported workspace registry version: ${version}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
version,
|
|
164
|
+
workspaces: Array.isArray(payload.workspaces)
|
|
165
|
+
? payload.workspaces.map((entry) => normalizeRegistryEntry(entry)).filter(Boolean)
|
|
166
|
+
: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeWorkspaceSlotEntry(rawEntry) {
|
|
171
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const slot = normalizeWorkspaceSlotName(rawEntry.slot);
|
|
176
|
+
const kind = normalizeOptionalString(rawEntry.kind);
|
|
177
|
+
if (!slot || !kind) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return Object.fromEntries(
|
|
182
|
+
Object.entries({
|
|
183
|
+
slot,
|
|
184
|
+
kind,
|
|
185
|
+
selector: normalizeOptionalString(rawEntry.selector) ?? undefined,
|
|
186
|
+
workspaceId: normalizeOptionalString(rawEntry.workspaceId) ?? undefined,
|
|
187
|
+
title: normalizeOptionalString(rawEntry.title) ?? undefined,
|
|
188
|
+
ideaId: normalizeOptionalString(rawEntry.ideaId) ?? undefined,
|
|
189
|
+
hostedWorkspaceId: normalizeOptionalString(rawEntry.hostedWorkspaceId) ?? undefined,
|
|
190
|
+
manifestPath: normalizeOptionalString(rawEntry.manifestPath) ?? undefined,
|
|
191
|
+
assignedAt: normalizeOptionalString(rawEntry.assignedAt) ?? undefined,
|
|
192
|
+
updatedAt: normalizeOptionalString(rawEntry.updatedAt) ?? undefined,
|
|
193
|
+
}).filter(([, value]) => value !== undefined),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeWorkspaceSlots(payload) {
|
|
198
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
199
|
+
return {
|
|
200
|
+
version: WORKSPACE_SLOTS_VERSION,
|
|
201
|
+
slots: {},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const version = normalizeOptionalString(payload.version) || WORKSPACE_SLOTS_VERSION;
|
|
206
|
+
if (version !== WORKSPACE_SLOTS_VERSION) {
|
|
207
|
+
throw new Error(`unsupported workspace slots version: ${version}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rawSlots =
|
|
211
|
+
payload.slots && typeof payload.slots === "object" && !Array.isArray(payload.slots) ? payload.slots : {};
|
|
212
|
+
const slots = {};
|
|
213
|
+
for (const [slotName, rawEntry] of Object.entries(rawSlots)) {
|
|
214
|
+
const normalizedSlotName = normalizeWorkspaceSlotName(slotName);
|
|
215
|
+
const entry = normalizeWorkspaceSlotEntry({
|
|
216
|
+
slot: slotName,
|
|
217
|
+
...(rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {}),
|
|
218
|
+
});
|
|
219
|
+
if (normalizedSlotName && entry) {
|
|
220
|
+
slots[normalizedSlotName] = entry;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
version,
|
|
226
|
+
slots,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function registrySortValue(entry) {
|
|
231
|
+
return Date.parse(entry.updatedAt || entry.capturedAt || entry.registeredAt || "") || 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sortRegistryEntries(entries) {
|
|
235
|
+
return [...entries].sort((left, right) => {
|
|
236
|
+
const dateDelta = registrySortValue(right) - registrySortValue(left);
|
|
237
|
+
if (dateDelta !== 0) {
|
|
238
|
+
return dateDelta;
|
|
239
|
+
}
|
|
240
|
+
return String(left.manifestPath).localeCompare(String(right.manifestPath));
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function getConfigHome(env = process.env) {
|
|
245
|
+
const xdgConfigHome = normalizeOptionalString(env?.XDG_CONFIG_HOME);
|
|
246
|
+
if (xdgConfigHome) {
|
|
247
|
+
return path.resolve(xdgConfigHome);
|
|
248
|
+
}
|
|
249
|
+
return path.join(os.homedir(), ".config");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function getOrpUserDir(env = process.env) {
|
|
253
|
+
return path.join(getConfigHome(env), "orp");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function getWorkspaceRegistryPath(options = {}) {
|
|
257
|
+
if (options.registryPath) {
|
|
258
|
+
return path.resolve(options.registryPath);
|
|
259
|
+
}
|
|
260
|
+
return path.join(getOrpUserDir(options.env), "workspace-registry.json");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function getWorkspaceSlotsPath(options = {}) {
|
|
264
|
+
if (options.slotsPath) {
|
|
265
|
+
return path.resolve(options.slotsPath);
|
|
266
|
+
}
|
|
267
|
+
return path.join(getOrpUserDir(options.env), "workspace-slots.json");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getWorkspaceStylesPath(options = {}) {
|
|
271
|
+
if (options.stylesPath) {
|
|
272
|
+
return path.resolve(options.stylesPath);
|
|
273
|
+
}
|
|
274
|
+
return path.join(getOrpUserDir(options.env), "workspace-styles.json");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getWorkspaceStyleBindingsPath(options = {}) {
|
|
278
|
+
if (options.styleBindingsPath) {
|
|
279
|
+
return path.resolve(options.styleBindingsPath);
|
|
280
|
+
}
|
|
281
|
+
return path.join(getOrpUserDir(options.env), "workspace-style-bindings.json");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getManagedWorkspaceDir(options = {}) {
|
|
285
|
+
return path.join(getOrpUserDir(options.env), "workspaces");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function isManagedWorkspaceManifestPath(manifestPath, options = {}) {
|
|
289
|
+
const candidate = normalizeOptionalString(manifestPath);
|
|
290
|
+
if (!candidate) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const managedDir = path.resolve(getManagedWorkspaceDir(options));
|
|
294
|
+
const resolvedPath = path.resolve(candidate);
|
|
295
|
+
return resolvedPath === managedDir || resolvedPath.startsWith(`${managedDir}${path.sep}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getManagedWorkspaceManifestPath(manifest, options = {}) {
|
|
299
|
+
const workspaceId = normalizeOptionalString(manifest?.workspaceId);
|
|
300
|
+
const title = normalizeOptionalString(manifest?.title);
|
|
301
|
+
const token = workspaceId || title || "workspace";
|
|
302
|
+
const fileName = `${slugify(token)}-${shortHash(token)}.json`;
|
|
303
|
+
return path.join(getManagedWorkspaceDir(options), fileName);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function summarizeManifestForRegistry(manifestPath, manifest) {
|
|
307
|
+
const normalizedPath = path.resolve(manifestPath);
|
|
308
|
+
const resumeSessions = buildResumeSessionRows(manifest.tabs || []);
|
|
309
|
+
|
|
310
|
+
return Object.fromEntries(
|
|
311
|
+
Object.entries({
|
|
312
|
+
manifestPath: normalizedPath,
|
|
313
|
+
workspaceId: normalizeOptionalString(manifest.workspaceId) ?? undefined,
|
|
314
|
+
title: normalizeOptionalString(manifest.title) ?? undefined,
|
|
315
|
+
host: normalizeOptionalString(manifest.capture?.host) ?? undefined,
|
|
316
|
+
captureMode: normalizeOptionalString(manifest.capture?.mode) ?? undefined,
|
|
317
|
+
capturedAt: normalizeOptionalString(manifest.capture?.capturedAt) ?? undefined,
|
|
318
|
+
trackingStartedAt: normalizeOptionalString(manifest.capture?.trackingStartedAt) ?? undefined,
|
|
319
|
+
windowId: manifest.capture?.windowId ?? undefined,
|
|
320
|
+
windowIndex: manifest.capture?.windowIndex ?? undefined,
|
|
321
|
+
tabCount: Array.isArray(manifest.tabs) ? manifest.tabs.length : 0,
|
|
322
|
+
codexSessionCount: resumeSessions.length,
|
|
323
|
+
tmuxSessionCount: countTabsWithValue(manifest.tabs || [], "tmuxSessionName"),
|
|
324
|
+
resumeSessions,
|
|
325
|
+
}).filter(([, value]) => value !== undefined),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function serializeManagedWorkspaceManifest(manifest) {
|
|
330
|
+
const normalized = normalizeWorkspaceManifest(manifest);
|
|
331
|
+
const tabs = Array.isArray(normalized.tabs)
|
|
332
|
+
? normalized.tabs.map((tab) => {
|
|
333
|
+
const resumeCommand = normalizeOptionalString(tab.resumeCommand);
|
|
334
|
+
const resumeTool = normalizeOptionalString(tab.resumeTool);
|
|
335
|
+
const resumeSessionId = normalizeOptionalString(tab.resumeSessionId ?? tab.sessionId);
|
|
336
|
+
return Object.fromEntries(
|
|
337
|
+
Object.entries({
|
|
338
|
+
title: normalizeOptionalString(tab.title) ?? undefined,
|
|
339
|
+
path: normalizeOptionalString(tab.path) ?? undefined,
|
|
340
|
+
resumeCommand: resumeCommand ?? undefined,
|
|
341
|
+
resumeTool: resumeTool ?? undefined,
|
|
342
|
+
resumeSessionId: resumeSessionId ?? undefined,
|
|
343
|
+
codexSessionId: resumeTool === "codex" ? resumeSessionId ?? undefined : undefined,
|
|
344
|
+
claudeSessionId: resumeTool === "claude" ? resumeSessionId ?? undefined : undefined,
|
|
345
|
+
}).filter(([, value]) => value !== undefined),
|
|
346
|
+
);
|
|
347
|
+
})
|
|
348
|
+
: [];
|
|
349
|
+
|
|
350
|
+
return `${JSON.stringify(
|
|
351
|
+
Object.fromEntries(
|
|
352
|
+
Object.entries({
|
|
353
|
+
version: normalized.version,
|
|
354
|
+
workspaceId: normalizeOptionalString(normalized.workspaceId) ?? undefined,
|
|
355
|
+
title: normalizeOptionalString(normalized.title) ?? undefined,
|
|
356
|
+
capture: normalized.capture || undefined,
|
|
357
|
+
tabs,
|
|
358
|
+
}).filter(([, value]) => value !== undefined),
|
|
359
|
+
),
|
|
360
|
+
null,
|
|
361
|
+
2,
|
|
362
|
+
)}\n`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function loadWorkspaceRegistry(options = {}) {
|
|
366
|
+
const registryPath = getWorkspaceRegistryPath(options);
|
|
367
|
+
try {
|
|
368
|
+
const raw = await fs.readFile(registryPath, "utf8");
|
|
369
|
+
return {
|
|
370
|
+
registryPath,
|
|
371
|
+
registry: normalizeRegistry(JSON.parse(raw)),
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
375
|
+
return {
|
|
376
|
+
registryPath,
|
|
377
|
+
registry: {
|
|
378
|
+
version: WORKSPACE_REGISTRY_VERSION,
|
|
379
|
+
workspaces: [],
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function saveWorkspaceRegistry(registryPath, registry) {
|
|
388
|
+
await fs.mkdir(path.dirname(registryPath), { recursive: true });
|
|
389
|
+
await fs.writeFile(`${registryPath}`, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function saveWorkspaceSlots(slotsPath, payload) {
|
|
393
|
+
await fs.mkdir(path.dirname(slotsPath), { recursive: true });
|
|
394
|
+
await fs.writeFile(`${slotsPath}`, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function loadWorkspaceSlots(options = {}) {
|
|
398
|
+
const slotsPath = getWorkspaceSlotsPath(options);
|
|
399
|
+
try {
|
|
400
|
+
const raw = await fs.readFile(slotsPath, "utf8");
|
|
401
|
+
return {
|
|
402
|
+
slotsPath,
|
|
403
|
+
slots: normalizeWorkspaceSlots(JSON.parse(raw)).slots,
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
407
|
+
return {
|
|
408
|
+
slotsPath,
|
|
409
|
+
slots: {},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function setWorkspaceSlot(slotName, assignment, options = {}) {
|
|
417
|
+
const normalizedSlot = normalizeWorkspaceSlotName(slotName);
|
|
418
|
+
if (!normalizedSlot) {
|
|
419
|
+
throw new Error(`unsupported workspace slot: ${slotName}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const entry = normalizeWorkspaceSlotEntry({
|
|
423
|
+
slot: normalizedSlot,
|
|
424
|
+
...(assignment && typeof assignment === "object" && !Array.isArray(assignment) ? assignment : {}),
|
|
425
|
+
});
|
|
426
|
+
if (!entry) {
|
|
427
|
+
throw new Error(`invalid workspace slot assignment for ${normalizedSlot}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const { slotsPath, slots } = await loadWorkspaceSlots(options);
|
|
431
|
+
const now = new Date().toISOString();
|
|
432
|
+
const existing = slots[normalizedSlot];
|
|
433
|
+
const nextSlots = {
|
|
434
|
+
...slots,
|
|
435
|
+
[normalizedSlot]: {
|
|
436
|
+
...entry,
|
|
437
|
+
slot: normalizedSlot,
|
|
438
|
+
assignedAt: existing?.assignedAt || now,
|
|
439
|
+
updatedAt: now,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
await saveWorkspaceSlots(slotsPath, {
|
|
444
|
+
version: WORKSPACE_SLOTS_VERSION,
|
|
445
|
+
slots: nextSlots,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
slotsPath,
|
|
450
|
+
slot: nextSlots[normalizedSlot],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function clearWorkspaceSlot(slotName, options = {}) {
|
|
455
|
+
const normalizedSlot = normalizeWorkspaceSlotName(slotName);
|
|
456
|
+
if (!normalizedSlot) {
|
|
457
|
+
throw new Error(`unsupported workspace slot: ${slotName}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const { slotsPath, slots } = await loadWorkspaceSlots(options);
|
|
461
|
+
if (!slots[normalizedSlot]) {
|
|
462
|
+
return {
|
|
463
|
+
slotsPath,
|
|
464
|
+
cleared: false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const nextSlots = { ...slots };
|
|
469
|
+
delete nextSlots[normalizedSlot];
|
|
470
|
+
await saveWorkspaceSlots(slotsPath, {
|
|
471
|
+
version: WORKSPACE_SLOTS_VERSION,
|
|
472
|
+
slots: nextSlots,
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
slotsPath,
|
|
476
|
+
cleared: true,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function registerWorkspaceManifest(manifestPath, manifest, options = {}) {
|
|
481
|
+
const resolvedManifest = normalizeWorkspaceManifest(manifest);
|
|
482
|
+
const { registryPath, registry } = await loadWorkspaceRegistry(options);
|
|
483
|
+
const now = new Date().toISOString();
|
|
484
|
+
const normalizedManifestPath = path.resolve(manifestPath);
|
|
485
|
+
const existingEntry = registry.workspaces.find((entry) => entry.manifestPath === normalizedManifestPath) || null;
|
|
486
|
+
const nextEntry = {
|
|
487
|
+
...summarizeManifestForRegistry(normalizedManifestPath, resolvedManifest),
|
|
488
|
+
registeredAt: existingEntry?.registeredAt || now,
|
|
489
|
+
updatedAt: now,
|
|
490
|
+
};
|
|
491
|
+
const nextRegistry = {
|
|
492
|
+
version: registry.version,
|
|
493
|
+
workspaces: sortRegistryEntries([
|
|
494
|
+
...registry.workspaces.filter((entry) => entry.manifestPath !== normalizedManifestPath),
|
|
495
|
+
nextEntry,
|
|
496
|
+
]),
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
await saveWorkspaceRegistry(registryPath, nextRegistry);
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
registryPath,
|
|
503
|
+
entry: nextEntry,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function cacheManagedWorkspaceManifest(manifest, options = {}) {
|
|
508
|
+
const resolvedManifest = normalizeWorkspaceManifest(manifest);
|
|
509
|
+
const manifestPath = getManagedWorkspaceManifestPath(resolvedManifest, options);
|
|
510
|
+
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
511
|
+
await fs.writeFile(`${manifestPath}`, serializeManagedWorkspaceManifest(resolvedManifest), "utf8");
|
|
512
|
+
const registration = await registerWorkspaceManifest(manifestPath, resolvedManifest, options);
|
|
513
|
+
return {
|
|
514
|
+
manifestPath,
|
|
515
|
+
manifest: resolvedManifest,
|
|
516
|
+
registryPath: registration.registryPath,
|
|
517
|
+
entry: registration.entry,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export async function listTrackedWorkspaces(options = {}) {
|
|
522
|
+
const { registryPath, registry } = await loadWorkspaceRegistry(options);
|
|
523
|
+
const workspaces = [];
|
|
524
|
+
|
|
525
|
+
for (const entry of registry.workspaces) {
|
|
526
|
+
try {
|
|
527
|
+
const rawManifest = await fs.readFile(entry.manifestPath, "utf8");
|
|
528
|
+
const manifest = normalizeWorkspaceManifest(JSON.parse(rawManifest));
|
|
529
|
+
workspaces.push({
|
|
530
|
+
...entry,
|
|
531
|
+
...summarizeManifestForRegistry(entry.manifestPath, manifest),
|
|
532
|
+
registeredAt: entry.registeredAt,
|
|
533
|
+
updatedAt: entry.updatedAt,
|
|
534
|
+
status: "ok",
|
|
535
|
+
});
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
538
|
+
workspaces.push({
|
|
539
|
+
...entry,
|
|
540
|
+
status: "missing",
|
|
541
|
+
error: "manifest file not found",
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
workspaces.push({
|
|
547
|
+
...entry,
|
|
548
|
+
status: "invalid",
|
|
549
|
+
error: error instanceof Error ? error.message : String(error),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
registryPath,
|
|
556
|
+
workspaces: sortRegistryEntries(workspaces),
|
|
557
|
+
};
|
|
558
|
+
}
|