codeden 0.1.1 → 0.2.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.
Files changed (36) hide show
  1. package/README.md +7 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/drizzle/20261108041036_session_worktree_cleanup/migration.sql +2 -0
  4. package/dist/drizzle/20261108041036_session_worktree_cleanup/snapshot.json +3424 -0
  5. package/dist/drizzle/20261108045102_project_pin_order/migration.sql +4 -0
  6. package/dist/drizzle/20261108045102_project_pin_order/snapshot.json +3444 -0
  7. package/dist/drizzle/20261108052259_queued_message_sender/migration.sql +1 -0
  8. package/dist/drizzle/20261108052259_queued_message_sender/snapshot.json +3454 -0
  9. package/dist/{main-DaGFUbbW.js → main-CzmG0d8t.js} +2 -2
  10. package/dist/{main-DaGFUbbW.js.map → main-CzmG0d8t.js.map} +1 -1
  11. package/dist/{main-BF0aQ9U8.js → main-pXwL7GDQ.js} +1555 -149
  12. package/dist/main-pXwL7GDQ.js.map +1 -0
  13. package/dist/{runner-D8m3WdtS.js → runner-CJetH9jW.js} +1947 -389
  14. package/dist/runner-CJetH9jW.js.map +1 -0
  15. package/dist/runner.js +1 -1
  16. package/dist/web/assets/{core-BmMheb4i.js → core-BeOBW-Wr.js} +1 -1
  17. package/dist/web/assets/engine-oniguruma-BLpLufIT.js +1 -0
  18. package/dist/web/assets/{highlighted-body-JLMTFACH-nUCrIe8P.js → highlighted-body-JLMTFACH-D-L40tHI.js} +1 -1
  19. package/dist/web/assets/{highlights-CdGBlIcw.js → highlights-ZCqssNA3.js} +1 -1
  20. package/dist/web/assets/index-D8qaPF_E.css +1 -0
  21. package/dist/web/assets/index-DR-I5EC5.js +892 -0
  22. package/dist/web/assets/langs-DKjK0GUN.js +1 -0
  23. package/dist/web/assets/mermaid-WFAXDIZX-fZTzmfWq.js +1 -0
  24. package/dist/web/assets/{shiki-D8HIISPT.js → shiki-CWXnlX2q.js} +2 -2
  25. package/dist/web/assets/{stream-tokenizer-BmWmYTv_.js → stream-tokenizer-BpK1CZzW.js} +1 -1
  26. package/dist/web/assets/{themes-CKsIQs7_.js → themes-Bm7B3n10.js} +2 -2
  27. package/dist/web/favicon.svg +1 -1
  28. package/dist/web/index.html +2 -2
  29. package/package.json +1 -1
  30. package/dist/main-BF0aQ9U8.js.map +0 -1
  31. package/dist/runner-D8m3WdtS.js.map +0 -1
  32. package/dist/web/assets/engine-oniguruma-CVzqqUIi.js +0 -1
  33. package/dist/web/assets/index-DKWZjMF1.js +0 -885
  34. package/dist/web/assets/index-DfoaM4E6.css +0 -1
  35. package/dist/web/assets/langs-kfQh6fa2.js +0 -1
  36. 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 { copyFile, mkdir, readFile, rm, rmdir, stat, 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;
@@ -6833,6 +6834,17 @@ const projectBase = {
6833
6834
  killSwitch: boolean().default(false),
6834
6835
  /** IANA timezone for cron automations; machine timezone when null. */
6835
6836
  timezone: string().nullable(),
6837
+ /**
6838
+ * Pinned projects are listed above the rest (§13). Defaults to false so events written
6839
+ * before pinning existed still parse.
6840
+ */
6841
+ pinned: boolean().default(false),
6842
+ /**
6843
+ * Manual position in the project list, lowest first (§13). Only the relative order within
6844
+ * the pinned and unpinned groups matters; ties fall back to the name. Set by
6845
+ * `projects.reordered`; archived projects keep theirs.
6846
+ */
6847
+ sortOrder: int().nonnegative().default(0),
6836
6848
  createdAt: Timestamp,
6837
6849
  updatedAt: Timestamp,
6838
6850
  archivedAt: Timestamp.nullable()
@@ -6856,6 +6868,20 @@ const Project = discriminatedUnion("kind", [object({
6856
6868
  ...projectBase,
6857
6869
  kind: literal("local")
6858
6870
  })]);
6871
+ /**
6872
+ * The canonical project order (§13): pinned first, then `sortOrder`, then name, then id.
6873
+ * The server lists projects in this order and the UI keeps it when applying live events.
6874
+ */
6875
+ function compareProjects(a, b) {
6876
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
6877
+ if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
6878
+ if (a.name !== b.name) return a.name < b.name ? -1 : 1;
6879
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
6880
+ }
6881
+ /** A sorted copy of `projects` in the canonical order. */
6882
+ function sortProjects(projects) {
6883
+ return [...projects].sort(compareProjects);
6884
+ }
6859
6885
  // ../../packages/contracts/src/work.ts
6860
6886
  /**
6861
6887
  * Situational awareness and conflict handling (§8.4). Every unit of work (a session or an
@@ -7013,7 +7039,12 @@ const ProviderCapabilities = object({
7013
7039
  reasoningEffort: boolean(),
7014
7040
  /** MCP servers can be injected (required for CodeDen tools). */
7015
7041
  mcp: boolean(),
7016
- skills: boolean()
7042
+ skills: boolean(),
7043
+ /**
7044
+ * An autonomy change reaches a running turn from its next tool call. When false (or absent),
7045
+ * a running turn keeps its autonomy and the change applies from the next turn.
7046
+ */
7047
+ liveAutonomy: boolean().optional()
7017
7048
  });
7018
7049
  /** A model a provider offers. */
