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,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
|
+
}
|