codeden 0.1.1 → 0.3.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/README.md +7 -1
- package/dist/THIRD-PARTY-NOTICES.txt +26 -0
- package/dist/cli.js +1 -1
- package/dist/drizzle/20261108041036_session_worktree_cleanup/migration.sql +2 -0
- package/dist/drizzle/20261108041036_session_worktree_cleanup/snapshot.json +3424 -0
- package/dist/drizzle/20261108045102_project_pin_order/migration.sql +4 -0
- package/dist/drizzle/20261108045102_project_pin_order/snapshot.json +3444 -0
- package/dist/drizzle/20261108052259_queued_message_sender/migration.sql +1 -0
- package/dist/drizzle/20261108052259_queued_message_sender/snapshot.json +3454 -0
- package/dist/drizzle/20261108085346_automation_created_by/migration.sql +1 -0
- package/dist/drizzle/20261108085346_automation_created_by/snapshot.json +3464 -0
- package/dist/drizzle/20261108090252_project_remote_warning/migration.sql +1 -0
- package/dist/drizzle/20261108090252_project_remote_warning/snapshot.json +3474 -0
- package/dist/{main-BF0aQ9U8.js → main-BAX1sPiD.js} +21971 -18752
- package/dist/main-BAX1sPiD.js.map +1 -0
- package/dist/{main-DaGFUbbW.js → main-DgpYK_Si.js} +2 -2
- package/dist/{main-DaGFUbbW.js.map → main-DgpYK_Si.js.map} +1 -1
- package/dist/{runner-D8m3WdtS.js → runner-DGomZl80.js} +2569 -472
- package/dist/runner-DGomZl80.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/web/assets/{core-BmMheb4i.js → core-D2h_xdbh.js} +1 -1
- package/dist/web/assets/engine-oniguruma-CG0oBFVI.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-nUCrIe8P.js → highlighted-body-JLMTFACH-DpId3Lg2.js} +1 -1
- package/dist/web/assets/{highlights-CdGBlIcw.js → highlights-C1SeynWZ.js} +1 -1
- package/dist/web/assets/index-BPuGYbOr.css +1 -0
- package/dist/web/assets/index-BXhyz2mO.js +914 -0
- package/dist/web/assets/langs-NwkcQ-R2.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-jqhY_qSL.js +1 -0
- package/dist/web/assets/{shiki-D8HIISPT.js → shiki-h6vxFpD2.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-BmWmYTv_.js → stream-tokenizer-D8Euixi9.js} +1 -1
- package/dist/web/assets/{themes-CKsIQs7_.js → themes-Bi1mPEwQ.js} +2 -2
- package/dist/web/favicon.svg +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-BF0aQ9U8.js.map +0 -1
- package/dist/runner-D8m3WdtS.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-CVzqqUIi.js +0 -1
- package/dist/web/assets/index-DKWZjMF1.js +0 -885
- package/dist/web/assets/index-DfoaM4E6.css +0 -1
- package/dist/web/assets/langs-kfQh6fa2.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-BIq7_eYY.js +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { accessSync, appendFileSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import os, { homedir, tmpdir } from "node:os";
|
|
6
6
|
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
7
7
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import { execFile, spawn } from "node:child_process";
|
|
9
|
+
import { gunzipSync } from "node:zlib";
|
|
9
10
|
import { createInterface } from "node:readline";
|
|
10
|
-
import { copyFile, mkdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
11
|
+
import { chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
|
11
12
|
import { STATUS_CODES, ServerResponse, createServer } from "node:http";
|
|
12
13
|
import { Http2ServerRequest, constants as constants$1 } from "node:http2";
|
|
13
14
|
import { Readable } from "node:stream";
|
|
@@ -3086,7 +3087,7 @@ function bucketFor(state, inst) {
|
|
|
3086
3087
|
return bucket;
|
|
3087
3088
|
}
|
|
3088
3089
|
let handoff;
|
|
3089
|
-
const open = [];
|
|
3090
|
+
const open$1 = [];
|
|
3090
3091
|
const memo = {
|
|
3091
3092
|
alloc(_inst, payload, empty) {
|
|
3092
3093
|
const bucket = handoff;
|
|
@@ -3097,7 +3098,7 @@ const memo = {
|
|
|
3097
3098
|
issues: null
|
|
3098
3099
|
};
|
|
3099
3100
|
bucket.set(payload.value, entry);
|
|
3100
|
-
open.push(entry);
|
|
3101
|
+
open$1.push(entry);
|
|
3101
3102
|
return empty;
|
|
3102
3103
|
},
|
|
3103
3104
|
guard(inst) {
|
|
@@ -3163,10 +3164,10 @@ const memo = {
|
|
|
3163
3164
|
return payload;
|
|
3164
3165
|
}
|
|
3165
3166
|
handoff = bucket;
|
|
3166
|
-
const depth = open.length;
|
|
3167
|
+
const depth = open$1.length;
|
|
3167
3168
|
const result = base(payload, ctx);
|
|
3168
3169
|
handoff = void 0;
|
|
3169
|
-
const entry = open.length > depth ? open.pop() : void 0;
|
|
3170
|
+
const entry = open$1.length > depth ? open$1.pop() : void 0;
|
|
3170
3171
|
if (result instanceof Promise) return result.then((r) => {
|
|
3171
3172
|
if (entry) entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
|
|
3172
3173
|
return r;
|
|
@@ -6003,12 +6004,16 @@ const Role = _enum([
|
|
|
6003
6004
|
"agent"
|
|
6004
6005
|
]);
|
|
6005
6006
|
/**
|
|
6006
|
-
*
|
|
6007
|
+
* Permission levels of an agent session (§11.2), mapped onto each provider's native enforcement.
|
|
6008
|
+
* Shown in the UI as "Permissions"; stored as `autonomy`.
|
|
6009
|
+
* - `read_only`: reads and runs checks, but never edits files, commits or pushes. Needs a
|
|
6010
|
+
* provider with `ProviderCapabilities.readOnly`.
|
|
6007
6011
|
* - `supervised`: every edit and command asks.
|
|
6008
6012
|
* - `auto`: edits and normal commands in the worktree run freely; risky actions ask (default).
|
|
6009
6013
|
* - `full`: no approvals inside the worktree.
|
|
6010
6014
|
*/
|
|
6011
6015
|
const AutonomyLevel = _enum([
|
|
6016
|
+
"read_only",
|
|
6012
6017
|
"supervised",
|
|
6013
6018
|
"auto",
|
|
6014
6019
|
"full"
|
|
@@ -6283,11 +6288,47 @@ const Attachment = discriminatedUnion("type", [object({
|
|
|
6283
6288
|
name: string().optional(),
|
|
6284
6289
|
mediaType: string().optional()
|
|
6285
6290
|
})]);
|
|
6291
|
+
/**
|
|
6292
|
+
* Who sent a message to a session: the user, the orchestrator (to a PM), a PM (to its
|
|
6293
|
+
* sessions), an automation, or CodeDen itself (automatic wake-ups, the factory loop).
|
|
6294
|
+
*/
|
|
6295
|
+
const MessageSender = _enum([
|
|
6296
|
+
"user",
|
|
6297
|
+
"orchestrator",
|
|
6298
|
+
"pm",
|
|
6299
|
+
"automation",
|
|
6300
|
+
"codeden"
|
|
6301
|
+
]);
|
|
6286
6302
|
/** Input that starts or steers a turn: text plus optional image/file attachments. */
|
|
6287
6303
|
const UserInput = object({
|
|
6288
6304
|
text: string(),
|
|
6289
|
-
attachments: array(Attachment).default([])
|
|
6305
|
+
attachments: array(Attachment).default([]),
|
|
6306
|
+
/**
|
|
6307
|
+
* Who wrote the message, when it was not the user: recorded so the transcript can show the
|
|
6308
|
+
* sender without reading the text. Absent on the user's own messages and on every message
|
|
6309
|
+
* stored before senders were recorded.
|
|
6310
|
+
*/
|
|
6311
|
+
from: MessageSender.optional()
|
|
6290
6312
|
});
|
|
6313
|
+
/**
|
|
6314
|
+
* The first line of a message from a PM or the orchestrator, telling the agent who wrote it.
|
|
6315
|
+
* The only definition of these words: the server writes them, the UI recognises them.
|
|
6316
|
+
*/
|
|
6317
|
+
const SENDER_LINES = {
|
|
6318
|
+
pm: "Message from your project manager:",
|
|
6319
|
+
orchestrator: "Message from the orchestrator:"
|
|
6320
|
+
};
|
|
6321
|
+
/**
|
|
6322
|
+
* Input for a message from a PM or the orchestrator: the sender is recorded in `from` and
|
|
6323
|
+
* named for the agent in the text's first line (`SENDER_LINES`).
|
|
6324
|
+
*/
|
|
6325
|
+
function messageFrom(from, text) {
|
|
6326
|
+
return {
|
|
6327
|
+
text: `${SENDER_LINES[from]}\n${text}`,
|
|
6328
|
+
attachments: [],
|
|
6329
|
+
from
|
|
6330
|
+
};
|
|
6331
|
+
}
|
|
6291
6332
|
const itemBase = {
|
|
6292
6333
|
/** Unique within the session. */
|
|
6293
6334
|
id: Id,
|
|
@@ -6812,6 +6853,18 @@ const FactoryLoopState = _enum([
|
|
|
6812
6853
|
"stopped"
|
|
6813
6854
|
]);
|
|
6814
6855
|
// ../../packages/contracts/src/entities/project.ts
|
|
6856
|
+
/**
|
|
6857
|
+
* Something re-detecting a project's folder found that CodeDen does not apply on its own (§9).
|
|
6858
|
+
* `github_remote_missing`: a `github` project's folder no longer has a GitHub `origin` (removed,
|
|
6859
|
+
* pointing at another host, or no git checkout at all). The project keeps its GitHub
|
|
6860
|
+
* coordinates until the user restores the remote or disconnects GitHub.
|
|
6861
|
+
*/
|
|
6862
|
+
const ProjectRemoteWarning = object({
|
|
6863
|
+
code: literal("github_remote_missing"),
|
|
6864
|
+
/** What `origin` points at now, credentials removed; null when there is no `origin`. */
|
|
6865
|
+
remoteUrl: string().nullable(),
|
|
6866
|
+
detectedAt: Timestamp
|
|
6867
|
+
});
|
|
6815
6868
|
const projectBase = {
|
|
6816
6869
|
id: Id,
|
|
6817
6870
|
name: string().min(1),
|
|
@@ -6833,6 +6886,23 @@ const projectBase = {
|
|
|
6833
6886
|
killSwitch: boolean().default(false),
|
|
6834
6887
|
/** IANA timezone for cron automations; machine timezone when null. */
|
|
6835
6888
|
timezone: string().nullable(),
|
|
6889
|
+
/**
|
|
6890
|
+
* Pinned projects are listed above the rest (§13). Defaults to false so events written
|
|
6891
|
+
* before pinning existed still parse.
|
|
6892
|
+
*/
|
|
6893
|
+
pinned: boolean().default(false),
|
|
6894
|
+
/**
|
|
6895
|
+
* Manual position in the project list, lowest first (§13). Only the relative order within
|
|
6896
|
+
* the pinned and unpinned groups matters; ties fall back to the name. Set by
|
|
6897
|
+
* `projects.reordered`; archived projects keep theirs.
|
|
6898
|
+
*/
|
|
6899
|
+
sortOrder: int().nonnegative().default(0),
|
|
6900
|
+
/**
|
|
6901
|
+
* Set when re-detection found the folder's git remote out of step with the project (see
|
|
6902
|
+
* `ProjectRemoteWarning`); absent when all is well. Cleared by the next detection that finds
|
|
6903
|
+
* the GitHub remote again, or by disconnecting GitHub.
|
|
6904
|
+
*/
|
|
6905
|
+
remoteWarning: ProjectRemoteWarning.optional(),
|
|
6836
6906
|
createdAt: Timestamp,
|
|
6837
6907
|
updatedAt: Timestamp,
|
|
6838
6908
|
archivedAt: Timestamp.nullable()
|
|
@@ -6847,6 +6917,9 @@ const GitHubRepo = object({
|
|
|
6847
6917
|
/**
|
|
6848
6918
|
* A project. `github` projects get the full issue/PR/evidence flow; `local` projects
|
|
6849
6919
|
* (no GitHub remote) get sessions, worktrees, checkpoints, diffs and automations only (§9).
|
|
6920
|
+
* The kind is detected when the project is added and again later (on server start, after
|
|
6921
|
+
* agent turns in local projects, on demand): a folder that gains a GitHub `origin` becomes a
|
|
6922
|
+
* `github` project. Losing the remote never downgrades a project on its own.
|
|
6850
6923
|
*/
|
|
6851
6924
|
const Project = discriminatedUnion("kind", [object({
|
|
6852
6925
|
...projectBase,
|
|
@@ -6856,6 +6929,20 @@ const Project = discriminatedUnion("kind", [object({
|
|
|
6856
6929
|
...projectBase,
|
|
6857
6930
|
kind: literal("local")
|
|
6858
6931
|
})]);
|
|
6932
|
+
/**
|
|
6933
|
+
* The canonical project order (§13): pinned first, then `sortOrder`, then name, then id.
|
|
6934
|
+
* The server lists projects in this order and the UI keeps it when applying live events.
|
|
6935
|
+
*/
|
|
6936
|
+
function compareProjects(a, b) {
|
|
6937
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
6938
|
+
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
|
6939
|
+
if (a.name !== b.name) return a.name < b.name ? -1 : 1;
|
|
6940
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
6941
|
+
}
|
|
6942
|
+
/** A sorted copy of `projects` in the canonical order. */
|
|
6943
|
+
function sortProjects(projects) {
|
|
6944
|
+
return [...projects].sort(compareProjects);
|
|
6945
|
+
}
|
|
6859
6946
|
// ../../packages/contracts/src/work.ts
|
|
6860
6947
|
/**
|
|
6861
6948
|
* Situational awareness and conflict handling (§8.4). Every unit of work (a session or an
|
|
@@ -7013,7 +7100,18 @@ const ProviderCapabilities = object({
|
|
|
7013
7100
|
reasoningEffort: boolean(),
|
|
7014
7101
|
/** MCP servers can be injected (required for CodeDen tools). */
|
|
7015
7102
|
mcp: boolean(),
|
|
7016
|
-
skills: boolean()
|
|
7103
|
+
skills: boolean(),
|
|
7104
|
+
/**
|
|
7105
|
+
* An autonomy change reaches a running turn from its next tool call. When false (or absent),
|
|
7106
|
+
* a running turn keeps its autonomy and the change applies from the next turn.
|
|
7107
|
+
*/
|
|
7108
|
+
liveAutonomy: boolean().optional(),
|
|
7109
|
+
/**
|
|
7110
|
+
* The `read_only` permission level is enforced natively: no file edits, no writing shell
|
|
7111
|
+
* commands, no commits or pushes (§11.2). When false (or absent), sessions on this provider
|
|
7112
|
+
* cannot use `read_only`.
|
|
7113
|
+
*/
|
|
7114
|
+
readOnly: boolean().optional()
|
|
7017
7115
|
});
|
|
7018
7116
|
/** A model a provider offers. */
|
|
7019
7117
|
const ModelInfo = object({
|
|
@@ -7532,7 +7630,8 @@ const RunnerCommand = discriminatedUnion("name", [
|
|
|
7532
7630
|
name: literal("session.configure"),
|
|
7533
7631
|
sessionId: Id,
|
|
7534
7632
|
model: string().nullable().optional(),
|
|
7535
|
-
effort: ReasoningEffort.nullable().optional()
|
|
7633
|
+
effort: ReasoningEffort.nullable().optional(),
|
|
7634
|
+
autonomy: AutonomyLevel.optional()
|
|
7536
7635
|
}),
|
|
7537
7636
|
object({
|
|
7538
7637
|
name: literal("pty.open"),
|
|
@@ -7671,7 +7770,11 @@ const RunnerCommandResults = {
|
|
|
7671
7770
|
ok: boolean()
|
|
7672
7771
|
})).optional()
|
|
7673
7772
|
}),
|
|
7674
|
-
"workspace.remove": object({
|
|
7773
|
+
"workspace.remove": object({
|
|
7774
|
+
removed: boolean(),
|
|
7775
|
+
/** A forced removal deleted uncommitted changes (absent from older runners). */
|
|
7776
|
+
discardedChanges: boolean().optional()
|
|
7777
|
+
}),
|
|
7675
7778
|
"session.start": object({ resumeToken: string().nullable() }),
|
|
7676
7779
|
"session.send": Empty,
|
|
7677
7780
|
"session.steer": Empty,
|
|
@@ -7780,8 +7883,17 @@ const QueuedMessage = object({
|
|
|
7780
7883
|
id: Id,
|
|
7781
7884
|
sessionId: Id,
|
|
7782
7885
|
input: UserInput,
|
|
7886
|
+
/** Who queued it. Events written before senders were recorded were all the user's. */
|
|
7887
|
+
from: MessageSender.default("user"),
|
|
7783
7888
|
createdAt: Timestamp
|
|
7784
7889
|
});
|
|
7890
|
+
object({ messages: array(QueuedMessage) });
|
|
7891
|
+
/**
|
|
7892
|
+
* `PATCH /api/sessions/:id/queue/:messageId`: replace a queued message's text (attachments
|
|
7893
|
+
* stay). Answers 409 `already_sent` once delivery has taken the message.
|
|
7894
|
+
*/
|
|
7895
|
+
const EditQueuedMessageRequest = object({ text: string().max(2e5) });
|
|
7896
|
+
object({ status: literal("steered") });
|
|
7785
7897
|
/** Structured result an agent reports with the `finish` tool (§8.3). */
|
|
7786
7898
|
const SessionResult = object({
|
|
7787
7899
|
outcome: _enum([
|
|
@@ -7981,6 +8093,13 @@ const Session = object({
|
|
|
7981
8093
|
rootThreadId: Id.nullable(),
|
|
7982
8094
|
/** Structured result from the agent's `finish` tool, once reported. */
|
|
7983
8095
|
result: SessionResult.nullable().optional(),
|
|
8096
|
+
/** When the session was archived: hidden from the sidebar, its worktree cleaned up. */
|
|
8097
|
+
archivedAt: Timestamp.nullable().optional(),
|
|
8098
|
+
/**
|
|
8099
|
+
* When CodeDen removed the session's worktree (archive or inactivity cleanup). The branch and
|
|
8100
|
+
* checkpoints stay; the worktree is recreated from the branch on the next message.
|
|
8101
|
+
*/
|
|
8102
|
+
worktreeRemovedAt: Timestamp.nullable().optional(),
|
|
7984
8103
|
createdAt: Timestamp,
|
|
7985
8104
|
updatedAt: Timestamp,
|
|
7986
8105
|
endedAt: Timestamp.nullable()
|
|
@@ -8117,6 +8236,12 @@ const AutomationGuards = object({
|
|
|
8117
8236
|
/** Run once for missed schedules after downtime. */
|
|
8118
8237
|
catchUpMissed: boolean().default(false)
|
|
8119
8238
|
});
|
|
8239
|
+
/** Who created a database automation (§10). */
|
|
8240
|
+
const AutomationCreator = _enum([
|
|
8241
|
+
"user",
|
|
8242
|
+
"pm",
|
|
8243
|
+
"import"
|
|
8244
|
+
]);
|
|
8120
8245
|
/** An automation: trigger + action + target + guards. Off by default. */
|
|
8121
8246
|
const Automation = object({
|
|
8122
8247
|
id: Id,
|
|
@@ -8142,6 +8267,11 @@ const Automation = object({
|
|
|
8142
8267
|
* `automation:<template id or name slug>`.
|
|
8143
8268
|
*/
|
|
8144
8269
|
workKey: string().min(1).max(200).optional(),
|
|
8270
|
+
/**
|
|
8271
|
+
* Who created a database automation: the user in the UI (`user`, the default when absent),
|
|
8272
|
+
* a PM through `create_automation` (`pm`), or an import of shared files (`import`).
|
|
8273
|
+
*/
|
|
8274
|
+
createdBy: AutomationCreator.optional(),
|
|
8145
8275
|
createdAt: Timestamp,
|
|
8146
8276
|
updatedAt: Timestamp
|
|
8147
8277
|
});
|
|
@@ -9156,9 +9286,14 @@ const UpdateMode = _enum([
|
|
|
9156
9286
|
/**
|
|
9157
9287
|
* Where the running CodeDen came from. `packaged`: an npm install (`npx`, global or a
|
|
9158
9288
|
* side-by-side version in `<home>/app`), which can update itself. `source`: the repository or
|
|
9159
|
-
* a workspace build, which never switches versions (pull and rebuild instead).
|
|
9289
|
+
* a workspace build, which never switches versions (pull and rebuild instead). `desktop`: the
|
|
9290
|
+
* desktop app, which updates itself from GitHub Releases (the npm path is off; §17).
|
|
9160
9291
|
*/
|
|
9161
|
-
const InstallSource = _enum([
|
|
9292
|
+
const InstallSource = _enum([
|
|
9293
|
+
"packaged",
|
|
9294
|
+
"source",
|
|
9295
|
+
"desktop"
|
|
9296
|
+
]);
|
|
9162
9297
|
/**
|
|
9163
9298
|
* Self-update progress. `installing`: npm is fetching `target` into `<home>/app/<target>`;
|
|
9164
9299
|
* `installed`: it is ready and starts on the next launch (or with a restart); `restarting`:
|
|
@@ -9290,6 +9425,138 @@ function comparePrerelease(a, b) {
|
|
|
9290
9425
|
}
|
|
9291
9426
|
return 0;
|
|
9292
9427
|
}
|
|
9428
|
+
// ../../packages/contracts/src/voice-api.ts
|
|
9429
|
+
/**
|
|
9430
|
+
* Voice engines on the server (§11.3). The browser's speech APIs stay the default; these
|
|
9431
|
+
* routes proxy ElevenLabs (text to speech, Scribe) and OpenAI (transcription) with API keys
|
|
9432
|
+
* from the OS keychain, so keys never reach the browser, and run local Whisper with a
|
|
9433
|
+
* runtime and model downloaded on demand.
|
|
9434
|
+
*/
|
|
9435
|
+
/** ElevenLabs MP3 output formats (`codec_sampleRate_bitrate`). */
|
|
9436
|
+
const TtsOutputFormat = _enum([
|
|
9437
|
+
"mp3_22050_32",
|
|
9438
|
+
"mp3_44100_64",
|
|
9439
|
+
"mp3_44100_128",
|
|
9440
|
+
"mp3_44100_192"
|
|
9441
|
+
]);
|
|
9442
|
+
/**
|
|
9443
|
+
* `POST /api/tts`: streams `audio/mpeg`. `voiceId` defaults to
|
|
9444
|
+
* `settings.voice.readAloud.voice`, then a stock voice; `modelId` to a low-latency model.
|
|
9445
|
+
*/
|
|
9446
|
+
const TtsRequest = object({
|
|
9447
|
+
text: string().min(1).max(5e3),
|
|
9448
|
+
voiceId: string().min(1).max(100).optional(),
|
|
9449
|
+
modelId: string().min(1).max(100).optional(),
|
|
9450
|
+
outputFormat: TtsOutputFormat.optional()
|
|
9451
|
+
});
|
|
9452
|
+
object({ voices: array(object({
|
|
9453
|
+
id: string(),
|
|
9454
|
+
name: string(),
|
|
9455
|
+
category: string().optional(),
|
|
9456
|
+
description: string().optional(),
|
|
9457
|
+
previewUrl: string().optional(),
|
|
9458
|
+
labels: record(string(), string()).optional()
|
|
9459
|
+
})) });
|
|
9460
|
+
/** Engines that transcribe on the server: the cloud engines and local Whisper. */
|
|
9461
|
+
const SttEngine = _enum([
|
|
9462
|
+
"elevenlabs",
|
|
9463
|
+
"openai",
|
|
9464
|
+
"local"
|
|
9465
|
+
]);
|
|
9466
|
+
/**
|
|
9467
|
+
* `POST /api/stt?engine=&language=`: the body is the recorded audio (raw bytes with an
|
|
9468
|
+
* `audio/*` content type, or multipart with a `file` field), up to 25 MB. `engine` defaults
|
|
9469
|
+
* to `settings.voice.dictation.engine` (when that is `browser`, ElevenLabs). The `local`
|
|
9470
|
+
* engine only takes WAV, 16-bit PCM, mono at `LOCAL_STT_SAMPLE_RATE`, at most
|
|
9471
|
+
* `LOCAL_STT_MAX_SECONDS` long (the UI converts the recording before uploading).
|
|
9472
|
+
*/
|
|
9473
|
+
const SttQuery = object({
|
|
9474
|
+
engine: SttEngine.optional(),
|
|
9475
|
+
/** ISO 639-1 hint, e.g. `en`. */
|
|
9476
|
+
language: string().min(2).max(10).optional()
|
|
9477
|
+
});
|
|
9478
|
+
object({
|
|
9479
|
+
text: string(),
|
|
9480
|
+
language: string().nullable(),
|
|
9481
|
+
engine: SttEngine,
|
|
9482
|
+
/** How long the server spent transcribing, in milliseconds (local engine). */
|
|
9483
|
+
elapsedMs: number$1().nonnegative().optional()
|
|
9484
|
+
});
|
|
9485
|
+
/**
|
|
9486
|
+
* Dictation engines (`settings.voice.dictation.engine`): the browser's speech recognition,
|
|
9487
|
+
* a cloud engine through the server, or `local` Whisper running on the CodeDen server.
|
|
9488
|
+
*/
|
|
9489
|
+
const DictationEngine = _enum([
|
|
9490
|
+
"browser",
|
|
9491
|
+
"elevenlabs",
|
|
9492
|
+
"openai",
|
|
9493
|
+
"local"
|
|
9494
|
+
]);
|
|
9495
|
+
/**
|
|
9496
|
+
* Local Whisper models CodeDen can download. `.en` models are English-only (more accurate
|
|
9497
|
+
* for English at the same size); the others detect or take a language.
|
|
9498
|
+
*/
|
|
9499
|
+
const WhisperModelId = _enum([
|
|
9500
|
+
"tiny",
|
|
9501
|
+
"tiny.en",
|
|
9502
|
+
"base",
|
|
9503
|
+
"base.en",
|
|
9504
|
+
"small",
|
|
9505
|
+
"small.en"
|
|
9506
|
+
]);
|
|
9507
|
+
/** Audio format the local engine accepts: WAV, 16-bit PCM, mono, at this sample rate. */
|
|
9508
|
+
const LOCAL_STT_SAMPLE_RATE = 16e3;
|
|
9509
|
+
const downloadFields = {
|
|
9510
|
+
state: _enum([
|
|
9511
|
+
"missing",
|
|
9512
|
+
"downloading",
|
|
9513
|
+
"installed",
|
|
9514
|
+
"failed"
|
|
9515
|
+
]),
|
|
9516
|
+
/** Bytes received so far (while downloading). */
|
|
9517
|
+
receivedBytes: int().nonnegative(),
|
|
9518
|
+
/** Download size in bytes (pinned, so known before the download starts). */
|
|
9519
|
+
totalBytes: int().nonnegative(),
|
|
9520
|
+
/** Size on disk when installed. */
|
|
9521
|
+
sizeOnDisk: int().nonnegative().nullable(),
|
|
9522
|
+
/** Why the last attempt failed. */
|
|
9523
|
+
error: string().nullable()
|
|
9524
|
+
};
|
|
9525
|
+
/** The Whisper runtime: a prebuilt native library for this platform, downloaded on demand. */
|
|
9526
|
+
const LocalSttRuntime = object({
|
|
9527
|
+
/** Runtime version CodeDen pins. */
|
|
9528
|
+
version: string(),
|
|
9529
|
+
...downloadFields
|
|
9530
|
+
});
|
|
9531
|
+
/** One downloadable model, with a speed and accuracy hint for the picker. */
|
|
9532
|
+
const LocalSttModel = object({
|
|
9533
|
+
id: WhisperModelId,
|
|
9534
|
+
/** e.g. `Base (English)`. */
|
|
9535
|
+
label: string(),
|
|
9536
|
+
/** Speed and accuracy hint, e.g. `Fast, good accuracy`. */
|
|
9537
|
+
hint: string(),
|
|
9538
|
+
englishOnly: boolean(),
|
|
9539
|
+
/** The suggested default. */
|
|
9540
|
+
recommended: boolean(),
|
|
9541
|
+
...downloadFields
|
|
9542
|
+
});
|
|
9543
|
+
object({
|
|
9544
|
+
/** False on platforms without a prebuilt runtime (`unsupportedReason` says why). */
|
|
9545
|
+
supported: boolean(),
|
|
9546
|
+
/** `process.platform-process.arch` of the server, e.g. `win32-x64`. */
|
|
9547
|
+
platform: string(),
|
|
9548
|
+
unsupportedReason: string().nullable(),
|
|
9549
|
+
runtime: LocalSttRuntime,
|
|
9550
|
+
models: array(LocalSttModel),
|
|
9551
|
+
/** Folder holding the models. */
|
|
9552
|
+
location: string()
|
|
9553
|
+
});
|
|
9554
|
+
/**
|
|
9555
|
+
* `POST /api/voice/local/models/:id/download` starts (or joins) the download of the runtime
|
|
9556
|
+
* and that model; `DELETE /api/voice/local/models/:id` cancels or deletes it. Both return
|
|
9557
|
+
* the new `LocalSttStatus`. The UI polls `GET /api/voice/local` while anything downloads.
|
|
9558
|
+
*/
|
|
9559
|
+
const LocalSttModelParams = object({ id: WhisperModelId });
|
|
9293
9560
|
// ../../packages/contracts/src/settings.ts
|
|
9294
9561
|
/** `auto` lets CodeDen pick the first ready provider (Claude, then Codex). */
|
|
9295
9562
|
const ProviderChoice = union([literal("auto"), ProviderId]);
|
|
@@ -9320,6 +9587,17 @@ const RoleDefaults = object({
|
|
|
9320
9587
|
model: string().nullable().default(null),
|
|
9321
9588
|
effort: ReasoningEffort.nullable().default(null)
|
|
9322
9589
|
});
|
|
9590
|
+
/** A model named by its provider and the provider's model id. */
|
|
9591
|
+
const ModelRef = object({
|
|
9592
|
+
provider: ProviderId,
|
|
9593
|
+
model: string().min(1)
|
|
9594
|
+
});
|
|
9595
|
+
/** A provider's custom models: ids are unique. */
|
|
9596
|
+
const CustomModelList = array(object({
|
|
9597
|
+
id: string().trim().min(1).max(200),
|
|
9598
|
+
/** Shown instead of the id in the picker. */
|
|
9599
|
+
label: string().trim().min(1).max(100).optional()
|
|
9600
|
+
})).refine((list) => new Set(list.map((m) => m.id)).size === list.length, { message: "Custom model ids must be unique" });
|
|
9323
9601
|
/**
|
|
9324
9602
|
* User settings persisted in `<home>/settings.json` (non-secret; API keys live in the OS
|
|
9325
9603
|
* keychain). Every field has a default, so `Settings.parse({})` yields a complete object and
|
|
@@ -9350,6 +9628,25 @@ const Settings = object({
|
|
|
9350
9628
|
noticeSummaries: boolean().default(true)
|
|
9351
9629
|
}).prefault({})
|
|
9352
9630
|
}).prefault({}),
|
|
9631
|
+
/** The model picker (§13). */
|
|
9632
|
+
models: object({
|
|
9633
|
+
/**
|
|
9634
|
+
* Starred models, listed in this order in the picker's Favourites section. Entries whose
|
|
9635
|
+
* model a provider no longer lists are kept (it may come back) but not shown.
|
|
9636
|
+
*/
|
|
9637
|
+
favorites: array(ModelRef).default([]),
|
|
9638
|
+
/**
|
|
9639
|
+
* Model ids left out of the picker, by provider id. A hidden model that is still chosen
|
|
9640
|
+
* somewhere (a session, a default, an automation) keeps showing there.
|
|
9641
|
+
*/
|
|
9642
|
+
hidden: record(ProviderId, array(string().min(1))).default({}),
|
|
9643
|
+
/**
|
|
9644
|
+
* Model ids added by the user, by provider id, merged into the picker after the models
|
|
9645
|
+
* the provider lists (a listed id wins). Their effort levels are those of the provider's
|
|
9646
|
+
* default model, or none when the provider has no effort levels.
|
|
9647
|
+
*/
|
|
9648
|
+
custom: record(ProviderId, CustomModelList).default({})
|
|
9649
|
+
}).prefault({}),
|
|
9353
9650
|
/** Follow-up prompt chips (§7.5). */
|
|
9354
9651
|
followups: object({
|
|
9355
9652
|
enabled: boolean().default(true),
|
|
@@ -9359,13 +9656,15 @@ const Settings = object({
|
|
|
9359
9656
|
/** Voice (§11.3). */
|
|
9360
9657
|
voice: object({
|
|
9361
9658
|
dictation: object({
|
|
9362
|
-
engine:
|
|
9363
|
-
"browser",
|
|
9364
|
-
"elevenlabs",
|
|
9365
|
-
"openai"
|
|
9366
|
-
]).default("browser"),
|
|
9659
|
+
engine: DictationEngine.default("browser"),
|
|
9367
9660
|
pushToTalkShortcut: string().nullable().default(null),
|
|
9368
|
-
|
|
9661
|
+
/**
|
|
9662
|
+
* BCP 47 tag such as `en-US`; null is auto (the browser engine follows the browser
|
|
9663
|
+
* language, the server engines detect it). English-only Whisper models ignore it.
|
|
9664
|
+
*/
|
|
9665
|
+
language: string().nullable().default(null),
|
|
9666
|
+
/** Local Whisper: which downloaded model transcribes (§11.3). */
|
|
9667
|
+
local: object({ model: WhisperModelId.default("base.en") }).prefault({})
|
|
9369
9668
|
}).prefault({}),
|
|
9370
9669
|
readAloud: object({
|
|
9371
9670
|
enabled: boolean().default(false),
|
|
@@ -9393,12 +9692,23 @@ const Settings = object({
|
|
|
9393
9692
|
}).prefault({}),
|
|
9394
9693
|
notifications: object({
|
|
9395
9694
|
toasts: boolean().default(true),
|
|
9396
|
-
browser: boolean().default(false)
|
|
9695
|
+
browser: boolean().default(false),
|
|
9696
|
+
/** A short chime when an agent is blocked on the user (an approval or a question). */
|
|
9697
|
+
sound: boolean().default(true)
|
|
9397
9698
|
}).prefault({}),
|
|
9398
9699
|
/** Global factory controls (§10.1). */
|
|
9399
9700
|
factory: object({
|
|
9400
9701
|
/** Stops every project's factory loop and all factory sessions. */
|
|
9401
9702
|
killSwitch: boolean().default(false) }).prefault({}),
|
|
9703
|
+
/**
|
|
9704
|
+
* Worktree cleanup (§9). Archiving a session removes its worktree (unless it has uncommitted changes and isn't forced); with `cleanup` on,
|
|
9705
|
+
* worktrees of sessions inactive for `afterDays` are removed too, unless the session's pull
|
|
9706
|
+
* request is still open or the worktree has uncommitted changes.
|
|
9707
|
+
*/
|
|
9708
|
+
worktrees: object({
|
|
9709
|
+
cleanup: boolean().default(true),
|
|
9710
|
+
afterDays: int().min(1).max(365).default(7)
|
|
9711
|
+
}).prefault({}),
|
|
9402
9712
|
/** Master switch; individual automations are still off by default (§10). */
|
|
9403
9713
|
automations: object({ enabled: boolean().default(true) }).prefault({}),
|
|
9404
9714
|
/**
|
|
@@ -9406,6 +9716,15 @@ killSwitch: boolean().default(false) }).prefault({}),
|
|
|
9406
9716
|
* `check: false` to turn checks off; that reads as `mode: "off"`.
|
|
9407
9717
|
*/
|
|
9408
9718
|
updates: preprocess((value) => typeof value === "object" && value !== null && !("mode" in value) && value.check === false ? { mode: "off" } : value, object({ mode: UpdateMode.default("notify") }).prefault({})),
|
|
9719
|
+
/** The desktop app (§17). Read by the desktop app only; `npx codeden` ignores it. */
|
|
9720
|
+
desktop: object({
|
|
9721
|
+
/** Start CodeDen (in the tray, window hidden) when you log in. */
|
|
9722
|
+
launchAtLogin: boolean().default(false),
|
|
9723
|
+
/** Closing the window keeps CodeDen running in the tray / menu bar; Quit really quits. */
|
|
9724
|
+
closeToTray: boolean().default(true),
|
|
9725
|
+
/** Keep the computer from sleeping while an agent is running. */
|
|
9726
|
+
keepAwake: boolean().default(true)
|
|
9727
|
+
}).prefault({}),
|
|
9409
9728
|
/** First-run setup (§14): set once the user finishes the detection checklist. */
|
|
9410
9729
|
setup: object({
|
|
9411
9730
|
completed: boolean().default(false),
|
|
@@ -9467,6 +9786,7 @@ discriminatedUnion("type", [
|
|
|
9467
9786
|
globalEvent("project.created", object({ project: Project })),
|
|
9468
9787
|
globalEvent("project.updated", object({ project: Project })),
|
|
9469
9788
|
globalEvent("project.archived", object({ projectId: Id })),
|
|
9789
|
+
globalEvent("projects.reordered", object({ projectIds: array(Id) })),
|
|
9470
9790
|
globalEvent("session.created", object({ session: Session })),
|
|
9471
9791
|
globalEvent("session.updated", object({ session: Session })),
|
|
9472
9792
|
globalEvent("inbox.created", object({ item: InboxItem })),
|
|
@@ -9502,7 +9822,17 @@ discriminatedUnion("type", [
|
|
|
9502
9822
|
globalEvent("message.dequeued", object({
|
|
9503
9823
|
sessionId: Id,
|
|
9504
9824
|
messageId: Id,
|
|
9505
|
-
reason: _enum([
|
|
9825
|
+
reason: _enum([
|
|
9826
|
+
"sent",
|
|
9827
|
+
"dropped",
|
|
9828
|
+
"deleted"
|
|
9829
|
+
])
|
|
9830
|
+
})),
|
|
9831
|
+
globalEvent("message.edited", object({
|
|
9832
|
+
sessionId: Id,
|
|
9833
|
+
messageId: Id,
|
|
9834
|
+
text: string(),
|
|
9835
|
+
at: Timestamp
|
|
9506
9836
|
})),
|
|
9507
9837
|
globalEvent("notice.created", object({ notice: Notice })),
|
|
9508
9838
|
globalEvent("notice.delivered", object({
|
|
@@ -9674,20 +10004,22 @@ const CreateProjectRequest = object({
|
|
|
9674
10004
|
defaultProvider: ProviderId.nullable().optional(),
|
|
9675
10005
|
autonomy: AutonomyLevel.nullable().optional()
|
|
9676
10006
|
});
|
|
10007
|
+
/** What CodeDen found in a project folder. */
|
|
10008
|
+
const ProjectDetection = object({
|
|
10009
|
+
isGitRepo: boolean(),
|
|
10010
|
+
/** Top level of the git checkout (may differ from the chosen folder). */
|
|
10011
|
+
gitRoot: string().nullable(),
|
|
10012
|
+
remoteUrl: string().nullable(),
|
|
10013
|
+
github: GitHubRepo.nullable(),
|
|
10014
|
+
defaultBranch: string().nullable(),
|
|
10015
|
+
currentBranch: string().nullable(),
|
|
10016
|
+
packageManager: PackageManager.nullable(),
|
|
10017
|
+
/** Suggested worktree setup commands, e.g. `pnpm install`. */
|
|
10018
|
+
suggestedSetupCommands: array(string())
|
|
10019
|
+
});
|
|
9677
10020
|
object({
|
|
9678
10021
|
project: Project,
|
|
9679
|
-
detected:
|
|
9680
|
-
isGitRepo: boolean(),
|
|
9681
|
-
/** Top level of the git checkout (may differ from the chosen folder). */
|
|
9682
|
-
gitRoot: string().nullable(),
|
|
9683
|
-
remoteUrl: string().nullable(),
|
|
9684
|
-
github: GitHubRepo.nullable(),
|
|
9685
|
-
defaultBranch: string().nullable(),
|
|
9686
|
-
currentBranch: string().nullable(),
|
|
9687
|
-
packageManager: PackageManager.nullable(),
|
|
9688
|
-
/** Suggested worktree setup commands, e.g. `pnpm install`. */
|
|
9689
|
-
suggestedSetupCommands: array(string())
|
|
9690
|
-
})
|
|
10022
|
+
detected: ProjectDetection
|
|
9691
10023
|
});
|
|
9692
10024
|
/** `PATCH /api/projects/:id`. */
|
|
9693
10025
|
const UpdateProjectRequest = object({
|
|
@@ -9695,9 +10027,17 @@ const UpdateProjectRequest = object({
|
|
|
9695
10027
|
defaultProvider: ProviderId.nullable().optional(),
|
|
9696
10028
|
autonomy: AutonomyLevel.nullable().optional(),
|
|
9697
10029
|
timezone: string().nullable().optional(),
|
|
9698
|
-
defaultBranch: string().nullable().optional()
|
|
10030
|
+
defaultBranch: string().nullable().optional(),
|
|
10031
|
+
/** Pin or unpin the project in the project list (§13). */
|
|
10032
|
+
pinned: boolean().optional()
|
|
9699
10033
|
});
|
|
9700
10034
|
/**
|
|
10035
|
+
* `PUT /api/projects/order`: the new manual order of the project list (§13). Lists every
|
|
10036
|
+
* non-archived project exactly once; the server rejects anything else (`invalid_order`).
|
|
10037
|
+
*/
|
|
10038
|
+
const ReorderProjectsRequest = object({ projectIds: array(Id).max(1e3) });
|
|
10039
|
+
object({ projects: array(Project) });
|
|
10040
|
+
/**
|
|
9701
10041
|
* Where a session runs (§9): a fresh worktree (default for agents), the repo root, or an
|
|
9702
10042
|
* existing directory (`path`).
|
|
9703
10043
|
*/
|
|
@@ -10278,6 +10618,97 @@ object({
|
|
|
10278
10618
|
/** The session start when accepted (possibly `conflict`); null when declined. */
|
|
10279
10619
|
start: StartSessionResponse.nullable()
|
|
10280
10620
|
});
|
|
10621
|
+
// ../../packages/contracts/src/automations-sharing.ts
|
|
10622
|
+
/**
|
|
10623
|
+
* Sharing automations (§10): duplicate, export as the Markdown + YAML frontmatter file format
|
|
10624
|
+
* (one `.md`, or all of a project's as a `.zip`), and import those files back. Imports are
|
|
10625
|
+
* parsed with the same parser and validation as automation files, never run anything and
|
|
10626
|
+
* always start off.
|
|
10627
|
+
*/
|
|
10628
|
+
/** Size limits for an import, checked by the server before anything is parsed. */
|
|
10629
|
+
const AUTOMATION_IMPORT_LIMITS = {
|
|
10630
|
+
/** Automation files in one import (Markdown files, zip entries or pasted text). */
|
|
10631
|
+
maxFiles: 50,
|
|
10632
|
+
/** One file's text, in UTF-16 code units (about bytes for ASCII). */
|
|
10633
|
+
maxFileChars: 131072,
|
|
10634
|
+
/** All files together, after unzipping. */
|
|
10635
|
+
maxTotalChars: 1048576,
|
|
10636
|
+
/** A zip as uploaded, before base64 (base64 makes it about a third bigger). */
|
|
10637
|
+
maxZipBytes: 1048576,
|
|
10638
|
+
/** Entries a zip may list, `.md` or not (more is refused before inflating anything). */
|
|
10639
|
+
maxZipEntries: 200
|
|
10640
|
+
};
|
|
10641
|
+
/** What to import: Markdown files, a zip of them (base64) and/or pasted Markdown. */
|
|
10642
|
+
const AutomationImportSource = object({
|
|
10643
|
+
files: array(object({
|
|
10644
|
+
/** File name, e.g. `release-notes.md`; its stem becomes the default work key. */
|
|
10645
|
+
name: string().trim().min(1).max(255),
|
|
10646
|
+
content: string().max(AUTOMATION_IMPORT_LIMITS.maxFileChars)
|
|
10647
|
+
})).max(AUTOMATION_IMPORT_LIMITS.maxFiles).optional(),
|
|
10648
|
+
/** A `.zip` of `.md` files, base64-encoded. Other entries are ignored. */
|
|
10649
|
+
zipBase64: string().max(Math.ceil(AUTOMATION_IMPORT_LIMITS.maxZipBytes / 3) * 4 + 4).optional(),
|
|
10650
|
+
/** One automation pasted as text. */
|
|
10651
|
+
text: string().max(AUTOMATION_IMPORT_LIMITS.maxFileChars).optional()
|
|
10652
|
+
}).refine((s) => Boolean(s.files?.length || s.zipBase64 || s.text?.trim()), { message: "Pass files, a zip or pasted text" });
|
|
10653
|
+
/** The definition an import would create (no id, target or switch yet). */
|
|
10654
|
+
const AutomationDraft = object({
|
|
10655
|
+
name: string().min(1),
|
|
10656
|
+
description: string().nullable(),
|
|
10657
|
+
trigger: AutomationTrigger,
|
|
10658
|
+
action: AutomationAction,
|
|
10659
|
+
guards: AutomationGuards,
|
|
10660
|
+
workKey: string().min(1).max(200)
|
|
10661
|
+
});
|
|
10662
|
+
object({ items: array(object({
|
|
10663
|
+
/** File name, unique within the import (duplicates get ` (2)`). */
|
|
10664
|
+
fileName: string(),
|
|
10665
|
+
/** Null when the file is invalid. */
|
|
10666
|
+
automation: AutomationDraft.nullable(),
|
|
10667
|
+
/** Validation error (bad YAML, unknown fields, invalid cron, ...); null when importable. */
|
|
10668
|
+
error: string().nullable(),
|
|
10669
|
+
/** Runs with Full permissions: it never asks before acting. */
|
|
10670
|
+
fullPermissions: boolean()
|
|
10671
|
+
})) });
|
|
10672
|
+
/**
|
|
10673
|
+
* `POST /api/automations/import`: re-parse the same source and create the selected files as
|
|
10674
|
+
* database automations, always off. `projectId` targets one project; omitted, all projects.
|
|
10675
|
+
*/
|
|
10676
|
+
const ImportAutomationsRequest = object({
|
|
10677
|
+
source: AutomationImportSource,
|
|
10678
|
+
projectId: Id.optional(),
|
|
10679
|
+
/** File names (from the preview) to import; omitted, every valid file. */
|
|
10680
|
+
select: array(string()).max(AUTOMATION_IMPORT_LIMITS.maxFiles).optional()
|
|
10681
|
+
});
|
|
10682
|
+
object({
|
|
10683
|
+
/** The new automations, all off. */
|
|
10684
|
+
created: array(AutomationListItem),
|
|
10685
|
+
/** Selected files that could not be imported, with the reason. */
|
|
10686
|
+
failed: array(object({
|
|
10687
|
+
fileName: string(),
|
|
10688
|
+
error: string()
|
|
10689
|
+
}))
|
|
10690
|
+
});
|
|
10691
|
+
object({
|
|
10692
|
+
/** `<slug>.md`. */
|
|
10693
|
+
fileName: string().min(1),
|
|
10694
|
+
markdown: string()
|
|
10695
|
+
});
|
|
10696
|
+
/** `GET /api/automations/export?projectId=`: the query; without a project, every automation. */
|
|
10697
|
+
const ExportAutomationsQuery = object({ projectId: Id.optional() });
|
|
10698
|
+
object({
|
|
10699
|
+
fileName: string().min(1),
|
|
10700
|
+
/** The zip, base64-encoded. */
|
|
10701
|
+
zipBase64: string(),
|
|
10702
|
+
count: int().nonnegative()
|
|
10703
|
+
});
|
|
10704
|
+
/**
|
|
10705
|
+
* `POST /api/automations/:id/duplicate`: an editable copy (also of file-based and built-in
|
|
10706
|
+
* automations), off. `projectId` targets one project; omitted, the original's target.
|
|
10707
|
+
*/
|
|
10708
|
+
const DuplicateAutomationRequest = object({
|
|
10709
|
+
projectId: Id.optional(),
|
|
10710
|
+
name: string().trim().min(1).max(200).optional()
|
|
10711
|
+
});
|
|
10281
10712
|
// ../../packages/contracts/src/browser-api.ts
|
|
10282
10713
|
/**
|
|
10283
10714
|
* Browser panel HTTP and WebSocket shapes (§12.2). Routes live under
|
|
@@ -10392,6 +10823,125 @@ discriminatedUnion("type", [
|
|
|
10392
10823
|
message: string()
|
|
10393
10824
|
})
|
|
10394
10825
|
]);
|
|
10826
|
+
object({
|
|
10827
|
+
/** Sessions with a turn starting or in progress. */
|
|
10828
|
+
running: int().nonnegative(),
|
|
10829
|
+
/** Sessions blocked on the user (an approval or a question). */
|
|
10830
|
+
waiting: int().nonnegative(),
|
|
10831
|
+
/** Open inbox items (questions, approvals, PRs to review, failures, ...). */
|
|
10832
|
+
inbox: int().nonnegative()
|
|
10833
|
+
});
|
|
10834
|
+
/** Count running and waiting sessions and open inbox items. */
|
|
10835
|
+
function summarizeActivity(sessions, inbox) {
|
|
10836
|
+
let running = 0;
|
|
10837
|
+
let waiting = 0;
|
|
10838
|
+
for (const s of sessions) if (s.status === "running" || s.status === "starting") running++;
|
|
10839
|
+
else if (s.status === "waiting") waiting++;
|
|
10840
|
+
return {
|
|
10841
|
+
running,
|
|
10842
|
+
waiting,
|
|
10843
|
+
inbox: inbox.filter((i) => i.status === "open").length
|
|
10844
|
+
};
|
|
10845
|
+
}
|
|
10846
|
+
/** The desktop updater's state, mirrored into `GET /api/updates` for the update banner. */
|
|
10847
|
+
const DesktopUpdateState = object({
|
|
10848
|
+
phase: _enum([
|
|
10849
|
+
"idle",
|
|
10850
|
+
"checking",
|
|
10851
|
+
"available",
|
|
10852
|
+
"downloading",
|
|
10853
|
+
"downloaded",
|
|
10854
|
+
"installing",
|
|
10855
|
+
"error"
|
|
10856
|
+
]),
|
|
10857
|
+
/** The running app version. */
|
|
10858
|
+
current: string(),
|
|
10859
|
+
/** The newest release found, null until a check succeeded. */
|
|
10860
|
+
latest: string().nullable(),
|
|
10861
|
+
checkedAt: Timestamp.nullable(),
|
|
10862
|
+
/** Download progress (0-100) while `downloading`. */
|
|
10863
|
+
percent: number$1().min(0).max(100).nullable(),
|
|
10864
|
+
/** Why the last check or download failed. */
|
|
10865
|
+
error: string().nullable(),
|
|
10866
|
+
/** Set when this build cannot update itself (a development run, an unsigned macOS app). */
|
|
10867
|
+
unsupported: string().nullable()
|
|
10868
|
+
});
|
|
10869
|
+
/** What the server asks the desktop updater to do (driven by the update banner and policy). */
|
|
10870
|
+
const DesktopUpdateCommand = _enum([
|
|
10871
|
+
"check",
|
|
10872
|
+
"download",
|
|
10873
|
+
"install"
|
|
10874
|
+
]);
|
|
10875
|
+
discriminatedUnion("type", [
|
|
10876
|
+
object({
|
|
10877
|
+
type: literal("ready"),
|
|
10878
|
+
url: string(),
|
|
10879
|
+
port: int().min(1).max(65535),
|
|
10880
|
+
launchUrl: string(),
|
|
10881
|
+
version: string()
|
|
10882
|
+
}),
|
|
10883
|
+
object({
|
|
10884
|
+
type: literal("failed"),
|
|
10885
|
+
message: string(),
|
|
10886
|
+
code: string().nullable()
|
|
10887
|
+
}),
|
|
10888
|
+
object({
|
|
10889
|
+
type: literal("update.command"),
|
|
10890
|
+
id: string(),
|
|
10891
|
+
command: DesktopUpdateCommand
|
|
10892
|
+
})
|
|
10893
|
+
]);
|
|
10894
|
+
discriminatedUnion("type", [
|
|
10895
|
+
object({
|
|
10896
|
+
type: literal("update.state"),
|
|
10897
|
+
state: DesktopUpdateState
|
|
10898
|
+
}),
|
|
10899
|
+
object({
|
|
10900
|
+
type: literal("update.done"),
|
|
10901
|
+
id: string(),
|
|
10902
|
+
error: string().nullable()
|
|
10903
|
+
}),
|
|
10904
|
+
object({
|
|
10905
|
+
type: literal("shutdown"),
|
|
10906
|
+
stopRunner: boolean()
|
|
10907
|
+
})
|
|
10908
|
+
]);
|
|
10909
|
+
discriminatedUnion("kind", [
|
|
10910
|
+
object({ kind: literal("home") }),
|
|
10911
|
+
object({ kind: literal("inbox") }),
|
|
10912
|
+
object({
|
|
10913
|
+
kind: literal("session"),
|
|
10914
|
+
sessionId: Id
|
|
10915
|
+
}),
|
|
10916
|
+
object({
|
|
10917
|
+
kind: literal("project"),
|
|
10918
|
+
projectId: Id
|
|
10919
|
+
}),
|
|
10920
|
+
object({
|
|
10921
|
+
kind: literal("settings"),
|
|
10922
|
+
section: string().nullable()
|
|
10923
|
+
})
|
|
10924
|
+
]);
|
|
10925
|
+
object({
|
|
10926
|
+
version: string(),
|
|
10927
|
+
platform: _enum([
|
|
10928
|
+
"win32",
|
|
10929
|
+
"darwin",
|
|
10930
|
+
"linux"
|
|
10931
|
+
]),
|
|
10932
|
+
/** True for an installed build (false when run from the repository). */
|
|
10933
|
+
packaged: boolean(),
|
|
10934
|
+
/** The app started this server (false when it attached to one started by `codeden`). */
|
|
10935
|
+
ownsServer: boolean(),
|
|
10936
|
+
/** The OS lets the app start at login (not on Linux AppImage, for example). */
|
|
10937
|
+
launchAtLoginSupported: boolean()
|
|
10938
|
+
});
|
|
10939
|
+
object({
|
|
10940
|
+
title: string().min(1).max(200),
|
|
10941
|
+
body: string().max(2e3).optional(),
|
|
10942
|
+
/** Passed back to `onNotificationClick` when the user clicks it. */
|
|
10943
|
+
tag: string().max(200).optional()
|
|
10944
|
+
});
|
|
10395
10945
|
// ../../packages/contracts/src/images-api.ts
|
|
10396
10946
|
/**
|
|
10397
10947
|
* Image generation settings screen and test button (§12.5). The `generate_image` MCP tool
|
|
@@ -10496,14 +11046,59 @@ object({
|
|
|
10496
11046
|
result: StartSessionResponse
|
|
10497
11047
|
});
|
|
10498
11048
|
/**
|
|
10499
|
-
* `PATCH /api/sessions/:id`: change the model or
|
|
10500
|
-
* (a running turn keeps its settings)
|
|
11049
|
+
* `PATCH /api/sessions/:id`: change the model, effort, autonomy or title mid-session.
|
|
11050
|
+
* Model and effort apply on the next turn (a running turn keeps its settings); `null` returns
|
|
11051
|
+
* to the role default.
|
|
10501
11052
|
*/
|
|
10502
11053
|
const UpdateSessionRequest = object({
|
|
10503
11054
|
model: string().min(1).nullable().optional(),
|
|
10504
11055
|
effort: ReasoningEffort.nullable().optional(),
|
|
11056
|
+
/**
|
|
11057
|
+
* Autonomy level (§11.2), saved on the session and pushed to the live provider session.
|
|
11058
|
+
* It applies from the next tool call when the provider reports `liveAutonomy`, otherwise
|
|
11059
|
+
* from the next turn. Approvals already waiting stay waiting.
|
|
11060
|
+
*/
|
|
11061
|
+
autonomy: AutonomyLevel.optional(),
|
|
10505
11062
|
title: string().trim().min(1).max(200).optional()
|
|
10506
|
-
}).refine((r) => r.model !== void 0 || r.effort !== void 0 || r.title !== void 0, { message: "Nothing to change" });
|
|
11063
|
+
}).refine((r) => r.model !== void 0 || r.effort !== void 0 || r.autonomy !== void 0 || r.title !== void 0, { message: "Nothing to change" });
|
|
11064
|
+
/**
|
|
11065
|
+
* `POST /api/sessions/:id/archive`: stop the session if it is running, hide it from the
|
|
11066
|
+
* sidebar and remove its worktree. The branch and checkpoints stay.
|
|
11067
|
+
*/
|
|
11068
|
+
const ArchiveSessionRequest = object({
|
|
11069
|
+
/** Remove the worktree even when it has uncommitted changes (those changes are lost). */
|
|
11070
|
+
force: boolean().default(false) });
|
|
11071
|
+
object({
|
|
11072
|
+
session: Session,
|
|
11073
|
+
worktree: object({
|
|
11074
|
+
status: _enum([
|
|
11075
|
+
"removed",
|
|
11076
|
+
"kept_changes",
|
|
11077
|
+
"none",
|
|
11078
|
+
"failed"
|
|
11079
|
+
]),
|
|
11080
|
+
message: string().nullable()
|
|
11081
|
+
})
|
|
11082
|
+
});
|
|
11083
|
+
object({
|
|
11084
|
+
/** The project after the check (unchanged unless `changed`). */
|
|
11085
|
+
project: Project,
|
|
11086
|
+
outcome: _enum([
|
|
11087
|
+
"unchanged",
|
|
11088
|
+
"git_detected",
|
|
11089
|
+
"github_connected",
|
|
11090
|
+
"github_changed",
|
|
11091
|
+
"github_remote_missing",
|
|
11092
|
+
"folder_missing"
|
|
11093
|
+
]),
|
|
11094
|
+
/** True when the check updated the project (a `project.updated` event was appended). */
|
|
11095
|
+
changed: boolean(),
|
|
11096
|
+
/** One line for the UI, e.g. "Connected to octo/app" or "Still no GitHub remote on origin". */
|
|
11097
|
+
message: string(),
|
|
11098
|
+
/** What was found in the folder; null when the folder is missing. */
|
|
11099
|
+
detected: ProjectDetection.nullable()
|
|
11100
|
+
});
|
|
11101
|
+
object({ project: Project });
|
|
10507
11102
|
// ../../packages/contracts/src/runner/events.ts
|
|
10508
11103
|
/**
|
|
10509
11104
|
* Durable runner -> server events. Journaled by the runner and delivered with a `seq`
|
|
@@ -10608,8 +11203,9 @@ const RunnerStream = discriminatedUnion("kind", [
|
|
|
10608
11203
|
* 4. runner replays journaled events with `seq > lastAckedSeq`, then streams live.
|
|
10609
11204
|
*
|
|
10610
11205
|
* After the handshake the server sends `command`s (answered by `result` with the same `id`)
|
|
10611
|
-
* and cumulative `ack`s; the runner sends `event`s (durable, with `seq`)
|
|
10612
|
-
* (ephemeral). Either side
|
|
11206
|
+
* and cumulative `ack`s; the runner sends `event`s (durable, with `seq`), `stream`s
|
|
11207
|
+
* (ephemeral) and `providers` (the current provider list, whenever it changes). Either side
|
|
11208
|
+
* may `ping`.
|
|
10613
11209
|
*/
|
|
10614
11210
|
/** Request id correlating a `command` with its `result`. */
|
|
10615
11211
|
const RequestId = string().min(1).max(100);
|
|
@@ -10708,6 +11304,10 @@ const RunnerToServerMessage = discriminatedUnion("type", [
|
|
|
10708
11304
|
ts: Timestamp,
|
|
10709
11305
|
stream: RunnerStream
|
|
10710
11306
|
}),
|
|
11307
|
+
object({
|
|
11308
|
+
type: literal("providers"),
|
|
11309
|
+
providers: array(ProviderInfo)
|
|
11310
|
+
}),
|
|
10711
11311
|
Ping,
|
|
10712
11312
|
Pong
|
|
10713
11313
|
]);
|
|
@@ -11027,57 +11627,11 @@ object({
|
|
|
11027
11627
|
* rest of the bundle, so the package version is the single source of truth at runtime;
|
|
11028
11628
|
* everywhere else (workspace, tests) it is `SOURCE_VERSION`.
|
|
11029
11629
|
*/
|
|
11030
|
-
const CODEDEN_VERSION = packageVersion() ?? "0.
|
|
11630
|
+
const CODEDEN_VERSION = packageVersion() ?? "0.3.0";
|
|
11031
11631
|
function packageVersion() {
|
|
11032
11632
|
const value = globalThis.__CODEDEN_PACKAGE_VERSION__;
|
|
11033
11633
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
11034
11634
|
}
|
|
11035
|
-
// ../../packages/contracts/src/voice-api.ts
|
|
11036
|
-
/**
|
|
11037
|
-
* Optional cloud voice engines (§11.3). The browser's speech APIs stay the default; these
|
|
11038
|
-
* routes proxy ElevenLabs (text to speech, Scribe) and OpenAI (transcription) with API keys
|
|
11039
|
-
* from the OS keychain, so keys never reach the browser.
|
|
11040
|
-
*/
|
|
11041
|
-
/** ElevenLabs MP3 output formats (`codec_sampleRate_bitrate`). */
|
|
11042
|
-
const TtsOutputFormat = _enum([
|
|
11043
|
-
"mp3_22050_32",
|
|
11044
|
-
"mp3_44100_64",
|
|
11045
|
-
"mp3_44100_128",
|
|
11046
|
-
"mp3_44100_192"
|
|
11047
|
-
]);
|
|
11048
|
-
/**
|
|
11049
|
-
* `POST /api/tts`: streams `audio/mpeg`. `voiceId` defaults to
|
|
11050
|
-
* `settings.voice.readAloud.voice`, then a stock voice; `modelId` to a low-latency model.
|
|
11051
|
-
*/
|
|
11052
|
-
const TtsRequest = object({
|
|
11053
|
-
text: string().min(1).max(5e3),
|
|
11054
|
-
voiceId: string().min(1).max(100).optional(),
|
|
11055
|
-
modelId: string().min(1).max(100).optional(),
|
|
11056
|
-
outputFormat: TtsOutputFormat.optional()
|
|
11057
|
-
});
|
|
11058
|
-
object({ voices: array(object({
|
|
11059
|
-
id: string(),
|
|
11060
|
-
name: string(),
|
|
11061
|
-
category: string().optional(),
|
|
11062
|
-
description: string().optional(),
|
|
11063
|
-
previewUrl: string().optional(),
|
|
11064
|
-
labels: record(string(), string()).optional()
|
|
11065
|
-
})) });
|
|
11066
|
-
/**
|
|
11067
|
-
* `POST /api/stt?engine=&language=`: the body is the recorded audio (raw bytes with an
|
|
11068
|
-
* `audio/*` content type, or multipart with a `file` field), up to 25 MB. `engine` defaults
|
|
11069
|
-
* to `settings.voice.dictation.engine` (when that is `browser`, ElevenLabs).
|
|
11070
|
-
*/
|
|
11071
|
-
const SttQuery = object({
|
|
11072
|
-
engine: _enum(["elevenlabs", "openai"]).optional(),
|
|
11073
|
-
/** ISO 639-1 hint, e.g. `en`. */
|
|
11074
|
-
language: string().min(2).max(10).optional()
|
|
11075
|
-
});
|
|
11076
|
-
object({
|
|
11077
|
-
text: string(),
|
|
11078
|
-
language: string().nullable(),
|
|
11079
|
-
engine: _enum(["elevenlabs", "openai"])
|
|
11080
|
-
});
|
|
11081
11635
|
// ../../node_modules/.pnpm/ws@8.22.1/node_modules/ws/lib/constants.js
|
|
11082
11636
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
11083
11637
|
const BINARY_TYPES = [
|
|
@@ -14828,6 +15382,43 @@ var AsyncQueue = class {
|
|
|
14828
15382
|
};
|
|
14829
15383
|
}
|
|
14830
15384
|
};
|
|
15385
|
+
// ../../packages/providers/src/models.ts
|
|
15386
|
+
function copy(m) {
|
|
15387
|
+
return {
|
|
15388
|
+
...m,
|
|
15389
|
+
efforts: [...m.efforts]
|
|
15390
|
+
};
|
|
15391
|
+
}
|
|
15392
|
+
/**
|
|
15393
|
+
* An adapter's current model list. It starts from a fallback and is replaced by what the
|
|
15394
|
+
* provider reports (model discovery, a running session). Listeners hear about real changes
|
|
15395
|
+
* only, so the runner can forward them to the server (§4.3).
|
|
15396
|
+
*/
|
|
15397
|
+
var ModelCatalog = class {
|
|
15398
|
+
models;
|
|
15399
|
+
listeners = /* @__PURE__ */ new Set();
|
|
15400
|
+
constructor(fallback) {
|
|
15401
|
+
this.models = fallback.map(copy);
|
|
15402
|
+
}
|
|
15403
|
+
/** A copy of the current list. */
|
|
15404
|
+
list() {
|
|
15405
|
+
return this.models.map(copy);
|
|
15406
|
+
}
|
|
15407
|
+
/** Replace the list. Returns false (and notifies nobody) when nothing changed. */
|
|
15408
|
+
set(models) {
|
|
15409
|
+
if (JSON.stringify(models) === JSON.stringify(this.models)) return false;
|
|
15410
|
+
this.models = models.map(copy);
|
|
15411
|
+
for (const listener of this.listeners) try {
|
|
15412
|
+
listener();
|
|
15413
|
+
} catch {}
|
|
15414
|
+
return true;
|
|
15415
|
+
}
|
|
15416
|
+
/** Call `listener` after every change. Returns the unsubscribe function. */
|
|
15417
|
+
subscribe(listener) {
|
|
15418
|
+
this.listeners.add(listener);
|
|
15419
|
+
return () => this.listeners.delete(listener);
|
|
15420
|
+
}
|
|
15421
|
+
};
|
|
14831
15422
|
// ../../packages/providers/src/util/exec.ts
|
|
14832
15423
|
function isFile(candidate) {
|
|
14833
15424
|
try {
|
|
@@ -14899,11 +15490,51 @@ function scriptFromCmdShim(shimPath, content) {
|
|
|
14899
15490
|
if (content === void 0 && !existsSync(script)) return void 0;
|
|
14900
15491
|
return script;
|
|
14901
15492
|
}
|
|
15493
|
+
/** True inside the desktop app, where `process.execPath` is the Electron binary (§17). */
|
|
15494
|
+
function isElectronRuntime(versions = process.versions) {
|
|
15495
|
+
return typeof versions.electron === "string";
|
|
15496
|
+
}
|
|
15497
|
+
/**
|
|
15498
|
+
* Environment for running one of CodeDen's own scripts (the runner, npm, Playwright's CLI)
|
|
15499
|
+
* with `process.execPath`. In the desktop app that is the Electron binary, which only acts
|
|
15500
|
+
* as Node.js with `ELECTRON_RUN_AS_NODE=1`; elsewhere the environment is returned unchanged.
|
|
15501
|
+
*/
|
|
15502
|
+
function ownNodeEnv(env = process.env, versions = process.versions) {
|
|
15503
|
+
return isElectronRuntime(versions) ? {
|
|
15504
|
+
...env,
|
|
15505
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
15506
|
+
} : env;
|
|
15507
|
+
}
|
|
15508
|
+
let systemNode;
|
|
15509
|
+
/**
|
|
15510
|
+
* The Node.js that runs `node-script` commands (npm-style shims of the user's own tools, such
|
|
15511
|
+
* as a globally installed Codex on Windows). Normally the Node running CodeDen. In the desktop
|
|
15512
|
+
* app the user's `node` from PATH is preferred, so their tools run exactly as in a terminal
|
|
15513
|
+
* and `ELECTRON_RUN_AS_NODE` never leaks into agent processes; without one, the Electron
|
|
15514
|
+
* binary runs them as Node.js.
|
|
15515
|
+
*/
|
|
15516
|
+
function scriptRuntime() {
|
|
15517
|
+
if (!isElectronRuntime()) return {
|
|
15518
|
+
path: process.execPath,
|
|
15519
|
+
electron: false
|
|
15520
|
+
};
|
|
15521
|
+
if (systemNode === void 0) {
|
|
15522
|
+
const found = resolveExecutable("node");
|
|
15523
|
+
systemNode = found?.kind === "native" ? found.path : null;
|
|
15524
|
+
}
|
|
15525
|
+
return systemNode ? {
|
|
15526
|
+
path: systemNode,
|
|
15527
|
+
electron: false
|
|
15528
|
+
} : {
|
|
15529
|
+
path: process.execPath,
|
|
15530
|
+
electron: true
|
|
15531
|
+
};
|
|
15532
|
+
}
|
|
14902
15533
|
/** The `[command, args]` to pass to `spawn` for a resolved executable (never needs `shell`). */
|
|
14903
15534
|
function spawnArgs(cmd, args) {
|
|
14904
15535
|
switch (cmd.kind) {
|
|
14905
15536
|
case "native": return [cmd.path, [...args]];
|
|
14906
|
-
case "node-script": return [
|
|
15537
|
+
case "node-script": return [scriptRuntime().path, [cmd.script, ...args]];
|
|
14907
15538
|
case "shell-script": return [process.env.ComSpec ?? "cmd.exe", [
|
|
14908
15539
|
"/d",
|
|
14909
15540
|
"/s",
|
|
@@ -14920,9 +15551,11 @@ function quoteCmdArg(arg) {
|
|
|
14920
15551
|
/** Spawn a resolved executable. Uses `windowsVerbatimArguments` for the cmd.exe fallback. */
|
|
14921
15552
|
function spawnResolved(cmd, args, options = {}) {
|
|
14922
15553
|
const [file, argv] = spawnArgs(cmd, args);
|
|
15554
|
+
const asNode = cmd.kind === "node-script" && scriptRuntime().electron;
|
|
14923
15555
|
return spawn(file, argv, {
|
|
14924
15556
|
windowsHide: true,
|
|
14925
15557
|
...options,
|
|
15558
|
+
...asNode ? { env: ownNodeEnv(options.env ?? process.env) } : {},
|
|
14926
15559
|
...cmd.kind === "shell-script" ? { windowsVerbatimArguments: true } : {}
|
|
14927
15560
|
});
|
|
14928
15561
|
}
|
|
@@ -15135,6 +15768,9 @@ function parseClaudeAuthStatus(stdout) {
|
|
|
15135
15768
|
return unknown;
|
|
15136
15769
|
}
|
|
15137
15770
|
}
|
|
15771
|
+
// ../../packages/providers/src/util/model.ts
|
|
15772
|
+
/** The id the model pickers use for "the provider's own default model". */
|
|
15773
|
+
const DEFAULT_MODEL_ID = "default";
|
|
15138
15774
|
/**
|
|
15139
15775
|
* The model to pass to a provider, or undefined to leave it out so the provider uses its own
|
|
15140
15776
|
* default (the user's config). `"default"` and empty values are never sent as model ids.
|
|
@@ -15143,6 +15779,213 @@ function explicitModel(model) {
|
|
|
15143
15779
|
const id = model?.trim();
|
|
15144
15780
|
return id && id !== "default" ? id : void 0;
|
|
15145
15781
|
}
|
|
15782
|
+
// ../../packages/providers/src/policy/read-only.ts
|
|
15783
|
+
/**
|
|
15784
|
+
* Read-only sessions (the orchestrator and PMs, and agent sessions with the `read_only`
|
|
15785
|
+
* permission level, §11.2) may inspect a repository but never change it. This allowlist decides
|
|
15786
|
+
* which shell commands they may run: commands that only read, with no output redirection and no
|
|
15787
|
+
* command substitution. Agent sessions may also run the project's checks (tests, lint, type
|
|
15788
|
+
* checks) without fix or update flags. Anything else is denied.
|
|
15789
|
+
*/
|
|
15790
|
+
/**
|
|
15791
|
+
* Split on every `;`, `&&`, `||`, `|` and newline, even inside quotes. Deliberately cruder
|
|
15792
|
+
* than the risk classifier's lexer: for an allowlist, over-splitting only denies more.
|
|
15793
|
+
*/
|
|
15794
|
+
function splitCommands(command) {
|
|
15795
|
+
return command.split(/\r?\n|;|&&|\|\||\|/).map((s) => s.trim()).filter(Boolean);
|
|
15796
|
+
}
|
|
15797
|
+
/** Tokenize one simple command, honouring single and double quotes. */
|
|
15798
|
+
function tokenize(command) {
|
|
15799
|
+
const tokens = [];
|
|
15800
|
+
for (const m of command.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g)) tokens.push(m[1] ?? m[2] ?? m[3] ?? "");
|
|
15801
|
+
return tokens;
|
|
15802
|
+
}
|
|
15803
|
+
/** Programs that only read (or print). */
|
|
15804
|
+
const READ_ONLY_BINS = /* @__PURE__ */ new Set([
|
|
15805
|
+
"ls",
|
|
15806
|
+
"dir",
|
|
15807
|
+
"cat",
|
|
15808
|
+
"type",
|
|
15809
|
+
"head",
|
|
15810
|
+
"tail",
|
|
15811
|
+
"less",
|
|
15812
|
+
"more",
|
|
15813
|
+
"wc",
|
|
15814
|
+
"grep",
|
|
15815
|
+
"egrep",
|
|
15816
|
+
"fgrep",
|
|
15817
|
+
"rg",
|
|
15818
|
+
"find",
|
|
15819
|
+
"fd",
|
|
15820
|
+
"tree",
|
|
15821
|
+
"pwd",
|
|
15822
|
+
"echo",
|
|
15823
|
+
"printf",
|
|
15824
|
+
"which",
|
|
15825
|
+
"where",
|
|
15826
|
+
"whoami",
|
|
15827
|
+
"date",
|
|
15828
|
+
"stat",
|
|
15829
|
+
"file",
|
|
15830
|
+
"du",
|
|
15831
|
+
"df",
|
|
15832
|
+
"sort",
|
|
15833
|
+
"uniq",
|
|
15834
|
+
"cut",
|
|
15835
|
+
"diff",
|
|
15836
|
+
"jq",
|
|
15837
|
+
"basename",
|
|
15838
|
+
"dirname",
|
|
15839
|
+
"realpath",
|
|
15840
|
+
"true",
|
|
15841
|
+
"get-childitem",
|
|
15842
|
+
"gci",
|
|
15843
|
+
"get-content",
|
|
15844
|
+
"gc",
|
|
15845
|
+
"get-item",
|
|
15846
|
+
"gi",
|
|
15847
|
+
"get-location",
|
|
15848
|
+
"gl",
|
|
15849
|
+
"select-string",
|
|
15850
|
+
"sls",
|
|
15851
|
+
"measure-object",
|
|
15852
|
+
"test-path",
|
|
15853
|
+
"resolve-path",
|
|
15854
|
+
"write-output",
|
|
15855
|
+
"write-host",
|
|
15856
|
+
"select-object",
|
|
15857
|
+
"where-object",
|
|
15858
|
+
"format-list",
|
|
15859
|
+
"format-table",
|
|
15860
|
+
"out-string",
|
|
15861
|
+
"sort-object",
|
|
15862
|
+
"gh"
|
|
15863
|
+
]);
|
|
15864
|
+
/** Git subcommands that never change the repository. */
|
|
15865
|
+
const READ_ONLY_GIT = /* @__PURE__ */ new Set([
|
|
15866
|
+
"status",
|
|
15867
|
+
"log",
|
|
15868
|
+
"show",
|
|
15869
|
+
"diff",
|
|
15870
|
+
"blame",
|
|
15871
|
+
"grep",
|
|
15872
|
+
"ls-files",
|
|
15873
|
+
"ls-tree",
|
|
15874
|
+
"rev-parse",
|
|
15875
|
+
"rev-list",
|
|
15876
|
+
"describe",
|
|
15877
|
+
"shortlog",
|
|
15878
|
+
"cat-file",
|
|
15879
|
+
"remote",
|
|
15880
|
+
"branch",
|
|
15881
|
+
"tag",
|
|
15882
|
+
"config",
|
|
15883
|
+
"reflog",
|
|
15884
|
+
"merge-base",
|
|
15885
|
+
"name-rev",
|
|
15886
|
+
"for-each-ref",
|
|
15887
|
+
"worktree"
|
|
15888
|
+
]);
|
|
15889
|
+
/** `gh` subcommand verbs that only read. */
|
|
15890
|
+
const READ_ONLY_GH = /* @__PURE__ */ new Set([
|
|
15891
|
+
"view",
|
|
15892
|
+
"list",
|
|
15893
|
+
"status",
|
|
15894
|
+
"diff",
|
|
15895
|
+
"checks"
|
|
15896
|
+
]);
|
|
15897
|
+
const bin = (token) => (token ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase().replace(/\.(exe|cmd|bat|ps1)$/, "") ?? "";
|
|
15898
|
+
/** Package managers whose `test`, `lint`, ... scripts count as checks. */
|
|
15899
|
+
const PACKAGE_MANAGERS = /* @__PURE__ */ new Set([
|
|
15900
|
+
"npm",
|
|
15901
|
+
"pnpm",
|
|
15902
|
+
"yarn",
|
|
15903
|
+
"bun"
|
|
15904
|
+
]);
|
|
15905
|
+
/** Script names a package manager may run as a check (`pnpm test`, `npm run lint`). */
|
|
15906
|
+
const CHECK_SCRIPTS = /* @__PURE__ */ new Set([
|
|
15907
|
+
"test",
|
|
15908
|
+
"tests",
|
|
15909
|
+
"lint",
|
|
15910
|
+
"typecheck",
|
|
15911
|
+
"type-check",
|
|
15912
|
+
"check",
|
|
15913
|
+
"types",
|
|
15914
|
+
"vitest",
|
|
15915
|
+
"jest",
|
|
15916
|
+
"biome",
|
|
15917
|
+
"eslint",
|
|
15918
|
+
"tsc"
|
|
15919
|
+
]);
|
|
15920
|
+
/** Programs (null: any arguments) and `<program> <subcommand>` pairs that run checks. */
|
|
15921
|
+
const CHECK_COMMANDS = {
|
|
15922
|
+
tsc: null,
|
|
15923
|
+
vitest: null,
|
|
15924
|
+
jest: null,
|
|
15925
|
+
eslint: null,
|
|
15926
|
+
pytest: null,
|
|
15927
|
+
mypy: null,
|
|
15928
|
+
pyright: null,
|
|
15929
|
+
biome: [
|
|
15930
|
+
"check",
|
|
15931
|
+
"lint",
|
|
15932
|
+
"ci"
|
|
15933
|
+
],
|
|
15934
|
+
ruff: ["check"],
|
|
15935
|
+
cargo: [
|
|
15936
|
+
"test",
|
|
15937
|
+
"check",
|
|
15938
|
+
"clippy"
|
|
15939
|
+
],
|
|
15940
|
+
go: ["test", "vet"],
|
|
15941
|
+
dotnet: ["test"],
|
|
15942
|
+
mvn: ["test", "verify"],
|
|
15943
|
+
gradle: ["test", "check"]
|
|
15944
|
+
};
|
|
15945
|
+
/** Flags that turn a check into a write (fixing, formatting, updating snapshots, watching). */
|
|
15946
|
+
const WRITING_FLAG = /^(--fix|--fix-type=.*|--write|--apply|--apply-unsafe|--unsafe|-u|--update|--update-snapshots?|--updatesnapshot|--watch|-w)(=.*)?$/;
|
|
15947
|
+
/** True when `name args` runs a known check without writing flags. */
|
|
15948
|
+
function isCheck(name, args) {
|
|
15949
|
+
if (args.some((a) => WRITING_FLAG.test(a))) return false;
|
|
15950
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
15951
|
+
if (PACKAGE_MANAGERS.has(name)) {
|
|
15952
|
+
const script = positional[0] === "run" ? positional[1] : positional[0];
|
|
15953
|
+
return script !== void 0 && CHECK_SCRIPTS.has(script);
|
|
15954
|
+
}
|
|
15955
|
+
if (name === "npx" || name === "pnpx" || name === "bunx") {
|
|
15956
|
+
const tool = positional[0];
|
|
15957
|
+
return tool !== void 0 && isCheck(bin(tool), args.slice(args.indexOf(tool) + 1));
|
|
15958
|
+
}
|
|
15959
|
+
if (name === "python" || name === "python3" || name === "py") return args[0] === "-m" && (args[1] === "pytest" || args[1] === "mypy");
|
|
15960
|
+
if (!Object.hasOwn(CHECK_COMMANDS, name)) return false;
|
|
15961
|
+
const subs = CHECK_COMMANDS[name];
|
|
15962
|
+
if (!subs) return true;
|
|
15963
|
+
return positional[0] !== void 0 && subs.includes(positional[0]);
|
|
15964
|
+
}
|
|
15965
|
+
/** Why a command is refused in a read-only session, or undefined when it may run. */
|
|
15966
|
+
function readOnlyCommandDenial(command, options = {}) {
|
|
15967
|
+
if (/\$\(|`|<\(|>\(/.test(command)) return "Command substitution is not allowed in a read-only session";
|
|
15968
|
+
for (const simple of splitCommands(command)) {
|
|
15969
|
+
if (/(^|[^0-9&<])>>?(?!\s*(&\d|\/dev\/null|nul\b|\$null))/i.test(simple)) return "Writing files is not allowed in a read-only session";
|
|
15970
|
+
const tokens = tokenize(simple);
|
|
15971
|
+
const name = bin(tokens[0]);
|
|
15972
|
+
const args = tokens.slice(1).map((a) => a.toLowerCase());
|
|
15973
|
+
if (name === "git") {
|
|
15974
|
+
const sub = args.find((a) => !a.startsWith("-"));
|
|
15975
|
+
if (!sub || !READ_ONLY_GIT.has(sub)) return `git ${sub ?? ""} may change the repository`;
|
|
15976
|
+
if ((sub === "branch" || sub === "tag" || sub === "remote" || sub === "worktree") && args.some((a) => /^-(d|D|m|M|f|-delete|-move|-force)$/.test(a) || a === "add")) return `git ${sub} with these options changes the repository`;
|
|
15977
|
+
if (sub === "config" && !args.some((a) => a === "--get" || a === "--list" || a === "-l")) return "Only reading git config is allowed";
|
|
15978
|
+
continue;
|
|
15979
|
+
}
|
|
15980
|
+
if (name === "gh") {
|
|
15981
|
+
if (!args.some((a) => READ_ONLY_GH.has(a)) || args.includes("api")) return "Only read-only gh commands are allowed (view, list, status, diff, checks)";
|
|
15982
|
+
continue;
|
|
15983
|
+
}
|
|
15984
|
+
if (name === "find" && args.some((a) => a === "-delete" || a === "-exec" || a === "-execdir")) return "find with -delete or -exec is not allowed in a read-only session";
|
|
15985
|
+
if (options.allowChecks && isCheck(name, args)) continue;
|
|
15986
|
+
if (!READ_ONLY_BINS.has(name)) return `${name || "This command"} is not on the read-only allowlist`;
|
|
15987
|
+
}
|
|
15988
|
+
}
|
|
15146
15989
|
// ../../packages/providers/src/util/text.ts
|
|
15147
15990
|
/** Max characters of tool output kept inline in an item (§6: larger outputs are truncated). */
|
|
15148
15991
|
const TOOL_OUTPUT_PREVIEW_CHARS = 16e3;
|
|
@@ -15197,50 +16040,110 @@ function unixToIso(value) {
|
|
|
15197
16040
|
const ms = value < 0xe8d4a51000 ? value * 1e3 : value;
|
|
15198
16041
|
return new Date(ms).toISOString();
|
|
15199
16042
|
}
|
|
15200
|
-
// ../../packages/providers/src/utility.ts
|
|
15201
|
-
/** Abort signal that fires on the caller's signal or after `timeoutMs`. */
|
|
15202
|
-
function oneShotSignal(request) {
|
|
15203
|
-
const controller = new AbortController();
|
|
15204
|
-
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Utility model timed out")), request.timeoutMs ?? 6e4);
|
|
15205
|
-
const onAbort = () => controller.abort(request.signal?.reason);
|
|
15206
|
-
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
15207
|
-
return {
|
|
15208
|
-
signal: controller.signal,
|
|
15209
|
-
dispose: () => {
|
|
15210
|
-
clearTimeout(timer);
|
|
15211
|
-
request.signal?.removeEventListener("abort", onAbort);
|
|
15212
|
-
}
|
|
15213
|
-
};
|
|
15214
|
-
}
|
|
15215
16043
|
// ../../packages/providers/src/policy/paths.ts
|
|
16044
|
+
const isWin = (ctx) => (ctx.platform ?? process.platform) === "win32";
|
|
15216
16045
|
function pathApi(ctx) {
|
|
15217
|
-
return (ctx
|
|
16046
|
+
return isWin(ctx) ? path.win32 : path.posix;
|
|
16047
|
+
}
|
|
16048
|
+
function homeOf(ctx) {
|
|
16049
|
+
return ctx.home ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
16050
|
+
}
|
|
16051
|
+
function tempRootsOf(ctx) {
|
|
16052
|
+
return ctx.tempRoots ?? osTempRoots(ctx.platform);
|
|
16053
|
+
}
|
|
16054
|
+
const HOME_VARS = /^(\$HOME|\$\{HOME\}|\$env:USERPROFILE|%USERPROFILE%)(?=$|[\\/])/i;
|
|
16055
|
+
const TEMP_VARS = /^(\$TMPDIR|\$\{TMPDIR\}|\$TEMP|\$TMP|\$env:TEMP|\$env:TMP|%TEMP%|%TMP%)(?=$|[\\/])/i;
|
|
16056
|
+
/**
|
|
16057
|
+
* Git Bash paths on Windows: `/c/Users/x` is `C:\Users\x`, and `/tmp` is the user's temp
|
|
16058
|
+
* directory.
|
|
16059
|
+
*/
|
|
16060
|
+
function fromGitBash(value, temp) {
|
|
16061
|
+
if (temp && /^\/tmp(?=$|\/)/.test(value)) return temp + value.slice(4);
|
|
16062
|
+
const drive = /^\/([a-z])(?=$|\/)/i.exec(value);
|
|
16063
|
+
if (drive?.[1]) return `${drive[1].toUpperCase()}:\\${value.slice(3)}`;
|
|
16064
|
+
return value;
|
|
15218
16065
|
}
|
|
15219
|
-
/**
|
|
16066
|
+
/**
|
|
16067
|
+
* Resolve `target` (absolute, relative to `dir`, `~`-prefixed, or starting with a home or
|
|
16068
|
+
* temp variable) to an absolute path.
|
|
16069
|
+
*/
|
|
15220
16070
|
function resolveAgainst(target, ctx) {
|
|
15221
16071
|
const p = pathApi(ctx);
|
|
15222
16072
|
let value = target.trim().replace(/^["']|["']$/g, "");
|
|
15223
|
-
const home = ctx
|
|
16073
|
+
const home = homeOf(ctx);
|
|
15224
16074
|
if (value === "~" || value.startsWith("~/") || value.startsWith("~\\")) value = home + value.slice(1);
|
|
15225
|
-
value = value.replace(
|
|
15226
|
-
|
|
16075
|
+
value = value.replace(HOME_VARS, home);
|
|
16076
|
+
const temp = tempRootsOf(ctx)[0];
|
|
16077
|
+
if (temp) value = value.replace(TEMP_VARS, temp);
|
|
16078
|
+
if (isWin(ctx)) value = fromGitBash(value, temp);
|
|
16079
|
+
return p.resolve(ctx.dir ?? ctx.cwd, value);
|
|
15227
16080
|
}
|
|
15228
16081
|
/** True when `target` resolves inside `root` (or equals it). Case-insensitive on Windows. */
|
|
15229
16082
|
function isInside$1(target, root, ctx) {
|
|
16083
|
+
const rel = relative$1(root, target, ctx);
|
|
16084
|
+
return rel === "" || !rel.startsWith("..") && !pathApi(ctx).isAbsolute(rel);
|
|
16085
|
+
}
|
|
16086
|
+
function relative$1(root, target, ctx) {
|
|
15230
16087
|
const p = pathApi(ctx);
|
|
15231
|
-
const
|
|
15232
|
-
|
|
15233
|
-
const r = p.resolve(s);
|
|
15234
|
-
return win ? r.toLowerCase() : r;
|
|
15235
|
-
};
|
|
15236
|
-
const rel = p.relative(norm(root), norm(target));
|
|
15237
|
-
return rel === "" || !rel.startsWith("..") && !p.isAbsolute(rel);
|
|
16088
|
+
const norm = (s) => isWin(ctx) ? p.resolve(s).toLowerCase() : p.resolve(s);
|
|
16089
|
+
return p.relative(norm(root), norm(target));
|
|
15238
16090
|
}
|
|
15239
16091
|
/** True when `target` is inside the worktree or one of the extra roots. */
|
|
15240
16092
|
function isInWorkspace(target, ctx) {
|
|
15241
16093
|
const abs = resolveAgainst(target, ctx);
|
|
15242
16094
|
return [ctx.cwd, ...ctx.extraRoots ?? []].some((root) => isInside$1(abs, root, ctx));
|
|
15243
16095
|
}
|
|
16096
|
+
/** True when `target` is strictly inside one of the OS temp directories (never the root). */
|
|
16097
|
+
function isInTemp(target, ctx) {
|
|
16098
|
+
const abs = resolveAgainst(target, ctx);
|
|
16099
|
+
return tempRootsOf(ctx).some((root) => {
|
|
16100
|
+
return relative$1(root, abs, ctx) !== "" && isInside$1(abs, root, ctx);
|
|
16101
|
+
});
|
|
16102
|
+
}
|
|
16103
|
+
/** True when agents may write `target` without asking: the worktree, extra roots or temp. */
|
|
16104
|
+
function isWritable(target, ctx) {
|
|
16105
|
+
return isInWorkspace(target, ctx) || isInTemp(target, ctx);
|
|
16106
|
+
}
|
|
16107
|
+
/**
|
|
16108
|
+
* Usable temp roots from `candidates`: absolute, deduplicated, and never a filesystem root,
|
|
16109
|
+
* the home directory or one of its parents (a stray `TMPDIR=/` must not open everything).
|
|
16110
|
+
*/
|
|
16111
|
+
function tempRootsFrom(candidates, platform, home) {
|
|
16112
|
+
const ctx = {
|
|
16113
|
+
cwd: "/",
|
|
16114
|
+
platform
|
|
16115
|
+
};
|
|
16116
|
+
const p = pathApi(ctx);
|
|
16117
|
+
const out = [];
|
|
16118
|
+
for (const candidate of candidates) {
|
|
16119
|
+
if (!candidate || !p.isAbsolute(candidate)) continue;
|
|
16120
|
+
const root = p.resolve(candidate);
|
|
16121
|
+
if (p.parse(root).root === root || p.dirname(root) === root) continue;
|
|
16122
|
+
if (home && isInside$1(home, root, ctx)) continue;
|
|
16123
|
+
if (!out.some((r) => relative$1(r, root, ctx) === "")) out.push(root);
|
|
16124
|
+
}
|
|
16125
|
+
return out;
|
|
16126
|
+
}
|
|
16127
|
+
const realpath$1 = (p) => {
|
|
16128
|
+
try {
|
|
16129
|
+
return realpathSync.native(p);
|
|
16130
|
+
} catch {
|
|
16131
|
+
return;
|
|
16132
|
+
}
|
|
16133
|
+
};
|
|
16134
|
+
let cachedTempRoots;
|
|
16135
|
+
/**
|
|
16136
|
+
* The OS temp directories for the running platform: `os.tmpdir()`, `%TEMP%` and `%TMP%` on
|
|
16137
|
+
* Windows, `$TMPDIR` and `/tmp` elsewhere, each also by its real path (macOS symlinks
|
|
16138
|
+
* `/tmp`, Windows may report a short 8.3 name). Empty when asked about another platform.
|
|
16139
|
+
*/
|
|
16140
|
+
function osTempRoots(platform = process.platform) {
|
|
16141
|
+
if (platform !== process.platform) return [];
|
|
16142
|
+
if (cachedTempRoots) return cachedTempRoots;
|
|
16143
|
+
const env = process.env;
|
|
16144
|
+
cachedTempRoots = tempRootsFrom([os.tmpdir(), ...platform === "win32" ? [env.TEMP, env.TMP] : [env.TMPDIR, "/tmp"]].flatMap((c) => c ? [c, realpath$1(c)] : []), platform, os.homedir());
|
|
16145
|
+
return cachedTempRoots;
|
|
16146
|
+
}
|
|
15244
16147
|
const SECRET_BASENAMES = /* @__PURE__ */ new Set([
|
|
15245
16148
|
".env",
|
|
15246
16149
|
".npmrc",
|
|
@@ -15284,41 +16187,297 @@ function isSecretPath(target) {
|
|
|
15284
16187
|
if (SECRET_EXTENSIONS.some((ext) => base.endsWith(ext))) return true;
|
|
15285
16188
|
return /(^|\/)\.(ssh|aws|gnupg|docker)\//i.test(normalized) && !base.endsWith(".pub");
|
|
15286
16189
|
}
|
|
15287
|
-
// ../../packages/providers/src/policy/
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15294
|
-
/**
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
/**
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
16190
|
+
// ../../packages/providers/src/policy/shell.ts
|
|
16191
|
+
/** Parse a command line into its simple commands. */
|
|
16192
|
+
function parseShell(src, dialect = "auto") {
|
|
16193
|
+
return new Lexer(src, dialect).list(null);
|
|
16194
|
+
}
|
|
16195
|
+
/** Placeholder for heredoc and here-string bodies, which are data, never commands. */
|
|
16196
|
+
const BODY = "<body>";
|
|
16197
|
+
/** The command being built and the word being read. */
|
|
16198
|
+
var Builder = class {
|
|
16199
|
+
out;
|
|
16200
|
+
cmd = {
|
|
16201
|
+
words: [],
|
|
16202
|
+
writes: [],
|
|
16203
|
+
nested: []
|
|
16204
|
+
};
|
|
16205
|
+
word = "";
|
|
16206
|
+
inWord = false;
|
|
16207
|
+
/** Where the current word goes: an argument, a redirect target or a heredoc delimiter. */
|
|
16208
|
+
target = "word";
|
|
16209
|
+
heredocStrip = false;
|
|
16210
|
+
heredocs = [];
|
|
16211
|
+
constructor(out) {
|
|
16212
|
+
this.out = out;
|
|
16213
|
+
}
|
|
16214
|
+
add(text) {
|
|
16215
|
+
this.word += text;
|
|
16216
|
+
this.inWord = true;
|
|
16217
|
+
}
|
|
16218
|
+
endWord() {
|
|
16219
|
+
if (!this.inWord) return;
|
|
16220
|
+
if (this.target === "write") this.cmd.writes.push(this.word);
|
|
16221
|
+
else if (this.target === "heredoc") this.heredocs.push({
|
|
16222
|
+
delim: this.word.replace(/\\/g, ""),
|
|
16223
|
+
strip: this.heredocStrip
|
|
16224
|
+
});
|
|
16225
|
+
else if (this.target === "word") this.cmd.words.push(this.word);
|
|
16226
|
+
this.target = "word";
|
|
16227
|
+
this.word = "";
|
|
16228
|
+
this.inWord = false;
|
|
16229
|
+
}
|
|
16230
|
+
endCommand() {
|
|
16231
|
+
this.endWord();
|
|
16232
|
+
this.target = "word";
|
|
16233
|
+
const { cmd } = this;
|
|
16234
|
+
if (cmd.words.length || cmd.writes.length || cmd.nested.length) this.out.push(cmd);
|
|
16235
|
+
this.cmd = {
|
|
16236
|
+
words: [],
|
|
16237
|
+
writes: [],
|
|
16238
|
+
nested: []
|
|
16239
|
+
};
|
|
16240
|
+
}
|
|
16241
|
+
};
|
|
16242
|
+
var Lexer = class {
|
|
16243
|
+
src;
|
|
16244
|
+
dialect;
|
|
16245
|
+
i = 0;
|
|
16246
|
+
constructor(src, dialect) {
|
|
16247
|
+
this.src = src;
|
|
16248
|
+
this.dialect = dialect;
|
|
16249
|
+
}
|
|
16250
|
+
/** Read simple commands until `closer` (`)` or a backtick) or the end of the input. */
|
|
16251
|
+
list(closer) {
|
|
16252
|
+
const out = [];
|
|
16253
|
+
const b = new Builder(out);
|
|
16254
|
+
const { src } = this;
|
|
16255
|
+
while (this.i < src.length) {
|
|
16256
|
+
const c = src[this.i];
|
|
16257
|
+
const next = src[this.i + 1];
|
|
16258
|
+
if (closer && c === closer) {
|
|
16259
|
+
this.i += 1;
|
|
16260
|
+
b.endCommand();
|
|
16261
|
+
return out;
|
|
16262
|
+
}
|
|
16263
|
+
if (c === "\n" || c === "\r") {
|
|
16264
|
+
b.endCommand();
|
|
16265
|
+
this.i += c === "\r" && next === "\n" ? 2 : 1;
|
|
16266
|
+
this.skipHeredocs(b.heredocs.splice(0));
|
|
16267
|
+
} else if (c === " " || c === " ") {
|
|
16268
|
+
b.endWord();
|
|
16269
|
+
this.i += 1;
|
|
16270
|
+
} else if (c === "#" && !b.inWord) while (this.i < src.length && src[this.i] !== "\n" && src[this.i] !== "\r") this.i += 1;
|
|
16271
|
+
else if (c === ";") {
|
|
16272
|
+
b.endCommand();
|
|
16273
|
+
this.i += 1;
|
|
16274
|
+
} else if (c === "|") {
|
|
16275
|
+
b.endCommand();
|
|
16276
|
+
this.i += next === "|" || next === "&" ? 2 : 1;
|
|
16277
|
+
} else if (c === "&" && next === ">") {
|
|
16278
|
+
b.endWord();
|
|
16279
|
+
b.target = "write";
|
|
16280
|
+
this.i += src[this.i + 2] === ">" ? 3 : 2;
|
|
16281
|
+
} else if (c === "&") {
|
|
16282
|
+
b.endCommand();
|
|
16283
|
+
this.i += next === "&" ? 2 : 1;
|
|
16284
|
+
} else if (c === ">" || c === "<") this.redirect(b, c);
|
|
16285
|
+
else if (c === "'" || c === "\"") {
|
|
16286
|
+
if (b.inWord && b.word.endsWith("@") && this.lineEndsAt(this.i + 1)) {
|
|
16287
|
+
this.skipHereString(c);
|
|
16288
|
+
b.add(BODY);
|
|
16289
|
+
} else if (c === "'") b.add(this.readSingle());
|
|
16290
|
+
else b.add(this.readDouble(b));
|
|
16291
|
+
} else if (c === "\\" && this.dialect !== "powershell") {
|
|
16292
|
+
if (next === "\n" || next === "\r") this.i += next === "\r" && src[this.i + 2] === "\n" ? 3 : 2;
|
|
16293
|
+
else {
|
|
16294
|
+
b.add(src.slice(this.i, this.i + 2));
|
|
16295
|
+
this.i += 2;
|
|
16296
|
+
}
|
|
16297
|
+
} else if (c === "`") this.backtick(b);
|
|
16298
|
+
else if (c === "(") {
|
|
16299
|
+
const start = this.i;
|
|
16300
|
+
this.i += 1;
|
|
16301
|
+
b.cmd.nested.push(this.list(")"));
|
|
16302
|
+
b.add(src.slice(start, this.i));
|
|
16303
|
+
} else {
|
|
16304
|
+
b.add(c);
|
|
16305
|
+
this.i += 1;
|
|
16306
|
+
}
|
|
16307
|
+
}
|
|
16308
|
+
b.endCommand();
|
|
16309
|
+
return out;
|
|
16310
|
+
}
|
|
16311
|
+
/** `>`, `>>`, `>|`, `2>`, `*>`, `2>&1`, `<`, `<<EOF`, `<<-EOF`, `<<<`, `<(…)`, `>(…)`. */
|
|
16312
|
+
redirect(b, c) {
|
|
16313
|
+
const { src } = this;
|
|
16314
|
+
if (b.inWord && /^(\d+|\*)$/.test(b.word)) {
|
|
16315
|
+
b.word = "";
|
|
16316
|
+
b.inWord = false;
|
|
16317
|
+
} else b.endWord();
|
|
16318
|
+
const next = src[this.i + 1];
|
|
16319
|
+
if (next === "(") {
|
|
16320
|
+
const start = this.i;
|
|
16321
|
+
this.i += 2;
|
|
16322
|
+
b.cmd.nested.push(this.list(")"));
|
|
16323
|
+
b.add(src.slice(start, this.i));
|
|
16324
|
+
return;
|
|
16325
|
+
}
|
|
16326
|
+
if (c === "<") {
|
|
16327
|
+
if (next === "<" && src[this.i + 2] === "<") {
|
|
16328
|
+
b.target = "skip";
|
|
16329
|
+
this.i += 3;
|
|
16330
|
+
} else if (next === "<") {
|
|
16331
|
+
this.i += 2;
|
|
16332
|
+
b.heredocStrip = src[this.i] === "-";
|
|
16333
|
+
if (b.heredocStrip) this.i += 1;
|
|
16334
|
+
b.target = "heredoc";
|
|
16335
|
+
} else this.i += 1;
|
|
16336
|
+
return;
|
|
16337
|
+
}
|
|
16338
|
+
let j = this.i + 1;
|
|
16339
|
+
if (src[j] === ">") j += 1;
|
|
16340
|
+
if (src[j] === "|") j += 1;
|
|
16341
|
+
if (src[j] === "&") {
|
|
16342
|
+
const dup = /^&(\d+|-)/.exec(src.slice(j));
|
|
16343
|
+
if (dup) {
|
|
16344
|
+
this.i = j + dup[0].length;
|
|
16345
|
+
return;
|
|
16346
|
+
}
|
|
16347
|
+
j += 1;
|
|
16348
|
+
}
|
|
16349
|
+
b.target = "write";
|
|
16350
|
+
this.i = j;
|
|
16351
|
+
}
|
|
16352
|
+
/**
|
|
16353
|
+
* PowerShell escapes the next character; POSIX starts a command substitution. (A closing
|
|
16354
|
+
* backtick never gets here: `list` checks for its closer first.)
|
|
16355
|
+
*/
|
|
16356
|
+
backtick(b) {
|
|
16357
|
+
const { src } = this;
|
|
16358
|
+
const next = src[this.i + 1];
|
|
16359
|
+
if (this.dialect === "powershell" || this.dialect === "auto" && (next === void 0 || /\s/.test(next))) {
|
|
16360
|
+
if (next === "\n" || next === "\r") this.i += next === "\r" && src[this.i + 2] === "\n" ? 3 : 2;
|
|
16361
|
+
else {
|
|
16362
|
+
b.add(src.slice(this.i + 1, this.i + 2));
|
|
16363
|
+
this.i += 2;
|
|
16364
|
+
}
|
|
16365
|
+
return;
|
|
16366
|
+
}
|
|
16367
|
+
const start = this.i;
|
|
16368
|
+
this.i += 1;
|
|
16369
|
+
b.cmd.nested.push(this.list("`"));
|
|
16370
|
+
b.add(src.slice(start, this.i));
|
|
16371
|
+
}
|
|
16372
|
+
readSingle() {
|
|
16373
|
+
const end = this.src.indexOf("'", this.i + 1);
|
|
16374
|
+
const stop = end === -1 ? this.src.length : end;
|
|
16375
|
+
const text = this.src.slice(this.i + 1, stop);
|
|
16376
|
+
this.i = stop + 1;
|
|
16377
|
+
return text;
|
|
16378
|
+
}
|
|
16379
|
+
/** A double-quoted string: escapes, and `$(…)` substitutions run as nested commands. */
|
|
16380
|
+
readDouble(b) {
|
|
16381
|
+
const { src } = this;
|
|
16382
|
+
let text = "";
|
|
16383
|
+
this.i += 1;
|
|
16384
|
+
while (this.i < src.length) {
|
|
16385
|
+
const c = src[this.i];
|
|
16386
|
+
const next = src[this.i + 1];
|
|
16387
|
+
if (c === "\"") {
|
|
16388
|
+
this.i += 1;
|
|
16389
|
+
return text;
|
|
16390
|
+
}
|
|
16391
|
+
if (c === "\\" && this.dialect !== "powershell" && next && "\"\\$`\n".includes(next)) {
|
|
16392
|
+
text += next;
|
|
16393
|
+
this.i += 2;
|
|
16394
|
+
} else if (c === "`" && this.dialect !== "posix" && next !== void 0) {
|
|
16395
|
+
text += next;
|
|
16396
|
+
this.i += 2;
|
|
16397
|
+
} else if (c === "`") {
|
|
16398
|
+
const start = this.i;
|
|
16399
|
+
this.i += 1;
|
|
16400
|
+
b.cmd.nested.push(this.list("`"));
|
|
16401
|
+
text += src.slice(start, this.i);
|
|
16402
|
+
} else if (c === "$" && next === "(") {
|
|
16403
|
+
const start = this.i;
|
|
16404
|
+
this.i += 2;
|
|
16405
|
+
b.cmd.nested.push(this.list(")"));
|
|
16406
|
+
text += src.slice(start, this.i);
|
|
16407
|
+
} else {
|
|
16408
|
+
text += c;
|
|
16409
|
+
this.i += 1;
|
|
16410
|
+
}
|
|
16411
|
+
}
|
|
16412
|
+
return text;
|
|
16413
|
+
}
|
|
16414
|
+
/** True when only spaces or tabs remain before the end of the line at `j`. */
|
|
16415
|
+
lineEndsAt(j) {
|
|
16416
|
+
return /^[ \t]*(\r?\n|$)/.exec(this.src.slice(j)) !== null;
|
|
16417
|
+
}
|
|
16418
|
+
/** Skip a PowerShell here-string (`@'` … `'@`) whose opening quote is at `this.i`. */
|
|
16419
|
+
skipHereString(quote) {
|
|
16420
|
+
const close = new RegExp(`(^|\\n)${quote}@`, "g");
|
|
16421
|
+
close.lastIndex = this.i + 1;
|
|
16422
|
+
const m = close.exec(this.src);
|
|
16423
|
+
this.i = m ? m.index + m[0].length : this.src.length;
|
|
16424
|
+
}
|
|
16425
|
+
/** Skip the bodies of heredocs opened on the line that just ended. */
|
|
16426
|
+
skipHeredocs(pending) {
|
|
16427
|
+
const { src } = this;
|
|
16428
|
+
for (const { delim, strip } of pending) while (this.i < src.length) {
|
|
16429
|
+
const nl = src.indexOf("\n", this.i);
|
|
16430
|
+
const end = nl === -1 ? src.length : nl;
|
|
16431
|
+
let line = src.slice(this.i, end).replace(/\r$/, "");
|
|
16432
|
+
if (strip) line = line.replace(/^\t+/, "");
|
|
16433
|
+
this.i = nl === -1 ? src.length : nl + 1;
|
|
16434
|
+
if (line === delim) break;
|
|
16435
|
+
}
|
|
16436
|
+
}
|
|
16437
|
+
};
|
|
16438
|
+
// ../../packages/providers/src/policy/risk.ts
|
|
16439
|
+
const SAFE = { risky: false };
|
|
16440
|
+
const risky = (action, reason) => ({
|
|
16441
|
+
risky: true,
|
|
16442
|
+
action,
|
|
16443
|
+
reason
|
|
16444
|
+
});
|
|
16445
|
+
/** How deep `bash -c "…"` style inline scripts are followed. */
|
|
16446
|
+
const MAX_DEPTH = 4;
|
|
16447
|
+
/**
|
|
16448
|
+
* Words that don't change what runs: elevation (`sudo`), wrappers (`env X=1`, `command`,
|
|
16449
|
+
* `exec`) and shell keywords that start a compound command (`if`, `then`, `do`, `{`, ...).
|
|
16450
|
+
*/
|
|
16451
|
+
function unwrap(words) {
|
|
16452
|
+
let elevated = false;
|
|
16453
|
+
let rest = words;
|
|
16454
|
+
for (;;) {
|
|
16455
|
+
const head = rest[0]?.toLowerCase();
|
|
16456
|
+
if (head === "sudo" || head === "doas" || head === "runas" || head === "gsudo") {
|
|
16457
|
+
elevated = true;
|
|
16458
|
+
rest = rest.slice(1).filter((t, i) => !(i === 0 && t.startsWith("-")));
|
|
16459
|
+
} else if (head === "env" || head === "command" || head === "exec" || head === "nohup") rest = rest.slice(1);
|
|
16460
|
+
else if (head && KEYWORDS.has(head)) rest = rest.slice(1);
|
|
16461
|
+
else if (head && /^[A-Za-z_][A-Za-z0-9_]*=/.test(head)) rest = rest.slice(1);
|
|
16462
|
+
else break;
|
|
15316
16463
|
}
|
|
15317
16464
|
return {
|
|
15318
16465
|
tokens: rest,
|
|
15319
16466
|
elevated
|
|
15320
16467
|
};
|
|
15321
16468
|
}
|
|
16469
|
+
const KEYWORDS = /* @__PURE__ */ new Set([
|
|
16470
|
+
"if",
|
|
16471
|
+
"then",
|
|
16472
|
+
"else",
|
|
16473
|
+
"elif",
|
|
16474
|
+
"do",
|
|
16475
|
+
"while",
|
|
16476
|
+
"until",
|
|
16477
|
+
"time",
|
|
16478
|
+
"!",
|
|
16479
|
+
"{"
|
|
16480
|
+
]);
|
|
15322
16481
|
const binName = (token) => (token ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase().replace(/\.(exe|cmd|bat|ps1)$/, "") ?? "";
|
|
15323
16482
|
const DOWNLOADERS = /\b(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\b|downloadstring/i;
|
|
15324
16483
|
const SHELLS = /^\s*(sudo\s+)?(sh|bash|zsh|fish|dash|ksh|iex|invoke-expression|python3?|node|pwsh|powershell|perl|ruby)(\s|$)/i;
|
|
@@ -15328,18 +16487,109 @@ function classifyCommand(command, ctx) {
|
|
|
15328
16487
|
for (let i = 1; i < pipeline.length; i += 1) if (DOWNLOADERS.test(pipeline[i - 1] ?? "") && SHELLS.test(pipeline[i] ?? "")) return risky("network", "Pipes a downloaded script straight into a shell");
|
|
15329
16488
|
if (/\b(iex|invoke-expression)\b[^|]*\b(irm|iwr|invoke-restmethod|invoke-webrequest|downloadstring)\b/i.test(command)) return risky("network", "Executes a downloaded script");
|
|
15330
16489
|
if (/\b(bash|sh|zsh)\s+(-c\s+)?["']?\$\(\s*(curl|wget)\b/i.test(command) || /<\(\s*(curl|wget)\b/i.test(command)) return risky("network", "Executes a downloaded script");
|
|
15331
|
-
|
|
15332
|
-
|
|
16490
|
+
return classifyList(parseShell(command, ctx.shell ?? "auto"), ctx, 0);
|
|
16491
|
+
}
|
|
16492
|
+
/**
|
|
16493
|
+
* Classify commands in order. A `cd` moves where later relative paths resolve; commands in
|
|
16494
|
+
* substitutions and subshells are classified too, without their `cd` leaking out.
|
|
16495
|
+
*/
|
|
16496
|
+
function classifyList(list, ctx, depth) {
|
|
16497
|
+
let here = ctx;
|
|
16498
|
+
for (const cmd of list) {
|
|
16499
|
+
for (const nested of cmd.nested) {
|
|
16500
|
+
const result = classifyList(nested, here, depth);
|
|
16501
|
+
if (result.risky) return result;
|
|
16502
|
+
}
|
|
16503
|
+
const result = classifySimple(cmd, here, depth);
|
|
15333
16504
|
if (result.risky) return result;
|
|
16505
|
+
here = afterCd(cmd, here);
|
|
15334
16506
|
}
|
|
15335
16507
|
return SAFE;
|
|
15336
16508
|
}
|
|
15337
|
-
|
|
15338
|
-
|
|
16509
|
+
const CD = /* @__PURE__ */ new Set([
|
|
16510
|
+
"cd",
|
|
16511
|
+
"pushd",
|
|
16512
|
+
"chdir",
|
|
16513
|
+
"set-location",
|
|
16514
|
+
"sl",
|
|
16515
|
+
"push-location"
|
|
16516
|
+
]);
|
|
16517
|
+
/** The context after `cmd` when it changes directory (`cd dir`, `Set-Location -Path dir`). */
|
|
16518
|
+
function afterCd(cmd, ctx) {
|
|
16519
|
+
const { tokens } = unwrap(cmd.words);
|
|
16520
|
+
if (!CD.has(binName(tokens[0]))) return ctx;
|
|
16521
|
+
const args = tokens.slice(1);
|
|
16522
|
+
if (args.includes("-")) return ctx;
|
|
16523
|
+
const target = namedArg(args, ["-path", "-literalpath"]) ?? args.find((a) => !a.startsWith("-")) ?? "~";
|
|
16524
|
+
return {
|
|
16525
|
+
...ctx,
|
|
16526
|
+
dir: resolveAgainst(target, ctx)
|
|
16527
|
+
};
|
|
16528
|
+
}
|
|
16529
|
+
/** The value after a PowerShell-style named parameter (`-Path x`), if present. */
|
|
16530
|
+
function namedArg(args, names) {
|
|
16531
|
+
const i = args.findIndex((a) => names.includes(a.toLowerCase()));
|
|
16532
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
16533
|
+
}
|
|
16534
|
+
/** The script of an inline shell invocation (`bash -c "…"`, `pwsh -Command …`, `cmd /c …`). */
|
|
16535
|
+
function inlineScript(bin, args) {
|
|
16536
|
+
if ([
|
|
16537
|
+
"sh",
|
|
16538
|
+
"bash",
|
|
16539
|
+
"zsh",
|
|
16540
|
+
"dash",
|
|
16541
|
+
"ksh",
|
|
16542
|
+
"ash",
|
|
16543
|
+
"fish"
|
|
16544
|
+
].includes(bin)) {
|
|
16545
|
+
const flag = args.findIndex((a) => /^-[a-z]*c[a-z]*$/i.test(a));
|
|
16546
|
+
const text = flag >= 0 ? args.slice(flag + 1).find((a) => !a.startsWith("-")) : void 0;
|
|
16547
|
+
return text ? {
|
|
16548
|
+
text,
|
|
16549
|
+
dialect: "posix"
|
|
16550
|
+
} : void 0;
|
|
16551
|
+
}
|
|
16552
|
+
if (bin === "pwsh" || bin === "powershell") {
|
|
16553
|
+
const flag = args.findIndex((a) => /^-(c|command)$/i.test(a));
|
|
16554
|
+
return flag >= 0 ? {
|
|
16555
|
+
text: args.slice(flag + 1).join(" "),
|
|
16556
|
+
dialect: "powershell"
|
|
16557
|
+
} : void 0;
|
|
16558
|
+
}
|
|
16559
|
+
if (bin === "cmd") {
|
|
16560
|
+
const flag = args.findIndex((a) => /^\/[ck]$/i.test(a));
|
|
16561
|
+
return flag >= 0 ? {
|
|
16562
|
+
text: args.slice(flag + 1).join(" "),
|
|
16563
|
+
dialect: "auto"
|
|
16564
|
+
} : void 0;
|
|
16565
|
+
}
|
|
16566
|
+
}
|
|
16567
|
+
/**
|
|
16568
|
+
* The argument as a path, or undefined when it can't be one: inline code, multi-line text
|
|
16569
|
+
* or anything with shell or code punctuation (`selected={row.key`) is not a file name.
|
|
16570
|
+
*/
|
|
16571
|
+
function asPath(arg) {
|
|
16572
|
+
const value = arg.replace(/^-+[a-z-]*=/i, "");
|
|
16573
|
+
if (!value || /[\n\r{}()<>|;=`]/.test(value)) return void 0;
|
|
16574
|
+
return value;
|
|
16575
|
+
}
|
|
16576
|
+
const DISCARD = /^(\/dev\/(null|stdout|stderr|tty)|nul|\$null)$/i;
|
|
16577
|
+
function classifySimple(cmd, ctx, depth) {
|
|
16578
|
+
const { tokens, elevated } = unwrap(cmd.words);
|
|
15339
16579
|
if (elevated) return risky("other", "Runs a command with elevated privileges");
|
|
15340
16580
|
const bin = binName(tokens[0]);
|
|
15341
16581
|
const args = tokens.slice(1);
|
|
15342
16582
|
const lower = args.map((a) => a.toLowerCase());
|
|
16583
|
+
const script = inlineScript(bin, args);
|
|
16584
|
+
if (script && depth < MAX_DEPTH) {
|
|
16585
|
+
const inner = classifyList(parseShell(script.text, script.dialect), ctx, depth + 1);
|
|
16586
|
+
if (inner.risky) return inner;
|
|
16587
|
+
}
|
|
16588
|
+
for (const target of cmd.writes) {
|
|
16589
|
+
if (DISCARD.test(target)) continue;
|
|
16590
|
+
const write = classifyWrite(target, ctx);
|
|
16591
|
+
if (write.risky) return write;
|
|
16592
|
+
}
|
|
15343
16593
|
if (bin === "git") return classifyGit(lower);
|
|
15344
16594
|
const install = classifyInstall(bin, lower);
|
|
15345
16595
|
if (install.risky) return install;
|
|
@@ -15357,44 +16607,51 @@ function classifySimple(simple, ctx) {
|
|
|
15357
16607
|
const targets = args.filter((a) => !a.startsWith("-") && !/^\/[a-z]$/i.test(a));
|
|
15358
16608
|
for (const target of targets) {
|
|
15359
16609
|
if (target === "*" || /^[\\/]$/.test(target) || /^[a-z]:[\\/]?$/i.test(target)) return risky("outside_worktree", `Deletes ${target}`);
|
|
15360
|
-
if (!
|
|
16610
|
+
if (!isWritable(target, ctx)) return risky("outside_worktree", `Deletes ${target}, outside the worktree`);
|
|
15361
16611
|
}
|
|
15362
16612
|
}
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
if (
|
|
15367
|
-
if (isSecretPath(target)) return risky("secrets", `Writes the secrets file ${target}`);
|
|
16613
|
+
const dest = copyDestination(bin, args);
|
|
16614
|
+
if (dest) {
|
|
16615
|
+
const write = classifyWrite(dest, ctx);
|
|
16616
|
+
if (write.risky) return write;
|
|
15368
16617
|
}
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
"move",
|
|
15374
|
-
"copy-item",
|
|
15375
|
-
"move-item",
|
|
15376
|
-
"xcopy",
|
|
15377
|
-
"robocopy",
|
|
15378
|
-
"tee",
|
|
15379
|
-
"set-content",
|
|
15380
|
-
"out-file",
|
|
15381
|
-
"add-content"
|
|
15382
|
-
].includes(bin)) {
|
|
15383
|
-
const positional = args.filter((a) => !a.startsWith("-") && !/^\/[a-z]+$/i.test(a));
|
|
15384
|
-
const dest = [
|
|
15385
|
-
"tee",
|
|
15386
|
-
"set-content",
|
|
15387
|
-
"out-file",
|
|
15388
|
-
"add-content"
|
|
15389
|
-
].includes(bin) ? positional[0] : positional.at(-1);
|
|
15390
|
-
if (dest && !isInWorkspace(dest, ctx)) return risky("outside_worktree", `Writes ${dest}, outside the worktree`);
|
|
15391
|
-
}
|
|
15392
|
-
const secret = args.find((a) => isSecretPath(a.replace(/^-+[a-z-]*=/i, "")));
|
|
16618
|
+
const secret = args.find((a) => {
|
|
16619
|
+
const value = asPath(a);
|
|
16620
|
+
return value !== void 0 && isSecretPath(value);
|
|
16621
|
+
});
|
|
15393
16622
|
if (secret) return risky("secrets", `Accesses the secrets file ${secret}`);
|
|
15394
16623
|
const listsEnvDrive = /^(get-childitem|gci|ls|dir)$/.test(bin) && lower.some((a) => a.startsWith("env:"));
|
|
15395
16624
|
if (bin === "printenv" || listsEnvDrive) return risky("secrets", "Dumps environment variables");
|
|
15396
16625
|
return SAFE;
|
|
15397
16626
|
}
|
|
16627
|
+
const COPIES = [
|
|
16628
|
+
"cp",
|
|
16629
|
+
"mv",
|
|
16630
|
+
"copy",
|
|
16631
|
+
"move",
|
|
16632
|
+
"copy-item",
|
|
16633
|
+
"move-item",
|
|
16634
|
+
"xcopy",
|
|
16635
|
+
"robocopy"
|
|
16636
|
+
];
|
|
16637
|
+
const WRITERS = [
|
|
16638
|
+
"tee",
|
|
16639
|
+
"set-content",
|
|
16640
|
+
"out-file",
|
|
16641
|
+
"add-content"
|
|
16642
|
+
];
|
|
16643
|
+
/** Where a copy, move or file-writing command writes. */
|
|
16644
|
+
function copyDestination(bin, args) {
|
|
16645
|
+
if (!COPIES.includes(bin) && !WRITERS.includes(bin)) return void 0;
|
|
16646
|
+
const named = WRITERS.includes(bin) ? namedArg(args, [
|
|
16647
|
+
"-path",
|
|
16648
|
+
"-filepath",
|
|
16649
|
+
"-literalpath"
|
|
16650
|
+
]) : namedArg(args, ["-destination"]);
|
|
16651
|
+
if (named) return named;
|
|
16652
|
+
const positional = args.filter((a) => !a.startsWith("-") && !/^\/[a-z]+$/i.test(a));
|
|
16653
|
+
return WRITERS.includes(bin) ? positional[0] : positional.at(-1);
|
|
16654
|
+
}
|
|
15398
16655
|
function classifyGit(args) {
|
|
15399
16656
|
let i = 0;
|
|
15400
16657
|
while (i < args.length && args[i]?.startsWith("-")) i += args[i] === "-c" || args[i] === "-C" ? 2 : 1;
|
|
@@ -15471,11 +16728,16 @@ function classifyInstall(bin, args) {
|
|
|
15471
16728
|
if (bin === "install-module" || bin === "install-package") return risky("other", `Installs a system-wide module (${bin})`);
|
|
15472
16729
|
return SAFE;
|
|
15473
16730
|
}
|
|
15474
|
-
/**
|
|
16731
|
+
/**
|
|
16732
|
+
* Classify a file write/edit. The worktree, extra roots and OS temp are fine; secrets ask. A
|
|
16733
|
+
* secrets file in temp counts as outside the worktree, so Full autonomy (which only asks for
|
|
16734
|
+
* writes outside the worktree) still asks for it.
|
|
16735
|
+
*/
|
|
15475
16736
|
function classifyWrite(target, ctx) {
|
|
15476
|
-
if (!
|
|
15477
|
-
if (isSecretPath(target)) return
|
|
15478
|
-
return
|
|
16737
|
+
if (!isWritable(target, ctx)) return risky("outside_worktree", `Writes ${target}, outside the worktree`);
|
|
16738
|
+
if (!isSecretPath(target)) return SAFE;
|
|
16739
|
+
if (!isInWorkspace(target, ctx)) return risky("outside_worktree", `Writes the secrets file ${target} in the temp directory`);
|
|
16740
|
+
return risky("secrets", `Writes the secrets file ${target}`);
|
|
15479
16741
|
}
|
|
15480
16742
|
/** Classify a file read or search. Reads anywhere are fine except credential files. */
|
|
15481
16743
|
function classifyRead(target, _ctx) {
|
|
@@ -15496,130 +16758,6 @@ function classifyFetch(url, ctx) {
|
|
|
15496
16758
|
return rule.startsWith("*.") ? host.endsWith(rule.slice(1)) : host === rule;
|
|
15497
16759
|
}) ? SAFE : risky("network", `Fetches ${host}, which is not on the allowed host list`);
|
|
15498
16760
|
}
|
|
15499
|
-
// ../../packages/providers/src/policy/read-only.ts
|
|
15500
|
-
/**
|
|
15501
|
-
* Read-only sessions (the orchestrator and PMs, §11.2) may inspect a repository but never
|
|
15502
|
-
* change it. This allowlist decides which shell commands they may run: commands that only
|
|
15503
|
-
* read, with no output redirection and no command substitution. Anything else is denied.
|
|
15504
|
-
*/
|
|
15505
|
-
/** Programs that only read (or print). */
|
|
15506
|
-
const READ_ONLY_BINS = /* @__PURE__ */ new Set([
|
|
15507
|
-
"ls",
|
|
15508
|
-
"dir",
|
|
15509
|
-
"cat",
|
|
15510
|
-
"type",
|
|
15511
|
-
"head",
|
|
15512
|
-
"tail",
|
|
15513
|
-
"less",
|
|
15514
|
-
"more",
|
|
15515
|
-
"wc",
|
|
15516
|
-
"grep",
|
|
15517
|
-
"egrep",
|
|
15518
|
-
"fgrep",
|
|
15519
|
-
"rg",
|
|
15520
|
-
"find",
|
|
15521
|
-
"fd",
|
|
15522
|
-
"tree",
|
|
15523
|
-
"pwd",
|
|
15524
|
-
"echo",
|
|
15525
|
-
"printf",
|
|
15526
|
-
"which",
|
|
15527
|
-
"where",
|
|
15528
|
-
"whoami",
|
|
15529
|
-
"date",
|
|
15530
|
-
"stat",
|
|
15531
|
-
"file",
|
|
15532
|
-
"du",
|
|
15533
|
-
"df",
|
|
15534
|
-
"sort",
|
|
15535
|
-
"uniq",
|
|
15536
|
-
"cut",
|
|
15537
|
-
"diff",
|
|
15538
|
-
"jq",
|
|
15539
|
-
"basename",
|
|
15540
|
-
"dirname",
|
|
15541
|
-
"realpath",
|
|
15542
|
-
"true",
|
|
15543
|
-
"get-childitem",
|
|
15544
|
-
"gci",
|
|
15545
|
-
"get-content",
|
|
15546
|
-
"gc",
|
|
15547
|
-
"get-item",
|
|
15548
|
-
"gi",
|
|
15549
|
-
"get-location",
|
|
15550
|
-
"gl",
|
|
15551
|
-
"select-string",
|
|
15552
|
-
"sls",
|
|
15553
|
-
"measure-object",
|
|
15554
|
-
"test-path",
|
|
15555
|
-
"resolve-path",
|
|
15556
|
-
"write-output",
|
|
15557
|
-
"write-host",
|
|
15558
|
-
"select-object",
|
|
15559
|
-
"where-object",
|
|
15560
|
-
"format-list",
|
|
15561
|
-
"format-table",
|
|
15562
|
-
"out-string",
|
|
15563
|
-
"sort-object",
|
|
15564
|
-
"gh"
|
|
15565
|
-
]);
|
|
15566
|
-
/** Git subcommands that never change the repository. */
|
|
15567
|
-
const READ_ONLY_GIT = /* @__PURE__ */ new Set([
|
|
15568
|
-
"status",
|
|
15569
|
-
"log",
|
|
15570
|
-
"show",
|
|
15571
|
-
"diff",
|
|
15572
|
-
"blame",
|
|
15573
|
-
"grep",
|
|
15574
|
-
"ls-files",
|
|
15575
|
-
"ls-tree",
|
|
15576
|
-
"rev-parse",
|
|
15577
|
-
"rev-list",
|
|
15578
|
-
"describe",
|
|
15579
|
-
"shortlog",
|
|
15580
|
-
"cat-file",
|
|
15581
|
-
"remote",
|
|
15582
|
-
"branch",
|
|
15583
|
-
"tag",
|
|
15584
|
-
"config",
|
|
15585
|
-
"reflog",
|
|
15586
|
-
"merge-base",
|
|
15587
|
-
"name-rev",
|
|
15588
|
-
"for-each-ref",
|
|
15589
|
-
"worktree"
|
|
15590
|
-
]);
|
|
15591
|
-
/** `gh` subcommand verbs that only read. */
|
|
15592
|
-
const READ_ONLY_GH = /* @__PURE__ */ new Set([
|
|
15593
|
-
"view",
|
|
15594
|
-
"list",
|
|
15595
|
-
"status",
|
|
15596
|
-
"diff",
|
|
15597
|
-
"checks"
|
|
15598
|
-
]);
|
|
15599
|
-
const bin = (token) => (token ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase().replace(/\.(exe|cmd|bat|ps1)$/, "") ?? "";
|
|
15600
|
-
/** Why a command is refused in a read-only session, or undefined when it may run. */
|
|
15601
|
-
function readOnlyCommandDenial(command) {
|
|
15602
|
-
if (/\$\(|`|<\(|>\(/.test(command)) return "Command substitution is not allowed in a read-only session";
|
|
15603
|
-
for (const simple of splitCommands(command)) {
|
|
15604
|
-
if (/(^|[^0-9&<])>>?(?!\s*(&\d|\/dev\/null|nul\b|\$null))/i.test(simple)) return "Writing files is not allowed in a read-only session";
|
|
15605
|
-
const tokens = tokenize(simple);
|
|
15606
|
-
const name = bin(tokens[0]);
|
|
15607
|
-
const args = tokens.slice(1).map((a) => a.toLowerCase());
|
|
15608
|
-
if (name === "git") {
|
|
15609
|
-
const sub = args.find((a) => !a.startsWith("-"));
|
|
15610
|
-
if (!sub || !READ_ONLY_GIT.has(sub)) return `git ${sub ?? ""} may change the repository`;
|
|
15611
|
-
if ((sub === "branch" || sub === "tag" || sub === "remote" || sub === "worktree") && args.some((a) => /^-(d|D|m|M|f|-delete|-move|-force)$/.test(a) || a === "add")) return `git ${sub} with these options changes the repository`;
|
|
15612
|
-
if (sub === "config" && !args.some((a) => a === "--get" || a === "--list" || a === "-l")) return "Only reading git config is allowed";
|
|
15613
|
-
continue;
|
|
15614
|
-
}
|
|
15615
|
-
if (name === "gh") {
|
|
15616
|
-
if (!args.some((a) => READ_ONLY_GH.has(a)) || args.includes("api")) return "Only read-only gh commands are allowed (view, list, status, diff, checks)";
|
|
15617
|
-
continue;
|
|
15618
|
-
}
|
|
15619
|
-
if (name === "find" && args.some((a) => a === "-delete" || a === "-exec" || a === "-execdir")) return "find with -delete or -exec is not allowed in a read-only session";
|
|
15620
|
-
if (!READ_ONLY_BINS.has(name)) return `${name || "This command"} is not on the read-only allowlist`;
|
|
15621
|
-
}
|
|
15622
|
-
}
|
|
15623
16761
|
// ../../packages/providers/src/claude/tools.ts
|
|
15624
16762
|
/** Claude tools that are not rendered as `tool_call` items. */
|
|
15625
16763
|
const SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
@@ -15818,7 +16956,13 @@ function completeToolCall(item, result) {
|
|
|
15818
16956
|
}
|
|
15819
16957
|
/** Risk of a Claude tool call under the `auto` policy (§11.2). */
|
|
15820
16958
|
function assessClaudeTool(name, input, ctx) {
|
|
15821
|
-
if (COMMAND_TOOLS.has(name))
|
|
16959
|
+
if (COMMAND_TOOLS.has(name)) {
|
|
16960
|
+
const shell = name === "PowerShell" ? "powershell" : "posix";
|
|
16961
|
+
return classifyCommand(str(input, "command") ?? "", {
|
|
16962
|
+
...ctx,
|
|
16963
|
+
shell
|
|
16964
|
+
});
|
|
16965
|
+
}
|
|
15822
16966
|
if (EDIT_TOOLS.has(name)) return classifyWrite(editPath(input), ctx);
|
|
15823
16967
|
if (name === "Read") return classifyRead(str(input, "file_path") ?? "", ctx);
|
|
15824
16968
|
if (name === "Grep" || name === "Glob") {
|
|
@@ -15853,14 +16997,17 @@ function approvalTitle(name, input) {
|
|
|
15853
16997
|
// ../../packages/providers/src/claude/permissions.ts
|
|
15854
16998
|
/**
|
|
15855
16999
|
* Native permission mode per autonomy level (§11.2):
|
|
17000
|
+
* - read_only -> `default`; edits and writing commands are denied by the PreToolUse hook
|
|
17001
|
+
* (`ClaudePolicy.denial`), so they never reach a prompt
|
|
15856
17002
|
* - supervised -> `default`, and every edit/command is forced to ask via a PreToolUse hook
|
|
15857
17003
|
* - auto -> `acceptEdits`, plus the risky-action classifier
|
|
15858
|
-
* - full -> `bypassPermissions`, asking only for writes outside the worktree
|
|
17004
|
+
* - full -> `bypassPermissions`, asking only for writes outside the worktree (the OS
|
|
17005
|
+
* temp directory counts as inside, except for secrets files there)
|
|
15859
17006
|
* Plan mode overrides all of them with `plan`.
|
|
15860
17007
|
*/
|
|
15861
17008
|
function permissionModeFor(autonomy, planMode) {
|
|
15862
17009
|
if (planMode) return "plan";
|
|
15863
|
-
if (autonomy === "supervised") return "default";
|
|
17010
|
+
if (autonomy === "read_only" || autonomy === "supervised") return "default";
|
|
15864
17011
|
if (autonomy === "auto") return "acceptEdits";
|
|
15865
17012
|
return "bypassPermissions";
|
|
15866
17013
|
}
|
|
@@ -15879,16 +17026,34 @@ var ClaudePolicy = class {
|
|
|
15879
17026
|
this.ctx = ctx;
|
|
15880
17027
|
this.readOnly = readOnly;
|
|
15881
17028
|
}
|
|
17029
|
+
get level() {
|
|
17030
|
+
return this.autonomy;
|
|
17031
|
+
}
|
|
17032
|
+
/**
|
|
17033
|
+
* True while edits and writing commands are refused: a read-only role (orchestrator, PMs),
|
|
17034
|
+
* or the `read_only` permission level, which can change live with `setAutonomy`.
|
|
17035
|
+
*/
|
|
17036
|
+
get isReadOnly() {
|
|
17037
|
+
return this.readOnly || this.autonomy === "read_only";
|
|
17038
|
+
}
|
|
17039
|
+
/** Switch the level; the next `gate()` uses it. `allow_session` decisions are kept. */
|
|
17040
|
+
setAutonomy(level) {
|
|
17041
|
+
this.autonomy = level;
|
|
17042
|
+
}
|
|
15882
17043
|
/** Why a call is refused outright (read-only sessions), or undefined. */
|
|
15883
17044
|
denial(toolName, input) {
|
|
15884
|
-
if (!this.
|
|
15885
|
-
if (EDIT_TOOLS.has(toolName)) return "This session is read-only: it cannot edit files. Act through CodeDen tools instead.";
|
|
17045
|
+
if (!this.isReadOnly) return void 0;
|
|
17046
|
+
if (EDIT_TOOLS.has(toolName)) return this.readOnly ? "This session is read-only: it cannot edit files. Act through CodeDen tools instead." : "This session has read-only permissions: it cannot edit files, commit or push. Report what you found instead.";
|
|
15886
17047
|
if (COMMAND_TOOLS.has(toolName)) {
|
|
15887
|
-
const reason = readOnlyCommandDenial(str(input, "command") ?? "");
|
|
17048
|
+
const reason = readOnlyCommandDenial(str(input, "command") ?? "", { allowChecks: !this.readOnly });
|
|
15888
17049
|
return reason ? `This session is read-only: ${reason}.` : void 0;
|
|
15889
17050
|
}
|
|
15890
17051
|
}
|
|
15891
|
-
|
|
17052
|
+
/**
|
|
17053
|
+
* Decide one tool call. `dir` is the shell's current directory as Claude reports it (an
|
|
17054
|
+
* earlier `cd` persists between Bash calls); relative paths resolve from it.
|
|
17055
|
+
*/
|
|
17056
|
+
gate(toolName, input, dir) {
|
|
15892
17057
|
if (toolName === "AskUserQuestion") return {
|
|
15893
17058
|
ask: true,
|
|
15894
17059
|
action: "other",
|
|
@@ -15900,7 +17065,10 @@ var ClaudePolicy = class {
|
|
|
15900
17065
|
reason: "The agent finished planning"
|
|
15901
17066
|
};
|
|
15902
17067
|
if (this.sessionAllow.has(this.key(toolName, input))) return { ask: false };
|
|
15903
|
-
const risk = assessClaudeTool(toolName, input,
|
|
17068
|
+
const risk = assessClaudeTool(toolName, input, dir ? {
|
|
17069
|
+
...this.ctx,
|
|
17070
|
+
dir
|
|
17071
|
+
} : this.ctx);
|
|
15904
17072
|
if (this.autonomy === "supervised") {
|
|
15905
17073
|
if (risk.risky) return {
|
|
15906
17074
|
ask: true,
|
|
@@ -15926,8 +17094,8 @@ var ClaudePolicy = class {
|
|
|
15926
17094
|
* Whether a `canUseTool` call (Claude's own permission prompt) needs the user. In supervised
|
|
15927
17095
|
* mode everything Claude asks about goes to the user; otherwise only gated calls do.
|
|
15928
17096
|
*/
|
|
15929
|
-
needsUser(toolName, input) {
|
|
15930
|
-
const gate = this.gate(toolName, input);
|
|
17097
|
+
needsUser(toolName, input, dir) {
|
|
17098
|
+
const gate = this.gate(toolName, input, dir);
|
|
15931
17099
|
if (gate.ask || this.autonomy !== "supervised") return gate;
|
|
15932
17100
|
if (this.sessionAllow.has(this.key(toolName, input))) return gate;
|
|
15933
17101
|
return {
|
|
@@ -16063,6 +17231,83 @@ function toSdkUserMessage(input) {
|
|
|
16063
17231
|
parent_tool_use_id: null
|
|
16064
17232
|
};
|
|
16065
17233
|
}
|
|
17234
|
+
/** The SDK's model rows as CodeDen models. The first row is Claude Code's default. */
|
|
17235
|
+
function toModelInfos(models) {
|
|
17236
|
+
return models.map((m, i) => ({
|
|
17237
|
+
id: m.value,
|
|
17238
|
+
label: m.displayName || m.value,
|
|
17239
|
+
...m.description ? { description: m.description } : {},
|
|
17240
|
+
efforts: m.supportedEffortLevels ?? [],
|
|
17241
|
+
...i === 0 ? { isDefault: true } : {}
|
|
17242
|
+
}));
|
|
17243
|
+
}
|
|
17244
|
+
/**
|
|
17245
|
+
* List Claude Code's models without a turn. `query()` with a streaming prompt that never
|
|
17246
|
+
* yields starts the CLI and sends only the `initialize` control request, whose answer carries
|
|
17247
|
+
* the model list (`supportedModels()`); no user message is sent, so no model request is made.
|
|
17248
|
+
* Nothing from the user's or a project's settings is loaded (no hooks run), nothing is
|
|
17249
|
+
* persisted, and the CLI is closed as soon as the list arrives or `timeoutMs` passes.
|
|
17250
|
+
* Rejects on timeout or when the CLI fails to start.
|
|
17251
|
+
*/
|
|
17252
|
+
async function listClaudeModels(query, executable, opts = {}) {
|
|
17253
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
17254
|
+
const abortController = new AbortController();
|
|
17255
|
+
const stopped = new Promise((resolve) => abortController.signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
17256
|
+
const idle = () => ({ [Symbol.asyncIterator]: () => ({ next: async () => {
|
|
17257
|
+
await stopped;
|
|
17258
|
+
return {
|
|
17259
|
+
done: true,
|
|
17260
|
+
value: void 0
|
|
17261
|
+
};
|
|
17262
|
+
} }) });
|
|
17263
|
+
let q;
|
|
17264
|
+
let timer;
|
|
17265
|
+
try {
|
|
17266
|
+
q = query({
|
|
17267
|
+
prompt: idle(),
|
|
17268
|
+
options: {
|
|
17269
|
+
...executableOptions(executable),
|
|
17270
|
+
cwd: tmpdir(),
|
|
17271
|
+
settingSources: [],
|
|
17272
|
+
persistSession: false,
|
|
17273
|
+
tools: [],
|
|
17274
|
+
mcpServers: {},
|
|
17275
|
+
strictMcpConfig: true,
|
|
17276
|
+
env: claudeEnv(void 0),
|
|
17277
|
+
abortController
|
|
17278
|
+
}
|
|
17279
|
+
});
|
|
17280
|
+
const stream = q;
|
|
17281
|
+
(async () => {
|
|
17282
|
+
for await (const _message of stream);
|
|
17283
|
+
})().catch(() => void 0);
|
|
17284
|
+
const timeout = new Promise((_, reject) => {
|
|
17285
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Claude Code did not list its models within ${timeoutMs}ms`)), timeoutMs);
|
|
17286
|
+
});
|
|
17287
|
+
return toModelInfos(await Promise.race([q.supportedModels(), timeout]));
|
|
17288
|
+
} finally {
|
|
17289
|
+
clearTimeout(timer);
|
|
17290
|
+
abortController.abort();
|
|
17291
|
+
try {
|
|
17292
|
+
q?.close();
|
|
17293
|
+
} catch {}
|
|
17294
|
+
}
|
|
17295
|
+
}
|
|
17296
|
+
// ../../packages/providers/src/utility.ts
|
|
17297
|
+
/** Abort signal that fires on the caller's signal or after `timeoutMs`. */
|
|
17298
|
+
function oneShotSignal(request) {
|
|
17299
|
+
const controller = new AbortController();
|
|
17300
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Utility model timed out")), request.timeoutMs ?? 6e4);
|
|
17301
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
17302
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
17303
|
+
return {
|
|
17304
|
+
signal: controller.signal,
|
|
17305
|
+
dispose: () => {
|
|
17306
|
+
clearTimeout(timer);
|
|
17307
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
17308
|
+
}
|
|
17309
|
+
};
|
|
17310
|
+
}
|
|
16066
17311
|
// ../../packages/providers/src/util/install-lock.ts
|
|
16067
17312
|
function isAlive$1(pid) {
|
|
16068
17313
|
try {
|
|
@@ -16196,6 +17441,163 @@ function npmFailure(output) {
|
|
|
16196
17441
|
if (summary) return summary;
|
|
16197
17442
|
return output.trim().split(/\r?\n/).slice(-3).join(" ").slice(0, 500);
|
|
16198
17443
|
}
|
|
17444
|
+
// ../../packages/providers/src/util/tar.ts
|
|
17445
|
+
const BLOCK = 512;
|
|
17446
|
+
/**
|
|
17447
|
+
* Extract a gzipped tarball (an npm package) into `dest`, dropping the first `strip` path
|
|
17448
|
+
* components (`package/` in npm tarballs). Only regular files and directories are written;
|
|
17449
|
+
* links and devices are skipped. Entries that would land outside `dest` (absolute paths,
|
|
17450
|
+
* `..`) fail the whole extraction. Returns the relative paths written.
|
|
17451
|
+
*/
|
|
17452
|
+
function extractTarGz(archive, dest, opts = {}) {
|
|
17453
|
+
const tar = gunzipSync(archive);
|
|
17454
|
+
const root = path.resolve(dest);
|
|
17455
|
+
const written = [];
|
|
17456
|
+
let offset = 0;
|
|
17457
|
+
let longName = null;
|
|
17458
|
+
let paxPath = null;
|
|
17459
|
+
while (offset + BLOCK <= tar.length) {
|
|
17460
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
17461
|
+
if (header.every((b) => b === 0)) break;
|
|
17462
|
+
if (!checksumOk(header)) throw new Error("Corrupt tarball (bad header checksum)");
|
|
17463
|
+
const size = readOctal(header, 124, 12);
|
|
17464
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
17465
|
+
const dataStart = offset + BLOCK;
|
|
17466
|
+
const data = tar.subarray(dataStart, dataStart + size);
|
|
17467
|
+
if (data.length < size) throw new Error("Corrupt tarball (truncated entry)");
|
|
17468
|
+
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
17469
|
+
if (type === "L") {
|
|
17470
|
+
longName = cString(data);
|
|
17471
|
+
continue;
|
|
17472
|
+
}
|
|
17473
|
+
if (type === "x") {
|
|
17474
|
+
paxPath = paxRecord(data, "path");
|
|
17475
|
+
continue;
|
|
17476
|
+
}
|
|
17477
|
+
if (type === "g") continue;
|
|
17478
|
+
const name = paxPath ?? longName ?? headerName(header);
|
|
17479
|
+
longName = null;
|
|
17480
|
+
paxPath = null;
|
|
17481
|
+
if (!(type === "0" || type === "\0" || type === "7") && type !== "5") continue;
|
|
17482
|
+
const rel = stripComponents(name, opts.strip ?? 0);
|
|
17483
|
+
if (!rel) continue;
|
|
17484
|
+
const target = path.resolve(root, rel);
|
|
17485
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`Unsafe path in tarball: ${name}`);
|
|
17486
|
+
if (type === "5") {
|
|
17487
|
+
mkdirSync(target, { recursive: true });
|
|
17488
|
+
continue;
|
|
17489
|
+
}
|
|
17490
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
17491
|
+
writeFileSync(target, data);
|
|
17492
|
+
written.push(rel);
|
|
17493
|
+
}
|
|
17494
|
+
return written;
|
|
17495
|
+
}
|
|
17496
|
+
function stripComponents(name, strip) {
|
|
17497
|
+
const parts = name.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
17498
|
+
if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) throw new Error(`Unsafe path in tarball: ${name}`);
|
|
17499
|
+
if (parts.includes("..")) throw new Error(`Unsafe path in tarball: ${name}`);
|
|
17500
|
+
const rest = parts.slice(strip).filter((p) => p !== ".");
|
|
17501
|
+
return rest.length ? rest.join("/") : null;
|
|
17502
|
+
}
|
|
17503
|
+
function headerName(header) {
|
|
17504
|
+
const name = cString(header.subarray(0, 100));
|
|
17505
|
+
if (!header.subarray(257, 263).toString("latin1").startsWith("ustar")) return name;
|
|
17506
|
+
const prefix = cString(header.subarray(345, 500));
|
|
17507
|
+
return prefix ? `${prefix}/${name}` : name;
|
|
17508
|
+
}
|
|
17509
|
+
function cString(buf) {
|
|
17510
|
+
const end = buf.indexOf(0);
|
|
17511
|
+
return buf.subarray(0, end === -1 ? buf.length : end).toString("utf8");
|
|
17512
|
+
}
|
|
17513
|
+
function readOctal(buf, start, length) {
|
|
17514
|
+
const text = cString(buf.subarray(start, start + length)).trim();
|
|
17515
|
+
const value = text ? Number.parseInt(text, 8) : 0;
|
|
17516
|
+
if (!Number.isFinite(value) || value < 0) throw new Error("Corrupt tarball (bad size)");
|
|
17517
|
+
return value;
|
|
17518
|
+
}
|
|
17519
|
+
function checksumOk(header) {
|
|
17520
|
+
const expected = readOctal(header, 148, 8);
|
|
17521
|
+
let sum = 0;
|
|
17522
|
+
for (let i = 0; i < BLOCK; i++) sum += i >= 148 && i < 156 ? 32 : header[i] ?? 0;
|
|
17523
|
+
return sum === expected;
|
|
17524
|
+
}
|
|
17525
|
+
/** One `key=value` record from a pax extended header (`<len> <key>=<value>\n` lines). */
|
|
17526
|
+
function paxRecord(data, key) {
|
|
17527
|
+
let i = 0;
|
|
17528
|
+
while (i < data.length) {
|
|
17529
|
+
const space = data.indexOf(32, i);
|
|
17530
|
+
if (space === -1) break;
|
|
17531
|
+
const len = Number.parseInt(data.subarray(i, space).toString("latin1"), 10);
|
|
17532
|
+
if (!Number.isFinite(len) || len <= 0) break;
|
|
17533
|
+
const record = data.subarray(space + 1, i + len - 1).toString("utf8");
|
|
17534
|
+
const eq = record.indexOf("=");
|
|
17535
|
+
if (eq !== -1 && record.slice(0, eq) === key) return record.slice(eq + 1);
|
|
17536
|
+
i += len;
|
|
17537
|
+
}
|
|
17538
|
+
return null;
|
|
17539
|
+
}
|
|
17540
|
+
/** The registry to use: an explicit one, npm's configured one, or the public registry. */
|
|
17541
|
+
function registryUrl(explicit, env = process.env) {
|
|
17542
|
+
return (explicit ?? env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
17543
|
+
}
|
|
17544
|
+
/**
|
|
17545
|
+
* Install one exact package version straight from an npm registry, without npm (the desktop
|
|
17546
|
+
* app has no npm, §17): read the version manifest, download the tarball, check it against the
|
|
17547
|
+
* manifest's `dist.integrity` (sha512), extract it into a staging folder and move it into
|
|
17548
|
+
* `<nodeModules>/<name>`. Dependencies are not installed: use it only for packages whose
|
|
17549
|
+
* runtime needs none (the Claude Agent SDK, installed without optional and peer deps).
|
|
17550
|
+
*/
|
|
17551
|
+
async function installFromRegistry(opts) {
|
|
17552
|
+
const doFetch = opts.fetch ?? fetch;
|
|
17553
|
+
const progress = opts.onProgress ?? (() => {});
|
|
17554
|
+
const signal = AbortSignal.timeout(opts.timeoutMs ?? 3e5);
|
|
17555
|
+
const base = registryUrl(opts.registry);
|
|
17556
|
+
const spec = `${opts.name}@${opts.version}`;
|
|
17557
|
+
const manifestUrl = `${base}/${opts.name.replace("/", "%2f")}/${encodeURIComponent(opts.version)}`;
|
|
17558
|
+
progress(`Looking up ${spec}`);
|
|
17559
|
+
const res = await doFetch(manifestUrl, {
|
|
17560
|
+
headers: { accept: "application/json" },
|
|
17561
|
+
signal
|
|
17562
|
+
});
|
|
17563
|
+
if (!res.ok) throw new Error(`the registry answered ${res.status} for ${spec}`);
|
|
17564
|
+
const manifest = await res.json();
|
|
17565
|
+
if (manifest.name !== opts.name || manifest.version !== opts.version) throw new Error(`the registry returned the wrong package for ${spec}`);
|
|
17566
|
+
const tarball = manifest.dist?.tarball;
|
|
17567
|
+
const integrity = manifest.dist?.integrity;
|
|
17568
|
+
if (typeof tarball !== "string" || !/^https?:\/\//.test(tarball)) throw new Error(`${spec} has no tarball URL`);
|
|
17569
|
+
const expected = typeof integrity === "string" ? sha512Of(integrity) : null;
|
|
17570
|
+
if (!expected) throw new Error(`${spec} has no sha512 integrity to check the download against`);
|
|
17571
|
+
progress(`Downloading ${spec}`);
|
|
17572
|
+
const download = await doFetch(tarball, { signal });
|
|
17573
|
+
if (!download.ok) throw new Error(`downloading ${spec} failed (${download.status})`);
|
|
17574
|
+
const archive = Buffer.from(await download.arrayBuffer());
|
|
17575
|
+
if (createHash("sha512").update(archive).digest("base64") !== expected) throw new Error(`the download of ${spec} does not match its published integrity (sha512)`);
|
|
17576
|
+
progress(`Unpacking ${spec}`);
|
|
17577
|
+
const target = path.join(opts.nodeModules, ...opts.name.split("/"));
|
|
17578
|
+
const staging = `${target}.staging-${randomBytes(4).toString("hex")}`;
|
|
17579
|
+
try {
|
|
17580
|
+
mkdirSync(staging, { recursive: true });
|
|
17581
|
+
extractTarGz(archive, staging, { strip: 1 });
|
|
17582
|
+
rmSync(target, {
|
|
17583
|
+
recursive: true,
|
|
17584
|
+
force: true,
|
|
17585
|
+
maxRetries: 3
|
|
17586
|
+
});
|
|
17587
|
+
renameSync(staging, target);
|
|
17588
|
+
} finally {
|
|
17589
|
+
rmSync(staging, {
|
|
17590
|
+
recursive: true,
|
|
17591
|
+
force: true,
|
|
17592
|
+
maxRetries: 2
|
|
17593
|
+
});
|
|
17594
|
+
}
|
|
17595
|
+
}
|
|
17596
|
+
/** The base64 sha512 digest in a Subresource Integrity string (`sha512-<base64> ...`). */
|
|
17597
|
+
function sha512Of(integrity) {
|
|
17598
|
+
for (const part of integrity.trim().split(/\s+/)) if (part.startsWith("sha512-")) return part.slice(7).replace(/\?.*$/, "");
|
|
17599
|
+
return null;
|
|
17600
|
+
}
|
|
16199
17601
|
// ../../packages/providers/src/claude/sdk/install.ts
|
|
16200
17602
|
/** The Claude Agent SDK package. */
|
|
16201
17603
|
const CLAUDE_SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
|
|
@@ -16222,6 +17624,18 @@ async function importClaudeSdkFrom(depsDir) {
|
|
|
16222
17624
|
if (typeof sdk.query !== "function") throw new Error(`${entry} does not export query()`);
|
|
16223
17625
|
return sdk;
|
|
16224
17626
|
}
|
|
17627
|
+
/** Which installer `installClaudeSdk` uses, and the npm to run when it is npm. */
|
|
17628
|
+
function chooseInstaller(opts, env = process.env) {
|
|
17629
|
+
const wanted = opts.installer ?? env.CODEDEN_DEPS_INSTALLER;
|
|
17630
|
+
if (wanted === "registry") return { kind: "registry" };
|
|
17631
|
+
const npm = opts.npm ?? resolveNpm(env);
|
|
17632
|
+
if (npm) return {
|
|
17633
|
+
kind: "npm",
|
|
17634
|
+
npm
|
|
17635
|
+
};
|
|
17636
|
+
if (wanted === "npm") throw new Error("npm was not found; install Node.js with npm and try again");
|
|
17637
|
+
return { kind: "registry" };
|
|
17638
|
+
}
|
|
16225
17639
|
const inflight = /* @__PURE__ */ new Map();
|
|
16226
17640
|
/**
|
|
16227
17641
|
* Install the tested SDK version into `depsDir` with npm, skipping its optional platform
|
|
@@ -16249,25 +17663,37 @@ async function runInstall(opts) {
|
|
|
16249
17663
|
name: "codeden-deps",
|
|
16250
17664
|
private: true
|
|
16251
17665
|
}, null, 2)}\n`);
|
|
16252
|
-
const
|
|
16253
|
-
if (!npm) throw new Error("npm was not found; install Node.js with npm and try again");
|
|
17666
|
+
const installer = chooseInstaller(opts);
|
|
16254
17667
|
const spec = `${CLAUDE_SDK_PACKAGE}@${CLAUDE_SDK_VERSION}`;
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
17668
|
+
if (installer.kind === "registry") {
|
|
17669
|
+
await installFromRegistry({
|
|
17670
|
+
name: CLAUDE_SDK_PACKAGE,
|
|
17671
|
+
version: CLAUDE_SDK_VERSION,
|
|
17672
|
+
nodeModules: join(opts.depsDir, "node_modules"),
|
|
17673
|
+
registry: opts.registry,
|
|
17674
|
+
fetch: opts.fetch,
|
|
17675
|
+
timeoutMs: opts.timeoutMs,
|
|
17676
|
+
onProgress: progress
|
|
17677
|
+
});
|
|
17678
|
+
recordDependency(manifest, CLAUDE_SDK_PACKAGE, CLAUDE_SDK_VERSION);
|
|
17679
|
+
} else {
|
|
17680
|
+
progress(`Downloading ${spec} from npm`);
|
|
17681
|
+
await runNpm(installer.npm, [
|
|
17682
|
+
"install",
|
|
17683
|
+
spec,
|
|
17684
|
+
"--omit=optional",
|
|
17685
|
+
"--omit=peer",
|
|
17686
|
+
"--no-audit",
|
|
17687
|
+
"--no-fund",
|
|
17688
|
+
"--save-exact",
|
|
17689
|
+
"--prefix",
|
|
17690
|
+
opts.depsDir
|
|
17691
|
+
], {
|
|
17692
|
+
cwd: opts.depsDir,
|
|
17693
|
+
timeoutMs: opts.timeoutMs ?? 3e5,
|
|
17694
|
+
onLine: progress
|
|
17695
|
+
});
|
|
17696
|
+
}
|
|
16271
17697
|
if (installedClaudeSdkVersion(opts.depsDir) !== "0.3.318") throw new Error(`npm finished but ${spec} is not in ${opts.depsDir}`);
|
|
16272
17698
|
progress("Checking the install");
|
|
16273
17699
|
await importClaudeSdkFrom(opts.depsDir);
|
|
@@ -16276,6 +17702,19 @@ async function runInstall(opts) {
|
|
|
16276
17702
|
release();
|
|
16277
17703
|
}
|
|
16278
17704
|
}
|
|
17705
|
+
/** Note the installed package in `<deps>/package.json`, as `npm install --save-exact` would. */
|
|
17706
|
+
function recordDependency(manifest, name, version) {
|
|
17707
|
+
let pkg = {};
|
|
17708
|
+
try {
|
|
17709
|
+
pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
17710
|
+
} catch {}
|
|
17711
|
+
const deps = typeof pkg.dependencies === "object" && pkg.dependencies || {};
|
|
17712
|
+
pkg.dependencies = {
|
|
17713
|
+
...deps,
|
|
17714
|
+
[name]: version
|
|
17715
|
+
};
|
|
17716
|
+
writeFileSync(manifest, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
17717
|
+
}
|
|
16279
17718
|
// ../../packages/providers/src/claude/sdk/load.ts
|
|
16280
17719
|
/** Where the SDK comes from in this process. `CODEDEN_CLAUDE_SDK=deps|workspace` overrides. */
|
|
16281
17720
|
function claudeSdkSource() {
|
|
@@ -16293,6 +17732,15 @@ function configureClaudeSdk(opts) {
|
|
|
16293
17732
|
loaded = void 0;
|
|
16294
17733
|
}
|
|
16295
17734
|
/**
|
|
17735
|
+
* True when the SDK can be loaded without installing it first: from the workspace, already
|
|
17736
|
+
* loaded, or already installed in the deps folder. Background work (model discovery) checks
|
|
17737
|
+
* this so it never triggers the on-demand install by itself.
|
|
17738
|
+
*/
|
|
17739
|
+
function claudeSdkReady() {
|
|
17740
|
+
if (claudeSdkSource() === "workspace" || loaded) return true;
|
|
17741
|
+
return depsDir !== void 0 && isClaudeSdkInstalled(depsDir);
|
|
17742
|
+
}
|
|
17743
|
+
/**
|
|
16296
17744
|
* The Claude Agent SDK module, installed first when needed. Failures are not cached, so the
|
|
16297
17745
|
* next session retries the install.
|
|
16298
17746
|
*/
|
|
@@ -17170,6 +18618,10 @@ var ClaudeSession = class {
|
|
|
17170
18618
|
abort;
|
|
17171
18619
|
stderrTail = "";
|
|
17172
18620
|
closed = false;
|
|
18621
|
+
/** The shell's current directory from the latest PreToolUse hook, for relative paths. */
|
|
18622
|
+
shellDir;
|
|
18623
|
+
/** Current autonomy: a query restarted after the CLI exits starts with it. */
|
|
18624
|
+
autonomy;
|
|
17173
18625
|
constructor(opts, config) {
|
|
17174
18626
|
this.opts = opts;
|
|
17175
18627
|
this.config = config;
|
|
@@ -17180,6 +18632,7 @@ var ClaudeSession = class {
|
|
|
17180
18632
|
emit
|
|
17181
18633
|
});
|
|
17182
18634
|
this.broker = new ApprovalBroker(emit);
|
|
18635
|
+
this.autonomy = opts.autonomy;
|
|
17183
18636
|
this.policy = new ClaudePolicy(opts.autonomy, {
|
|
17184
18637
|
cwd: opts.cwd,
|
|
17185
18638
|
extraRoots: opts.additionalDirectories
|
|
@@ -17190,6 +18643,26 @@ var ClaudeSession = class {
|
|
|
17190
18643
|
resumeToken() {
|
|
17191
18644
|
return this.mapper.resumeToken ?? this.opts.resumeToken;
|
|
17192
18645
|
}
|
|
18646
|
+
/**
|
|
18647
|
+
* Switch autonomy. The policy changes at once, so the next tool call (even in a running
|
|
18648
|
+
* turn) follows the new level; approvals already waiting stay waiting. The CLI's own
|
|
18649
|
+
* permission mode follows where it can (`setPermissionMode`); when the CLI refuses a mode
|
|
18650
|
+
* (`bypassPermissions` needs a launch flag), it keeps `acceptEdits` and CodeDen's policy
|
|
18651
|
+
* decides through the PreToolUse hook and `canUseTool` either way. Plan mode is left alone.
|
|
18652
|
+
*/
|
|
18653
|
+
async setAutonomy(level) {
|
|
18654
|
+
this.autonomy = level;
|
|
18655
|
+
this.policy.setAutonomy(level);
|
|
18656
|
+
const q = this.q;
|
|
18657
|
+
if (!q || this.opts.planMode) return;
|
|
18658
|
+
const mode = permissionModeFor(level, false);
|
|
18659
|
+
try {
|
|
18660
|
+
await q.setPermissionMode(mode);
|
|
18661
|
+
} catch {
|
|
18662
|
+
if (mode !== "bypassPermissions") return;
|
|
18663
|
+
await q.setPermissionMode("acceptEdits").catch(() => {});
|
|
18664
|
+
}
|
|
18665
|
+
}
|
|
17193
18666
|
/** Model/effort for the following turns, applied live through the SDK's streaming query. */
|
|
17194
18667
|
async configure(change) {
|
|
17195
18668
|
const q = this.q;
|
|
@@ -17263,13 +18736,15 @@ var ClaudeSession = class {
|
|
|
17263
18736
|
const preToolUse = async (hookInput) => {
|
|
17264
18737
|
const name = str(hookInput, "tool_name") ?? "";
|
|
17265
18738
|
const toolInput = hookInput.tool_input ?? {};
|
|
18739
|
+
const cwd = str(hookInput, "cwd");
|
|
18740
|
+
if (cwd && (path.isAbsolute(cwd) || path.win32.isAbsolute(cwd))) this.shellDir = cwd;
|
|
17266
18741
|
const denied = this.policy.denial(name, toolInput);
|
|
17267
18742
|
if (denied) return { hookSpecificOutput: {
|
|
17268
18743
|
hookEventName: "PreToolUse",
|
|
17269
18744
|
permissionDecision: "deny",
|
|
17270
18745
|
permissionDecisionReason: denied
|
|
17271
18746
|
} };
|
|
17272
|
-
const gate = this.policy.gate(name, toolInput);
|
|
18747
|
+
const gate = this.policy.gate(name, toolInput, this.shellDir);
|
|
17273
18748
|
if (!gate.ask) return {};
|
|
17274
18749
|
return { hookSpecificOutput: {
|
|
17275
18750
|
hookEventName: "PreToolUse",
|
|
@@ -17280,7 +18755,10 @@ var ClaudeSession = class {
|
|
|
17280
18755
|
const q = this.config.query({
|
|
17281
18756
|
prompt: prompts,
|
|
17282
18757
|
options: buildQueryOptions({
|
|
17283
|
-
session:
|
|
18758
|
+
session: {
|
|
18759
|
+
...this.opts,
|
|
18760
|
+
autonomy: this.autonomy
|
|
18761
|
+
},
|
|
17284
18762
|
executable: this.config.executable,
|
|
17285
18763
|
canUseTool: this.canUseTool,
|
|
17286
18764
|
hooks: { PreToolUse: [{ hooks: [preToolUse] }] },
|
|
@@ -17331,7 +18809,7 @@ var ClaudeSession = class {
|
|
|
17331
18809
|
behavior: "deny",
|
|
17332
18810
|
message: denied
|
|
17333
18811
|
};
|
|
17334
|
-
const gate = this.policy.needsUser(toolName, input);
|
|
18812
|
+
const gate = this.policy.needsUser(toolName, input, this.shellDir);
|
|
17335
18813
|
if (!gate.ask) return {
|
|
17336
18814
|
behavior: "allow",
|
|
17337
18815
|
updatedInput: input
|
|
@@ -17437,7 +18915,9 @@ const capabilities$2 = {
|
|
|
17437
18915
|
planMode: true,
|
|
17438
18916
|
reasoningEffort: true,
|
|
17439
18917
|
mcp: true,
|
|
17440
|
-
skills: true
|
|
18918
|
+
skills: true,
|
|
18919
|
+
liveAutonomy: true,
|
|
18920
|
+
readOnly: true
|
|
17441
18921
|
};
|
|
17442
18922
|
const EFFORTS = [
|
|
17443
18923
|
"low",
|
|
@@ -17446,8 +18926,8 @@ const EFFORTS = [
|
|
|
17446
18926
|
"xhigh",
|
|
17447
18927
|
"max"
|
|
17448
18928
|
];
|
|
17449
|
-
/** Fallback model list (CLI aliases) until a session reports the real one. */
|
|
17450
|
-
const FALLBACK_MODELS
|
|
18929
|
+
/** Fallback model list (CLI aliases) until discovery or a session reports the real one. */
|
|
18930
|
+
const FALLBACK_MODELS = [
|
|
17451
18931
|
{
|
|
17452
18932
|
id: "default",
|
|
17453
18933
|
label: "Default",
|
|
@@ -17476,7 +18956,8 @@ var ClaudeAdapter = class {
|
|
|
17476
18956
|
options;
|
|
17477
18957
|
id = CLAUDE_PROVIDER_ID;
|
|
17478
18958
|
displayName = "Claude Code";
|
|
17479
|
-
|
|
18959
|
+
catalog = new ModelCatalog(FALLBACK_MODELS);
|
|
18960
|
+
refreshing;
|
|
17480
18961
|
cachedExe;
|
|
17481
18962
|
constructor(options = {}) {
|
|
17482
18963
|
this.options = options;
|
|
@@ -17493,7 +18974,22 @@ var ClaudeAdapter = class {
|
|
|
17493
18974
|
return { ...capabilities$2 };
|
|
17494
18975
|
}
|
|
17495
18976
|
models() {
|
|
17496
|
-
return this.
|
|
18977
|
+
return this.catalog.list();
|
|
18978
|
+
}
|
|
18979
|
+
onModelsChanged(listener) {
|
|
18980
|
+
return this.catalog.subscribe(listener);
|
|
18981
|
+
}
|
|
18982
|
+
/**
|
|
18983
|
+
* Load Claude Code's model list without a turn (`listClaudeModels`: the CLI starts and
|
|
18984
|
+
* answers `initialize`; no message is sent). Concurrent calls share one discovery; a failed
|
|
18985
|
+
* discovery keeps the last list. Skipped when Claude Code is not installed, or when the SDK
|
|
18986
|
+
* would have to be installed first. Never throws.
|
|
18987
|
+
*/
|
|
18988
|
+
refreshModels() {
|
|
18989
|
+
this.refreshing ??= this.discoverModels().finally(() => {
|
|
18990
|
+
this.refreshing = void 0;
|
|
18991
|
+
});
|
|
18992
|
+
return this.refreshing;
|
|
17497
18993
|
}
|
|
17498
18994
|
async openSession(opts) {
|
|
17499
18995
|
const executable = this.executable();
|
|
@@ -17503,20 +18999,24 @@ var ClaudeAdapter = class {
|
|
|
17503
18999
|
executable,
|
|
17504
19000
|
query,
|
|
17505
19001
|
onModels: (models) => {
|
|
17506
|
-
if (
|
|
17507
|
-
this.modelList = models.map((m, i) => ({
|
|
17508
|
-
id: m.value,
|
|
17509
|
-
label: m.displayName || m.value,
|
|
17510
|
-
...m.description ? { description: m.description } : {},
|
|
17511
|
-
efforts: m.supportedEffortLevels ?? [],
|
|
17512
|
-
...i === 0 ? { isDefault: true } : {}
|
|
17513
|
-
}));
|
|
19002
|
+
if (models.length) this.catalog.set(toModelInfos(models));
|
|
17514
19003
|
}
|
|
17515
19004
|
});
|
|
17516
19005
|
}
|
|
17517
19006
|
utilityModel() {
|
|
17518
19007
|
return new ClaudeUtilityModel(() => this.executable());
|
|
17519
19008
|
}
|
|
19009
|
+
async discoverModels() {
|
|
19010
|
+
const exe = this.executable();
|
|
19011
|
+
if (!exe) return;
|
|
19012
|
+
const load = this.options.loadSdk ?? (claudeSdkReady() ? loadClaudeSdk : void 0);
|
|
19013
|
+
if (!load) return;
|
|
19014
|
+
try {
|
|
19015
|
+
const { query } = await load();
|
|
19016
|
+
const models = await listClaudeModels(query, exe, { timeoutMs: this.options.modelListTimeoutMs });
|
|
19017
|
+
if (models.length) this.catalog.set(models);
|
|
19018
|
+
} catch {}
|
|
19019
|
+
}
|
|
17520
19020
|
};
|
|
17521
19021
|
// ../../packages/providers/src/codex/detect.ts
|
|
17522
19022
|
/** Oldest Codex version whose app-server protocol matches the generated types. */
|
|
@@ -17766,7 +19266,10 @@ const OPT_OUT = [
|
|
|
17766
19266
|
"fuzzyFileSearch/sessionUpdated",
|
|
17767
19267
|
"fuzzyFileSearch/sessionCompleted"
|
|
17768
19268
|
];
|
|
17769
|
-
/**
|
|
19269
|
+
/**
|
|
19270
|
+
* Spawn `codex app-server` (stdio) and complete the `initialize` handshake. Aborting `signal`
|
|
19271
|
+
* stops the process at any point, including during the handshake.
|
|
19272
|
+
*/
|
|
17770
19273
|
async function startAppServer(exe, handlers, opts = {}) {
|
|
17771
19274
|
const rpc = new CodexRpc(spawnResolved(exe, ["app-server"], {
|
|
17772
19275
|
stdio: [
|
|
@@ -17780,6 +19283,10 @@ async function startAppServer(exe, handlers, opts = {}) {
|
|
|
17780
19283
|
},
|
|
17781
19284
|
...opts.cwd ? { cwd: opts.cwd } : {}
|
|
17782
19285
|
}), handlers);
|
|
19286
|
+
if (opts.signal) {
|
|
19287
|
+
if (opts.signal.aborted) rpc.close();
|
|
19288
|
+
else opts.signal.addEventListener("abort", () => void rpc.close(), { once: true });
|
|
19289
|
+
}
|
|
17783
19290
|
const params = {
|
|
17784
19291
|
clientInfo: {
|
|
17785
19292
|
name: "codeden",
|
|
@@ -17793,16 +19300,79 @@ async function startAppServer(exe, handlers, opts = {}) {
|
|
|
17793
19300
|
}
|
|
17794
19301
|
};
|
|
17795
19302
|
try {
|
|
17796
|
-
const info = await rpc.request("initialize", params);
|
|
17797
|
-
rpc.notify("initialized");
|
|
17798
|
-
return {
|
|
17799
|
-
rpc,
|
|
17800
|
-
info
|
|
17801
|
-
};
|
|
17802
|
-
} catch (error) {
|
|
17803
|
-
rpc.close();
|
|
17804
|
-
throw error;
|
|
19303
|
+
const info = await rpc.request("initialize", params);
|
|
19304
|
+
rpc.notify("initialized");
|
|
19305
|
+
return {
|
|
19306
|
+
rpc,
|
|
19307
|
+
info
|
|
19308
|
+
};
|
|
19309
|
+
} catch (error) {
|
|
19310
|
+
rpc.close();
|
|
19311
|
+
throw error;
|
|
19312
|
+
}
|
|
19313
|
+
}
|
|
19314
|
+
/** `model/list` pages are followed up to this many (a guard against a looping cursor). */
|
|
19315
|
+
const MAX_PAGES = 20;
|
|
19316
|
+
/**
|
|
19317
|
+
* Map an app-server `Model` to CodeDen's `ModelInfo`. The id is the model slug Codex accepts
|
|
19318
|
+
* in `thread/start` and `turn/start`. Efforts CodeDen does not know are dropped, and missing
|
|
19319
|
+
* fields (older app-servers) are tolerated.
|
|
19320
|
+
*/
|
|
19321
|
+
function modelInfoFromCodex(m) {
|
|
19322
|
+
const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort).filter((e) => ReasoningEffort.safeParse(e).success);
|
|
19323
|
+
const defaultEffort = ReasoningEffort.safeParse(m.defaultReasoningEffort);
|
|
19324
|
+
return {
|
|
19325
|
+
id: m.model,
|
|
19326
|
+
label: m.displayName || m.model,
|
|
19327
|
+
...m.description ? { description: m.description } : {},
|
|
19328
|
+
efforts,
|
|
19329
|
+
...defaultEffort.success ? { defaultEffort: defaultEffort.data } : {},
|
|
19330
|
+
...m.isDefault ? { isDefault: true } : {}
|
|
19331
|
+
};
|
|
19332
|
+
}
|
|
19333
|
+
/** The models Codex shows in its own picker, as `ModelInfo`. */
|
|
19334
|
+
function visibleCodexModels(models) {
|
|
19335
|
+
return models.filter((m) => !m.hidden && m.model).map(modelInfoFromCodex);
|
|
19336
|
+
}
|
|
19337
|
+
/**
|
|
19338
|
+
* Ask a short-lived `codex app-server` for its model list (`model/list`, every page). No
|
|
19339
|
+
* thread is started and no turn runs. Rejects on failure or after `timeoutMs`; the process
|
|
19340
|
+
* is stopped either way.
|
|
19341
|
+
*/
|
|
19342
|
+
async function listCodexModels(exe, opts = {}) {
|
|
19343
|
+
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
19344
|
+
const abort = new AbortController();
|
|
19345
|
+
let timer;
|
|
19346
|
+
const timeout = new Promise((_, reject) => {
|
|
19347
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`codex app-server did not list models within ${timeoutMs}ms`)), timeoutMs);
|
|
19348
|
+
});
|
|
19349
|
+
try {
|
|
19350
|
+
return await Promise.race([fetchModels(exe, abort.signal), timeout]);
|
|
19351
|
+
} finally {
|
|
19352
|
+
clearTimeout(timer);
|
|
19353
|
+
abort.abort();
|
|
19354
|
+
}
|
|
19355
|
+
}
|
|
19356
|
+
async function fetchModels(exe, signal) {
|
|
19357
|
+
const { rpc } = await startAppServer(exe, {
|
|
19358
|
+
onNotification: () => {},
|
|
19359
|
+
onRequest: async () => {
|
|
19360
|
+
throw new Error("CodeDen's model discovery answers no requests");
|
|
19361
|
+
},
|
|
19362
|
+
onExit: () => {}
|
|
19363
|
+
}, {
|
|
19364
|
+
cwd: tmpdir(),
|
|
19365
|
+
signal
|
|
19366
|
+
});
|
|
19367
|
+
const models = [];
|
|
19368
|
+
let cursor = null;
|
|
19369
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
19370
|
+
const res = await rpc.request("model/list", cursor ? { cursor } : {});
|
|
19371
|
+
models.push(...res.data ?? []);
|
|
19372
|
+
cursor = res.nextCursor ?? null;
|
|
19373
|
+
if (!cursor) break;
|
|
17805
19374
|
}
|
|
19375
|
+
return visibleCodexModels(models);
|
|
17806
19376
|
}
|
|
17807
19377
|
/**
|
|
17808
19378
|
* One-shot Codex call for the utility model: an ephemeral thread in a read-only sandbox with
|
|
@@ -18000,14 +19570,16 @@ async function handleServerRequest(req, ac) {
|
|
|
18000
19570
|
// ../../packages/providers/src/codex/config.ts
|
|
18001
19571
|
/**
|
|
18002
19572
|
* Native sandbox + approval policy per autonomy level (§11.2):
|
|
19573
|
+
* - read_only -> read-only sandbox + `never`: writes fail instead of asking, no network
|
|
18003
19574
|
* - supervised -> `untrusted`: every command that isn't known-safe and every patch asks
|
|
18004
|
-
* - auto -> workspace-write sandbox + `on-request`: the sandbox confines writes
|
|
18005
|
-
*
|
|
19575
|
+
* - auto -> workspace-write sandbox + `on-request`: the sandbox confines writes (to the
|
|
19576
|
+
* worktree, extra roots and the OS temp directory) and network; escaping it
|
|
19577
|
+
* (outside the worktree, network, force pushes) asks
|
|
18006
19578
|
* - full -> workspace-write + `never`, with network on: no approvals, still confined
|
|
18007
|
-
* to the worktree
|
|
19579
|
+
* to the worktree, extra roots and temp
|
|
18008
19580
|
*/
|
|
18009
19581
|
function codexPolicy(autonomy, readOnly = false) {
|
|
18010
|
-
if (readOnly) return {
|
|
19582
|
+
if (readOnly || autonomy === "read_only") return {
|
|
18011
19583
|
approvalPolicy: "never",
|
|
18012
19584
|
sandbox: "read-only",
|
|
18013
19585
|
networkAccess: false
|
|
@@ -18029,13 +19601,41 @@ function codexPolicy(autonomy, readOnly = false) {
|
|
|
18029
19601
|
};
|
|
18030
19602
|
}
|
|
18031
19603
|
/**
|
|
19604
|
+
* Extra writable roots for the workspace-write sandbox: the session's additional directories,
|
|
19605
|
+
* plus the OS temp directories in `auto` and `full`, so writing a log or scratch file there
|
|
19606
|
+
* never needs an approval (§11.2).
|
|
19607
|
+
*/
|
|
19608
|
+
function codexWritableRoots(opts, tempRoots = osTempRoots()) {
|
|
19609
|
+
const temp = opts.readOnly || opts.autonomy === "read_only" || opts.autonomy === "supervised" ? [] : tempRoots;
|
|
19610
|
+
return [.../* @__PURE__ */ new Set([...opts.additionalDirectories, ...temp])];
|
|
19611
|
+
}
|
|
19612
|
+
/**
|
|
19613
|
+
* The `turn/start` sandbox override for an autonomy level: the same sandbox `thread/start`
|
|
19614
|
+
* configures, so a mid-session change applies from the next turn.
|
|
19615
|
+
*/
|
|
19616
|
+
function codexSandboxPolicy(opts, tempRoots = osTempRoots()) {
|
|
19617
|
+
const { sandbox, networkAccess } = codexPolicy(opts.autonomy, opts.readOnly);
|
|
19618
|
+
if (sandbox === "read-only") return {
|
|
19619
|
+
type: "readOnly",
|
|
19620
|
+
networkAccess
|
|
19621
|
+
};
|
|
19622
|
+
return {
|
|
19623
|
+
type: "workspaceWrite",
|
|
19624
|
+
writableRoots: codexWritableRoots(opts, tempRoots),
|
|
19625
|
+
networkAccess,
|
|
19626
|
+
excludeTmpdirEnvVar: false,
|
|
19627
|
+
excludeSlashTmp: false
|
|
19628
|
+
};
|
|
19629
|
+
}
|
|
19630
|
+
/**
|
|
18032
19631
|
* Config overrides for `thread/start` / `thread/resume` (same semantics as `-c key=value`).
|
|
18033
19632
|
* Sent over stdin as JSON, so MCP bearer tokens never appear on a command line.
|
|
18034
19633
|
*/
|
|
18035
|
-
function codexConfigOverrides(opts) {
|
|
19634
|
+
function codexConfigOverrides(opts, tempRoots = osTempRoots()) {
|
|
18036
19635
|
const { networkAccess } = codexPolicy(opts.autonomy, opts.readOnly);
|
|
18037
19636
|
const config = { "sandbox_workspace_write.network_access": networkAccess };
|
|
18038
|
-
|
|
19637
|
+
const roots = codexWritableRoots(opts, tempRoots);
|
|
19638
|
+
if (roots.length) config["sandbox_workspace_write.writable_roots"] = roots;
|
|
18039
19639
|
for (const server of opts.mcpServers) Object.assign(config, mcpOverride(server));
|
|
18040
19640
|
return config;
|
|
18041
19641
|
}
|
|
@@ -18690,10 +20290,14 @@ function rateLimits(snapshot) {
|
|
|
18690
20290
|
return { rateLimits: out };
|
|
18691
20291
|
}
|
|
18692
20292
|
// ../../packages/providers/src/codex/session.ts
|
|
18693
|
-
/**
|
|
18694
|
-
|
|
20293
|
+
/**
|
|
20294
|
+
* CodeDen effort -> Codex reasoning effort. `max` is sent only to models that list it
|
|
20295
|
+
* (`supported`); older models top out at `xhigh`.
|
|
20296
|
+
*/
|
|
20297
|
+
function codexEffort(effort, supported) {
|
|
18695
20298
|
if (!effort) return void 0;
|
|
18696
|
-
|
|
20299
|
+
if (effort !== "max") return effort;
|
|
20300
|
+
return supported?.includes("max") ? "max" : "xhigh";
|
|
18697
20301
|
}
|
|
18698
20302
|
/**
|
|
18699
20303
|
* A Codex session over `codex app-server` (§7.2): one app-server process and one thread per
|
|
@@ -18710,7 +20314,12 @@ var CodexSession = class {
|
|
|
18710
20314
|
turnPending = false;
|
|
18711
20315
|
/** Model/effort changes from `configure`, applied on every following turn. */
|
|
18712
20316
|
nextTurn = {};
|
|
20317
|
+
/** Current autonomy; after `setAutonomy`, every following `turn/start` carries it. */
|
|
20318
|
+
autonomy;
|
|
20319
|
+
autonomyChanged = false;
|
|
18713
20320
|
closed = false;
|
|
20321
|
+
/** The model the thread started with (the user's Codex config when none was chosen). */
|
|
20322
|
+
threadModel;
|
|
18714
20323
|
filePaths = /* @__PURE__ */ new Map();
|
|
18715
20324
|
constructor(opts, config) {
|
|
18716
20325
|
this.opts = opts;
|
|
@@ -18722,6 +20331,7 @@ var CodexSession = class {
|
|
|
18722
20331
|
this.mapper = new CodexEventMapper(opts.sessionId, emit);
|
|
18723
20332
|
this.mapper.codexThreadId = opts.resumeToken;
|
|
18724
20333
|
this.broker = new ApprovalBroker(emit);
|
|
20334
|
+
this.autonomy = opts.autonomy;
|
|
18725
20335
|
}
|
|
18726
20336
|
/** Start the app-server and the thread. Called by the adapter before returning the session. */
|
|
18727
20337
|
async start() {
|
|
@@ -18741,19 +20351,29 @@ var CodexSession = class {
|
|
|
18741
20351
|
...change
|
|
18742
20352
|
};
|
|
18743
20353
|
}
|
|
20354
|
+
/**
|
|
20355
|
+
* Autonomy from the next `turn/start` on, as approval-policy and sandbox overrides. A running
|
|
20356
|
+
* turn keeps the policy it started with (the app-server fixes it per turn).
|
|
20357
|
+
*/
|
|
20358
|
+
async setAutonomy(level) {
|
|
20359
|
+
this.autonomy = level;
|
|
20360
|
+
this.autonomyChanged = true;
|
|
20361
|
+
}
|
|
18744
20362
|
async send(input) {
|
|
18745
20363
|
if (this.closed) throw new ProviderSessionError("closed", "Session is closed");
|
|
18746
20364
|
if (this.turnPending || this.mapper.currentTurnId) throw new ProviderSessionError("turn_running", "A turn is already running");
|
|
18747
20365
|
this.turnPending = true;
|
|
18748
20366
|
try {
|
|
18749
20367
|
const rpc = await this.ensureServer();
|
|
18750
|
-
const effort = codexEffort(this.nextTurn.effort !== void 0 ? this.nextTurn.effort ?? void 0 : this.opts.effort);
|
|
18751
20368
|
const model = await this.turnModel(rpc);
|
|
20369
|
+
const inUse = model ?? explicitModel(this.opts.model) ?? this.threadModel;
|
|
20370
|
+
const effort = codexEffort(this.nextTurn.effort !== void 0 ? this.nextTurn.effort ?? void 0 : this.opts.effort, inUse ? this.config.modelEfforts?.(inUse) : void 0);
|
|
18752
20371
|
const res = await rpc.request("turn/start", {
|
|
18753
20372
|
threadId: this.threadId(),
|
|
18754
20373
|
input: toCodexInput(input),
|
|
18755
20374
|
...effort ? { effort } : {},
|
|
18756
|
-
...model !== void 0 ? { model } : {}
|
|
20375
|
+
...model !== void 0 ? { model } : {},
|
|
20376
|
+
...this.autonomyChanged ? this.policyOverride() : {}
|
|
18757
20377
|
});
|
|
18758
20378
|
this.mapper.beginTurn(res.turn.id);
|
|
18759
20379
|
} catch (error) {
|
|
@@ -18795,6 +20415,21 @@ var CodexSession = class {
|
|
|
18795
20415
|
this.events.close();
|
|
18796
20416
|
await this.rpc?.close();
|
|
18797
20417
|
}
|
|
20418
|
+
/** Session options with the current autonomy (a restarted app-server resumes with it). */
|
|
20419
|
+
currentOptions() {
|
|
20420
|
+
return {
|
|
20421
|
+
...this.opts,
|
|
20422
|
+
autonomy: this.autonomy
|
|
20423
|
+
};
|
|
20424
|
+
}
|
|
20425
|
+
/** `turn/start` overrides that carry the current autonomy (§11.2). */
|
|
20426
|
+
policyOverride() {
|
|
20427
|
+
const opts = this.currentOptions();
|
|
20428
|
+
return {
|
|
20429
|
+
approvalPolicy: codexPolicy(opts.autonomy, opts.readOnly).approvalPolicy,
|
|
20430
|
+
sandboxPolicy: codexSandboxPolicy(opts)
|
|
20431
|
+
};
|
|
20432
|
+
}
|
|
18798
20433
|
threadId() {
|
|
18799
20434
|
const id = this.mapper.codexThreadId;
|
|
18800
20435
|
if (!id) throw new ProviderSessionError("not_started", "Codex thread is not started");
|
|
@@ -18828,12 +20463,13 @@ var CodexSession = class {
|
|
|
18828
20463
|
cwd: this.opts.cwd
|
|
18829
20464
|
});
|
|
18830
20465
|
this.rpc = rpc;
|
|
18831
|
-
const
|
|
20466
|
+
const opts = this.currentOptions();
|
|
20467
|
+
const { approvalPolicy, sandbox } = codexPolicy(opts.autonomy, opts.readOnly);
|
|
18832
20468
|
const common = {
|
|
18833
20469
|
cwd: this.opts.cwd,
|
|
18834
20470
|
approvalPolicy,
|
|
18835
20471
|
sandbox,
|
|
18836
|
-
config: codexConfigOverrides(
|
|
20472
|
+
config: codexConfigOverrides(opts),
|
|
18837
20473
|
...explicitModel(this.opts.model) ? { model: explicitModel(this.opts.model) } : {},
|
|
18838
20474
|
...this.opts.systemPrompt ? { developerInstructions: this.opts.systemPrompt } : {}
|
|
18839
20475
|
};
|
|
@@ -18860,6 +20496,7 @@ var CodexSession = class {
|
|
|
18860
20496
|
}
|
|
18861
20497
|
res ??= await rpc.request("thread/start", common);
|
|
18862
20498
|
this.mapper.codexThreadId = res.thread.id;
|
|
20499
|
+
this.threadModel = res.model || void 0;
|
|
18863
20500
|
this.events.push({
|
|
18864
20501
|
type: "session.updated",
|
|
18865
20502
|
resumeToken: res.thread.id,
|
|
@@ -18940,40 +20577,39 @@ const capabilities$1 = {
|
|
|
18940
20577
|
planMode: false,
|
|
18941
20578
|
reasoningEffort: true,
|
|
18942
20579
|
mcp: true,
|
|
18943
|
-
skills: true
|
|
20580
|
+
skills: true,
|
|
20581
|
+
liveAutonomy: false,
|
|
20582
|
+
readOnly: true
|
|
18944
20583
|
};
|
|
18945
|
-
const
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
"
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
label: m.displayName || m.model,
|
|
18965
|
-
...m.description ? { description: m.description } : {},
|
|
18966
|
-
efforts,
|
|
18967
|
-
...defaultEffort.success ? { defaultEffort: defaultEffort.data } : {},
|
|
18968
|
-
...m.isDefault ? { isDefault: true } : {}
|
|
18969
|
-
};
|
|
20584
|
+
const CONFIGURED_MODEL = "The model configured in ~/.codex/config.toml";
|
|
20585
|
+
/**
|
|
20586
|
+
* Until Codex has listed its models, one entry that leaves the choice to the user's Codex
|
|
20587
|
+
* config. `note` says why the real list is missing when discovery failed.
|
|
20588
|
+
*/
|
|
20589
|
+
function fallbackModels(note) {
|
|
20590
|
+
return [{
|
|
20591
|
+
id: DEFAULT_MODEL_ID,
|
|
20592
|
+
label: "Default",
|
|
20593
|
+
description: note ? `${CONFIGURED_MODEL}. ${note}` : CONFIGURED_MODEL,
|
|
20594
|
+
efforts: [
|
|
20595
|
+
"minimal",
|
|
20596
|
+
"low",
|
|
20597
|
+
"medium",
|
|
20598
|
+
"high",
|
|
20599
|
+
"xhigh"
|
|
20600
|
+
],
|
|
20601
|
+
isDefault: true
|
|
20602
|
+
}];
|
|
18970
20603
|
}
|
|
18971
20604
|
/** Codex through `codex app-server` JSON-RPC (§7.2). */
|
|
18972
20605
|
var CodexAdapter = class {
|
|
18973
20606
|
options;
|
|
18974
20607
|
id = CODEX_PROVIDER_ID;
|
|
18975
20608
|
displayName = "Codex";
|
|
18976
|
-
|
|
20609
|
+
catalog = new ModelCatalog(fallbackModels());
|
|
20610
|
+
/** True once Codex reported a real model list (discovery or a session). */
|
|
20611
|
+
listed = false;
|
|
20612
|
+
refreshing;
|
|
18977
20613
|
cachedExe;
|
|
18978
20614
|
constructor(options = {}) {
|
|
18979
20615
|
this.options = options;
|
|
@@ -18990,7 +20626,20 @@ var CodexAdapter = class {
|
|
|
18990
20626
|
return { ...capabilities$1 };
|
|
18991
20627
|
}
|
|
18992
20628
|
models() {
|
|
18993
|
-
return this.
|
|
20629
|
+
return this.catalog.list();
|
|
20630
|
+
}
|
|
20631
|
+
onModelsChanged(listener) {
|
|
20632
|
+
return this.catalog.subscribe(listener);
|
|
20633
|
+
}
|
|
20634
|
+
/**
|
|
20635
|
+
* Load Codex's model list through a short-lived `codex app-server` (`model/list`; no thread,
|
|
20636
|
+
* no turn). Concurrent calls share one discovery. Never throws.
|
|
20637
|
+
*/
|
|
20638
|
+
refreshModels() {
|
|
20639
|
+
this.refreshing ??= this.discoverModels().finally(() => {
|
|
20640
|
+
this.refreshing = void 0;
|
|
20641
|
+
});
|
|
20642
|
+
return this.refreshing;
|
|
18994
20643
|
}
|
|
18995
20644
|
async openSession(opts) {
|
|
18996
20645
|
const executable = this.executable();
|
|
@@ -18998,9 +20647,10 @@ var CodexAdapter = class {
|
|
|
18998
20647
|
const session = new CodexSession(opts, {
|
|
18999
20648
|
executable,
|
|
19000
20649
|
onModels: (models) => {
|
|
19001
|
-
const visible = models
|
|
19002
|
-
if (visible.length) this.
|
|
19003
|
-
}
|
|
20650
|
+
const visible = visibleCodexModels(models);
|
|
20651
|
+
if (visible.length) this.setModels(visible);
|
|
20652
|
+
},
|
|
20653
|
+
modelEfforts: (model) => this.catalog.list().find((m) => m.id === model)?.efforts
|
|
19004
20654
|
});
|
|
19005
20655
|
try {
|
|
19006
20656
|
await session.start();
|
|
@@ -19013,7 +20663,65 @@ var CodexAdapter = class {
|
|
|
19013
20663
|
utilityModel() {
|
|
19014
20664
|
return new CodexUtilityModel(() => this.executable());
|
|
19015
20665
|
}
|
|
20666
|
+
async discoverModels() {
|
|
20667
|
+
const exe = this.executable();
|
|
20668
|
+
if (!exe) return;
|
|
20669
|
+
try {
|
|
20670
|
+
const models = await listCodexModels(exe, { timeoutMs: this.options.modelListTimeoutMs });
|
|
20671
|
+
if (models.length) this.setModels(models);
|
|
20672
|
+
else this.discoveryFailed("Codex listed no models.");
|
|
20673
|
+
} catch (error) {
|
|
20674
|
+
const reason = (error instanceof Error ? error.message : String(error)).slice(0, 200);
|
|
20675
|
+
this.discoveryFailed(`Codex's model list could not be loaded: ${reason}`);
|
|
20676
|
+
}
|
|
20677
|
+
}
|
|
20678
|
+
setModels(models) {
|
|
20679
|
+
this.listed = true;
|
|
20680
|
+
this.catalog.set(models);
|
|
20681
|
+
}
|
|
20682
|
+
/** Keep the last real list; without one, say why only the configured default is offered. */
|
|
20683
|
+
discoveryFailed(note) {
|
|
20684
|
+
if (!this.listed) this.catalog.set(fallbackModels(note));
|
|
20685
|
+
}
|
|
20686
|
+
};
|
|
20687
|
+
// ../../packages/providers/src/fake/mcp-result.ts
|
|
20688
|
+
/** A long, unbroken token, like the ids, hashes and base64 blobs real tool results carry. */
|
|
20689
|
+
const blob = "c0de".repeat(12) + "d3n".repeat(60);
|
|
20690
|
+
/** Input of the scripted `[mcp]` tool call: an issue reference with a long unbroken URL. */
|
|
20691
|
+
const fakeMcpInput = {
|
|
20692
|
+
issue: {
|
|
20693
|
+
number: 3,
|
|
20694
|
+
url: `https://github.com/codeden-e2e/demo-repo/issues/3?trace=${blob}`
|
|
20695
|
+
},
|
|
20696
|
+
priority: "low"
|
|
19016
20697
|
};
|
|
20698
|
+
/**
|
|
20699
|
+
* Result text of the scripted `[mcp]` tool call: one compact line of JSON, several kilobytes
|
|
20700
|
+
* long, shaped like a triage result. Lets tests check that long tool output stays inside the
|
|
20701
|
+
* transcript column.
|
|
20702
|
+
*/
|
|
20703
|
+
function fakeMcpResult() {
|
|
20704
|
+
const body = Array.from({ length: 12 }, (_, i) => `Step ${i + 1}: list the models the provider reports before a session starts.`).join(" ");
|
|
20705
|
+
return JSON.stringify({ issue: {
|
|
20706
|
+
projectId: "a72f9074-7b37-4693-bbae-8b108576aefc",
|
|
20707
|
+
number: 3,
|
|
20708
|
+
title: "Discover the real model list without running a session first",
|
|
20709
|
+
state: "open",
|
|
20710
|
+
url: `https://github.com/codeden-e2e/demo-repo/issues/3?trace=${blob}`,
|
|
20711
|
+
labels: [
|
|
20712
|
+
"enhancement",
|
|
20713
|
+
"providers",
|
|
20714
|
+
"low-priority"
|
|
20715
|
+
],
|
|
20716
|
+
body,
|
|
20717
|
+
trace: blob,
|
|
20718
|
+
blockedBy: [{
|
|
20719
|
+
number: 2,
|
|
20720
|
+
title: "Model picker redesign",
|
|
20721
|
+
state: "open"
|
|
20722
|
+
}]
|
|
20723
|
+
} });
|
|
20724
|
+
}
|
|
19017
20725
|
// ../../packages/providers/src/fake/session.ts
|
|
19018
20726
|
var Interrupted$1 = class extends Error {};
|
|
19019
20727
|
/** A scripted, deterministic session. Ids are counters scoped to the session. */
|
|
@@ -19029,6 +20737,7 @@ var FakeSession = class {
|
|
|
19029
20737
|
constructor(opts, fake) {
|
|
19030
20738
|
this.opts = opts;
|
|
19031
20739
|
this.fake = fake;
|
|
20740
|
+
this.autonomy = opts.autonomy;
|
|
19032
20741
|
this.token = opts.resumeToken ?? `fake-${opts.sessionId}`;
|
|
19033
20742
|
this.rootThreadId = `${opts.sessionId}:thread-root`;
|
|
19034
20743
|
this.events.push({
|
|
@@ -19056,6 +20765,11 @@ var FakeSession = class {
|
|
|
19056
20765
|
model: change.model ?? "fake-1"
|
|
19057
20766
|
});
|
|
19058
20767
|
}
|
|
20768
|
+
/** Current autonomy. Full skips the scripted command approval, as it would for real. */
|
|
20769
|
+
autonomy;
|
|
20770
|
+
async setAutonomy(level) {
|
|
20771
|
+
this.autonomy = level;
|
|
20772
|
+
}
|
|
19059
20773
|
async send(input) {
|
|
19060
20774
|
if (this.events.isClosed) throw new ProviderSessionError("closed", "Session is closed");
|
|
19061
20775
|
if (this.turn) throw new ProviderSessionError("turn_running", "A turn is already running");
|
|
@@ -19137,7 +20851,9 @@ var FakeSession = class {
|
|
|
19137
20851
|
}
|
|
19138
20852
|
await this.streamAssistant(turn, threadId, this.replyFor(prompt));
|
|
19139
20853
|
await this.todoList(turn);
|
|
19140
|
-
if (
|
|
20854
|
+
if (prompt.includes("[mcp]")) await this.mcpToolCall(turn);
|
|
20855
|
+
const readOnly = this.autonomy === "read_only";
|
|
20856
|
+
if ((!readOnly && (this.fake.approval || prompt.includes("[approval]")) && this.autonomy !== "full" ? await this.approval(turn) : true) && !readOnly) await this.commandToolCall(turn);
|
|
19141
20857
|
if (this.fake.subagent || prompt.includes("[subagent]")) await this.subagent(turn);
|
|
19142
20858
|
if (this.fake.followups ?? true) {
|
|
19143
20859
|
await this.step(turn);
|
|
@@ -19322,6 +21038,45 @@ var FakeSession = class {
|
|
|
19322
21038
|
});
|
|
19323
21039
|
return allowed;
|
|
19324
21040
|
}
|
|
21041
|
+
/** An MCP tool call with a long, one-line JSON result (`[mcp]` in the prompt). */
|
|
21042
|
+
async mcpToolCall(turn) {
|
|
21043
|
+
const { threadId, turnId } = turn;
|
|
21044
|
+
const base = {
|
|
21045
|
+
id: this.nextId("item"),
|
|
21046
|
+
kind: "tool_call",
|
|
21047
|
+
toolKind: "mcp",
|
|
21048
|
+
toolName: "mcp__codeden__triage_issue",
|
|
21049
|
+
server: "codeden",
|
|
21050
|
+
tool: "triage_issue",
|
|
21051
|
+
title: "codeden: triage_issue",
|
|
21052
|
+
input: fakeMcpInput
|
|
21053
|
+
};
|
|
21054
|
+
await this.step(turn);
|
|
21055
|
+
this.emit({
|
|
21056
|
+
type: "item.started",
|
|
21057
|
+
threadId,
|
|
21058
|
+
turnId,
|
|
21059
|
+
item: {
|
|
21060
|
+
...base,
|
|
21061
|
+
status: "in_progress"
|
|
21062
|
+
}
|
|
21063
|
+
});
|
|
21064
|
+
await this.step(turn);
|
|
21065
|
+
this.emit({
|
|
21066
|
+
type: "item.completed",
|
|
21067
|
+
threadId,
|
|
21068
|
+
turnId,
|
|
21069
|
+
item: {
|
|
21070
|
+
...base,
|
|
21071
|
+
status: "completed",
|
|
21072
|
+
output: {
|
|
21073
|
+
text: fakeMcpResult(),
|
|
21074
|
+
truncated: false
|
|
21075
|
+
},
|
|
21076
|
+
durationMs: 12
|
|
21077
|
+
}
|
|
21078
|
+
});
|
|
21079
|
+
}
|
|
19325
21080
|
async commandToolCall(turn) {
|
|
19326
21081
|
const { threadId, turnId } = turn;
|
|
19327
21082
|
const id = this.nextId("item");
|
|
@@ -19490,13 +21245,16 @@ const capabilities = {
|
|
|
19490
21245
|
planMode: false,
|
|
19491
21246
|
reasoningEffort: false,
|
|
19492
21247
|
mcp: false,
|
|
19493
|
-
skills: false
|
|
21248
|
+
skills: false,
|
|
21249
|
+
liveAutonomy: true,
|
|
21250
|
+
readOnly: true
|
|
19494
21251
|
};
|
|
19495
21252
|
/**
|
|
19496
21253
|
* Deterministic scripted provider for demos and tests (§7.2). Every turn streams:
|
|
19497
21254
|
* user message, assistant message (word deltas), a todo list, a command tool call,
|
|
19498
21255
|
* optionally an approval request (waits for `respond`) and a subagent child thread,
|
|
19499
|
-
* followups, usage, then `turn.completed`. Prompts containing `[fail]` fail the turn
|
|
21256
|
+
* followups, usage, then `turn.completed`. Prompts containing `[fail]` fail the turn;
|
|
21257
|
+
* `[mcp]` adds an MCP tool call with a long JSON result before the command.
|
|
19500
21258
|
*/
|
|
19501
21259
|
var FakeProvider = class {
|
|
19502
21260
|
options;
|
|
@@ -20449,12 +22207,14 @@ function createImageEngineRegistry(opts = {}) {
|
|
|
20449
22207
|
/** The set of adapters a runner has loaded, keyed by id. */
|
|
20450
22208
|
var ProviderRegistry = class {
|
|
20451
22209
|
adapters = /* @__PURE__ */ new Map();
|
|
22210
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20452
22211
|
constructor(adapters = []) {
|
|
20453
22212
|
for (const adapter of adapters) this.register(adapter);
|
|
20454
22213
|
}
|
|
20455
22214
|
register(adapter) {
|
|
20456
22215
|
if (this.adapters.has(adapter.id)) throw new Error(`Provider "${adapter.id}" is already registered`);
|
|
20457
22216
|
this.adapters.set(adapter.id, adapter);
|
|
22217
|
+
adapter.onModelsChanged?.(() => this.notify());
|
|
20458
22218
|
}
|
|
20459
22219
|
get(id) {
|
|
20460
22220
|
return this.adapters.get(id);
|
|
@@ -20465,6 +22225,24 @@ var ProviderRegistry = class {
|
|
|
20465
22225
|
info() {
|
|
20466
22226
|
return this.list().map(providerInfo);
|
|
20467
22227
|
}
|
|
22228
|
+
/**
|
|
22229
|
+
* Refresh the model lists of all adapters (or those in `ids`) that support it. Never throws;
|
|
22230
|
+
* changes reach `onChange` listeners.
|
|
22231
|
+
*/
|
|
22232
|
+
async refreshModels(ids) {
|
|
22233
|
+
const adapters = ids ? this.list().filter((a) => ids.includes(a.id)) : this.list();
|
|
22234
|
+
await Promise.all(adapters.map((a) => a.refreshModels?.().catch(() => void 0)));
|
|
22235
|
+
}
|
|
22236
|
+
/** Call `listener` whenever `info()` changes (a provider's models). Returns unsubscribe. */
|
|
22237
|
+
onChange(listener) {
|
|
22238
|
+
this.listeners.add(listener);
|
|
22239
|
+
return () => this.listeners.delete(listener);
|
|
22240
|
+
}
|
|
22241
|
+
notify() {
|
|
22242
|
+
for (const listener of this.listeners) try {
|
|
22243
|
+
listener();
|
|
22244
|
+
} catch {}
|
|
22245
|
+
}
|
|
20468
22246
|
};
|
|
20469
22247
|
/** Fresh instances of the built-in real providers (Claude Code, Codex). */
|
|
20470
22248
|
function builtinProviders() {
|
|
@@ -23983,7 +25761,8 @@ function installChromium(log) {
|
|
|
23983
25761
|
"chromium"
|
|
23984
25762
|
], {
|
|
23985
25763
|
windowsHide: true,
|
|
23986
|
-
timeout: 6e5
|
|
25764
|
+
timeout: 6e5,
|
|
25765
|
+
env: ownNodeEnv()
|
|
23987
25766
|
}, (error) => error ? reject(error) : resolve());
|
|
23988
25767
|
});
|
|
23989
25768
|
}
|
|
@@ -24022,13 +25801,18 @@ function firstLine(error) {
|
|
|
24022
25801
|
/** Names of CodeDen-internal variables that must never reach agents, shells or setup scripts. */
|
|
24023
25802
|
const SECRET = /^CODEDEN_.*(TOKEN|SECRET|KEY|PASSWORD)/i;
|
|
24024
25803
|
/**
|
|
25804
|
+
* Set when the desktop app runs CodeDen's own processes on its Electron binary (§17). It must
|
|
25805
|
+
* not reach the user's shells and agents: an Electron app started there would run as Node.js.
|
|
25806
|
+
*/
|
|
25807
|
+
const RUNTIME_ONLY = /* @__PURE__ */ new Set(["ELECTRON_RUN_AS_NODE"]);
|
|
25808
|
+
/**
|
|
24025
25809
|
* Environment for child processes (§15): the user's environment minus CodeDen's own
|
|
24026
25810
|
* secrets. `CODEDEN_HOME` and similar non-secret variables are kept.
|
|
24027
25811
|
*/
|
|
24028
25812
|
function childEnv(extra = {}) {
|
|
24029
25813
|
const out = {};
|
|
24030
25814
|
for (const [key, value] of Object.entries(process.env)) {
|
|
24031
|
-
if (value === void 0 || SECRET.test(key)) continue;
|
|
25815
|
+
if (value === void 0 || SECRET.test(key) || RUNTIME_ONLY.has(key)) continue;
|
|
24032
25816
|
out[key] = value;
|
|
24033
25817
|
}
|
|
24034
25818
|
return {
|
|
@@ -24345,6 +26129,22 @@ async function topLevel(cwd) {
|
|
|
24345
26129
|
return resolve(await gitText(cwd, ["rev-parse", "--show-toplevel"]));
|
|
24346
26130
|
}
|
|
24347
26131
|
/**
|
|
26132
|
+
* Copy the index for use as a temporary index, keeping its modification time. Git trusts an
|
|
26133
|
+
* entry's cached stat data only when the entry is older than the index file; an entry written
|
|
26134
|
+
* in the same timestamp tick as the index is "racily clean" and its content is re-read. A
|
|
26135
|
+
* copy stamped with the current time (Linux `copyFile` does that) would make such entries
|
|
26136
|
+
* look safe, and a same-size rewrite in that tick would be missed by diffs and checkpoints.
|
|
26137
|
+
*/
|
|
26138
|
+
async function copyIndex(realIndex, tmp) {
|
|
26139
|
+
try {
|
|
26140
|
+
await copyFile(realIndex, tmp);
|
|
26141
|
+
} catch {
|
|
26142
|
+
return;
|
|
26143
|
+
}
|
|
26144
|
+
const { atime, mtime } = await stat(realIndex);
|
|
26145
|
+
await utimes(tmp, atime, mtime);
|
|
26146
|
+
}
|
|
26147
|
+
/**
|
|
24348
26148
|
* Write the full working tree (tracked + untracked, minus ignored files) as a tree object
|
|
24349
26149
|
* using a temporary index seeded from a copy of the real one (for its stat cache). The
|
|
24350
26150
|
* user's index, HEAD and branch are never modified.
|
|
@@ -24359,7 +26159,7 @@ async function snapshotTree(cwd) {
|
|
|
24359
26159
|
]);
|
|
24360
26160
|
const tmp = join(tmpdir(), `codeden-index-${randomBytes(8).toString("hex")}`);
|
|
24361
26161
|
try {
|
|
24362
|
-
await
|
|
26162
|
+
await copyIndex(realIndex, tmp);
|
|
24363
26163
|
const env = { GIT_INDEX_FILE: tmp };
|
|
24364
26164
|
await git(top, [
|
|
24365
26165
|
"add",
|
|
@@ -24768,6 +26568,303 @@ async function listPorts(probe = []) {
|
|
|
24768
26568
|
url: `http://localhost:${port}`
|
|
24769
26569
|
}));
|
|
24770
26570
|
}
|
|
26571
|
+
// ../runner/src/util/paths.ts
|
|
26572
|
+
/** True when `child` is `dir` itself or somewhere inside it. */
|
|
26573
|
+
function isInside(dir, child) {
|
|
26574
|
+
const rel = relative(resolve(dir), resolve(child));
|
|
26575
|
+
return rel === "" || !rel.startsWith("..") && !/^[a-zA-Z]:/.test(rel);
|
|
26576
|
+
}
|
|
26577
|
+
// ../runner/src/workspace/delete-tree.ts
|
|
26578
|
+
/** Error codes Windows returns while another process still holds a file or folder open. */
|
|
26579
|
+
const IN_USE = /* @__PURE__ */ new Set([
|
|
26580
|
+
"EBUSY",
|
|
26581
|
+
"EPERM",
|
|
26582
|
+
"EACCES",
|
|
26583
|
+
"ENOTEMPTY"
|
|
26584
|
+
]);
|
|
26585
|
+
const errorCode = (error) => error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
26586
|
+
/** Run `op`, retrying briefly (about 1.5s for 5 attempts) while the path is in use. */
|
|
26587
|
+
async function retry(op, attempts = 5) {
|
|
26588
|
+
for (let attempt = 1;; attempt++) try {
|
|
26589
|
+
await op();
|
|
26590
|
+
return;
|
|
26591
|
+
} catch (error) {
|
|
26592
|
+
const code = errorCode(error);
|
|
26593
|
+
if (code === "ENOENT") return;
|
|
26594
|
+
if (!code || !IN_USE.has(code) || attempt >= attempts) throw error;
|
|
26595
|
+
await new Promise((r) => setTimeout(r, 100 * attempt));
|
|
26596
|
+
}
|
|
26597
|
+
}
|
|
26598
|
+
/** Delete a file or link (never what a link points at); clears the read-only flag if needed. */
|
|
26599
|
+
function removeFile(path) {
|
|
26600
|
+
return retry(async () => {
|
|
26601
|
+
try {
|
|
26602
|
+
await unlink(path);
|
|
26603
|
+
} catch (error) {
|
|
26604
|
+
if (errorCode(error) !== "EPERM" && errorCode(error) !== "EACCES") throw error;
|
|
26605
|
+
await chmod(path, 438).catch(() => void 0);
|
|
26606
|
+
await unlink(path);
|
|
26607
|
+
}
|
|
26608
|
+
});
|
|
26609
|
+
}
|
|
26610
|
+
async function kind(path, entry) {
|
|
26611
|
+
if (entry.isSymbolicLink()) return "other";
|
|
26612
|
+
if (entry.isDirectory()) return "dir";
|
|
26613
|
+
if (entry.isFile()) return "other";
|
|
26614
|
+
const stat = await lstat(path).catch(() => void 0);
|
|
26615
|
+
return stat?.isDirectory() && !stat.isSymbolicLink() ? "dir" : "other";
|
|
26616
|
+
}
|
|
26617
|
+
/**
|
|
26618
|
+
* Empty `dir`: files and links are unlinked, real folders are emptied and removed. A symlink
|
|
26619
|
+
* or junction is never descended into (Windows reports junctions as links), so nothing
|
|
26620
|
+
* outside the tree is touched. Every branch is finished before the first failure is thrown.
|
|
26621
|
+
*/
|
|
26622
|
+
async function emptyDir(dir) {
|
|
26623
|
+
let entries;
|
|
26624
|
+
try {
|
|
26625
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
26626
|
+
} catch (error) {
|
|
26627
|
+
if (errorCode(error) === "ENOENT") return;
|
|
26628
|
+
throw error;
|
|
26629
|
+
}
|
|
26630
|
+
const failed = (await Promise.allSettled(entries.map(async (entry) => {
|
|
26631
|
+
const path = join(dir, entry.name);
|
|
26632
|
+
if (await kind(path, entry) !== "dir") return removeFile(path);
|
|
26633
|
+
await emptyDir(path);
|
|
26634
|
+
await retry(() => rmdir(path));
|
|
26635
|
+
}))).find((r) => r.status === "rejected");
|
|
26636
|
+
if (failed) throw failed.reason;
|
|
26637
|
+
}
|
|
26638
|
+
/** A short, user-facing error for a failed delete or move of the worktree folder `root`. */
|
|
26639
|
+
function deleteFailed(root, error) {
|
|
26640
|
+
const file = error && typeof error === "object" && "path" in error ? String(error.path) : root;
|
|
26641
|
+
const where = relative(root, file);
|
|
26642
|
+
const code = errorCode(error);
|
|
26643
|
+
if (code && IN_USE.has(code)) return new CommandError("worktree_in_use", `${where ? `A file in the worktree is in use (${where})` : "The worktree is in use"}. Close whatever is using it (a terminal, dev server or editor), then try again.`);
|
|
26644
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
26645
|
+
return new CommandError("remove_failed", `Could not delete ${where || root}: ${reason}`);
|
|
26646
|
+
}
|
|
26647
|
+
/**
|
|
26648
|
+
* Delete a worktree folder without ever following a symlink or junction out of it.
|
|
26649
|
+
*
|
|
26650
|
+
* `git worktree remove` can't be trusted with this on Windows: Git treats junctions as
|
|
26651
|
+
* folders, so it deletes the contents of whatever a live junction points at, and it stops at
|
|
26652
|
+
* a dangling one with "Directory not empty". In a pnpm install the packages are deleted before
|
|
26653
|
+
* the junctions that point at them, so it fails halfway, after removing `.git`. Node's
|
|
26654
|
+
* recursive `rm` is no better here: on Windows it waits for as long as a file stays locked.
|
|
26655
|
+
*
|
|
26656
|
+
* `.git` goes first, so a delete that a locked file interrupts leaves an orphan folder the
|
|
26657
|
+
* next attempt cleans up (never a damaged worktree that looks dirty). Each locked entry is
|
|
26658
|
+
* retried for about 1.5s, then this throws `worktree_in_use`.
|
|
26659
|
+
*/
|
|
26660
|
+
async function deleteTree(root) {
|
|
26661
|
+
try {
|
|
26662
|
+
const stat = await lstat(root).catch(() => void 0);
|
|
26663
|
+
if (!stat) return;
|
|
26664
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return await removeFile(root);
|
|
26665
|
+
await removeFile(join(root, ".git"));
|
|
26666
|
+
await emptyDir(root);
|
|
26667
|
+
await retry(() => rmdir(root));
|
|
26668
|
+
} catch (error) {
|
|
26669
|
+
throw deleteFailed(root, error);
|
|
26670
|
+
}
|
|
26671
|
+
}
|
|
26672
|
+
// ../runner/src/workspace/trash.ts
|
|
26673
|
+
/**
|
|
26674
|
+
* True for a session folder CodeDen created and owns: exactly `<home>/wt/<project>/<session>`.
|
|
26675
|
+
* Only these may be deleted once they are no longer git worktrees.
|
|
26676
|
+
*/
|
|
26677
|
+
function isManagedWorktreePath(home, path) {
|
|
26678
|
+
const rel = relative(resolve(home, "wt"), resolve(path));
|
|
26679
|
+
if (!rel || isAbsolute(rel)) return false;
|
|
26680
|
+
const parts = rel.split(/[\\/]+/);
|
|
26681
|
+
return parts.length === 2 && !parts.includes("..") && !parts.includes(".") && parts[0] !== ".trash";
|
|
26682
|
+
}
|
|
26683
|
+
/** Where removed worktrees wait to be deleted. `.trash` can never be a project slug. */
|
|
26684
|
+
function trashDir(home) {
|
|
26685
|
+
return join(resolve(home), "wt", ".trash");
|
|
26686
|
+
}
|
|
26687
|
+
/**
|
|
26688
|
+
* Move a session folder into the trash. A rename is all or nothing: while anything in the
|
|
26689
|
+
* folder is held open (a process's working directory, an open file), Windows refuses it and
|
|
26690
|
+
* the folder stays exactly as it was (`worktree_in_use`). Returns null when the trash is on
|
|
26691
|
+
* another volume.
|
|
26692
|
+
*/
|
|
26693
|
+
async function moveToTrash(home, path) {
|
|
26694
|
+
const trash = trashDir(home);
|
|
26695
|
+
await mkdir(trash, { recursive: true });
|
|
26696
|
+
const stamp = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
26697
|
+
const dest = join(trash, `${basename(dirname(path))}-${basename(path)}-${stamp}`);
|
|
26698
|
+
try {
|
|
26699
|
+
await retry(() => rename(path, dest), 8);
|
|
26700
|
+
return dest;
|
|
26701
|
+
} catch (error) {
|
|
26702
|
+
if (errorCode(error) === "EXDEV") return null;
|
|
26703
|
+
throw deleteFailed(path, error);
|
|
26704
|
+
}
|
|
26705
|
+
}
|
|
26706
|
+
const runs = /* @__PURE__ */ new Map();
|
|
26707
|
+
/**
|
|
26708
|
+
* Delete everything in the trash, one folder at a time, never following a link (the links in
|
|
26709
|
+
* a trashed pnpm install still point at the old path, where a new worktree may live by now).
|
|
26710
|
+
* Runs in the background after each removal and when the runner starts; a folder something
|
|
26711
|
+
* still holds open stays for the next run. Calls during a run share it and add one more pass.
|
|
26712
|
+
*/
|
|
26713
|
+
function emptyTrash(home, log) {
|
|
26714
|
+
const trash = trashDir(home);
|
|
26715
|
+
const current = runs.get(trash);
|
|
26716
|
+
if (current) {
|
|
26717
|
+
current.again = true;
|
|
26718
|
+
return current.done;
|
|
26719
|
+
}
|
|
26720
|
+
const run = {
|
|
26721
|
+
again: true,
|
|
26722
|
+
done: Promise.resolve()
|
|
26723
|
+
};
|
|
26724
|
+
run.done = (async () => {
|
|
26725
|
+
try {
|
|
26726
|
+
while (run.again) {
|
|
26727
|
+
run.again = false;
|
|
26728
|
+
const entries = await readdir(trash).catch(() => []);
|
|
26729
|
+
for (const name of entries) await deleteTree(join(trash, name)).catch((error) => log?.(`worktree trash: ${error instanceof Error ? error.message : String(error)}`));
|
|
26730
|
+
}
|
|
26731
|
+
} finally {
|
|
26732
|
+
runs.delete(trash);
|
|
26733
|
+
}
|
|
26734
|
+
})();
|
|
26735
|
+
runs.set(trash, run);
|
|
26736
|
+
return run.done;
|
|
26737
|
+
}
|
|
26738
|
+
/**
|
|
26739
|
+
* Delete a worktree folder that has been cleared for removal. A session folder under
|
|
26740
|
+
* `<home>/wt` goes to the trash, so the removal is instant and the path is free right away,
|
|
26741
|
+
* and is deleted in the background. Anything else is deleted in place.
|
|
26742
|
+
*/
|
|
26743
|
+
async function discardFolder(home, path, log) {
|
|
26744
|
+
if (isManagedWorktreePath(home, path) ? await moveToTrash(home, path) : null) emptyTrash(home, log);
|
|
26745
|
+
else await deleteTree(path);
|
|
26746
|
+
}
|
|
26747
|
+
// ../runner/src/workspace/removal.ts
|
|
26748
|
+
async function canonical(path) {
|
|
26749
|
+
const real = await realpath(path).catch(() => resolve(path));
|
|
26750
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
26751
|
+
}
|
|
26752
|
+
async function listWorktrees(repo) {
|
|
26753
|
+
const out = await gitText(repo, [
|
|
26754
|
+
"worktree",
|
|
26755
|
+
"list",
|
|
26756
|
+
"--porcelain"
|
|
26757
|
+
]).catch(() => "");
|
|
26758
|
+
const list = [];
|
|
26759
|
+
for (const block of out.split(/\r?\n\r?\n/)) {
|
|
26760
|
+
const lines = block.split(/\r?\n/);
|
|
26761
|
+
if (!lines[0]?.startsWith("worktree ")) continue;
|
|
26762
|
+
const locked = lines.some((l) => l === "locked" || l.startsWith("locked "));
|
|
26763
|
+
list.push({
|
|
26764
|
+
path: lines[0].slice(9),
|
|
26765
|
+
locked
|
|
26766
|
+
});
|
|
26767
|
+
}
|
|
26768
|
+
return list;
|
|
26769
|
+
}
|
|
26770
|
+
/** Classify `path` against the worktrees of `repo` (see `WorktreeState`). */
|
|
26771
|
+
async function worktreeState(repo, path) {
|
|
26772
|
+
const target = await canonical(path);
|
|
26773
|
+
let registered = false;
|
|
26774
|
+
let locked = false;
|
|
26775
|
+
let containsOther = false;
|
|
26776
|
+
for (const wt of await listWorktrees(repo)) {
|
|
26777
|
+
const other = await canonical(wt.path);
|
|
26778
|
+
if (other === target) {
|
|
26779
|
+
registered = true;
|
|
26780
|
+
locked = wt.locked;
|
|
26781
|
+
} else if (isInside(target, other)) containsOther = true;
|
|
26782
|
+
}
|
|
26783
|
+
if (isInside(target, await canonical(repo))) containsOther = true;
|
|
26784
|
+
const info = {
|
|
26785
|
+
registered,
|
|
26786
|
+
locked,
|
|
26787
|
+
containsOther
|
|
26788
|
+
};
|
|
26789
|
+
const stat = await lstat(path).catch(() => void 0);
|
|
26790
|
+
if (!stat) return {
|
|
26791
|
+
state: "missing",
|
|
26792
|
+
...info
|
|
26793
|
+
};
|
|
26794
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return {
|
|
26795
|
+
state: "orphan",
|
|
26796
|
+
...info
|
|
26797
|
+
};
|
|
26798
|
+
const dotGit = await lstat(resolve(path, ".git")).catch(() => void 0);
|
|
26799
|
+
if (!dotGit) return {
|
|
26800
|
+
state: "orphan",
|
|
26801
|
+
...info
|
|
26802
|
+
};
|
|
26803
|
+
return {
|
|
26804
|
+
state: dotGit.isFile() && registered ? "registered" : "foreign",
|
|
26805
|
+
...info
|
|
26806
|
+
};
|
|
26807
|
+
}
|
|
26808
|
+
/** Whether a registered worktree has uncommitted changes (ignored files don't count). */
|
|
26809
|
+
async function isDirty(path) {
|
|
26810
|
+
return (await git(path, ["status", "--porcelain"])).stdout.toString("utf8").trim() !== "";
|
|
26811
|
+
}
|
|
26812
|
+
/**
|
|
26813
|
+
* Decide whether `workspace.remove` may delete the path, before anything is touched (terminals
|
|
26814
|
+
* and processes in it are stopped only after this passes). Refuses (`not_a_worktree`) anything
|
|
26815
|
+
* that isn't a linked worktree of the repository, unless it is an orphan inside CodeDen's
|
|
26816
|
+
* worktree folder, and anything containing the repository or another worktree. Unless forced,
|
|
26817
|
+
* keeps a locked worktree (`worktree_locked`) or one with uncommitted changes
|
|
26818
|
+
* (`worktree_dirty`); ignored files (build output, `node_modules`) don't count.
|
|
26819
|
+
*/
|
|
26820
|
+
async function assertRemovable(cmd, home) {
|
|
26821
|
+
const path = resolve(cmd.path);
|
|
26822
|
+
const info = await worktreeState(cmd.repoPath, path);
|
|
26823
|
+
const leftAlone = (why) => new CommandError("not_a_worktree", `${path} ${why}, so it was left alone`);
|
|
26824
|
+
if (info.state === "missing") return info;
|
|
26825
|
+
if (info.containsOther) throw leftAlone("contains the repository or another worktree");
|
|
26826
|
+
if (info.state === "foreign") throw leftAlone("is not a worktree of this repository");
|
|
26827
|
+
if (info.state === "orphan" && !isManagedWorktreePath(home, path)) throw leftAlone("is no longer a git worktree and is outside CodeDen's worktree folder");
|
|
26828
|
+
if (info.state !== "registered" || cmd.force) return info;
|
|
26829
|
+
if (info.locked) throw new CommandError("worktree_locked", `${path} is locked; unlock it or remove it with force`);
|
|
26830
|
+
if (await isDirty(path)) throw new CommandError("worktree_dirty", `${path} has uncommitted changes; commit them or remove it with force`);
|
|
26831
|
+
return info;
|
|
26832
|
+
}
|
|
26833
|
+
/** Drop git's records of worktrees whose folders are gone. */
|
|
26834
|
+
async function pruneWorktrees(repo) {
|
|
26835
|
+
await git(repo, ["worktree", "prune"]).catch(() => void 0);
|
|
26836
|
+
}
|
|
26837
|
+
/**
|
|
26838
|
+
* `workspace.remove`: delete a session worktree and git's record of it. Idempotent: a missing
|
|
26839
|
+
* path, or a half-deleted orphan inside `<home>/wt`, ends removed too. The folder goes to the
|
|
26840
|
+
* trash or is deleted in place, never through a link (`discardFolder`), then
|
|
26841
|
+
* `git worktree prune` drops the record. The branch stays unless `deleteBranch` names it.
|
|
26842
|
+
*/
|
|
26843
|
+
async function removeWorkspace(cmd, home, log) {
|
|
26844
|
+
const path = resolve(cmd.path);
|
|
26845
|
+
const info = await assertRemovable(cmd, home);
|
|
26846
|
+
const registered = info.state === "registered";
|
|
26847
|
+
const discardedChanges = registered && cmd.force === true && await isDirty(path);
|
|
26848
|
+
try {
|
|
26849
|
+
if (registered && info.locked) await git(cmd.repoPath, [
|
|
26850
|
+
"worktree",
|
|
26851
|
+
"unlock",
|
|
26852
|
+
path
|
|
26853
|
+
]).catch(() => void 0);
|
|
26854
|
+
if (info.state !== "missing") await discardFolder(home, path, log);
|
|
26855
|
+
} finally {
|
|
26856
|
+
await pruneWorktrees(cmd.repoPath);
|
|
26857
|
+
}
|
|
26858
|
+
if (cmd.deleteBranch) await git(cmd.repoPath, [
|
|
26859
|
+
"branch",
|
|
26860
|
+
"-D",
|
|
26861
|
+
cmd.deleteBranch
|
|
26862
|
+
]).catch(() => void 0);
|
|
26863
|
+
return {
|
|
26864
|
+
removed: true,
|
|
26865
|
+
...discardedChanges ? { discardedChanges } : {}
|
|
26866
|
+
};
|
|
26867
|
+
}
|
|
24771
26868
|
// ../runner/src/workspace/project-config.ts
|
|
24772
26869
|
const PROJECT_CONFIG_PATH = ".codeden/project.json";
|
|
24773
26870
|
/**
|
|
@@ -24910,20 +27007,41 @@ async function resolveBase(repo, cmd) {
|
|
|
24910
27007
|
])) return `origin/${def}`;
|
|
24911
27008
|
return def ?? "HEAD";
|
|
24912
27009
|
}
|
|
24913
|
-
|
|
27010
|
+
function isEmptyDir(path) {
|
|
27011
|
+
try {
|
|
27012
|
+
return readdirSync(path).length === 0;
|
|
27013
|
+
} catch {
|
|
27014
|
+
return false;
|
|
27015
|
+
}
|
|
27016
|
+
}
|
|
27017
|
+
/**
|
|
27018
|
+
* Make `path` ready for `git worktree add`: reuse a worktree of this repository, delete the
|
|
27019
|
+
* orphan folder an interrupted removal left inside `<home>/wt` (stopping processes in it
|
|
27020
|
+
* first), and drop git's stale record of it. Returns the worktree to reuse, if any.
|
|
27021
|
+
*/
|
|
27022
|
+
async function clearTarget(repo, path, ctx) {
|
|
27023
|
+
const info = await worktreeState(repo, path);
|
|
27024
|
+
if (info.state === "registered") return {
|
|
27025
|
+
cwd: path,
|
|
27026
|
+
branch: await currentBranch(path),
|
|
27027
|
+
created: false
|
|
27028
|
+
};
|
|
27029
|
+
if (info.state === "orphan" && isManagedWorktreePath(ctx.home, path) && !info.containsOther) {
|
|
27030
|
+
await ctx.supervisor.killUnder(path);
|
|
27031
|
+
await discardFolder(ctx.home, path);
|
|
27032
|
+
} else if (info.state !== "missing" && !isEmptyDir(path)) throw new CommandError("path_exists", `${path} exists and is not a worktree`);
|
|
27033
|
+
if (info.registered) await pruneWorktrees(repo);
|
|
27034
|
+
return null;
|
|
27035
|
+
}
|
|
27036
|
+
async function createWorktree(cmd, ctx) {
|
|
24914
27037
|
const repo = await topLevel(cmd.repoPath).catch(() => {
|
|
24915
27038
|
throw new CommandError("not_a_repo", `${cmd.repoPath} is not a git repository`);
|
|
24916
27039
|
});
|
|
24917
27040
|
const short = sessionShort(cmd.sessionId);
|
|
24918
|
-
const
|
|
24919
|
-
|
|
24920
|
-
|
|
24921
|
-
|
|
24922
|
-
branch: await currentBranch(path),
|
|
24923
|
-
created: false
|
|
24924
|
-
};
|
|
24925
|
-
if (readdirSync(path).length > 0) throw new CommandError("path_exists", `${path} exists and is not a worktree`);
|
|
24926
|
-
}
|
|
27041
|
+
const slug = cmd.projectSlug ?? slugFromPath(repo);
|
|
27042
|
+
const path = resolve(cmd.path ?? join(ctx.home, "wt", slug, short));
|
|
27043
|
+
const reuse = await clearTarget(repo, path, ctx);
|
|
27044
|
+
if (reuse) return reuse;
|
|
24927
27045
|
await mkdir(dirname(path), { recursive: true });
|
|
24928
27046
|
const base = await resolveBase(repo, cmd);
|
|
24929
27047
|
if (cmd.branch && await branchExists(repo, cmd.branch)) {
|
|
@@ -24983,7 +27101,7 @@ async function prepareWorkspace(cmd, ctx) {
|
|
|
24983
27101
|
created: false
|
|
24984
27102
|
};
|
|
24985
27103
|
break;
|
|
24986
|
-
case "worktree": target = await createWorktree(cmd, ctx
|
|
27104
|
+
case "worktree": target = await createWorktree(cmd, ctx);
|
|
24987
27105
|
}
|
|
24988
27106
|
const { config, error } = await readProjectConfig(target.cwd);
|
|
24989
27107
|
if (error) ctx.emit({
|
|
@@ -25008,44 +27126,6 @@ async function prepareWorkspace(cmd, ctx) {
|
|
|
25008
27126
|
setup
|
|
25009
27127
|
};
|
|
25010
27128
|
}
|
|
25011
|
-
/** `workspace.remove`: remove a worktree (retrying briefly for Windows file locks). */
|
|
25012
|
-
async function removeWorkspace(cmd) {
|
|
25013
|
-
const path = resolve(cmd.path);
|
|
25014
|
-
let removed = false;
|
|
25015
|
-
let lastError;
|
|
25016
|
-
for (let attempt = 0; attempt < 5 && !removed; attempt++) try {
|
|
25017
|
-
const args = [
|
|
25018
|
-
"worktree",
|
|
25019
|
-
"remove",
|
|
25020
|
-
...cmd.force ? ["--force"] : [],
|
|
25021
|
-
path
|
|
25022
|
-
];
|
|
25023
|
-
await git(cmd.repoPath, args);
|
|
25024
|
-
removed = true;
|
|
25025
|
-
} catch (error) {
|
|
25026
|
-
lastError = error;
|
|
25027
|
-
if (!existsSync(path)) break;
|
|
25028
|
-
if (!cmd.force && /modified or untracked|contains/.test(String(error))) break;
|
|
25029
|
-
await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
|
|
25030
|
-
}
|
|
25031
|
-
if (!removed && existsSync(path) && cmd.force) {
|
|
25032
|
-
await rm(path, {
|
|
25033
|
-
recursive: true,
|
|
25034
|
-
force: true,
|
|
25035
|
-
maxRetries: 5,
|
|
25036
|
-
retryDelay: 200
|
|
25037
|
-
});
|
|
25038
|
-
removed = true;
|
|
25039
|
-
}
|
|
25040
|
-
await git(cmd.repoPath, ["worktree", "prune"]).catch(() => void 0);
|
|
25041
|
-
if (!removed && existsSync(path)) throw lastError instanceof Error ? lastError : new CommandError("remove_failed", `Could not remove ${path}`);
|
|
25042
|
-
if (cmd.deleteBranch) await git(cmd.repoPath, [
|
|
25043
|
-
"branch",
|
|
25044
|
-
"-D",
|
|
25045
|
-
cmd.deleteBranch
|
|
25046
|
-
]).catch(() => void 0);
|
|
25047
|
-
return { removed: removed || !existsSync(path) };
|
|
25048
|
-
}
|
|
25049
27129
|
// ../runner/src/commands.ts
|
|
25050
27130
|
/** Execute one server command (§4.3). Throws `CommandError` for expected failures. */
|
|
25051
27131
|
async function executeCommand(core, command) {
|
|
@@ -25069,7 +27149,9 @@ async function executeCommand(core, command) {
|
|
|
25069
27149
|
return {};
|
|
25070
27150
|
case "provider.detect": {
|
|
25071
27151
|
const adapters = command.providerId ? registry.list().filter((a) => a.id === command.providerId) : registry.list();
|
|
25072
|
-
|
|
27152
|
+
const statuses = await Promise.all(adapters.map((a) => a.detect()));
|
|
27153
|
+
registry.refreshModels(adapters.map((a) => a.id));
|
|
27154
|
+
return { statuses };
|
|
25073
27155
|
}
|
|
25074
27156
|
case "provider.oneShot": {
|
|
25075
27157
|
const utility = registry.get(command.providerId)?.utilityModel?.();
|
|
@@ -25093,9 +27175,10 @@ async function executeCommand(core, command) {
|
|
|
25093
27175
|
emit: core.emit
|
|
25094
27176
|
});
|
|
25095
27177
|
case "workspace.remove":
|
|
27178
|
+
await assertRemovable(command, core.home);
|
|
25096
27179
|
await terminals.closeUnder(command.path);
|
|
25097
27180
|
await core.supervisor.killUnder(command.path);
|
|
25098
|
-
return removeWorkspace(command);
|
|
27181
|
+
return removeWorkspace(command, core.home, core.log);
|
|
25099
27182
|
case "session.start":
|
|
25100
27183
|
if (core.isDraining()) throw new CommandError("draining", "Runner is draining");
|
|
25101
27184
|
return host.start(command);
|
|
@@ -25373,12 +27456,6 @@ async function handleMcpRequest(request, upstream) {
|
|
|
25373
27456
|
return jsonRpcError(502, `CodeDen server unreachable: ${error.message}`);
|
|
25374
27457
|
}
|
|
25375
27458
|
}
|
|
25376
|
-
// ../runner/src/util/paths.ts
|
|
25377
|
-
/** True when `child` is `dir` itself or somewhere inside it. */
|
|
25378
|
-
function isInside(dir, child) {
|
|
25379
|
-
const rel = relative(resolve(dir), resolve(child));
|
|
25380
|
-
return rel === "" || !rel.startsWith("..") && !/^[a-zA-Z]:/.test(rel);
|
|
25381
|
-
}
|
|
25382
27459
|
// ../runner/src/process/supervisor.ts
|
|
25383
27460
|
/**
|
|
25384
27461
|
* Tracks every child process the runner starts so they can be killed as whole process
|
|
@@ -26808,6 +28885,10 @@ var ServerConnection = class {
|
|
|
26808
28885
|
for (const event of this.core.journal.after(msg.lastAckedSeq)) this.send(event);
|
|
26809
28886
|
this.attached = true;
|
|
26810
28887
|
this.onAttach(this);
|
|
28888
|
+
this.send({
|
|
28889
|
+
type: "providers",
|
|
28890
|
+
providers: this.core.registry.info()
|
|
28891
|
+
});
|
|
26811
28892
|
return;
|
|
26812
28893
|
case "ack":
|
|
26813
28894
|
this.core.journal.ack(msg.seq);
|
|
@@ -26967,14 +29048,20 @@ var SessionHost = class {
|
|
|
26967
29048
|
async interrupt(sessionId) {
|
|
26968
29049
|
await this.get(sessionId).session.interrupt();
|
|
26969
29050
|
}
|
|
26970
|
-
/**
|
|
29051
|
+
/**
|
|
29052
|
+
* Model/effort for the next turn and the autonomy level, when the provider can change them
|
|
29053
|
+
* live. Checks every requested setting before applying any.
|
|
29054
|
+
*/
|
|
26971
29055
|
async configure(cmd) {
|
|
26972
29056
|
const { session } = this.get(cmd.sessionId);
|
|
26973
|
-
if (!session.configure) throw new CommandError("unsupported", "This provider cannot change model or effort live");
|
|
26974
29057
|
const change = {};
|
|
26975
29058
|
if (cmd.model !== void 0) change.model = cmd.model;
|
|
26976
29059
|
if (cmd.effort !== void 0) change.effort = cmd.effort;
|
|
26977
|
-
|
|
29060
|
+
const modelOrEffort = Object.keys(change).length > 0;
|
|
29061
|
+
if (modelOrEffort && !session.configure) throw new CommandError("unsupported", "This provider cannot change model or effort live");
|
|
29062
|
+
if (cmd.autonomy !== void 0 && !session.setAutonomy) throw new CommandError("unsupported", "This provider cannot change autonomy live");
|
|
29063
|
+
if (modelOrEffort) await session.configure?.(change);
|
|
29064
|
+
if (cmd.autonomy !== void 0) await session.setAutonomy?.(cmd.autonomy);
|
|
26978
29065
|
}
|
|
26979
29066
|
async respond(cmd) {
|
|
26980
29067
|
await this.get(cmd.sessionId).session.respond(cmd.requestId, cmd.response);
|
|
@@ -27141,6 +29228,12 @@ async function startRunner(opts) {
|
|
|
27141
29228
|
stream: data
|
|
27142
29229
|
});
|
|
27143
29230
|
};
|
|
29231
|
+
const stopProviderUpdates = registry.onChange(() => {
|
|
29232
|
+
if (!stopped) attached?.deliver({
|
|
29233
|
+
type: "providers",
|
|
29234
|
+
providers: registry.info()
|
|
29235
|
+
});
|
|
29236
|
+
});
|
|
27144
29237
|
const supervisor = new ProcessSupervisor();
|
|
27145
29238
|
const host = new SessionHost(registry, emit, { log });
|
|
27146
29239
|
const terminals = new TerminalHost({
|
|
@@ -27165,6 +29258,7 @@ async function startRunner(opts) {
|
|
|
27165
29258
|
browser,
|
|
27166
29259
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27167
29260
|
emit,
|
|
29261
|
+
log,
|
|
27168
29262
|
isDraining: () => draining,
|
|
27169
29263
|
drain: ({ reason, timeoutMs, exitWhenDrained }) => {
|
|
27170
29264
|
if (!draining) {
|
|
@@ -27237,11 +29331,14 @@ async function startRunner(opts) {
|
|
|
27237
29331
|
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}\n`, { mode: 384 });
|
|
27238
29332
|
renameSync(tmp, lockFile);
|
|
27239
29333
|
log(`runner ${journal.runnerId} listening on 127.0.0.1:${port}`);
|
|
29334
|
+
registry.refreshModels();
|
|
29335
|
+
emptyTrash(opts.home, log);
|
|
27240
29336
|
const removeLock = () => removeOwnLock(lockFile);
|
|
27241
29337
|
process.once("exit", removeLock);
|
|
27242
29338
|
async function close() {
|
|
27243
29339
|
closing ??= (async () => {
|
|
27244
29340
|
removeLock();
|
|
29341
|
+
stopProviderUpdates();
|
|
27245
29342
|
await host.closeAll();
|
|
27246
29343
|
await terminals.closeAll();
|
|
27247
29344
|
await browserManager.shutdown().catch((error) => log(`browser: ${String(error)}`));
|
|
@@ -27266,6 +29363,6 @@ async function startRunner(opts) {
|
|
|
27266
29363
|
close
|
|
27267
29364
|
};
|
|
27268
29365
|
}
|
|
27269
|
-
export {
|
|
29366
|
+
export { ExportAutomationsQuery as $, datetime as $n, TriageIssueRequest as $t, SessionFileQuery as A, SessionOptions as An, UiClientMessage as At, ImageTestRequest as B, InboxItemKind as Bn, UpdateInstallRequest as Bt, isElectronRuntime as C, GitPushRequest as Cn, SecretKey as Ct, CODEDEN_VERSION as D, TerminalClientMessage as Dn, StartSessionRequest as Dt, wrapper_default as E, SpeechSummaryRequest as En, SetSecretRequest as Et, ListConflictsQuery as F, Project as Fn, LOCAL_STT_SAMPLE_RATE as Ft, BrowserNavigateRequest as G, AgentConfigLocator as Gn, NOTICE_BATCH_CODE as Gt, BrowserClientMessage as H, AgentConfigDeleteQuery as Hn, compareVersions$1 as Ht, ListNoticesQuery as I, sortProjects as In, LocalSttModelParams as It, BrowserViewportRequest as J, McpServerTarget as Jn, GithubIssue as Jt, BrowserNewTabRequest as K, AgentConfigWriteRequest as Kn, CreateGithubIssueRequest as Kt, ResolveConflictRequest as L, DEFAULT_VISION_PATH as Ln, SttQuery as Lt, RunnerToServerMessage as M, ConflictDecision as Mn, HealthResponse as Mt, ServerLock as N, StartedBy as Nn, agentEventToNewEvent as Nt, UsageQuery as O, TerminalQuery as On, SteerRequest as Ot, ArchiveSessionRequest as P, WorkKey as Pn, Settings as Pt, DuplicateAutomationRequest as Q, ReasoningEffort as Qn, MergePrRequest as Qt, RoleMessageRequest as R, FactoryLimits as Rn, TtsRequest as Rt, acquireInstallLock as S, GitCommitRequest as Sn, RespondRequest as St, import_websocket_server as T, SessionResult as Tn, SessionStartMeta as Tt, BrowserDevServerRequest as U, AgentConfigDiffRequest as Un, isNewerVersion as Ut, summarizeActivity as V, messageFrom as Vn, UpdateStatus as Vt, BrowserHistoryRequest as W, AgentConfigListQuery as Wn, ConflictRecord as Wt, AUTOMATION_IMPORT_LIMITS as X, AutonomyLevel as Xn, GithubListQuery as Xt, BrowserWsQuery as Y, McpServerUpsertRequest as Yn, GithubLabelRequest as Yt, AutomationImportSource as Z, ProviderId as Zn, GithubPr as Zt, importClaudeSdkFrom as _, TriagePolicy as _n, unknown as _r, FileWriteRequest as _t, Hono as a, FactoryTuning as an, discriminatedUnion as ar, ListAutomationRunsQuery as at, runNpm as b, DiffScope as bn, safeParse$1 as br, ReorderProjectsRequest as bt, serve as c, ReviewVerdict as cn, literal as cr, SetAutomationEnabledRequest as ct, createImageEngineRegistry as d, Artifact as dn, object as dr, TriageReport as dt, githubPrId as en, _enum as er, ImportAutomationsRequest as et, IMAGE_ASPECT_RATIOS as f, Automation as fn, optional as fr, UpcomingRunsQuery as ft, CLAUDE_SDK_VERSION as g, AutomationTrigger as gn, union as gr, FileReadQuery as gt, claudeSdkSource as h, AutomationRun as hn, string as hr, CreateProjectRequest as ht, killTree as i, FactoryTriageRequest as in, custom as ir, CreateAutomationRequest as it, RunnerLock as j, ProjectConfigFile as jn, UpdateProjectRequest as jt, UsageSettings as k, RunnerCommandResults as kn, ThreadItemsQuery as kt, upgradeWebSocket as l, SetAutonomyModeRequest as ln, looseObject as lr, StarterSetRequest as lt, ImageGenerationError as m, AutomationGuards as mn, record as mr, UpdateAutomationRequest as mt, readLock as n, DiscoveryArea as nn, array as nr, AutomationGuardsPatch as nt, tryDecodeURIComponent as o, KillSwitchRequest as on, int as or, ListAutomationsQuery as ot, resolveImageOutput as p, AutomationAction as pn, preprocess as pr, UpdateAutomationProjectSettingsRequest as pt, BrowserOpenRequest as q, McpServerDiffRequest as qn, GithubCommentRequest as qt, isAlive as r, FactoryCandidate as rn, boolean as rr, AutomationProjectRequest as rt, bufferToFormData as s, PutVisionRequest as sn, intersection as sr, RunAutomationRequest as st, startRunner as t, parseGithubPrId as tn, _null as tr, AutomationFileFrontmatter as tt, ImageEngineRegistry as u, isFactoryMode as un, number$1 as ur, TriageDecisionRequest as ut, installClaudeSdk as v, Session as vn, ZodError as vr, ListInboxQuery as vt, ownNodeEnv as w, RestoreFilesRequest as wn, SendMessageRequest as wt, resolveNpm as x, EditQueuedMessageRequest as xn, prettifyError as xr, ResolveInboxRequest as xt, isClaudeSdkInstalled as y, SessionStatus as yn, toJSONSchema as yr, ListSessionsQuery as yt, UpdateSessionRequest as z, InboxItem as zn, AppPointer as zt };
|
|
27270
29367
|
|
|
27271
|
-
//# sourceMappingURL=runner-
|
|
29368
|
+
//# sourceMappingURL=runner-DGomZl80.js.map
|