@oxbit/cli 0.7.1 → 0.9.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.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync, constants as constants$1, watch } from "node:fs";
2
+ import { realpathSync, constants as constants$1, watch, createWriteStream } from "node:fs";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import * as fs from "node:fs/promises";
5
5
  import path from "node:path";
6
- import { spawn, execFileSync } from "node:child_process";
7
- import { parseArgs } from "node:util";
6
+ import { spawn, execFile, execFileSync } from "node:child_process";
7
+ import { promisify, parseArgs } from "node:util";
8
8
  import http, { createServer } from "node:http";
9
9
  import os, { userInfo } from "node:os";
10
10
  import { createHash, randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
@@ -24,6 +24,7 @@ import ts from "typescript";
24
24
  import ignore from "ignore";
25
25
  import * as Y from "yjs";
26
26
  import { Awareness, encodeAwarenessUpdate, applyAwarenessUpdate, removeAwarenessStates } from "y-protocols/awareness";
27
+ import { pipeline } from "node:stream/promises";
27
28
  import { createSocket } from "node:dgram";
28
29
  const REQUIRED_NODE_MAJOR = 24;
29
30
  function nodeVersionError(version = process.versions.node) {
@@ -35,8 +36,8 @@ if (failure) {
35
36
  process.stderr.write(failure + "\n");
36
37
  process.exit(1);
37
38
  }
38
- function setting(name, env = process.env) {
39
- return env[`OXBIT_${name}`];
39
+ function setting(name, env2 = process.env) {
40
+ return env2[`OXBIT_${name}`];
40
41
  }
41
42
  function workspaceDataDir(root, home = os.homedir()) {
42
43
  const id = createHash("sha256").update(root).digest("hex").slice(0, 24);
@@ -54,8 +55,8 @@ const MAX_BUFFER_BYTES = 1024 * 1024;
54
55
  const READ_CHUNK_BYTES = 512 * 1024;
55
56
  const RUNTIME_SERVICE_TYPE = "_oxbit._tcp";
56
57
  class RpcError extends Error {
57
- constructor(code, message, data) {
58
- super(message);
58
+ constructor(code, message2, data) {
59
+ super(message2);
59
60
  this.code = code;
60
61
  this.data = data;
61
62
  this.name = "RpcError";
@@ -275,14 +276,14 @@ class WorkspaceFiles {
275
276
  const full = await this.resolve(relative);
276
277
  const entries = await fs.readdir(full, { withFileTypes: true });
277
278
  const result = [];
278
- for (const entry of entries) {
279
- if (entry.name === ".git" || entry.name.startsWith(".oxbit-tmp-"))
279
+ for (const entry2 of entries) {
280
+ if (entry2.name === ".git" || entry2.name.startsWith(".oxbit-tmp-"))
280
281
  continue;
281
- const child = path.posix.join(relative, entry.name);
282
+ const child = path.posix.join(relative, entry2.name);
282
283
  try {
283
284
  const target = await this.resolve(child);
284
285
  const stat = await fs.stat(target);
285
- if (entry.isSymbolicLink() && stat.isDirectory()) {
286
+ if (entry2.isSymbolicLink() && stat.isDirectory()) {
286
287
  let ancestor = path.resolve(this.root, relative || "."), cycle = false;
287
288
  while (this.inside(ancestor)) {
288
289
  if (await fs.realpath(ancestor) === target) {
@@ -297,7 +298,7 @@ class WorkspaceFiles {
297
298
  if (stat.isFile() || stat.isDirectory())
298
299
  result.push({
299
300
  path: child,
300
- name: entry.name,
301
+ name: entry2.name,
301
302
  kind: stat.isDirectory() ? "directory" : "file",
302
303
  size: stat.size,
303
304
  readonly: await this.readonly(target)
@@ -586,8 +587,8 @@ function trackProcess(pid, onExit) {
586
587
  function trackChild(child) {
587
588
  trackProcess(child.pid, (done) => child.once("close", done));
588
589
  }
589
- function resolveTerminalShell(env = process.env, platform = process.platform) {
590
- let shell = env.SHELL;
590
+ function resolveTerminalShell(env2 = process.env, platform = process.platform) {
591
+ let shell = env2.SHELL;
591
592
  if (!shell && platform !== "win32") {
592
593
  try {
593
594
  shell = userInfo().shell || void 0;
@@ -932,11 +933,11 @@ const $id$1 = "https://oxbit.dev/schemas/settings.v1.schema.json";
932
933
  const title$2 = "Oxbit Settings";
933
934
  const description$2 = "User, private workspace and repository settings. Literal dotted IDs; objects merge recursively, arrays and scalar values replace. Unrecognized extension settings are preserved. Null is a value, not a reset, and must be valid for its setting.";
934
935
  const type$2 = "object";
935
- const properties$2 = /* @__PURE__ */ JSON.parse(`{"$schema":{"type":"string","description":"JSON Schema URI. Ignored when merging effective preferences."},"agentACP.provider":{"title":"Default agent","type":"string","default":"codex","enum":["codex","cursor","amp"]},"agentACP.codex.command":{"title":"Codex ACP executable","type":"string","default":"npx"},"agentACP.codex.args":{"title":"Codex ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/codex-acp@2.2.2\\"]"},"agentACP.cursor.command":{"title":"Cursor ACP executable","type":"string","default":"agent"},"agentACP.cursor.args":{"title":"Cursor ACP arguments (JSON array)","type":"string","default":"[\\"acp\\"]"},"agentACP.amp.command":{"title":"Amp Agent ACP executable","type":"string","default":"npx"},"agentACP.amp.args":{"title":"Amp Agent ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"amp-acp@0.10.0\\"]"},"workbench.keymap":{"title":"Keymap","description":"Keyboard layout applied on top of the Oxbit defaults. User keybindings always win.","type":"string","default":"default","enum":["default","vscode","jetbrains","macos","sublime","atom","visual-studio","emacs"]},"runtime.keepAlive":{"title":"Keep Runtime Alive","description":"How long a runtime Oxbit started keeps running after the last client disconnects or its connection heartbeats stop. Terminals and tasks continue while it runs.","type":"string","default":"75s","enum":["75s","15m","1h","8h","untilStopped"]},"runtime.autoReconnect":{"title":"Reconnect Automatically","description":"Reconnect to the runtime after the connection drops, including when the iOS app returns to the foreground.","type":"boolean","default":true},"workbench.tooltipDelay":{"title":"Tooltip Delay","description":"Delay in milliseconds before showing a tooltip on hover. Keyboard focus shows tooltips immediately.","type":"number","default":400,"minimum":0,"maximum":5000},"workbench.colorTheme":{"title":"Color Theme","description":"Specifies the color theme used in the workbench.","type":"string","default":"Graphite (dark)"},"workbench.density":{"title":"Layout Density","description":"Controls row heights, padding and label sizes across the workbench.","type":"string","default":"compact","enum":["compact","comfortable"]},"workbench.sidebarLocation":{"title":"Sidebar Location","description":"Where the primary sidebar is docked.","type":"string","default":"left","enum":["left","right"]},"editor.fontFamily":{"title":"Font Family","description":"Controls the font family of the editor.","type":"string","default":"'JetBrains Mono', ui-monospace, monospace"},"editor.fontSize":{"title":"Font Size","description":"Font size in pixels.","type":"number","default":13,"minimum":8,"maximum":32},"editor.lineHeight":{"title":"Line Height","description":"Line height in pixels. 0 uses 1.55 × font size.","type":"number","default":20,"minimum":0,"maximum":60},"editor.fontLigatures":{"title":"Font Ligatures","description":"Enables font ligatures when the font supports them.","type":"boolean","default":false},"editor.tabSize":{"title":"Tab Size","description":"The number of spaces a tab is equal to.","type":"number","default":2,"minimum":1,"maximum":8},"editor.insertSpaces":{"title":"Insert Spaces","description":"Insert spaces when pressing Tab.","type":"boolean","default":true},"editor.detectIndentation":{"title":"Detect Indentation","description":"Detect Tab Size and Insert Spaces from file contents on open.","type":"boolean","default":true},"editor.renderIndentGuides":{"title":"Indentation Guides","description":"Render vertical guides for each indent level.","type":"boolean","default":true},"editor.wordWrap":{"title":"Word Wrap","description":"Controls how lines should wrap.","type":"string","default":"off","enum":["off","on","bounded"]},"editor.renderWhitespace":{"title":"Render Whitespace","description":"Controls how the editor renders whitespace characters.","type":"string","default":"selection","enum":["none","boundary","selection","all"]},"editor.cursorBlinking":{"title":"Cursor Blinking","description":"Cursor animation style.","type":"string","default":"blink","enum":["blink","smooth","phase","solid"]},"editor.minimap":{"title":"Minimap","description":"Show a minimap of the document.","type":"boolean","default":false},"files.autoSave":{"title":"Auto Save","description":"Controls auto save of editors that have unsaved changes.","type":"string","default":"off","enum":["off","afterDelay","onFocusChange","onWindowChange"]},"files.autoSaveDelay":{"title":"Auto Save Delay","description":"Delay in milliseconds after which an editor is saved automatically. Applies when Auto Save is afterDelay.","type":"number","default":1000,"minimum":100,"maximum":60000},"files.trimTrailingWhitespace":{"title":"Trim Trailing Whitespace","description":"Remove trailing whitespace when saving.","type":"boolean","default":true},"editor.formatOnSave":{"title":"Format On Save","description":"Format a file on save. A formatter must be available.","type":"boolean","default":false},"editor.defaultFormatter":{"title":"Default Formatter","description":"Formatter used when several are available.","type":"string","default":"oxbit.prettier","anyOf":[{"enum":["oxbit.prettier","oxbit.builtin-ts"]},{"type":"string"}]},"terminal.fontSize":{"title":"Terminal Font Size","description":"Font size in pixels for the terminal.","type":"number","default":12,"minimum":8,"maximum":32},"terminal.scrollback":{"title":"Scrollback","description":"Maximum number of lines kept in the terminal buffer.","type":"number","default":5000,"minimum":100,"maximum":100000},"terminal.confirmOnKill":{"title":"Confirm On Kill","description":"Ask before killing a terminal with a running process.","type":"boolean","default":true},"scm.autoFetch":{"title":"Auto Fetch","description":"Periodically fetch from the default remote.","type":"boolean","default":true},"scm.diffLayout":{"title":"Diff Layout","description":"Default layout for diff editors.","type":"string","default":"side-by-side","enum":["side-by-side","inline"]},"files.associations":{"title":"File Associations","description":"Map filename or workspace-relative glob patterns to language IDs. Explicit associations take precedence over built-in detection.","type":"object","default":{},"propertyNames":{"minLength":1,"maxLength":1024},"additionalProperties":{"type":"string","pattern":"^[\\\\w+-]+$"}},"languageServers":{"title":"Language Servers","description":"Configure enabled state, selectors, rootMarkers, executable, args, env, initializationOptions, settings, and priority by server ID. Servers install on first use in a trusted runtime. License keys belong in the runtime-local credential file.","type":"object","default":{},"propertyNames":{"pattern":"^[a-zA-Z0-9._-]+$"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Enable this language server."},"selectors":{"type":"array","items":{"type":"object","properties":{"language":{"type":"string"},"pattern":{"type":"string"},"scheme":{"const":"file"}},"additionalProperties":false,"minProperties":1}},"rootMarkers":{"type":"array","items":{"type":"string","pattern":"^(?!/)(?!.*\\\\\\\\)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))[^\\\\u0000]+$","minLength":1}},"executable":{"type":"string","pattern":"^[^\\\\u0000]*$","minLength":1},"args":{"type":"array","items":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"env":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"additionalProperties":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"initializationOptions":{"type":"object","additionalProperties":true,"description":"Server-specific initialization options. Store license credentials in the runtime credential file."},"settings":{"type":"object","additionalProperties":true,"description":"Server-specific configuration; nested objects merge across files."},"priority":{"type":"number"}},"additionalProperties":false}},"ui.fontFamily":{"title":"UI Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"ui.fontSize":{"title":"UI Font Size","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":12,"minimum":8,"maximum":72},"ui.fontWeight":{"title":"UI Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"ui.fontStyle":{"title":"UI Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"ui.lineHeight":{"title":"UI Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1.4,"minimum":1,"maximum":3},"ui.letterSpacing":{"title":"UI Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"ui.fontLigatures":{"title":"UI Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"terminal.fontFamily":{"title":"Terminal Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"terminal.fontWeight":{"title":"Terminal Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"terminal.fontStyle":{"title":"Terminal Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"terminal.lineHeight":{"title":"Terminal Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1,"minimum":1,"maximum":3},"terminal.letterSpacing":{"title":"Terminal Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"terminal.fontLigatures":{"title":"Terminal Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"workbench.iconTheme":{"title":"File Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"workbench.productIconTheme":{"title":"Product Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"editor.semanticHighlighting":{"title":"Semantic Highlighting","description":"Color symbols using language server information.","type":"boolean","default":true},"editor.inlayHints.types":{"title":"Type Inlay Hints","description":"Show supported type hints in the visible editor.","type":"boolean","default":true},"editor.inlayHints.parameters":{"title":"Parameter Inlay Hints","description":"Show supported parameter hints in the visible editor.","type":"boolean","default":true},"editor.largeFileIntelligence":{"title":"Large File Intelligence","description":"Allow full semantic highlighting and inlay hints for files larger than 1 MiB.","type":"boolean","default":false},"workbench.locale":{"title":"Display Language","type":"string","default":"en","enum":["en","de","es","ja","zh"]},"workbench.reducedMotion":{"title":"Reduced Motion","type":"boolean","default":false},"desktop.projects.openBehavior":{"title":"Open projects","type":"string","default":"currentWindow","enum":["currentWindow","newWindow"]},"desktop.tools.gitPath":{"title":"git executable path","type":"string","default":""},"desktop.tools.ghPath":{"title":"gh executable path","type":"string","default":""},"project.intelligence":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Index project dependencies, imports and related files."},"exclude":{"type":"array","items":{"type":"string","maxLength":1024},"default":[]},"maxFiles":{"type":"integer","minimum":1,"maximum":100000,"default":20000},"maxFileBytes":{"type":"integer","minimum":1,"maximum":5242880,"default":1048576}},"additionalProperties":false,"title":"Project Intelligence","description":"Partial overrides of project.json intelligence. Objects merge across files."},"project.schemas":{"type":"object","properties":{"catalog":{"type":"boolean","default":true,"description":"Discover file schemas through SchemaStore. Bundled Oxbit settings support remains available."},"download":{"type":"boolean","default":true,"description":"Allow remote schema downloads; cached and bundled schemas work offline."},"associations":{"type":"array","maxItems":1000,"default":[],"items":{"type":"object","properties":{"url":{"type":"string","minLength":1,"description":"Schema URL or workspace file URI."},"fileMatch":{"type":"array","items":{"type":"string","maxLength":1024}},"schema":{"type":["object","boolean"],"description":"An inline JSON Schema."}},"additionalProperties":false,"anyOf":[{"required":["url"]},{"required":["schema"]}]}}},"additionalProperties":false,"title":"JSON Schemas","description":"Partial overrides of project.json schemas. Association arrays replace lower arrays."}}`);
936
+ const properties$2 = /* @__PURE__ */ JSON.parse(`{"$schema":{"type":"string","description":"JSON Schema URI. Ignored when merging effective preferences."},"agentACP.provider":{"title":"Default agent","description":"A built-in agent ID, an ACP Registry agent ID, or custom.","type":"string","default":"codex"},"agentACP.useModifierToSend":{"title":"Send with Ctrl/Cmd+Enter only","description":"Enter inserts a new line and Ctrl/Cmd+Enter sends the message.","type":"boolean","default":false},"agentACP.notifications":{"title":"Agent desktop notifications","description":"Show a system notification when the agent needs input or finishes while Oxbit is in the background.","type":"string","default":"whenHidden","enum":["whenHidden","never"]},"agentACP.codex.command":{"title":"Codex ACP executable","type":"string","default":"npx"},"agentACP.codex.args":{"title":"Codex ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/codex-acp@2.2.2\\"]"},"agentACP.claude.command":{"title":"Claude Agent executable","type":"string","default":"npx"},"agentACP.claude.args":{"title":"Claude Agent arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/claude-agent-acp@0.89.1\\"]"},"agentACP.gemini.command":{"title":"Gemini CLI executable","type":"string","default":"npx"},"agentACP.gemini.args":{"title":"Gemini CLI arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@google/gemini-cli@0.63.0\\",\\"--acp\\"]"},"agentACP.copilot.command":{"title":"GitHub Copilot executable","type":"string","default":"npx"},"agentACP.copilot.args":{"title":"GitHub Copilot arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@github/copilot@1.0.95\\",\\"--acp\\"]"},"agentACP.cursor.command":{"title":"Cursor ACP executable","type":"string","default":"agent"},"agentACP.cursor.args":{"title":"Cursor ACP arguments (JSON array)","type":"string","default":"[\\"acp\\"]"},"agentACP.amp.command":{"title":"Amp Agent ACP executable","type":"string","default":"npx"},"agentACP.amp.args":{"title":"Amp Agent ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"amp-acp@0.10.0\\"]"},"agentACP.custom.name":{"title":"Custom agent name","type":"string","default":""},"agentACP.custom.command":{"title":"Custom agent executable","type":"string","default":""},"agentACP.custom.args":{"title":"Custom agent arguments (JSON array)","type":"string","default":"[]"},"agentACP.oxbit.api":{"title":"Oxbit Agent API","description":"openai uses the Responses API, anthropic the Messages API, and openai-compatible Chat Completions at the base URL.","type":"string","default":"openai","enum":["openai","anthropic","openai-compatible"]},"agentACP.oxbit.baseUrl":{"title":"Oxbit Agent base URL","description":"Origin and path prefix for openai-compatible, for example https://openrouter.ai/api.","type":"string","default":""},"agentACP.oxbit.model":{"title":"Oxbit Agent model","description":"Model ID for new conversations.","type":"string","default":""},"workbench.keymap":{"title":"Keymap","description":"Keyboard layout applied on top of the Oxbit defaults. User keybindings always win.","type":"string","default":"default","enum":["default","vscode","jetbrains","macos","sublime","atom","visual-studio","emacs"]},"runtime.keepAlive":{"title":"Keep Runtime Alive","description":"How long a runtime Oxbit started keeps running after the last client disconnects or its connection heartbeats stop. Terminals and tasks continue while it runs.","type":"string","default":"75s","enum":["75s","15m","1h","8h","untilStopped"]},"runtime.autoReconnect":{"title":"Reconnect Automatically","description":"Reconnect to the runtime after the connection drops, including when the iOS app returns to the foreground.","type":"boolean","default":true},"workbench.tooltipDelay":{"title":"Tooltip Delay","description":"Delay in milliseconds before showing a tooltip on hover. Keyboard focus shows tooltips immediately.","type":"number","default":400,"minimum":0,"maximum":5000},"workbench.colorTheme":{"title":"Color Theme","description":"Specifies the color theme used in the workbench.","type":"string","default":"Graphite (dark)"},"workbench.density":{"title":"Layout Density","description":"Controls row heights, padding and label sizes across the workbench.","type":"string","default":"compact","enum":["compact","comfortable"]},"workbench.sidebarLocation":{"title":"Sidebar Location","description":"Where the primary sidebar is docked.","type":"string","default":"left","enum":["left","right"]},"editor.fontFamily":{"title":"Font Family","description":"Controls the font family of the editor.","type":"string","default":"'JetBrains Mono', ui-monospace, monospace"},"editor.fontSize":{"title":"Font Size","description":"Font size in pixels.","type":"number","default":13,"minimum":8,"maximum":32},"editor.lineHeight":{"title":"Line Height","description":"Line height in pixels. 0 uses 1.55 × font size.","type":"number","default":20,"minimum":0,"maximum":60},"editor.fontLigatures":{"title":"Font Ligatures","description":"Enables font ligatures when the font supports them.","type":"boolean","default":false},"editor.tabSize":{"title":"Tab Size","description":"The number of spaces a tab is equal to.","type":"number","default":2,"minimum":1,"maximum":8},"editor.insertSpaces":{"title":"Insert Spaces","description":"Insert spaces when pressing Tab.","type":"boolean","default":true},"editor.detectIndentation":{"title":"Detect Indentation","description":"Detect Tab Size and Insert Spaces from file contents on open.","type":"boolean","default":true},"editor.renderIndentGuides":{"title":"Indentation Guides","description":"Render vertical guides for each indent level.","type":"boolean","default":true},"editor.wordWrap":{"title":"Word Wrap","description":"Controls how lines should wrap.","type":"string","default":"off","enum":["off","on","bounded"]},"editor.renderWhitespace":{"title":"Render Whitespace","description":"Controls how the editor renders whitespace characters.","type":"string","default":"selection","enum":["none","boundary","selection","all"]},"editor.cursorBlinking":{"title":"Cursor Blinking","description":"Cursor animation style.","type":"string","default":"blink","enum":["blink","smooth","phase","solid"]},"editor.minimap":{"title":"Minimap","description":"Show a minimap of the document.","type":"boolean","default":false},"files.autoSave":{"title":"Auto Save","description":"Controls auto save of editors that have unsaved changes.","type":"string","default":"off","enum":["off","afterDelay","onFocusChange","onWindowChange"]},"files.autoSaveDelay":{"title":"Auto Save Delay","description":"Delay in milliseconds after which an editor is saved automatically. Applies when Auto Save is afterDelay.","type":"number","default":1000,"minimum":100,"maximum":60000},"files.trimTrailingWhitespace":{"title":"Trim Trailing Whitespace","description":"Remove trailing whitespace when saving.","type":"boolean","default":true},"editor.formatOnSave":{"title":"Format On Save","description":"Format a file on save. A formatter must be available.","type":"boolean","default":false},"editor.defaultFormatter":{"title":"Default Formatter","description":"Formatter used when several are available.","type":"string","default":"oxbit.prettier","anyOf":[{"enum":["oxbit.prettier","oxbit.builtin-ts"]},{"type":"string"}]},"terminal.fontSize":{"title":"Terminal Font Size","description":"Font size in pixels for the terminal.","type":"number","default":12,"minimum":8,"maximum":32},"terminal.scrollback":{"title":"Scrollback","description":"Maximum number of lines kept in the terminal buffer.","type":"number","default":5000,"minimum":100,"maximum":100000},"terminal.confirmOnKill":{"title":"Confirm On Kill","description":"Ask before killing a terminal with a running process.","type":"boolean","default":true},"scm.autoFetch":{"title":"Auto Fetch","description":"Periodically fetch from the default remote.","type":"boolean","default":true},"scm.diffLayout":{"title":"Diff Layout","description":"Default layout for diff editors.","type":"string","default":"side-by-side","enum":["side-by-side","inline"]},"files.associations":{"title":"File Associations","description":"Map filename or workspace-relative glob patterns to language IDs. Explicit associations take precedence over built-in detection.","type":"object","default":{},"propertyNames":{"minLength":1,"maxLength":1024},"additionalProperties":{"type":"string","pattern":"^[\\\\w+-]+$"}},"languageServers":{"title":"Language Servers","description":"Configure enabled state, selectors, rootMarkers, executable, args, env, initializationOptions, settings, and priority by server ID. Servers install on first use in a trusted runtime. License keys belong in the runtime-local credential file.","type":"object","default":{},"propertyNames":{"pattern":"^[a-zA-Z0-9._-]+$"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Enable this language server."},"selectors":{"type":"array","items":{"type":"object","properties":{"language":{"type":"string"},"pattern":{"type":"string"},"scheme":{"const":"file"}},"additionalProperties":false,"minProperties":1}},"rootMarkers":{"type":"array","items":{"type":"string","pattern":"^(?!/)(?!.*\\\\\\\\)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))[^\\\\u0000]+$","minLength":1}},"executable":{"type":"string","pattern":"^[^\\\\u0000]*$","minLength":1},"args":{"type":"array","items":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"env":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"additionalProperties":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"initializationOptions":{"type":"object","additionalProperties":true,"description":"Server-specific initialization options. Store license credentials in the runtime credential file."},"settings":{"type":"object","additionalProperties":true,"description":"Server-specific configuration; nested objects merge across files."},"priority":{"type":"number"}},"additionalProperties":false}},"ui.fontFamily":{"title":"UI Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"ui.fontSize":{"title":"UI Font Size","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":12,"minimum":8,"maximum":72},"ui.fontWeight":{"title":"UI Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"ui.fontStyle":{"title":"UI Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"ui.lineHeight":{"title":"UI Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1.4,"minimum":1,"maximum":3},"ui.letterSpacing":{"title":"UI Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"ui.fontLigatures":{"title":"UI Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"terminal.fontFamily":{"title":"Terminal Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"terminal.fontWeight":{"title":"Terminal Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"terminal.fontStyle":{"title":"Terminal Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"terminal.lineHeight":{"title":"Terminal Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1,"minimum":1,"maximum":3},"terminal.letterSpacing":{"title":"Terminal Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"terminal.fontLigatures":{"title":"Terminal Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"workbench.iconTheme":{"title":"File Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"workbench.productIconTheme":{"title":"Product Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"editor.semanticHighlighting":{"title":"Semantic Highlighting","description":"Color symbols using language server information.","type":"boolean","default":true},"editor.inlayHints.types":{"title":"Type Inlay Hints","description":"Show supported type hints in the visible editor.","type":"boolean","default":true},"editor.inlayHints.parameters":{"title":"Parameter Inlay Hints","description":"Show supported parameter hints in the visible editor.","type":"boolean","default":true},"editor.largeFileIntelligence":{"title":"Large File Intelligence","description":"Allow full semantic highlighting and inlay hints for files larger than 1 MiB.","type":"boolean","default":false},"workbench.locale":{"title":"Display Language","type":"string","default":"en","enum":["en","de","es","ja","zh"]},"workbench.reducedMotion":{"title":"Reduced Motion","type":"boolean","default":false},"desktop.projects.openBehavior":{"title":"Open projects","type":"string","default":"currentWindow","enum":["currentWindow","newWindow"]},"desktop.tools.gitPath":{"title":"git executable path","type":"string","default":""},"desktop.tools.ghPath":{"title":"gh executable path","type":"string","default":""},"project.intelligence":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Index project dependencies, imports and related files."},"exclude":{"type":"array","items":{"type":"string","maxLength":1024},"default":[]},"maxFiles":{"type":"integer","minimum":1,"maximum":100000,"default":20000},"maxFileBytes":{"type":"integer","minimum":1,"maximum":5242880,"default":1048576}},"additionalProperties":false,"title":"Project Intelligence","description":"Partial overrides of project.json intelligence. Objects merge across files."},"project.schemas":{"type":"object","properties":{"catalog":{"type":"boolean","default":true,"description":"Discover file schemas through SchemaStore. Bundled Oxbit settings support remains available."},"download":{"type":"boolean","default":true,"description":"Allow remote schema downloads; cached and bundled schemas work offline."},"associations":{"type":"array","maxItems":1000,"default":[],"items":{"type":"object","properties":{"url":{"type":"string","minLength":1,"description":"Schema URL or workspace file URI."},"fileMatch":{"type":"array","items":{"type":"string","maxLength":1024}},"schema":{"type":["object","boolean"],"description":"An inline JSON Schema."}},"additionalProperties":false,"anyOf":[{"required":["url"]},{"required":["schema"]}]}}},"additionalProperties":false,"title":"JSON Schemas","description":"Partial overrides of project.json schemas. Association arrays replace lower arrays."}}`);
936
937
  const propertyNames = { "anyOf": [{ "not": { "pattern": "^\\[.*\\]$" } }, { "pattern": "^\\[[^\\[\\]]+\\]$" }] };
937
938
  const patternProperties = { "^\\[[^\\[\\]]+\\]$": { "$ref": "#/definitions/languageOverrides" } };
938
939
  const additionalProperties$1 = true;
939
- const definitions$1 = { "languageOverrides": { "type": "object", "properties": { "agentACP.provider": { "$ref": "#/properties/agentACP.provider" }, "agentACP.codex.command": { "$ref": "#/properties/agentACP.codex.command" }, "agentACP.codex.args": { "$ref": "#/properties/agentACP.codex.args" }, "agentACP.cursor.command": { "$ref": "#/properties/agentACP.cursor.command" }, "agentACP.cursor.args": { "$ref": "#/properties/agentACP.cursor.args" }, "agentACP.amp.command": { "$ref": "#/properties/agentACP.amp.command" }, "agentACP.amp.args": { "$ref": "#/properties/agentACP.amp.args" }, "workbench.keymap": { "$ref": "#/properties/workbench.keymap" }, "runtime.keepAlive": { "$ref": "#/properties/runtime.keepAlive" }, "runtime.autoReconnect": { "$ref": "#/properties/runtime.autoReconnect" }, "workbench.tooltipDelay": { "$ref": "#/properties/workbench.tooltipDelay" }, "workbench.colorTheme": { "$ref": "#/properties/workbench.colorTheme" }, "workbench.density": { "$ref": "#/properties/workbench.density" }, "workbench.sidebarLocation": { "$ref": "#/properties/workbench.sidebarLocation" }, "editor.fontFamily": { "$ref": "#/properties/editor.fontFamily" }, "editor.fontSize": { "$ref": "#/properties/editor.fontSize" }, "editor.lineHeight": { "$ref": "#/properties/editor.lineHeight" }, "editor.fontLigatures": { "$ref": "#/properties/editor.fontLigatures" }, "editor.tabSize": { "$ref": "#/properties/editor.tabSize" }, "editor.insertSpaces": { "$ref": "#/properties/editor.insertSpaces" }, "editor.detectIndentation": { "$ref": "#/properties/editor.detectIndentation" }, "editor.renderIndentGuides": { "$ref": "#/properties/editor.renderIndentGuides" }, "editor.wordWrap": { "$ref": "#/properties/editor.wordWrap" }, "editor.renderWhitespace": { "$ref": "#/properties/editor.renderWhitespace" }, "editor.cursorBlinking": { "$ref": "#/properties/editor.cursorBlinking" }, "editor.minimap": { "$ref": "#/properties/editor.minimap" }, "files.autoSave": { "$ref": "#/properties/files.autoSave" }, "files.autoSaveDelay": { "$ref": "#/properties/files.autoSaveDelay" }, "files.trimTrailingWhitespace": { "$ref": "#/properties/files.trimTrailingWhitespace" }, "editor.formatOnSave": { "$ref": "#/properties/editor.formatOnSave" }, "editor.defaultFormatter": { "$ref": "#/properties/editor.defaultFormatter" }, "terminal.fontSize": { "$ref": "#/properties/terminal.fontSize" }, "terminal.scrollback": { "$ref": "#/properties/terminal.scrollback" }, "terminal.confirmOnKill": { "$ref": "#/properties/terminal.confirmOnKill" }, "scm.autoFetch": { "$ref": "#/properties/scm.autoFetch" }, "scm.diffLayout": { "$ref": "#/properties/scm.diffLayout" }, "files.associations": { "$ref": "#/properties/files.associations" }, "languageServers": { "$ref": "#/properties/languageServers" }, "ui.fontFamily": { "$ref": "#/properties/ui.fontFamily" }, "ui.fontSize": { "$ref": "#/properties/ui.fontSize" }, "ui.fontWeight": { "$ref": "#/properties/ui.fontWeight" }, "ui.fontStyle": { "$ref": "#/properties/ui.fontStyle" }, "ui.lineHeight": { "$ref": "#/properties/ui.lineHeight" }, "ui.letterSpacing": { "$ref": "#/properties/ui.letterSpacing" }, "ui.fontLigatures": { "$ref": "#/properties/ui.fontLigatures" }, "terminal.fontFamily": { "$ref": "#/properties/terminal.fontFamily" }, "terminal.fontWeight": { "$ref": "#/properties/terminal.fontWeight" }, "terminal.fontStyle": { "$ref": "#/properties/terminal.fontStyle" }, "terminal.lineHeight": { "$ref": "#/properties/terminal.lineHeight" }, "terminal.letterSpacing": { "$ref": "#/properties/terminal.letterSpacing" }, "terminal.fontLigatures": { "$ref": "#/properties/terminal.fontLigatures" }, "workbench.iconTheme": { "$ref": "#/properties/workbench.iconTheme" }, "workbench.productIconTheme": { "$ref": "#/properties/workbench.productIconTheme" }, "editor.semanticHighlighting": { "$ref": "#/properties/editor.semanticHighlighting" }, "editor.inlayHints.types": { "$ref": "#/properties/editor.inlayHints.types" }, "editor.inlayHints.parameters": { "$ref": "#/properties/editor.inlayHints.parameters" }, "editor.largeFileIntelligence": { "$ref": "#/properties/editor.largeFileIntelligence" }, "workbench.locale": { "$ref": "#/properties/workbench.locale" }, "workbench.reducedMotion": { "$ref": "#/properties/workbench.reducedMotion" }, "desktop.projects.openBehavior": { "$ref": "#/properties/desktop.projects.openBehavior" }, "desktop.tools.gitPath": { "$ref": "#/properties/desktop.tools.gitPath" }, "desktop.tools.ghPath": { "$ref": "#/properties/desktop.tools.ghPath" }, "project.intelligence": false, "project.schemas": false }, "additionalProperties": true, "description": "Settings for a language ID, such as [mdx] or [php]. Runtime project options belong at the top level.", "propertyNames": { "not": { "pattern": "^\\[.*\\]$" } } } };
940
+ const definitions$1 = { "languageOverrides": { "type": "object", "properties": { "agentACP.provider": { "$ref": "#/properties/agentACP.provider" }, "agentACP.useModifierToSend": { "$ref": "#/properties/agentACP.useModifierToSend" }, "agentACP.notifications": { "$ref": "#/properties/agentACP.notifications" }, "agentACP.codex.command": { "$ref": "#/properties/agentACP.codex.command" }, "agentACP.codex.args": { "$ref": "#/properties/agentACP.codex.args" }, "agentACP.claude.command": { "$ref": "#/properties/agentACP.claude.command" }, "agentACP.claude.args": { "$ref": "#/properties/agentACP.claude.args" }, "agentACP.gemini.command": { "$ref": "#/properties/agentACP.gemini.command" }, "agentACP.gemini.args": { "$ref": "#/properties/agentACP.gemini.args" }, "agentACP.copilot.command": { "$ref": "#/properties/agentACP.copilot.command" }, "agentACP.copilot.args": { "$ref": "#/properties/agentACP.copilot.args" }, "agentACP.cursor.command": { "$ref": "#/properties/agentACP.cursor.command" }, "agentACP.cursor.args": { "$ref": "#/properties/agentACP.cursor.args" }, "agentACP.amp.command": { "$ref": "#/properties/agentACP.amp.command" }, "agentACP.amp.args": { "$ref": "#/properties/agentACP.amp.args" }, "agentACP.custom.name": { "$ref": "#/properties/agentACP.custom.name" }, "agentACP.custom.command": { "$ref": "#/properties/agentACP.custom.command" }, "agentACP.custom.args": { "$ref": "#/properties/agentACP.custom.args" }, "agentACP.oxbit.api": { "$ref": "#/properties/agentACP.oxbit.api" }, "agentACP.oxbit.baseUrl": { "$ref": "#/properties/agentACP.oxbit.baseUrl" }, "agentACP.oxbit.model": { "$ref": "#/properties/agentACP.oxbit.model" }, "workbench.keymap": { "$ref": "#/properties/workbench.keymap" }, "runtime.keepAlive": { "$ref": "#/properties/runtime.keepAlive" }, "runtime.autoReconnect": { "$ref": "#/properties/runtime.autoReconnect" }, "workbench.tooltipDelay": { "$ref": "#/properties/workbench.tooltipDelay" }, "workbench.colorTheme": { "$ref": "#/properties/workbench.colorTheme" }, "workbench.density": { "$ref": "#/properties/workbench.density" }, "workbench.sidebarLocation": { "$ref": "#/properties/workbench.sidebarLocation" }, "editor.fontFamily": { "$ref": "#/properties/editor.fontFamily" }, "editor.fontSize": { "$ref": "#/properties/editor.fontSize" }, "editor.lineHeight": { "$ref": "#/properties/editor.lineHeight" }, "editor.fontLigatures": { "$ref": "#/properties/editor.fontLigatures" }, "editor.tabSize": { "$ref": "#/properties/editor.tabSize" }, "editor.insertSpaces": { "$ref": "#/properties/editor.insertSpaces" }, "editor.detectIndentation": { "$ref": "#/properties/editor.detectIndentation" }, "editor.renderIndentGuides": { "$ref": "#/properties/editor.renderIndentGuides" }, "editor.wordWrap": { "$ref": "#/properties/editor.wordWrap" }, "editor.renderWhitespace": { "$ref": "#/properties/editor.renderWhitespace" }, "editor.cursorBlinking": { "$ref": "#/properties/editor.cursorBlinking" }, "editor.minimap": { "$ref": "#/properties/editor.minimap" }, "files.autoSave": { "$ref": "#/properties/files.autoSave" }, "files.autoSaveDelay": { "$ref": "#/properties/files.autoSaveDelay" }, "files.trimTrailingWhitespace": { "$ref": "#/properties/files.trimTrailingWhitespace" }, "editor.formatOnSave": { "$ref": "#/properties/editor.formatOnSave" }, "editor.defaultFormatter": { "$ref": "#/properties/editor.defaultFormatter" }, "terminal.fontSize": { "$ref": "#/properties/terminal.fontSize" }, "terminal.scrollback": { "$ref": "#/properties/terminal.scrollback" }, "terminal.confirmOnKill": { "$ref": "#/properties/terminal.confirmOnKill" }, "scm.autoFetch": { "$ref": "#/properties/scm.autoFetch" }, "scm.diffLayout": { "$ref": "#/properties/scm.diffLayout" }, "files.associations": { "$ref": "#/properties/files.associations" }, "languageServers": { "$ref": "#/properties/languageServers" }, "ui.fontFamily": { "$ref": "#/properties/ui.fontFamily" }, "ui.fontSize": { "$ref": "#/properties/ui.fontSize" }, "ui.fontWeight": { "$ref": "#/properties/ui.fontWeight" }, "ui.fontStyle": { "$ref": "#/properties/ui.fontStyle" }, "ui.lineHeight": { "$ref": "#/properties/ui.lineHeight" }, "ui.letterSpacing": { "$ref": "#/properties/ui.letterSpacing" }, "ui.fontLigatures": { "$ref": "#/properties/ui.fontLigatures" }, "terminal.fontFamily": { "$ref": "#/properties/terminal.fontFamily" }, "terminal.fontWeight": { "$ref": "#/properties/terminal.fontWeight" }, "terminal.fontStyle": { "$ref": "#/properties/terminal.fontStyle" }, "terminal.lineHeight": { "$ref": "#/properties/terminal.lineHeight" }, "terminal.letterSpacing": { "$ref": "#/properties/terminal.letterSpacing" }, "terminal.fontLigatures": { "$ref": "#/properties/terminal.fontLigatures" }, "workbench.iconTheme": { "$ref": "#/properties/workbench.iconTheme" }, "workbench.productIconTheme": { "$ref": "#/properties/workbench.productIconTheme" }, "editor.semanticHighlighting": { "$ref": "#/properties/editor.semanticHighlighting" }, "editor.inlayHints.types": { "$ref": "#/properties/editor.inlayHints.types" }, "editor.inlayHints.parameters": { "$ref": "#/properties/editor.inlayHints.parameters" }, "editor.largeFileIntelligence": { "$ref": "#/properties/editor.largeFileIntelligence" }, "workbench.locale": { "$ref": "#/properties/workbench.locale" }, "workbench.reducedMotion": { "$ref": "#/properties/workbench.reducedMotion" }, "desktop.projects.openBehavior": { "$ref": "#/properties/desktop.projects.openBehavior" }, "desktop.tools.gitPath": { "$ref": "#/properties/desktop.tools.gitPath" }, "desktop.tools.ghPath": { "$ref": "#/properties/desktop.tools.ghPath" }, "project.intelligence": false, "project.schemas": false }, "additionalProperties": true, "description": "Settings for a language ID, such as [mdx] or [php]. Runtime project options belong at the top level.", "propertyNames": { "not": { "pattern": "^\\[.*\\]$" } } } };
940
941
  const schema = {
941
942
  $schema: $schema$2,
942
943
  $id: $id$1,
@@ -1041,9 +1042,9 @@ function lspWatchPattern(glob, rootUri) {
1041
1042
  pattern = glob.pattern;
1042
1043
  const uri = typeof glob.baseUri === "string" ? glob.baseUri : glob.baseUri?.uri;
1043
1044
  if (!uri) return;
1044
- const url = new URL(uri);
1045
- if (url.protocol !== "file:" || url.host || url.search || url.hash) return;
1046
- base = decodeURIComponent(url.pathname).replace(/\/$/, "");
1045
+ const url2 = new URL(uri);
1046
+ if (url2.protocol !== "file:" || url2.host || url2.search || url2.hash) return;
1047
+ base = decodeURIComponent(url2.pathname).replace(/\/$/, "");
1047
1048
  if (pattern.startsWith("/")) return;
1048
1049
  } else return;
1049
1050
  if (base !== root && !base.startsWith(root + "/")) return;
@@ -1112,7 +1113,35 @@ const ACP_PROVIDERS = [
1112
1113
  command: "npx",
1113
1114
  args: ["-y", "@agentclientprotocol/codex-acp@2.2.2"],
1114
1115
  setup: "Uses the Codex ACP adapter. Sign in through an advertised authentication method, or use your existing Codex credentials.",
1115
- url: "https://github.com/agentclientprotocol/codex-acp"
1116
+ url: "https://github.com/agentclientprotocol/codex-acp",
1117
+ registryId: "codex-acp"
1118
+ },
1119
+ {
1120
+ id: "claude",
1121
+ name: "Claude Agent",
1122
+ command: "npx",
1123
+ args: ["-y", "@agentclientprotocol/claude-agent-acp@0.89.1"],
1124
+ setup: "Uses the Claude Agent ACP adapter. Sign in through an advertised authentication method, or use your existing Claude Code credentials.",
1125
+ url: "https://github.com/agentclientprotocol/claude-agent-acp",
1126
+ registryId: "claude-acp"
1127
+ },
1128
+ {
1129
+ id: "gemini",
1130
+ name: "Gemini CLI",
1131
+ command: "npx",
1132
+ args: ["-y", "@google/gemini-cli@0.63.0", "--acp"],
1133
+ setup: "Runs Gemini CLI in ACP mode. Sign in through an advertised authentication method, or set GEMINI_API_KEY in the runtime environment.",
1134
+ url: "https://github.com/google-gemini/gemini-cli",
1135
+ registryId: "gemini"
1136
+ },
1137
+ {
1138
+ id: "copilot",
1139
+ name: "GitHub Copilot",
1140
+ command: "npx",
1141
+ args: ["-y", "@github/copilot@1.0.95", "--acp"],
1142
+ setup: "Runs GitHub Copilot CLI in ACP mode. Sign in through an advertised authentication method, or run copilot and use /login first.",
1143
+ url: "https://github.com/github/copilot-cli",
1144
+ registryId: "github-copilot-cli"
1116
1145
  },
1117
1146
  {
1118
1147
  id: "cursor",
@@ -1120,7 +1149,8 @@ const ACP_PROVIDERS = [
1120
1149
  command: "agent",
1121
1150
  args: ["acp"],
1122
1151
  setup: "Install Cursor CLI and run agent login first. If your executable is cursor-agent, change the command below.",
1123
- url: "https://cursor.com/docs/cli/acp"
1152
+ url: "https://cursor.com/docs/cli/acp",
1153
+ registryId: "cursor"
1124
1154
  },
1125
1155
  {
1126
1156
  id: "amp",
@@ -1128,9 +1158,15 @@ const ACP_PROVIDERS = [
1128
1158
  command: "npx",
1129
1159
  args: ["-y", "amp-acp@0.10.0"],
1130
1160
  setup: "Uses the community Amp ACP adapter. Install Amp CLI and run amp login first. Set AMP_CLI_PATH in the runtime environment if needed.",
1131
- url: "https://github.com/tao12345666333/amp-acp"
1161
+ url: "https://github.com/tao12345666333/amp-acp",
1162
+ registryId: "amp-acp"
1132
1163
  }
1133
1164
  ];
1165
+ const ACP_CUSTOM_PROVIDER = "custom";
1166
+ const ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
1167
+ const acpPreset = (id) => ACP_PROVIDERS.find((provider) => provider.id === id);
1168
+ const acpBuiltinForRegistry = (registryId) => ACP_PROVIDERS.find((provider) => provider.registryId === registryId);
1169
+ const PUSH_RELAY_URL = "https://oxbit-push.ryanyannelli.workers.dev";
1134
1170
  const taskPhases = [
1135
1171
  "preinit",
1136
1172
  "init",
@@ -1167,29 +1203,29 @@ function settingsFile(layers, scope) {
1167
1203
  return { ...structuredClone(layers[scope]), ...Object.fromEntries(Object.entries(layers[scope === "user" ? "userLanguages" : "workspaceLanguages"]).filter(([, value]) => Object.keys(value).length).map(([language, value]) => [`[${language}]`, structuredClone(value)])) };
1168
1204
  }
1169
1205
  const SDK_VERSION = "1.0.0";
1170
- function invalid(message) {
1171
- throw new RpcError("INVALID_TASK_CONFIG", message);
1206
+ function invalid$1(message2) {
1207
+ throw new RpcError("INVALID_TASK_CONFIG", message2);
1172
1208
  }
1173
1209
  function object$2(value, label) {
1174
1210
  if (!value || typeof value !== "object" || Array.isArray(value))
1175
- invalid(`${label} must be an object`);
1211
+ invalid$1(`${label} must be an object`);
1176
1212
  return value;
1177
1213
  }
1178
1214
  function taskName(value) {
1179
1215
  if (typeof value !== "string" || !value.trim() || value.length > 120 || /[\x00-\x1f\x7f]/.test(value) || ["__proto__", "constructor", "prototype"].includes(value))
1180
- invalid("Task name must contain 1–120 printable characters");
1216
+ invalid$1("Task name must contain 1–120 printable characters");
1181
1217
  return value;
1182
1218
  }
1183
1219
  function stringMap(value, label) {
1184
1220
  const record = object$2(value, label);
1185
1221
  for (const [key, item] of Object.entries(record))
1186
1222
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ["__proto__", "constructor", "prototype"].includes(key) || typeof item !== "string" || item.length > 32768 || item.includes("\0"))
1187
- invalid(`${label}: invalid environment entry ${key}`);
1223
+ invalid$1(`${label}: invalid environment entry ${key}`);
1188
1224
  return record;
1189
1225
  }
1190
1226
  function number(value, min, max, label) {
1191
1227
  if (!Number.isSafeInteger(value) || Number(value) < min || Number(value) > max)
1192
- invalid(`${label} must be an integer between ${min} and ${max}`);
1228
+ invalid$1(`${label} must be an integer between ${min} and ${max}`);
1193
1229
  }
1194
1230
  function port(value) {
1195
1231
  if (value === "auto") return value;
@@ -1206,14 +1242,14 @@ function hooks(value) {
1206
1242
  const data = object$2(value ?? {}, "worktree");
1207
1243
  for (const key of Object.keys(data))
1208
1244
  if (!taskPhases.includes(key))
1209
- invalid(`Unknown worktree phase: ${key}`);
1245
+ invalid$1(`Unknown worktree phase: ${key}`);
1210
1246
  for (const phase of taskPhases)
1211
1247
  if (data[phase] !== void 0) {
1212
1248
  const lines = Array.isArray(data[phase]) ? data[phase] : [data[phase]];
1213
1249
  if (lines.length > 128 || lines.some(
1214
1250
  (line) => typeof line !== "string" || line.length > 32768 || line.includes("\0")
1215
1251
  ))
1216
- invalid(`worktree.${phase} must be a script or array of scripts`);
1252
+ invalid$1(`worktree.${phase} must be a script or array of scripts`);
1217
1253
  }
1218
1254
  return Object.fromEntries(
1219
1255
  taskPhases.filter((phase) => data[phase] !== void 0).map((phase) => [phase, data[phase]])
@@ -1240,9 +1276,9 @@ function definition(value) {
1240
1276
  "stop"
1241
1277
  ]);
1242
1278
  for (const key of Object.keys(data))
1243
- if (!known.has(key)) invalid(`Unknown task field: ${key}`);
1279
+ if (!known.has(key)) invalid$1(`Unknown task field: ${key}`);
1244
1280
  if (typeof data.command !== "string" || data.command.length > 32768 || data.command.includes("\0") || !data.command.trim() && !data.dependsOn?.length)
1245
- invalid("A task needs a command or dependencies");
1281
+ invalid$1("A task needs a command or dependencies");
1246
1282
  for (const [key, choices] of Object.entries({
1247
1283
  type: ["command", "service"],
1248
1284
  execution: ["shell", "process"],
@@ -1250,39 +1286,39 @@ function definition(value) {
1250
1286
  group: ["build", "test"]
1251
1287
  }))
1252
1288
  if (data[key] !== void 0 && !choices.includes(data[key]))
1253
- invalid(`Invalid ${key}`);
1289
+ invalid$1(`Invalid ${key}`);
1254
1290
  for (const key of ["args", "dependsOn"])
1255
1291
  if (data[key] !== void 0 && (!Array.isArray(data[key]) || data[key].length > 128 || data[key].some(
1256
1292
  (v) => typeof v !== "string" || v.length > 32768 || v.includes("\0")
1257
1293
  )))
1258
- invalid(`${key} must be an array of strings`);
1294
+ invalid$1(`${key} must be an array of strings`);
1259
1295
  for (const key of ["cwd", "host", "hostname"])
1260
1296
  if (data[key] !== void 0 && (typeof data[key] !== "string" || data[key].length > 4096 || /[\x00-\x1f]/.test(data[key])))
1261
- invalid(`Invalid ${key}`);
1297
+ invalid$1(`Invalid ${key}`);
1262
1298
  if (data.env !== void 0) stringMap(data.env, "task.env");
1263
1299
  if (data.port !== void 0) port(data.port);
1264
1300
  if (data.ports !== void 0) {
1265
1301
  const entries = Object.entries(object$2(data.ports, "ports"));
1266
- if (entries.length > 16) invalid("Maximum 16 named ports per task");
1302
+ if (entries.length > 16) invalid$1("Maximum 16 named ports per task");
1267
1303
  for (const [key, value2] of entries) {
1268
1304
  if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key) || key.toLowerCase() === "port")
1269
- invalid("Port names must be identifiers other than 'port'");
1305
+ invalid$1("Port names must be identifiers other than 'port'");
1270
1306
  port(value2);
1271
1307
  }
1272
1308
  if (new Set(entries.map(([key]) => key.toUpperCase())).size !== entries.length)
1273
- invalid("Port names must be unique ignoring case");
1309
+ invalid$1("Port names must be unique ignoring case");
1274
1310
  }
1275
1311
  if (data.ready !== void 0) {
1276
1312
  const ready = object$2(data.ready, "ready");
1277
1313
  for (const key of Object.keys(ready))
1278
1314
  if (!["pattern", "url", "timeoutMs", "intervalMs"].includes(key))
1279
- invalid(`Unknown readiness field: ${key}`);
1315
+ invalid$1(`Unknown readiness field: ${key}`);
1280
1316
  if (ready.pattern !== void 0) {
1281
1317
  if (typeof ready.pattern !== "string" || !ready.pattern.length || ready.pattern.length > 256)
1282
- invalid("ready.pattern must be 1–256 characters");
1318
+ invalid$1("ready.pattern must be 1–256 characters");
1283
1319
  }
1284
1320
  if (ready.url !== void 0 && (typeof ready.url !== "string" || ready.url.length > 4096))
1285
- invalid("Invalid readiness URL");
1321
+ invalid$1("Invalid readiness URL");
1286
1322
  if (ready.timeoutMs !== void 0)
1287
1323
  number(ready.timeoutMs, 100, 36e5, "ready.timeoutMs");
1288
1324
  if (ready.intervalMs !== void 0)
@@ -1292,9 +1328,9 @@ function definition(value) {
1292
1328
  const restart = object$2(data.restart, "restart");
1293
1329
  for (const key of Object.keys(restart))
1294
1330
  if (!["policy", "maxAttempts", "delayMs"].includes(key))
1295
- invalid(`Unknown restart field: ${key}`);
1331
+ invalid$1(`Unknown restart field: ${key}`);
1296
1332
  if (!["never", "on-failure"].includes(restart.policy))
1297
- invalid("Invalid restart policy");
1333
+ invalid$1("Invalid restart policy");
1298
1334
  if (restart.maxAttempts !== void 0)
1299
1335
  number(restart.maxAttempts, 0, 10, "restart.maxAttempts");
1300
1336
  if (restart.delayMs !== void 0)
@@ -1304,9 +1340,9 @@ function definition(value) {
1304
1340
  const stop2 = object$2(data.stop, "stop");
1305
1341
  for (const key of Object.keys(stop2))
1306
1342
  if (!["signal", "timeoutMs"].includes(key))
1307
- invalid(`Unknown stop field: ${key}`);
1343
+ invalid$1(`Unknown stop field: ${key}`);
1308
1344
  if (stop2.signal !== void 0 && !["SIGTERM", "SIGINT"].includes(stop2.signal))
1309
- invalid("Invalid stop signal");
1345
+ invalid$1("Invalid stop signal");
1310
1346
  if (stop2.timeoutMs !== void 0)
1311
1347
  number(stop2.timeoutMs, 100, 6e4, "stop.timeoutMs");
1312
1348
  }
@@ -1317,7 +1353,7 @@ const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
1317
1353
  function json(text2) {
1318
1354
  const errors = [];
1319
1355
  const result = parse$1(text2, errors, { allowTrailingComma: true });
1320
- if (errors.length) invalid(`Invalid JSON near offset ${errors[0].offset}`);
1356
+ if (errors.length) invalid$1(`Invalid JSON near offset ${errors[0].offset}`);
1321
1357
  return object$2(result, "configuration");
1322
1358
  }
1323
1359
  function editJson(text2, location, value) {
@@ -1369,11 +1405,11 @@ function quotePath(value) {
1369
1405
  }
1370
1406
  function xml(text2) {
1371
1407
  if (/<!DOCTYPE|<!ENTITY/i.test(text2))
1372
- invalid("XML document types and entities are not supported");
1408
+ invalid$1("XML document types and entities are not supported");
1373
1409
  return new DOMParser({
1374
- onError: (level, message) => {
1410
+ onError: (level, message2) => {
1375
1411
  if (level !== "warning")
1376
- invalid(`Invalid run configuration XML: ${message}`);
1412
+ invalid$1(`Invalid run configuration XML: ${message2}`);
1377
1413
  }
1378
1414
  }).parseFromString(text2, "application/xml");
1379
1415
  }
@@ -1406,7 +1442,7 @@ function jetbrains(element) {
1406
1442
  const setting2 = (name2, nativeName = name2) => values[name2] ?? element.getAttribute(nativeName) ?? children(element, nativeName)[0]?.getAttribute("value") ?? "";
1407
1443
  let task = { command: "" }, disabledReason;
1408
1444
  const envs = Array.from(element.getElementsByTagName("env"));
1409
- const env = Object.fromEntries(
1445
+ const env2 = Object.fromEntries(
1410
1446
  envs.map((node) => [
1411
1447
  node.getAttribute("name"),
1412
1448
  node.getAttribute("value") ?? ""
@@ -1449,7 +1485,7 @@ function jetbrains(element) {
1449
1485
  disabledReason = "Gradle custom VM/script parameters need an explicit Oxbit command";
1450
1486
  } else
1451
1487
  disabledReason = `${type2 || "This IDE configuration"} requires an IDE/debug adapter; add an explicit Oxbit command to enable it`;
1452
- task.env = env;
1488
+ task.env = env2;
1453
1489
  const before = Array.from(element.getElementsByTagName("method")).flatMap((node) => children(node, "option")).filter((node) => node.getAttribute("enabled") === "true");
1454
1490
  if (before.length)
1455
1491
  disabledReason = "Before-launch IDE steps require an explicit command/dependency mapping";
@@ -1528,7 +1564,7 @@ function readSource(kind, text2) {
1528
1564
  result.worktree = hooks(kind === "oxbit" ? data.worktree : meta.worktree);
1529
1565
  if (kind === "oxbit") {
1530
1566
  if (data.version !== 1)
1531
- invalid("Unsupported Oxbit tasks version (expected 1)");
1567
+ invalid$1("Unsupported Oxbit tasks version (expected 1)");
1532
1568
  for (const key of Object.keys(data))
1533
1569
  if (![
1534
1570
  "$schema",
@@ -1538,7 +1574,7 @@ function readSource(kind, text2) {
1538
1574
  "env",
1539
1575
  "worktree"
1540
1576
  ].includes(key))
1541
- invalid(`Unknown Oxbit configuration field: ${key}`);
1577
+ invalid$1(`Unknown Oxbit configuration field: ${key}`);
1542
1578
  for (const [name, task] of Object.entries(
1543
1579
  object$2(data.tasks ?? {}, "tasks")
1544
1580
  ))
@@ -1571,10 +1607,10 @@ function readSource(kind, text2) {
1571
1607
  );
1572
1608
  } else if (kind === "vscode-launch") {
1573
1609
  if (!Array.isArray(data.configurations))
1574
- invalid("VS Code launch configurations must be an array");
1575
- for (const entry of data.configurations) {
1610
+ invalid$1("VS Code launch configurations must be an array");
1611
+ for (const entry2 of data.configurations) {
1576
1612
  const platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1577
- const item = { ...entry, ...entry[platform] };
1613
+ const item = { ...entry2, ...entry2[platform] };
1578
1614
  const python = ["python", "debugpy"].includes(item.type);
1579
1615
  let disabledReason;
1580
1616
  if (item.request !== "launch" || !["node", "pwa-node", "python", "debugpy"].includes(item.type))
@@ -1603,11 +1639,11 @@ function readSource(kind, text2) {
1603
1639
  );
1604
1640
  } else if (kind === "vscode") {
1605
1641
  if (data.version !== "2.0.0")
1606
- invalid("Only VS Code tasks version 2.0.0 is supported");
1607
- if (!Array.isArray(data.tasks)) invalid("VS Code tasks must be an array");
1642
+ invalid$1("Only VS Code tasks version 2.0.0 is supported");
1643
+ if (!Array.isArray(data.tasks)) invalid$1("VS Code tasks must be an array");
1608
1644
  for (const raw of data.tasks) {
1609
- const entry = object$2(raw, "VS Code task"), platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1610
- const item = { ...entry, ...entry[platform] };
1645
+ const entry2 = object$2(raw, "VS Code task"), platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1646
+ const item = { ...entry2, ...entry2[platform] };
1611
1647
  const args = (item.args ?? []).map(
1612
1648
  (arg) => typeof arg === "string" ? arg : object$2(arg, "argument").value
1613
1649
  );
@@ -1664,10 +1700,10 @@ function readSource(kind, text2) {
1664
1700
  }
1665
1701
  }
1666
1702
  }
1667
- if (result.tasks.length > 256) invalid("Maximum 256 tasks per configuration");
1703
+ if (result.tasks.length > 256) invalid$1("Maximum 256 tasks per configuration");
1668
1704
  const names = /* @__PURE__ */ new Set();
1669
1705
  for (const task of result.tasks) {
1670
- if (names.has(task.name)) invalid(`Duplicate task name: ${task.name}`);
1706
+ if (names.has(task.name)) invalid$1(`Duplicate task name: ${task.name}`);
1671
1707
  names.add(task.name);
1672
1708
  if (!task.disabledReason) task.definition = definition(task.definition);
1673
1709
  }
@@ -1675,7 +1711,7 @@ function readSource(kind, text2) {
1675
1711
  }
1676
1712
  function writeTask(kind, text2, name, task, sourceCommand) {
1677
1713
  if (kind === "make" || kind === "just")
1678
- invalid("Recipe imports are read-only; copy to Oxbit to customize");
1714
+ invalid$1("Recipe imports are read-only; copy to Oxbit to customize");
1679
1715
  if (kind === "jetbrains") {
1680
1716
  const doc = xml(text2);
1681
1717
  let target = Array.from(doc.getElementsByTagName("configuration")).find(
@@ -1713,11 +1749,11 @@ function writeTask(kind, text2, name, task, sourceCommand) {
1713
1749
  }
1714
1750
  if (kind === "procfile") {
1715
1751
  if (task && (task.command.includes("\n") || Object.keys(task).some((key) => !["command", "type"].includes(key))))
1716
- invalid(
1752
+ invalid$1(
1717
1753
  "Procfile supports a single-line service command. Copy to Oxbit for advanced settings."
1718
1754
  );
1719
1755
  if (!/^[\w-]+$/.test(name))
1720
- invalid(
1756
+ invalid$1(
1721
1757
  "Procfile names must contain letters, numbers, hyphens or underscores"
1722
1758
  );
1723
1759
  const lines = text2.split(/\r?\n/), index = lines.findIndex((line) => line.startsWith(name + ":"));
@@ -1810,10 +1846,10 @@ function writeTask(kind, text2, name, task, sourceCommand) {
1810
1846
  try {
1811
1847
  script = JSON.parse(script);
1812
1848
  } catch {
1813
- invalid("Composer script array must be valid JSON");
1849
+ invalid$1("Composer script array must be valid JSON");
1814
1850
  }
1815
1851
  if (!Array.isArray(script) || script.some((v) => typeof v !== "string"))
1816
- invalid("Composer script array must contain strings");
1852
+ invalid$1("Composer script array must contain strings");
1817
1853
  }
1818
1854
  next = editJson(next, ["scripts", name], script);
1819
1855
  }
@@ -1823,9 +1859,9 @@ function writeTask(kind, text2, name, task, sourceCommand) {
1823
1859
  delete metadata.args;
1824
1860
  return editJson(next, ["oxbit", "tasks", name], metadata);
1825
1861
  }
1826
- function writeSettings(kind, text2, worktree, env) {
1862
+ function writeSettings(kind, text2, worktree, env2) {
1827
1863
  if (["make", "just", "procfile"].includes(kind))
1828
- invalid(
1864
+ invalid$1(
1829
1865
  "This source cannot store lifecycle settings; save an Oxbit configuration first"
1830
1866
  );
1831
1867
  if (kind === "jetbrains") {
@@ -1839,7 +1875,7 @@ function writeSettings(kind, text2, worktree, env) {
1839
1875
  doc.documentElement.appendChild(component);
1840
1876
  }
1841
1877
  option(doc, component, "worktree", JSON.stringify(worktree));
1842
- option(doc, component, "env", JSON.stringify(env));
1878
+ option(doc, component, "env", JSON.stringify(env2));
1843
1879
  return new XMLSerializer().serializeToString(doc);
1844
1880
  }
1845
1881
  let next = editJson(
@@ -1851,7 +1887,7 @@ function writeSettings(kind, text2, worktree, env) {
1851
1887
  next = editJson(next, ["worktree", "setup"], worktree.init);
1852
1888
  next = editJson(next, ["worktree", "teardown"], worktree.preteardown);
1853
1889
  }
1854
- return editJson(next, kind === "oxbit" ? ["env"] : ["oxbit", "env"], env);
1890
+ return editJson(next, kind === "oxbit" ? ["env"] : ["oxbit", "env"], env2);
1855
1891
  }
1856
1892
  const digest$2 = (text2) => createHash("sha256").update(text2).digest("hex");
1857
1893
  const missing$1 = (error) => error.code === "ENOENT";
@@ -2008,10 +2044,10 @@ class TaskConfigStore {
2008
2044
  const entries = await fs.readdir(path.join(this.root, directory), {
2009
2045
  withFileTypes: true
2010
2046
  });
2011
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name)).slice(0, 128))
2012
- if (entry.isFile() && entry.name.endsWith(".xml"))
2047
+ for (const entry2 of entries.sort((a, b) => a.name.localeCompare(b.name)).slice(0, 128))
2048
+ if (entry2.isFile() && entry2.name.endsWith(".xml"))
2013
2049
  candidates.push({
2014
- file: path.join(directory, entry.name),
2050
+ file: path.join(directory, entry2.name),
2015
2051
  kind: "jetbrains"
2016
2052
  });
2017
2053
  } catch (error) {
@@ -2028,7 +2064,7 @@ class TaskConfigStore {
2028
2064
  { file: this.privatePath, kind: "oxbit", private: true }
2029
2065
  );
2030
2066
  const sources = [], tasks = [];
2031
- let worktree = {}, env = {}, settingsFound = false, noImports = false;
2067
+ let worktree = {}, env2 = {}, settingsFound = false, noImports = false;
2032
2068
  for (const candidate of candidates) {
2033
2069
  if (noImports) continue;
2034
2070
  const file = path.resolve(this.root, candidate.file), source2 = {
@@ -2062,7 +2098,7 @@ class TaskConfigStore {
2062
2098
  noImports = json(text2).autoDetect === false;
2063
2099
  if (!settingsFound && source2.writable && !["procfile"].includes(source2.kind)) {
2064
2100
  worktree = parsed.worktree;
2065
- env = parsed.env;
2101
+ env2 = parsed.env;
2066
2102
  settingsFound = true;
2067
2103
  }
2068
2104
  for (const task of parsed.tasks)
@@ -2099,7 +2135,7 @@ class TaskConfigStore {
2099
2135
  sources,
2100
2136
  tasks,
2101
2137
  worktree,
2102
- env
2138
+ env: env2
2103
2139
  };
2104
2140
  }
2105
2141
  async source(id, expected) {
@@ -2424,22 +2460,22 @@ function tcpReady(host, port2, timeoutMs = 1e3) {
2424
2460
  socket.once("timeout", () => finish(false));
2425
2461
  });
2426
2462
  }
2427
- function expand(value, env, strict = true) {
2463
+ function expand(value, env2, strict = true) {
2428
2464
  return value.replace(
2429
2465
  /\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
2430
2466
  (match, braced, bare) => {
2431
2467
  if (match === "$$") return "$";
2432
2468
  const key = braced ?? bare;
2433
- if (env[key] === void 0 && strict)
2469
+ if (env2[key] === void 0 && strict)
2434
2470
  throw new RpcError(
2435
2471
  "UNRESOLVED_VARIABLE",
2436
2472
  `Variable ${key} is not defined`
2437
2473
  );
2438
- return env[key] ?? "";
2474
+ return env2[key] ?? "";
2439
2475
  }
2440
2476
  );
2441
2477
  }
2442
- function taskEnvironment(store, task, ports, catalog, context = {}) {
2478
+ function taskEnvironment(store, task, ports, catalog, context = {}, base = process.env) {
2443
2479
  const root = context.projectDir ?? context.root ?? store.root, plan = ports.get(task.id);
2444
2480
  const variables = {
2445
2481
  OXBIT_PROJECT_ID: store.projectId,
@@ -2467,26 +2503,26 @@ function taskEnvironment(store, task, ports, catalog, context = {}) {
2467
2503
  for (const [name, port2] of Object.entries(plan.ports))
2468
2504
  variables[`OXBIT_${name.toUpperCase()}_PORT`] = String(port2);
2469
2505
  }
2470
- const env = { ...process.env, ...variables };
2471
- env.PASEO_SOURCE_CHECKOUT_PATH = variables.OXBIT_SOURCE_DIR;
2472
- env.PASEO_WORKTREE_PATH = root;
2473
- env.PASEO_ROOT_PATH = variables.OXBIT_SOURCE_DIR;
2474
- env.PASEO_BRANCH_NAME = variables.OXBIT_BRANCH;
2475
- env.PASEO_WORKSPACE_ID = variables.OXBIT_WORKSPACE_ID;
2476
- env.PASEO_SCRIPTNAME = task.name;
2506
+ const env2 = { ...base, ...variables };
2507
+ env2.PASEO_SOURCE_CHECKOUT_PATH = variables.OXBIT_SOURCE_DIR;
2508
+ env2.PASEO_WORKTREE_PATH = root;
2509
+ env2.PASEO_ROOT_PATH = variables.OXBIT_SOURCE_DIR;
2510
+ env2.PASEO_BRANCH_NAME = variables.OXBIT_BRANCH;
2511
+ env2.PASEO_WORKSPACE_ID = variables.OXBIT_WORKSPACE_ID;
2512
+ env2.PASEO_SCRIPTNAME = task.name;
2477
2513
  if (plan) {
2478
- env.PASEO_PORT = variables.OXBIT_PORT;
2479
- env.PASEO_URL = variables.OXBIT_URL;
2480
- env.PASEO_WORKTREE_PORT = variables.OXBIT_PORT;
2481
- env.PORT = variables.OXBIT_PORT;
2482
- env.HOST = variables.OXBIT_HOST;
2514
+ env2.PASEO_PORT = variables.OXBIT_PORT;
2515
+ env2.PASEO_URL = variables.OXBIT_URL;
2516
+ env2.PASEO_WORKTREE_PORT = variables.OXBIT_PORT;
2517
+ env2.PORT = variables.OXBIT_PORT;
2518
+ env2.HOST = variables.OXBIT_HOST;
2483
2519
  }
2484
2520
  for (const [key, value] of Object.entries(variables))
2485
2521
  if (key.startsWith("OXBIT_SERVICE_"))
2486
- env[key.replace("OXBIT_", "PASEO_")] = value;
2522
+ env2[key.replace("OXBIT_", "PASEO_")] = value;
2487
2523
  const custom = { ...catalog.env, ...task.env }, resolved = /* @__PURE__ */ new Set(), resolving = /* @__PURE__ */ new Set();
2488
2524
  function resolve(key) {
2489
- if (resolved.has(key)) return env[key];
2525
+ if (resolved.has(key)) return env2[key];
2490
2526
  if (resolving.has(key))
2491
2527
  throw new RpcError(
2492
2528
  "UNRESOLVED_VARIABLE",
@@ -2505,13 +2541,13 @@ function taskEnvironment(store, task, ports, catalog, context = {}) {
2505
2541
  const other = match[1] ?? match[2];
2506
2542
  if (other !== key && Object.hasOwn(custom, other)) resolve(other);
2507
2543
  }
2508
- env[key] = expand(value, env);
2544
+ env2[key] = expand(value, env2);
2509
2545
  resolving.delete(key);
2510
2546
  resolved.add(key);
2511
- return env[key];
2547
+ return env2[key];
2512
2548
  }
2513
2549
  for (const key of Object.keys(custom)) resolve(key);
2514
- return { env, variables };
2550
+ return { env: env2, variables };
2515
2551
  }
2516
2552
  function detectLinks(text2) {
2517
2553
  const clean = text2.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
@@ -2519,11 +2555,11 @@ function detectLinks(text2) {
2519
2555
  for (const match of clean.matchAll(/https?:\/\/[^\s<>"'\x00-\x1f]+/g)) {
2520
2556
  const raw = match[0].replace(/[),.;\]}]+$/, "");
2521
2557
  try {
2522
- const url = new URL(raw);
2523
- if (!url.username && !url.password) {
2524
- if (["0.0.0.0", "[::]"].includes(url.hostname))
2525
- url.hostname = "localhost";
2526
- found.add(url.href);
2558
+ const url2 = new URL(raw);
2559
+ if (!url2.username && !url2.password) {
2560
+ if (["0.0.0.0", "[::]"].includes(url2.hostname))
2561
+ url2.hostname = "localhost";
2562
+ found.add(url2.href);
2527
2563
  }
2528
2564
  } catch {
2529
2565
  }
@@ -2531,35 +2567,38 @@ function detectLinks(text2) {
2531
2567
  return [...found].slice(0, 32);
2532
2568
  }
2533
2569
  class TaskRunner {
2534
- constructor(store, emit, forwardPort) {
2570
+ constructor(store, emit, forwardPort, shellEnvironment = async () => process.env) {
2535
2571
  this.store = store;
2536
2572
  this.emit = emit;
2537
2573
  this.forwardPort = forwardPort;
2574
+ this.shellEnvironment = shellEnvironment;
2538
2575
  this.ports = new TaskPorts(store.root);
2539
2576
  }
2540
2577
  store;
2541
2578
  emit;
2542
2579
  forwardPort;
2580
+ shellEnvironment;
2543
2581
  runs = /* @__PURE__ */ new Map();
2544
2582
  ports;
2545
2583
  publicPorts = /* @__PURE__ */ new Map();
2546
2584
  closed = false;
2585
+ environment;
2547
2586
  publicLinks(run2, links) {
2548
2587
  if (!this.forwardPort) return links;
2549
2588
  const plan = this.ports.get(run2.task.id);
2550
2589
  return links.map((link) => {
2551
2590
  try {
2552
- const url = new URL(link), port2 = this.publicPorts.get(run2.task.id)?.get(Number(url.port || (url.protocol === "https:" ? 443 : 80)));
2591
+ const url2 = new URL(link), port2 = this.publicPorts.get(run2.task.id)?.get(Number(url2.port || (url2.protocol === "https:" ? 443 : 80)));
2553
2592
  if (port2 && [
2554
2593
  "localhost",
2555
2594
  "127.0.0.1",
2556
2595
  "[::1]",
2557
2596
  plan?.hostname,
2558
2597
  plan?.host
2559
- ].includes(url.hostname)) {
2560
- url.hostname = "127.0.0.1";
2561
- url.port = String(port2);
2562
- return url.href;
2598
+ ].includes(url2.hostname)) {
2599
+ url2.hostname = "127.0.0.1";
2600
+ url2.port = String(port2);
2601
+ return url2.href;
2563
2602
  }
2564
2603
  } catch {
2565
2604
  }
@@ -2584,10 +2623,10 @@ class TaskRunner {
2584
2623
  for (const update of run2.updates) update();
2585
2624
  run2.updates.clear();
2586
2625
  }
2587
- state(run2, state2, message) {
2588
- if (run2.value.state === state2 && run2.value.message === message) return;
2626
+ state(run2, state2, message2) {
2627
+ if (run2.value.state === state2 && run2.value.message === message2) return;
2589
2628
  run2.value.state = state2;
2590
- run2.value.message = message;
2629
+ run2.value.message = message2;
2591
2630
  this.publish(run2);
2592
2631
  }
2593
2632
  owned(id, owner) {
@@ -2685,10 +2724,10 @@ class TaskRunner {
2685
2724
  )
2686
2725
  );
2687
2726
  }
2688
- async runHook(owner, command, name, context, env, signal) {
2727
+ async runHook(owner, command, name, context, env2, signal) {
2689
2728
  if (signal?.aborted)
2690
2729
  throw new RpcError("CANCELLED", "Worktree operation was cancelled");
2691
- const catalog = { ...await this.store.catalog(), tasks: [], env };
2730
+ const catalog = { ...await this.store.catalog(), tasks: [], env: env2 };
2692
2731
  signal?.throwIfAborted();
2693
2732
  const run2 = this.begin(
2694
2733
  owner,
@@ -2822,12 +2861,14 @@ class TaskRunner {
2822
2861
  }
2823
2862
  async spawnAttempt(run2) {
2824
2863
  const generation = ++run2.generation;
2825
- const { env, variables } = taskEnvironment(
2864
+ const base = await (this.environment ??= this.shellEnvironment(this.store.root));
2865
+ const { env: env2, variables } = taskEnvironment(
2826
2866
  this.store,
2827
2867
  run2.task,
2828
2868
  this.ports,
2829
2869
  run2.catalog,
2830
- run2.context
2870
+ run2.context,
2871
+ base
2831
2872
  );
2832
2873
  await fs.mkdir(variables.OXBIT_PREINIT_DIR, {
2833
2874
  recursive: true,
@@ -2835,7 +2876,7 @@ class TaskRunner {
2835
2876
  });
2836
2877
  const cwd = path.resolve(
2837
2878
  run2.context.root ?? this.store.root,
2838
- expand(run2.task.cwd ?? ".", env)
2879
+ expand(run2.task.cwd ?? ".", env2)
2839
2880
  );
2840
2881
  const real = await fs.realpath(cwd);
2841
2882
  const bound = await fs.realpath(run2.context.root ?? this.store.root), relative = path.relative(bound, real);
@@ -2844,7 +2885,7 @@ class TaskRunner {
2844
2885
  "INVALID_PATH",
2845
2886
  "Task working directory must be within this workspace"
2846
2887
  );
2847
- const args = (run2.task.args ?? []).map((arg) => expand(arg, env));
2888
+ const args = (run2.task.args ?? []).map((arg) => expand(arg, env2));
2848
2889
  const plan = this.ports.get(run2.task.id);
2849
2890
  if (plan && this.forwardPort) {
2850
2891
  const forwarded = /* @__PURE__ */ new Map();
@@ -2866,15 +2907,15 @@ class TaskRunner {
2866
2907
  run2.readySeen = false;
2867
2908
  run2.timedOut = false;
2868
2909
  run2.tail = "";
2869
- const command = run2.task.execution === "process" ? expand(run2.task.command, env) : run2.task.command + (args.length ? " " + args.map(shellQuote).join(" ") : "");
2910
+ const command = run2.task.execution === "process" ? expand(run2.task.command, env2) : run2.task.command + (args.length ? " " + args.map(shellQuote).join(" ") : "");
2870
2911
  const child = run2.task.execution === "process" ? spawn(command, args, {
2871
2912
  cwd: real,
2872
- env,
2913
+ env: env2,
2873
2914
  detached: process.platform !== "win32",
2874
2915
  stdio: ["ignore", "pipe", "pipe"]
2875
2916
  }) : spawn(command, {
2876
2917
  cwd: real,
2877
- env,
2918
+ env: env2,
2878
2919
  shell: true,
2879
2920
  detached: process.platform !== "win32",
2880
2921
  stdio: ["ignore", "pipe", "pipe"]
@@ -2958,19 +2999,19 @@ class TaskRunner {
2958
2999
  let healthy = !run2.task.ready?.pattern || run2.readySeen;
2959
3000
  try {
2960
3001
  if (run2.task.ready?.url) {
2961
- const url = new URL(
3002
+ const url2 = new URL(
2962
3003
  expand(run2.task.ready.url, {
2963
3004
  ...process.env,
2964
3005
  ...run2.value.variables
2965
3006
  })
2966
3007
  );
2967
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
3008
+ if (!["http:", "https:"].includes(url2.protocol) || url2.username || url2.password)
2968
3009
  throw new Error(
2969
3010
  "Readiness URL must use HTTP(S) without credentials"
2970
3011
  );
2971
- if (plan && url.hostname === plan.hostname)
2972
- url.hostname = plan.host === "0.0.0.0" ? "127.0.0.1" : plan.host;
2973
- const response = await fetch(url, {
3012
+ if (plan && url2.hostname === plan.hostname)
3013
+ url2.hostname = plan.host === "0.0.0.0" ? "127.0.0.1" : plan.host;
3014
+ const response = await fetch(url2, {
2974
3015
  signal: AbortSignal.timeout(1500),
2975
3016
  redirect: "manual"
2976
3017
  });
@@ -3055,7 +3096,7 @@ class TaskRunner {
3055
3096
  await this.ports.reserveAgain(run2.task.id).catch(() => {
3056
3097
  });
3057
3098
  }
3058
- finish(run2, code, signal, message) {
3099
+ finish(run2, code, signal, message2) {
3059
3100
  if (run2.ended) return;
3060
3101
  run2.ended = true;
3061
3102
  clearTimeout(run2.monitor);
@@ -3068,7 +3109,7 @@ class TaskRunner {
3068
3109
  this.state(
3069
3110
  run2,
3070
3111
  run2.stopRequested ? "stopped" : code === 0 && run2.task.type !== "service" ? "completed" : "failed",
3071
- message
3112
+ message2
3072
3113
  );
3073
3114
  run2.doneResolve();
3074
3115
  this.emit(
@@ -3346,13 +3387,13 @@ ${command}`,
3346
3387
  "INVALID_STATE",
3347
3388
  "Switch to the source checkout before removing this worktree"
3348
3389
  );
3349
- const exists = await fs.lstat(record.path).catch((error) => {
3390
+ const exists2 = await fs.lstat(record.path).catch((error) => {
3350
3391
  if (error.code === "ENOENT") return null;
3351
3392
  throw error;
3352
3393
  });
3353
- if (exists && (exists.isSymbolicLink() || await fs.realpath(record.path) !== record.path))
3394
+ if (exists2 && (exists2.isSymbolicLink() || await fs.realpath(record.path) !== record.path))
3354
3395
  throw new RpcError("INVALID_STATE", "Managed worktree path changed");
3355
- if (exists) {
3396
+ if (exists2) {
3356
3397
  const known = await this.git(
3357
3398
  ["worktree", "list", "--porcelain", "-z"],
3358
3399
  void 0,
@@ -3379,9 +3420,9 @@ ${command}`,
3379
3420
  record.operationPid = process.pid;
3380
3421
  record.message = void 0;
3381
3422
  await this.persist(records2);
3382
- let removed = !exists;
3423
+ let removed = !exists2;
3383
3424
  try {
3384
- if (exists) {
3425
+ if (exists2) {
3385
3426
  await this.phase(record, "preteardown", owner, signal);
3386
3427
  await this.runner.stopInDirectory(record.path);
3387
3428
  if (signal?.aborted)
@@ -3416,6 +3457,8 @@ function bounded(value) {
3416
3457
  );
3417
3458
  return value;
3418
3459
  }
3460
+ const BRANCH_DIFF_FILES = 50;
3461
+ const BRANCH_DIFF_TEXT = 2e5;
3419
3462
  const conflicted = (index, working) => ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(index + working);
3420
3463
  const digest$1 = (value) => createHash("sha256").update(value).digest("hex");
3421
3464
  class Git {
@@ -3642,14 +3685,14 @@ class Git {
3642
3685
  if (error.code !== "ENOENT") throw error;
3643
3686
  }
3644
3687
  }
3645
- const binary = diff.includes("Binary files ") || before.stdout.includes("\0") || after.includes("\0");
3688
+ const binary2 = diff.includes("Binary files ") || before.stdout.includes("\0") || after.includes("\0");
3646
3689
  const pieces = diff.split(/(?=^@@ )/m);
3647
- const canHunk = !binary && !change?.conflict && !change?.originalPath && pieces.length > 1 && !/^(new file|deleted file|old mode|new mode|diff --cc)/m.test(diff) && (diff.match(/^diff --git /gm)?.length ?? 0) === 1;
3690
+ const canHunk = !binary2 && !change?.conflict && !change?.originalPath && pieces.length > 1 && !/^(new file|deleted file|old mode|new mode|diff --cc)/m.test(diff) && (diff.match(/^diff --git /gm)?.length ?? 0) === 1;
3648
3691
  return bounded({
3649
- before: binary ? "" : before.exitCode ? "" : before.stdout,
3650
- after: binary ? "" : after,
3692
+ before: binary2 ? "" : before.exitCode ? "" : before.stdout,
3693
+ after: binary2 ? "" : after,
3651
3694
  diff,
3652
- binary,
3695
+ binary: binary2,
3653
3696
  fingerprint: digest$1(diff),
3654
3697
  hunks: canHunk ? pieces.slice(1).map((patch, index) => ({
3655
3698
  index,
@@ -3746,18 +3789,18 @@ class Git {
3746
3789
  if (typeof params.path !== "string")
3747
3790
  throw new RpcError("INVALID_PARAMS", "A file path is required");
3748
3791
  await this.files.resolve(params.path, true);
3749
- const detail = await this.show(params.ref, signal), file = detail.files.find((file2) => file2.path === params.path);
3792
+ const detail2 = await this.show(params.ref, signal), file = detail2.files.find((file2) => file2.path === params.path);
3750
3793
  if (!file)
3751
3794
  throw new RpcError("NOT_FOUND", "File is not part of this commit");
3752
- const before = detail.commit.parents[0] ? await this.optional(
3795
+ const before = detail2.commit.parents[0] ? await this.optional(
3753
3796
  [
3754
3797
  "show",
3755
- `${detail.commit.parents[0]}:${file.originalPath ?? file.path}`
3798
+ `${detail2.commit.parents[0]}:${file.originalPath ?? file.path}`
3756
3799
  ],
3757
3800
  signal
3758
3801
  ) : void 0;
3759
3802
  const after = await this.optional(
3760
- ["show", `${detail.commit.id}:${file.path}`],
3803
+ ["show", `${detail2.commit.id}:${file.path}`],
3761
3804
  signal
3762
3805
  );
3763
3806
  const diff = await this.run(
@@ -3768,19 +3811,19 @@ class Git {
3768
3811
  "--no-ext-diff",
3769
3812
  "--no-textconv",
3770
3813
  "--no-color",
3771
- detail.commit.id,
3814
+ detail2.commit.id,
3772
3815
  "--",
3773
3816
  file.path,
3774
3817
  ...file.originalPath ? [file.originalPath] : []
3775
3818
  ],
3776
3819
  signal
3777
3820
  );
3778
- const binary = diff.stdout.includes("Binary files ") || before?.stdout.includes("\0") || after.stdout.includes("\0");
3821
+ const binary2 = diff.stdout.includes("Binary files ") || before?.stdout.includes("\0") || after.stdout.includes("\0");
3779
3822
  return bounded({
3780
- before: binary || before?.exitCode ? "" : before?.stdout ?? "",
3781
- after: binary || after.exitCode ? "" : after.stdout,
3823
+ before: binary2 || before?.exitCode ? "" : before?.stdout ?? "",
3824
+ after: binary2 || after.exitCode ? "" : after.stdout,
3782
3825
  diff: diff.stdout,
3783
- binary: !!binary
3826
+ binary: !!binary2
3784
3827
  });
3785
3828
  }
3786
3829
  async stashes(signal) {
@@ -3826,6 +3869,88 @@ class Git {
3826
3869
  )).stdout
3827
3870
  });
3828
3871
  }
3872
+ /** The upstream remote's default branch, then `origin/HEAD`, then local `main` or `master`. */
3873
+ async baseBranch(branch, signal) {
3874
+ const remote = branch ? (await this.optional(["config", "--get", `branch.${branch}.remote`], signal)).stdout.trim() : "";
3875
+ const candidates = [
3876
+ ...remote && remote !== "." ? [`refs/remotes/${remote}/HEAD`] : [],
3877
+ "refs/remotes/origin/HEAD",
3878
+ "refs/heads/main",
3879
+ "refs/heads/master"
3880
+ ];
3881
+ for (const ref of candidates) {
3882
+ const commit = await this.optional(
3883
+ ["rev-parse", "--verify", "--quiet", "--end-of-options", `${ref}^{commit}`],
3884
+ signal
3885
+ );
3886
+ if (commit.exitCode) continue;
3887
+ const name = await this.optional(["rev-parse", "--abbrev-ref", ref], signal);
3888
+ return { name: name.stdout.trim() || ref, commit: commit.stdout.trim() };
3889
+ }
3890
+ return void 0;
3891
+ }
3892
+ async untrackedPatch(relative, signal) {
3893
+ const result = await this.run(
3894
+ ["diff", "--no-index", "--no-ext-diff", "--no-textconv", "--no-color", "--src-prefix=a/", "--dst-prefix=b/", "--", "/dev/null", relative],
3895
+ signal,
3896
+ this.files.root,
3897
+ void 0,
3898
+ true
3899
+ );
3900
+ if (result.exitCode > 1)
3901
+ throw new RpcError("GIT_FAILED", result.stderr.trim() || `Git exited ${result.exitCode}`);
3902
+ return result.stdout;
3903
+ }
3904
+ /** Committed, staged, unstaged and untracked changes since the merge base with the base branch. */
3905
+ async branchDiff(params, signal) {
3906
+ const limit2 = Number(params.limit ?? BRANCH_DIFF_FILES);
3907
+ if (!Number.isInteger(limit2) || limit2 < 0 || limit2 > BRANCH_DIFF_FILES)
3908
+ throw new RpcError("INVALID_PARAMS", "Invalid file limit");
3909
+ const inside = await this.optional(["rev-parse", "--is-inside-work-tree"], signal);
3910
+ if (inside.exitCode || inside.stdout.trim() !== "true") {
3911
+ if (inside.exitCode && !inside.stderr.includes("not a git repository"))
3912
+ throw new RpcError("GIT_FAILED", inside.stderr.trim());
3913
+ return { repository: false };
3914
+ }
3915
+ const head = await this.head(signal);
3916
+ if (!head) throw new RpcError("NOT_FOUND", "This branch has no commits yet");
3917
+ const branch = (await this.optional(["symbolic-ref", "-q", "--short", "HEAD"], signal)).stdout.trim();
3918
+ const base = await this.baseBranch(branch, signal);
3919
+ if (!base) throw new RpcError("NOT_FOUND", "No base branch: origin/HEAD, main and master are missing");
3920
+ const mergeBase = await this.optional(["merge-base", base.commit, head], signal);
3921
+ if (mergeBase.exitCode)
3922
+ throw new RpcError("NOT_FOUND", `${branch || "HEAD"} has no common history with ${base.name}`);
3923
+ const from = mergeBase.stdout.trim();
3924
+ const names = async (args) => (await this.run(args, signal)).stdout.split("\0").filter(Boolean);
3925
+ const tracked = await names(["diff", "--name-only", "--relative", "-z", from, "--"]);
3926
+ const untracked = await names(["ls-files", "--others", "--exclude-standard", "-z", "--"]);
3927
+ const files = [];
3928
+ let size = 0, truncated = false;
3929
+ for (const [index, relative] of [...tracked, ...untracked].entries()) {
3930
+ if (files.length >= limit2 || size >= BRANCH_DIFF_TEXT) {
3931
+ truncated = true;
3932
+ break;
3933
+ }
3934
+ let diff = "";
3935
+ try {
3936
+ diff = index < tracked.length ? (await this.run(
3937
+ ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--relative", "--src-prefix=a/", "--dst-prefix=b/", from, "--", relative],
3938
+ signal
3939
+ )).stdout : await this.untrackedPatch(relative, signal);
3940
+ } catch (error) {
3941
+ if (error.code !== "OUTPUT_LIMIT") throw error;
3942
+ truncated = true;
3943
+ }
3944
+ const binary2 = /^Binary files /m.test(diff);
3945
+ if (size + diff.length > BRANCH_DIFF_TEXT) {
3946
+ diff = diff.slice(0, BRANCH_DIFF_TEXT - size);
3947
+ truncated = true;
3948
+ }
3949
+ size += diff.length;
3950
+ files.push({ path: relative, diff, binary: binary2 });
3951
+ }
3952
+ return bounded({ base: base.name, head: branch || head.slice(0, 7), files, truncated });
3953
+ }
3829
3954
  async action(method, params, signal, progress) {
3830
3955
  const pending = this.queue.catch(() => {
3831
3956
  }).then(() => this.execute(method, params, signal, progress));
@@ -4319,9 +4444,9 @@ class ExternalSources {
4319
4444
  }
4320
4445
  }
4321
4446
  async authorize(uri) {
4322
- const url = new URL(uri);
4323
- if (url.protocol !== "file:" || url.host || url.search || url.hash) throw new RpcError("PATH_DENIED", "External sources require local file URIs");
4324
- const file = await fs.realpath(fileURLToPath(url));
4447
+ const url2 = new URL(uri);
4448
+ if (url2.protocol !== "file:" || url2.host || url2.search || url2.hash) throw new RpcError("PATH_DENIED", "External sources require local file URIs");
4449
+ const file = await fs.realpath(fileURLToPath(url2));
4325
4450
  if (!this.roots.some((root) => contains(root, file))) throw new RpcError("PATH_DENIED", "Source is outside registered dependency roots");
4326
4451
  const stat = await fs.stat(file);
4327
4452
  if (!stat.isFile() || stat.size > 20 * 1024 * 1024) throw new RpcError("PATH_DENIED", "Source is not a supported text file");
@@ -4331,16 +4456,16 @@ class ExternalSources {
4331
4456
  return { handle, uri, name: path.basename(file), readonly: true };
4332
4457
  }
4333
4458
  async read(handle) {
4334
- const entry = this.handles.get(handle);
4335
- if (!entry || !this.roots.some((root) => contains(root, entry.file)) || await fs.realpath(entry.file) !== entry.file) throw new RpcError("PATH_DENIED", "External source handle expired");
4336
- const descriptor = await fs.open(entry.file, constants$1.O_RDONLY | constants$1.O_NOFOLLOW);
4459
+ const entry2 = this.handles.get(handle);
4460
+ if (!entry2 || !this.roots.some((root) => contains(root, entry2.file)) || await fs.realpath(entry2.file) !== entry2.file) throw new RpcError("PATH_DENIED", "External source handle expired");
4461
+ const descriptor = await fs.open(entry2.file, constants$1.O_RDONLY | constants$1.O_NOFOLLOW);
4337
4462
  try {
4338
4463
  const stat = await descriptor.stat();
4339
4464
  if (!stat.isFile() || stat.size > 20 * 1024 * 1024) throw new RpcError("PATH_DENIED", "External source is not a supported text file");
4340
4465
  const bytes = await descriptor.readFile();
4341
- const current = await fs.stat(entry.file);
4342
- if (stat.dev !== current.dev || stat.ino !== current.ino || bytes.length > 20 * 1024 * 1024 || await fs.realpath(entry.file) !== entry.file || bytes.includes(0)) throw new RpcError("PATH_DENIED", "External source changed or is binary");
4343
- return { handle, uri: entry.uri, name: path.basename(entry.file), text: new TextDecoder("utf-8", { fatal: true }).decode(bytes), revision: createHash("sha256").update(bytes).digest("hex"), readonly: true };
4466
+ const current = await fs.stat(entry2.file);
4467
+ if (stat.dev !== current.dev || stat.ino !== current.ino || bytes.length > 20 * 1024 * 1024 || await fs.realpath(entry2.file) !== entry2.file || bytes.includes(0)) throw new RpcError("PATH_DENIED", "External source changed or is binary");
4468
+ return { handle, uri: entry2.uri, name: path.basename(entry2.file), text: new TextDecoder("utf-8", { fatal: true }).decode(bytes), revision: createHash("sha256").update(bytes).digest("hex"), readonly: true };
4344
4469
  } finally {
4345
4470
  await descriptor.close();
4346
4471
  }
@@ -4531,17 +4656,17 @@ class LanguageServer {
4531
4656
  }
4532
4657
  }
4533
4658
  }
4534
- send(message) {
4535
- if (message.id !== void 0 && !message.method) {
4536
- const pending = this.incoming.get(message.id);
4659
+ send(message2) {
4660
+ if (message2.id !== void 0 && !message2.method) {
4661
+ const pending = this.incoming.get(message2.id);
4537
4662
  if (!pending) return;
4538
- this.incoming.delete(message.id);
4539
- if (message.error) pending.reject(new ResponseError(message.error.code, message.error.message, message.error.data));
4540
- else pending.resolve(message.result);
4663
+ this.incoming.delete(message2.id);
4664
+ if (message2.error) pending.reject(new ResponseError(message2.error.code, message2.error.message, message2.error.data));
4665
+ else pending.resolve(message2.result);
4541
4666
  return;
4542
4667
  }
4543
4668
  if (!this.rpc) throw new RpcError("LSP_UNAVAILABLE", "Language server is stopped");
4544
- void this.rpc.sendNotification(message.method, ...Array.isArray(message.params) ? message.params : [message.params]).catch((error) => {
4669
+ void this.rpc.sendNotification(message2.method, ...Array.isArray(message2.params) ? message2.params : [message2.params]).catch((error) => {
4545
4670
  if (this.rpc) this.emit("window/logMessage", { type: 1, message: String(error) });
4546
4671
  });
4547
4672
  }
@@ -4577,7 +4702,10 @@ class LanguageServer {
4577
4702
  if (resume) this.paused = false;
4578
4703
  if (!resume && this.phase === "failed") throw new RpcError("LSP_FAILED", this.error || "Language server failed. Retry from Language Servers.");
4579
4704
  if (this.paused) throw new RpcError("LSP_STOPPED", "Language server is stopped. Start it from Language Servers.");
4580
- if (this.starting) return this.starting;
4705
+ if (this.starting) {
4706
+ this.emit("oxbit/serverState", { state: this.phase });
4707
+ return this.starting;
4708
+ }
4581
4709
  if (this.child && this.capabilities)
4582
4710
  return { capabilities: this.capabilities, registrations: [...this.registrations.values()], rootUri: this.uri(""), projectRootUri: pathToFileURL(this.options.root ?? this.files.root).href, version: this.launchSpec?.version };
4583
4711
  this.error = "";
@@ -4696,12 +4824,12 @@ class LanguageServer {
4696
4824
  });
4697
4825
  connection.listen();
4698
4826
  this.child.stderr.on("data", (data) => {
4699
- let message = data.toString().slice(0, 16e3);
4827
+ let message2 = data.toString().slice(0, 16e3);
4700
4828
  const key = this.launchSpec?.initializationOptions?.licenceKey;
4701
- if (typeof key === "string" && key) message = message.replaceAll(key, "[redacted]");
4702
- this.output.push(message);
4829
+ if (typeof key === "string" && key) message2 = message2.replaceAll(key, "[redacted]");
4830
+ this.output.push(message2);
4703
4831
  while (this.output.join("").length > 16e3) this.output.shift();
4704
- this.emit("window/logMessage", { type: 4, message });
4832
+ this.emit("window/logMessage", { type: 4, message: message2 });
4705
4833
  });
4706
4834
  const launched = this.child;
4707
4835
  this.child.on("error", (error) => {
@@ -4762,61 +4890,61 @@ class LanguageServer {
4762
4890
  this.emit("oxbit/serverState", { state: "running", name: this.serverName });
4763
4891
  return { capabilities: this.capabilities, registrations: [...this.registrations.values()], rootUri: this.uri(""), projectRootUri: pathToFileURL(this.options.root ?? this.files.root).href, version: this.launchSpec?.version };
4764
4892
  }
4765
- receive(message) {
4766
- if (setting("LSP_TRACE")) this.emit("window/logMessage", { type: 4, message: `${this.serverName}: ${message.method ?? "response"}` });
4767
- if (message.id !== void 0 && message.method) {
4768
- if (message.method === "vscode/content" && this.launchSpec?.schemaContent) {
4893
+ receive(message2) {
4894
+ if (setting("LSP_TRACE")) this.emit("window/logMessage", { type: 4, message: `${this.serverName}: ${message2.method ?? "response"}` });
4895
+ if (message2.id !== void 0 && message2.method) {
4896
+ if (message2.method === "vscode/content" && this.launchSpec?.schemaContent) {
4769
4897
  const connection = this.rpc;
4770
- const uri = Array.isArray(message.params) && message.params.length === 1 ? message.params[0] : message.params;
4898
+ const uri = Array.isArray(message2.params) && message2.params.length === 1 ? message2.params[0] : message2.params;
4771
4899
  void this.launchSpec.schemaContent(uri).then(
4772
4900
  (result) => {
4773
- if (this.rpc === connection) this.send({ id: message.id, result });
4901
+ if (this.rpc === connection) this.send({ id: message2.id, result });
4774
4902
  },
4775
4903
  (error) => {
4776
- if (this.rpc === connection) this.send({ id: message.id, error: { code: -32603, message: String(error) } });
4904
+ if (this.rpc === connection) this.send({ id: message2.id, error: { code: -32603, message: String(error) } });
4777
4905
  }
4778
4906
  );
4779
4907
  return;
4780
4908
  }
4781
- if (["workspace/semanticTokens/refresh", "workspace/inlayHint/refresh"].includes(message.method)) {
4782
- this.emit("oxbit/refresh", { method: message.method });
4783
- this.send({ id: message.id, result: null });
4909
+ if (["workspace/semanticTokens/refresh", "workspace/inlayHint/refresh"].includes(message2.method)) {
4910
+ this.emit("oxbit/refresh", { method: message2.method });
4911
+ this.send({ id: message2.id, result: null });
4784
4912
  return;
4785
4913
  }
4786
- if (message.method === "window/showMessageRequest") {
4914
+ if (message2.method === "window/showMessageRequest") {
4787
4915
  const connection = this.rpc;
4788
4916
  const timer = setTimeout(() => {
4789
- if (this.rpc === connection) this.send({ id: message.id, result: null });
4917
+ if (this.rpc === connection) this.send({ id: message2.id, result: null });
4790
4918
  }, 3e4);
4791
4919
  timer.unref();
4792
- this.emit("oxbit/serverRequest", { id: message.id, method: message.method, params: message.params });
4920
+ this.emit("oxbit/serverRequest", { id: message2.id, method: message2.method, params: message2.params });
4793
4921
  return;
4794
4922
  }
4795
- if (message.method === "workspace/applyEdit") {
4923
+ if (message2.method === "workspace/applyEdit") {
4796
4924
  const child = this.child;
4797
4925
  void (async () => {
4798
- await this.validate(message.params?.edit);
4799
- return this.applyEdit ? await this.applyEdit(message.params.edit, message.params.label) : { applied: false, failureReason: "No initiating document client is available" };
4926
+ await this.validate(message2.params?.edit);
4927
+ return this.applyEdit ? await this.applyEdit(message2.params.edit, message2.params.label) : { applied: false, failureReason: "No initiating document client is available" };
4800
4928
  })().catch((error) => ({ applied: false, failureReason: String(error) })).then((result) => {
4801
- if (this.child === child) this.send({ jsonrpc: "2.0", id: message.id, result });
4929
+ if (this.child === child) this.send({ jsonrpc: "2.0", id: message2.id, result });
4802
4930
  });
4803
4931
  return;
4804
4932
  }
4805
- if (message.method === "client/registerCapability" || message.method === "client/unregisterCapability") {
4806
- const adding = message.method === "client/registerCapability";
4807
- const registrations = adding ? message.params?.registrations : message.params?.unregisterations;
4933
+ if (message2.method === "client/registerCapability" || message2.method === "client/unregisterCapability") {
4934
+ const adding = message2.method === "client/registerCapability";
4935
+ const registrations = adding ? message2.params?.registrations : message2.params?.unregisterations;
4808
4936
  const syncMethods = ["textDocument/didOpen", "textDocument/didClose", "textDocument/didChange", "textDocument/didSave", "textDocument/willSave", "textDocument/willSaveWaitUntil"];
4809
4937
  const watcherShape = (watcher) => (typeof watcher?.globPattern === "string" || typeof watcher?.globPattern?.pattern === "string") && (watcher.kind === void 0 || Number.isInteger(watcher.kind) && watcher.kind >= 1 && watcher.kind <= 7);
4810
4938
  const supported = (item) => item && typeof item.id === "string" && (lspCapabilityKeys[item.method] || syncMethods.includes(item.method) || item.method === "workspace/didChangeConfiguration" || item.method === "workspace/didChangeWorkspaceFolders" || item.method === "workspace/didChangeWatchedFiles" && Array.isArray(item.registerOptions?.watchers) && item.registerOptions.watchers.every(watcherShape));
4811
4939
  if (!Array.isArray(registrations) || new Set(registrations.map((item) => item?.id)).size !== registrations.length || registrations.some((item) => adding ? !supported(item) || this.registrations.has(item.id) : this.registrations.get(item.id)?.method !== item.method)) {
4812
- this.send({ id: message.id, error: { code: -32602, message: "Unsupported or unknown capability registration" } });
4940
+ this.send({ id: message2.id, error: { code: -32602, message: "Unsupported or unknown capability registration" } });
4813
4941
  } else {
4814
4942
  const previousSync = new Map([...this.documents.keys()].map((uri) => [uri, this.syncOptions(uri).openClose]));
4815
4943
  for (const item of registrations) {
4816
4944
  if (adding) this.registrations.set(item.id, item);
4817
4945
  else this.registrations.delete(item.id);
4818
4946
  }
4819
- this.send({ id: message.id, result: null });
4947
+ this.send({ id: message2.id, result: null });
4820
4948
  for (const [uri, wasOpen] of previousSync) {
4821
4949
  const isOpen = this.syncOptions(uri).openClose;
4822
4950
  if (!wasOpen && isOpen) this.opened(uri);
@@ -4826,15 +4954,15 @@ class LanguageServer {
4826
4954
  }
4827
4955
  return;
4828
4956
  }
4829
- if (message.method === "workspace/workspaceFolders") {
4830
- this.send({ jsonrpc: "2.0", id: message.id, result: [{ uri: pathToFileURL(this.options.root ?? this.files.root).href, name: path.basename(this.options.root ?? this.files.root) }] });
4957
+ if (message2.method === "workspace/workspaceFolders") {
4958
+ this.send({ jsonrpc: "2.0", id: message2.id, result: [{ uri: pathToFileURL(this.options.root ?? this.files.root).href, name: path.basename(this.options.root ?? this.files.root) }] });
4831
4959
  return;
4832
4960
  }
4833
- if (message.method === "workspace/configuration")
4961
+ if (message2.method === "workspace/configuration")
4834
4962
  this.send({
4835
4963
  jsonrpc: "2.0",
4836
- id: message.id,
4837
- result: (message.params?.items ?? []).map((item) => {
4964
+ id: message2.id,
4965
+ result: (message2.params?.items ?? []).map((item) => {
4838
4966
  if (item.scopeUri) {
4839
4967
  try {
4840
4968
  const scope = fileURLToPath(item.scopeUri), root = this.options.root ?? this.files.root;
@@ -4849,28 +4977,28 @@ class LanguageServer {
4849
4977
  return item.section.split(".").reduce((value, key) => value?.[key], settings) ?? null;
4850
4978
  })
4851
4979
  });
4852
- else if (message.method === "window/workDoneProgress/create")
4853
- this.send({ jsonrpc: "2.0", id: message.id, result: null });
4854
- else this.send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `Unsupported server request ${message.method}` } });
4980
+ else if (message2.method === "window/workDoneProgress/create")
4981
+ this.send({ jsonrpc: "2.0", id: message2.id, result: null });
4982
+ else this.send({ jsonrpc: "2.0", id: message2.id, error: { code: -32601, message: `Unsupported server request ${message2.method}` } });
4855
4983
  return;
4856
4984
  }
4857
- if (message.method === "tsserver/request" && this.companion) {
4985
+ if (message2.method === "tsserver/request" && this.companion) {
4858
4986
  const child = this.child;
4859
- for (const [id, command, payload] of Array.isArray(message.params?.[0]) ? message.params : [message.params]) void this.companion.request("workspace/executeCommand", {
4987
+ for (const [id, command, payload] of Array.isArray(message2.params?.[0]) ? message2.params : [message2.params]) void this.companion.request("workspace/executeCommand", {
4860
4988
  command: "typescript.tsserverRequest",
4861
4989
  arguments: [command, payload]
4862
4990
  }).catch(() => null).then((result) => {
4863
- if (this.child === child) this.send({ jsonrpc: "2.0", method: "tsserver/response", params: Array.isArray(message.params?.[0]) ? [[id, result?.body ?? null]] : [id, result?.body ?? null] });
4991
+ if (this.child === child) this.send({ jsonrpc: "2.0", method: "tsserver/response", params: Array.isArray(message2.params?.[0]) ? [[id, result?.body ?? null]] : [id, result?.body ?? null] });
4864
4992
  });
4865
4993
  return;
4866
4994
  }
4867
- if (message.method === "textDocument/publishDiagnostics" && this.launchSpec?.diagnostics) message.params = { ...message.params, diagnostics: this.launchSpec.diagnostics(message.params.diagnostics ?? []) };
4868
- if (message.method === "textDocument/publishDiagnostics" && this.companion) {
4869
- this.primaryDiagnostics.set(message.params.uri, message.params.diagnostics ?? []);
4870
- this.emit(message.method, { ...message.params, diagnostics: [...message.params.diagnostics ?? [], ...this.companionDiagnostics.get(message.params.uri) ?? []] });
4995
+ if (message2.method === "textDocument/publishDiagnostics" && this.launchSpec?.diagnostics) message2.params = { ...message2.params, diagnostics: this.launchSpec.diagnostics(message2.params.diagnostics ?? []) };
4996
+ if (message2.method === "textDocument/publishDiagnostics" && this.companion) {
4997
+ this.primaryDiagnostics.set(message2.params.uri, message2.params.diagnostics ?? []);
4998
+ this.emit(message2.method, { ...message2.params, diagnostics: [...message2.params.diagnostics ?? [], ...this.companionDiagnostics.get(message2.params.uri) ?? []] });
4871
4999
  return;
4872
5000
  }
4873
- if (message.method && !this.options.onServerMessage?.(message.method, message.params)) this.emit(message.method, message.params);
5001
+ if (message2.method && !this.options.onServerMessage?.(message2.method, message2.params)) this.emit(message2.method, message2.params);
4874
5002
  }
4875
5003
  respond(id, result) {
4876
5004
  this.send({ id, result });
@@ -5101,8 +5229,8 @@ async function dependencyRoots(root, signal) {
5101
5229
  for (const base of resolve.resolve.paths(name) ?? []) {
5102
5230
  try {
5103
5231
  const installed = await fs.realpath(path.join(base, name));
5104
- const pkg = JSON.parse(await fs.readFile(path.join(installed, "package.json"), "utf8"));
5105
- if (pkg.name !== name || installed === path.parse(installed).root) break;
5232
+ const pkg2 = JSON.parse(await fs.readFile(path.join(installed, "package.json"), "utf8"));
5233
+ if (pkg2.name !== name || installed === path.parse(installed).root) break;
5106
5234
  if (!roots.has(installed)) {
5107
5235
  roots.add(installed);
5108
5236
  queue.push({ directory: installed, development: false });
@@ -5207,6 +5335,47 @@ function deferToRuff(spec) {
5207
5335
  diagnostics: (diagnostics) => diagnostics.filter((item) => item.code !== void 0)
5208
5336
  };
5209
5337
  }
5338
+ const phpProbes = [
5339
+ { command: ["herd", "which-php"], php: (output) => output.includes("No usable PHP version found") ? void 0 : [output.trim()] },
5340
+ { command: ["valet", "which-php"], php: (output) => [output.trim()] },
5341
+ { command: ["./vendor/bin/sail", "ps"], php: () => ["./vendor/bin/sail", "php"] },
5342
+ { command: ["lando", "php", "-r", "echo PHP_BINARY;"], php: () => ["lando", "php"] },
5343
+ { command: ["ddev", "php", "-r", "echo PHP_BINARY;"], php: () => ["ddev", "php"] },
5344
+ { command: ["php", "-r", "echo PHP_BINARY;"], php: (output) => [output.trim()] }
5345
+ ];
5346
+ function probe(command, cwd, env2, timeout, signal) {
5347
+ return new Promise((resolve) => {
5348
+ const child = spawn(command[0], command.slice(1), { cwd, env: env2, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "ignore"] });
5349
+ trackChild(child);
5350
+ let output = "";
5351
+ const finish = (value) => {
5352
+ clearTimeout(timer);
5353
+ signal?.removeEventListener("abort", stop2);
5354
+ resolve(value);
5355
+ };
5356
+ const stop2 = () => {
5357
+ killProcess(child);
5358
+ finish();
5359
+ };
5360
+ const timer = setTimeout(stop2, timeout);
5361
+ signal?.addEventListener("abort", stop2, { once: true });
5362
+ child.stdout.on("data", (data) => {
5363
+ output = (output + data.toString()).slice(-4096);
5364
+ });
5365
+ child.on("error", () => finish());
5366
+ child.on("close", (code) => finish(code === 0 ? output : void 0));
5367
+ });
5368
+ }
5369
+ async function laravelPhpCommand(root, env2, signal, timeout = 5e3) {
5370
+ for (const { command, php } of phpProbes) {
5371
+ signal?.throwIfAborted();
5372
+ const output = await probe(command, root, env2, timeout, signal);
5373
+ const found = output === void 0 ? void 0 : php(output);
5374
+ if (found?.[0]) return found;
5375
+ }
5376
+ signal?.throwIfAborted();
5377
+ throw new Error("Laravel needs PHP on the runtime host. No PHP was found through Herd, Valet, Sail, Lando, DDEV or php on PATH. Install PHP or set languageServers.laravel.initializationOptions.phpCommand.");
5378
+ }
5210
5379
  const npmPresets = {
5211
5380
  typescript: ["typescript-language-server", "lib/cli.mjs", ["typescript"]],
5212
5381
  html: ["vscode-langservers-extracted", "bin/vscode-html-language-server"],
@@ -5222,11 +5391,11 @@ const npmPresets = {
5222
5391
  };
5223
5392
  async function resolveLaunch(id, root, installer, signal) {
5224
5393
  if (id === "laravel" || id === "ruff") {
5225
- const binary = await installer.native(id, signal);
5226
- return { executable: binary.executable, args: id === "ruff" ? ["server"] : [], version: binary.version };
5394
+ const binary2 = await installer.native(id, signal);
5395
+ return { executable: binary2.executable, args: id === "ruff" ? ["server"] : [], version: binary2.version };
5227
5396
  }
5228
5397
  if (id === "marksman" || id === "taplo") {
5229
- const binary = await installer.native(id, signal);
5398
+ const binary2 = await installer.native(id, signal);
5230
5399
  const schemaDirectory = path.join(installer.cache, "schemas");
5231
5400
  if (id === "taplo") {
5232
5401
  await fs.mkdir(schemaDirectory, { recursive: true });
@@ -5234,9 +5403,9 @@ async function resolveLaunch(id, root, installer, signal) {
5234
5403
  await fs.writeFile(path.join(schemaDirectory, "cargo-config.json"), JSON.stringify(cargoConfigSchema));
5235
5404
  }
5236
5405
  return {
5237
- executable: binary.executable,
5406
+ executable: binary2.executable,
5238
5407
  args: id === "marksman" ? ["server"] : ["lsp", "stdio"],
5239
- version: binary.version,
5408
+ version: binary2.version,
5240
5409
  ...id === "marksman" ? { env: { DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: "1", DOTNET_CLI_TELEMETRY_OPTOUT: "1" } } : {},
5241
5410
  ...id === "taplo" ? { dependencyRoots: [schemaDirectory], settingsSection: "evenBetterToml", initializationOptions: { cachePath: path.join(installer.cache, "taplo-schemas"), configurationSection: "evenBetterToml" }, settings: { evenBetterToml: { schema: { enabled: true, catalogs: [], associations: {
5242
5411
  "(^|/)Cargo\\.toml$": pathToFileURL(path.join(schemaDirectory, "cargo.json")).href,
@@ -5253,9 +5422,9 @@ async function resolveLaunch(id, root, installer, signal) {
5253
5422
  settings: { xml: { telemetry: { enabled: false }, downloadExternalResources: { enabled: false } } }
5254
5423
  };
5255
5424
  }
5256
- const entry = npmPresets[id];
5257
- if (!entry) throw new Error(`Unknown language server preset: ${id}`);
5258
- const [name, bin, dependencies = []] = entry;
5425
+ const entry2 = npmPresets[id];
5426
+ if (!entry2) throw new Error(`Unknown language server preset: ${id}`);
5427
+ const [name, bin, dependencies = []] = entry2;
5259
5428
  const installation = await installer.npm(id, [name, ...dependencies], signal);
5260
5429
  const modules = path.join(installation.directory, "node_modules");
5261
5430
  const bootstrap = await consoleBootstrap(installer.cache);
@@ -5322,10 +5491,10 @@ function lockedPackages(roots) {
5322
5491
  const selected = /* @__PURE__ */ new Map();
5323
5492
  function visit(location) {
5324
5493
  if (selected.has(location)) return;
5325
- const entry = packages[location];
5326
- if (!entry?.resolved || !entry.integrity || !entry.version) throw new Error(`Missing locked package: ${location}`);
5327
- selected.set(location, entry);
5328
- for (const name of Object.keys({ ...entry.dependencies, ...entry.optionalDependencies })) {
5494
+ const entry2 = packages[location];
5495
+ if (!entry2?.resolved || !entry2.integrity || !entry2.version) throw new Error(`Missing locked package: ${location}`);
5496
+ selected.set(location, entry2);
5497
+ for (const name of Object.keys({ ...entry2.dependencies, ...entry2.optionalDependencies })) {
5329
5498
  let base = location, found;
5330
5499
  while (true) {
5331
5500
  const candidate = `${base ? base + "/" : ""}node_modules/${name}`;
@@ -5337,7 +5506,7 @@ function lockedPackages(roots) {
5337
5506
  base = base.includes("/node_modules/") ? base.slice(0, base.lastIndexOf("/node_modules/")) : "";
5338
5507
  }
5339
5508
  if (found) visit(found);
5340
- else if (!entry.optionalDependencies?.[name]) throw new Error(`Unresolved locked dependency: ${name}`);
5509
+ else if (!entry2.optionalDependencies?.[name]) throw new Error(`Unresolved locked dependency: ${name}`);
5341
5510
  }
5342
5511
  }
5343
5512
  roots.forEach((name) => visit(`node_modules/${name}`));
@@ -5356,13 +5525,13 @@ class ManagedInstaller {
5356
5525
  cache;
5357
5526
  platform;
5358
5527
  download;
5359
- async bytes(url, integrity, signal) {
5360
- if (!url.startsWith("https://")) throw new Error("Managed artifacts require HTTPS");
5528
+ async bytes(url2, integrity, signal) {
5529
+ if (!url2.startsWith("https://")) throw new Error("Managed artifacts require HTTPS");
5361
5530
  let failure2;
5362
5531
  for (let attempt = 0; attempt < 3; attempt++) {
5363
5532
  signal?.throwIfAborted();
5364
5533
  try {
5365
- const response = await this.download(url, { signal: AbortSignal.any([AbortSignal.timeout(12e4), ...signal ? [signal] : []]) });
5534
+ const response = await this.download(url2, { signal: AbortSignal.any([AbortSignal.timeout(12e4), ...signal ? [signal] : []]) });
5366
5535
  if (!response.ok || !response.body) throw new Error(`Artifact download failed (${response.status})`);
5367
5536
  const chunks = [];
5368
5537
  let size = 0;
@@ -5395,13 +5564,13 @@ class ManagedInstaller {
5395
5564
  strict: true,
5396
5565
  preservePaths: false,
5397
5566
  filter(name, candidate) {
5398
- const entry = candidate;
5567
+ const entry2 = candidate;
5399
5568
  const normalized = name.split("/").slice(strip).join("/");
5400
5569
  if (name.startsWith("/") || name.includes("\\") || name.split("/").includes("..")) rejection ??= new Error("Unsafe archive path");
5401
- else if (!["File", "OldFile", "Directory", "SymbolicLink"].includes(entry.type)) rejection ??= new Error("Unsupported archive entry");
5402
- else if (entry.type === "SymbolicLink") {
5403
- const target = path.resolve(directory, path.dirname(normalized), entry.linkpath ?? "");
5404
- if (path.isAbsolute(entry.linkpath ?? "") || !target.startsWith(directory + path.sep)) rejection ??= new Error("Archive link leaves installation");
5570
+ else if (!["File", "OldFile", "Directory", "SymbolicLink"].includes(entry2.type)) rejection ??= new Error("Unsupported archive entry");
5571
+ else if (entry2.type === "SymbolicLink") {
5572
+ const target = path.resolve(directory, path.dirname(normalized), entry2.linkpath ?? "");
5573
+ if (path.isAbsolute(entry2.linkpath ?? "") || !target.startsWith(directory + path.sep)) rejection ??= new Error("Archive link leaves installation");
5405
5574
  }
5406
5575
  return !rejection;
5407
5576
  }
@@ -5447,19 +5616,22 @@ class ManagedInstaller {
5447
5616
  }
5448
5617
  }
5449
5618
  } catch {
5450
- try {
5451
- if (Date.now() - (await fs.stat(lock)).mtimeMs > 6e4) {
5452
- await fs.rm(lock, { recursive: true, force: true });
5453
- continue;
5454
- }
5455
- } catch {
5619
+ }
5620
+ try {
5621
+ if (Date.now() - (await fs.stat(lock)).mtimeMs > 6e4) {
5622
+ await fs.rm(lock, { recursive: true, force: true });
5623
+ continue;
5456
5624
  }
5625
+ } catch {
5457
5626
  }
5458
5627
  if (Date.now() > deadline) throw new Error("Timed out waiting for another language server installation");
5459
5628
  await setTimeout$1(100, void 0, { signal });
5460
5629
  }
5461
5630
  }
5462
5631
  const stage = path.join(base, `.staging-${randomUUID()}`);
5632
+ const heartbeat = setInterval(() => void fs.utimes(lock, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => {
5633
+ }), 1e4);
5634
+ heartbeat.unref();
5463
5635
  try {
5464
5636
  if (await complete()) return target;
5465
5637
  for (const name of await fs.readdir(base)) if (name.startsWith(".staging-")) await fs.rm(path.join(base, name), { recursive: true, force: true });
@@ -5475,6 +5647,7 @@ class ManagedInstaller {
5475
5647
  await fs.rename(stage, target);
5476
5648
  return target;
5477
5649
  } finally {
5650
+ clearInterval(heartbeat);
5478
5651
  await fs.rm(stage, { recursive: true, force: true });
5479
5652
  await fs.rm(lock, { recursive: true, force: true });
5480
5653
  }
@@ -5485,7 +5658,7 @@ class ManagedInstaller {
5485
5658
  const queue = [...entries].sort(([a], [b]) => a.split("/").length - b.split("/").length);
5486
5659
  for (let start = 0; start < queue.length; start += 6) {
5487
5660
  const batch = queue.slice(start, start + 6);
5488
- const bytes = await Promise.all(batch.map(([, entry]) => this.bytes(entry.resolved, entry.integrity, signal)));
5661
+ const bytes = await Promise.all(batch.map(([, entry2]) => this.bytes(entry2.resolved, entry2.integrity, signal)));
5489
5662
  for (const [index, [location]] of batch.entries()) {
5490
5663
  signal?.throwIfAborted();
5491
5664
  await this.extract(bytes[index], path.join(stage, location), 1);
@@ -5540,17 +5713,17 @@ class JsonSchemas {
5540
5713
  uri.hash = "";
5541
5714
  return uri.href;
5542
5715
  }
5543
- async cached(url) {
5716
+ async cached(url2) {
5544
5717
  try {
5545
- const file = path.join(this.cache, digest(url) + ".json");
5718
+ const file = path.join(this.cache, digest(url2) + ".json");
5546
5719
  if ((await fs.stat(file)).size > maxBytes * 3) return;
5547
5720
  const value = JSON.parse(await fs.readFile(file, "utf8"));
5548
- if (value.url === url && typeof value.text === "string" && typeof value.fetchedAt === "number" && value.integrity === digest(value.text)) return value;
5721
+ if (value.url === url2 && typeof value.text === "string" && typeof value.fetchedAt === "number" && value.integrity === digest(value.text)) return value;
5549
5722
  } catch {
5550
5723
  }
5551
5724
  }
5552
- async fetchJson(url) {
5553
- let current = url;
5725
+ async fetchJson(url2) {
5726
+ let current = url2;
5554
5727
  const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(8e3)]);
5555
5728
  for (let redirects = 0; redirects < 6; redirects++) {
5556
5729
  const response = await this.download(current, { signal, redirect: "manual", headers: { Accept: "application/schema+json, application/json" } });
@@ -5584,47 +5757,47 @@ class JsonSchemas {
5584
5757
  throw new Error("Too many JSON schema redirects");
5585
5758
  }
5586
5759
  async remote(value, enabled) {
5587
- const url = this.remoteUri(value), cached2 = await this.cached(url);
5760
+ const url2 = this.remoteUri(value), cached2 = await this.cached(url2);
5588
5761
  if (cached2 && (!enabled || Date.now() - cached2.fetchedAt < maxAge)) return cached2.text;
5589
5762
  if (!enabled) throw new Error("Schema downloads are disabled and this schema is not cached");
5590
- if ((this.failures.get(url) ?? 0) > Date.now()) {
5763
+ if ((this.failures.get(url2) ?? 0) > Date.now()) {
5591
5764
  if (cached2) return cached2.text;
5592
5765
  throw new Error("Schema download is temporarily unavailable");
5593
5766
  }
5594
- let pending = this.pending.get(url);
5767
+ let pending = this.pending.get(url2);
5595
5768
  if (!pending) {
5596
5769
  pending = (async () => {
5597
5770
  try {
5598
- const text2 = await this.fetchJson(url);
5771
+ const text2 = await this.fetchJson(url2);
5599
5772
  await fs.mkdir(this.cache, { recursive: true, mode: 448 });
5600
- const target = path.join(this.cache, digest(url) + ".json"), temporary = target + "." + randomUUID();
5773
+ const target = path.join(this.cache, digest(url2) + ".json"), temporary = target + "." + randomUUID();
5601
5774
  try {
5602
- await fs.writeFile(temporary, JSON.stringify({ url, fetchedAt: Date.now(), integrity: digest(text2), text: text2 }), { mode: 384 });
5775
+ await fs.writeFile(temporary, JSON.stringify({ url: url2, fetchedAt: Date.now(), integrity: digest(text2), text: text2 }), { mode: 384 });
5603
5776
  await fs.rename(temporary, target);
5604
5777
  } finally {
5605
5778
  await fs.rm(temporary, { force: true });
5606
5779
  }
5607
5780
  return text2;
5608
5781
  } catch (error) {
5609
- this.failures.set(url, Date.now() + 6e4);
5782
+ this.failures.set(url2, Date.now() + 6e4);
5610
5783
  if (cached2) return cached2.text;
5611
5784
  throw error;
5612
5785
  } finally {
5613
- this.pending.delete(url);
5786
+ this.pending.delete(url2);
5614
5787
  }
5615
5788
  })();
5616
- this.pending.set(url, pending);
5789
+ this.pending.set(url2, pending);
5617
5790
  }
5618
5791
  return pending;
5619
5792
  }
5620
5793
  async content(uri, enabled = true) {
5621
5794
  if (typeof uri !== "string" || uri.length > 8192) throw new Error("Invalid schema URI");
5622
- const url = new URL(uri);
5623
- url.hash = "";
5624
- if (url.href === SETTINGS_SCHEMA_URI || this.settingsPaths.schemaFile && url.href === pathToFileURL(this.settingsPaths.schemaFile).href) return JSON.stringify(settingsSchema);
5625
- if (url.protocol === "file:") {
5626
- url.hash = "";
5627
- const file = await this.files.resolve(path.relative(this.files.root, fileURLToPath(url)).split(path.sep).join("/"));
5795
+ const url2 = new URL(uri);
5796
+ url2.hash = "";
5797
+ if (url2.href === SETTINGS_SCHEMA_URI || this.settingsPaths.schemaFile && url2.href === pathToFileURL(this.settingsPaths.schemaFile).href) return JSON.stringify(settingsSchema);
5798
+ if (url2.protocol === "file:") {
5799
+ url2.hash = "";
5800
+ const file = await this.files.resolve(path.relative(this.files.root, fileURLToPath(url2)).split(path.sep).join("/"));
5628
5801
  if ((await fs.stat(file)).size > maxBytes) throw new Error("JSON schema exceeds 5 MiB");
5629
5802
  return fs.readFile(file, "utf8");
5630
5803
  }
@@ -5773,6 +5946,8 @@ class LanguageServerManager {
5773
5946
  else spec = await resolveLaunch(definition2.id, root, this.installer, signal);
5774
5947
  spec = { ...spec, args: definition2.args ?? spec.args, env: { ...spec.env, ...definition2.env }, initializationOptions: merge(spec.initializationOptions, definition2.initializationOptions), settings: merge(spec.settings, definition2.settings) };
5775
5948
  if (ruff) spec = deferToRuff(spec);
5949
+ if (definition2.id === "laravel" && !spec.initializationOptions?.phpCommand && (spec.initializationOptions?.phpEnvironment ?? "auto") === "auto")
5950
+ spec.initializationOptions = { ...spec.initializationOptions, phpCommand: await laravelPhpCommand(root, { ...process.env, ...spec.env }, signal) };
5776
5951
  if (definition2.id === "json") {
5777
5952
  const options2 = this.project ? (await this.project.reload()).schemas : { catalog: true, download: true, associations: [] };
5778
5953
  spec.settings = await this.schemas.settings(spec.settings ?? {}, options2);
@@ -5813,7 +5988,7 @@ class LanguageServerManager {
5813
5988
  if (!instance.attachments.size && !instance.idle) {
5814
5989
  instance.idle = setTimeout(() => {
5815
5990
  instance.idle = void 0;
5816
- void this.stop(false, instance.id);
5991
+ if (this.instances.get(instance.id) === instance) void this.stop(false, instance.id);
5817
5992
  }, this.idleMs);
5818
5993
  instance.idle.unref();
5819
5994
  }
@@ -6092,24 +6267,24 @@ class ProjectStore {
6092
6267
  } catch {
6093
6268
  }
6094
6269
  const entries = await fs.readdir(directoryPath, { withFileTypes: true }).catch(() => []);
6095
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
6096
- const relative = path.posix.join(directory, entry.name);
6097
- if (entry.isSymbolicLink() || entry.name.startsWith(".oxbit-tmp-") || config.intelligence.exclude.some((pattern) => matchesFilePattern(pattern, relative) || matchesFilePattern(pattern, relative + "/"))) continue;
6270
+ for (const entry2 of entries.sort((a, b) => a.name.localeCompare(b.name))) {
6271
+ const relative = path.posix.join(directory, entry2.name);
6272
+ if (entry2.isSymbolicLink() || entry2.name.startsWith(".oxbit-tmp-") || config.intelligence.exclude.some((pattern) => matchesFilePattern(pattern, relative) || matchesFilePattern(pattern, relative + "/"))) continue;
6098
6273
  let ignored = false;
6099
6274
  for (const [base, rules] of ignores) if (!base || relative.startsWith(base + "/")) {
6100
- const match = rules.test(relative.slice(base ? base.length + 1 : 0) + (entry.isDirectory() ? "/" : ""));
6275
+ const match = rules.test(relative.slice(base ? base.length + 1 : 0) + (entry2.isDirectory() ? "/" : ""));
6101
6276
  if (match.ignored) ignored = true;
6102
6277
  else if (match.unignored) ignored = false;
6103
6278
  }
6104
6279
  if (ignored) continue;
6105
- if (entry.isDirectory()) {
6106
- if (!excludedDirectories.has(entry.name)) {
6280
+ if (entry2.isDirectory()) {
6281
+ if (!excludedDirectories.has(entry2.name)) {
6107
6282
  if (queue.length < config.intelligence.maxFiles) queue.push(relative);
6108
6283
  else result.truncated = true;
6109
6284
  }
6110
6285
  continue;
6111
6286
  }
6112
- if (!entry.isFile()) continue;
6287
+ if (!entry2.isFile()) continue;
6113
6288
  if (result.files.length >= config.intelligence.maxFiles) {
6114
6289
  result.truncated = true;
6115
6290
  break;
@@ -6134,12 +6309,12 @@ class ProjectStore {
6134
6309
  const text2 = await fs.readFile(full, "utf8").catch(() => "");
6135
6310
  bytes += stat.size;
6136
6311
  texts.set(full, text2);
6137
- if (entry.name === "package.json" || entry.name === "composer.json") {
6312
+ if (entry2.name === "package.json" || entry2.name === "composer.json") {
6138
6313
  try {
6139
- const data = JSON.parse(text2), ecosystem = entry.name === "package.json" ? "npm" : "composer";
6140
- const pkg = { path: relative, name: typeof data.name === "string" ? data.name : directory || config.name, ecosystem, dependencies: [] };
6141
- for (const kind of ecosystem === "npm" ? ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] : ["require", "require-dev"]) for (const [name, version] of Object.entries(data[kind] ?? {})) if (typeof version === "string") pkg.dependencies.push({ name, version, kind });
6142
- result.packages.push(pkg);
6314
+ const data = JSON.parse(text2), ecosystem = entry2.name === "package.json" ? "npm" : "composer";
6315
+ const pkg2 = { path: relative, name: typeof data.name === "string" ? data.name : directory || config.name, ecosystem, dependencies: [] };
6316
+ for (const kind of ecosystem === "npm" ? ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] : ["require", "require-dev"]) for (const [name, version] of Object.entries(data[kind] ?? {})) if (typeof version === "string") pkg2.dependencies.push({ name, version, kind });
6317
+ result.packages.push(pkg2);
6143
6318
  } catch {
6144
6319
  result.warnings.push(`Invalid manifest: ${relative}`);
6145
6320
  }
@@ -6175,7 +6350,7 @@ class ProjectStore {
6175
6350
  const values = [base, ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".mdx", ".vue", ".astro", ".json"].flatMap((ext) => [base + ext, path.join(base, "index" + ext)])];
6176
6351
  return values.find((file) => fullPaths.has(file));
6177
6352
  };
6178
- const workspacePackages = new Map(result.packages.filter((pkg) => pkg.ecosystem === "npm").map((pkg) => [pkg.name, pkg.path]));
6353
+ const workspacePackages = new Map(result.packages.filter((pkg2) => pkg2.ecosystem === "npm").map((pkg2) => [pkg2.name, pkg2.path]));
6179
6354
  const exportPath = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.map(exportPath).find(Boolean) : value && typeof value === "object" ? exportPath(value.types ?? value.import ?? value.default ?? value.require) : void 0;
6180
6355
  let analyzed = 0;
6181
6356
  for (const record of result.files) {
@@ -6206,8 +6381,8 @@ class ProjectStore {
6206
6381
  if (!target && workspaceManifest) {
6207
6382
  const file = path.join(this.files.root, workspaceManifest), manifest = JSON.parse(texts.get(file));
6208
6383
  const subpath = specifier.slice(packageName.length), key = subpath ? "." + subpath : ".";
6209
- const entry = exportPath(manifest.exports?.[key] ?? (!subpath ? manifest.exports : void 0)) ?? (!subpath ? manifest.types ?? manifest.typings ?? manifest.module ?? manifest.main ?? "index" : subpath.slice(1));
6210
- if (typeof entry === "string") target = candidate(path.resolve(path.dirname(file), entry));
6384
+ const entry2 = exportPath(manifest.exports?.[key] ?? (!subpath ? manifest.exports : void 0)) ?? (!subpath ? manifest.types ?? manifest.typings ?? manifest.module ?? manifest.main ?? "index" : subpath.slice(1));
6385
+ if (typeof entry2 === "string") target = candidate(path.resolve(path.dirname(file), entry2));
6211
6386
  }
6212
6387
  return { specifier, ...target && fullPaths.has(target) ? { target: fullPaths.get(target) } : !specifier.startsWith(".") && !specifier.startsWith("/") ? { package: specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0] } : {} };
6213
6388
  });
@@ -6217,7 +6392,7 @@ class ProjectStore {
6217
6392
  for (const name of names) if (name && ts.isIdentifier(name) && record.symbols.length < 100) record.symbols.push({ name: name.text, kind: ts.SyntaxKind[statement.kind], line: ast.getLineAndCharacterOfPosition(name.getStart(ast)).line + 1 });
6218
6393
  }
6219
6394
  }
6220
- const dependencies = new Set(result.packages.flatMap((pkg) => pkg.dependencies.map((item) => item.name)));
6395
+ const dependencies = new Set(result.packages.flatMap((pkg2) => pkg2.dependencies.map((item) => item.name)));
6221
6396
  result.frameworks = ["laravel/framework", "next", "nuxt", "astro", "vue", "react", "@mdx-js/mdx"].filter((name) => dependencies.has(name));
6222
6397
  if (result.truncated) result.warnings.push(`File/directory inventory reached the ${config.intelligence.maxFiles} entry budget`);
6223
6398
  result.warnings = result.warnings.slice(0, 100);
@@ -6769,9 +6944,9 @@ class Collaboration {
6769
6944
  }
6770
6945
  }
6771
6946
  async changed(relative) {
6772
- const entry = this.rooms.get(relative);
6773
- if (!entry) return;
6774
- const room = await entry;
6947
+ const entry2 = this.rooms.get(relative);
6948
+ if (!entry2) return;
6949
+ const room = await entry2;
6775
6950
  if (room.saving) return;
6776
6951
  let disk;
6777
6952
  try {
@@ -6820,9 +6995,9 @@ class Collaboration {
6820
6995
  );
6821
6996
  }
6822
6997
  async leave(connection, relative) {
6823
- for (const [key, entry] of this.rooms) {
6998
+ for (const [key, entry2] of this.rooms) {
6824
6999
  if (relative && relative !== key) continue;
6825
- const room = await entry;
7000
+ const room = await entry2;
6826
7001
  room.members.delete(connection);
6827
7002
  const clients = room.clients.get(connection);
6828
7003
  if (clients)
@@ -6830,7 +7005,7 @@ class Collaboration {
6830
7005
  room.clients.delete(connection);
6831
7006
  if (!room.members.size) {
6832
7007
  await room.persist;
6833
- if (!room.members.size && this.rooms.get(key) === entry) {
7008
+ if (!room.members.size && this.rooms.get(key) === entry2) {
6834
7009
  this.rooms.delete(key);
6835
7010
  this.lsp.closeCanonical(key);
6836
7011
  room.awareness.destroy();
@@ -6840,8 +7015,8 @@ class Collaboration {
6840
7015
  }
6841
7016
  }
6842
7017
  async close() {
6843
- for (const entry of this.rooms.values()) {
6844
- const room = await entry;
7018
+ for (const entry2 of this.rooms.values()) {
7019
+ const room = await entry2;
6845
7020
  await room.persist;
6846
7021
  room.awareness.destroy();
6847
7022
  room.doc.destroy();
@@ -6851,7 +7026,7 @@ class Collaboration {
6851
7026
  }
6852
7027
  const terminal = (state2) => !["pending", "running", "unknown"].includes(state2);
6853
7028
  const object$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
6854
- const str = (value, max = 4096) => typeof value === "string" ? value.slice(0, max) : void 0;
7029
+ const str$1 = (value, max = 4096) => typeof value === "string" ? value.slice(0, max) : void 0;
6855
7030
  const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 512 && !value.includes("\0");
6856
7031
  const preview = (value, max = 8192) => value === void 0 ? void 0 : (typeof value === "string" ? value : JSON.stringify(value)).slice(0, max);
6857
7032
  const statusNames = {
@@ -6890,7 +7065,7 @@ function normalizeDelegation(provider, update) {
6890
7065
  {
6891
7066
  providerId: sub.threadId,
6892
7067
  toolId,
6893
- name: str(str(sub.path)?.split("/").filter(Boolean).at(-1), 200),
7068
+ name: str$1(str$1(sub.path)?.split("/").filter(Boolean).at(-1), 200),
6894
7069
  state: state(sub.activity === "started" ? "running" : sub.activity),
6895
7070
  evidence: "provider",
6896
7071
  spawn: sub.activity === "started"
@@ -6906,7 +7081,7 @@ function normalizeDelegation(provider, update) {
6906
7081
  return [
6907
7082
  {
6908
7083
  toolId,
6909
- task: str(raw.prompt),
7084
+ task: str$1(raw.prompt),
6910
7085
  state: update.status === "failed" ? "failed" : "unknown",
6911
7086
  evidence: "tool",
6912
7087
  spawn: true
@@ -6915,11 +7090,11 @@ function normalizeDelegation(provider, update) {
6915
7090
  return ids.map((providerId) => ({
6916
7091
  providerId,
6917
7092
  toolId,
6918
- parent: str(collab.senderThreadId, 512),
6919
- task: str(raw.prompt),
6920
- model: str(raw.model, 200),
7093
+ parent: str$1(collab.senderThreadId, 512),
7094
+ task: str$1(raw.prompt),
7095
+ model: str$1(raw.model, 200),
6921
7096
  state: state(states[providerId]?.status) ?? (collab.tool === "spawnAgent" && update.status === "failed" ? "failed" : void 0),
6922
- result: str(states[providerId]?.message, 16384),
7097
+ result: str$1(states[providerId]?.message, 16384),
6923
7098
  evidence: "provider",
6924
7099
  spawn: collab.tool === "spawnAgent",
6925
7100
  reopen: ["resumeAgent", "sendInput", "followupTask"].includes(collab.tool) && ["running", "pendingInit"].includes(states[providerId]?.status)
@@ -6933,9 +7108,9 @@ function normalizeDelegation(provider, update) {
6933
7108
  {
6934
7109
  toolId,
6935
7110
  providerId: identity(task.agentId ?? outcome.agentId) ? task.agentId ?? outcome.agentId : void 0,
6936
- name: str(task.description, 200),
6937
- task: str(task.prompt),
6938
- model: str(task.model, 200),
7111
+ name: str$1(task.description, 200),
7112
+ task: str$1(task.prompt),
7113
+ model: str$1(task.model, 200),
6939
7114
  durationMs: Number.isFinite(task.durationMs ?? outcome.durationMs) && (task.durationMs ?? outcome.durationMs) >= 0 ? task.durationMs ?? outcome.durationMs : void 0,
6940
7115
  state: state(outcomeName),
6941
7116
  result: preview(task.result ?? outcome.result ?? output.result),
@@ -6948,9 +7123,9 @@ function normalizeDelegation(provider, update) {
6948
7123
  return [
6949
7124
  {
6950
7125
  toolId,
6951
- name: str(raw.description ?? raw.subagent_type, 200),
6952
- task: str(raw.prompt),
6953
- model: str(raw.model, 200),
7126
+ name: str$1(raw.description ?? raw.subagent_type, 200),
7127
+ task: str$1(raw.prompt),
7128
+ model: str$1(raw.model, 200),
6954
7129
  state: update.status === "failed" ? "failed" : "unknown",
6955
7130
  evidence: "tool",
6956
7131
  spawn: true
@@ -7109,8 +7284,8 @@ class SubagentRegistry {
7109
7284
  parentId: this.idForSession(sessionId),
7110
7285
  sessionId: child,
7111
7286
  providerId: child,
7112
- name: str(update.name, 200) || record.name,
7113
- task: str(update.task) ?? record.task,
7287
+ name: str$1(update.name, 200) || record.name,
7288
+ task: str$1(update.task) ?? record.task,
7114
7289
  evidence: "native",
7115
7290
  visibility: "full"
7116
7291
  });
@@ -7232,20 +7407,20 @@ class SubagentRegistry {
7232
7407
  }
7233
7408
  toolActivity(record, update) {
7234
7409
  const id = `tool:${update.toolCallId}`;
7235
- let entry = record.activity.find((a) => a.id === id);
7236
- if (!entry) {
7237
- entry = { id, kind: "tool" };
7238
- record.activity.push(entry);
7239
- }
7240
- entry.title = str(update.title, 200) ?? entry.title ?? "Delegated task";
7241
- if (!["completed", "failed"].includes(entry.status ?? ""))
7242
- entry.status = str(update.status, 40) ?? entry.status;
7243
- entry.input = preview(update.rawInput) ?? entry.input;
7244
- entry.output = preview(toolOutput(update)) ?? entry.output;
7245
- entry.locations = Array.isArray(update.locations) ? update.locations.filter((l) => typeof l.path === "string").slice(0, 16).map((l) => ({
7410
+ let entry2 = record.activity.find((a) => a.id === id);
7411
+ if (!entry2) {
7412
+ entry2 = { id, kind: "tool" };
7413
+ record.activity.push(entry2);
7414
+ }
7415
+ entry2.title = str$1(update.title, 200) ?? entry2.title ?? "Delegated task";
7416
+ if (!["completed", "failed"].includes(entry2.status ?? ""))
7417
+ entry2.status = str$1(update.status, 40) ?? entry2.status;
7418
+ entry2.input = preview(update.rawInput) ?? entry2.input;
7419
+ entry2.output = preview(toolOutput(update)) ?? entry2.output;
7420
+ entry2.locations = Array.isArray(update.locations) ? update.locations.filter((l) => typeof l.path === "string").slice(0, 16).map((l) => ({
7246
7421
  path: l.path.slice(0, 4096),
7247
7422
  line: Number.isSafeInteger(l.line) ? l.line : void 0
7248
- })) : entry.locations;
7423
+ })) : entry2.locations;
7249
7424
  }
7250
7425
  activity(sessionId, update) {
7251
7426
  const id = this.idForSession(sessionId);
@@ -7264,24 +7439,24 @@ class SubagentRegistry {
7264
7439
  "user_message_chunk"
7265
7440
  ].includes(update.sessionUpdate)) {
7266
7441
  const role = update.sessionUpdate === "agent_thought_chunk" ? "thought" : update.sessionUpdate === "user_message_chunk" ? "user" : "agent";
7267
- const content = str(update.content?.text ?? update.content?.resource?.text, 16384) ?? "";
7442
+ const content = str$1(update.content?.text ?? update.content?.resource?.text, 16384) ?? "";
7268
7443
  const previous = record.activity.at(-1);
7269
- let entry = identity(update.messageId) ? record.activity.find(
7444
+ let entry2 = identity(update.messageId) ? record.activity.find(
7270
7445
  (a) => a.kind === "message" && a.id === `message:provider:${update.messageId}`
7271
7446
  ) : previous?.kind === "message" && previous.role === role ? previous : void 0;
7272
- if (!entry) {
7273
- entry = {
7447
+ if (!entry2) {
7448
+ entry2 = {
7274
7449
  id: identity(update.messageId) ? `message:provider:${update.messageId}` : `message:local:${++this.serial}`,
7275
7450
  kind: "message",
7276
7451
  role,
7277
7452
  text: ""
7278
7453
  };
7279
- record.activity.push(entry);
7454
+ record.activity.push(entry2);
7280
7455
  }
7281
- if (entry.text.length + content.length > 16384) record.truncated = true;
7282
- entry.text = (entry.text + content).slice(-16384);
7456
+ if (entry2.text.length + content.length > 16384) record.truncated = true;
7457
+ entry2.text = (entry2.text + content).slice(-16384);
7283
7458
  if (role === "agent") {
7284
- record.result = entry.text;
7459
+ record.result = entry2.text;
7285
7460
  phase = "responding";
7286
7461
  } else if (role === "thought") phase = "thinking";
7287
7462
  } else if (["tool_call", "tool_call_update"].includes(update.sessionUpdate) && identity(update.toolCallId)) {
@@ -7319,12 +7494,12 @@ class SubagentRegistry {
7319
7494
  const id = this.idForSession(sessionId), record = id && this.records.get(id);
7320
7495
  if (!record) return;
7321
7496
  const entryId = `terminal:${terminalId}`;
7322
- const entry = record.activity.find((a) => a.id === entryId) ?? {
7497
+ const entry2 = record.activity.find((a) => a.id === entryId) ?? {
7323
7498
  id: entryId,
7324
7499
  kind: "terminal"
7325
7500
  };
7326
- if (!record.activity.includes(entry)) record.activity.push(entry);
7327
- Object.assign(entry, {
7501
+ if (!record.activity.includes(entry2)) record.activity.push(entry2);
7502
+ Object.assign(entry2, {
7328
7503
  title: title2.slice(0, 200),
7329
7504
  text: output.slice(-8192),
7330
7505
  status: ended ? "completed" : "in_progress"
@@ -7350,7 +7525,7 @@ class SubagentRegistry {
7350
7525
  let cached;
7351
7526
  function runtimeVersion() {
7352
7527
  if (cached) return cached;
7353
- return cached = "0.7.1";
7528
+ return cached = "0.9.0";
7354
7529
  }
7355
7530
  const MAX_REQUEST_BYTES = 1024 * 1024;
7356
7531
  const MAX_CONCURRENT_REQUESTS = 16;
@@ -7475,7 +7650,7 @@ function validateArguments(name, args) {
7475
7650
  if (args.line !== void 0 && (!Number.isSafeInteger(args.line) || args.line < 1)) return "line must be a positive integer";
7476
7651
  if (args.limit !== void 0 && (!Number.isSafeInteger(args.limit) || args.limit < 1)) return "limit must be a positive integer";
7477
7652
  if (name === "oxbit_propose_edit" && typeof args.content !== "string") return "content must be a string";
7478
- if (name === "oxbit_update_plan" && (!Array.isArray(args.entries) || args.entries.length > 50 || args.entries.some((entry) => !object(entry) || typeof entry.content !== "string" || !["pending", "in_progress", "completed"].includes(String(entry.status)) || entry.priority !== void 0 && typeof entry.priority !== "string"))) return "entries must contain plan content and status";
7653
+ if (name === "oxbit_update_plan" && (!Array.isArray(args.entries) || args.entries.length > 50 || args.entries.some((entry2) => !object(entry2) || typeof entry2.content !== "string" || !["pending", "in_progress", "completed"].includes(String(entry2.status)) || entry2.priority !== void 0 && typeof entry2.priority !== "string"))) return "entries must contain plan content and status";
7479
7654
  return void 0;
7480
7655
  }
7481
7656
  function reply(response, status2, body) {
@@ -7487,8 +7662,8 @@ function reply(response, status2, body) {
7487
7662
  response.setHeader("content-type", "application/json; charset=utf-8");
7488
7663
  response.end(JSON.stringify(body));
7489
7664
  }
7490
- function rpcError(id, code, message) {
7491
- return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
7665
+ function rpcError(id, code, message2) {
7666
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message: message2 } };
7492
7667
  }
7493
7668
  class AgentMCP {
7494
7669
  constructor(handler) {
@@ -7573,25 +7748,25 @@ class AgentMCP {
7573
7748
  if (length > MAX_REQUEST_BYTES) return reply(response, 413);
7574
7749
  chunks.push(chunk);
7575
7750
  }
7576
- let message;
7751
+ let message2;
7577
7752
  try {
7578
- message = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7753
+ message2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7579
7754
  } catch {
7580
7755
  return reply(response, 400, rpcError(null, -32700, "Invalid JSON"));
7581
7756
  }
7582
- if (!object(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string" || message.id !== void 0 && typeof message.id !== "string" && typeof message.id !== "number") return reply(response, 400, rpcError(object(message) ? message.id : null, -32600, "Invalid request"));
7757
+ if (!object(message2) || message2.jsonrpc !== "2.0" || typeof message2.method !== "string" || message2.id !== void 0 && typeof message2.id !== "string" && typeof message2.id !== "number") return reply(response, 400, rpcError(object(message2) ? message2.id : null, -32600, "Invalid request"));
7583
7758
  const version = request.headers["mcp-protocol-version"];
7584
- if (version && !PROTOCOL_VERSIONS.includes(version)) return reply(response, 400, rpcError(message.id, -32600, "Unsupported MCP protocol version"));
7585
- if (message.id === void 0) return reply(response, 202);
7586
- const id = message.id;
7587
- if (message.method === "initialize") {
7588
- const requested = object(message.params) && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0;
7759
+ if (version && !PROTOCOL_VERSIONS.includes(version)) return reply(response, 400, rpcError(message2.id, -32600, "Unsupported MCP protocol version"));
7760
+ if (message2.id === void 0) return reply(response, 202);
7761
+ const id = message2.id;
7762
+ if (message2.method === "initialize") {
7763
+ const requested = object(message2.params) && typeof message2.params.protocolVersion === "string" ? message2.params.protocolVersion : void 0;
7589
7764
  return reply(response, 200, { jsonrpc: "2.0", id, result: { protocolVersion: requested && PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0], capabilities: { tools: { listChanged: false } }, serverInfo: { name: "oxbit-workspace", version: runtimeVersion() } } });
7590
7765
  }
7591
- if (message.method === "ping") return reply(response, 200, { jsonrpc: "2.0", id, result: {} });
7592
- if (message.method === "tools/list") return reply(response, 200, { jsonrpc: "2.0", id, result: { tools: NATIVE_AGENT_TOOLS } });
7593
- if (message.method !== "tools/call") return reply(response, 200, rpcError(id, -32601, "Method not found"));
7594
- const params = message.params;
7766
+ if (message2.method === "ping") return reply(response, 200, { jsonrpc: "2.0", id, result: {} });
7767
+ if (message2.method === "tools/list") return reply(response, 200, { jsonrpc: "2.0", id, result: { tools: NATIVE_AGENT_TOOLS } });
7768
+ if (message2.method !== "tools/call") return reply(response, 200, rpcError(id, -32601, "Method not found"));
7769
+ const params = message2.params;
7595
7770
  if (!object(params) || typeof params.name !== "string" || !object(params.arguments ?? {})) return reply(response, 200, rpcError(id, -32602, "Invalid tool call"));
7596
7771
  const args = params.arguments ?? {};
7597
7772
  const invalid2 = validateArguments(params.name, args);
@@ -7612,7 +7787,332 @@ class AgentMCP {
7612
7787
  }
7613
7788
  }
7614
7789
  }
7790
+ const run$1 = promisify(execFile);
7791
+ const TTL = 3e5, MiB = 1024 * 1024, MAX_LISTING = 2 * MiB, MAX_ARCHIVE = 512 * MiB;
7792
+ const REGISTRY_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
7793
+ const VERSION = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
7794
+ const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
7795
+ const TARGETS = ["darwin", "linux", "windows"].flatMap((os2) => [`${os2}-aarch64`, `${os2}-x86_64`]);
7796
+ const ARCHIVES = [".tar.gz", ".tgz", ".tar.bz2", ".tar.xz", ".zip"];
7797
+ const invalid = () => {
7798
+ throw new Error("invalid");
7799
+ };
7800
+ const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7801
+ const str = (value, max) => typeof value === "string" && value && value.length <= max && !value.includes("\0") ? value : invalid();
7802
+ const optional = (value, max) => value === void 0 ? void 0 : str(value, max);
7803
+ const list = (value) => value === void 0 ? [] : Array.isArray(value) && value.length <= 128 && value.every((v) => typeof v === "string" && v.length <= 16384 && !v.includes("\0")) ? value : invalid();
7804
+ function env(value) {
7805
+ if (value === void 0) return {};
7806
+ if (!plain(value)) return invalid();
7807
+ const entries = Object.entries(value);
7808
+ if (entries.length > 64 || entries.some(([k, v]) => !ENV_KEY.test(k) || typeof v !== "string" || v.length > 16384 || v.includes("\0"))) return invalid();
7809
+ return Object.fromEntries(entries);
7810
+ }
7811
+ function url(value) {
7812
+ const parsed = new URL(str(value, 4096));
7813
+ return ["https:", "http:"].includes(parsed.protocol) ? parsed.href : invalid();
7814
+ }
7815
+ function pkg(value) {
7816
+ if (value === void 0) return;
7817
+ if (!plain(value)) return invalid();
7818
+ const name = str(value.package, 512);
7819
+ if (name.startsWith("-")) return invalid();
7820
+ return { package: name, args: list(value.args), env: env(value.env) };
7821
+ }
7822
+ function binary(value) {
7823
+ if (value === void 0) return;
7824
+ if (!plain(value)) return invalid();
7825
+ const targets = {};
7826
+ for (const [key, t] of Object.entries(value)) {
7827
+ if (!plain(t)) return invalid();
7828
+ const sha256 = t.sha256 === void 0 ? void 0 : typeof t.sha256 === "string" && /^[a-f0-9]{64}$/i.test(t.sha256) ? t.sha256.toLowerCase() : invalid();
7829
+ const target = { archive: url(t.archive), cmd: str(t.cmd, 1024), args: list(t.args), env: env(t.env), sha256 };
7830
+ if (TARGETS.includes(key)) targets[key] = target;
7831
+ }
7832
+ return targets;
7833
+ }
7834
+ function entry(value) {
7835
+ if (!plain(value) || !plain(value.distribution)) return invalid();
7836
+ const id = str(value.id, 64), version = str(value.version, 64);
7837
+ if (!REGISTRY_ID.test(id) || !VERSION.test(version)) return invalid();
7838
+ if (typeof value.description !== "string" || value.description.length > 4e3) return invalid();
7839
+ const d = value.distribution;
7840
+ const distribution = { npx: pkg(d.npx), uvx: pkg(d.uvx), binary: binary(d.binary) };
7841
+ if (!distribution.npx && !distribution.uvx && !distribution.binary) return invalid();
7842
+ return {
7843
+ id,
7844
+ version,
7845
+ name: str(value.name, 200),
7846
+ description: value.description,
7847
+ repository: optional(value.repository, 2048),
7848
+ website: optional(value.website, 2048),
7849
+ license: optional(value.license, 200),
7850
+ distribution
7851
+ };
7852
+ }
7853
+ function parseRegistry(text2) {
7854
+ const value = JSON.parse(text2);
7855
+ if (!plain(value) || !Array.isArray(value.agents)) throw new Error("The ACP Registry response has no agents list");
7856
+ const agents = /* @__PURE__ */ new Map();
7857
+ for (const item of value.agents.slice(0, 2e3)) {
7858
+ try {
7859
+ const agent = entry(item);
7860
+ if (!agents.has(agent.id)) agents.set(agent.id, agent);
7861
+ } catch {
7862
+ }
7863
+ }
7864
+ return [...agents.values()];
7865
+ }
7866
+ function platformTarget(platform = process.platform, arch = process.arch) {
7867
+ const os2 = { darwin: "darwin", linux: "linux", win32: "windows" }[platform];
7868
+ const cpu = { arm64: "aarch64", x64: "x86_64" }[arch];
7869
+ return os2 && cpu ? `${os2}-${cpu}` : void 0;
7870
+ }
7871
+ function pickDistribution(agent, target = platformTarget()) {
7872
+ const { npx, uvx, binary: binary2 } = agent.distribution;
7873
+ if (npx) return { kind: "npx", spec: npx };
7874
+ if (uvx) return { kind: "uvx", spec: uvx };
7875
+ const spec = target ? binary2?.[target] : void 0;
7876
+ if (spec) return { kind: "binary", spec };
7877
+ }
7878
+ async function* capped(body, max, what) {
7879
+ let length = 0;
7880
+ for await (const chunk of body) {
7881
+ length += chunk.length;
7882
+ if (length > max) throw new Error(`${what} exceeds ${max / MiB} MiB`);
7883
+ yield chunk;
7884
+ }
7885
+ }
7886
+ const exists = (file) => fs.stat(file).then(() => true, () => false);
7887
+ const message = (error) => error instanceof Error ? error.message : String(error);
7888
+ async function installedCommand(dir, cmd) {
7889
+ const root = await fs.realpath(dir);
7890
+ const inside = (file) => file.startsWith(root + path.sep);
7891
+ const escape = new RpcError("INVALID_PARAMS", `The agent command ${cmd} resolves outside its install directory`);
7892
+ const target = path.resolve(root, cmd);
7893
+ if (!inside(target)) throw escape;
7894
+ const real = await fs.realpath(target).catch(() => {
7895
+ throw new RpcError("INVALID_PARAMS", `The agent archive has no ${cmd}`);
7896
+ });
7897
+ if (!inside(real)) throw escape;
7898
+ const stat = await fs.stat(real);
7899
+ if (!stat.isFile()) throw new RpcError("INVALID_PARAMS", `The agent command ${cmd} is not a file`);
7900
+ if (process.platform !== "win32") await fs.chmod(real, stat.mode | 73);
7901
+ return real;
7902
+ }
7903
+ function untilAborted(promise, signal) {
7904
+ if (!signal) return promise;
7905
+ if (signal.aborted) return Promise.reject(new Error("Agent connection cancelled"));
7906
+ return new Promise((resolve, reject) => {
7907
+ const stop2 = () => reject(new Error("Agent connection cancelled"));
7908
+ signal.addEventListener("abort", stop2, { once: true });
7909
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", stop2));
7910
+ });
7911
+ }
7912
+ async function extract(archive, dest, format, signal) {
7913
+ const tar = ["tar", ["-xf", archive, "-C", dest]];
7914
+ const attempts = format !== ".zip" ? [tar] : [...process.platform === "darwin" ? [tar] : [], ["unzip", ["-q", archive, "-d", dest]]];
7915
+ const missing2 = [];
7916
+ let failure2;
7917
+ for (const [tool, args] of attempts) {
7918
+ await fs.rm(dest, { recursive: true, force: true });
7919
+ await fs.mkdir(dest);
7920
+ try {
7921
+ await run$1(tool, args, { signal, maxBuffer: MiB });
7922
+ return;
7923
+ } catch (error) {
7924
+ if (signal?.aborted) throw new Error("Agent connection cancelled");
7925
+ if (error.code === "ENOENT") missing2.push(tool);
7926
+ else failure2 = new Error(`${tool} could not extract the agent archive: ${String(error.stderr || message(error)).trim().slice(0, 500)}`);
7927
+ }
7928
+ }
7929
+ throw failure2 ?? new RpcError("INVALID_PARAMS", `Install ${missing2.join(" or ")} to extract this agent archive`);
7930
+ }
7931
+ class ACPRegistry {
7932
+ constructor(dataDir, url2 = ACP_REGISTRY_URL, download = fetch, target = platformTarget()) {
7933
+ this.dataDir = dataDir;
7934
+ this.url = url2;
7935
+ this.download = download;
7936
+ this.target = target;
7937
+ this.installRoot = path.join(dataDir, "acp-agents");
7938
+ }
7939
+ dataDir;
7940
+ url;
7941
+ download;
7942
+ target;
7943
+ fetching;
7944
+ installs = /* @__PURE__ */ new Map();
7945
+ installRoot;
7946
+ get cacheFile() {
7947
+ return path.join(this.dataDir, "acp-registry.json");
7948
+ }
7949
+ checkUrl(value) {
7950
+ const parsed = new URL(value), registry = new URL(this.url);
7951
+ const loopback = (u) => u.protocol === "http:" && u.hostname === "127.0.0.1";
7952
+ if (parsed.protocol === "https:" || loopback(registry) && loopback(parsed)) return parsed.href;
7953
+ throw new Error(`ACP agent downloads require HTTPS (${parsed.origin})`);
7954
+ }
7955
+ async open(source2, signal, max, what) {
7956
+ let current = this.checkUrl(source2);
7957
+ for (let redirects = 0; ; redirects++) {
7958
+ const response = await this.download(current, { signal, redirect: "manual" });
7959
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
7960
+ await response.body?.cancel();
7961
+ const location = response.headers.get("location");
7962
+ if (!location) throw new Error(`${what} redirect has no location`);
7963
+ if (redirects >= 5) throw new Error(`${what} has too many redirects`);
7964
+ current = this.checkUrl(new URL(location, current).href);
7965
+ continue;
7966
+ }
7967
+ if (!response.ok || !response.body) {
7968
+ await response.body?.cancel();
7969
+ throw new Error(`${what} download failed (HTTP ${response.status})`);
7970
+ }
7971
+ if (Number(response.headers.get("content-length")) > max) {
7972
+ await response.body.cancel();
7973
+ throw new Error(`${what} exceeds ${max / MiB} MiB`);
7974
+ }
7975
+ return capped(response.body, max, what);
7976
+ }
7977
+ }
7978
+ async cached() {
7979
+ try {
7980
+ const value = JSON.parse(await fs.readFile(this.cacheFile, "utf8"));
7981
+ if (value?.url === this.url && typeof value.text === "string" && !Number.isNaN(Date.parse(value.fetchedAt)))
7982
+ return { agents: parseRegistry(value.text), fetchedAt: value.fetchedAt };
7983
+ } catch {
7984
+ }
7985
+ }
7986
+ async fetchRegistry() {
7987
+ const chunks = [];
7988
+ for await (const chunk of await this.open(this.url, AbortSignal.timeout(15e3), MAX_LISTING, "The ACP Registry")) chunks.push(chunk);
7989
+ const text2 = Buffer.concat(chunks).toString("utf8");
7990
+ const agents = parseRegistry(text2), fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
7991
+ await fs.mkdir(this.dataDir, { recursive: true, mode: 448 });
7992
+ const temp = `${this.cacheFile}.${process.pid}.tmp`;
7993
+ await fs.writeFile(temp, JSON.stringify({ url: this.url, fetchedAt, text: text2 }), { mode: 384 });
7994
+ await fs.rename(temp, this.cacheFile);
7995
+ return { agents, fetchedAt };
7996
+ }
7997
+ async load(refresh = false) {
7998
+ const cached2 = await this.cached();
7999
+ const age = cached2 ? Date.now() - Date.parse(cached2.fetchedAt) : Infinity;
8000
+ if (cached2 && !refresh && age >= 0 && age < TTL) return cached2;
8001
+ try {
8002
+ this.fetching ??= this.fetchRegistry().finally(() => this.fetching = void 0);
8003
+ return await this.fetching;
8004
+ } catch (error) {
8005
+ const reason = `Could not reach the ACP Registry: ${message(error)}`;
8006
+ if (cached2) return { ...cached2, error: reason };
8007
+ throw new RpcError("UNAVAILABLE", reason);
8008
+ }
8009
+ }
8010
+ dir(agent) {
8011
+ return path.join(this.installRoot, agent.id, agent.version);
8012
+ }
8013
+ unavailable(picked) {
8014
+ if (!picked) return "No build for this runtime's platform";
8015
+ if (picked.kind !== "binary") return void 0;
8016
+ if (!picked.spec.sha256) return "The registry publishes no checksum for this build";
8017
+ const file = new URL(picked.spec.archive).pathname.toLowerCase();
8018
+ if (!ARCHIVES.some((ext) => file.endsWith(ext))) return "Oxbit cannot extract this build's archive format";
8019
+ }
8020
+ async listing(refresh = false) {
8021
+ const { agents, fetchedAt, error } = await this.load(refresh);
8022
+ const listed = await Promise.all(agents.map(async (agent) => {
8023
+ const picked = pickDistribution(agent, this.target), builtin = acpBuiltinForRegistry(agent.id)?.id;
8024
+ const { npx, uvx } = agent.distribution;
8025
+ const distribution = picked?.kind ?? (npx ? "npx" : uvx ? "uvx" : "binary");
8026
+ const reason = this.unavailable(picked);
8027
+ return {
8028
+ id: agent.id,
8029
+ name: agent.name,
8030
+ version: agent.version,
8031
+ description: agent.description,
8032
+ ...agent.repository ? { repository: agent.repository } : {},
8033
+ ...agent.website ? { website: agent.website } : {},
8034
+ ...agent.license ? { license: agent.license } : {},
8035
+ distribution,
8036
+ available: !reason,
8037
+ ...reason ? { reason } : {},
8038
+ ...picked?.kind === "binary" ? { installed: await exists(this.dir(agent)) } : {},
8039
+ ...builtin ? { builtin } : {}
8040
+ };
8041
+ }));
8042
+ return { agents: listed, fetchedAt, ...error ? { error } : {} };
8043
+ }
8044
+ async resolve(id, version, signal) {
8045
+ const agent = (await this.load()).agents.find((a) => a.id === id);
8046
+ if (!agent) throw new RpcError("INVALID_PARAMS", "Unknown agent");
8047
+ if (version !== void 0 && version !== agent.version)
8048
+ throw new RpcError("INVALID_PARAMS", `${agent.name} ${version} is no longer in the ACP Registry; refresh the agent list`);
8049
+ const picked = pickDistribution(agent, this.target);
8050
+ if (!picked) throw new RpcError("INVALID_PARAMS", `${agent.name} has no ACP Registry build for ${this.target ?? `${process.platform}-${process.arch}`}`);
8051
+ const base = { name: agent.name, env: picked.spec.env };
8052
+ if (picked.kind !== "binary") {
8053
+ const { package: name, args } = picked.spec;
8054
+ return { ...base, command: picked.kind, args: picked.kind === "npx" ? ["-y", name, ...args] : [name, ...args] };
8055
+ }
8056
+ return { ...base, command: await this.install(agent, picked.spec, signal), args: picked.spec.args };
8057
+ }
8058
+ /** Concurrent callers share one download, which stops only after every caller has cancelled. */
8059
+ async install(agent, spec, signal) {
8060
+ const key = `${agent.id}@${agent.version}`;
8061
+ let shared = this.installs.get(key);
8062
+ if (!shared) {
8063
+ const abort = new AbortController();
8064
+ const promise = this.installOnce(agent, spec, abort.signal).finally(() => this.installs.delete(key));
8065
+ promise.catch(() => {
8066
+ });
8067
+ shared = { promise, abort, waiters: 0 };
8068
+ this.installs.set(key, shared);
8069
+ }
8070
+ const install = shared;
8071
+ install.waiters++;
8072
+ try {
8073
+ return await untilAborted(install.promise, signal);
8074
+ } finally {
8075
+ if (--install.waiters === 0 && signal?.aborted) install.abort.abort();
8076
+ }
8077
+ }
8078
+ async installOnce(agent, spec, signal) {
8079
+ const dir = this.dir(agent);
8080
+ if (await exists(dir)) return installedCommand(dir, spec.cmd);
8081
+ if (!spec.sha256) throw new RpcError("INVALID_PARAMS", `${agent.name} has no sha256 checksum in the ACP Registry, so Oxbit does not install it`);
8082
+ const file = new URL(spec.archive).pathname.toLowerCase();
8083
+ const format = ARCHIVES.find((ext) => file.endsWith(ext));
8084
+ if (!format) throw new RpcError("INVALID_PARAMS", `Oxbit cannot extract ${path.posix.basename(file)}; supported archives are ${ARCHIVES.join(", ")}`);
8085
+ await fs.mkdir(path.dirname(dir), { recursive: true, mode: 448 });
8086
+ const temp = await fs.mkdtemp(path.join(path.dirname(dir), `.${agent.version}-`));
8087
+ try {
8088
+ const archive = path.join(temp, `archive${format}`), files = path.join(temp, "files");
8089
+ const hash2 = createHash("sha256");
8090
+ const body = await this.open(spec.archive, signal, MAX_ARCHIVE, `The ${agent.name} archive`);
8091
+ await pipeline(async function* () {
8092
+ for await (const chunk of body) {
8093
+ hash2.update(chunk);
8094
+ yield chunk;
8095
+ }
8096
+ }, createWriteStream(archive, { mode: 384 }), { signal });
8097
+ if (hash2.digest("hex") !== spec.sha256)
8098
+ throw new RpcError("INVALID_PARAMS", `The ${agent.name} archive does not match its sha256 checksum`);
8099
+ await extract(archive, files, format, signal);
8100
+ await installedCommand(files, spec.cmd);
8101
+ await fs.rename(files, dir).catch(async (error) => {
8102
+ if (!await exists(dir)) throw error;
8103
+ });
8104
+ return await installedCommand(dir, spec.cmd);
8105
+ } finally {
8106
+ await fs.rm(temp, { recursive: true, force: true });
8107
+ }
8108
+ }
8109
+ }
7615
8110
  const MAX_BYTES = 1024 * 1024;
8111
+ const IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
8112
+ const MAX_IMAGES = 4;
8113
+ const MAX_IMAGE_DATA = Math.ceil(5 * 1024 * 1024 / 3) * 4;
8114
+ const MAX_QUEUED_IMAGE_DATA = 16 * 1024 * 1024;
8115
+ const withoutImageData = (key, value) => key === "data" ? void 0 : value;
7616
8116
  const EDITOR_UNSUPPORTED = "The connected editor does not support this tool; update Oxbit";
7617
8117
  function strings(value) {
7618
8118
  if (!Array.isArray(value) || value.length > 128 || value.some(
@@ -7626,14 +8126,19 @@ function strings(value) {
7626
8126
  }
7627
8127
  class AgentACP {
7628
8128
  constructor(files, emit, stateChanged = () => {
8129
+ }, acpRegistry, statusChanged = () => {
7629
8130
  }) {
7630
8131
  this.files = files;
7631
8132
  this.emit = emit;
7632
8133
  this.stateChanged = stateChanged;
8134
+ this.acpRegistry = acpRegistry;
8135
+ this.statusChanged = statusChanged;
7633
8136
  }
7634
8137
  files;
7635
8138
  emit;
7636
8139
  stateChanged;
8140
+ acpRegistry;
8141
+ statusChanged;
7637
8142
  agents = /* @__PURE__ */ new Map();
7638
8143
  mcp = new AgentMCP((id, name, args) => this.tool(id, name, args));
7639
8144
  get(owner, id) {
@@ -7642,8 +8147,8 @@ class AgentACP {
7642
8147
  throw new RpcError("NOT_FOUND", "Agent connection has ended");
7643
8148
  return agent;
7644
8149
  }
7645
- send(agent, message) {
7646
- const data = JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n";
8150
+ send(agent, message2) {
8151
+ const data = JSON.stringify({ jsonrpc: "2.0", ...message2 }) + "\n";
7647
8152
  if (agent.stopped || !agent.child.stdin.writable)
7648
8153
  throw new Error("Agent process has ended");
7649
8154
  if (Buffer.byteLength(data) > 2 * MAX_BYTES || agent.child.stdin.writableLength > MAX_BYTES)
@@ -7672,9 +8177,9 @@ class AgentACP {
7672
8177
  publish(agent, event, params) {
7673
8178
  const payload = { id: agent.connection.id, ...params, seq: ++agent.eventSequence };
7674
8179
  if (["acp.update", "acp.subagent", "acp.terminal", "acp.turnStarted", "acp.turnEnded"].includes(event)) {
7675
- const entry = JSON.parse(JSON.stringify({ name: event, params: payload }));
7676
- agent.events.push(entry);
7677
- agent.eventBytes += Buffer.byteLength(JSON.stringify(entry));
8180
+ const entry2 = JSON.parse(JSON.stringify({ name: event, params: payload }));
8181
+ agent.events.push(entry2);
8182
+ agent.eventBytes += Buffer.byteLength(JSON.stringify(entry2));
7678
8183
  while (agent.events.length > 2e3 || agent.eventBytes > 2 * MAX_BYTES) {
7679
8184
  agent.eventBytes -= Buffer.byteLength(JSON.stringify(agent.events.shift()));
7680
8185
  agent.truncated = true;
@@ -7683,6 +8188,20 @@ class AgentACP {
7683
8188
  if (agent.client) this.emit(agent.client, event, payload);
7684
8189
  this.stateChanged();
7685
8190
  }
8191
+ report(agent, status2) {
8192
+ try {
8193
+ this.statusChanged({
8194
+ owner: agent.owner,
8195
+ agent: agent.connection.name ?? agent.connection.provider,
8196
+ status: status2,
8197
+ pending: agent.approvals.size,
8198
+ startedAt: agent.startedAt
8199
+ });
8200
+ } catch (error) {
8201
+ process.stderr.write(`Oxbit agent status listener failed: ${String(error)}
8202
+ `);
8203
+ }
8204
+ }
7686
8205
  list(owner) {
7687
8206
  return { sessions: [...this.agents.values()].filter((a) => a.owner === owner && !a.stopped).map((a) => ({
7688
8207
  connection: a.connection,
@@ -7742,27 +8261,15 @@ class AgentACP {
7742
8261
  return [...this.agents.values()].some((a) => this.working(a));
7743
8262
  }
7744
8263
  async start(owner, launch, signal, client = owner) {
7745
- const preset2 = ACP_PROVIDERS.find((p) => p.id === launch.provider);
7746
- if (!preset2)
7747
- throw new RpcError("INVALID_PARAMS", "Choose Codex, Cursor, or Amp");
7748
8264
  if (signal?.aborted) throw new Error("Agent connection cancelled");
7749
- const owned = [...this.agents.values()].filter((a) => a.owner === owner);
7750
- if (owned.length >= 3) {
7751
- const idle = owned.find((a) => !a.client && !this.working(a));
7752
- if (!idle)
7753
- throw new RpcError("BUSY", "3 agents are running for this device; stop one in Runtime sessions");
7754
- this.stopAgent(idle, "Stopped to start another agent");
7755
- }
7756
- const command = requireString(
7757
- { command: launch.command ?? preset2.command },
7758
- "command"
7759
- );
7760
- if (!command.trim() || command.includes("\0"))
7761
- throw new RpcError("INVALID_PARAMS", "Invalid agent executable");
7762
- const args = strings(launch.args ?? [...preset2.args]);
8265
+ this.slot(owner, false);
8266
+ const resolved = await this.resolveLaunch(launch, signal);
8267
+ if (signal?.aborted) throw new Error("Agent connection cancelled");
8268
+ this.slot(owner, true);
8269
+ const { command, args } = resolved;
7763
8270
  const child = spawn(command, args, {
7764
8271
  cwd: this.files.root,
7765
- env: process.env,
8272
+ env: resolved.env && Object.keys(resolved.env).length ? { ...process.env, ...resolved.env } : process.env,
7766
8273
  stdio: "pipe",
7767
8274
  detached: process.platform !== "win32",
7768
8275
  windowsHide: true
@@ -7775,7 +8282,8 @@ class AgentACP {
7775
8282
  child,
7776
8283
  connection: {
7777
8284
  id: randomUUID(),
7778
- provider: preset2.id,
8285
+ provider: resolved.provider,
8286
+ name: resolved.name,
7779
8287
  root: this.files.root,
7780
8288
  authMethods: []
7781
8289
  },
@@ -7829,10 +8337,10 @@ class AgentACP {
7829
8337
  return;
7830
8338
  }
7831
8339
  try {
7832
- const message = JSON.parse(line);
7833
- if (message?.jsonrpc !== "2.0")
8340
+ const message2 = JSON.parse(line);
8341
+ if (message2?.jsonrpc !== "2.0")
7834
8342
  throw new Error("Invalid ACP envelope");
7835
- void this.message(agent, message).catch(
8343
+ void this.message(agent, message2).catch(
7836
8344
  (error) => this.stopAgent(agent, String(error))
7837
8345
  );
7838
8346
  } catch {
@@ -7852,7 +8360,7 @@ class AgentACP {
7852
8360
  clientCapabilities: {
7853
8361
  fs: { readTextFile: true, writeTextFile: true },
7854
8362
  terminal: true,
7855
- ...preset2.id === "codex" ? { subagents: {} } : {}
8363
+ ...resolved.provider === "codex" ? { subagents: {} } : {}
7856
8364
  },
7857
8365
  clientInfo: { name: "oxbit", title: "Oxbit", version: runtimeVersion() }
7858
8366
  });
@@ -7869,6 +8377,43 @@ class AgentACP {
7869
8377
  signal?.removeEventListener("abort", abort);
7870
8378
  }
7871
8379
  }
8380
+ slot(owner, evict) {
8381
+ const owned = [...this.agents.values()].filter((a) => a.owner === owner);
8382
+ if (owned.length < 3) return;
8383
+ const idle = owned.find((a) => !a.client && !this.working(a));
8384
+ if (!idle)
8385
+ throw new RpcError("BUSY", "3 agents are running for this device; stop one in Runtime sessions");
8386
+ if (evict) this.stopAgent(idle, "Stopped to start another agent");
8387
+ }
8388
+ async resolveLaunch(launch, signal) {
8389
+ const provider = launch?.provider;
8390
+ if (typeof provider !== "string" || !provider || provider.length > 64)
8391
+ throw new RpcError("INVALID_PARAMS", "Unknown agent");
8392
+ if (launch.registry !== void 0) {
8393
+ const { id, version } = launch.registry ?? {};
8394
+ if (typeof id !== "string" || !REGISTRY_ID.test(id) || version !== void 0 && typeof version !== "string")
8395
+ throw new RpcError("INVALID_PARAMS", "Invalid ACP Registry agent");
8396
+ const builtin = acpBuiltinForRegistry(id);
8397
+ if (builtin)
8398
+ return { provider: builtin.id, name: builtin.name, command: builtin.command, args: [...builtin.args] };
8399
+ if (!this.acpRegistry)
8400
+ throw new RpcError("INVALID_PARAMS", "This runtime cannot launch ACP Registry agents");
8401
+ return { provider: id, ...await this.acpRegistry.resolve(id, version, signal) };
8402
+ }
8403
+ const preset2 = acpPreset(provider);
8404
+ if (!preset2 && (typeof launch.command !== "string" || !launch.command.trim()))
8405
+ throw new RpcError("INVALID_PARAMS", provider === ACP_CUSTOM_PROVIDER ? "Enter a command for the custom agent" : "Unknown agent");
8406
+ const command = requireString({ command: launch.command ?? preset2.command }, "command");
8407
+ if (!command.trim() || command.includes("\0"))
8408
+ throw new RpcError("INVALID_PARAMS", "Invalid agent executable");
8409
+ const custom = typeof launch.name === "string" ? launch.name.trim().slice(0, 80) : "";
8410
+ return {
8411
+ provider,
8412
+ name: preset2?.name ?? (custom || "Custom agent"),
8413
+ command,
8414
+ args: strings(launch.args ?? (preset2 ? [...preset2.args] : []))
8415
+ };
8416
+ }
7872
8417
  async call(owner, id, method, params = {}, signal) {
7873
8418
  const agent = this.get(owner, id);
7874
8419
  if (agent.busy)
@@ -7912,11 +8457,16 @@ class AgentACP {
7912
8457
  agent.busy = false;
7913
8458
  }
7914
8459
  }
7915
- if (["session/new", "session/load", "session/resume"].includes(method)) {
8460
+ if (["session/new", "session/load", "session/resume", "session/fork"].includes(method)) {
7916
8461
  if (method === "session/load" && !agent.connection.capabilities?.loadSession)
7917
8462
  throw new Error("This agent does not support loading conversations");
7918
8463
  if (method === "session/resume" && !agent.connection.capabilities?.sessionCapabilities?.resume)
7919
8464
  throw new Error("This agent does not support resuming conversations");
8465
+ if (method === "session/fork" && !agent.connection.capabilities?.sessionCapabilities?.fork)
8466
+ throw new Error("This agent does not support forking conversations");
8467
+ const source2 = agent.connection.sessionId;
8468
+ if (method === "session/fork" && !source2)
8469
+ throw new Error("Start a conversation first");
7920
8470
  if (agent.subagents?.activeCount)
7921
8471
  throw new Error(
7922
8472
  "Wait for active subagents or disconnect before changing conversations"
@@ -7928,7 +8478,7 @@ class AgentACP {
7928
8478
  const previousBytes = agent.eventBytes;
7929
8479
  const previousTitle = agent.title;
7930
8480
  const previousTruncated = agent.truncated;
7931
- const target = method === "session/new" ? void 0 : requireString(params, "sessionId");
8481
+ const target = method === "session/new" || method === "session/fork" ? void 0 : requireString(params, "sessionId");
7932
8482
  if (agent.approvals.size)
7933
8483
  throw new Error(
7934
8484
  "Resolve pending requests before changing conversations"
@@ -7949,7 +8499,7 @@ class AgentACP {
7949
8499
  const result = await this.request(agent, method, {
7950
8500
  cwd: this.files.root,
7951
8501
  mcpServers: [await this.mcp.server(agent.connection.id, !!agent.connection.capabilities?.mcpCapabilities?.http)],
7952
- ...target ? { sessionId: target } : {}
8502
+ ...target ? { sessionId: target } : method === "session/fork" ? { sessionId: source2 } : {}
7953
8503
  });
7954
8504
  const sessionId2 = target ?? result?.sessionId;
7955
8505
  if (typeof sessionId2 !== "string" || !sessionId2)
@@ -8017,41 +8567,22 @@ class AgentACP {
8017
8567
  const text2 = requireString(params, "text", MAX_BYTES);
8018
8568
  if (!text2.trim()) throw new Error("Enter a message");
8019
8569
  const prompt = [{ type: "text", text: text2 }];
8020
- if (params.context !== void 0) {
8021
- if (!Array.isArray(params.context) || params.context.length > 8)
8022
- throw new Error("Attach up to eight context items");
8023
- for (const item of params.context) {
8024
- const content = requireString(item, "text", 2e5);
8025
- const relative = requireString(item, "path");
8026
- const absolute = await this.files.resolve(relative);
8027
- const label = typeof item.label === "string" ? item.label.slice(0, 1024) : relative;
8028
- if (agent.connection.capabilities?.promptCapabilities?.embeddedContext)
8029
- prompt.push({
8030
- type: "resource",
8031
- resource: {
8032
- uri: pathToFileURL(absolute).href + (Number.isSafeInteger(item.line) ? `#L${item.line}` : ""),
8033
- mimeType: "text/plain",
8034
- text: `Editor snapshot: ${label}
8035
- ${content}`
8036
- }
8037
- });
8038
- else
8039
- prompt.push({
8040
- type: "text",
8041
- text: `Attached editor context: ${label}
8042
- ${content}`
8043
- });
8044
- }
8045
- }
8046
- if (Buffer.byteLength(JSON.stringify(prompt)) > MAX_BYTES)
8570
+ if (params.context !== void 0) prompt.push(...await this.contextBlocks(agent, params.context));
8571
+ if (Buffer.byteLength(JSON.stringify(prompt.filter((block) => block.type !== "image"))) > MAX_BYTES)
8047
8572
  throw new Error("The message and attachments are too large");
8048
8573
  if (agent.busy || agent.stopped || agent.connection.sessionId !== sessionId)
8049
8574
  throw new Error("Agent connection is busy or ended");
8050
8575
  const messageId = params.messageId === void 0 ? randomUUID() : requireString(params, "messageId", 256);
8051
8576
  agent.busy = true;
8052
8577
  agent.turn++;
8578
+ agent.startedAt = Date.now();
8053
8579
  if (agent.title === "New conversation") agent.title = text2.slice(0, 100);
8054
- this.publish(agent, "acp.turnStarted", { messageId, text: text2, context: params.context ?? [] });
8580
+ this.publish(agent, "acp.turnStarted", {
8581
+ messageId,
8582
+ text: text2,
8583
+ context: JSON.parse(JSON.stringify(params.context ?? [], withoutImageData))
8584
+ });
8585
+ this.report(agent, "working");
8055
8586
  const abort = () => this.stopAgent(agent, "Agent request interrupted");
8056
8587
  signal?.addEventListener("abort", abort, { once: true });
8057
8588
  let stopReason;
@@ -8076,11 +8607,55 @@ ${content}`
8076
8607
  signal?.removeEventListener("abort", abort);
8077
8608
  if (!agent.stopped) {
8078
8609
  this.publish(agent, "acp.turnEnded", { stopReason, error: failure2 });
8610
+ if (failure2 !== void 0) this.report(agent, "failed");
8611
+ else if (stopReason !== "cancelled") this.report(agent, "finished");
8079
8612
  this.publishQueue(agent);
8080
8613
  queueMicrotask(() => this.runQueue(agent));
8081
8614
  }
8082
8615
  }
8083
8616
  }
8617
+ /** Validates attached context and converts it into ACP prompt content blocks. */
8618
+ async contextBlocks(agent, context) {
8619
+ if (context === void 0) return [];
8620
+ if (!Array.isArray(context) || context.length > 8)
8621
+ throw new Error("Attach up to eight context items");
8622
+ if (context.filter((item) => item?.kind === "image").length > MAX_IMAGES)
8623
+ throw new Error(`Attach up to ${MAX_IMAGES} images per message`);
8624
+ const blocks = [];
8625
+ for (const item of context) {
8626
+ if (item?.kind === "image") {
8627
+ if (!agent.connection.capabilities?.promptCapabilities?.image)
8628
+ throw new RpcError("INVALID_PARAMS", "This agent does not accept images");
8629
+ const mimeType = requireString(item, "mimeType", 64);
8630
+ const data = requireString(item, "data", MAX_IMAGE_DATA);
8631
+ if (!IMAGE_TYPES.includes(mimeType) || !data || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
8632
+ throw new RpcError("INVALID_PARAMS", "Invalid image attachment");
8633
+ blocks.push({ type: "image", mimeType, data });
8634
+ continue;
8635
+ }
8636
+ const content = requireString(item, "text", 2e5);
8637
+ const relative = requireString(item, "path");
8638
+ const absolute = await this.files.resolve(relative);
8639
+ const label = typeof item.label === "string" ? item.label.slice(0, 1024) : relative;
8640
+ if (agent.connection.capabilities?.promptCapabilities?.embeddedContext)
8641
+ blocks.push({
8642
+ type: "resource",
8643
+ resource: {
8644
+ uri: pathToFileURL(absolute).href + (Number.isSafeInteger(item.line) ? `#L${item.line}` : ""),
8645
+ mimeType: "text/plain",
8646
+ text: `Editor snapshot: ${label}
8647
+ ${content}`
8648
+ }
8649
+ });
8650
+ else
8651
+ blocks.push({
8652
+ type: "text",
8653
+ text: `Attached editor context: ${label}
8654
+ ${content}`
8655
+ });
8656
+ }
8657
+ return blocks;
8658
+ }
8084
8659
  publishQueue(agent) {
8085
8660
  this.publish(agent, "acp.queue", { queue: agent.queue, paused: agent.queuePaused });
8086
8661
  return { queue: [...agent.queue], paused: agent.queuePaused };
@@ -8091,18 +8666,13 @@ ${content}`
8091
8666
  const text2 = requireString(params, "text", MAX_BYTES).trim();
8092
8667
  if (!text2) throw new Error("Enter a message");
8093
8668
  const messageId = params.messageId === void 0 ? randomUUID() : requireString(params, "messageId", 256);
8094
- if (params.context !== void 0) {
8095
- if (!Array.isArray(params.context) || params.context.length > 8)
8096
- throw new Error("Attach up to eight context items");
8097
- for (const item of params.context) {
8098
- requireString(item, "text", 2e5);
8099
- await this.files.resolve(requireString(item, "path"));
8100
- }
8101
- }
8669
+ if (params.context !== void 0) await this.contextBlocks(agent, params.context);
8102
8670
  if (agent.stopped) throw new Error("Agent connection has ended");
8103
8671
  if (agent.queue.some((p) => p.id === messageId)) return this.publishQueue(agent);
8104
8672
  const prompt = { id: messageId, text: text2, ...params.context ? { context: structuredClone(params.context) } : {} };
8105
- if (agent.queue.length >= 16 || Buffer.byteLength(JSON.stringify([...agent.queue, prompt])) > MAX_BYTES)
8673
+ const queued = [...agent.queue, prompt];
8674
+ const imageData = queued.flatMap((p) => p.context ?? []).reduce((n, item) => n + (item.data?.length ?? 0), 0);
8675
+ if (agent.queue.length >= 16 || imageData > MAX_QUEUED_IMAGE_DATA || Buffer.byteLength(JSON.stringify(queued, withoutImageData)) > MAX_BYTES)
8106
8676
  throw new Error("Queued messages exceed the limit; remove a message first");
8107
8677
  if (interrupt) {
8108
8678
  agent.queue.unshift(prompt);
@@ -8121,6 +8691,14 @@ ${content}`
8121
8691
  this.publishQueue(agent);
8122
8692
  return { prompt };
8123
8693
  }
8694
+ moveQueued(owner, id, promptId, index) {
8695
+ const agent = this.get(owner, id);
8696
+ const from = agent.queue.findIndex((p) => p.id === promptId);
8697
+ if (from < 0) throw new Error("The queued message has already started or was removed");
8698
+ const [prompt] = agent.queue.splice(from, 1);
8699
+ agent.queue.splice(Math.max(0, Math.min(index, agent.queue.length)), 0, prompt);
8700
+ return this.publishQueue(agent);
8701
+ }
8124
8702
  resumeQueue(owner, id) {
8125
8703
  const agent = this.get(owner, id);
8126
8704
  agent.queuePaused = false;
@@ -8282,6 +8860,7 @@ ${content}`
8282
8860
  if (method !== "fs/read_text_file" && !method.startsWith("oxbit/"))
8283
8861
  registry.waiting(sessionId, requestId, true, params.toolCallId ?? params.toolCall?.toolCallId);
8284
8862
  this.publish(agent, "acp.request", this.requestEvent(agent, requestId, request));
8863
+ if (method === "session/request_permission") this.report(agent, "waiting");
8285
8864
  });
8286
8865
  }
8287
8866
  async tool(id, name, args) {
@@ -8351,10 +8930,10 @@ ${content}`
8351
8930
  break;
8352
8931
  case "oxbit_update_plan": {
8353
8932
  if (!Array.isArray(args.entries) || args.entries.length > 50) throw new Error("Provide up to 50 plan entries");
8354
- const entries = args.entries.map((entry) => {
8355
- const content = requireString(entry, "content", 2e3);
8356
- if (!["pending", "in_progress", "completed"].includes(entry.status)) throw new Error("Invalid plan status");
8357
- return { content, status: entry.status, ...typeof entry.priority === "string" ? { priority: entry.priority.slice(0, 40) } : {} };
8933
+ const entries = args.entries.map((entry2) => {
8934
+ const content = requireString(entry2, "content", 2e3);
8935
+ if (!["pending", "in_progress", "completed"].includes(entry2.status)) throw new Error("Invalid plan status");
8936
+ return { content, status: entry2.status, ...typeof entry2.priority === "string" ? { priority: entry2.priority.slice(0, 40) } : {} };
8358
8937
  });
8359
8938
  publish({ sessionUpdate: "plan", entries });
8360
8939
  result = { entries };
@@ -8370,52 +8949,52 @@ ${content}`
8370
8949
  throw error;
8371
8950
  }
8372
8951
  }
8373
- async message(agent, message) {
8952
+ async message(agent, message2) {
8374
8953
  if (agent.stopped) return;
8375
- if (!message.method) {
8376
- const pending = agent.pending.get(message.id);
8954
+ if (!message2.method) {
8955
+ const pending = agent.pending.get(message2.id);
8377
8956
  if (!pending) return;
8378
8957
  clearTimeout(pending.timer);
8379
- agent.pending.delete(message.id);
8380
- if (message.error)
8958
+ agent.pending.delete(message2.id);
8959
+ if (message2.error)
8381
8960
  pending.reject(
8382
8961
  new RpcError(
8383
- String(message.error.code),
8384
- message.error.message ?? "ACP request failed",
8385
- message.error.data
8962
+ String(message2.error.code),
8963
+ message2.error.message ?? "ACP request failed",
8964
+ message2.error.data
8386
8965
  )
8387
8966
  );
8388
- else pending.resolve(message.result);
8967
+ else pending.resolve(message2.result);
8389
8968
  return;
8390
8969
  }
8391
- const params = message.params ?? {};
8392
- if (message.id === void 0) {
8393
- if (message.method === "session/update" && agent.newSessionUpdates) {
8970
+ const params = message2.params ?? {};
8971
+ if (message2.id === void 0) {
8972
+ if (message2.method === "session/update" && agent.newSessionUpdates) {
8394
8973
  if (agent.newSessionUpdates.length >= 100)
8395
8974
  throw new Error("Too many updates while creating a session");
8396
8975
  agent.newSessionUpdates.push(params);
8397
8976
  return;
8398
8977
  }
8399
- if (message.method === "session/update")
8978
+ if (message2.method === "session/update")
8400
8979
  this.routeUpdate(agent, params.sessionId, params.update);
8401
- else if (message.method.startsWith("cursor/") && agent.connection.provider === "cursor")
8980
+ else if (message2.method.startsWith("cursor/") && agent.connection.provider === "cursor")
8402
8981
  this.routeUpdate(
8403
8982
  agent,
8404
8983
  params.sessionId ?? agent.connection.sessionId,
8405
- { ...params, sessionUpdate: message.method }
8984
+ { ...params, sessionUpdate: message2.method }
8406
8985
  );
8407
8986
  return;
8408
8987
  }
8409
8988
  try {
8410
8989
  const registry = agent.subagents;
8411
- const cursorRequest = message.method.startsWith("cursor/") && agent.connection.provider === "cursor";
8990
+ const cursorRequest = message2.method.startsWith("cursor/") && agent.connection.provider === "cursor";
8412
8991
  const sessionId = params.sessionId ?? (cursorRequest ? agent.connection.sessionId : void 0);
8413
8992
  if (!registry?.acceptsRequest(sessionId))
8414
8993
  throw new Error("Unknown ACP session");
8415
8994
  params.sessionId = sessionId;
8416
- if (message.method.startsWith("terminal/")) {
8417
- const result2 = await this.terminal(agent, message.method, params);
8418
- this.send(agent, { id: message.id, result: result2 });
8995
+ if (message2.method.startsWith("terminal/")) {
8996
+ const result2 = await this.terminal(agent, message2.method, params);
8997
+ this.send(agent, { id: message2.id, result: result2 });
8419
8998
  return;
8420
8999
  }
8421
9000
  if (![
@@ -8424,19 +9003,19 @@ ${content}`
8424
9003
  "session/request_permission",
8425
9004
  "cursor/ask_question",
8426
9005
  "cursor/create_plan"
8427
- ].includes(message.method)) {
9006
+ ].includes(message2.method)) {
8428
9007
  this.send(agent, {
8429
- id: message.id,
9008
+ id: message2.id,
8430
9009
  error: { code: -32601, message: "Client method is not supported" }
8431
9010
  });
8432
9011
  return;
8433
9012
  }
8434
- const result = await this.clientRequest(agent, message.method, params);
8435
- if (!agent.stopped) this.send(agent, { id: message.id, result });
9013
+ const result = await this.clientRequest(agent, message2.method, params);
9014
+ if (!agent.stopped) this.send(agent, { id: message2.id, result });
8436
9015
  } catch (error) {
8437
9016
  if (!agent.stopped)
8438
9017
  this.send(agent, {
8439
- id: message.id,
9018
+ id: message2.id,
8440
9019
  error: { code: -32603, message: String(error) }
8441
9020
  });
8442
9021
  }
@@ -8472,15 +9051,15 @@ ${content}`
8472
9051
  const command = requireString(params, "command");
8473
9052
  if (!command || command.includes("\0"))
8474
9053
  throw new Error("Invalid terminal command");
8475
- const env = { ...process.env };
9054
+ const env2 = { ...process.env };
8476
9055
  if (params.env !== void 0) {
8477
9056
  if (!Array.isArray(params.env) || params.env.length > 128)
8478
9057
  throw new Error("Invalid terminal environment");
8479
- for (const entry of params.env) {
8480
- const name = requireString(entry, "name"), value = requireString(entry, "value", 16384);
9058
+ for (const entry2 of params.env) {
9059
+ const name = requireString(entry2, "name"), value = requireString(entry2, "value", 16384);
8481
9060
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) || value.includes("\0"))
8482
9061
  throw new Error("Invalid environment variable");
8483
- env[name] = value;
9062
+ env2[name] = value;
8484
9063
  }
8485
9064
  }
8486
9065
  const limit2 = params.outputByteLimit ?? MAX_BYTES;
@@ -8492,7 +9071,7 @@ ${content}`
8492
9071
  throw new Error("Agent connection has ended");
8493
9072
  const child = spawn(command, strings(params.args ?? []), {
8494
9073
  cwd,
8495
- env,
9074
+ env: env2,
8496
9075
  stdio: "pipe",
8497
9076
  detached: process.platform !== "win32",
8498
9077
  windowsHide: true
@@ -8612,6 +9191,124 @@ ${content}`
8612
9191
  return this.mcp.close();
8613
9192
  }
8614
9193
  }
9194
+ const CHECKPOINT_TIMEOUT = 5e3;
9195
+ const TREE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
9196
+ const GITLINK = "160000";
9197
+ const BATCH = 500;
9198
+ const unavailable = (reason) => ({ unavailable: reason });
9199
+ class ACPCheckpoints {
9200
+ constructor(root, executable = "git") {
9201
+ this.root = root;
9202
+ this.executable = executable;
9203
+ }
9204
+ root;
9205
+ executable;
9206
+ queue = Promise.resolve();
9207
+ async run(args, signal, index, acceptFailure = false) {
9208
+ const result = await runCommand(this.executable, ["--literal-pathspecs", "-c", "color.ui=false", ...args], {
9209
+ cwd: this.root,
9210
+ signal,
9211
+ maxBytes: MAX_MESSAGE_BYTES,
9212
+ env: {
9213
+ ...process.env,
9214
+ GIT_TERMINAL_PROMPT: "0",
9215
+ GIT_OPTIONAL_LOCKS: "0",
9216
+ LC_ALL: "C",
9217
+ ...index ? { GIT_INDEX_FILE: index } : {}
9218
+ }
9219
+ });
9220
+ if (result.exitCode && !acceptFailure)
9221
+ throw new RpcError("GIT_FAILED", result.stderr.trim() || `Git exited ${result.exitCode}`, { exitCode: result.exitCode });
9222
+ return result;
9223
+ }
9224
+ serial(task) {
9225
+ const next = this.queue.then(task, task);
9226
+ this.queue = next.catch(() => {
9227
+ });
9228
+ return next;
9229
+ }
9230
+ async withIndex(task) {
9231
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), "oxbit-checkpoint-"));
9232
+ try {
9233
+ return await task(path.join(directory, "index"));
9234
+ } finally {
9235
+ await fs.rm(directory, { recursive: true, force: true });
9236
+ }
9237
+ }
9238
+ /** Copies the real index so `add -A` reuses its stat data, then writes the tree. */
9239
+ async snapshot(signal) {
9240
+ const repository = await this.run(["rev-parse", "--git-dir", "--show-toplevel"], signal, void 0, true);
9241
+ if (repository.exitCode) return unavailable("not-repository");
9242
+ const top = repository.stdout.trim().split("\n").at(-1) ?? "";
9243
+ if (await fs.realpath(top).catch(() => top) !== this.root) return unavailable("subdirectory");
9244
+ const real = path.resolve(this.root, (await this.run(["rev-parse", "--git-path", "index"], signal)).stdout.trim());
9245
+ return this.withIndex(async (index) => {
9246
+ await fs.copyFile(real, index).catch((error) => {
9247
+ if (error.code !== "ENOENT") throw error;
9248
+ });
9249
+ await this.run(["add", "-A"], signal, index);
9250
+ return (await this.run(["write-tree"], signal, index)).stdout.trim();
9251
+ });
9252
+ }
9253
+ create(signal) {
9254
+ const timeout = AbortSignal.timeout(CHECKPOINT_TIMEOUT);
9255
+ return this.serial(async () => {
9256
+ try {
9257
+ const tree = await this.snapshot(signal ? AbortSignal.any([signal, timeout]) : timeout);
9258
+ return typeof tree === "string" ? { tree } : tree;
9259
+ } catch (error) {
9260
+ if (signal?.aborted) throw error;
9261
+ return unavailable(timeout.aborted ? "timeout" : "failed");
9262
+ }
9263
+ });
9264
+ }
9265
+ async changes(tree, signal) {
9266
+ if (typeof tree !== "string" || !TREE.test(tree)) throw new RpcError("INVALID_PARAMS", "Choose a valid checkpoint");
9267
+ const current = await this.snapshot(signal);
9268
+ if (typeof current !== "string") return current;
9269
+ if ((await this.run(["cat-file", "-e", `${tree}^{tree}`], signal, void 0, true)).exitCode) return unavailable("missing");
9270
+ const output = (await this.run(["diff-tree", "-r", "-z", "--raw", "--no-renames", current, tree], signal)).stdout.split("\0");
9271
+ const changes = [];
9272
+ for (let i = 0; i + 1 < output.length; i += 2) {
9273
+ const [from, to, , , status2] = output[i].replace(/^:/, "").split(" ");
9274
+ if (from === GITLINK || to === GITLINK) continue;
9275
+ changes.push({ status: status2, path: output[i + 1] });
9276
+ }
9277
+ return { tree: current, changes };
9278
+ }
9279
+ diff(tree, signal) {
9280
+ return this.serial(() => this.changes(tree, signal));
9281
+ }
9282
+ /** Writes only paths that differ from the checkpoint. `paths` is the set the client checked for unsaved edits. */
9283
+ restore(tree, paths, signal) {
9284
+ if (paths !== void 0 && (!Array.isArray(paths) || paths.some((item) => typeof item !== "string")))
9285
+ throw new RpcError("INVALID_PARAMS", "paths must be a string array");
9286
+ return this.serial(async () => {
9287
+ const diff = await this.changes(tree, signal);
9288
+ if ("unavailable" in diff) throw new RpcError("CHECKPOINT_UNAVAILABLE", "This checkpoint is no longer available");
9289
+ const checked = paths && new Set(paths);
9290
+ if (checked && diff.changes.some((change) => !checked.has(change.path)))
9291
+ throw new RpcError("CONFLICT", "Files changed while preparing the restore. Try again.");
9292
+ const deleted = diff.changes.filter((change) => change.status === "D").map((change) => change.path);
9293
+ const restored = diff.changes.filter((change) => change.status !== "D").map((change) => change.path);
9294
+ for (const file of deleted) await this.remove(file);
9295
+ if (restored.length)
9296
+ await this.withIndex(async (index) => {
9297
+ await this.run(["read-tree", tree], signal, index);
9298
+ for (let i = 0; i < restored.length; i += BATCH)
9299
+ await this.run(["checkout-index", "-f", "--", ...restored.slice(i, i + BATCH)], signal, index);
9300
+ });
9301
+ return { restored, deleted };
9302
+ });
9303
+ }
9304
+ async remove(relative) {
9305
+ const full = path.resolve(this.root, relative);
9306
+ if (!full.startsWith(this.root + path.sep)) throw new RpcError("PATH_DENIED", `Path is outside the workspace: ${relative}`);
9307
+ await fs.rm(full, { force: true });
9308
+ for (let directory = path.dirname(full); directory !== this.root; directory = path.dirname(directory))
9309
+ if (!await fs.rmdir(directory).then(() => true, () => false)) break;
9310
+ }
9311
+ }
8615
9312
  var re = { exports: {} };
8616
9313
  var constants;
8617
9314
  var hasRequiredConstants;
@@ -9289,7 +9986,7 @@ function requireSort() {
9289
9986
  if (hasRequiredSort) return sort_1;
9290
9987
  hasRequiredSort = 1;
9291
9988
  const compareBuild = requireCompareBuild();
9292
- const sort = (list, loose) => list.sort((a, b) => compareBuild(a, b, loose));
9989
+ const sort = (list2, loose) => list2.sort((a, b) => compareBuild(a, b, loose));
9293
9990
  sort_1 = sort;
9294
9991
  return sort_1;
9295
9992
  }
@@ -9299,7 +9996,7 @@ function requireRsort() {
9299
9996
  if (hasRequiredRsort) return rsort_1;
9300
9997
  hasRequiredRsort = 1;
9301
9998
  const compareBuild = requireCompareBuild();
9302
- const rsort = (list, loose) => list.sort((a, b) => compareBuild(b, a, loose));
9999
+ const rsort = (list2, loose) => list2.sort((a, b) => compareBuild(b, a, loose));
9303
10000
  rsort_1 = rsort;
9304
10001
  return rsort_1;
9305
10002
  }
@@ -11460,8 +12157,8 @@ function createKernel({
11460
12157
  }
11461
12158
  extensionChanged(id);
11462
12159
  },
11463
- async load(url, options2) {
11464
- const parsed = new URL(url, globalThis.location?.href ?? "file:///");
12160
+ async load(url2, options2) {
12161
+ const parsed = new URL(url2, globalThis.location?.href ?? "file:///");
11465
12162
  if (!["http:", "https:", "file:"].includes(parsed.protocol) && (!extensionOrigins.has(`${parsed.protocol}//${parsed.host}`) || parsed.username || parsed.password))
11466
12163
  throw new Error(
11467
12164
  "Extensions require a trusted HTTP(S), file, or application ESM artifact"
@@ -11612,9 +12309,9 @@ function bonjourName() {
11612
12309
  return macLocalName || void 0;
11613
12310
  }
11614
12311
  function lanAddresses$1(interfaces = os.networkInterfaces()) {
11615
- const addresses = Object.values(interfaces).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal);
12312
+ const addresses = Object.values(interfaces).flatMap((entries) => entries ?? []).filter((entry2) => !entry2.internal);
11616
12313
  const of = (family) => [
11617
- ...new Set(addresses.filter((entry) => entry.family === family).map((entry) => entry.address.split("%")[0]))
12314
+ ...new Set(addresses.filter((entry2) => entry2.family === family).map((entry2) => entry2.address.split("%")[0]))
11618
12315
  ];
11619
12316
  return { v4: of("IPv4"), v6: of("IPv6") };
11620
12317
  }
@@ -11630,6 +12327,267 @@ function lanUrl(port2, interfaces = os.networkInterfaces()) {
11630
12327
  const address = lanAddresses$1(interfaces).v4[0];
11631
12328
  return address ? `http://${address}:${port2}` : void 0;
11632
12329
  }
12330
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
12331
+ const shellOwned = /* @__PURE__ */ new Set(["PWD", "OLDPWD", "SHLVL", "_", "OXBIT_RESOLVING_ENVIRONMENT"]);
12332
+ const launchOwned = (key) => /^(SSH_|GH_|GITHUB_)/.test(key) || key.toLowerCase().endsWith("_proxy");
12333
+ function parseShellEnvironment(output, mark) {
12334
+ const start = output.indexOf(mark), end = output.lastIndexOf(mark);
12335
+ if (start < 0 || end <= start) return void 0;
12336
+ const env2 = {};
12337
+ for (const entry2 of output.slice(start + mark.length, end).split("\0")) {
12338
+ const separator = entry2.indexOf("="), key = entry2.slice(0, separator);
12339
+ if (separator > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && !shellOwned.has(key))
12340
+ env2[key] = entry2.slice(separator + 1);
12341
+ }
12342
+ return env2;
12343
+ }
12344
+ function loginShellEnvironment(cwd, inherited = process.env, platform = process.platform, timeout = 1e4) {
12345
+ if (platform === "win32") return Promise.resolve(inherited);
12346
+ const { shell, args } = resolveTerminalShell(inherited, platform);
12347
+ const mark = randomBytes(12).toString("hex");
12348
+ return new Promise((resolve) => {
12349
+ const chunks = [];
12350
+ let bytes = 0, settled = false;
12351
+ const child = spawn(shell, [...args, "-c", `printf ${mark}; /usr/bin/env -0; printf ${mark}`], {
12352
+ cwd,
12353
+ env: { ...inherited, OXBIT_RESOLVING_ENVIRONMENT: "1" },
12354
+ stdio: ["ignore", "pipe", "ignore"],
12355
+ detached: true
12356
+ });
12357
+ trackChild(child);
12358
+ const finish = (env2) => {
12359
+ if (settled) return;
12360
+ settled = true;
12361
+ clearTimeout(timer);
12362
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
12363
+ if (!env2) {
12364
+ resolve(inherited);
12365
+ return;
12366
+ }
12367
+ const merged = { ...inherited, ...env2 };
12368
+ for (const [key, value] of Object.entries(inherited))
12369
+ if (launchOwned(key)) merged[key] = value;
12370
+ resolve(merged);
12371
+ };
12372
+ const timer = setTimeout(() => finish(), timeout);
12373
+ child.stdout.on("data", (chunk) => {
12374
+ bytes += chunk.length;
12375
+ if (bytes > MAX_OUTPUT_BYTES) finish();
12376
+ else chunks.push(chunk);
12377
+ });
12378
+ child.on("error", () => finish());
12379
+ child.on("close", (code) => finish(code === 0 ? parseShellEnvironment(Buffer.concat(chunks).toString(), mark) : void 0));
12380
+ });
12381
+ }
12382
+ const PUSH_DEVICES_FILE = "push-devices.json";
12383
+ const MAX_DEVICES = 16;
12384
+ const RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
12385
+ const WINDOW_SECONDS = 15 * 60;
12386
+ const RELAY_TIMEOUT_MS = 1e4;
12387
+ const HANDLE = /^[A-Za-z0-9_-]{1,2048}$/;
12388
+ const STATUSES = ["working", "waiting", "finished", "failed"];
12389
+ const TITLES = { waiting: "Agent needs your input", finished: "Agent finished", failed: "Agent turn failed" };
12390
+ const log = (message2) => process.stderr.write(`Oxbit push: ${message2}
12391
+ `);
12392
+ function deviceIdOf(params) {
12393
+ const value = requireString(params, "deviceId", 128);
12394
+ if (!value) throw new RpcError("INVALID_PARAMS", "Invalid deviceId");
12395
+ return value;
12396
+ }
12397
+ function handleOf(params, key) {
12398
+ const value = params[key];
12399
+ if (value === void 0 || value === null) return void 0;
12400
+ if (typeof value !== "string" || !HANDLE.test(value)) throw new RpcError("INVALID_PARAMS", `Invalid ${key}`);
12401
+ return value;
12402
+ }
12403
+ const text$1 = (value) => typeof value === "string" && value.length <= 1024;
12404
+ function activityOf(value) {
12405
+ if (value === null) return null;
12406
+ const v = value ?? {};
12407
+ const startedAt = v.startedAt;
12408
+ if (typeof value !== "object" || !text$1(v.agent) || !text$1(v.title) || !text$1(v.detail) || !STATUSES.includes(v.status) || !Number.isSafeInteger(v.pending) || v.pending < 0 || startedAt !== void 0 && (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt < 0))
12409
+ throw new RpcError("INVALID_PARAMS", "Invalid activity");
12410
+ return {
12411
+ agent: v.agent,
12412
+ title: v.title,
12413
+ status: v.status,
12414
+ detail: v.detail,
12415
+ pending: v.pending,
12416
+ ...startedAt === void 0 ? {} : { startedAt }
12417
+ };
12418
+ }
12419
+ function restore(entry2) {
12420
+ try {
12421
+ const activity = entry2.activity === void 0 ? null : activityOf(entry2.activity);
12422
+ const device = {
12423
+ deviceId: deviceIdOf(entry2),
12424
+ session: requireString(entry2, "session", 128),
12425
+ alert: handleOf(entry2, "alert"),
12426
+ liveActivity: handleOf(entry2, "liveActivity"),
12427
+ active: false,
12428
+ ...activity ? { activity } : {},
12429
+ updatedAt: entry2.updatedAt
12430
+ };
12431
+ return Number.isFinite(device.updatedAt) ? device : void 0;
12432
+ } catch {
12433
+ return void 0;
12434
+ }
12435
+ }
12436
+ function detail(status2, pending) {
12437
+ if (status2 === "working") return "Working";
12438
+ if (status2 === "waiting") return pending === 1 ? "1 request needs an answer" : `${pending} requests need an answer`;
12439
+ return TITLES[status2];
12440
+ }
12441
+ function activityState(snapshot, event) {
12442
+ return {
12443
+ status: event.status,
12444
+ title: snapshot.title,
12445
+ detail: detail(event.status, event.pending),
12446
+ pending: event.pending,
12447
+ ...event.status === "working" && event.startedAt !== void 0 ? { startedAt: Math.floor(event.startedAt / 1e3) } : {}
12448
+ };
12449
+ }
12450
+ class PushDevices {
12451
+ constructor(file) {
12452
+ this.file = file;
12453
+ }
12454
+ file;
12455
+ devices = /* @__PURE__ */ new Map();
12456
+ /** Last Live Activity state sent per device. */
12457
+ sent = /* @__PURE__ */ new Map();
12458
+ persistence = Promise.resolve();
12459
+ static async open(dataDir) {
12460
+ const store = new PushDevices(path.join(dataDir, PUSH_DEVICES_FILE));
12461
+ try {
12462
+ const state2 = JSON.parse(await fs.readFile(store.file, "utf8"));
12463
+ if (state2?.version === 1 && Array.isArray(state2.devices))
12464
+ for (const entry2 of state2.devices) {
12465
+ const device = entry2 && typeof entry2 === "object" ? restore(entry2) : void 0;
12466
+ if (device) store.devices.set(device.deviceId, device);
12467
+ }
12468
+ } catch (error) {
12469
+ if (error.code !== "ENOENT") log(`ignoring unreadable ${PUSH_DEVICES_FILE}: ${String(error)}`);
12470
+ }
12471
+ store.prune();
12472
+ return store;
12473
+ }
12474
+ async register(session, connection, params) {
12475
+ const deviceId = deviceIdOf(params), alert = handleOf(params, "alert"), liveActivity = handleOf(params, "liveActivity");
12476
+ const previous = this.devices.get(deviceId);
12477
+ this.sent.delete(deviceId);
12478
+ this.devices.set(deviceId, {
12479
+ ...previous,
12480
+ deviceId,
12481
+ session,
12482
+ alert,
12483
+ liveActivity,
12484
+ active: true,
12485
+ connection,
12486
+ updatedAt: Date.now()
12487
+ });
12488
+ await this.persist();
12489
+ return {};
12490
+ }
12491
+ async state(session, connection, params) {
12492
+ const deviceId = deviceIdOf(params);
12493
+ if (typeof params.active !== "boolean") throw new RpcError("INVALID_PARAMS", "active must be boolean");
12494
+ const activity = params.activity === void 0 ? void 0 : activityOf(params.activity);
12495
+ const device = this.devices.get(deviceId);
12496
+ if (!device || device.session !== session) throw new RpcError("NOT_FOUND", "Register the device first");
12497
+ device.active = params.active;
12498
+ if (params.active) device.connection = connection;
12499
+ if (activity) device.activity = activity;
12500
+ else if (activity === null) delete device.activity;
12501
+ device.updatedAt = Date.now();
12502
+ this.sent.delete(deviceId);
12503
+ await this.persist();
12504
+ return {};
12505
+ }
12506
+ async unregister(session, params) {
12507
+ const deviceId = deviceIdOf(params);
12508
+ if (this.devices.get(deviceId)?.session === session) {
12509
+ this.devices.delete(deviceId);
12510
+ this.sent.delete(deviceId);
12511
+ await this.persist();
12512
+ }
12513
+ return {};
12514
+ }
12515
+ /** Marks devices inactive when the connection that set them active closes. */
12516
+ disconnect(connection) {
12517
+ let changed = false;
12518
+ for (const device of this.devices.values())
12519
+ if (device.active && device.connection === connection) {
12520
+ device.active = false;
12521
+ changed = true;
12522
+ }
12523
+ if (changed) this.persist().catch((error) => log(`cannot save devices: ${String(error)}`));
12524
+ }
12525
+ /** Sends alert and Live Activity pushes to the owner's inactive devices; resolves after every send settles. */
12526
+ async notify(event) {
12527
+ const sends = [];
12528
+ for (const device of this.devices.values()) {
12529
+ if (device.active || device.session !== event.owner) continue;
12530
+ if (event.status !== "working" && device.alert)
12531
+ sends.push(this.send(device.deviceId, "alert", {
12532
+ handle: device.alert,
12533
+ alert: { title: TITLES[event.status], body: device.activity?.title || event.agent }
12534
+ }));
12535
+ if (!device.liveActivity || !device.activity) continue;
12536
+ const state2 = activityState(device.activity, event), end = event.status === "finished" || event.status === "failed";
12537
+ const key = JSON.stringify([end, state2]);
12538
+ if (this.sent.get(device.deviceId) === key) continue;
12539
+ this.sent.set(device.deviceId, key);
12540
+ const window = Math.floor(Date.now() / 1e3) + WINDOW_SECONDS;
12541
+ sends.push(this.send(device.deviceId, "liveActivity", {
12542
+ handle: device.liveActivity,
12543
+ activity: { event: end ? "end" : "update", state: state2, ...end ? { dismissalDate: window } : { staleDate: window } }
12544
+ }));
12545
+ }
12546
+ await Promise.all(sends);
12547
+ }
12548
+ flush() {
12549
+ return this.persistence.catch(() => {
12550
+ });
12551
+ }
12552
+ async send(deviceId, kind, request) {
12553
+ const relay = (process.env.OXBIT_PUSH_RELAY ?? PUSH_RELAY_URL).replace(/\/+$/, "");
12554
+ try {
12555
+ const response = await fetch(`${relay}/v1/push`, {
12556
+ method: "POST",
12557
+ headers: { "Content-Type": "application/json" },
12558
+ body: JSON.stringify(request),
12559
+ signal: AbortSignal.timeout(RELAY_TIMEOUT_MS)
12560
+ });
12561
+ const result = await response.json().catch(() => ({}));
12562
+ const device = this.devices.get(deviceId);
12563
+ if (result.gone && device?.[kind] === request.handle) {
12564
+ delete device[kind];
12565
+ if (kind === "liveActivity") this.sent.delete(deviceId);
12566
+ await this.persist();
12567
+ } else if (!response.ok || !result.sent)
12568
+ log(`relay did not send to ${deviceId}: ${result.reason ?? `HTTP ${response.status}`}`);
12569
+ } catch (error) {
12570
+ log(`push to ${deviceId} failed: ${error instanceof Error ? error.message : String(error)}`);
12571
+ }
12572
+ }
12573
+ prune(now = Date.now()) {
12574
+ for (const [id, device] of this.devices)
12575
+ if (now - device.updatedAt > RETENTION_MS) this.devices.delete(id);
12576
+ const oldest = [...this.devices.values()].sort((a, b) => a.updatedAt - b.updatedAt);
12577
+ for (const device of oldest.slice(0, Math.max(0, oldest.length - MAX_DEVICES))) this.devices.delete(device.deviceId);
12578
+ for (const id of this.sent.keys()) if (!this.devices.has(id)) this.sent.delete(id);
12579
+ }
12580
+ persist() {
12581
+ this.prune();
12582
+ const payload = JSON.stringify({ version: 1, devices: [...this.devices.values()] });
12583
+ this.persistence = this.persistence.catch(() => {
12584
+ }).then(async () => {
12585
+ await fs.writeFile(this.file + ".tmp", payload, { mode: 384 });
12586
+ await fs.rename(this.file + ".tmp", this.file);
12587
+ });
12588
+ return this.persistence;
12589
+ }
12590
+ }
11633
12591
  const MAX_SOCKET_BACKLOG_BYTES = MAX_FILE_WRITE_MESSAGE_BYTES;
11634
12592
  const allCapabilities = [
11635
12593
  "filesystem.read",
@@ -11734,13 +12692,13 @@ async function createRuntime(options2) {
11734
12692
  const origin = request.headers.origin;
11735
12693
  return origin === void 0 ? !required : allowedOrigins().has(origin);
11736
12694
  };
11737
- const send = (connection, message) => {
12695
+ const send = (connection, message2) => {
11738
12696
  if (connection.ws.readyState !== WebSocket.OPEN) return;
11739
12697
  if (connection.ws.bufferedAmount > MAX_SOCKET_BACKLOG_BYTES) {
11740
12698
  connection.ws.close(1013, "Slow consumer; reconnect to resynchronize");
11741
12699
  return;
11742
12700
  }
11743
- connection.ws.send(JSON.stringify(message));
12701
+ connection.ws.send(JSON.stringify(message2));
11744
12702
  };
11745
12703
  const event = (connection, name, params, stream, seq) => send(connection, {
11746
12704
  v: 1,
@@ -11771,10 +12729,12 @@ async function createRuntime(options2) {
11771
12729
  throw new RpcError("FORBIDDEN", "Owner access is required");
11772
12730
  return session;
11773
12731
  };
12732
+ const acpRegistry = new ACPRegistry(dataDir, options2.acpRegistryUrl);
12733
+ const pushDevices = await PushDevices.open(dataDir);
11774
12734
  const agents = new AgentACP(files, (connectionId, name, params) => {
11775
12735
  const connection = connections.get(connectionId);
11776
12736
  if (connection?.session && !connection.session.revoked && trusted) event(connection, name, params);
11777
- }, () => updateIdle());
12737
+ }, () => updateIdle(), acpRegistry, (status2) => void pushDevices.notify(status2));
11778
12738
  const processEvent = ({ event: name, params, stream, seq }, ownerId) => {
11779
12739
  const cap = name.startsWith("terminal.") ? "terminal" : "tasks";
11780
12740
  for (const c of connections.values()) {
@@ -11784,7 +12744,7 @@ async function createRuntime(options2) {
11784
12744
  };
11785
12745
  const processes = new Processes(root, processEvent);
11786
12746
  const taskStore = await TaskConfigStore.create(root, { home: options2.tasksHome ?? setting("TASKS_HOME"), gitPath: options2.desktop?.gitPath });
11787
- const tasks = new TaskRunner(taskStore, processEvent, options2.forwardTaskPort);
12747
+ const tasks = new TaskRunner(taskStore, processEvent, options2.forwardTaskPort, loginShellEnvironment);
11788
12748
  const worktrees = new TaskWorktrees(taskStore, tasks);
11789
12749
  let projectChanged = () => {
11790
12750
  };
@@ -11834,6 +12794,7 @@ async function createRuntime(options2) {
11834
12794
  }
11835
12795
  );
11836
12796
  const git = new Git(files, options2.desktop?.gitPath);
12797
+ const checkpoints = new ACPCheckpoints(files.root, options2.desktop?.gitPath);
11837
12798
  const extensions = new RuntimeExtensions(files, {
11838
12799
  "runtime.git": {
11839
12800
  status: (signal) => git.status(signal),
@@ -11939,29 +12900,29 @@ async function createRuntime(options2) {
11939
12900
  httpJson(response, 403, { error: "Origin is not allowed" });
11940
12901
  return;
11941
12902
  }
11942
- const url = new URL(request.url ?? "/", `http://${host}:${port2}`);
11943
- if (url.pathname === "/api/health") {
12903
+ const url2 = new URL(request.url ?? "/", `http://${host}:${port2}`);
12904
+ if (url2.pathname === "/api/health") {
11944
12905
  httpJson(response, 200, { ok: true, protocol: 1, ...identity2 });
11945
12906
  return;
11946
12907
  }
11947
- if (url.pathname === "/api/pair" && request.method === "POST") {
12908
+ if (url2.pathname === "/api/pair" && request.method === "POST") {
11948
12909
  if (options2.desktop) {
11949
12910
  httpJson(response, 403, { error: "Desktop pairing requires the parent channel" });
11950
12911
  return;
11951
12912
  }
11952
- const address2 = request.socket.remoteAddress ?? "unknown", entry = attempts.get(address2) ?? {
12913
+ const address2 = request.socket.remoteAddress ?? "unknown", entry2 = attempts.get(address2) ?? {
11953
12914
  count: 0,
11954
12915
  reset: Date.now() + 6e4
11955
12916
  };
11956
- if (Date.now() > entry.reset) {
11957
- entry.count = 0;
11958
- entry.reset = Date.now() + 6e4;
12917
+ if (Date.now() > entry2.reset) {
12918
+ entry2.count = 0;
12919
+ entry2.reset = Date.now() + 6e4;
11959
12920
  }
11960
- if (++entry.count > pairingAttemptsPerMinute) {
12921
+ if (++entry2.count > pairingAttemptsPerMinute) {
11961
12922
  httpJson(response, 429, { error: "Pairing rate limit reached" });
11962
12923
  return;
11963
12924
  }
11964
- attempts.set(address2, entry);
12925
+ attempts.set(address2, entry2);
11965
12926
  const input = await body(request);
11966
12927
  const code = typeof input.code === "string" ? input.code : "";
11967
12928
  const a = Buffer.from(code), b = Buffer.from(pairingCode);
@@ -11977,7 +12938,7 @@ async function createRuntime(options2) {
11977
12938
  httpJson(response, 200, { ...result, runtime: identity2 });
11978
12939
  return;
11979
12940
  }
11980
- if (url.pathname === "/api/grants") {
12941
+ if (url2.pathname === "/api/grants") {
11981
12942
  const session = httpSession(request);
11982
12943
  if (!session?.owner) {
11983
12944
  httpJson(response, 403, { error: "Owner access is required" });
@@ -12006,7 +12967,7 @@ async function createRuntime(options2) {
12006
12967
  return;
12007
12968
  }
12008
12969
  }
12009
- if (url.pathname.startsWith("/api/grants/") && request.method === "DELETE") {
12970
+ if (url2.pathname.startsWith("/api/grants/") && request.method === "DELETE") {
12010
12971
  if (!httpSession(request)?.owner) {
12011
12972
  httpJson(response, 403, { error: "Owner access is required" });
12012
12973
  return;
@@ -12015,7 +12976,7 @@ async function createRuntime(options2) {
12015
12976
  response,
12016
12977
  200,
12017
12978
  await revoke(
12018
- decodeURIComponent(url.pathname.slice("/api/grants/".length))
12979
+ decodeURIComponent(url2.pathname.slice("/api/grants/".length))
12019
12980
  )
12020
12981
  );
12021
12982
  return;
@@ -12026,7 +12987,7 @@ async function createRuntime(options2) {
12026
12987
  }
12027
12988
  let relative;
12028
12989
  try {
12029
- relative = decodeURIComponent(url.pathname);
12990
+ relative = decodeURIComponent(url2.pathname);
12030
12991
  } catch {
12031
12992
  httpJson(response, 400, { error: "Invalid path" });
12032
12993
  return;
@@ -12120,7 +13081,7 @@ async function createRuntime(options2) {
12120
13081
  if (method.startsWith("terminal.")) return { cap: "terminal", trust: true };
12121
13082
  if (method.startsWith("tasks.")) return { cap: "tasks", trust: !["tasks.catalog", "tasks.list", "tasks.attach", "tasks.worktrees"].includes(method) };
12122
13083
  if (method.startsWith("git.")) return { cap: "git", trust: true };
12123
- if (method.startsWith("acp.")) return { cap: "extensions", trust: true };
13084
+ if (method.startsWith("acp.") || method.startsWith("push.")) return { cap: "extensions", trust: true };
12124
13085
  if (method.startsWith("lsp.")) return { cap: "lsp", trust: true };
12125
13086
  if (method.startsWith("collab.")) return { cap: "collaboration" };
12126
13087
  if (method.startsWith("extensions."))
@@ -12150,9 +13111,10 @@ async function createRuntime(options2) {
12150
13111
  const required = permission(method), session = authorized(connection, required.cap, required.trust);
12151
13112
  if (method.startsWith("acp.")) {
12152
13113
  owner(connection);
12153
- if (!["acp.start", "acp.list", "acp.attach", "acp.disconnect"].includes(method))
13114
+ if (!["acp.start", "acp.list", "acp.attach", "acp.detach", "acp.stop", "acp.disconnect", "acp.registry"].includes(method) && !method.startsWith("acp.checkpoint."))
12154
13115
  agents.control(session.id, requireString(params, "id"), connection.id);
12155
13116
  }
13117
+ if (method.startsWith("push.")) owner(connection);
12156
13118
  if (method.startsWith("project.")) owner(connection);
12157
13119
  if (method.startsWith("settings.")) owner(connection);
12158
13120
  switch (method) {
@@ -12178,6 +13140,8 @@ async function createRuntime(options2) {
12178
13140
  return agents.start(session.id, params, signal, connection.id);
12179
13141
  case "acp.list":
12180
13142
  return agents.list(session.id);
13143
+ case "acp.registry":
13144
+ return acpRegistry.listing(params.refresh === true);
12181
13145
  case "acp.attach":
12182
13146
  return agents.attach(session.id, requireString(params, "id"), connection.id, params.clientCapabilities?.editorTools === true);
12183
13147
  case "acp.enqueue":
@@ -12185,6 +13149,9 @@ async function createRuntime(options2) {
12185
13149
  return agents.enqueue(session.id, requireString(params, "id"), params, method === "acp.interrupt");
12186
13150
  case "acp.dequeue":
12187
13151
  return agents.dequeue(session.id, requireString(params, "id"), requireString(params, "promptId"));
13152
+ case "acp.moveQueued":
13153
+ if (!Number.isInteger(params.index)) throw new RpcError("INVALID_PARAMS", "index must be an integer");
13154
+ return agents.moveQueued(session.id, requireString(params, "id"), requireString(params, "promptId"), params.index);
12188
13155
  case "acp.resumeQueue":
12189
13156
  return agents.resumeQueue(session.id, requireString(params, "id"));
12190
13157
  case "acp.call":
@@ -12195,9 +13162,26 @@ async function createRuntime(options2) {
12195
13162
  return agents.cancel(session.id, requireString(params, "id"));
12196
13163
  case "acp.stop":
12197
13164
  return agents.stop(session.id, requireString(params, "id"));
13165
+ case "acp.detach":
13166
+ agents.detach(connection.id);
13167
+ return {};
12198
13168
  case "acp.disconnect":
12199
13169
  agents.disconnect(session.id, connection.id);
12200
13170
  return {};
13171
+ case "push.register":
13172
+ return pushDevices.register(session.id, connection.id, params);
13173
+ case "push.state":
13174
+ return pushDevices.state(session.id, connection.id, params);
13175
+ case "push.unregister":
13176
+ return pushDevices.unregister(session.id, params);
13177
+ case "acp.checkpoint.create":
13178
+ return checkpoints.create(signal);
13179
+ case "acp.checkpoint.diff":
13180
+ return checkpoints.diff(params.tree, signal);
13181
+ case "acp.checkpoint.restore":
13182
+ if (agents.list(session.id).sessions.some((agent) => agent.busy || agent.pendingRequests))
13183
+ throw new RpcError("BUSY", "Wait for the agent to finish before restoring a checkpoint");
13184
+ return checkpoints.restore(params.tree, params.paths, signal);
12201
13185
  case "workspace.info":
12202
13186
  return sessionInfo(session);
12203
13187
  case "workspace.trust":
@@ -12371,6 +13355,8 @@ async function createRuntime(options2) {
12371
13355
  return git.show(params.ref, signal);
12372
13356
  case "git.commitDiff":
12373
13357
  return git.commitDiff(params, signal);
13358
+ case "git.branchDiff":
13359
+ return git.branchDiff(params, signal);
12374
13360
  case "git.stashes":
12375
13361
  return git.stashes(signal);
12376
13362
  case "git.stashDiff":
@@ -12535,6 +13521,25 @@ async function createRuntime(options2) {
12535
13521
  params.encoding,
12536
13522
  params.eol
12537
13523
  );
13524
+ case "search.files": {
13525
+ const term = requireString(params, "query", 1024).trim().toLowerCase();
13526
+ if (!term) return [];
13527
+ const result = await runCommand(options2.desktop?.rgPath ?? "rg", ["--files", "--hidden", "--glob", "!.git", "--glob", "!node_modules", "--glob", "!.oxbit-tmp-*"], {
13528
+ cwd: root,
13529
+ signal,
13530
+ maxBytes: 64 * MAX_BUFFER_BYTES
13531
+ });
13532
+ if (result.exitCode > 1) throw new RpcError("SEARCH_FAILED", result.stderr.trim());
13533
+ const matches = (path2) => {
13534
+ let at = 0;
13535
+ for (const char of term) {
13536
+ at = path2.indexOf(char, at) + 1;
13537
+ if (!at) return false;
13538
+ }
13539
+ return true;
13540
+ };
13541
+ return result.stdout.split("\n").filter((path2) => path2 && matches(path2.toLowerCase())).sort().slice(0, 1e3);
13542
+ }
12538
13543
  case "search.query": {
12539
13544
  const query = requireString(params, "query", 8192);
12540
13545
  if (!query) return [];
@@ -12669,32 +13674,32 @@ async function createRuntime(options2) {
12669
13674
  authDeadline.unref();
12670
13675
  ws.on("message", (raw) => {
12671
13676
  void (async () => {
12672
- let message;
13677
+ let message2;
12673
13678
  try {
12674
- message = parseClientMessage(raw.toString());
13679
+ message2 = parseClientMessage(raw.toString());
12675
13680
  } catch (error) {
12676
13681
  event(connection, "protocol.error", errorShape(error));
12677
13682
  ws.close(1008, "Invalid protocol message");
12678
13683
  return;
12679
13684
  }
12680
- if (message.type === "cancel") {
13685
+ if (message2.type === "cancel") {
12681
13686
  authorized(connection);
12682
- connection.pending.get(message.id)?.abort();
13687
+ connection.pending.get(message2.id)?.abort();
12683
13688
  return;
12684
13689
  }
12685
- if (message.type === "ack") {
13690
+ if (message2.type === "ack") {
12686
13691
  const session = authorized(connection, "terminal");
12687
- if (!message.stream.startsWith("terminal:"))
13692
+ if (!message2.stream.startsWith("terminal:"))
12688
13693
  throw new RpcError("INVALID_PARAMS", "Unknown stream");
12689
13694
  processes.ack(
12690
- message.stream.slice(9),
13695
+ message2.stream.slice(9),
12691
13696
  session.id,
12692
13697
  connection.id,
12693
- message.seq
13698
+ message2.seq
12694
13699
  );
12695
13700
  return;
12696
13701
  }
12697
- const { id, method, params } = message;
13702
+ const { id, method, params } = message2;
12698
13703
  if (connection.pending.size >= 64) {
12699
13704
  send(connection, {
12700
13705
  v: 1,
@@ -12800,6 +13805,7 @@ async function createRuntime(options2) {
12800
13805
  clearTimeout(authDeadline);
12801
13806
  agents.detach(connection.id);
12802
13807
  lsp.detach(connection.id);
13808
+ pushDevices.disconnect(connection.id);
12803
13809
  connections.delete(connection.id);
12804
13810
  updateIdle();
12805
13811
  for (const pending of connection.edits.values()) {
@@ -12950,6 +13956,7 @@ async function createRuntime(options2) {
12950
13956
  await Promise.allSettled([...inflight.values()]);
12951
13957
  await collaboration.close();
12952
13958
  await persistence;
13959
+ await pushDevices.flush();
12953
13960
  await new Promise((resolve) => websocket.close(() => resolve()));
12954
13961
  await new Promise((resolve) => server.close(() => resolve()));
12955
13962
  }
@@ -13147,7 +14154,7 @@ function lanAddresses() {
13147
14154
  return { v4: of("IPv4"), v6: of("IPv6") };
13148
14155
  }
13149
14156
  const stderrLog = (line) => process.stderr.write(line + "\n");
13150
- function advertise(ad, log = stderrLog) {
14157
+ function advertise(ad, log2 = stderrLog) {
13151
14158
  const instance = instanceLabel(ad.name, ad.id);
13152
14159
  const current = (ttl) => records({ ...ad, ...(ad.addresses ?? lanAddresses)() }, ttl);
13153
14160
  const timers = [];
@@ -13166,11 +14173,11 @@ function advertise(ad, log = stderrLog) {
13166
14173
  };
13167
14174
  const fail = (error) => {
13168
14175
  if (closed) return;
13169
- log(`mDNS advertising disabled: ${error.message}`);
14176
+ log2(`mDNS advertising disabled: ${error.message}`);
13170
14177
  stop2();
13171
14178
  };
13172
14179
  const sendFailure = (error) => {
13173
- if (!sendFailed) log(`mDNS send failed: ${error.message}`);
14180
+ if (!sendFailed) log2(`mDNS send failed: ${error.message}`);
13174
14181
  sendFailed = true;
13175
14182
  };
13176
14183
  const send = (packet, port2 = PORT, address = GROUP, done) => {
@@ -13240,8 +14247,8 @@ function advertise(ad, log = stderrLog) {
13240
14247
  })
13241
14248
  };
13242
14249
  }
13243
- const dataDirFor = (root, env = process.env) => {
13244
- const directory = setting("DATA_DIR", env);
14250
+ const dataDirFor = (root, env2 = process.env) => {
14251
+ const directory = setting("DATA_DIR", env2);
13245
14252
  return directory ? path.resolve(directory) : workspaceDataDir(root);
13246
14253
  };
13247
14254
  const recordFile = (dataDir) => path.join(dataDir, "daemon.json");
@@ -13252,14 +14259,14 @@ function launchUrl(daemon, file) {
13252
14259
  if (file) params.set("open", file);
13253
14260
  return `http://${localHost(daemon.host)}:${daemon.port}/#${params}`;
13254
14261
  }
13255
- function browserCommand(url, platform) {
13256
- if (platform === "darwin") return { command: "open", args: [url] };
14262
+ function browserCommand(url2, platform) {
14263
+ if (platform === "darwin") return { command: "open", args: [url2] };
13257
14264
  if (platform === "win32")
13258
- return { command: "cmd", args: ["/c", "start", "", url] };
13259
- return { command: "xdg-open", args: [url] };
14265
+ return { command: "cmd", args: ["/c", "start", "", url2] };
14266
+ return { command: "xdg-open", args: [url2] };
13260
14267
  }
13261
- function launchBrowser(url) {
13262
- const { command, args } = browserCommand(url, process.platform);
14268
+ function launchBrowser(url2) {
14269
+ const { command, args } = browserCommand(url2, process.platform);
13263
14270
  const child = spawn(command, args, { detached: true, stdio: "ignore" });
13264
14271
  child.on("error", () => {
13265
14272
  });
@@ -13357,7 +14364,7 @@ function parseDuration(value) {
13357
14364
  const duration = match ? Number(match[1]) * units[match[2] ?? ""] : NaN;
13358
14365
  return Number.isSafeInteger(duration) ? duration : void 0;
13359
14366
  }
13360
- function parse(argv, env = process.env) {
14367
+ function parse(argv, env2 = process.env) {
13361
14368
  const parsed = tokenize(argv);
13362
14369
  if ("error" in parsed) return parsed;
13363
14370
  const { values, positionals } = parsed;
@@ -13369,23 +14376,23 @@ function parse(argv, env = process.env) {
13369
14376
  return { error: "--desktop cannot be combined with browser runtime options" };
13370
14377
  if (values.lan && values.host)
13371
14378
  return { error: "--lan binds every interface and cannot be combined with --host" };
13372
- const requested = text(values.port) ?? env.PORT;
14379
+ const requested = text(values.port) ?? env2.PORT;
13373
14380
  const port2 = requested === void 0 ? 0 : Number(requested);
13374
14381
  if (!Number.isInteger(port2) || port2 < 0 || port2 > 65535)
13375
14382
  return {
13376
14383
  error: `Port must be a whole number between 0 and 65535, received "${requested}"`
13377
14384
  };
13378
- const lifetime = text(values["keep-alive"]) ?? setting("KEEP_ALIVE", env);
14385
+ const lifetime = text(values["keep-alive"]) ?? setting("KEEP_ALIVE", env2);
13379
14386
  const keepAlive = lifetime === void 0 ? 0 : parseDuration(lifetime);
13380
14387
  if (keepAlive === void 0)
13381
14388
  return {
13382
14389
  error: `Keep-alive must be a duration such as 75s, 15m, 1h, forever, or milliseconds, received "${lifetime}"`
13383
14390
  };
13384
14391
  return {
13385
- target: positionals[0] ?? setting("WORKSPACE", env) ?? ".",
14392
+ target: positionals[0] ?? setting("WORKSPACE", env2) ?? ".",
13386
14393
  port: port2,
13387
14394
  keepAlive,
13388
- host: values.lan ? "0.0.0.0" : text(values.host) ?? env.HOST ?? DEFAULT_HOST,
14395
+ host: values.lan ? "0.0.0.0" : text(values.host) ?? env2.HOST ?? DEFAULT_HOST,
13389
14396
  lan: values.lan === true,
13390
14397
  open: values["no-open"] !== true,
13391
14398
  foreground: values.foreground === true,
@@ -13417,15 +14424,15 @@ function serveFailure(error, host, port2) {
13417
14424
  return `Cannot serve on ${address}: the address is not available on this machine.`;
13418
14425
  return `The runtime could not start: ${failure2.message ?? String(error)}`;
13419
14426
  }
13420
- async function serve(invocation, target, dataDir, env) {
14427
+ async function serve(invocation, target, dataDir, env2) {
13421
14428
  try {
13422
- return await serveRuntime(invocation, target, dataDir, env);
14429
+ return await serveRuntime(invocation, target, dataDir, env2);
13423
14430
  } catch (error) {
13424
14431
  process.stderr.write(serveFailure(error, invocation.host, invocation.port) + "\n");
13425
14432
  return 1;
13426
14433
  }
13427
14434
  }
13428
- async function serveRuntime(invocation, target, dataDir, env) {
14435
+ async function serveRuntime(invocation, target, dataDir, env2) {
13429
14436
  const host = invocation.host;
13430
14437
  const port2 = invocation.port;
13431
14438
  let stop2 = () => {
@@ -13435,10 +14442,10 @@ async function serveRuntime(invocation, target, dataDir, env) {
13435
14442
  port: port2,
13436
14443
  host,
13437
14444
  dataDir,
13438
- pairingCode: setting("PAIRING_CODE", env),
13439
- pairingAttemptsPerMinute: setting("PAIRING_ATTEMPTS_PER_MINUTE", env) === void 0 ? void 0 : Number(setting("PAIRING_ATTEMPTS_PER_MINUTE", env)),
13440
- origins: setting("ORIGINS", env)?.split(",").filter(Boolean),
13441
- webRoot: setting("WEB_ROOT", env),
14445
+ pairingCode: setting("PAIRING_CODE", env2),
14446
+ pairingAttemptsPerMinute: setting("PAIRING_ATTEMPTS_PER_MINUTE", env2) === void 0 ? void 0 : Number(setting("PAIRING_ATTEMPTS_PER_MINUTE", env2)),
14447
+ origins: setting("ORIGINS", env2)?.split(",").filter(Boolean),
14448
+ webRoot: setting("WEB_ROOT", env2),
13442
14449
  ...invocation.keepAlive ? { idleShutdown: { afterMs: invocation.keepAlive, onIdle: () => stop2() } } : {}
13443
14450
  });
13444
14451
  let advertiser;
@@ -13456,16 +14463,16 @@ async function serveRuntime(invocation, target, dataDir, env) {
13456
14463
  startedAt: Date.now()
13457
14464
  };
13458
14465
  await writeRecord(dataDir, daemon);
13459
- const url = launchUrl(daemon, target.file);
14466
+ const url2 = launchUrl(daemon, target.file);
13460
14467
  process.stdout.write(
13461
14468
  (invocation.lan ? networkSummary(daemon) : `Oxbit runtime: http://${host}:${runtime.port}
13462
14469
  Workspace: ${runtime.root}
13463
14470
  Owner pairing code: ${runtime.pairingCode}
13464
14471
  Runtime id: ${runtime.identity.id}
13465
- `) + `Open: ${url}
14472
+ `) + `Open: ${url2}
13466
14473
  `
13467
14474
  );
13468
- if (invocation.open) launchBrowser(url);
14475
+ if (invocation.open) launchBrowser(url2);
13469
14476
  await new Promise((resolve) => {
13470
14477
  const shutdown = () => {
13471
14478
  void Promise.resolve(advertiser?.close()).then(() => runtime.close()).then(() => removeRecord(dataDir)).then(resolve, resolve);
@@ -13487,29 +14494,29 @@ Owner pairing code: ${daemon.pairingCode}
13487
14494
  Runtime id: ${daemon.id}
13488
14495
  `;
13489
14496
  }
13490
- async function detach(invocation, target, dataDir, entry) {
14497
+ async function detach(invocation, target, dataDir, entry2) {
13491
14498
  await fs.mkdir(dataDir, { recursive: true, mode: 448 });
13492
- const log = await fs.open(logFile(dataDir), "a", 384);
13493
- const args = [entry, "--foreground", "--no-open", ...invocation.lan ? ["--lan"] : ["--host", invocation.host]];
14499
+ const log2 = await fs.open(logFile(dataDir), "a", 384);
14500
+ const args = [entry2, "--foreground", "--no-open", ...invocation.lan ? ["--lan"] : ["--host", invocation.host]];
13494
14501
  if (invocation.port) args.push("--port", String(invocation.port));
13495
14502
  args.push("--keep-alive", invocation.keepAlive ? String(invocation.keepAlive) : "forever");
13496
14503
  args.push(target.root);
13497
- const env = { ...process.env, OXBIT_DATA_DIR: dataDir };
13498
- delete env.PORT;
13499
- delete env.HOST;
13500
- delete env.OXBIT_WORKSPACE;
13501
- delete env.OXBIT_KEEP_ALIVE;
14504
+ const env2 = { ...process.env, OXBIT_DATA_DIR: dataDir };
14505
+ delete env2.PORT;
14506
+ delete env2.HOST;
14507
+ delete env2.OXBIT_WORKSPACE;
14508
+ delete env2.OXBIT_KEEP_ALIVE;
13502
14509
  const child = spawn(process.execPath, args, {
13503
14510
  detached: true,
13504
- stdio: ["ignore", log.fd, log.fd],
13505
- env
14511
+ stdio: ["ignore", log2.fd, log2.fd],
14512
+ env: env2
13506
14513
  });
13507
14514
  let exit;
13508
14515
  child.on("exit", (code) => {
13509
14516
  exit = code ?? 1;
13510
14517
  });
13511
14518
  child.unref();
13512
- await log.close();
14519
+ await log2.close();
13513
14520
  const deadline = Date.now() + READY_TIMEOUT;
13514
14521
  while (Date.now() < deadline) {
13515
14522
  const daemon = await liveDaemon(dataDir);
@@ -13565,11 +14572,11 @@ Open: ${launchUrl(daemon)}
13565
14572
  }
13566
14573
  async function run(argv, context = {}) {
13567
14574
  const {
13568
- env = process.env,
14575
+ env: env2 = process.env,
13569
14576
  cwd = process.cwd(),
13570
- entry = fileURLToPath(import.meta.url)
14577
+ entry: entry2 = fileURLToPath(import.meta.url)
13571
14578
  } = context;
13572
- const invocation = parse(argv, env);
14579
+ const invocation = parse(argv, env2);
13573
14580
  if ("error" in invocation) {
13574
14581
  process.stderr.write(`${invocation.error}
13575
14582
 
@@ -13594,10 +14601,10 @@ ${USAGE}`);
13594
14601
  return 1;
13595
14602
  }
13596
14603
  if (invocation.action === "desktop") return launchDesktop(target.file ? path.join(target.root, target.file) : target.root);
13597
- const dataDir = dataDirFor(target.root, env);
14604
+ const dataDir = dataDirFor(target.root, env2);
13598
14605
  if (invocation.action === "stop") return stop(dataDir, target.root);
13599
14606
  if (invocation.action === "status") return status(dataDir, target.root);
13600
- if (invocation.foreground) return serve(invocation, target, dataDir, env);
14607
+ if (invocation.foreground) return serve(invocation, target, dataDir, env2);
13601
14608
  const existing = await liveDaemon(dataDir);
13602
14609
  if (existing && invocation.lan && !wildcardHost(existing.host)) {
13603
14610
  process.stderr.write(`A runtime already serves ${existing.root} on ${existing.host}. Run oxbit --stop, then oxbit --lan.
@@ -13608,18 +14615,18 @@ ${USAGE}`);
13608
14615
  if (existing) daemon = existing;
13609
14616
  else
13610
14617
  try {
13611
- daemon = await detach(invocation, target, dataDir, entry);
14618
+ daemon = await detach(invocation, target, dataDir, entry2);
13612
14619
  } catch (error) {
13613
14620
  process.stderr.write(`${error.message}
13614
14621
  `);
13615
14622
  return 1;
13616
14623
  }
13617
- const url = launchUrl(daemon, target.file);
13618
- process.stdout.write(invocation.lan ? `${networkSummary(daemon)}Open: ${url}
14624
+ const url2 = launchUrl(daemon, target.file);
14625
+ process.stdout.write(invocation.lan ? `${networkSummary(daemon)}Open: ${url2}
13619
14626
  ` : `${daemon.root}
13620
- ${url}
14627
+ ${url2}
13621
14628
  `);
13622
- if (invocation.open) launchBrowser(url);
14629
+ if (invocation.open) launchBrowser(url2);
13623
14630
  return 0;
13624
14631
  }
13625
14632
  function desktopCommand(target, platform = process.platform) {