@piagent/platform 1.17.0 → 1.18.0
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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/catalog/claude-model-additions.json +49 -2
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +6 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +6 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +7 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +7 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/evals/runtime-conformance-v1/runner.mjs +13 -17
- package/package.json +1 -1
- package/packages/piagent-core/managed/native-catalog.mjs +14 -1
- package/packages/piagent-core/managed/request-stream.mjs +28 -0
- package/packages/piagent-core/managed/session.mjs +2 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CeQ79h30.js → MarkdownMessage-DlbuyiQ3.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-BNQK7rHr.js → index-BGxsZHB5.js} +14 -14
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/gateway-service.ts +26 -0
- package/packages/piagent-webui/gateway/git-branches.ts +94 -0
- package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +10 -3
- package/packages/piagent-webui/server/loopback-server.ts +9 -2
- package/packages/piagent-webui/server/member-routes.ts +38 -0
- package/scripts/configure-model-scope.sh +1 -0
- package/templates/global/settings.json +2 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
|
|
8
8
|
<meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
|
|
9
9
|
<title>Piagent WebUI</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BGxsZHB5.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./profile-state.ts";
|
|
21
21
|
import { buildSessionCatalog, projectRefForCwd } from "./session-catalog.ts";
|
|
22
22
|
import { suggestPaths } from "./path-suggestions.ts";
|
|
23
|
+
import { GitBranchError, listBranches, switchBranch, type SwitchRequest } from "./git-branches.ts";
|
|
23
24
|
import { listAgentCommands } from "../../piagent-core/runtime/resources/agent-resources.mjs";
|
|
24
25
|
import { sessionRefForPath } from "../ownership/session-refs.ts";
|
|
25
26
|
import { SessionMetadataStore } from "./session-metadata-store.ts";
|
|
@@ -229,6 +230,8 @@ export async function startPiagentGateway(options: {
|
|
|
229
230
|
?? (await runtimes!.listSessions()).find((info) => typeof info.cwd === "string" && projectRefForCwd(key, info.cwd) === projectRef)?.cwd
|
|
230
231
|
?? (relay ? (await relay.folders()).find((folder) => projectRefForCwd(key, folder) === projectRef) : undefined);
|
|
231
232
|
const relayPost = (path: string, body: unknown) => relay!.json("POST", path, body);
|
|
233
|
+
const runningInProject = async (projectRef: string) => (await hubCatalog()).sessions.filter((row) => row.projectRef === projectRef
|
|
234
|
+
&& !row.archived && ["running", "waiting-approval", "paused"].includes(row.liveState)).length;
|
|
232
235
|
const commands = new SessionCommandController({ catalog: hubCatalog, runtimes, metadata,
|
|
233
236
|
store: new SessionCommandStore(state.root, key), events,
|
|
234
237
|
// Resolved at call time: the attachment registry is built after this
|
|
@@ -303,6 +306,29 @@ export async function startPiagentGateway(options: {
|
|
|
303
306
|
const root = await projectFolder(projectRef);
|
|
304
307
|
return root ? { commands: listAgentCommands({ cwd: root }) } : null;
|
|
305
308
|
},
|
|
309
|
+
// The project's Git branches and a switch between them. A switch waits
|
|
310
|
+
// until no conversation in the folder runs (company ones included): the
|
|
311
|
+
// agent would find its files changed under it.
|
|
312
|
+
listBranches: async (projectRef) => {
|
|
313
|
+
const root = await projectFolder(projectRef);
|
|
314
|
+
if (!root) return null;
|
|
315
|
+
const [list, running] = await Promise.all([listBranches(root), runningInProject(projectRef)]);
|
|
316
|
+
return list ? { ...list, running } : { repository: false, running };
|
|
317
|
+
},
|
|
318
|
+
switchBranch: async (projectRef, request) => {
|
|
319
|
+
const root = await projectFolder(projectRef);
|
|
320
|
+
if (!root) return null;
|
|
321
|
+
const value = request as Partial<SwitchRequest> | null;
|
|
322
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.branch !== "string"
|
|
323
|
+
|| Object.keys(value).some((name) => !["branch", "create", "remote"].includes(name))
|
|
324
|
+
|| (value.create !== undefined && typeof value.create !== "boolean")
|
|
325
|
+
|| (value.remote !== undefined && value.remote !== null && typeof value.remote !== "string")) throw new GitBranchError("branch-request-invalid");
|
|
326
|
+
if (!(await listBranches(root))) throw new GitBranchError("not-a-git-repository");
|
|
327
|
+
if (await runningInProject(projectRef) > 0) throw new GitBranchError("branch-switch-blocked-running");
|
|
328
|
+
const head = await switchBranch(root, { branch: value.branch, create: value.create === true, remote: value.remote ?? null });
|
|
329
|
+
events.publish("catalog.changed", { reasonCode: "git-branch-switched" });
|
|
330
|
+
return { head, branches: await listBranches(root) };
|
|
331
|
+
},
|
|
306
332
|
updates: { status: () => updates!.status(), check: () => updates!.check(), apply: (request) => updates!.apply(request) },
|
|
307
333
|
answerSessionQuestion: (sessionRef, questionRef, answer) => company(sessionRef)
|
|
308
334
|
? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/questions/${encodeURIComponent(questionRef)}/answer`, answer)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { readGitHead, type GitHead } from "./git-head.ts";
|
|
4
|
+
|
|
5
|
+
// The branches of a project folder and a switch between them, for the branch
|
|
6
|
+
// menu in the dashboard. Git itself does the switch (`git switch`), so it
|
|
7
|
+
// refuses exactly what it refuses in a Terminal: local changes a checkout
|
|
8
|
+
// would overwrite, a name that already exists, a branch that is not there.
|
|
9
|
+
export type Branch = { name: string; current: boolean; remote: string | null; committedAt: string | null };
|
|
10
|
+
export type BranchList = { repository: true; head: GitHead | null; branches: Branch[]; truncated: boolean; changedFiles: number };
|
|
11
|
+
export type SwitchRequest = { branch: string; create?: boolean; remote?: string | null };
|
|
12
|
+
|
|
13
|
+
const GIT = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
14
|
+
const LIMIT = 200;
|
|
15
|
+
const SEP = "\u0000";
|
|
16
|
+
|
|
17
|
+
export class GitBranchError extends Error {
|
|
18
|
+
readonly detail: string | null;
|
|
19
|
+
constructor(code: string, detail: string | null = null) { super(code); this.detail = detail; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function git(cwd: string, args: string[], timeout = 15_000): Promise<string> {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
execFile(GIT, ["-C", cwd, ...args], { timeout, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
|
|
25
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" } }, (error, stdout, stderr) => {
|
|
26
|
+
if (error) reject(Object.assign(error, { stderr: String(stderr ?? "") }));
|
|
27
|
+
else resolve(String(stdout));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A branch name git accepts that cannot be read as an option.
|
|
33
|
+
export async function validBranchName(cwd: string, name: string): Promise<boolean> {
|
|
34
|
+
if (!name || name.length > 200 || name.startsWith("-") || /[\u0000-\u001f\u007f]/.test(name)) return false;
|
|
35
|
+
try { await git(cwd, ["check-ref-format", "--branch", name], 5_000); return true; } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function listBranches(cwd: string): Promise<BranchList | null> {
|
|
39
|
+
const head = readGitHead(cwd);
|
|
40
|
+
if (!head) {
|
|
41
|
+
try { await git(cwd, ["rev-parse", "--git-dir"], 5_000); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
let refs: string;
|
|
44
|
+
try {
|
|
45
|
+
refs = await git(cwd, ["for-each-ref", "--sort=-committerdate", `--count=${LIMIT * 2}`,
|
|
46
|
+
"--format=%(refname)%00%(committerdate:iso-strict)", "refs/heads", "refs/remotes"]);
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
const local = new Map<string, Branch>(), remote: Branch[] = [];
|
|
49
|
+
for (const line of refs.split("\n")) {
|
|
50
|
+
const [ref, date] = line.split(SEP);
|
|
51
|
+
if (!ref) continue;
|
|
52
|
+
const committedAt = date && Number.isFinite(Date.parse(date)) ? new Date(date).toISOString() : null;
|
|
53
|
+
if (ref.startsWith("refs/heads/")) {
|
|
54
|
+
const name = ref.slice("refs/heads/".length);
|
|
55
|
+
local.set(name, { name, current: !head?.detached && head?.name === name, remote: null, committedAt });
|
|
56
|
+
} else if (ref.startsWith("refs/remotes/")) {
|
|
57
|
+
const rest = ref.slice("refs/remotes/".length), slash = rest.indexOf("/");
|
|
58
|
+
if (slash <= 0 || rest.endsWith("/HEAD")) continue;
|
|
59
|
+
remote.push({ name: rest.slice(slash + 1), current: false, remote: rest.slice(0, slash), committedAt });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// A remote branch shows only when no local branch has its name: switching
|
|
63
|
+
// to it creates the local branch that tracks it.
|
|
64
|
+
const offered = remote.filter((item) => !local.has(item.name)).filter((item, index, all) =>
|
|
65
|
+
all.findIndex((other) => other.name === item.name) === index);
|
|
66
|
+
const all = [...local.values(), ...offered];
|
|
67
|
+
let changedFiles = 0;
|
|
68
|
+
try { changedFiles = (await git(cwd, ["status", "--porcelain=v1", "--untracked-files=no"], 10_000)).split("\n").filter(Boolean).length; }
|
|
69
|
+
catch { /* the count is advisory */ }
|
|
70
|
+
return { repository: true, head, branches: all.slice(0, LIMIT), truncated: all.length > LIMIT, changedFiles };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function failure(error: unknown): GitBranchError {
|
|
74
|
+
const stderr = String((error as { stderr?: unknown })?.stderr ?? "");
|
|
75
|
+
const detail = stderr.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 6).join("\n").slice(0, 600) || null;
|
|
76
|
+
if (/would be overwritten by checkout|Please commit your changes or stash them/i.test(stderr)) return new GitBranchError("branch-switch-local-changes", detail);
|
|
77
|
+
if (/already exists/i.test(stderr)) return new GitBranchError("branch-exists", detail);
|
|
78
|
+
if (/invalid reference|did not match any|not a commit|no such branch/i.test(stderr)) return new GitBranchError("branch-not-found", detail);
|
|
79
|
+
if (/you need to resolve your current index first|unmerged|in the middle of/i.test(stderr)) return new GitBranchError("branch-switch-unfinished-merge", detail);
|
|
80
|
+
if ((error as { killed?: boolean })?.killed) return new GitBranchError("branch-switch-timeout", detail);
|
|
81
|
+
return new GitBranchError("branch-switch-failed", detail);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function switchBranch(cwd: string, request: SwitchRequest): Promise<GitHead | null> {
|
|
85
|
+
const name = request.branch.trim();
|
|
86
|
+
if (!(await validBranchName(cwd, name))) throw new GitBranchError("branch-name-invalid");
|
|
87
|
+
const remote = request.remote ?? null;
|
|
88
|
+
if (remote !== null && (!/^[A-Za-z0-9._-]{1,100}$/.test(remote) || request.create)) throw new GitBranchError("branch-request-invalid");
|
|
89
|
+
const args = request.create ? ["switch", "-c", name]
|
|
90
|
+
: remote ? ["switch", "-c", name, "--track", `${remote}/${name}`]
|
|
91
|
+
: ["switch", name];
|
|
92
|
+
try { await git(cwd, args, 60_000); } catch (error) { throw failure(error); }
|
|
93
|
+
return readGitHead(cwd);
|
|
94
|
+
}
|
|
@@ -18,9 +18,10 @@ export type SessionOperationObservation = Readonly<{
|
|
|
18
18
|
|
|
19
19
|
const DEFAULT_RETRY_POLICY: SessionOperationRetryPolicy = Object.freeze({
|
|
20
20
|
// Pi's provider retry is useful before a response exists, but a long retry
|
|
21
|
-
// chain burns quota and keeps the browser in Working.
|
|
22
|
-
//
|
|
23
|
-
|
|
21
|
+
// chain burns quota and keeps the browser in Working. Two automatic retries
|
|
22
|
+
// (the company turn retry's budget, managed/request-stream.mjs) absorb a transient
|
|
23
|
+
// failure without replaying a persistent one for long.
|
|
24
|
+
maximumAttempts: 2,
|
|
24
25
|
maximumDelayMs: 8_000
|
|
25
26
|
});
|
|
26
27
|
|
|
@@ -145,6 +146,12 @@ export class SessionOperationLifecycle {
|
|
|
145
146
|
if (this.#phase === "pending") this.#phase = "running";
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
// Replay safety belongs to the model call that failed: Pi retries only
|
|
150
|
+
// that call, after the tools of earlier ones ran once. A new answer
|
|
151
|
+
// starts clean; output or a tool of this answer makes it unsafe.
|
|
152
|
+
if (type === "message_start" && event?.message?.role === "assistant") {
|
|
153
|
+
this.#visibleAssistantOutput = false; this.#toolCallObserved = false;
|
|
154
|
+
}
|
|
148
155
|
if (type === "message_update") {
|
|
149
156
|
const update = event?.assistantMessageEvent;
|
|
150
157
|
if (update?.type === "text_delta" && hasVisibleText(update.delta)) this.#visibleAssistantOutput = true;
|
|
@@ -72,9 +72,15 @@ export async function startLoopbackServer(options: {
|
|
|
72
72
|
answerSessionQuestion?: (sessionRef: string, questionRef: string, answer: unknown) => unknown | Promise<unknown>;
|
|
73
73
|
suggestPaths?: (projectRef: string, query: string) => unknown | Promise<unknown>;
|
|
74
74
|
listCommands?: (projectRef: string) => unknown | Promise<unknown>;
|
|
75
|
+
listBranches?: (projectRef: string) => unknown | Promise<unknown>;
|
|
76
|
+
switchBranch?: (projectRef: string, request: unknown) => unknown | Promise<unknown>;
|
|
75
77
|
updates?: UpdateRoutes;
|
|
76
78
|
bootstrapTtlMs?: number;
|
|
77
79
|
sessionTtlMs?: number;
|
|
80
|
+
// Requests a minute from one address before it has a browser session (the
|
|
81
|
+
// page, its assets, the bootstrap). A test harness opening dozens of fresh
|
|
82
|
+
// pages from one address raises it; Piagent itself never does.
|
|
83
|
+
anonymousRequestsPerMinute?: number;
|
|
78
84
|
}): Promise<LoopbackServer> {
|
|
79
85
|
const assets = new Map(loadStaticBundle(options.staticRoot));
|
|
80
86
|
const styleNonce = randomBytes(16).toString("base64");
|
|
@@ -112,7 +118,7 @@ export async function startLoopbackServer(options: {
|
|
|
112
118
|
if (now - rate.windowStart >= 60_000) Object.assign(rate, { windowStart: now, requests: 0, bootstrapFailures: 0 });
|
|
113
119
|
rate.requests += 1; rates.set(rateKey, rate);
|
|
114
120
|
while (rates.size > 128) rates.delete(rates.keys().next().value as string);
|
|
115
|
-
if (rate.requests > (browserSession ? SESSION_REQUESTS_PER_MINUTE : REQUESTS_PER_MINUTE)) return errorResponse(response, 429, "rate-limit");
|
|
121
|
+
if (rate.requests > (browserSession ? SESSION_REQUESTS_PER_MINUTE : options.anonymousRequestsPerMinute ?? REQUESTS_PER_MINUTE)) return errorResponse(response, 429, "rate-limit");
|
|
116
122
|
let url: URL;
|
|
117
123
|
try { url = new URL(request.url ?? "/", origin); }
|
|
118
124
|
catch { return errorResponse(response, 400, "invalid-url"); }
|
|
@@ -294,7 +300,8 @@ export async function startLoopbackServer(options: {
|
|
|
294
300
|
catch { return errorResponse(response, 503, "attachment-runtime-unavailable"); }
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
// The main agent's questions to the member,
|
|
303
|
+
// The main agent's questions to the member, a project's Git branches, and
|
|
304
|
+
// Piagent's own updates.
|
|
298
305
|
if (await routeMemberRequest({ request, response, url, origin, requestOrigin, auth, now, consumeControl }, options)) return;
|
|
299
306
|
|
|
300
307
|
if (request.method === "POST" && url.pathname.startsWith("/api/v1/approvals/") && url.pathname.endsWith("/decision") && options.executeApproval) {
|
|
@@ -12,6 +12,7 @@ const MAX_CONTROL_BODY_BYTES = 70_000;
|
|
|
12
12
|
const CURSOR = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
13
13
|
const QUESTION_PATH = /^\/api\/v1\/sessions\/([^/]+)\/questions(?:\/([^/]+)\/answer)?$/;
|
|
14
14
|
const PROJECT_PATH = /^\/api\/v1\/projects\/([^/]+)\/(paths|commands)$/;
|
|
15
|
+
const BRANCH_PATH = /^\/api\/v1\/projects\/([^/]+)\/branches(\/switch)?$/;
|
|
15
16
|
|
|
16
17
|
export type UpdateRoutes = {
|
|
17
18
|
// What is installed, what is available, and the last update run.
|
|
@@ -34,6 +35,10 @@ type Options = {
|
|
|
34
35
|
suggestPaths?: (projectRef: string, query: string) => unknown | Promise<unknown>;
|
|
35
36
|
// The project's and the member's commands and skills; null as above.
|
|
36
37
|
listCommands?: (projectRef: string) => unknown | Promise<unknown>;
|
|
38
|
+
// The project's Git branches, and a switch to one of them (or a new one);
|
|
39
|
+
// null as above.
|
|
40
|
+
listBranches?: (projectRef: string) => unknown | Promise<unknown>;
|
|
41
|
+
switchBranch?: (projectRef: string, request: unknown) => unknown | Promise<unknown>;
|
|
37
42
|
updates?: UpdateRoutes;
|
|
38
43
|
};
|
|
39
44
|
|
|
@@ -53,6 +58,11 @@ async function jsonBody(context: Context, code: string): Promise<{ ok: true; val
|
|
|
53
58
|
catch (error) { errorResponse(context.response, (error as Error).message === "body-limit" ? 413 : 400, code); return { ok: false }; }
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
const BRANCH_STATUS: Record<string, number> = {
|
|
62
|
+
"branch-request-invalid": 400, "branch-name-invalid": 400, "not-a-git-repository": 404, "branch-not-found": 404,
|
|
63
|
+
"branch-switch-blocked-running": 409, "branch-switch-local-changes": 409, "branch-exists": 409, "branch-switch-unfinished-merge": 409
|
|
64
|
+
};
|
|
65
|
+
|
|
56
66
|
const UPDATE_STATUS: Record<string, number> = {
|
|
57
67
|
"update-not-installable": 409, "update-blocked-running": 409, "update-already-running": 409,
|
|
58
68
|
"update-version-changed": 409, "update-not-available": 409, "update-request-invalid": 400
|
|
@@ -96,6 +106,34 @@ export async function routeMemberRequest(context: Context, options: Options): Pr
|
|
|
96
106
|
return true;
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
const branches = BRANCH_PATH.exec(url.pathname);
|
|
110
|
+
if (branches && (request.method === "GET" && !branches[2] && options.listBranches || request.method === "POST" && branches[2] && options.switchBranch)) {
|
|
111
|
+
const projectRef = decodeURIComponent(branches[1]);
|
|
112
|
+
if (request.method === "GET") {
|
|
113
|
+
if (!auth.authenticate(request)) { errorResponse(response, 401, "authentication-required"); return true; }
|
|
114
|
+
if (!CURSOR.test(projectRef)) { errorResponse(response, 400, "invalid-project-ref"); return true; }
|
|
115
|
+
try {
|
|
116
|
+
const value = await options.listBranches!(projectRef);
|
|
117
|
+
if (value === null) errorResponse(response, 404, "project-not-found"); else jsonResponse(response, 200, value);
|
|
118
|
+
} catch { errorResponse(response, 503, "branches-unavailable"); }
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (!authorizeChange(context)) return true;
|
|
122
|
+
if (!CURSOR.test(projectRef)) { errorResponse(response, 400, "invalid-project-ref"); return true; }
|
|
123
|
+
const body = await jsonBody(context, "branch-request-invalid");
|
|
124
|
+
if (!body.ok) return true;
|
|
125
|
+
try {
|
|
126
|
+
const value = await options.switchBranch!(projectRef, body.value);
|
|
127
|
+
if (value === null) errorResponse(response, 404, "project-not-found"); else jsonResponse(response, 200, value);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const code = error instanceof Error ? error.message : "branch-switch-failed";
|
|
130
|
+
const detail = (error as { detail?: unknown })?.detail;
|
|
131
|
+
jsonResponse(response, BRANCH_STATUS[code] ?? 500, { error: { code: BRANCH_STATUS[code] || code === "branch-switch-failed" || code === "branch-switch-timeout" ? code : "branch-switch-failed",
|
|
132
|
+
...(typeof detail === "string" ? { detail } : {}) } });
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
99
137
|
if (!options.updates || !url.pathname.startsWith("/api/v1/updates")) return false;
|
|
100
138
|
const updates = options.updates;
|
|
101
139
|
if (request.method === "GET" && url.pathname === "/api/v1/updates") {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"anthropic/claude-opus-4-8:xhigh",
|
|
28
28
|
"anthropic/claude-fable-5:xhigh",
|
|
29
29
|
"anthropic/claude-sonnet-5-5:high",
|
|
30
|
-
"anthropic/claude-opus-5-5:high"
|
|
30
|
+
"anthropic/claude-opus-5-5:high",
|
|
31
|
+
"anthropic/claude-haiku-5-5:medium"
|
|
31
32
|
],
|
|
32
33
|
"compaction": {
|
|
33
34
|
"enabled": true,
|