@uniqbit/mate-core 0.16.0 → 0.17.0-canary.2
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/package.json +1 -1
- package/src/cli/commands/companion/companion.ts +8 -0
- package/src/cli/commands/companion/prepare.ts +76 -0
- package/src/cli/commands/companion/register.ts +47 -0
- package/src/cli/commands/launch/claude.ts +5 -3
- package/src/cli/commands/launch/opencode.ts +5 -3
- package/src/cli/commands/launch/shared.ts +41 -12
- package/src/cli/commands/shared/companion-selection.ts +3 -1
- package/src/cli/commands/studio/index.ts +85 -6
- package/src/cli/commands/studio/routes.ts +1 -1
- package/src/cli/commands/studio/selection.ts +11 -2
- package/src/cli/commands/studio/server.ts +212 -17
- package/src/cli/commands/studio/vault.ts +607 -0
- package/src/cli/commands/studio/views/client.ts +141 -1
- package/src/cli/commands/studio/views/companion-selector.tsx +1 -1
- package/src/cli/commands/studio/views/document.tsx +136 -3
- package/src/cli/commands/studio/views/model.ts +13 -0
- package/src/cli/commands/studio/views/styles.ts +16 -0
- package/src/cli/main.ts +33 -4
- package/src/cli/usage.ts +6 -3
- package/src/index.ts +1 -0
- package/src/lib/context-mode-package.ts +7 -4
- package/src/lib/opencode-plugin-package.ts +10 -4
- package/src/lib/orchestrator/adapters/base.ts +65 -10
- package/src/lib/orchestrator/adapters/opencode.ts +4 -2
- package/src/lib/orchestrator/companion-registration.ts +73 -0
- package/src/lib/orchestrator/framework-context.ts +29 -3
- package/src/lib/orchestrator/launcher.ts +46 -22
- package/src/lib/orchestrator/projection-record.ts +24 -0
- package/src/lib/orchestrator/types.ts +11 -0
- package/src/lib/prebuilt-workspace-prepare.ts +152 -0
- package/src/lib/prebuilt-workspace.ts +236 -0
- package/src/lib/preinstalled-plugins.ts +82 -0
- package/src/lib/update-checker.ts +39 -0
- package/src/opencode/companion-hooks.ts +45 -20
- package/src/playbooks/companion-guidance.ts +1 -0
- package/src/runtime/companion-guidance.ts +61 -15
- package/src/runtime/env.ts +3 -3
- package/src/runtime/index.ts +1 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +127 -77
- package/src/tools/setup/capabilities/context-mode.ts +5 -0
- package/src/tools/setup/plugin.ts +7 -1
- package/src/tools/setup/providers/opencode.ts +24 -5
|
@@ -4,6 +4,7 @@ import { STUDIO_HOSTNAME } from "./routes";
|
|
|
4
4
|
import { parseStudioSelection, resolveCompanion } from "./selection";
|
|
5
5
|
import { createStudioSnapshotCache, type StudioSnapshotCache } from "./snapshot";
|
|
6
6
|
import type { StudioPage } from "./views/model";
|
|
7
|
+
import { createVaultManager, resolveVaultPath, VaultPathError, type VaultManager } from "./vault";
|
|
7
8
|
|
|
8
9
|
export { STUDIO_HOSTNAME } from "./routes";
|
|
9
10
|
|
|
@@ -34,6 +35,13 @@ export interface StudioServerDeps {
|
|
|
34
35
|
renderDocument?: (page: StudioPage) => string | Promise<string>;
|
|
35
36
|
serve?: (options: StudioServeOptions) => StudioBoundServer;
|
|
36
37
|
snapshots?: StudioSnapshotCache;
|
|
38
|
+
vault?: VaultManager;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StudioServerOptions {
|
|
42
|
+
port?: number;
|
|
43
|
+
hostname?: string;
|
|
44
|
+
writable?: boolean;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/**
|
|
@@ -51,9 +59,21 @@ function html(body: string): Response {
|
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
function json(value: unknown, status = 200): Response {
|
|
63
|
+
return new Response(JSON.stringify(value), {
|
|
64
|
+
status,
|
|
65
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface StudioFetch {
|
|
70
|
+
(request: Request): Promise<Response>;
|
|
71
|
+
close(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
|
-
* The studio request handler. Read
|
|
56
|
-
*
|
|
75
|
+
* The studio request handler. Read endpoints are refused before collection, and
|
|
76
|
+
* the vault save route is the only write path. The response is
|
|
57
77
|
* already correct for the URL that asked for it — the companion, the change,
|
|
58
78
|
* and the view are read from the request rather than reconciled in the browser.
|
|
59
79
|
* Collection is held in one snapshot cache for the life of the handler, so a
|
|
@@ -62,31 +82,148 @@ function html(body: string): Response {
|
|
|
62
82
|
*/
|
|
63
83
|
export function createStudioFetch(
|
|
64
84
|
deps: StudioServerDeps = {},
|
|
65
|
-
|
|
85
|
+
options: Pick<StudioServerOptions, "writable"> = {},
|
|
86
|
+
): StudioFetch {
|
|
66
87
|
const inventory = deps.collectStudioInventory ?? collectStudioInventory;
|
|
67
88
|
const companion = deps.assembleCompanionPayload ?? assembleCompanionPayload;
|
|
68
89
|
const render = deps.renderDocument ?? renderStudioDocument;
|
|
69
90
|
const snapshots =
|
|
70
91
|
deps.snapshots ?? createStudioSnapshotCache({ assembleCompanionPayload: companion });
|
|
92
|
+
const vault = deps.vault ?? createVaultManager();
|
|
93
|
+
const writable = options.writable === true;
|
|
71
94
|
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
const handler = async (request: Request): Promise<Response> => {
|
|
96
|
+
const url = new URL(request.url);
|
|
97
|
+
const isSave = url.pathname === "/api/vault/save";
|
|
98
|
+
if (isSave && request.method !== "POST") {
|
|
99
|
+
return new Response("vault save requires POST", { status: 405, headers: { allow: "POST" } });
|
|
100
|
+
}
|
|
101
|
+
if (!READ_METHODS.has(request.method) && !(isSave && request.method === "POST")) {
|
|
74
102
|
return new Response("studio serves read requests only", {
|
|
75
103
|
status: 405,
|
|
76
|
-
headers: { allow: "GET, HEAD" },
|
|
104
|
+
headers: { allow: isSave ? "POST" : "GET, HEAD" },
|
|
77
105
|
});
|
|
78
106
|
}
|
|
79
107
|
|
|
80
|
-
const url = new URL(request.url);
|
|
81
108
|
const respond = (response: Response) =>
|
|
82
109
|
request.method === "HEAD"
|
|
83
110
|
? new Response(null, { status: response.status, headers: response.headers })
|
|
84
111
|
: response;
|
|
85
112
|
|
|
113
|
+
if (url.pathname === "/api/vault/tree") {
|
|
114
|
+
const selected = await selectedCompanion(url, inventory);
|
|
115
|
+
if (!selected) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
116
|
+
try {
|
|
117
|
+
return respond(
|
|
118
|
+
json(await vault.tree(selected.path, url.searchParams.get("refresh") === "1")),
|
|
119
|
+
);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return respond(
|
|
122
|
+
json({ reason: error instanceof Error ? error.message : String(error) }, 400),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (url.pathname === "/api/vault/file") {
|
|
128
|
+
const selected = await selectedCompanion(url, inventory);
|
|
129
|
+
if (!selected) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
130
|
+
try {
|
|
131
|
+
return respond(json(await vault.open(selected.path, url.searchParams.get("path") ?? "")));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return respond(
|
|
134
|
+
json({ reason: error instanceof Error ? error.message : String(error) }, 400),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (url.pathname === "/api/vault/events") {
|
|
140
|
+
const selected = await selectedCompanion(url, inventory);
|
|
141
|
+
if (!selected) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
142
|
+
try {
|
|
143
|
+
const opened = await resolveVaultPath(selected.path, url.searchParams.get("path") ?? "");
|
|
144
|
+
return respond(vaultEvents(vault, selected.path, opened.relative));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return respond(
|
|
147
|
+
json({ reason: error instanceof Error ? error.message : String(error) }, 400),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isSave) {
|
|
153
|
+
if (!writable)
|
|
154
|
+
return respond(json({ reason: "start Studio with --writable to enable saving" }, 403));
|
|
155
|
+
let body: { companion?: string; path?: string; content?: string; token?: string };
|
|
156
|
+
try {
|
|
157
|
+
body = (await request.json()) as typeof body;
|
|
158
|
+
} catch {
|
|
159
|
+
return respond(json({ reason: "save requires a JSON body" }, 400));
|
|
160
|
+
}
|
|
161
|
+
const selected = await selectedCompanion(
|
|
162
|
+
new URL(`${url.origin}/?companion=${encodeURIComponent(body.companion ?? "")}`),
|
|
163
|
+
inventory,
|
|
164
|
+
);
|
|
165
|
+
if (!selected || typeof body.path !== "string" || typeof body.content !== "string") {
|
|
166
|
+
return respond(
|
|
167
|
+
json({ reason: "save requires a registered companion, path, and content" }, 400),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const result = await vault.save(selected.path, body.path, body.content, body.token);
|
|
172
|
+
return respond(json(result, result.kind === "conflict" ? 409 : 200));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return respond(
|
|
175
|
+
json(
|
|
176
|
+
{ reason: error instanceof Error ? error.message : String(error) },
|
|
177
|
+
error instanceof VaultPathError ? 400 : 500,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
86
183
|
if (url.pathname !== "/") return respond(new Response("not found", { status: 404 }));
|
|
87
184
|
|
|
88
|
-
return respond(
|
|
185
|
+
return respond(
|
|
186
|
+
html(await render(await collectStudioPage(url, inventory, snapshots, vault, writable))),
|
|
187
|
+
);
|
|
89
188
|
};
|
|
189
|
+
|
|
190
|
+
handler.close = () => vault.stop();
|
|
191
|
+
return handler;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function selectedCompanion(
|
|
195
|
+
url: URL,
|
|
196
|
+
collectInventory: () => Promise<StudioInventory>,
|
|
197
|
+
): Promise<ReturnType<typeof resolveCompanion>> {
|
|
198
|
+
const inventory = await collectInventory();
|
|
199
|
+
return resolveCompanion(inventory, parseStudioSelection(url).companionDigest);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function vaultEvents(vault: VaultManager, companionPath: string, requestedPath: string): Response {
|
|
203
|
+
const encoder = new TextEncoder();
|
|
204
|
+
let unsubscribe = () => {};
|
|
205
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
206
|
+
start(controller) {
|
|
207
|
+
controller.enqueue(encoder.encode(": studio vault events\n\n"));
|
|
208
|
+
unsubscribe = vault.subscribe(companionPath, requestedPath, (event) => {
|
|
209
|
+
try {
|
|
210
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
211
|
+
} catch {
|
|
212
|
+
unsubscribe();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
cancel() {
|
|
217
|
+
unsubscribe();
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
return new Response(stream, {
|
|
221
|
+
headers: {
|
|
222
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
223
|
+
"cache-control": "no-cache",
|
|
224
|
+
connection: "keep-alive",
|
|
225
|
+
},
|
|
226
|
+
});
|
|
90
227
|
}
|
|
91
228
|
|
|
92
229
|
/**
|
|
@@ -97,6 +234,8 @@ async function collectStudioPage(
|
|
|
97
234
|
url: URL,
|
|
98
235
|
collectInventory: () => Promise<StudioInventory>,
|
|
99
236
|
snapshots: StudioSnapshotCache,
|
|
237
|
+
vault: VaultManager,
|
|
238
|
+
writable: boolean,
|
|
100
239
|
): Promise<StudioPage> {
|
|
101
240
|
const selection = parseStudioSelection(url);
|
|
102
241
|
const inventory = await collectInventory();
|
|
@@ -108,9 +247,51 @@ async function collectStudioPage(
|
|
|
108
247
|
payload: null,
|
|
109
248
|
error: null,
|
|
110
249
|
collectedAt: null,
|
|
250
|
+
writable,
|
|
251
|
+
vault: null,
|
|
111
252
|
};
|
|
112
253
|
|
|
113
|
-
if (!companion)
|
|
254
|
+
if (!companion) {
|
|
255
|
+
vault.deactivate();
|
|
256
|
+
return page;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (selection.view === "vault") {
|
|
260
|
+
try {
|
|
261
|
+
const tree = await vault.tree(companion.path, selection.refresh);
|
|
262
|
+
let open = null;
|
|
263
|
+
let refusal: string | null = null;
|
|
264
|
+
if (selection.openPath) {
|
|
265
|
+
try {
|
|
266
|
+
open = await vault.open(companion.path, selection.openPath);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
refusal = error instanceof Error ? error.message : String(error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
...page,
|
|
273
|
+
vault: {
|
|
274
|
+
tree: tree.tree,
|
|
275
|
+
open,
|
|
276
|
+
refusal,
|
|
277
|
+
incoming: null,
|
|
278
|
+
overwritten: null,
|
|
279
|
+
watching: tree.watching,
|
|
280
|
+
warning: tree.warning,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return {
|
|
285
|
+
...page,
|
|
286
|
+
error: {
|
|
287
|
+
companionPath: companion.path,
|
|
288
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
vault.deactivate();
|
|
114
295
|
|
|
115
296
|
try {
|
|
116
297
|
const snapshot = await snapshots.read(companion.path, selection.refresh);
|
|
@@ -144,19 +325,33 @@ function bunServe(options: StudioServeOptions): StudioBoundServer {
|
|
|
144
325
|
}
|
|
145
326
|
|
|
146
327
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* a browser at a URL nothing answers.
|
|
328
|
+
* A bind failure propagates: the caller reports it and exits rather than
|
|
329
|
+
* opening a browser at a URL nothing answers.
|
|
150
330
|
*/
|
|
151
|
-
export function startStudioServer(
|
|
331
|
+
export function startStudioServer(
|
|
332
|
+
deps: StudioServerDeps = {},
|
|
333
|
+
options: StudioServerOptions = {},
|
|
334
|
+
): StudioServerHandle {
|
|
152
335
|
const serve = deps.serve ?? bunServe;
|
|
153
|
-
const
|
|
336
|
+
const port = options.port ?? 0;
|
|
337
|
+
const hostname = options.hostname ?? STUDIO_HOSTNAME;
|
|
338
|
+
const fetchHandler = createStudioFetch(deps, { writable: options.writable });
|
|
339
|
+
const server = serve({ port, hostname, fetch: fetchHandler });
|
|
340
|
+
const urlHostname =
|
|
341
|
+
hostname === STUDIO_HOSTNAME || hostname === "localhost" || hostname === "::1"
|
|
342
|
+
? "localhost"
|
|
343
|
+
: hostname.includes(":") && !hostname.startsWith("[")
|
|
344
|
+
? `[${hostname}]`
|
|
345
|
+
: hostname;
|
|
154
346
|
|
|
155
347
|
return {
|
|
156
|
-
url: `http
|
|
348
|
+
url: `http://${urlHostname}:${server.port}`,
|
|
157
349
|
port: server.port,
|
|
158
|
-
hostname
|
|
159
|
-
stop: () =>
|
|
350
|
+
hostname,
|
|
351
|
+
stop: async () => {
|
|
352
|
+
fetchHandler.close();
|
|
353
|
+
await server.stop(true);
|
|
354
|
+
},
|
|
160
355
|
};
|
|
161
356
|
}
|
|
162
357
|
|