7019
7050
  const ModelInfo = object({
@@ -7532,7 +7563,8 @@ const RunnerCommand = discriminatedUnion("name", [
7532
7563
  name: literal("session.configure"),
7533
7564
  sessionId: Id,
7534
7565
  model: string().nullable().optional(),
7535
- effort: ReasoningEffort.nullable().optional()
7566
+ effort: ReasoningEffort.nullable().optional(),
7567
+ autonomy: AutonomyLevel.optional()
7536
7568
  }),
7537
7569
  object({
7538
7570
  name: literal("pty.open"),
@@ -7780,8 +7812,23 @@ const QueuedMessage = object({
7780
7812
  id: Id,
7781
7813
  sessionId: Id,
7782
7814
  input: UserInput,
7815
+ /** Who queued it. Events written before senders were recorded were all the user's. */
7816
+ from: _enum([
7817
+ "user",
7818
+ "orchestrator",
7819
+ "pm",
7820
+ "automation",
7821
+ "codeden"
7822
+ ]).default("user"),
7783
7823
  createdAt: Timestamp
7784
7824
  });
7825
+ object({ messages: array(QueuedMessage) });
7826
+ /**
7827
+ * `PATCH /api/sessions/:id/queue/:messageId`: replace a queued message's text (attachments
7828
+ * stay). Answers 409 `already_sent` once delivery has taken the message.
7829
+ */
7830
+ const EditQueuedMessageRequest = object({ text: string().max(2e5) });
7831
+ object({ status: literal("steered") });
7785
7832
  /** Structured result an agent reports with the `finish` tool (§8.3). */
7786
7833
  const SessionResult = object({
7787
7834
  outcome: _enum([
@@ -7981,6 +8028,13 @@ const Session = object({
7981
8028
  rootThreadId: Id.nullable(),
7982
8029
  /** Structured result from the agent's `finish` tool, once reported. */
7983
8030
  result: SessionResult.nullable().optional(),
8031
+ /** When the session was archived: hidden from the sidebar, its worktree cleaned up. */
8032
+ archivedAt: Timestamp.nullable().optional(),
8033
+ /**
8034
+ * When CodeDen removed the session's worktree (archive or inactivity cleanup). The branch and
8035
+ * checkpoints stay; the worktree is recreated from the branch on the next message.
8036
+ */
8037
+ worktreeRemovedAt: Timestamp.nullable().optional(),
7984
8038
  createdAt: Timestamp,
7985
8039
  updatedAt: Timestamp,
7986
8040
  endedAt: Timestamp.nullable()
@@ -9156,9 +9210,14 @@ const UpdateMode = _enum([
9156
9210
  /**
9157
9211
  * Where the running CodeDen came from. `packaged`: an npm install (`npx`, global or a
9158
9212
  * 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).
9213
+ * a workspace build, which never switches versions (pull and rebuild instead). `desktop`: the
9214
+ * desktop app, which updates itself from GitHub Releases (the npm path is off; §17).
9160
9215
  */
9161
- const InstallSource = _enum(["packaged", "source"]);
9216
+ const InstallSource = _enum([
9217
+ "packaged",
9218
+ "source",
9219
+ "desktop"
9220
+ ]);
9162
9221
  /**
9163
9222
  * Self-update progress. `installing`: npm is fetching `target` into `<home>/app/<target>`;
9164
9223
  * `installed`: it is ready and starts on the next launch (or with a restart); `restarting`:
@@ -9290,6 +9349,138 @@ function comparePrerelease(a, b) {
9290
9349
  }
9291
9350
  return 0;
9292
9351
  }
9352
+ // ../../packages/contracts/src/voice-api.ts
9353
+ /**
9354
+ * Voice engines on the server (§11.3). The browser's speech APIs stay the default; these
9355
+ * routes proxy ElevenLabs (text to speech, Scribe) and OpenAI (transcription) with API keys
9356
+ * from the OS keychain, so keys never reach the browser, and run local Whisper with a
9357
+ * runtime and model downloaded on demand.
9358
+ */
9359
+ /** ElevenLabs MP3 output formats (`codec_sampleRate_bitrate`). */
9360
+ const TtsOutputFormat = _enum([
9361
+ "mp3_22050_32",
9362
+ "mp3_44100_64",
9363
+ "mp3_44100_128",
9364
+ "mp3_44100_192"
9365
+ ]);
9366
+ /**
9367
+ * `POST /api/tts`: streams `audio/mpeg`. `voiceId` defaults to
9368
+ * `settings.voice.readAloud.voice`, then a stock voice; `modelId` to a low-latency model.
9369
+ */
9370
+ const TtsRequest = object({
9371
+ text: string().min(1).max(5e3),
9372
+ voiceId: string().min(1).max(100).optional(),
9373
+ modelId: string().min(1).max(100).optional(),
9374
+ outputFormat: TtsOutputFormat.optional()
9375
+ });
9376
+ object({ voices: array(object({
9377
+ id: string(),
9378
+ name: string(),
9379
+ category: string().optional(),
9380
+ description: string().optional(),
9381
+ previewUrl: string().optional(),
9382
+ labels: record(string(), string()).optional()
9383
+ })) });
9384
+ /** Engines that transcribe on the server: the cloud engines and local Whisper. */
9385
+ const SttEngine = _enum([
9386
+ "elevenlabs",
9387
+ "openai",
9388
+ "local"
9389
+ ]);
9390
+ /**
9391
+ * `POST /api/stt?engine=&language=`: the body is the recorded audio (raw bytes with an
9392
+ * `audio/*` content type, or multipart with a `file` field), up to 25 MB. `engine` defaults
9393
+ * to `settings.voice.dictation.engine` (when that is `browser`, ElevenLabs). The `local`
9394
+ * engine only takes WAV, 16-bit PCM, mono at `LOCAL_STT_SAMPLE_RATE`, at most
9395
+ * `LOCAL_STT_MAX_SECONDS` long (the UI converts the recording before uploading).
9396
+ */
9397
+ const SttQuery = object({
9398
+ engine: SttEngine.optional(),
9399
+ /** ISO 639-1 hint, e.g. `en`. */
9400
+ language: string().min(2).max(10).optional()
9401
+ });
9402
+ object({
9403
+ text: string(),
9404
+ language: string().nullable(),
9405
+ engine: SttEngine,
9406
+ /** How long the server spent transcribing, in milliseconds (local engine). */
9407
+ elapsedMs: number$1().nonnegative().optional()
9408
+ });
9409
+ /**
9410
+ * Dictation engines (`settings.voice.dictation.engine`): the browser's speech recognition,
9411
+ * a cloud engine through the server, or `local` Whisper running on the CodeDen server.
9412
+ */
9413
+ const DictationEngine = _enum([
9414
+ "browser",
9415
+ "elevenlabs",
9416
+ "openai",
9417
+ "local"
9418
+ ]);
9419
+ /**
9420
+ * Local Whisper models CodeDen can download. `.en` models are English-only (more accurate
9421
+ * for English at the same size); the others detect or take a language.
9422
+ */
9423
+ const WhisperModelId = _enum([
9424
+ "tiny",
9425
+ "tiny.en",
9426
+ "base",
9427
+ "base.en",
9428
+ "small",
9429
+ "small.en"
9430
+ ]);
9431
+ /** Audio format the local engine accepts: WAV, 16-bit PCM, mono, at this sample rate. */
9432
+ const LOCAL_STT_SAMPLE_RATE = 16e3;
9433
+ const downloadFields = {
9434
+ state: _enum([
9435
+ "missing",
9436
+ "downloading",
9437
+ "installed",
9438
+ "failed"
9439
+ ]),
9440
+ /** Bytes received so far (while downloading). */
9441
+ receivedBytes: int().nonnegative(),
9442
+ /** Download size in bytes (pinned, so known before the download starts). */
9443
+ totalBytes: int().nonnegative(),
9444
+ /** Size on disk when installed. */
9445
+ sizeOnDisk: int().nonnegative().nullable(),
9446
+ /** Why the last attempt failed. */
9447
+ error: string().nullable()
9448
+ };
9449
+ /** The Whisper runtime: a prebuilt native library for this platform, downloaded on demand. */
9450
+ const LocalSttRuntime = object({
9451
+ /** Runtime version CodeDen pins. */
9452
+ version: string(),
9453
+ ...downloadFields
9454
+ });
9455
+ /** One downloadable model, with a speed and accuracy hint for the picker. */
9456
+ const LocalSttModel = object({
9457
+ id: WhisperModelId,
9458
+ /** e.g. `Base (English)`. */
9459
+ label: string(),
9460
+ /** Speed and accuracy hint, e.g. `Fast, good accuracy`. */
9461
+ hint: string(),
9462
+ englishOnly: boolean(),
9463
+ /** The suggested default. */
9464
+ recommended: boolean(),
9465
+ ...downloadFields
9466
+ });
9467
+ object({
9468
+ /** False on platforms without a prebuilt runtime (`unsupportedReason` says why). */
9469
+ supported: boolean(),
9470
+ /** `process.platform-process.arch` of the server, e.g. `win32-x64`. */
9471
+ platform: string(),
9472
+ unsupportedReason: string().nullable(),
9473
+ runtime: LocalSttRuntime,
9474
+ models: array(LocalSttModel),
9475
+ /** Folder holding the models. */
9476
+ location: string()
9477
+ });
9478
+ /**
9479
+ * `POST /api/voice/local/models/:id/download` starts (or joins) the download of the runtime
9480
+ * and that model; `DELETE /api/voice/local/models/:id` cancels or deletes it. Both return
9481
+ * the new `LocalSttStatus`. The UI polls `GET /api/voice/local` while anything downloads.
9482
+ */
9483
+ const LocalSttModelParams = object({ id: WhisperModelId });
9293
9484
  // ../../packages/contracts/src/settings.ts
9294
9485
  /** `auto` lets CodeDen pick the first ready provider (Claude, then Codex). */
9295
9486
  const ProviderChoice = union([literal("auto"), ProviderId]);
@@ -9320,6 +9511,17 @@ const RoleDefaults = object({
9320
9511
  model: string().nullable().default(null),
9321
9512
  effort: ReasoningEffort.nullable().default(null)
9322
9513
  });
9514
+ /** A model named by its provider and the provider's model id. */
9515
+ const ModelRef = object({
9516
+ provider: ProviderId,
9517
+ model: string().min(1)
9518
+ });
9519
+ /** A provider's custom models: ids are unique. */
9520
+ const CustomModelList = array(object({
9521
+ id: string().trim().min(1).max(200),
9522
+ /** Shown instead of the id in the picker. */
9523
+ label: string().trim().min(1).max(100).optional()
9524
+ })).refine((list) => new Set(list.map((m) => m.id)).size === list.length, { message: "Custom model ids must be unique" });
9323
9525
  /**
9324
9526
  * User settings persisted in `<home>/settings.json` (non-secret; API keys live in the OS
9325
9527
  * keychain). Every field has a default, so `Settings.parse({})` yields a complete object and
@@ -9350,6 +9552,25 @@ const Settings = object({
9350
9552
  noticeSummaries: boolean().default(true)
9351
9553
  }).prefault({})
9352
9554
  }).prefault({}),
9555
+ /** The model picker (§13). */
9556
+ models: object({
9557
+ /**
9558
+ * Starred models, listed in this order in the picker's Favourites section. Entries whose
9559
+ * model a provider no longer lists are kept (it may come back) but not shown.
9560
+ */
9561
+ favorites: array(ModelRef).default([]),
9562
+ /**
9563
+ * Model ids left out of the picker, by provider id. A hidden model that is still chosen
9564
+ * somewhere (a session, a default, an automation) keeps showing there.
9565
+ */
9566
+ hidden: record(ProviderId, array(string().min(1))).default({}),
9567
+ /**
9568
+ * Model ids added by the user, by provider id, merged into the picker after the models
9569
+ * the provider lists (a listed id wins). Their effort levels are those of the provider's
9570
+ * default model, or none when the provider has no effort levels.
9571
+ */
9572
+ custom: record(ProviderId, CustomModelList).default({})
9573
+ }).prefault({}),
9353
9574
  /** Follow-up prompt chips (§7.5). */
9354
9575
  followups: object({
9355
9576
  enabled: boolean().default(true),
@@ -9359,13 +9580,15 @@ const Settings = object({
9359
9580
  /** Voice (§11.3). */
9360
9581
  voice: object({
9361
9582
  dictation: object({
9362
- engine: _enum([
9363
- "browser",
9364
- "elevenlabs",
9365
- "openai"
9366
- ]).default("browser"),
9583
+ engine: DictationEngine.default("browser"),
9367
9584
  pushToTalkShortcut: string().nullable().default(null),
9368
- language: string().nullable().default(null)
9585
+ /**
9586
+ * BCP 47 tag such as `en-US`; null is auto (the browser engine follows the browser
9587
+ * language, the server engines detect it). English-only Whisper models ignore it.
9588
+ */
9589
+ language: string().nullable().default(null),
9590
+ /** Local Whisper: which downloaded model transcribes (§11.3). */
9591
+ local: object({ model: WhisperModelId.default("base.en") }).prefault({})
9369
9592
  }).prefault({}),
9370
9593
  readAloud: object({
9371
9594
  enabled: boolean().default(false),
@@ -9393,12 +9616,23 @@ const Settings = object({
9393
9616
  }).prefault({}),
9394
9617
  notifications: object({
9395
9618
  toasts: boolean().default(true),
9396
- browser: boolean().default(false)
9619
+ browser: boolean().default(false),
9620
+ /** A short chime when an agent is blocked on the user (an approval or a question). */
9621
+ sound: boolean().default(true)
9397
9622
  }).prefault({}),
9398
9623
  /** Global factory controls (§10.1). */
9399
9624
  factory: object({
9400
9625
  /** Stops every project's factory loop and all factory sessions. */
9401
9626
  killSwitch: boolean().default(false) }).prefault({}),
9627
+ /**
9628
+ * Worktree cleanup (§9). Archiving a session always removes its worktree; with `cleanup` on,
9629
+ * worktrees of sessions inactive for `afterDays` are removed too, unless the session's pull
9630
+ * request is still open or the worktree has uncommitted changes.
9631
+ */
9632
+ worktrees: object({
9633
+ cleanup: boolean().default(true),
9634
+ afterDays: int().min(1).max(365).default(7)
9635
+ }).prefault({}),
9402
9636
  /** Master switch; individual automations are still off by default (§10). */
9403
9637
  automations: object({ enabled: boolean().default(true) }).prefault({}),
9404
9638
  /**
@@ -9406,6 +9640,15 @@ killSwitch: boolean().default(false) }).prefault({}),
9406
9640
  * `check: false` to turn checks off; that reads as `mode: "off"`.
9407
9641
  */
9408
9642
  updates: preprocess((value) => typeof value === "object" && value !== null && !("mode" in value) && value.check === false ? { mode: "off" } : value, object({ mode: UpdateMode.default("notify") }).prefault({})),
9643
+ /** The desktop app (§17). Read by the desktop app only; `npx codeden` ignores it. */
9644
+ desktop: object({
9645
+ /** Start CodeDen (in the tray, window hidden) when you log in. */
9646
+ launchAtLogin: boolean().default(false),
9647
+ /** Closing the window keeps CodeDen running in the tray / menu bar; Quit really quits. */
9648
+ closeToTray: boolean().default(true),
9649
+ /** Keep the computer from sleeping while an agent is running. */
9650
+ keepAwake: boolean().default(true)
9651
+ }).prefault({}),
9409
9652
  /** First-run setup (§14): set once the user finishes the detection checklist. */
9410
9653
  setup: object({
9411
9654
  completed: boolean().default(false),
@@ -9467,6 +9710,7 @@ discriminatedUnion("type", [
9467
9710
  globalEvent("project.created", object({ project: Project })),
9468
9711
  globalEvent("project.updated", object({ project: Project })),
9469
9712
  globalEvent("project.archived", object({ projectId: Id })),
9713
+ globalEvent("projects.reordered", object({ projectIds: array(Id) })),
9470
9714
  globalEvent("session.created", object({ session: Session })),
9471
9715
  globalEvent("session.updated", object({ session: Session })),
9472
9716
  globalEvent("inbox.created", object({ item: InboxItem })),
@@ -9502,7 +9746,17 @@ discriminatedUnion("type", [
9502
9746
  globalEvent("message.dequeued", object({
9503
9747
  sessionId: Id,
9504
9748
  messageId: Id,
9505
- reason: _enum(["sent", "dropped"])
9749
+ reason: _enum([
9750
+ "sent",
9751
+ "dropped",
9752
+ "deleted"
9753
+ ])
9754
+ })),
9755
+ globalEvent("message.edited", object({
9756
+ sessionId: Id,
9757
+ messageId: Id,
9758
+ text: string(),
9759
+ at: Timestamp
9506
9760
  })),
9507
9761
  globalEvent("notice.created", object({ notice: Notice })),
9508
9762
  globalEvent("notice.delivered", object({
@@ -9695,9 +9949,17 @@ const UpdateProjectRequest = object({
9695
9949
  defaultProvider: ProviderId.nullable().optional(),
9696
9950
  autonomy: AutonomyLevel.nullable().optional(),
9697
9951
  timezone: string().nullable().optional(),
9698
- defaultBranch: string().nullable().optional()
9952
+ defaultBranch: string().nullable().optional(),
9953
+ /** Pin or unpin the project in the project list (§13). */
9954
+ pinned: boolean().optional()
9699
9955
  });
9700
9956
  /**
9957
+ * `PUT /api/projects/order`: the new manual order of the project list (§13). Lists every
9958
+ * non-archived project exactly once; the server rejects anything else (`invalid_order`).
9959
+ */
9960
+ const ReorderProjectsRequest = object({ projectIds: array(Id).max(1e3) });
9961
+ object({ projects: array(Project) });
9962
+ /**
9701
9963
  * Where a session runs (§9): a fresh worktree (default for agents), the repo root, or an
9702
9964
  * existing directory (`path`).
9703
9965
  */
@@ -10392,6 +10654,125 @@ discriminatedUnion("type", [
10392
10654
  message: string()
10393
10655
  })
10394
10656
  ]);
10657
+ object({
10658
+ /** Sessions with a turn starting or in progress. */
10659
+ running: int().nonnegative(),
10660
+ /** Sessions blocked on the user (an approval or a question). */
10661
+ waiting: int().nonnegative(),
10662
+ /** Open inbox items (questions, approvals, PRs to review, failures, ...). */
10663
+ inbox: int().nonnegative()
10664
+ });
10665
+ /** Count running and waiting sessions and open inbox items. */
10666
+ function summarizeActivity(sessions, inbox) {
10667
+ let running = 0;
10668
+ let waiting = 0;
10669
+ for (const s of sessions) if (s.status === "running" || s.status === "starting") running++;
10670
+ else if (s.status === "waiting") waiting++;
10671
+ return {
10672
+ running,
10673
+ waiting,
10674
+ inbox: inbox.filter((i) => i.status === "open").length
10675
+ };
10676
+ }
10677
+ /** The desktop updater's state, mirrored into `GET /api/updates` for the update banner. */
10678
+ const DesktopUpdateState = object({
10679
+ phase: _enum([
10680
+ "idle",
10681
+ "checking",
10682
+ "available",
10683
+ "downloading",
10684
+ "downloaded",
10685
+ "installing",
10686
+ "error"
10687
+ ]),
10688
+ /** The running app version. */
10689
+ current: string(),
10690
+ /** The newest release found, null until a check succeeded. */
10691
+ latest: string().nullable(),
10692
+ checkedAt: Timestamp.nullable(),
10693
+ /** Download progress (0-100) while `downloading`. */
10694
+ percent: number$1().min(0).max(100).nullable(),
10695
+ /** Why the last check or download failed. */
10696
+ error: string().nullable(),
10697
+ /** Set when this build cannot update itself (a development run, an unsigned macOS app). */
10698
+ unsupported: string().nullable()
10699
+ });
10700
+ /** What the server asks the desktop updater to do (driven by the update banner and policy). */
10701
+ const DesktopUpdateCommand = _enum([
10702
+ "check",
10703
+ "download",
10704
+ "install"
10705
+ ]);
10706
+ discriminatedUnion("type", [
10707
+ object({
10708
+ type: literal("ready"),
10709
+ url: string(),
10710
+ port: int().min(1).max(65535),
10711
+ launchUrl: string(),
10712
+ version: string()
10713
+ }),
10714
+ object({
10715
+ type: literal("failed"),
10716
+ message: string(),
10717
+ code: string().nullable()
10718
+ }),
10719
+ object({
10720
+ type: literal("update.command"),
10721
+ id: string(),
10722
+ command: DesktopUpdateCommand
10723
+ })
10724
+ ]);
10725
+ discriminatedUnion("type", [
10726
+ object({
10727
+ type: literal("update.state"),
10728
+ state: DesktopUpdateState
10729
+ }),
10730
+ object({
10731
+ type: literal("update.done"),
10732
+ id: string(),
10733
+ error: string().nullable()
10734
+ }),
10735
+ object({
10736
+ type: literal("shutdown"),
10737
+ stopRunner: boolean()
10738
+ })
10739
+ ]);
10740
+ discriminatedUnion("kind", [
10741
+ object({ kind: literal("home") }),
10742
+ object({ kind: literal("inbox") }),
10743
+ object({
10744
+ kind: literal("session"),
10745
+ sessionId: Id
10746
+ }),
10747
+ object({
10748
+ kind: literal("project"),
10749
+ projectId: Id
10750
+ }),
10751
+ object({
10752
+ kind: literal("settings"),
10753
+ section: string().nullable()
10754
+ })
10755
+ ]);
10756
+ object({
10757
+ version: string(),
10758
+ platform: _enum([
10759
+ "win32",
10760
+ "darwin",
10761
+ "linux"
10762
+ ]),
10763
+ /** True for an installed build (false when run from the repository). */
10764
+ packaged: boolean(),
10765
+ /** The app started this server (false when it attached to one started by `codeden`). */
10766
+ ownsServer: boolean(),
10767
+ /** The OS lets the app start at login (not on Linux AppImage, for example). */
10768
+ launchAtLoginSupported: boolean()
10769
+ });
10770
+ object({
10771
+ title: string().min(1).max(200),
10772
+ body: string().max(2e3).optional(),
10773
+ /** Passed back to `onNotificationClick` when the user clicks it. */
10774
+ tag: string().max(200).optional()
10775
+ });
10395
10776
  // ../../packages/contracts/src/images-api.ts
10396
10777
  /**
10397
10778
  * Image generation settings screen and test button (§12.5). The `generate_image` MCP tool
@@ -10496,14 +10877,40 @@ object({
10496
10877
  result: StartSessionResponse
10497
10878
  });
10498
10879
  /**
10499
- * `PATCH /api/sessions/:id`: change the model or effort mid-session. Applied on the next turn
10500
- * (a running turn keeps its settings). `null` returns to the role default.
10880
+ * `PATCH /api/sessions/:id`: change the model, effort, autonomy or title mid-session.
10881
+ * Model and effort apply on the next turn (a running turn keeps its settings); `null` returns
10882
+ * to the role default.
10501
10883
  */
10502
10884
  const UpdateSessionRequest = object({
10503
10885
  model: string().min(1).nullable().optional(),
10504
10886
  effort: ReasoningEffort.nullable().optional(),
10887
+ /**
10888
+ * Autonomy level (§11.2), saved on the session and pushed to the live provider session.
10889
+ * It applies from the next tool call when the provider reports `liveAutonomy`, otherwise
10890
+ * from the next turn. Approvals already waiting stay waiting.
10891
+ */
10892
+ autonomy: AutonomyLevel.optional(),
10505
10893
  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" });
10894
+ }).refine((r) => r.model !== void 0 || r.effort !== void 0 || r.autonomy !== void 0 || r.title !== void 0, { message: "Nothing to change" });
10895
+ /**
10896
+ * `POST /api/sessions/:id/archive`: stop the session if it is running, hide it from the
10897
+ * sidebar and remove its worktree. The branch and checkpoints stay.
10898
+ */
10899
+ const ArchiveSessionRequest = object({
10900
+ /** Remove the worktree even when it has uncommitted changes (those changes are lost). */
10901
+ force: boolean().default(false) });
10902
+ object({
10903
+ session: Session,
10904
+ worktree: object({
10905
+ status: _enum([
10906
+ "removed",
10907
+ "kept_changes",
10908
+ "none",
10909
+ "failed"
10910
+ ]),
10911
+ message: string().nullable()
10912
+ })
10913
+ });
10507
10914
  // ../../packages/contracts/src/runner/events.ts
10508
10915
  /**
10509
10916
  * Durable runner -> server events. Journaled by the runner and delivered with a `seq`
@@ -10608,8 +11015,9 @@ const RunnerStream = discriminatedUnion("kind", [
10608
11015
  * 4. runner replays journaled events with `seq > lastAckedSeq`, then streams live.
10609
11016
  *
10610
11017
  * 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`) and `stream`s
10612
- * (ephemeral). Either side may `ping`.
11018
+ * and cumulative `ack`s; the runner sends `event`s (durable, with `seq`), `stream`s
11019
+ * (ephemeral) and `providers` (the current provider list, whenever it changes). Either side
11020
+ * may `ping`.
10613
11021
  */
10614
11022
  /** Request id correlating a `command` with its `result`. */
10615
11023
  const RequestId = string().min(1).max(100);
@@ -10708,6 +11116,10 @@ const RunnerToServerMessage = discriminatedUnion("type", [
10708
11116
  ts: Timestamp,
10709
11117
  stream: RunnerStream
10710
11118
  }),
11119
+ object({
11120
+ type: literal("providers"),
11121
+ providers: array(ProviderInfo)
11122
+ }),
10711
11123
  Ping,
10712
11124
  Pong
10713
11125
  ]);
@@ -11027,57 +11439,11 @@ object({
11027
11439
  * rest of the bundle, so the package version is the single source of truth at runtime;
11028
11440
  * everywhere else (workspace, tests) it is `SOURCE_VERSION`.
11029
11441
  */
11030
- const CODEDEN_VERSION = packageVersion() ?? "0.1.1";
11442
+ const CODEDEN_VERSION = packageVersion() ?? "0.2.0";
11031
11443
  function packageVersion() {
11032
11444
  const value = globalThis.__CODEDEN_PACKAGE_VERSION__;
11033
11445
  return typeof value === "string" && value.length > 0 ? value : void 0;
11034
11446
  }
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
11447
  // ../../node_modules/.pnpm/ws@8.22.1/node_modules/ws/lib/constants.js
11082
11448
  var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
11083
11449
  const BINARY_TYPES = [
@@ -14828,6 +15194,43 @@ var AsyncQueue = class {
14828
15194
  };
14829
15195
  }
14830
15196
  };
15197
+ // ../../packages/providers/src/models.ts
15198
+ function copy(m) {
15199
+ return {
15200
+ ...m,
15201
+ efforts: [...m.efforts]
15202
+ };
15203
+ }
15204
+ /**
15205
+ * An adapter's current model list. It starts from a fallback and is replaced by what the
15206
+ * provider reports (model discovery, a running session). Listeners hear about real changes
15207
+ * only, so the runner can forward them to the server (§4.3).
15208
+ */
15209
+ var ModelCatalog = class {
15210
+ models;
15211
+ listeners = /* @__PURE__ */ new Set();
15212
+ constructor(fallback) {
15213
+ this.models = fallback.map(copy);
15214
+ }
15215
+ /** A copy of the current list. */
15216
+ list() {
15217
+ return this.models.map(copy);
15218
+ }
15219
+ /** Replace the list. Returns false (and notifies nobody) when nothing changed. */
15220
+ set(models) {
15221
+ if (JSON.stringify(models) === JSON.stringify(this.models)) return false;
15222
+ this.models = models.map(copy);
15223
+ for (const listener of this.listeners) try {
15224
+ listener();
15225
+ } catch {}
15226
+ return true;
15227
+ }
15228
+ /** Call `listener` after every change. Returns the unsubscribe function. */
15229
+ subscribe(listener) {
15230
+ this.listeners.add(listener);
15231
+ return () => this.listeners.delete(listener);
15232
+ }
15233
+ };
14831
15234
  // ../../packages/providers/src/util/exec.ts
14832
15235
  function isFile(candidate) {
14833
15236
  try {
@@ -14899,11 +15302,51 @@ function scriptFromCmdShim(shimPath, content) {
14899
15302
  if (content === void 0 && !existsSync(script)) return void 0;
14900
15303
  return script;
14901
15304
  }
15305
+ /** True inside the desktop app, where `process.execPath` is the Electron binary (§17). */
15306
+ function isElectronRuntime(versions = process.versions) {
15307
+ return typeof versions.electron === "string";
15308
+ }
15309
+ /**
15310
+ * Environment for running one of CodeDen's own scripts (the runner, npm, Playwright's CLI)
15311
+ * with `process.execPath`. In the desktop app that is the Electron binary, which only acts
15312
+ * as Node.js with `ELECTRON_RUN_AS_NODE=1`; elsewhere the environment is returned unchanged.
15313
+ */
15314
+ function ownNodeEnv(env = process.env, versions = process.versions) {
15315
+ return isElectronRuntime(versions) ? {
15316
+ ...env,
15317
+ ELECTRON_RUN_AS_NODE: "1"
15318
+ } : env;
15319
+ }
15320
+ let systemNode;
15321
+ /**
15322
+ * The Node.js that runs `node-script` commands (npm-style shims of the user's own tools, such
15323
+ * as a globally installed Codex on Windows). Normally the Node running CodeDen. In the desktop
15324
+ * app the user's `node` from PATH is preferred, so their tools run exactly as in a terminal
15325
+ * and `ELECTRON_RUN_AS_NODE` never leaks into agent processes; without one, the Electron
15326
+ * binary runs them as Node.js.
15327
+ */
15328
+ function scriptRuntime() {
15329
+ if (!isElectronRuntime()) return {
15330
+ path: process.execPath,
15331
+ electron: false
15332
+ };
15333
+ if (systemNode === void 0) {
15334
+ const found = resolveExecutable("node");
15335
+ systemNode = found?.kind === "native" ? found.path : null;
15336
+ }
15337
+ return systemNode ? {
15338
+ path: systemNode,
15339
+ electron: false
15340
+ } : {
15341
+ path: process.execPath,
15342
+ electron: true
15343
+ };
15344
+ }
14902
15345
  /** The `[command, args]` to pass to `spawn` for a resolved executable (never needs `shell`). */
14903
15346
  function spawnArgs(cmd, args) {
14904
15347
  switch (cmd.kind) {
14905
15348
  case "native": return [cmd.path, [...args]];
14906
- case "node-script": return [process.execPath, [cmd.script, ...args]];
15349
+ case "node-script": return [scriptRuntime().path, [cmd.script, ...args]];
14907
15350
  case "shell-script": return [process.env.ComSpec ?? "cmd.exe", [
14908
15351
  "/d",
14909
15352
  "/s",
@@ -14920,9 +15363,11 @@ function quoteCmdArg(arg) {
14920
15363
  /** Spawn a resolved executable. Uses `windowsVerbatimArguments` for the cmd.exe fallback. */
14921
15364
  function spawnResolved(cmd, args, options = {}) {
14922
15365
  const [file, argv] = spawnArgs(cmd, args);
15366
+ const asNode = cmd.kind === "node-script" && scriptRuntime().electron;
14923
15367
  return spawn(file, argv, {
14924
15368
  windowsHide: true,
14925
15369
  ...options,
15370
+ ...asNode ? { env: ownNodeEnv(options.env ?? process.env) } : {},
14926
15371
  ...cmd.kind === "shell-script" ? { windowsVerbatimArguments: true } : {}
14927
15372
  });
14928
15373
  }
@@ -15135,6 +15580,9 @@ function parseClaudeAuthStatus(stdout) {
15135
15580
  return unknown;
15136
15581
  }
15137
15582
  }
15583
+ // ../../packages/providers/src/util/model.ts
15584
+ /** The id the model pickers use for "the provider's own default model". */
15585
+ const DEFAULT_MODEL_ID = "default";
15138
15586
  /**
15139
15587
  * The model to pass to a provider, or undefined to leave it out so the provider uses its own
15140
15588
  * default (the user's config). `"default"` and empty values are never sent as model ids.
@@ -15143,24 +15591,161 @@ function explicitModel(model) {
15143
15591
  const id = model?.trim();
15144
15592
  return id && id !== "default" ? id : void 0;
15145
15593
  }
15146
- // ../../packages/providers/src/util/text.ts
15147
- /** Max characters of tool output kept inline in an item (§6: larger outputs are truncated). */
15148
- const TOOL_OUTPUT_PREVIEW_CHARS = 16e3;
15149
- /** Build a `ToolOutput` preview, truncating long text. */
15150
- function toolOutput(text, max = TOOL_OUTPUT_PREVIEW_CHARS) {
15151
- const totalBytes = Buffer.byteLength(text, "utf8");
15152
- if (text.length <= max) return {
15153
- text,
15154
- truncated: false,
15155
- totalBytes
15156
- };
15157
- return {
15158
- text: `${text.slice(0, max)}\n...`,
15159
- truncated: true,
15160
- totalBytes
15161
- };
15162
- }
15163
- /** One-line summary for titles: first line, trimmed and shortened. */
15594
+ // ../../packages/providers/src/policy/read-only.ts
15595
+ /**
15596
+ * Read-only sessions (the orchestrator and PMs, §11.2) may inspect a repository but never
15597
+ * change it. This allowlist decides which shell commands they may run: commands that only
15598
+ * read, with no output redirection and no command substitution. Anything else is denied.
15599
+ */
15600
+ /**
15601
+ * Split on every `;`, `&&`, `||`, `|` and newline, even inside quotes. Deliberately cruder
15602
+ * than the risk classifier's lexer: for an allowlist, over-splitting only denies more.
15603
+ */
15604
+ function splitCommands(command) {
15605
+ return command.split(/\r?\n|;|&&|\|\||\|/).map((s) => s.trim()).filter(Boolean);
15606
+ }
15607
+ /** Tokenize one simple command, honouring single and double quotes. */
15608
+ function tokenize(command) {
15609
+ const tokens = [];
15610
+ for (const m of command.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g)) tokens.push(m[1] ?? m[2] ?? m[3] ?? "");
15611
+ return tokens;
15612
+ }
15613
+ /** Programs that only read (or print). */
15614
+ const READ_ONLY_BINS = /* @__PURE__ */ new Set([
15615
+ "ls",
15616
+ "dir",
15617
+ "cat",
15618
+ "type",
15619
+ "head",
15620
+ "tail",
15621
+ "less",
15622
+ "more",
15623
+ "wc",
15624
+ "grep",
15625
+ "egrep",
15626
+ "fgrep",
15627
+ "rg",
15628
+ "find",
15629
+ "fd",
15630
+ "tree",
15631
+ "pwd",
15632
+ "echo",
15633
+ "printf",
15634
+ "which",
15635
+ "where",
15636
+ "whoami",
15637
+ "date",
15638
+ "stat",
15639
+ "file",
15640
+ "du",
15641
+ "df",
15642
+ "sort",
15643
+ "uniq",
15644
+ "cut",
15645
+ "diff",
15646
+ "jq",
15647
+ "basename",
15648
+ "dirname",
15649
+ "realpath",
15650
+ "true",
15651
+ "get-childitem",
15652
+ "gci",
15653
+ "get-content",
15654
+ "gc",
15655
+ "get-item",
15656
+ "gi",
15657
+ "get-location",
15658
+ "gl",
15659
+ "select-string",
15660
+ "sls",
15661
+ "measure-object",
15662
+ "test-path",
15663
+ "resolve-path",
15664
+ "write-output",
15665
+ "write-host",
15666
+ "select-object",
15667
+ "where-object",
15668
+ "format-list",
15669
+ "format-table",
15670
+ "out-string",
15671
+ "sort-object",
15672
+ "gh"
15673
+ ]);
15674
+ /** Git subcommands that never change the repository. */
15675
+ const READ_ONLY_GIT = /* @__PURE__ */ new Set([
15676
+ "status",
15677
+ "log",
15678
+ "show",
15679
+ "diff",
15680
+ "blame",
15681
+ "grep",
15682
+ "ls-files",
15683
+ "ls-tree",
15684
+ "rev-parse",
15685
+ "rev-list",
15686
+ "describe",
15687
+ "shortlog",
15688
+ "cat-file",
15689
+ "remote",
15690
+ "branch",
15691
+ "tag",
15692
+ "config",
15693
+ "reflog",
15694
+ "merge-base",
15695
+ "name-rev",
15696
+ "for-each-ref",
15697
+ "worktree"
15698
+ ]);
15699
+ /** `gh` subcommand verbs that only read. */
15700
+ const READ_ONLY_GH = /* @__PURE__ */ new Set([
15701
+ "view",
15702
+ "list",
15703
+ "status",
15704
+ "diff",
15705
+ "checks"
15706
+ ]);
15707
+ const bin = (token) => (token ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase().replace(/\.(exe|cmd|bat|ps1)$/, "") ?? "";
15708
+ /** Why a command is refused in a read-only session, or undefined when it may run. */
15709
+ function readOnlyCommandDenial(command) {
15710
+ if (/\$\(|`|<\(|>\(/.test(command)) return "Command substitution is not allowed in a read-only session";
15711
+ for (const simple of splitCommands(command)) {
15712
+ if (/(^|[^0-9&<])>>?(?!\s*(&\d|\/dev\/null|nul\b|\$null))/i.test(simple)) return "Writing files is not allowed in a read-only session";
15713
+ const tokens = tokenize(simple);
15714
+ const name = bin(tokens[0]);
15715
+ const args = tokens.slice(1).map((a) => a.toLowerCase());
15716
+ if (name === "git") {
15717
+ const sub = args.find((a) => !a.startsWith("-"));
15718
+ if (!sub || !READ_ONLY_GIT.has(sub)) return `git ${sub ?? ""} may change the repository`;
15719
+ 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`;
15720
+ if (sub === "config" && !args.some((a) => a === "--get" || a === "--list" || a === "-l")) return "Only reading git config is allowed";
15721
+ continue;
15722
+ }
15723
+ if (name === "gh") {
15724
+ 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)";
15725
+ continue;
15726
+ }
15727
+ 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";
15728
+ if (!READ_ONLY_BINS.has(name)) return `${name || "This command"} is not on the read-only allowlist`;
15729
+ }
15730
+ }
15731
+ // ../../packages/providers/src/util/text.ts
15732
+ /** Max characters of tool output kept inline in an item (§6: larger outputs are truncated). */
15733
+ const TOOL_OUTPUT_PREVIEW_CHARS = 16e3;
15734
+ /** Build a `ToolOutput` preview, truncating long text. */
15735
+ function toolOutput(text, max = TOOL_OUTPUT_PREVIEW_CHARS) {
15736
+ const totalBytes = Buffer.byteLength(text, "utf8");
15737
+ if (text.length <= max) return {
15738
+ text,
15739
+ truncated: false,
15740
+ totalBytes
15741
+ };
15742
+ return {
15743
+ text: `${text.slice(0, max)}\n...`,
15744
+ truncated: true,
15745
+ totalBytes
15746
+ };
15747
+ }
15748
+ /** One-line summary for titles: first line, trimmed and shortened. */
15164
15749
  function shorten(text, max = 80) {
15165
15750
  const line = text.trim().split(/\r?\n/)[0] ?? "";
15166
15751
  return line.length > max ? `${line.slice(0, max - 1)}…` : line;
@@ -15197,50 +15782,110 @@ function unixToIso(value) {
15197
15782
  const ms = value < 0xe8d4a51000 ? value * 1e3 : value;
15198
15783
  return new Date(ms).toISOString();
15199
15784
  }
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
15785
  // ../../packages/providers/src/policy/paths.ts
15786
+ const isWin = (ctx) => (ctx.platform ?? process.platform) === "win32";
15216
15787
  function pathApi(ctx) {
15217
- return (ctx.platform ?? process.platform) === "win32" ? path.win32 : path.posix;
15788
+ return isWin(ctx) ? path.win32 : path.posix;
15789
+ }
15790
+ function homeOf(ctx) {
15791
+ return ctx.home ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
15792
+ }
15793
+ function tempRootsOf(ctx) {
15794
+ return ctx.tempRoots ?? osTempRoots(ctx.platform);
15218
15795
  }
15219
- /** Resolve `target` (absolute, relative to cwd, or `~`-prefixed) to an absolute path. */
15796
+ const HOME_VARS = /^(\$HOME|\$\{HOME\}|\$env:USERPROFILE|%USERPROFILE%)(?=$|[\\/])/i;
15797
+ const TEMP_VARS = /^(\$TMPDIR|\$\{TMPDIR\}|\$TEMP|\$TMP|\$env:TEMP|\$env:TMP|%TEMP%|%TMP%)(?=$|[\\/])/i;
15798
+ /**
15799
+ * Git Bash paths on Windows: `/c/Users/x` is `C:\Users\x`, and `/tmp` is the user's temp
15800
+ * directory.
15801
+ */
15802
+ function fromGitBash(value, temp) {
15803
+ if (temp && /^\/tmp(?=$|\/)/.test(value)) return temp + value.slice(4);
15804
+ const drive = /^\/([a-z])(?=$|\/)/i.exec(value);
15805
+ if (drive?.[1]) return `${drive[1].toUpperCase()}:\\${value.slice(3)}`;
15806
+ return value;
15807
+ }
15808
+ /**
15809
+ * Resolve `target` (absolute, relative to `dir`, `~`-prefixed, or starting with a home or
15810
+ * temp variable) to an absolute path.
15811
+ */
15220
15812
  function resolveAgainst(target, ctx) {
15221
15813
  const p = pathApi(ctx);
15222
15814
  let value = target.trim().replace(/^["']|["']$/g, "");
15223
- const home = ctx.home ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
15815
+ const home = homeOf(ctx);
15224
15816
  if (value === "~" || value.startsWith("~/") || value.startsWith("~\\")) value = home + value.slice(1);
15225
- value = value.replace(/^\$HOME(?=$|[\\/])/, home).replace(/^\$env:USERPROFILE(?=$|[\\/])/i, home);
15226
- return p.resolve(ctx.cwd, value);
15817
+ value = value.replace(HOME_VARS, home);
15818
+ const temp = tempRootsOf(ctx)[0];
15819
+ if (temp) value = value.replace(TEMP_VARS, temp);
15820
+ if (isWin(ctx)) value = fromGitBash(value, temp);
15821
+ return p.resolve(ctx.dir ?? ctx.cwd, value);
15227
15822
  }
15228
15823
  /** True when `target` resolves inside `root` (or equals it). Case-insensitive on Windows. */
15229
15824
  function isInside$1(target, root, ctx) {
15825
+ const rel = relative$1(root, target, ctx);
15826
+ return rel === "" || !rel.startsWith("..") && !pathApi(ctx).isAbsolute(rel);
15827
+ }
15828
+ function relative$1(root, target, ctx) {
15230
15829
  const p = pathApi(ctx);
15231
- const win = (ctx.platform ?? process.platform) === "win32";
15232
- const norm = (s) => {
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);
15830
+ const norm = (s) => isWin(ctx) ? p.resolve(s).toLowerCase() : p.resolve(s);
15831
+ return p.relative(norm(root), norm(target));
15238
15832
  }
15239
15833
  /** True when `target` is inside the worktree or one of the extra roots. */
15240
15834
  function isInWorkspace(target, ctx) {
15241
15835
  const abs = resolveAgainst(target, ctx);
15242
15836
  return [ctx.cwd, ...ctx.extraRoots ?? []].some((root) => isInside$1(abs, root, ctx));
15243
15837
  }
15838
+ /** True when `target` is strictly inside one of the OS temp directories (never the root). */
15839
+ function isInTemp(target, ctx) {
15840
+ const abs = resolveAgainst(target, ctx);
15841
+ return tempRootsOf(ctx).some((root) => {
15842
+ return relative$1(root, abs, ctx) !== "" && isInside$1(abs, root, ctx);
15843
+ });
15844
+ }
15845
+ /** True when agents may write `target` without asking: the worktree, extra roots or temp. */
15846
+ function isWritable(target, ctx) {
15847
+ return isInWorkspace(target, ctx) || isInTemp(target, ctx);
15848
+ }
15849
+ /**
15850
+ * Usable temp roots from `candidates`: absolute, deduplicated, and never a filesystem root,
15851
+ * the home directory or one of its parents (a stray `TMPDIR=/` must not open everything).
15852
+ */
15853
+ function tempRootsFrom(candidates, platform, home) {
15854
+ const ctx = {
15855
+ cwd: "/",
15856
+ platform
15857
+ };
15858
+ const p = pathApi(ctx);
15859
+ const out = [];
15860
+ for (const candidate of candidates) {
15861
+ if (!candidate || !p.isAbsolute(candidate)) continue;
15862
+ const root = p.resolve(candidate);
15863
+ if (p.parse(root).root === root || p.dirname(root) === root) continue;
15864
+ if (home && isInside$1(home, root, ctx)) continue;
15865
+ if (!out.some((r) => relative$1(r, root, ctx) === "")) out.push(root);
15866
+ }
15867
+ return out;
15868
+ }
15869
+ const realpath = (p) => {
15870
+ try {
15871
+ return realpathSync.native(p);
15872
+ } catch {
15873
+ return;
15874
+ }
15875
+ };
15876
+ let cachedTempRoots;
15877
+ /**
15878
+ * The OS temp directories for the running platform: `os.tmpdir()`, `%TEMP%` and `%TMP%` on
15879
+ * Windows, `$TMPDIR` and `/tmp` elsewhere, each also by its real path (macOS symlinks
15880
+ * `/tmp`, Windows may report a short 8.3 name). Empty when asked about another platform.
15881
+ */
15882
+ function osTempRoots(platform = process.platform) {
15883
+ if (platform !== process.platform) return [];
15884
+ if (cachedTempRoots) return cachedTempRoots;
15885
+ const env = process.env;
15886
+ cachedTempRoots = tempRootsFrom([os.tmpdir(), ...platform === "win32" ? [env.TEMP, env.TMP] : [env.TMPDIR, "/tmp"]].flatMap((c) => c ? [c, realpath(c)] : []), platform, os.homedir());
15887
+ return cachedTempRoots;
15888
+ }
15244
15889
  const SECRET_BASENAMES = /* @__PURE__ */ new Set([
15245
15890
  ".env",
15246
15891
  ".npmrc",
@@ -15284,6 +15929,254 @@ function isSecretPath(target) {
15284
15929
  if (SECRET_EXTENSIONS.some((ext) => base.endsWith(ext))) return true;
15285
15930
  return /(^|\/)\.(ssh|aws|gnupg|docker)\//i.test(normalized) && !base.endsWith(".pub");
15286
15931
  }
15932
+ // ../../packages/providers/src/policy/shell.ts
15933
+ /** Parse a command line into its simple commands. */
15934
+ function parseShell(src, dialect = "auto") {
15935
+ return new Lexer(src, dialect).list(null);
15936
+ }
15937
+ /** Placeholder for heredoc and here-string bodies, which are data, never commands. */
15938
+ const BODY = "<body>";
15939
+ /** The command being built and the word being read. */
15940
+ var Builder = class {
15941
+ out;
15942
+ cmd = {
15943
+ words: [],
15944
+ writes: [],
15945
+ nested: []
15946
+ };
15947
+ word = "";
15948
+ inWord = false;
15949
+ /** Where the current word goes: an argument, a redirect target or a heredoc delimiter. */
15950
+ target = "word";
15951
+ heredocStrip = false;
15952
+ heredocs = [];
15953
+ constructor(out) {
15954
+ this.out = out;
15955
+ }
15956
+ add(text) {
15957
+ this.word += text;
15958
+ this.inWord = true;
15959
+ }
15960
+ endWord() {
15961
+ if (!this.inWord) return;
15962
+ if (this.target === "write") this.cmd.writes.push(this.word);
15963
+ else if (this.target === "heredoc") this.heredocs.push({
15964
+ delim: this.word.replace(/\\/g, ""),
15965
+ strip: this.heredocStrip
15966
+ });
15967
+ else if (this.target === "word") this.cmd.words.push(this.word);
15968
+ this.target = "word";
15969
+ this.word = "";
15970
+ this.inWord = false;
15971
+ }
15972
+ endCommand() {
15973
+ this.endWord();
15974
+ this.target = "word";
15975
+ const { cmd } = this;
15976
+ if (cmd.words.length || cmd.writes.length || cmd.nested.length) this.out.push(cmd);
15977
+ this.cmd = {
15978
+ words: [],
15979
+ writes: [],
15980
+ nested: []
15981
+ };
15982
+ }
15983
+ };
15984
+ var Lexer = class {
15985
+ src;
15986
+ dialect;
15987
+ i = 0;
15988
+ constructor(src, dialect) {
15989
+ this.src = src;
15990
+ this.dialect = dialect;
15991
+ }
15992
+ /** Read simple commands until `closer` (`)` or a backtick) or the end of the input. */
15993
+ list(closer) {
15994
+ const out = [];
15995
+ const b = new Builder(out);
15996
+ const { src } = this;
15997
+ while (this.i < src.length) {
15998
+ const c = src[this.i];
15999
+ const next = src[this.i + 1];
16000
+ if (closer && c === closer) {
16001
+ this.i += 1;
16002
+ b.endCommand();
16003
+ return out;
16004
+ }
16005
+ if (c === "\n" || c === "\r") {
16006
+ b.endCommand();
16007
+ this.i += c === "\r" && next === "\n" ? 2 : 1;
16008
+ this.skipHeredocs(b.heredocs.splice(0));
16009
+ } else if (c === " " || c === " ") {
16010
+ b.endWord();
16011
+ this.i += 1;
16012
+ } else if (c === "#" && !b.inWord) while (this.i < src.length && src[this.i] !== "\n" && src[this.i] !== "\r") this.i += 1;
16013
+ else if (c === ";") {
16014
+ b.endCommand();
16015
+ this.i += 1;
16016
+ } else if (c === "|") {
16017
+ b.endCommand();
16018
+ this.i += next === "|" || next === "&" ? 2 : 1;
16019
+ } else if (c === "&" && next === ">") {
16020
+ b.endWord();
16021
+ b.target = "write";
16022
+ this.i += src[this.i + 2] === ">" ? 3 : 2;
16023
+ } else if (c === "&") {
16024
+ b.endCommand();
16025
+ this.i += next === "&" ? 2 : 1;
16026
+ } else if (c === ">" || c === "<") this.redirect(b, c);
16027
+ else if (c === "'" || c === "\"") {
16028
+ if (b.inWord && b.word.endsWith("@") && this.lineEndsAt(this.i + 1)) {
16029
+ this.skipHereString(c);
16030
+ b.add(BODY);
16031
+ } else if (c === "'") b.add(this.readSingle());
16032
+ else b.add(this.readDouble(b));
16033
+ } else if (c === "\\" && this.dialect !== "powershell") {
16034
+ if (next === "\n" || next === "\r") this.i += next === "\r" && src[this.i + 2] === "\n" ? 3 : 2;
16035
+ else {
16036
+ b.add(src.slice(this.i, this.i + 2));
16037
+ this.i += 2;
16038
+ }
16039
+ } else if (c === "`") this.backtick(b);
16040
+ else if (c === "(") {
16041
+ const start = this.i;
16042
+ this.i += 1;
16043
+ b.cmd.nested.push(this.list(")"));
16044
+ b.add(src.slice(start, this.i));
16045
+ } else {
16046
+ b.add(c);
16047
+ this.i += 1;
16048
+ }
16049
+ }
16050
+ b.endCommand();
16051
+ return out;
16052
+ }
16053
+ /** `>`, `>>`, `>|`, `2>`, `*>`, `2>&1`, `<`, `<<EOF`, `<<-EOF`, `<<<`, `<(…)`, `>(…)`. */
16054
+ redirect(b, c) {
16055
+ const { src } = this;
16056
+ if (b.inWord && /^(\d+|\*)$/.test(b.word)) {
16057
+ b.word = "";
16058
+ b.inWord = false;
16059
+ } else b.endWord();
16060
+ const next = src[this.i + 1];
16061
+ if (next === "(") {
16062
+ const start = this.i;
16063
+ this.i += 2;
16064
+ b.cmd.nested.push(this.list(")"));
16065
+ b.add(src.slice(start, this.i));
16066
+ return;
16067
+ }
16068
+ if (c === "<") {
16069
+ if (next === "<" && src[this.i + 2] === "<") {
16070
+ b.target = "skip";
16071
+ this.i += 3;
16072
+ } else if (next === "<") {
16073
+ this.i += 2;
16074
+ b.heredocStrip = src[this.i] === "-";
16075
+ if (b.heredocStrip) this.i += 1;
16076
+ b.target = "heredoc";
16077
+ } else this.i += 1;
16078
+ return;
16079
+ }
16080
+ let j = this.i + 1;
16081
+ if (src[j] === ">") j += 1;
16082
+ if (src[j] === "|") j += 1;
16083
+ if (src[j] === "&") {
16084
+ const dup = /^&(\d+|-)/.exec(src.slice(j));
16085
+ if (dup) {
16086
+ this.i = j + dup[0].length;
16087
+ return;
16088
+ }
16089
+ j += 1;
16090
+ }
16091
+ b.target = "write";
16092
+ this.i = j;
16093
+ }
16094
+ /**
16095
+ * PowerShell escapes the next character; POSIX starts a command substitution. (A closing
16096
+ * backtick never gets here: `list` checks for its closer first.)
16097
+ */
16098
+ backtick(b) {
16099
+ const { src } = this;
16100
+ const next = src[this.i + 1];
16101
+ if (this.dialect === "powershell" || this.dialect === "auto" && (next === void 0 || /\s/.test(next))) {
16102
+ if (next === "\n" || next === "\r") this.i += next === "\r" && src[this.i + 2] === "\n" ? 3 : 2;
16103
+ else {
16104
+ b.add(src.slice(this.i + 1, this.i + 2));
16105
+ this.i += 2;
16106
+ }
16107
+ return;
16108
+ }
16109
+ const start = this.i;
16110
+ this.i += 1;
16111
+ b.cmd.nested.push(this.list("`"));
16112
+ b.add(src.slice(start, this.i));
16113
+ }
16114
+ readSingle() {
16115
+ const end = this.src.indexOf("'", this.i + 1);
16116
+ const stop = end === -1 ? this.src.length : end;
16117
+ const text = this.src.slice(this.i + 1, stop);
16118
+ this.i = stop + 1;
16119
+ return text;
16120
+ }
16121
+ /** A double-quoted string: escapes, and `$(…)` substitutions run as nested commands. */
16122
+ readDouble(b) {
16123
+ const { src } = this;
16124
+ let text = "";
16125
+ this.i += 1;
16126
+ while (this.i < src.length) {
16127
+ const c = src[this.i];
16128
+ const next = src[this.i + 1];
16129
+ if (c === "\"") {
16130
+ this.i += 1;
16131
+ return text;
16132
+ }
16133
+ if (c === "\\" && this.dialect !== "powershell" && next && "\"\\$`\n".includes(next)) {
16134
+ text += next;
16135
+ this.i += 2;
16136
+ } else if (c === "`" && this.dialect !== "posix" && next !== void 0) {
16137
+ text += next;
16138
+ this.i += 2;
16139
+ } else if (c === "`") {
16140
+ const start = this.i;
16141
+ this.i += 1;
16142
+ b.cmd.nested.push(this.list("`"));
16143
+ text += src.slice(start, this.i);
16144
+ } else if (c === "$" && next === "(") {
16145
+ const start = this.i;
16146
+ this.i += 2;
16147
+ b.cmd.nested.push(this.list(")"));
16148
+ text += src.slice(start, this.i);
16149
+ } else {
16150
+ text += c;
16151
+ this.i += 1;
16152
+ }
16153
+ }
16154
+ return text;
16155
+ }
16156
+ /** True when only spaces or tabs remain before the end of the line at `j`. */
16157
+ lineEndsAt(j) {
16158
+ return /^[ \t]*(\r?\n|$)/.exec(this.src.slice(j)) !== null;
16159
+ }
16160
+ /** Skip a PowerShell here-string (`@'` … `'@`) whose opening quote is at `this.i`. */
16161
+ skipHereString(quote) {
16162
+ const close = new RegExp(`(^|\\n)${quote}@`, "g");
16163
+ close.lastIndex = this.i + 1;
16164
+ const m = close.exec(this.src);
16165
+ this.i = m ? m.index + m[0].length : this.src.length;
16166
+ }
16167
+ /** Skip the bodies of heredocs opened on the line that just ended. */
16168
+ skipHeredocs(pending) {
16169
+ const { src } = this;
16170
+ for (const { delim, strip } of pending) while (this.i < src.length) {
16171
+ const nl = src.indexOf("\n", this.i);
16172
+ const end = nl === -1 ? src.length : nl;
16173
+ let line = src.slice(this.i, end).replace(/\r$/, "");
16174
+ if (strip) line = line.replace(/^\t+/, "");
16175
+ this.i = nl === -1 ? src.length : nl + 1;
16176
+ if (line === delim) break;
16177
+ }
16178
+ }
16179
+ };
15287
16180
  // ../../packages/providers/src/policy/risk.ts
15288
16181
  const SAFE = { risky: false };
15289
16182
  const risky = (action, reason) => ({
@@ -15291,26 +16184,22 @@ const risky = (action, reason) => ({
15291
16184
  action,
15292
16185
  reason
15293
16186
  });
15294
- /** Split a shell command line into simple commands (on `;`, `&&`, `||`, `|`, newlines). */
15295
- function splitCommands(command) {
15296
- return command.split(/\r?\n|;|&&|\|\||\|/).map((s) => s.trim()).filter(Boolean);
15297
- }
15298
- /** Tokenize one simple command, honouring single and double quotes. */
15299
- function tokenize(command) {
15300
- const tokens = [];
15301
- for (const m of command.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g)) tokens.push(m[1] ?? m[2] ?? m[3] ?? "");
15302
- return tokens;
15303
- }
15304
- /** Strip wrappers that don't change what runs (`sudo`, `env X=1`, `command`, `exec`). */
15305
- function unwrap(tokens) {
16187
+ /** How deep `bash -c "…"` style inline scripts are followed. */
16188
+ const MAX_DEPTH = 4;
16189
+ /**
16190
+ * Words that don't change what runs: elevation (`sudo`), wrappers (`env X=1`, `command`,
16191
+ * `exec`) and shell keywords that start a compound command (`if`, `then`, `do`, `{`, ...).
16192
+ */
16193
+ function unwrap(words) {
15306
16194
  let elevated = false;
15307
- let rest = tokens;
16195
+ let rest = words;
15308
16196
  for (;;) {
15309
16197
  const head = rest[0]?.toLowerCase();
15310
16198
  if (head === "sudo" || head === "doas" || head === "runas" || head === "gsudo") {
15311
16199
  elevated = true;
15312
16200
  rest = rest.slice(1).filter((t, i) => !(i === 0 && t.startsWith("-")));
15313
16201
  } else if (head === "env" || head === "command" || head === "exec" || head === "nohup") rest = rest.slice(1);
16202
+ else if (head && KEYWORDS.has(head)) rest = rest.slice(1);
15314
16203
  else if (head && /^[A-Za-z_][A-Za-z0-9_]*=/.test(head)) rest = rest.slice(1);
15315
16204
  else break;
15316
16205
  }
@@ -15319,6 +16208,18 @@ function unwrap(tokens) {
15319
16208
  elevated
15320
16209
  };
15321
16210
  }
16211
+ const KEYWORDS = /* @__PURE__ */ new Set([
16212
+ "if",
16213
+ "then",
16214
+ "else",
16215
+ "elif",
16216
+ "do",
16217
+ "while",
16218
+ "until",
16219
+ "time",
16220
+ "!",
16221
+ "{"
16222
+ ]);
15322
16223
  const binName = (token) => (token ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase().replace(/\.(exe|cmd|bat|ps1)$/, "") ?? "";
15323
16224
  const DOWNLOADERS = /\b(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\b|downloadstring/i;
15324
16225
  const SHELLS = /^\s*(sudo\s+)?(sh|bash|zsh|fish|dash|ksh|iex|invoke-expression|python3?|node|pwsh|powershell|perl|ruby)(\s|$)/i;
@@ -15328,18 +16229,109 @@ function classifyCommand(command, ctx) {
15328
16229
  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
16230
  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
16231
  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
- for (const simple of splitCommands(command)) {
15332
- const result = classifySimple(simple, ctx);
16232
+ return classifyList(parseShell(command, ctx.shell ?? "auto"), ctx, 0);
16233
+ }
16234
+ /**
16235
+ * Classify commands in order. A `cd` moves where later relative paths resolve; commands in
16236
+ * substitutions and subshells are classified too, without their `cd` leaking out.
16237
+ */
16238
+ function classifyList(list, ctx, depth) {
16239
+ let here = ctx;
16240
+ for (const cmd of list) {
16241
+ for (const nested of cmd.nested) {
16242
+ const result = classifyList(nested, here, depth);
16243
+ if (result.risky) return result;
16244
+ }
16245
+ const result = classifySimple(cmd, here, depth);
15333
16246
  if (result.risky) return result;
16247
+ here = afterCd(cmd, here);
15334
16248
  }
15335
16249
  return SAFE;
15336
16250
  }
15337
- function classifySimple(simple, ctx) {
15338
- const { tokens, elevated } = unwrap(tokenize(simple));
16251
+ const CD = /* @__PURE__ */ new Set([
16252
+ "cd",
16253
+ "pushd",
16254
+ "chdir",
16255
+ "set-location",
16256
+ "sl",
16257
+ "push-location"
16258
+ ]);
16259
+ /** The context after `cmd` when it changes directory (`cd dir`, `Set-Location -Path dir`). */
16260
+ function afterCd(cmd, ctx) {
16261
+ const { tokens } = unwrap(cmd.words);
16262
+ if (!CD.has(binName(tokens[0]))) return ctx;
16263
+ const args = tokens.slice(1);
16264
+ if (args.includes("-")) return ctx;
16265
+ const target = namedArg(args, ["-path", "-literalpath"]) ?? args.find((a) => !a.startsWith("-")) ?? "~";
16266
+ return {
16267
+ ...ctx,
16268
+ dir: resolveAgainst(target, ctx)
16269
+ };
16270
+ }
16271
+ /** The value after a PowerShell-style named parameter (`-Path x`), if present. */
16272
+ function namedArg(args, names) {
16273
+ const i = args.findIndex((a) => names.includes(a.toLowerCase()));
16274
+ return i >= 0 ? args[i + 1] : void 0;
16275
+ }
16276
+ /** The script of an inline shell invocation (`bash -c "…"`, `pwsh -Command …`, `cmd /c …`). */
16277
+ function inlineScript(bin, args) {
16278
+ if ([
16279
+ "sh",
16280
+ "bash",
16281
+ "zsh",
16282
+ "dash",
16283
+ "ksh",
16284
+ "ash",
16285
+ "fish"
16286
+ ].includes(bin)) {
16287
+ const flag = args.findIndex((a) => /^-[a-z]*c[a-z]*$/i.test(a));
16288
+ const text = flag >= 0 ? args.slice(flag + 1).find((a) => !a.startsWith("-")) : void 0;
16289
+ return text ? {
16290
+ text,
16291
+ dialect: "posix"
16292
+ } : void 0;
16293
+ }
16294
+ if (bin === "pwsh" || bin === "powershell") {
16295
+ const flag = args.findIndex((a) => /^-(c|command)$/i.test(a));
16296
+ return flag >= 0 ? {
16297
+ text: args.slice(flag + 1).join(" "),
16298
+ dialect: "powershell"
16299
+ } : void 0;
16300
+ }
16301
+ if (bin === "cmd") {
16302
+ const flag = args.findIndex((a) => /^\/[ck]$/i.test(a));
16303
+ return flag >= 0 ? {
16304
+ text: args.slice(flag + 1).join(" "),
16305
+ dialect: "auto"
16306
+ } : void 0;
16307
+ }
16308
+ }
16309
+ /**
16310
+ * The argument as a path, or undefined when it can't be one: inline code, multi-line text
16311
+ * or anything with shell or code punctuation (`selected={row.key`) is not a file name.
16312
+ */
16313
+ function asPath(arg) {
16314
+ const value = arg.replace(/^-+[a-z-]*=/i, "");
16315
+ if (!value || /[\n\r{}()<>|;=`]/.test(value)) return void 0;
16316
+ return value;
16317
+ }
16318
+ const DISCARD = /^(\/dev\/(null|stdout|stderr|tty)|nul|\$null)$/i;
16319
+ function classifySimple(cmd, ctx, depth) {
16320
+ const { tokens, elevated } = unwrap(cmd.words);
15339
16321
  if (elevated) return risky("other", "Runs a command with elevated privileges");
15340
16322
  const bin = binName(tokens[0]);
15341
16323
  const args = tokens.slice(1);
15342
16324
  const lower = args.map((a) => a.toLowerCase());
16325
+ const script = inlineScript(bin, args);
16326
+ if (script && depth < MAX_DEPTH) {
16327
+ const inner = classifyList(parseShell(script.text, script.dialect), ctx, depth + 1);
16328
+ if (inner.risky) return inner;
16329
+ }
16330
+ for (const target of cmd.writes) {
16331
+ if (DISCARD.test(target)) continue;
16332
+ const write = classifyWrite(target, ctx);
16333
+ if (write.risky) return write;
16334
+ }
15343
16335
  if (bin === "git") return classifyGit(lower);
15344
16336
  const install = classifyInstall(bin, lower);
15345
16337
  if (install.risky) return install;
@@ -15357,44 +16349,51 @@ function classifySimple(simple, ctx) {
15357
16349
  const targets = args.filter((a) => !a.startsWith("-") && !/^\/[a-z]$/i.test(a));
15358
16350
  for (const target of targets) {
15359
16351
  if (target === "*" || /^[\\/]$/.test(target) || /^[a-z]:[\\/]?$/i.test(target)) return risky("outside_worktree", `Deletes ${target}`);
15360
- if (!isInWorkspace(target, ctx)) return risky("outside_worktree", `Deletes ${target}, outside the worktree`);
16352
+ if (!isWritable(target, ctx)) return risky("outside_worktree", `Deletes ${target}, outside the worktree`);
15361
16353
  }
15362
16354
  }
15363
- for (const m of simple.matchAll(/(?:^|[^0-9&])>>?\s*("[^"]+"|'[^']+'|[^\s;&|]+)/g)) {
15364
- const target = m[1] ?? "";
15365
- if (/^(\/dev\/null|nul|\$null)$/i.test(target) || /^&\d$/.test(target)) continue;
15366
- if (!isInWorkspace(target, ctx)) return risky("outside_worktree", `Writes ${target}, outside the worktree`);
15367
- if (isSecretPath(target)) return risky("secrets", `Writes the secrets file ${target}`);
16355
+ const dest = copyDestination(bin, args);
16356
+ if (dest) {
16357
+ const write = classifyWrite(dest, ctx);
16358
+ if (write.risky) return write;
15368
16359
  }
15369
- if ([
15370
- "cp",
15371
- "mv",
15372
- "copy",
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, "")));
16360
+ const secret = args.find((a) => {
16361
+ const value = asPath(a);
16362
+ return value !== void 0 && isSecretPath(value);
16363
+ });
15393
16364
  if (secret) return risky("secrets", `Accesses the secrets file ${secret}`);
15394
16365
  const listsEnvDrive = /^(get-childitem|gci|ls|dir)$/.test(bin) && lower.some((a) => a.startsWith("env:"));
15395
16366
  if (bin === "printenv" || listsEnvDrive) return risky("secrets", "Dumps environment variables");
15396
16367
  return SAFE;
15397
16368
  }
16369
+ const COPIES = [
16370
+ "cp",
16371
+ "mv",
16372
+ "copy",
16373
+ "move",
16374
+ "copy-item",
16375
+ "move-item",
16376
+ "xcopy",
16377
+ "robocopy"
16378
+ ];
16379
+ const WRITERS = [
16380
+ "tee",
16381
+ "set-content",
16382
+ "out-file",
16383
+ "add-content"
16384
+ ];
16385
+ /** Where a copy, move or file-writing command writes. */
16386
+ function copyDestination(bin, args) {
16387
+ if (!COPIES.includes(bin) && !WRITERS.includes(bin)) return void 0;
16388
+ const named = WRITERS.includes(bin) ? namedArg(args, [
16389
+ "-path",
16390
+ "-filepath",
16391
+ "-literalpath"
16392
+ ]) : namedArg(args, ["-destination"]);
16393
+ if (named) return named;
16394
+ const positional = args.filter((a) => !a.startsWith("-") && !/^\/[a-z]+$/i.test(a));
16395
+ return WRITERS.includes(bin) ? positional[0] : positional.at(-1);
16396
+ }
15398
16397
  function classifyGit(args) {
15399
16398
  let i = 0;
15400
16399
  while (i < args.length && args[i]?.startsWith("-")) i += args[i] === "-c" || args[i] === "-C" ? 2 : 1;
@@ -15471,11 +16470,16 @@ function classifyInstall(bin, args) {
15471
16470
  if (bin === "install-module" || bin === "install-package") return risky("other", `Installs a system-wide module (${bin})`);
15472
16471
  return SAFE;
15473
16472
  }
15474
- /** Classify a file write/edit. */
16473
+ /**
16474
+ * Classify a file write/edit. The worktree, extra roots and OS temp are fine; secrets ask. A
16475
+ * secrets file in temp counts as outside the worktree, so Full autonomy (which only asks for
16476
+ * writes outside the worktree) still asks for it.
16477
+ */
15475
16478
  function classifyWrite(target, ctx) {
15476
- if (!isInWorkspace(target, ctx)) return risky("outside_worktree", `Writes ${target}, outside the worktree`);
15477
- if (isSecretPath(target)) return risky("secrets", `Writes the secrets file ${target}`);
15478
- return SAFE;
16479
+ if (!isWritable(target, ctx)) return risky("outside_worktree", `Writes ${target}, outside the worktree`);
16480
+ if (!isSecretPath(target)) return SAFE;
16481
+ if (!isInWorkspace(target, ctx)) return risky("outside_worktree", `Writes the secrets file ${target} in the temp directory`);
16482
+ return risky("secrets", `Writes the secrets file ${target}`);
15479
16483
  }
15480
16484
  /** Classify a file read or search. Reads anywhere are fine except credential files. */
15481
16485
  function classifyRead(target, _ctx) {
@@ -15496,130 +16500,6 @@ function classifyFetch(url, ctx) {
15496
16500
  return rule.startsWith("*.") ? host.endsWith(rule.slice(1)) : host === rule;
15497
16501
  }) ? SAFE : risky("network", `Fetches ${host}, which is not on the allowed host list`);
15498
16502
  }
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
16503
  // ../../packages/providers/src/claude/tools.ts
15624
16504
  /** Claude tools that are not rendered as `tool_call` items. */
15625
16505
  const SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
@@ -15818,7 +16698,13 @@ function completeToolCall(item, result) {
15818
16698
  }
15819
16699
  /** Risk of a Claude tool call under the `auto` policy (§11.2). */
15820
16700
  function assessClaudeTool(name, input, ctx) {
15821
- if (COMMAND_TOOLS.has(name)) return classifyCommand(str(input, "command") ?? "", ctx);
16701
+ if (COMMAND_TOOLS.has(name)) {
16702
+ const shell = name === "PowerShell" ? "powershell" : "posix";
16703
+ return classifyCommand(str(input, "command") ?? "", {
16704
+ ...ctx,
16705
+ shell
16706
+ });
16707
+ }
15822
16708
  if (EDIT_TOOLS.has(name)) return classifyWrite(editPath(input), ctx);
15823
16709
  if (name === "Read") return classifyRead(str(input, "file_path") ?? "", ctx);
15824
16710
  if (name === "Grep" || name === "Glob") {
@@ -15855,7 +16741,8 @@ function approvalTitle(name, input) {
15855
16741
  * Native permission mode per autonomy level (§11.2):
15856
16742
  * - supervised -> `default`, and every edit/command is forced to ask via a PreToolUse hook
15857
16743
  * - auto -> `acceptEdits`, plus the risky-action classifier
15858
- * - full -> `bypassPermissions`, asking only for writes outside the worktree
16744
+ * - full -> `bypassPermissions`, asking only for writes outside the worktree (the OS
16745
+ * temp directory counts as inside, except for secrets files there)
15859
16746
  * Plan mode overrides all of them with `plan`.
15860
16747
  */
15861
16748
  function permissionModeFor(autonomy, planMode) {
@@ -15879,6 +16766,13 @@ var ClaudePolicy = class {
15879
16766
  this.ctx = ctx;
15880
16767
  this.readOnly = readOnly;
15881
16768
  }
16769
+ get level() {
16770
+ return this.autonomy;
16771
+ }
16772
+ /** Switch the level; the next `gate()` uses it. `allow_session` decisions are kept. */
16773
+ setAutonomy(level) {
16774
+ this.autonomy = level;
16775
+ }
15882
16776
  /** Why a call is refused outright (read-only sessions), or undefined. */
15883
16777
  denial(toolName, input) {
15884
16778
  if (!this.readOnly) return void 0;
@@ -15888,7 +16782,11 @@ var ClaudePolicy = class {
15888
16782
  return reason ? `This session is read-only: ${reason}.` : void 0;
15889
16783
  }
15890
16784
  }
15891
- gate(toolName, input) {
16785
+ /**
16786
+ * Decide one tool call. `dir` is the shell's current directory as Claude reports it (an
16787
+ * earlier `cd` persists between Bash calls); relative paths resolve from it.
16788
+ */
16789
+ gate(toolName, input, dir) {
15892
16790
  if (toolName === "AskUserQuestion") return {
15893
16791
  ask: true,
15894
16792
  action: "other",
@@ -15900,7 +16798,10 @@ var ClaudePolicy = class {
15900
16798
  reason: "The agent finished planning"
15901
16799
  };
15902
16800
  if (this.sessionAllow.has(this.key(toolName, input))) return { ask: false };
15903
- const risk = assessClaudeTool(toolName, input, this.ctx);
16801
+ const risk = assessClaudeTool(toolName, input, dir ? {
16802
+ ...this.ctx,
16803
+ dir
16804
+ } : this.ctx);
15904
16805
  if (this.autonomy === "supervised") {
15905
16806
  if (risk.risky) return {
15906
16807
  ask: true,
@@ -15926,8 +16827,8 @@ var ClaudePolicy = class {
15926
16827
  * Whether a `canUseTool` call (Claude's own permission prompt) needs the user. In supervised
15927
16828
  * mode everything Claude asks about goes to the user; otherwise only gated calls do.
15928
16829
  */
15929
- needsUser(toolName, input) {
15930
- const gate = this.gate(toolName, input);
16830
+ needsUser(toolName, input, dir) {
16831
+ const gate = this.gate(toolName, input, dir);
15931
16832
  if (gate.ask || this.autonomy !== "supervised") return gate;
15932
16833
  if (this.sessionAllow.has(this.key(toolName, input))) return gate;
15933
16834
  return {
@@ -16063,6 +16964,83 @@ function toSdkUserMessage(input) {
16063
16964
  parent_tool_use_id: null
16064
16965
  };
16065
16966
  }
16967
+ /** The SDK's model rows as CodeDen models. The first row is Claude Code's default. */
16968
+ function toModelInfos(models) {
16969
+ return models.map((m, i) => ({
16970
+ id: m.value,
16971
+ label: m.displayName || m.value,
16972
+ ...m.description ? { description: m.description } : {},
16973
+ efforts: m.supportedEffortLevels ?? [],
16974
+ ...i === 0 ? { isDefault: true } : {}
16975
+ }));
16976
+ }
16977
+ /**
16978
+ * List Claude Code's models without a turn. `query()` with a streaming prompt that never
16979
+ * yields starts the CLI and sends only the `initialize` control request, whose answer carries
16980
+ * the model list (`supportedModels()`); no user message is sent, so no model request is made.
16981
+ * Nothing from the user's or a project's settings is loaded (no hooks run), nothing is
16982
+ * persisted, and the CLI is closed as soon as the list arrives or `timeoutMs` passes.
16983
+ * Rejects on timeout or when the CLI fails to start.
16984
+ */
16985
+ async function listClaudeModels(query, executable, opts = {}) {
16986
+ const timeoutMs = opts.timeoutMs ?? 3e4;
16987
+ const abortController = new AbortController();
16988
+ const stopped = new Promise((resolve) => abortController.signal.addEventListener("abort", () => resolve(), { once: true }));
16989
+ const idle = () => ({ [Symbol.asyncIterator]: () => ({ next: async () => {
16990
+ await stopped;
16991
+ return {
16992
+ done: true,
16993
+ value: void 0
16994
+ };
16995
+ } }) });
16996
+ let q;
16997
+ let timer;
16998
+ try {
16999
+ q = query({
17000
+ prompt: idle(),
17001
+ options: {
17002
+ ...executableOptions(executable),
17003
+ cwd: tmpdir(),
17004
+ settingSources: [],
17005
+ persistSession: false,
17006
+ tools: [],
17007
+ mcpServers: {},
17008
+ strictMcpConfig: true,
17009
+ env: claudeEnv(void 0),
17010
+ abortController
17011
+ }
17012
+ });
17013
+ const stream = q;
17014
+ (async () => {
17015
+ for await (const _message of stream);
17016
+ })().catch(() => void 0);
17017
+ const timeout = new Promise((_, reject) => {
17018
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Claude Code did not list its models within ${timeoutMs}ms`)), timeoutMs);
17019
+ });
17020
+ return toModelInfos(await Promise.race([q.supportedModels(), timeout]));
17021
+ } finally {
17022
+ clearTimeout(timer);
17023
+ abortController.abort();
17024
+ try {
17025
+ q?.close();
17026
+ } catch {}
17027
+ }
17028
+ }
17029
+ // ../../packages/providers/src/utility.ts
17030
+ /** Abort signal that fires on the caller's signal or after `timeoutMs`. */
17031
+ function oneShotSignal(request) {
17032
+ const controller = new AbortController();
17033
+ const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Utility model timed out")), request.timeoutMs ?? 6e4);
17034
+ const onAbort = () => controller.abort(request.signal?.reason);
17035
+ request.signal?.addEventListener("abort", onAbort, { once: true });
17036
+ return {
17037
+ signal: controller.signal,
17038
+ dispose: () => {
17039
+ clearTimeout(timer);
17040
+ request.signal?.removeEventListener("abort", onAbort);
17041
+ }
17042
+ };
17043
+ }
16066
17044
  // ../../packages/providers/src/util/install-lock.ts
16067
17045
  function isAlive$1(pid) {
16068
17046
  try {
@@ -16196,6 +17174,163 @@ function npmFailure(output) {
16196
17174
  if (summary) return summary;
16197
17175
  return output.trim().split(/\r?\n/).slice(-3).join(" ").slice(0, 500);
16198
17176
  }
17177
+ // ../../packages/providers/src/util/tar.ts
17178
+ const BLOCK = 512;
17179
+ /**
17180
+ * Extract a gzipped tarball (an npm package) into `dest`, dropping the first `strip` path
17181
+ * components (`package/` in npm tarballs). Only regular files and directories are written;
17182
+ * links and devices are skipped. Entries that would land outside `dest` (absolute paths,
17183
+ * `..`) fail the whole extraction. Returns the relative paths written.
17184
+ */
17185
+ function extractTarGz(archive, dest, opts = {}) {
17186
+ const tar = gunzipSync(archive);
17187
+ const root = path.resolve(dest);
17188
+ const written = [];
17189
+ let offset = 0;
17190
+ let longName = null;
17191
+ let paxPath = null;
17192
+ while (offset + BLOCK <= tar.length) {
17193
+ const header = tar.subarray(offset, offset + BLOCK);
17194
+ if (header.every((b) => b === 0)) break;
17195
+ if (!checksumOk(header)) throw new Error("Corrupt tarball (bad header checksum)");
17196
+ const size = readOctal(header, 124, 12);
17197
+ const type = String.fromCharCode(header[156] ?? 0);
17198
+ const dataStart = offset + BLOCK;
17199
+ const data = tar.subarray(dataStart, dataStart + size);
17200
+ if (data.length < size) throw new Error("Corrupt tarball (truncated entry)");
17201
+ offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
17202
+ if (type === "L") {
17203
+ longName = cString(data);
17204
+ continue;
17205
+ }
17206
+ if (type === "x") {
17207
+ paxPath = paxRecord(data, "path");
17208
+ continue;
17209
+ }
17210
+ if (type === "g") continue;
17211
+ const name = paxPath ?? longName ?? headerName(header);
17212
+ longName = null;
17213
+ paxPath = null;
17214
+ if (!(type === "0" || type === "\0" || type === "7") && type !== "5") continue;
17215
+ const rel = stripComponents(name, opts.strip ?? 0);
17216
+ if (!rel) continue;
17217
+ const target = path.resolve(root, rel);
17218
+ if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`Unsafe path in tarball: ${name}`);
17219
+ if (type === "5") {
17220
+ mkdirSync(target, { recursive: true });
17221
+ continue;
17222
+ }
17223
+ mkdirSync(path.dirname(target), { recursive: true });
17224
+ writeFileSync(target, data);
17225
+ written.push(rel);
17226
+ }
17227
+ return written;
17228
+ }
17229
+ function stripComponents(name, strip) {
17230
+ const parts = name.replace(/\\/g, "/").split("/").filter(Boolean);
17231
+ if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) throw new Error(`Unsafe path in tarball: ${name}`);
17232
+ if (parts.includes("..")) throw new Error(`Unsafe path in tarball: ${name}`);
17233
+ const rest = parts.slice(strip).filter((p) => p !== ".");
17234
+ return rest.length ? rest.join("/") : null;
17235
+ }
17236
+ function headerName(header) {
17237
+ const name = cString(header.subarray(0, 100));
17238
+ if (!header.subarray(257, 263).toString("latin1").startsWith("ustar")) return name;
17239
+ const prefix = cString(header.subarray(345, 500));
17240
+ return prefix ? `${prefix}/${name}` : name;
17241
+ }
17242
+ function cString(buf) {
17243
+ const end = buf.indexOf(0);
17244
+ return buf.subarray(0, end === -1 ? buf.length : end).toString("utf8");
17245
+ }
17246
+ function readOctal(buf, start, length) {
17247
+ const text = cString(buf.subarray(start, start + length)).trim();
17248
+ const value = text ? Number.parseInt(text, 8) : 0;
17249
+ if (!Number.isFinite(value) || value < 0) throw new Error("Corrupt tarball (bad size)");
17250
+ return value;
17251
+ }
17252
+ function checksumOk(header) {
17253
+ const expected = readOctal(header, 148, 8);
17254
+ let sum = 0;
17255
+ for (let i = 0; i < BLOCK; i++) sum += i >= 148 && i < 156 ? 32 : header[i] ?? 0;
17256
+ return sum === expected;
17257
+ }
17258
+ /** One `key=value` record from a pax extended header (`<len> <key>=<value>\n` lines). */
17259
+ function paxRecord(data, key) {
17260
+ let i = 0;
17261
+ while (i < data.length) {
17262
+ const space = data.indexOf(32, i);
17263
+ if (space === -1) break;
17264
+ const len = Number.parseInt(data.subarray(i, space).toString("latin1"), 10);
17265
+ if (!Number.isFinite(len) || len <= 0) break;
17266
+ const record = data.subarray(space + 1, i + len - 1).toString("utf8");
17267
+ const eq = record.indexOf("=");
17268
+ if (eq !== -1 && record.slice(0, eq) === key) return record.slice(eq + 1);
17269
+ i += len;
17270
+ }
17271
+ return null;
17272
+ }
17273
+ /** The registry to use: an explicit one, npm's configured one, or the public registry. */
17274
+ function registryUrl(explicit, env = process.env) {
17275
+ return (explicit ?? env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
17276
+ }
17277
+ /**
17278
+ * Install one exact package version straight from an npm registry, without npm (the desktop
17279
+ * app has no npm, §17): read the version manifest, download the tarball, check it against the
17280
+ * manifest's `dist.integrity` (sha512), extract it into a staging folder and move it into
17281
+ * `<nodeModules>/<name>`. Dependencies are not installed: use it only for packages whose
17282
+ * runtime needs none (the Claude Agent SDK, installed without optional and peer deps).
17283
+ */
17284
+ async function installFromRegistry(opts) {
17285
+ const doFetch = opts.fetch ?? fetch;
17286
+ const progress = opts.onProgress ?? (() => {});
17287
+ const signal = AbortSignal.timeout(opts.timeoutMs ?? 3e5);
17288
+ const base = registryUrl(opts.registry);
17289
+ const spec = `${opts.name}@${opts.version}`;
17290
+ const manifestUrl = `${base}/${opts.name.replace("/", "%2f")}/${encodeURIComponent(opts.version)}`;
17291
+ progress(`Looking up ${spec}`);
17292
+ const res = await doFetch(manifestUrl, {
17293
+ headers: { accept: "application/json" },
17294
+ signal
17295
+ });
17296
+ if (!res.ok) throw new Error(`the registry answered ${res.status} for ${spec}`);
17297
+ const manifest = await res.json();
17298
+ if (manifest.name !== opts.name || manifest.version !== opts.version) throw new Error(`the registry returned the wrong package for ${spec}`);
17299
+ const tarball = manifest.dist?.tarball;
17300
+ const integrity = manifest.dist?.integrity;
17301
+ if (typeof tarball !== "string" || !/^https?:\/\//.test(tarball)) throw new Error(`${spec} has no tarball URL`);
17302
+ const expected = typeof integrity === "string" ? sha512Of(integrity) : null;
17303
+ if (!expected) throw new Error(`${spec} has no sha512 integrity to check the download against`);
17304
+ progress(`Downloading ${spec}`);
17305
+ const download = await doFetch(tarball, { signal });
17306
+ if (!download.ok) throw new Error(`downloading ${spec} failed (${download.status})`);
17307
+ const archive = Buffer.from(await download.arrayBuffer());
17308
+ if (createHash("sha512").update(archive).digest("base64") !== expected) throw new Error(`the download of ${spec} does not match its published integrity (sha512)`);
17309
+ progress(`Unpacking ${spec}`);
17310
+ const target = path.join(opts.nodeModules, ...opts.name.split("/"));
17311
+ const staging = `${target}.staging-${randomBytes(4).toString("hex")}`;
17312
+ try {
17313
+ mkdirSync(staging, { recursive: true });
17314
+ extractTarGz(archive, staging, { strip: 1 });
17315
+ rmSync(target, {
17316
+ recursive: true,
17317
+ force: true,
17318
+ maxRetries: 3
17319
+ });
17320
+ renameSync(staging, target);
17321
+ } finally {
17322
+ rmSync(staging, {
17323
+ recursive: true,
17324
+ force: true,
17325
+ maxRetries: 2
17326
+ });
17327
+ }
17328
+ }
17329
+ /** The base64 sha512 digest in a Subresource Integrity string (`sha512-<base64> ...`). */
17330
+ function sha512Of(integrity) {
17331
+ for (const part of integrity.trim().split(/\s+/)) if (part.startsWith("sha512-")) return part.slice(7).replace(/\?.*$/, "");
17332
+ return null;
17333
+ }
16199
17334
  // ../../packages/providers/src/claude/sdk/install.ts
16200
17335
  /** The Claude Agent SDK package. */
16201
17336
  const CLAUDE_SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
@@ -16222,6 +17357,18 @@ async function importClaudeSdkFrom(depsDir) {
16222
17357
  if (typeof sdk.query !== "function") throw new Error(`${entry} does not export query()`);
16223
17358
  return sdk;
16224
17359
  }
17360
+ /** Which installer `installClaudeSdk` uses, and the npm to run when it is npm. */
17361
+ function chooseInstaller(opts, env = process.env) {
17362
+ const wanted = opts.installer ?? env.CODEDEN_DEPS_INSTALLER;
17363
+ if (wanted === "registry") return { kind: "registry" };
17364
+ const npm = opts.npm ?? resolveNpm(env);
17365
+ if (npm) return {
17366
+ kind: "npm",
17367
+ npm
17368
+ };
17369
+ if (wanted === "npm") throw new Error("npm was not found; install Node.js with npm and try again");
17370
+ return { kind: "registry" };
17371
+ }
16225
17372
  const inflight = /* @__PURE__ */ new Map();
16226
17373
  /**
16227
17374
  * Install the tested SDK version into `depsDir` with npm, skipping its optional platform
@@ -16249,25 +17396,37 @@ async function runInstall(opts) {
16249
17396
  name: "codeden-deps",
16250
17397
  private: true
16251
17398
  }, null, 2)}\n`);
16252
- const npm = opts.npm ?? resolveNpm();
16253
- if (!npm) throw new Error("npm was not found; install Node.js with npm and try again");
17399
+ const installer = chooseInstaller(opts);
16254
17400
  const spec = `${CLAUDE_SDK_PACKAGE}@${CLAUDE_SDK_VERSION}`;
16255
- progress(`Downloading ${spec} from npm`);
16256
- await runNpm(npm, [
16257
- "install",
16258
- spec,
16259
- "--omit=optional",
16260
- "--omit=peer",
16261
- "--no-audit",
16262
- "--no-fund",
16263
- "--save-exact",
16264
- "--prefix",
16265
- opts.depsDir
16266
- ], {
16267
- cwd: opts.depsDir,
16268
- timeoutMs: opts.timeoutMs ?? 3e5,
16269
- onLine: progress
16270
- });
17401
+ if (installer.kind === "registry") {
17402
+ await installFromRegistry({
17403
+ name: CLAUDE_SDK_PACKAGE,
17404
+ version: CLAUDE_SDK_VERSION,
17405
+ nodeModules: join(opts.depsDir, "node_modules"),
17406
+ registry: opts.registry,
17407
+ fetch: opts.fetch,
17408
+ timeoutMs: opts.timeoutMs,
17409
+ onProgress: progress
17410
+ });
17411
+ recordDependency(manifest, CLAUDE_SDK_PACKAGE, CLAUDE_SDK_VERSION);
17412
+ } else {
17413
+ progress(`Downloading ${spec} from npm`);
17414
+ await runNpm(installer.npm, [
17415
+ "install",
17416
+ spec,
17417
+ "--omit=optional",
17418
+ "--omit=peer",
17419
+ "--no-audit",
17420
+ "--no-fund",
17421
+ "--save-exact",
17422
+ "--prefix",
17423
+ opts.depsDir
17424
+ ], {
17425
+ cwd: opts.depsDir,
17426
+ timeoutMs: opts.timeoutMs ?? 3e5,
17427
+ onLine: progress
17428
+ });
17429
+ }
16271
17430
  if (installedClaudeSdkVersion(opts.depsDir) !== "0.3.318") throw new Error(`npm finished but ${spec} is not in ${opts.depsDir}`);
16272
17431
  progress("Checking the install");
16273
17432
  await importClaudeSdkFrom(opts.depsDir);
@@ -16276,6 +17435,19 @@ async function runInstall(opts) {
16276
17435
  release();
16277
17436
  }
16278
17437
  }
17438
+ /** Note the installed package in `<deps>/package.json`, as `npm install --save-exact` would. */
17439
+ function recordDependency(manifest, name, version) {
17440
+ let pkg = {};
17441
+ try {
17442
+ pkg = JSON.parse(readFileSync(manifest, "utf8"));
17443
+ } catch {}
17444
+ const deps = typeof pkg.dependencies === "object" && pkg.dependencies || {};
17445
+ pkg.dependencies = {
17446
+ ...deps,
17447
+ [name]: version
17448
+ };
17449
+ writeFileSync(manifest, `${JSON.stringify(pkg, null, 2)}\n`);
17450
+ }
16279
17451
  // ../../packages/providers/src/claude/sdk/load.ts
16280
17452
  /** Where the SDK comes from in this process. `CODEDEN_CLAUDE_SDK=deps|workspace` overrides. */
16281
17453
  function claudeSdkSource() {
@@ -16293,6 +17465,15 @@ function configureClaudeSdk(opts) {
16293
17465
  loaded = void 0;
16294
17466
  }
16295
17467
  /**
17468
+ * True when the SDK can be loaded without installing it first: from the workspace, already
17469
+ * loaded, or already installed in the deps folder. Background work (model discovery) checks
17470
+ * this so it never triggers the on-demand install by itself.
17471
+ */
17472
+ function claudeSdkReady() {
17473
+ if (claudeSdkSource() === "workspace" || loaded) return true;
17474
+ return depsDir !== void 0 && isClaudeSdkInstalled(depsDir);
17475
+ }
17476
+ /**
16296
17477
  * The Claude Agent SDK module, installed first when needed. Failures are not cached, so the
16297
17478
  * next session retries the install.
16298
17479
  */
@@ -17170,6 +18351,10 @@ var ClaudeSession = class {
17170
18351
  abort;
17171
18352
  stderrTail = "";
17172
18353
  closed = false;
18354
+ /** The shell's current directory from the latest PreToolUse hook, for relative paths. */
18355
+ shellDir;
18356
+ /** Current autonomy: a query restarted after the CLI exits starts with it. */
18357
+ autonomy;
17173
18358
  constructor(opts, config) {
17174
18359
  this.opts = opts;
17175
18360
  this.config = config;
@@ -17180,6 +18365,7 @@ var ClaudeSession = class {
17180
18365
  emit
17181
18366
  });
17182
18367
  this.broker = new ApprovalBroker(emit);
18368
+ this.autonomy = opts.autonomy;
17183
18369
  this.policy = new ClaudePolicy(opts.autonomy, {
17184
18370
  cwd: opts.cwd,
17185
18371
  extraRoots: opts.additionalDirectories
@@ -17190,6 +18376,26 @@ var ClaudeSession = class {
17190
18376
  resumeToken() {
17191
18377
  return this.mapper.resumeToken ?? this.opts.resumeToken;
17192
18378
  }
18379
+ /**
18380
+ * Switch autonomy. The policy changes at once, so the next tool call (even in a running
18381
+ * turn) follows the new level; approvals already waiting stay waiting. The CLI's own
18382
+ * permission mode follows where it can (`setPermissionMode`); when the CLI refuses a mode
18383
+ * (`bypassPermissions` needs a launch flag), it keeps `acceptEdits` and CodeDen's policy
18384
+ * decides through the PreToolUse hook and `canUseTool` either way. Plan mode is left alone.
18385
+ */
18386
+ async setAutonomy(level) {
18387
+ this.autonomy = level;
18388
+ this.policy.setAutonomy(level);
18389
+ const q = this.q;
18390
+ if (!q || this.opts.planMode) return;
18391
+ const mode = permissionModeFor(level, false);
18392
+ try {
18393
+ await q.setPermissionMode(mode);
18394
+ } catch {
18395
+ if (mode !== "bypassPermissions") return;
18396
+ await q.setPermissionMode("acceptEdits").catch(() => {});
18397
+ }
18398
+ }
17193
18399
  /** Model/effort for the following turns, applied live through the SDK's streaming query. */
17194
18400
  async configure(change) {
17195
18401
  const q = this.q;
@@ -17263,13 +18469,15 @@ var ClaudeSession = class {
17263
18469
  const preToolUse = async (hookInput) => {
17264
18470
  const name = str(hookInput, "tool_name") ?? "";
17265
18471
  const toolInput = hookInput.tool_input ?? {};
18472
+ const cwd = str(hookInput, "cwd");
18473
+ if (cwd && (path.isAbsolute(cwd) || path.win32.isAbsolute(cwd))) this.shellDir = cwd;
17266
18474
  const denied = this.policy.denial(name, toolInput);
17267
18475
  if (denied) return { hookSpecificOutput: {
17268
18476
  hookEventName: "PreToolUse",
17269
18477
  permissionDecision: "deny",
17270
18478
  permissionDecisionReason: denied
17271
18479
  } };
17272
- const gate = this.policy.gate(name, toolInput);
18480
+ const gate = this.policy.gate(name, toolInput, this.shellDir);
17273
18481
  if (!gate.ask) return {};
17274
18482
  return { hookSpecificOutput: {
17275
18483
  hookEventName: "PreToolUse",
@@ -17280,7 +18488,10 @@ var ClaudeSession = class {
17280
18488
  const q = this.config.query({
17281
18489
  prompt: prompts,
17282
18490
  options: buildQueryOptions({
17283
- session: this.opts,
18491
+ session: {
18492
+ ...this.opts,
18493
+ autonomy: this.autonomy
18494
+ },
17284
18495
  executable: this.config.executable,
17285
18496
  canUseTool: this.canUseTool,
17286
18497
  hooks: { PreToolUse: [{ hooks: [preToolUse] }] },
@@ -17331,7 +18542,7 @@ var ClaudeSession = class {
17331
18542
  behavior: "deny",
17332
18543
  message: denied
17333
18544
  };
17334
- const gate = this.policy.needsUser(toolName, input);
18545
+ const gate = this.policy.needsUser(toolName, input, this.shellDir);
17335
18546
  if (!gate.ask) return {
17336
18547
  behavior: "allow",
17337
18548
  updatedInput: input
@@ -17437,7 +18648,8 @@ const capabilities$2 = {
17437
18648
  planMode: true,
17438
18649
  reasoningEffort: true,
17439
18650
  mcp: true,
17440
- skills: true
18651
+ skills: true,
18652
+ liveAutonomy: true
17441
18653
  };
17442
18654
  const EFFORTS = [
17443
18655
  "low",
@@ -17446,8 +18658,8 @@ const EFFORTS = [
17446
18658
  "xhigh",
17447
18659
  "max"
17448
18660
  ];
17449
- /** Fallback model list (CLI aliases) until a session reports the real one. */
17450
- const FALLBACK_MODELS$1 = [
18661
+ /** Fallback model list (CLI aliases) until discovery or a session reports the real one. */
18662
+ const FALLBACK_MODELS = [
17451
18663
  {
17452
18664
  id: "default",
17453
18665
  label: "Default",
@@ -17476,7 +18688,8 @@ var ClaudeAdapter = class {
17476
18688
  options;
17477
18689
  id = CLAUDE_PROVIDER_ID;
17478
18690
  displayName = "Claude Code";
17479
- modelList = FALLBACK_MODELS$1;
18691
+ catalog = new ModelCatalog(FALLBACK_MODELS);
18692
+ refreshing;
17480
18693
  cachedExe;
17481
18694
  constructor(options = {}) {
17482
18695
  this.options = options;
@@ -17493,7 +18706,22 @@ var ClaudeAdapter = class {
17493
18706
  return { ...capabilities$2 };
17494
18707
  }
17495
18708
  models() {
17496
- return this.modelList.map((m) => ({ ...m }));
18709
+ return this.catalog.list();
18710
+ }
18711
+ onModelsChanged(listener) {
18712
+ return this.catalog.subscribe(listener);
18713
+ }
18714
+ /**
18715
+ * Load Claude Code's model list without a turn (`listClaudeModels`: the CLI starts and
18716
+ * answers `initialize`; no message is sent). Concurrent calls share one discovery; a failed
18717
+ * discovery keeps the last list. Skipped when Claude Code is not installed, or when the SDK
18718
+ * would have to be installed first. Never throws.
18719
+ */
18720
+ refreshModels() {
18721
+ this.refreshing ??= this.discoverModels().finally(() => {
18722
+ this.refreshing = void 0;
18723
+ });
18724
+ return this.refreshing;
17497
18725
  }
17498
18726
  async openSession(opts) {
17499
18727
  const executable = this.executable();
@@ -17503,20 +18731,24 @@ var ClaudeAdapter = class {
17503
18731
  executable,
17504
18732
  query,
17505
18733
  onModels: (models) => {
17506
- if (!models.length) return;
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
- }));
18734
+ if (models.length) this.catalog.set(toModelInfos(models));
17514
18735
  }
17515
18736
  });
17516
18737
  }
17517
18738
  utilityModel() {
17518
18739
  return new ClaudeUtilityModel(() => this.executable());
17519
18740
  }
18741
+ async discoverModels() {
18742
+ const exe = this.executable();
18743
+ if (!exe) return;
18744
+ const load = this.options.loadSdk ?? (claudeSdkReady() ? loadClaudeSdk : void 0);
18745
+ if (!load) return;
18746
+ try {
18747
+ const { query } = await load();
18748
+ const models = await listClaudeModels(query, exe, { timeoutMs: this.options.modelListTimeoutMs });
18749
+ if (models.length) this.catalog.set(models);
18750
+ } catch {}
18751
+ }
17520
18752
  };
17521
18753
  // ../../packages/providers/src/codex/detect.ts
17522
18754
  /** Oldest Codex version whose app-server protocol matches the generated types. */
@@ -17766,7 +18998,10 @@ const OPT_OUT = [
17766
18998
  "fuzzyFileSearch/sessionUpdated",
17767
18999
  "fuzzyFileSearch/sessionCompleted"
17768
19000
  ];
17769
- /** Spawn `codex app-server` (stdio) and complete the `initialize` handshake. */
19001
+ /**
19002
+ * Spawn `codex app-server` (stdio) and complete the `initialize` handshake. Aborting `signal`
19003
+ * stops the process at any point, including during the handshake.
19004
+ */
17770
19005
  async function startAppServer(exe, handlers, opts = {}) {
17771
19006
  const rpc = new CodexRpc(spawnResolved(exe, ["app-server"], {
17772
19007
  stdio: [
@@ -17780,6 +19015,10 @@ async function startAppServer(exe, handlers, opts = {}) {
17780
19015
  },
17781
19016
  ...opts.cwd ? { cwd: opts.cwd } : {}
17782
19017
  }), handlers);
19018
+ if (opts.signal) {
19019
+ if (opts.signal.aborted) rpc.close();
19020
+ else opts.signal.addEventListener("abort", () => void rpc.close(), { once: true });
19021
+ }
17783
19022
  const params = {
17784
19023
  clientInfo: {
17785
19024
  name: "codeden",
@@ -17804,6 +19043,69 @@ async function startAppServer(exe, handlers, opts = {}) {
17804
19043
  throw error;
17805
19044
  }
17806
19045
  }
19046
+ /** `model/list` pages are followed up to this many (a guard against a looping cursor). */
19047
+ const MAX_PAGES = 20;
19048
+ /**
19049
+ * Map an app-server `Model` to CodeDen's `ModelInfo`. The id is the model slug Codex accepts
19050
+ * in `thread/start` and `turn/start`. Efforts CodeDen does not know are dropped, and missing
19051
+ * fields (older app-servers) are tolerated.
19052
+ */
19053
+ function modelInfoFromCodex(m) {
19054
+ const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort).filter((e) => ReasoningEffort.safeParse(e).success);
19055
+ const defaultEffort = ReasoningEffort.safeParse(m.defaultReasoningEffort);
19056
+ return {
19057
+ id: m.model,
19058
+ label: m.displayName || m.model,
19059
+ ...m.description ? { description: m.description } : {},
19060
+ efforts,
19061
+ ...defaultEffort.success ? { defaultEffort: defaultEffort.data } : {},
19062
+ ...m.isDefault ? { isDefault: true } : {}
19063
+ };
19064
+ }
19065
+ /** The models Codex shows in its own picker, as `ModelInfo`. */
19066
+ function visibleCodexModels(models) {
19067
+ return models.filter((m) => !m.hidden && m.model).map(modelInfoFromCodex);
19068
+ }
19069
+ /**
19070
+ * Ask a short-lived `codex app-server` for its model list (`model/list`, every page). No
19071
+ * thread is started and no turn runs. Rejects on failure or after `timeoutMs`; the process
19072
+ * is stopped either way.
19073
+ */
19074
+ async function listCodexModels(exe, opts = {}) {
19075
+ const timeoutMs = opts.timeoutMs ?? 2e4;
19076
+ const abort = new AbortController();
19077
+ let timer;
19078
+ const timeout = new Promise((_, reject) => {
19079
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`codex app-server did not list models within ${timeoutMs}ms`)), timeoutMs);
19080
+ });
19081
+ try {
19082
+ return await Promise.race([fetchModels(exe, abort.signal), timeout]);
19083
+ } finally {
19084
+ clearTimeout(timer);
19085
+ abort.abort();
19086
+ }
19087
+ }
19088
+ async function fetchModels(exe, signal) {
19089
+ const { rpc } = await startAppServer(exe, {
19090
+ onNotification: () => {},
19091
+ onRequest: async () => {
19092
+ throw new Error("CodeDen's model discovery answers no requests");
19093
+ },
19094
+ onExit: () => {}
19095
+ }, {
19096
+ cwd: tmpdir(),
19097
+ signal
19098
+ });
19099
+ const models = [];
19100
+ let cursor = null;
19101
+ for (let page = 0; page < MAX_PAGES; page += 1) {
19102
+ const res = await rpc.request("model/list", cursor ? { cursor } : {});
19103
+ models.push(...res.data ?? []);
19104
+ cursor = res.nextCursor ?? null;
19105
+ if (!cursor) break;
19106
+ }
19107
+ return visibleCodexModels(models);
19108
+ }
17807
19109
  /**
17808
19110
  * One-shot Codex call for the utility model: an ephemeral thread in a read-only sandbox with
17809
19111
  * approvals off, low reasoning effort, and no MCP servers. Returns the final agent message.
@@ -18001,10 +19303,11 @@ async function handleServerRequest(req, ac) {
18001
19303
  /**
18002
19304
  * Native sandbox + approval policy per autonomy level (§11.2):
18003
19305
  * - supervised -> `untrusted`: every command that isn't known-safe and every patch asks
18004
- * - auto -> workspace-write sandbox + `on-request`: the sandbox confines writes and
18005
- * network; escaping it (outside the worktree, network, force pushes) asks
19306
+ * - auto -> workspace-write sandbox + `on-request`: the sandbox confines writes (to the
19307
+ * worktree, extra roots and the OS temp directory) and network; escaping it
19308
+ * (outside the worktree, network, force pushes) asks
18006
19309
  * - full -> workspace-write + `never`, with network on: no approvals, still confined
18007
- * to the worktree
19310
+ * to the worktree, extra roots and temp
18008
19311
  */
18009
19312
  function codexPolicy(autonomy, readOnly = false) {
18010
19313
  if (readOnly) return {
@@ -18029,13 +19332,41 @@ function codexPolicy(autonomy, readOnly = false) {
18029
19332
  };
18030
19333
  }
18031
19334
  /**
19335
+ * Extra writable roots for the workspace-write sandbox: the session's additional directories,
19336
+ * plus the OS temp directories in `auto` and `full`, so writing a log or scratch file there
19337
+ * never needs an approval (§11.2).
19338
+ */
19339
+ function codexWritableRoots(opts, tempRoots = osTempRoots()) {
19340
+ const temp = opts.readOnly || opts.autonomy === "supervised" ? [] : tempRoots;
19341
+ return [.../* @__PURE__ */ new Set([...opts.additionalDirectories, ...temp])];
19342
+ }
19343
+ /**
19344
+ * The `turn/start` sandbox override for an autonomy level: the same sandbox `thread/start`
19345
+ * configures, so a mid-session change applies from the next turn.
19346
+ */
19347
+ function codexSandboxPolicy(opts, tempRoots = osTempRoots()) {
19348
+ const { sandbox, networkAccess } = codexPolicy(opts.autonomy, opts.readOnly);
19349
+ if (sandbox === "read-only") return {
19350
+ type: "readOnly",
19351
+ networkAccess
19352
+ };
19353
+ return {
19354
+ type: "workspaceWrite",
19355
+ writableRoots: codexWritableRoots(opts, tempRoots),
19356
+ networkAccess,
19357
+ excludeTmpdirEnvVar: false,
19358
+ excludeSlashTmp: false
19359
+ };
19360
+ }
19361
+ /**
18032
19362
  * Config overrides for `thread/start` / `thread/resume` (same semantics as `-c key=value`).
18033
19363
  * Sent over stdin as JSON, so MCP bearer tokens never appear on a command line.
18034
19364
  */
18035
- function codexConfigOverrides(opts) {
19365
+ function codexConfigOverrides(opts, tempRoots = osTempRoots()) {
18036
19366
  const { networkAccess } = codexPolicy(opts.autonomy, opts.readOnly);
18037
19367
  const config = { "sandbox_workspace_write.network_access": networkAccess };
18038
- if (opts.additionalDirectories.length) config["sandbox_workspace_write.writable_roots"] = [...opts.additionalDirectories];
19368
+ const roots = codexWritableRoots(opts, tempRoots);
19369
+ if (roots.length) config["sandbox_workspace_write.writable_roots"] = roots;
18039
19370
  for (const server of opts.mcpServers) Object.assign(config, mcpOverride(server));
18040
19371
  return config;
18041
19372
  }
@@ -18690,10 +20021,14 @@ function rateLimits(snapshot) {
18690
20021
  return { rateLimits: out };
18691
20022
  }
18692
20023
  // ../../packages/providers/src/codex/session.ts
18693
- /** CodeDen effort -> Codex reasoning effort (Codex tops out at `xhigh`). */
18694
- function codexEffort(effort) {
20024
+ /**
20025
+ * CodeDen effort -> Codex reasoning effort. `max` is sent only to models that list it
20026
+ * (`supported`); older models top out at `xhigh`.
20027
+ */
20028
+ function codexEffort(effort, supported) {
18695
20029
  if (!effort) return void 0;
18696
- return effort === "max" ? "xhigh" : effort;
20030
+ if (effort !== "max") return effort;
20031
+ return supported?.includes("max") ? "max" : "xhigh";
18697
20032
  }
18698
20033
  /**
18699
20034
  * A Codex session over `codex app-server` (§7.2): one app-server process and one thread per
@@ -18710,7 +20045,12 @@ var CodexSession = class {
18710
20045
  turnPending = false;
18711
20046
  /** Model/effort changes from `configure`, applied on every following turn. */
18712
20047
  nextTurn = {};
20048
+ /** Current autonomy; after `setAutonomy`, every following `turn/start` carries it. */
20049
+ autonomy;
20050
+ autonomyChanged = false;
18713
20051
  closed = false;
20052
+ /** The model the thread started with (the user's Codex config when none was chosen). */
20053
+ threadModel;
18714
20054
  filePaths = /* @__PURE__ */ new Map();
18715
20055
  constructor(opts, config) {
18716
20056
  this.opts = opts;
@@ -18722,6 +20062,7 @@ var CodexSession = class {
18722
20062
  this.mapper = new CodexEventMapper(opts.sessionId, emit);
18723
20063
  this.mapper.codexThreadId = opts.resumeToken;
18724
20064
  this.broker = new ApprovalBroker(emit);
20065
+ this.autonomy = opts.autonomy;
18725
20066
  }
18726
20067
  /** Start the app-server and the thread. Called by the adapter before returning the session. */
18727
20068
  async start() {
@@ -18741,19 +20082,29 @@ var CodexSession = class {
18741
20082
  ...change
18742
20083
  };
18743
20084
  }
20085
+ /**
20086
+ * Autonomy from the next `turn/start` on, as approval-policy and sandbox overrides. A running
20087
+ * turn keeps the policy it started with (the app-server fixes it per turn).
20088
+ */
20089
+ async setAutonomy(level) {
20090
+ this.autonomy = level;
20091
+ this.autonomyChanged = true;
20092
+ }
18744
20093
  async send(input) {
18745
20094
  if (this.closed) throw new ProviderSessionError("closed", "Session is closed");
18746
20095
  if (this.turnPending || this.mapper.currentTurnId) throw new ProviderSessionError("turn_running", "A turn is already running");
18747
20096
  this.turnPending = true;
18748
20097
  try {
18749
20098
  const rpc = await this.ensureServer();
18750
- const effort = codexEffort(this.nextTurn.effort !== void 0 ? this.nextTurn.effort ?? void 0 : this.opts.effort);
18751
20099
  const model = await this.turnModel(rpc);
20100
+ const inUse = model ?? explicitModel(this.opts.model) ?? this.threadModel;
20101
+ const effort = codexEffort(this.nextTurn.effort !== void 0 ? this.nextTurn.effort ?? void 0 : this.opts.effort, inUse ? this.config.modelEfforts?.(inUse) : void 0);
18752
20102
  const res = await rpc.request("turn/start", {
18753
20103
  threadId: this.threadId(),
18754
20104
  input: toCodexInput(input),
18755
20105
  ...effort ? { effort } : {},
18756
- ...model !== void 0 ? { model } : {}
20106
+ ...model !== void 0 ? { model } : {},
20107
+ ...this.autonomyChanged ? this.policyOverride() : {}
18757
20108
  });
18758
20109
  this.mapper.beginTurn(res.turn.id);
18759
20110
  } catch (error) {
@@ -18795,6 +20146,21 @@ var CodexSession = class {
18795
20146
  this.events.close();
18796
20147
  await this.rpc?.close();
18797
20148
  }
20149
+ /** Session options with the current autonomy (a restarted app-server resumes with it). */
20150
+ currentOptions() {
20151
+ return {
20152
+ ...this.opts,
20153
+ autonomy: this.autonomy
20154
+ };
20155
+ }
20156
+ /** `turn/start` overrides that carry the current autonomy (§11.2). */
20157
+ policyOverride() {
20158
+ const opts = this.currentOptions();
20159
+ return {
20160
+ approvalPolicy: codexPolicy(opts.autonomy, opts.readOnly).approvalPolicy,
20161
+ sandboxPolicy: codexSandboxPolicy(opts)
20162
+ };
20163
+ }
18798
20164
  threadId() {
18799
20165
  const id = this.mapper.codexThreadId;
18800
20166
  if (!id) throw new ProviderSessionError("not_started", "Codex thread is not started");
@@ -18828,12 +20194,13 @@ var CodexSession = class {
18828
20194
  cwd: this.opts.cwd
18829
20195
  });
18830
20196
  this.rpc = rpc;
18831
- const { approvalPolicy, sandbox } = codexPolicy(this.opts.autonomy, this.opts.readOnly);
20197
+ const opts = this.currentOptions();
20198
+ const { approvalPolicy, sandbox } = codexPolicy(opts.autonomy, opts.readOnly);
18832
20199
  const common = {
18833
20200
  cwd: this.opts.cwd,
18834
20201
  approvalPolicy,
18835
20202
  sandbox,
18836
- config: codexConfigOverrides(this.opts),
20203
+ config: codexConfigOverrides(opts),
18837
20204
  ...explicitModel(this.opts.model) ? { model: explicitModel(this.opts.model) } : {},
18838
20205
  ...this.opts.systemPrompt ? { developerInstructions: this.opts.systemPrompt } : {}
18839
20206
  };
@@ -18860,6 +20227,7 @@ var CodexSession = class {
18860
20227
  }
18861
20228
  res ??= await rpc.request("thread/start", common);
18862
20229
  this.mapper.codexThreadId = res.thread.id;
20230
+ this.threadModel = res.model || void 0;
18863
20231
  this.events.push({
18864
20232
  type: "session.updated",
18865
20233
  resumeToken: res.thread.id,
@@ -18940,40 +20308,38 @@ const capabilities$1 = {
18940
20308
  planMode: false,
18941
20309
  reasoningEffort: true,
18942
20310
  mcp: true,
18943
- skills: true
20311
+ skills: true,
20312
+ liveAutonomy: false
18944
20313
  };
18945
- const FALLBACK_MODELS = [{
18946
- id: "default",
18947
- label: "Default",
18948
- description: "The model configured in ~/.codex/config.toml",
18949
- efforts: [
18950
- "minimal",
18951
- "low",
18952
- "medium",
18953
- "high",
18954
- "xhigh"
18955
- ],
18956
- isDefault: true
18957
- }];
18958
- /** Map an app-server `Model` to CodeDen's `ModelInfo`. */
18959
- function modelInfoFromCodex(m) {
18960
- const efforts = m.supportedReasoningEfforts.map((e) => e.reasoningEffort).filter((e) => ReasoningEffort.safeParse(e).success);
18961
- const defaultEffort = ReasoningEffort.safeParse(m.defaultReasoningEffort);
18962
- return {
18963
- id: m.model,
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
- };
20314
+ const CONFIGURED_MODEL = "The model configured in ~/.codex/config.toml";
20315
+ /**
20316
+ * Until Codex has listed its models, one entry that leaves the choice to the user's Codex
20317
+ * config. `note` says why the real list is missing when discovery failed.
20318
+ */
20319
+ function fallbackModels(note) {
20320
+ return [{
20321
+ id: DEFAULT_MODEL_ID,
20322
+ label: "Default",
20323
+ description: note ? `${CONFIGURED_MODEL}. ${note}` : CONFIGURED_MODEL,
20324
+ efforts: [
20325
+ "minimal",
20326
+ "low",
20327
+ "medium",
20328
+ "high",
20329
+ "xhigh"
20330
+ ],
20331
+ isDefault: true
20332
+ }];
18970
20333
  }
18971
20334
  /** Codex through `codex app-server` JSON-RPC (§7.2). */
18972
20335
  var CodexAdapter = class {
18973
20336
  options;
18974
20337
  id = CODEX_PROVIDER_ID;
18975
20338
  displayName = "Codex";
18976
- modelList = FALLBACK_MODELS;
20339
+ catalog = new ModelCatalog(fallbackModels());
20340
+ /** True once Codex reported a real model list (discovery or a session). */
20341
+ listed = false;
20342
+ refreshing;
18977
20343
  cachedExe;
18978
20344
  constructor(options = {}) {
18979
20345
  this.options = options;
@@ -18990,7 +20356,20 @@ var CodexAdapter = class {
18990
20356
  return { ...capabilities$1 };
18991
20357
  }
18992
20358
  models() {
18993
- return this.modelList.map((m) => ({ ...m }));
20359
+ return this.catalog.list();
20360
+ }
20361
+ onModelsChanged(listener) {
20362
+ return this.catalog.subscribe(listener);
20363
+ }
20364
+ /**
20365
+ * Load Codex's model list through a short-lived `codex app-server` (`model/list`; no thread,
20366
+ * no turn). Concurrent calls share one discovery. Never throws.
20367
+ */
20368
+ refreshModels() {
20369
+ this.refreshing ??= this.discoverModels().finally(() => {
20370
+ this.refreshing = void 0;
20371
+ });
20372
+ return this.refreshing;
18994
20373
  }
18995
20374
  async openSession(opts) {
18996
20375
  const executable = this.executable();
@@ -18998,9 +20377,10 @@ var CodexAdapter = class {
18998
20377
  const session = new CodexSession(opts, {
18999
20378
  executable,
19000
20379
  onModels: (models) => {
19001
- const visible = models.filter((m) => !m.hidden).map(modelInfoFromCodex);
19002
- if (visible.length) this.modelList = visible;
19003
- }
20380
+ const visible = visibleCodexModels(models);
20381
+ if (visible.length) this.setModels(visible);
20382
+ },
20383
+ modelEfforts: (model) => this.catalog.list().find((m) => m.id === model)?.efforts
19004
20384
  });
19005
20385
  try {
19006
20386
  await session.start();
@@ -19013,7 +20393,65 @@ var CodexAdapter = class {
19013
20393
  utilityModel() {
19014
20394
  return new CodexUtilityModel(() => this.executable());
19015
20395
  }
20396
+ async discoverModels() {
20397
+ const exe = this.executable();
20398
+ if (!exe) return;
20399
+ try {
20400
+ const models = await listCodexModels(exe, { timeoutMs: this.options.modelListTimeoutMs });
20401
+ if (models.length) this.setModels(models);
20402
+ else this.discoveryFailed("Codex listed no models.");
20403
+ } catch (error) {
20404
+ const reason = (error instanceof Error ? error.message : String(error)).slice(0, 200);
20405
+ this.discoveryFailed(`Codex's model list could not be loaded: ${reason}`);
20406
+ }
20407
+ }
20408
+ setModels(models) {
20409
+ this.listed = true;
20410
+ this.catalog.set(models);
20411
+ }
20412
+ /** Keep the last real list; without one, say why only the configured default is offered. */
20413
+ discoveryFailed(note) {
20414
+ if (!this.listed) this.catalog.set(fallbackModels(note));
20415
+ }
20416
+ };
20417
+ // ../../packages/providers/src/fake/mcp-result.ts
20418
+ /** A long, unbroken token, like the ids, hashes and base64 blobs real tool results carry. */
20419
+ const blob = "c0de".repeat(12) + "d3n".repeat(60);
20420
+ /** Input of the scripted `[mcp]` tool call: an issue reference with a long unbroken URL. */
20421
+ const fakeMcpInput = {
20422
+ issue: {
20423
+ number: 3,
20424
+ url: `https://github.com/codeden-e2e/demo-repo/issues/3?trace=${blob}`
20425
+ },
20426
+ priority: "low"
19016
20427
  };
20428
+ /**
20429
+ * Result text of the scripted `[mcp]` tool call: one compact line of JSON, several kilobytes
20430
+ * long, shaped like a triage result. Lets tests check that long tool output stays inside the
20431
+ * transcript column.
20432
+ */
20433
+ function fakeMcpResult() {
20434
+ const body = Array.from({ length: 12 }, (_, i) => `Step ${i + 1}: list the models the provider reports before a session starts.`).join(" ");
20435
+ return JSON.stringify({ issue: {
20436
+ projectId: "a72f9074-7b37-4693-bbae-8b108576aefc",
20437
+ number: 3,
20438
+ title: "Discover the real model list without running a session first",
20439
+ state: "open",
20440
+ url: `https://github.com/codeden-e2e/demo-repo/issues/3?trace=${blob}`,
20441
+ labels: [
20442
+ "enhancement",
20443
+ "providers",
20444
+ "low-priority"
20445
+ ],
20446
+ body,
20447
+ trace: blob,
20448
+ blockedBy: [{
20449
+ number: 2,
20450
+ title: "Model picker redesign",
20451
+ state: "open"
20452
+ }]
20453
+ } });
20454
+ }
19017
20455
  // ../../packages/providers/src/fake/session.ts
19018
20456
  var Interrupted$1 = class extends Error {};
19019
20457
  /** A scripted, deterministic session. Ids are counters scoped to the session. */
@@ -19029,6 +20467,7 @@ var FakeSession = class {
19029
20467
  constructor(opts, fake) {
19030
20468
  this.opts = opts;
19031
20469
  this.fake = fake;
20470
+ this.autonomy = opts.autonomy;
19032
20471
  this.token = opts.resumeToken ?? `fake-${opts.sessionId}`;
19033
20472
  this.rootThreadId = `${opts.sessionId}:thread-root`;
19034
20473
  this.events.push({
@@ -19056,6 +20495,11 @@ var FakeSession = class {
19056
20495
  model: change.model ?? "fake-1"
19057
20496
  });
19058
20497
  }
20498
+ /** Current autonomy. Full skips the scripted command approval, as it would for real. */
20499
+ autonomy;
20500
+ async setAutonomy(level) {
20501
+ this.autonomy = level;
20502
+ }
19059
20503
  async send(input) {
19060
20504
  if (this.events.isClosed) throw new ProviderSessionError("closed", "Session is closed");
19061
20505
  if (this.turn) throw new ProviderSessionError("turn_running", "A turn is already running");
@@ -19137,7 +20581,8 @@ var FakeSession = class {
19137
20581
  }
19138
20582
  await this.streamAssistant(turn, threadId, this.replyFor(prompt));
19139
20583
  await this.todoList(turn);
19140
- if (this.fake.approval || prompt.includes("[approval]") ? await this.approval(turn) : true) await this.commandToolCall(turn);
20584
+ if (prompt.includes("[mcp]")) await this.mcpToolCall(turn);
20585
+ if ((this.fake.approval || prompt.includes("[approval]")) && this.autonomy !== "full" ? await this.approval(turn) : true) await this.commandToolCall(turn);
19141
20586
  if (this.fake.subagent || prompt.includes("[subagent]")) await this.subagent(turn);
19142
20587
  if (this.fake.followups ?? true) {
19143
20588
  await this.step(turn);
@@ -19322,6 +20767,45 @@ var FakeSession = class {
19322
20767
  });
19323
20768
  return allowed;
19324
20769
  }
20770
+ /** An MCP tool call with a long, one-line JSON result (`[mcp]` in the prompt). */
20771
+ async mcpToolCall(turn) {
20772
+ const { threadId, turnId } = turn;
20773
+ const base = {
20774
+ id: this.nextId("item"),
20775
+ kind: "tool_call",
20776
+ toolKind: "mcp",
20777
+ toolName: "mcp__codeden__triage_issue",
20778
+ server: "codeden",
20779
+ tool: "triage_issue",
20780
+ title: "codeden: triage_issue",
20781
+ input: fakeMcpInput
20782
+ };
20783
+ await this.step(turn);
20784
+ this.emit({
20785
+ type: "item.started",
20786
+ threadId,
20787
+ turnId,
20788
+ item: {
20789
+ ...base,
20790
+ status: "in_progress"
20791
+ }
20792
+ });
20793
+ await this.step(turn);
20794
+ this.emit({
20795
+ type: "item.completed",
20796
+ threadId,
20797
+ turnId,
20798
+ item: {
20799
+ ...base,
20800
+ status: "completed",
20801
+ output: {
20802
+ text: fakeMcpResult(),
20803
+ truncated: false
20804
+ },
20805
+ durationMs: 12
20806
+ }
20807
+ });
20808
+ }
19325
20809
  async commandToolCall(turn) {
19326
20810
  const { threadId, turnId } = turn;
19327
20811
  const id = this.nextId("item");
@@ -19490,13 +20974,15 @@ const capabilities = {
19490
20974
  planMode: false,
19491
20975
  reasoningEffort: false,
19492
20976
  mcp: false,
19493
- skills: false
20977
+ skills: false,
20978
+ liveAutonomy: true
19494
20979
  };
19495
20980
  /**
19496
20981
  * Deterministic scripted provider for demos and tests (§7.2). Every turn streams:
19497
20982
  * user message, assistant message (word deltas), a todo list, a command tool call,
19498
20983
  * optionally an approval request (waits for `respond`) and a subagent child thread,
19499
- * followups, usage, then `turn.completed`. Prompts containing `[fail]` fail the turn.
20984
+ * followups, usage, then `turn.completed`. Prompts containing `[fail]` fail the turn;
20985
+ * `[mcp]` adds an MCP tool call with a long JSON result before the command.
19500
20986
  */
19501
20987
  var FakeProvider = class {
19502
20988
  options;
@@ -20449,12 +21935,14 @@ function createImageEngineRegistry(opts = {}) {
20449
21935
  /** The set of adapters a runner has loaded, keyed by id. */
20450
21936
  var ProviderRegistry = class {
20451
21937
  adapters = /* @__PURE__ */ new Map();
21938
+ listeners = /* @__PURE__ */ new Set();
20452
21939
  constructor(adapters = []) {
20453
21940
  for (const adapter of adapters) this.register(adapter);
20454
21941
  }
20455
21942
  register(adapter) {
20456
21943
  if (this.adapters.has(adapter.id)) throw new Error(`Provider "${adapter.id}" is already registered`);
20457
21944
  this.adapters.set(adapter.id, adapter);
21945
+ adapter.onModelsChanged?.(() => this.notify());
20458
21946
  }
20459
21947
  get(id) {
20460
21948
  return this.adapters.get(id);
@@ -20465,6 +21953,24 @@ var ProviderRegistry = class {
20465
21953
  info() {
20466
21954
  return this.list().map(providerInfo);
20467
21955
  }
21956
+ /**
21957
+ * Refresh the model lists of all adapters (or those in `ids`) that support it. Never throws;
21958
+ * changes reach `onChange` listeners.
21959
+ */
21960
+ async refreshModels(ids) {
21961
+ const adapters = ids ? this.list().filter((a) => ids.includes(a.id)) : this.list();
21962
+ await Promise.all(adapters.map((a) => a.refreshModels?.().catch(() => void 0)));
21963
+ }
21964
+ /** Call `listener` whenever `info()` changes (a provider's models). Returns unsubscribe. */
21965
+ onChange(listener) {
21966
+ this.listeners.add(listener);
21967
+ return () => this.listeners.delete(listener);
21968
+ }
21969
+ notify() {
21970
+ for (const listener of this.listeners) try {
21971
+ listener();
21972
+ } catch {}
21973
+ }
20468
21974
  };
20469
21975
  /** Fresh instances of the built-in real providers (Claude Code, Codex). */
20470
21976
  function builtinProviders() {
@@ -23983,7 +25489,8 @@ function installChromium(log) {
23983
25489
  "chromium"
23984
25490
  ], {
23985
25491
  windowsHide: true,
23986
- timeout: 6e5
25492
+ timeout: 6e5,
25493
+ env: ownNodeEnv()
23987
25494
  }, (error) => error ? reject(error) : resolve());
23988
25495
  });
23989
25496
  }
@@ -24022,13 +25529,18 @@ function firstLine(error) {
24022
25529
  /** Names of CodeDen-internal variables that must never reach agents, shells or setup scripts. */
24023
25530
  const SECRET = /^CODEDEN_.*(TOKEN|SECRET|KEY|PASSWORD)/i;
24024
25531
  /**
25532
+ * Set when the desktop app runs CodeDen's own processes on its Electron binary (§17). It must
25533
+ * not reach the user's shells and agents: an Electron app started there would run as Node.js.
25534
+ */
25535
+ const RUNTIME_ONLY = /* @__PURE__ */ new Set(["ELECTRON_RUN_AS_NODE"]);
25536
+ /**
24025
25537
  * Environment for child processes (§15): the user's environment minus CodeDen's own
24026
25538
  * secrets. `CODEDEN_HOME` and similar non-secret variables are kept.
24027
25539
  */
24028
25540
  function childEnv(extra = {}) {
24029
25541
  const out = {};
24030
25542
  for (const [key, value] of Object.entries(process.env)) {
24031
- if (value === void 0 || SECRET.test(key)) continue;
25543
+ if (value === void 0 || SECRET.test(key) || RUNTIME_ONLY.has(key)) continue;
24032
25544
  out[key] = value;
24033
25545
  }
24034
25546
  return {
@@ -24345,6 +25857,22 @@ async function topLevel(cwd) {
24345
25857
  return resolve(await gitText(cwd, ["rev-parse", "--show-toplevel"]));
24346
25858
  }
24347
25859
  /**
25860
+ * Copy the index for use as a temporary index, keeping its modification time. Git trusts an
25861
+ * entry's cached stat data only when the entry is older than the index file; an entry written
25862
+ * in the same timestamp tick as the index is "racily clean" and its content is re-read. A
25863
+ * copy stamped with the current time (Linux `copyFile` does that) would make such entries
25864
+ * look safe, and a same-size rewrite in that tick would be missed by diffs and checkpoints.
25865
+ */
25866
+ async function copyIndex(realIndex, tmp) {
25867
+ try {
25868
+ await copyFile(realIndex, tmp);
25869
+ } catch {
25870
+ return;
25871
+ }
25872
+ const { atime, mtime } = await stat(realIndex);
25873
+ await utimes(tmp, atime, mtime);
25874
+ }
25875
+ /**
24348
25876
  * Write the full working tree (tracked + untracked, minus ignored files) as a tree object
24349
25877
  * using a temporary index seeded from a copy of the real one (for its stat cache). The
24350
25878
  * user's index, HEAD and branch are never modified.
@@ -24359,7 +25887,7 @@ async function snapshotTree(cwd) {
24359
25887
  ]);
24360
25888
  const tmp = join(tmpdir(), `codeden-index-${randomBytes(8).toString("hex")}`);
24361
25889
  try {
24362
- await copyFile(realIndex, tmp).catch(() => void 0);
25890
+ await copyIndex(realIndex, tmp);
24363
25891
  const env = { GIT_INDEX_FILE: tmp };
24364
25892
  await git(top, [
24365
25893
  "add",
@@ -25008,6 +26536,15 @@ async function prepareWorkspace(cmd, ctx) {
25008
26536
  setup
25009
26537
  };
25010
26538
  }
26539
+ /**
26540
+ * Refuse to remove a worktree with uncommitted changes (`worktree_dirty`) unless forced. Runs
26541
+ * before terminals and processes in the worktree are stopped, so a dirty worktree is left
26542
+ * exactly as it was. Ignored files (build output, `node_modules`) don't count.
26543
+ */
26544
+ async function assertRemovable(cmd) {
26545
+ if (cmd.force || !existsSync(cmd.path) || !await isWorkTree(cmd.path)) return;
26546
+ if ((await git(cmd.path, ["status", "--porcelain"])).stdout.toString("utf8").trim()) throw new CommandError("worktree_dirty", `${cmd.path} has uncommitted changes; commit them or remove it with force`);
26547
+ }
25011
26548
  /** `workspace.remove`: remove a worktree (retrying briefly for Windows file locks). */
25012
26549
  async function removeWorkspace(cmd) {
25013
26550
  const path = resolve(cmd.path);
@@ -25069,7 +26606,9 @@ async function executeCommand(core, command) {
25069
26606
  return {};
25070
26607
  case "provider.detect": {
25071
26608
  const adapters = command.providerId ? registry.list().filter((a) => a.id === command.providerId) : registry.list();
25072
- return { statuses: await Promise.all(adapters.map((a) => a.detect())) };
26609
+ const statuses = await Promise.all(adapters.map((a) => a.detect()));
26610
+ registry.refreshModels(adapters.map((a) => a.id));
26611
+ return { statuses };
25073
26612
  }
25074
26613
  case "provider.oneShot": {
25075
26614
  const utility = registry.get(command.providerId)?.utilityModel?.();
@@ -25093,6 +26632,7 @@ async function executeCommand(core, command) {
25093
26632
  emit: core.emit
25094
26633
  });
25095
26634
  case "workspace.remove":
26635
+ await assertRemovable(command);
25096
26636
  await terminals.closeUnder(command.path);
25097
26637
  await core.supervisor.killUnder(command.path);
25098
26638
  return removeWorkspace(command);
@@ -26808,6 +28348,10 @@ var ServerConnection = class {
26808
28348
  for (const event of this.core.journal.after(msg.lastAckedSeq)) this.send(event);
26809
28349
  this.attached = true;
26810
28350
  this.onAttach(this);
28351
+ this.send({
28352
+ type: "providers",
28353
+ providers: this.core.registry.info()
28354
+ });
26811
28355
  return;
26812
28356
  case "ack":
26813
28357
  this.core.journal.ack(msg.seq);
@@ -26967,14 +28511,20 @@ var SessionHost = class {
26967
28511
  async interrupt(sessionId) {
26968
28512
  await this.get(sessionId).session.interrupt();
26969
28513
  }
26970
- /** Model/effort for the next turn, when the provider can change them live. */
28514
+ /**
28515
+ * Model/effort for the next turn and the autonomy level, when the provider can change them
28516
+ * live. Checks every requested setting before applying any.
28517
+ */
26971
28518
  async configure(cmd) {
26972
28519
  const { session } = this.get(cmd.sessionId);
26973
- if (!session.configure) throw new CommandError("unsupported", "This provider cannot change model or effort live");
26974
28520
  const change = {};
26975
28521
  if (cmd.model !== void 0) change.model = cmd.model;
26976
28522
  if (cmd.effort !== void 0) change.effort = cmd.effort;
26977
- await session.configure(change);
28523
+ const modelOrEffort = Object.keys(change).length > 0;
28524
+ if (modelOrEffort && !session.configure) throw new CommandError("unsupported", "This provider cannot change model or effort live");
28525
+ if (cmd.autonomy !== void 0 && !session.setAutonomy) throw new CommandError("unsupported", "This provider cannot change autonomy live");
28526
+ if (modelOrEffort) await session.configure?.(change);
28527
+ if (cmd.autonomy !== void 0) await session.setAutonomy?.(cmd.autonomy);
26978
28528
  }
26979
28529
  async respond(cmd) {
26980
28530
  await this.get(cmd.sessionId).session.respond(cmd.requestId, cmd.response);
@@ -27141,6 +28691,12 @@ async function startRunner(opts) {
27141
28691
  stream: data
27142
28692
  });
27143
28693
  };
28694
+ const stopProviderUpdates = registry.onChange(() => {
28695
+ if (!stopped) attached?.deliver({
28696
+ type: "providers",
28697
+ providers: registry.info()
28698
+ });
28699
+ });
27144
28700
  const supervisor = new ProcessSupervisor();
27145
28701
  const host = new SessionHost(registry, emit, { log });
27146
28702
  const terminals = new TerminalHost({
@@ -27237,11 +28793,13 @@ async function startRunner(opts) {
27237
28793
  writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}\n`, { mode: 384 });
27238
28794
  renameSync(tmp, lockFile);
27239
28795
  log(`runner ${journal.runnerId} listening on 127.0.0.1:${port}`);
28796
+ registry.refreshModels();
27240
28797
  const removeLock = () => removeOwnLock(lockFile);
27241
28798
  process.once("exit", removeLock);
27242
28799
  async function close() {
27243
28800
  closing ??= (async () => {
27244
28801
  removeLock();
28802
+ stopProviderUpdates();
27245
28803
  await host.closeAll();
27246
28804
  await terminals.closeAll();
27247
28805
  await browserManager.shutdown().catch((error) => log(`browser: ${String(error)}`));
@@ -27266,6 +28824,6 @@ async function startRunner(opts) {
27266
28824
  close
27267
28825
  };
27268
28826
  }
27269
- export { RunAutomationRequest as $, unknown as $n, Automation as $t, SessionFileQuery as A, McpServerTarget as An, isNewerVersion as At, BrowserClientMessage as B, custom as Bn, TriageIssueRequest as Bt, import_websocket_server as C, InboxItemKind as Cn, HealthResponse as Ct, CODEDEN_VERSION as D, AgentConfigLocator as Dn, UpdateInstallRequest as Dt, TtsRequest as E, AgentConfigListQuery as En, AppPointer as Et, ListNoticesQuery as F, datetime as Fn, GithubIssue as Ft, BrowserOpenRequest as G, looseObject as Gn, FactoryTriageRequest as Gt, BrowserHistoryRequest as H, int as Hn, parseGithubPrId as Ht, ResolveConflictRequest as I, _enum as In, GithubLabelRequest as It, AutomationFileFrontmatter as J, optional as Jn, PutVisionRequest as Jt, BrowserViewportRequest as K, number$1 as Kn, FactoryTuning as Kt, RoleMessageRequest as L, _null as Ln, GithubListQuery as Lt, RunnerToServerMessage as M, AutonomyLevel as Mn, NOTICE_BATCH_CODE as Mt, ServerLock as N, ProviderId as Nn, CreateGithubIssueRequest as Nt, UsageQuery as O, AgentConfigWriteRequest as On, UpdateStatus as Ot, ListConflictsQuery as P, ReasoningEffort as Pn, GithubCommentRequest as Pt, ListAutomationsQuery as Q, union as Qn, Artifact as Qt, UpdateSessionRequest as R, array as Rn, GithubPr as Rt, acquireInstallLock as S, InboxItem as Sn, UpdateProjectRequest as St, SttQuery as T, AgentConfigDiffRequest as Tn, Settings as Tt, BrowserNavigateRequest as U, intersection as Un, DiscoveryArea as Ut, BrowserDevServerRequest as V, discriminatedUnion as Vn, githubPrId as Vt, BrowserNewTabRequest as W, literal as Wn, FactoryCandidate as Wt, CreateAutomationRequest as X, record as Xn, SetAutonomyModeRequest as Xt, AutomationProjectRequest as Y, preprocess as Yn, ReviewVerdict as Yt, ListAutomationRunsQuery as Z, string as Zn, isFactoryMode as Zt, importClaudeSdkFrom as _, StartedBy as _n, SetSecretRequest as _t, Hono as a, DiffScope as an, UpdateAutomationProjectSettingsRequest as at, runNpm as b, DEFAULT_VISION_PATH as bn, ThreadItemsQuery as bt, serve as c, RestoreFilesRequest as cn, FileReadQuery as ct, createImageEngineRegistry as d, TerminalClientMessage as dn, ListSessionsQuery as dt, AutomationGuards as en, ZodError as er, SetAutomationEnabledRequest as et, IMAGE_ASPECT_RATIOS as f, TerminalQuery as fn, ResolveInboxRequest as ft, CLAUDE_SDK_VERSION as g, ConflictDecision as gn, SessionStartMeta as gt, claudeSdkSource as h, ProjectConfigFile as hn, SendMessageRequest as ht, killTree as i, SessionStatus as in, UpcomingRunsQuery as it, RunnerLock as j, McpServerUpsertRequest as jn, ConflictRecord as jt, UsageSettings as k, McpServerDiffRequest as kn, compareVersions$1 as kt, upgradeWebSocket as l, SessionResult as ln, FileWriteRequest as lt, ImageGenerationError as m, SessionOptions as mn, SecretKey as mt, readLock as n, TriagePolicy as nn, safeParse$1 as nr, TriageDecisionRequest as nt, tryDecodeURIComponent as o, GitCommitRequest as on, UpdateAutomationRequest as ot, resolveImageOutput as p, RunnerCommandResults as pn, RespondRequest as pt, BrowserWsQuery as q, object as qn, KillSwitchRequest as qt, isAlive as r, Session as rn, prettifyError as rr, TriageReport as rt, bufferToFormData as s, GitPushRequest as sn, CreateProjectRequest as st, startRunner as t, AutomationRun as tn, toJSONSchema as tr, StarterSetRequest as tt, ImageEngineRegistry as u, SpeechSummaryRequest as un, ListInboxQuery as ut, installClaudeSdk as v, WorkKey as vn, StartSessionRequest as vt, wrapper_default as w, AgentConfigDeleteQuery as wn, agentEventToNewEvent as wt, resolveNpm as x, FactoryLimits as xn, UiClientMessage as xt, isClaudeSdkInstalled as y, Project as yn, SteerRequest as yt, ImageTestRequest as z, boolean as zn, MergePrRequest as zt };
28827
+ export { ListAutomationRunsQuery as $, literal as $n, FactoryTuning as $t, SessionFileQuery as A, FactoryLimits as An, LocalSttModelParams as At, ImageTestRequest as B, McpServerUpsertRequest as Bn, CreateGithubIssueRequest as Bt, isElectronRuntime as C, ProjectConfigFile as Cn, ThreadItemsQuery as Ct, CODEDEN_VERSION as D, Project as Dn, agentEventToNewEvent as Dt, wrapper_default as E, WorkKey as En, HealthResponse as Et, ListConflictsQuery as F, AgentConfigListQuery as Fn, UpdateStatus as Ft, BrowserNavigateRequest as G, _enum as Gn, GithubPr as Gt, BrowserClientMessage as H, ProviderId as Hn, GithubIssue as Ht, ListNoticesQuery as I, AgentConfigLocator as In, compareVersions$1 as It, BrowserViewportRequest as J, boolean as Jn, githubPrId as Jt, BrowserNewTabRequest as K, _null as Kn, MergePrRequest as Kt, ResolveConflictRequest as L, AgentConfigWriteRequest as Ln, isNewerVersion as Lt, RunnerToServerMessage as M, InboxItemKind as Mn, TtsRequest as Mt, ServerLock as N, AgentConfigDeleteQuery as Nn, AppPointer as Nt, UsageQuery as O, sortProjects as On, Settings as Ot, ArchiveSessionRequest as P, AgentConfigDiffRequest as Pn, UpdateInstallRequest as Pt, CreateAutomationRequest as Q, intersection as Qn, FactoryTriageRequest as Qt, RoleMessageRequest as R, McpServerDiffRequest as Rn, ConflictRecord as Rt, acquireInstallLock as S, SessionOptions as Sn, SteerRequest as St, import_websocket_server as T, StartedBy as Tn, UpdateProjectRequest as Tt, BrowserDevServerRequest as U, ReasoningEffort as Un, GithubLabelRequest as Ut, summarizeActivity as V, AutonomyLevel as Vn, GithubCommentRequest as Vt, BrowserHistoryRequest as W, datetime as Wn, GithubListQuery as Wt, AutomationFileFrontmatter as X, discriminatedUnion as Xn, DiscoveryArea as Xt, BrowserWsQuery as Y, custom as Yn, parseGithubPrId as Yt, AutomationProjectRequest as Z, int as Zn, FactoryCandidate as Zt, importClaudeSdkFrom as _, SessionResult as _n, SecretKey as _t, Hono as a, Artifact as an, record as ar, TriageReport as at, runNpm as b, TerminalQuery as bn, SetSecretRequest as bt, serve as c, AutomationRun as cn, unknown as cr, UpdateAutomationRequest as ct, createImageEngineRegistry as d, SessionStatus as dn, safeParse$1 as dr, FileWriteRequest as dt, KillSwitchRequest as en, looseObject as er, ListAutomationsQuery as et, IMAGE_ASPECT_RATIOS as f, DiffScope as fn, prettifyError as fr, ListInboxQuery as ft, CLAUDE_SDK_VERSION as g, RestoreFilesRequest as gn, RespondRequest as gt, claudeSdkSource as h, GitPushRequest as hn, ResolveInboxRequest as ht, killTree as i, isFactoryMode as in, preprocess as ir, TriageDecisionRequest as it, RunnerLock as j, InboxItem as jn, SttQuery as jt, UsageSettings as k, DEFAULT_VISION_PATH as kn, LOCAL_STT_SAMPLE_RATE as kt, upgradeWebSocket as l, TriagePolicy as ln, ZodError as lr, CreateProjectRequest as lt, ImageGenerationError as m, GitCommitRequest as mn, ReorderProjectsRequest as mt, readLock as n, ReviewVerdict as nn, object as nr, SetAutomationEnabledRequest as nt, tryDecodeURIComponent as o, Automation as on, string as or, UpcomingRunsQuery as ot, resolveImageOutput as p, EditQueuedMessageRequest as pn, ListSessionsQuery as pt, BrowserOpenRequest as q, array as qn, TriageIssueRequest as qt, isAlive as r, SetAutonomyModeRequest as rn, optional as rr, StarterSetRequest as rt, bufferToFormData as s, AutomationGuards as sn, union as sr, UpdateAutomationProjectSettingsRequest as st, startRunner as t, PutVisionRequest as tn, number$1 as tr, RunAutomationRequest as tt, ImageEngineRegistry as u, Session as un, toJSONSchema as ur, FileReadQuery as ut, installClaudeSdk as v, SpeechSummaryRequest as vn, SendMessageRequest as vt, ownNodeEnv as w, ConflictDecision as wn, UiClientMessage as wt, resolveNpm as x, RunnerCommandResults as xn, StartSessionRequest as xt, isClaudeSdkInstalled as y, TerminalClientMessage as yn, SessionStartMeta as yt, UpdateSessionRequest as z, McpServerTarget as zn, NOTICE_BATCH_CODE as zt };
27270
28828
 
27271
- //# sourceMappingURL=runner-D8m3WdtS.js.map
28829
+ //# sourceMappingURL=runner-CJetH9jW.js.map