@epoch-agent/server 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createServer } from 'http';
2
- import { t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_DEFAULTS, readRecording } from '@epoch-agent/runtime';
2
+ import { t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_DEFAULTS, readRecording } from '@epoch-agent/runtime';
3
3
  import { timingSafeEqual, randomBytes } from 'crypto';
4
- import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType } from '@epoch-agent/protocol';
4
+ import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType } from '@epoch-agent/protocol';
5
5
  import { resolve, join, relative, isAbsolute, sep, dirname, basename, extname } from 'path';
6
6
  import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, accessSync, constants } from 'fs';
7
7
  import { homedir } from 'os';
8
+ import { execFile, spawn } from 'child_process';
8
9
  import { stat, readFile } from 'fs/promises';
9
- import { execFile } from 'child_process';
10
10
  import { fileURLToPath } from 'url';
11
11
 
12
12
  // src/index.ts
@@ -714,6 +714,10 @@ function sendJson(res, status, body, extra = {}) {
714
714
  });
715
715
  res.end(payload);
716
716
  }
717
+ function sendEmpty(res, status, extra = {}) {
718
+ res.writeHead(status, { ...BASE_HEADERS, ...extra });
719
+ res.end();
720
+ }
717
721
  function sendError(res, status, code, message, extra = {}) {
718
722
  sendJson(res, status, { error: { code, message } }, extra);
719
723
  }
@@ -1325,7 +1329,9 @@ function toWireSettingRow(row) {
1325
1329
  layer: row.layer,
1326
1330
  chain: row.chain.map(toWireSettingStep),
1327
1331
  overridable: row.overridable,
1328
- writes: row.writes.map(toWireSettingWrite)
1332
+ writes: row.writes.map(toWireSettingWrite),
1333
+ ...row.valueKind === void 0 ? {} : { valueKind: row.valueKind },
1334
+ ...row.choices === void 0 ? {} : { choices: [...row.choices] }
1329
1335
  };
1330
1336
  }
1331
1337
  function toWireSettingStep(step) {
@@ -1442,6 +1448,7 @@ function config(ctx, res, lang) {
1442
1448
  compression: runtime.compression,
1443
1449
  ...runtime.config.budget?.maxCostUsd !== void 0 ? { maxCostUsd: runtime.config.budget.maxCostUsd } : {},
1444
1450
  ...runtime.config.brand !== void 0 ? { brand: runtime.config.brand } : {},
1451
+ ...runtime.config.sidebarMenu !== void 0 ? { sidebarMenu: runtime.config.sidebarMenu } : {},
1445
1452
  provider: runtime.providerInfo,
1446
1453
  tools: runtime.tools,
1447
1454
  diagnostics: (lang && runtime.diagnosticsIn?.(lang)) ?? runtime.diagnosticList,
@@ -1453,7 +1460,8 @@ function config(ctx, res, lang) {
1453
1460
  dbPath: runtime.config.dbPath,
1454
1461
  ...runtime.config.profile !== "default" ? { profile: runtime.config.profile } : {}
1455
1462
  },
1456
- lanExposed: ctx.lanExposed
1463
+ lanExposed: ctx.lanExposed,
1464
+ nativeDirPicker: ctx.nativeDirPicker
1457
1465
  });
1458
1466
  }
1459
1467
  function listSessions(ctx, res, query, _lang) {
@@ -1532,7 +1540,11 @@ function listMessages(ctx, res, sessionId, lang) {
1532
1540
  return sendUnknownSession(res, sessionId, lang);
1533
1541
  }
1534
1542
  const messages = ctx.runtime.sessionStore?.loadMessages(sessionId) ?? [];
1535
- sendJson(res, 200, { messages });
1543
+ const usage = ctx.runtime.sessionUsage(sessionId);
1544
+ sendJson(res, 200, {
1545
+ messages,
1546
+ ...usage ? { usage } : {}
1547
+ });
1536
1548
  }
1537
1549
  async function postMessage(ctx, req, res, sessionId, lang) {
1538
1550
  if (!ctx.hub.has(sessionId)) {
@@ -1683,13 +1695,13 @@ function getTools(ctx, res, sessionId, lang) {
1683
1695
  sendJson(res, 200, payload);
1684
1696
  }
1685
1697
  function getSettings(ctx, res, sessionId, lang) {
1686
- if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
1698
+ if (!ctx.hub.has(sessionId)) {
1687
1699
  return sendUnknownSession(res, sessionId, lang);
1688
1700
  }
1689
1701
  sendJson(res, 200, collectSettings(ctx.runtime, sessionId));
1690
1702
  }
1691
1703
  async function writeSetting(ctx, req, res, sessionId, lang) {
1692
- if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
1704
+ if (!ctx.hub.has(sessionId)) {
1693
1705
  return sendUnknownSession(res, sessionId, lang);
1694
1706
  }
1695
1707
  const body = await readJsonBody(req, void 0, lang);
@@ -2382,9 +2394,14 @@ async function setPermission(ctx, req, res, sessionId, lang) {
2382
2394
  );
2383
2395
  }
2384
2396
  const before = perms.level();
2385
- const result = perms.setLevel(level);
2397
+ const result = perms.setLevel(level, { remember: true });
2386
2398
  const state = permissionState(ctx, sessionId);
2387
- const payload = result.ok ? { ok: true, changed: state.level !== before, state } : { ok: false, refused: REFUSAL[result.reason], changed: false, state };
2399
+ const payload = result.ok ? {
2400
+ ok: true,
2401
+ changed: state.level !== before,
2402
+ state,
2403
+ ...result.notRemembered ? { notRemembered: result.notRemembered } : {}
2404
+ } : { ok: false, refused: REFUSAL[result.reason], changed: false, state };
2388
2405
  sendJson(res, 200, payload);
2389
2406
  }
2390
2407
  // src/plan.ts
@@ -2440,6 +2457,15 @@ function toWirePluginHit(hit, installedNames) {
2440
2457
  installed: installedNames.has(hit.entry.name)
2441
2458
  };
2442
2459
  }
2460
+ function toWireMarketplace(market) {
2461
+ return {
2462
+ name: market.name,
2463
+ source: market.source,
2464
+ entries: market.entries,
2465
+ addedAt: market.addedAt,
2466
+ local: market.local
2467
+ };
2468
+ }
2443
2469
  function toWirePreview2(preview) {
2444
2470
  const { manifest, source, inventory } = preview;
2445
2471
  return {
@@ -2492,6 +2518,7 @@ function listPlugins(ctx, res, lang) {
2492
2518
  sendJson(res, 200, {
2493
2519
  installed: installed.map(toWirePluginEntry),
2494
2520
  hits: control.search().map((hit) => toWirePluginHit(hit, installedNames)),
2521
+ marketplaces: control.marketplaces().map(toWireMarketplace),
2495
2522
  pendingRestart: control.pendingRestart
2496
2523
  });
2497
2524
  }
@@ -2521,6 +2548,14 @@ async function previewPluginInstall(ctx, req, res, lang) {
2521
2548
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2522
2549
  sendPreview(res, await control.preview(fieldOf(asRecord(body.value), "ref"), lang));
2523
2550
  }
2551
+ async function previewLocalPluginInstall(ctx, req, res, lang) {
2552
+ if (refusedByLan(ctx, res, lang)) return;
2553
+ const control = controlOf(ctx, res, lang);
2554
+ if (!control) return;
2555
+ const body = await readJsonBody(req);
2556
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2557
+ sendPreview(res, await control.previewLocal(fieldOf(asRecord(body.value), "path"), lang));
2558
+ }
2524
2559
  async function previewPluginUpdate(ctx, req, res, lang) {
2525
2560
  if (refusedByLan(ctx, res, lang)) return;
2526
2561
  const control = controlOf(ctx, res, lang);
@@ -2561,6 +2596,19 @@ async function installPluginFromMarket(ctx, req, res, lang) {
2561
2596
  await control.install(fieldOf(payload, "ref"), fieldOf(payload, "token"), lang)
2562
2597
  );
2563
2598
  }
2599
+ async function installLocalPlugin(ctx, req, res, lang) {
2600
+ if (refusedByLan(ctx, res, lang)) return;
2601
+ const control = controlOf(ctx, res, lang);
2602
+ if (!control) return;
2603
+ const body = await readJsonBody(req);
2604
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2605
+ const payload = asRecord(body.value);
2606
+ sendAction(
2607
+ res,
2608
+ control,
2609
+ await control.installLocal(fieldOf(payload, "path"), fieldOf(payload, "token"), lang)
2610
+ );
2611
+ }
2564
2612
  async function updateInstalledPlugin(ctx, req, res, lang) {
2565
2613
  if (refusedByLan(ctx, res, lang)) return;
2566
2614
  const control = controlOf(ctx, res, lang);
@@ -2582,12 +2630,37 @@ async function uninstallPluginByName(ctx, req, res, lang) {
2582
2630
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2583
2631
  sendAction(res, control, await control.uninstall(fieldOf(asRecord(body.value), "name"), lang));
2584
2632
  }
2633
+ function sendMarket(res, outcome) {
2634
+ sendJson(res, 200, {
2635
+ ok: outcome.ok,
2636
+ marketplace: outcome.ok && outcome.marketplace ? toWireMarketplace(outcome.marketplace) : null,
2637
+ reason: outcome.ok ? null : outcome.reason,
2638
+ detail: outcome.ok ? null : outcome.detail
2639
+ });
2640
+ }
2641
+ async function addPluginMarketplace(ctx, req, res, lang) {
2642
+ if (refusedByLan(ctx, res, lang)) return;
2643
+ const control = controlOf(ctx, res, lang);
2644
+ if (!control) return;
2645
+ const body = await readJsonBody(req);
2646
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2647
+ sendMarket(res, await control.addMarketplace(fieldOf(asRecord(body.value), "source"), lang));
2648
+ }
2649
+ async function removePluginMarketplace(ctx, req, res, lang) {
2650
+ if (refusedByLan(ctx, res, lang)) return;
2651
+ const control = controlOf(ctx, res, lang);
2652
+ if (!control) return;
2653
+ const body = await readJsonBody(req);
2654
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2655
+ sendMarket(res, await control.removeMarketplace(fieldOf(asRecord(body.value), "name"), lang));
2656
+ }
2585
2657
  function toWireProvider(option, lang) {
2586
2658
  return {
2587
2659
  type: option.type,
2588
2660
  label: providerLabel(option, lang),
2589
2661
  envVar: option.envVar,
2590
- hasKey: option.hasKey
2662
+ hasKey: option.hasKey,
2663
+ keyHint: option.keyHint
2591
2664
  };
2592
2665
  }
2593
2666
  function toWireSuggestions(view) {
@@ -2615,6 +2688,36 @@ async function discoverProviderModels(ctx, req, res, type, lang) {
2615
2688
  suggestions: toWireSuggestions(suggestions)
2616
2689
  });
2617
2690
  }
2691
+ async function setProviderKey(ctx, req, res, type, lang) {
2692
+ if (!isProviderType(type)) {
2693
+ return sendError(res, 404, "unknown-provider", t("web.provider_unknown", { name: type }, lang));
2694
+ }
2695
+ const envVar = apiKeyEnvVar(type);
2696
+ if (envVar === null) {
2697
+ return sendError(
2698
+ res,
2699
+ 400,
2700
+ "key-not-applicable",
2701
+ t("web.provider_key_not_applicable", { name: type }, lang)
2702
+ );
2703
+ }
2704
+ const body = await readJsonBody(req);
2705
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2706
+ const raw = asRecord(body.value)["apiKey"];
2707
+ const apiKey = typeof raw === "string" ? raw.trim() : "";
2708
+ if (apiKey === "") {
2709
+ return sendError(res, 400, "empty-key", t("web.provider_key_empty", void 0, lang));
2710
+ }
2711
+ const written = await ctx.runtime.modelCatalog.setKey(type, apiKey);
2712
+ sendJson(res, 200, {
2713
+ provider: type,
2714
+ hasKey: true,
2715
+ keyHint: written.keyHint,
2716
+ backend: written.backend,
2717
+ encrypted: written.encrypted,
2718
+ envPath: written.envPath
2719
+ });
2720
+ }
2618
2721
  async function addRole(ctx, req, res, lang) {
2619
2722
  if (ctx.lanExposed) {
2620
2723
  return sendError(
@@ -3014,7 +3117,7 @@ async function previewSkillImport(ctx, req, res, lang) {
3014
3117
  const body = await readJsonBody(req);
3015
3118
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3016
3119
  const source = pathOf(asRecord(body.value));
3017
- const outcome = await ctx.runtime.skillImport.preview(source, lang);
3120
+ const outcome = await ctx.runtime.skillWrite.preview(source, lang);
3018
3121
  if (!outcome.ok) {
3019
3122
  return sendJson(res, 200, {
3020
3123
  ok: false,
@@ -3045,7 +3148,7 @@ async function importSkills(ctx, req, res, lang) {
3045
3148
  const payload = asRecord(body.value);
3046
3149
  const source = pathOf(payload);
3047
3150
  const token = typeof payload["token"] === "string" ? payload["token"] : "";
3048
- const outcome = await ctx.runtime.skillImport.import(source, token, lang);
3151
+ const outcome = await ctx.runtime.skillWrite.import(source, token, lang);
3049
3152
  if (!outcome.ok) {
3050
3153
  return sendJson(res, 200, {
3051
3154
  ok: false,
@@ -3063,6 +3166,30 @@ async function importSkills(ctx, req, res, lang) {
3063
3166
  reason: null
3064
3167
  });
3065
3168
  }
3169
+ // src/skill-remove.ts
3170
+ async function removeSkill(ctx, req, res, lang) {
3171
+ const body = await readJsonBody(req);
3172
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3173
+ const payload = asRecord(body.value);
3174
+ const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
3175
+ const outcome = await ctx.runtime.skillWrite.remove(name, lang);
3176
+ if (!outcome.ok) {
3177
+ return sendJson(res, 200, {
3178
+ ok: false,
3179
+ name: "",
3180
+ path: null,
3181
+ detail: outcome.detail,
3182
+ reason: outcome.reason
3183
+ });
3184
+ }
3185
+ sendJson(res, 200, {
3186
+ ok: true,
3187
+ name: outcome.name,
3188
+ path: outcome.path,
3189
+ detail: null,
3190
+ reason: null
3191
+ });
3192
+ }
3066
3193
  // src/sse.ts
3067
3194
  var DEFAULT_HEARTBEAT_MS = 15e3;
3068
3195
  function parseLastEventId(req) {
@@ -3134,6 +3261,9 @@ function getTasks(ctx, res, sessionId, _lang, registry = LIVE_TASKS) {
3134
3261
  sendJson(res, 200, collectTasks(sessionId, registry));
3135
3262
  }
3136
3263
  var DIR_PAGE_LIMIT = 1e3;
3264
+ function workspaceHome() {
3265
+ return homedir();
3266
+ }
3137
3267
  function workspaceAnchors(workspaces) {
3138
3268
  const seen = /* @__PURE__ */ new Set();
3139
3269
  const entries = [];
@@ -3227,10 +3357,127 @@ function parentOf(path) {
3227
3357
  function displayName2(path) {
3228
3358
  return basename(path) || path;
3229
3359
  }
3360
+ function present(value) {
3361
+ return value !== void 0 && value !== "";
3362
+ }
3363
+ function resolveNativeDirPicker(facts) {
3364
+ return isSameMachine(facts);
3365
+ }
3366
+ function isSameMachine(facts) {
3367
+ if (facts.lanExposed) return false;
3368
+ if (present(facts.env.SSH_CONNECTION) || present(facts.env.SSH_TTY)) return false;
3369
+ return facts.platform === "darwin" || facts.platform === "win32";
3370
+ }
3371
+ function nativePickerCommands(platform, prompt) {
3372
+ if (platform === "darwin") {
3373
+ return [
3374
+ {
3375
+ command: "osascript",
3376
+ args: [
3377
+ "-e",
3378
+ `set epochChosenFolder to choose folder with prompt ${asAppleScriptString(prompt)}`,
3379
+ "-e",
3380
+ "POSIX path of epochChosenFolder"
3381
+ ]
3382
+ }
3383
+ ];
3384
+ }
3385
+ if (platform === "win32") {
3386
+ const script = win32PickScript(prompt);
3387
+ return [
3388
+ { command: "pwsh", args: ["-NoProfile", "-STA", "-Command", script] },
3389
+ { command: "powershell.exe", args: ["-NoProfile", "-STA", "-Command", script] }
3390
+ ];
3391
+ }
3392
+ return null;
3393
+ }
3394
+ function win32PickScript(prompt) {
3395
+ return [
3396
+ "Add-Type -AssemblyName System.Windows.Forms",
3397
+ "$d = New-Object System.Windows.Forms.FolderBrowserDialog",
3398
+ `$d.Description = ${asPowerShellString(prompt)}`,
3399
+ "$d.ShowNewFolderButton = $true",
3400
+ "if ($d.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { exit 1 }",
3401
+ "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($d.SelectedPath))"
3402
+ ].join("; ");
3403
+ }
3404
+ function asAppleScriptString(text) {
3405
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
3406
+ }
3407
+ function asPowerShellString(text) {
3408
+ return `'${text.replace(/'/g, "''")}'`;
3409
+ }
3410
+ function decodeNativePick(platform, stdout) {
3411
+ const raw = stdout.trim();
3412
+ if (platform !== "win32") return raw;
3413
+ return Buffer.from(raw, "base64").toString("utf-8").trim();
3414
+ }
3415
+ function isPickCanceled(platform, code, stderr) {
3416
+ if (code !== 1) return false;
3417
+ if (platform === "win32") return true;
3418
+ return /-128|user canceled/i.test(stderr);
3419
+ }
3420
+ var inFlight = false;
3421
+ async function pickNativeDirectory(opts) {
3422
+ const platform = opts.platform ?? process.platform;
3423
+ const candidates = nativePickerCommands(platform, opts.prompt);
3424
+ if (!candidates) {
3425
+ return { ok: false, reason: "unsupported", message: `unsupported platform: ${platform}` };
3426
+ }
3427
+ if (inFlight) {
3428
+ return { ok: false, reason: "busy", message: "a directory picker is already open" };
3429
+ }
3430
+ const run = opts.runner ?? execFileRunner;
3431
+ inFlight = true;
3432
+ try {
3433
+ let last = "";
3434
+ for (const [index, candidate] of candidates.entries()) {
3435
+ const attempt = await run(candidate.command, candidate.args, opts.signal);
3436
+ if (attempt.missing) {
3437
+ last = attempt.message || `${candidate.command} not found`;
3438
+ if (index < candidates.length - 1) continue;
3439
+ return { ok: false, reason: "failed", message: last };
3440
+ }
3441
+ if (!attempt.code) {
3442
+ const path = decodeNativePick(platform, attempt.stdout);
3443
+ return path ? { ok: true, path } : { ok: false, reason: "failed", message: "picker returned an empty path" };
3444
+ }
3445
+ if (isPickCanceled(platform, attempt.code, attempt.stderr)) {
3446
+ return { ok: true, canceled: true };
3447
+ }
3448
+ return { ok: false, reason: "failed", message: attempt.stderr.trim() || attempt.message };
3449
+ }
3450
+ return { ok: false, reason: "failed", message: last };
3451
+ } finally {
3452
+ inFlight = false;
3453
+ }
3454
+ }
3455
+ var execFileRunner = (command, args, signal) => new Promise((done) => {
3456
+ execFile(
3457
+ command,
3458
+ [...args],
3459
+ {
3460
+ ...signal ? { signal } : {},
3461
+ encoding: "utf-8",
3462
+ windowsHide: true,
3463
+ maxBuffer: 1024 * 1024
3464
+ },
3465
+ (error, stdout, stderr) => {
3466
+ const err = error;
3467
+ done({
3468
+ code: err?.code ?? 0,
3469
+ stdout,
3470
+ stderr,
3471
+ missing: err?.code === "ENOENT",
3472
+ message: err?.message ?? ""
3473
+ });
3474
+ }
3475
+ );
3476
+ });
3230
3477
  // src/workspace/dirs.ts
3231
3478
  function browseWorkspaceDirs(ctx, res, rawUrl, lang) {
3232
3479
  const query = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams;
3233
- const path = dirsQuery(query);
3480
+ const path = dirsQuery(query) ?? atQuery(query);
3234
3481
  if (path === void 0) {
3235
3482
  return sendJson(res, 200, workspaceAnchors(ctx.runtime.workspaces));
3236
3483
  }
@@ -3260,6 +3507,9 @@ function dirsQuery(query) {
3260
3507
  const value = query.get("path")?.trim();
3261
3508
  return value ? value : void 0;
3262
3509
  }
3510
+ function atQuery(query) {
3511
+ return query.get("at")?.trim().toLowerCase() === "home" ? workspaceHome() : void 0;
3512
+ }
3263
3513
  function hiddenQuery(query) {
3264
3514
  const value = query.get("hidden")?.trim().toLowerCase();
3265
3515
  return value === "1" || value === "true";
@@ -3297,6 +3547,34 @@ async function createWorkspaceDir(req, res, lang) {
3297
3547
  }
3298
3548
  });
3299
3549
  }
3550
+ async function pickWorkspaceDir(ctx, req, res, lang) {
3551
+ if (!ctx.nativeDirPicker) {
3552
+ return sendError(res, 403, "native-picker-unavailable", t("web.pick_unavailable", {}, lang));
3553
+ }
3554
+ const abort = new AbortController();
3555
+ req.on("close", () => abort.abort());
3556
+ const outcome = await pickNativeDirectory({
3557
+ prompt: t("web.pick_prompt", {}, lang),
3558
+ signal: abort.signal
3559
+ });
3560
+ if (res.writableEnded || abort.signal.aborted) return;
3561
+ if (outcome.ok) {
3562
+ return sendJson(
3563
+ res,
3564
+ 200,
3565
+ outcome.path === void 0 ? { canceled: true } : { path: outcome.path }
3566
+ );
3567
+ }
3568
+ if (outcome.reason === "busy") {
3569
+ return sendError(res, 409, "native-picker-busy", t("web.pick_busy", {}, lang));
3570
+ }
3571
+ sendError(
3572
+ res,
3573
+ 500,
3574
+ "native-picker-failed",
3575
+ t("web.pick_failed", { detail: outcome.message }, lang)
3576
+ );
3577
+ }
3300
3578
  function isSingleSegment(name) {
3301
3579
  if (name.length === 0) return false;
3302
3580
  if (name === "." || name === "..") return false;
@@ -3311,6 +3589,195 @@ function mkdirFailure(cause, path, lang) {
3311
3589
  if (code === "ENOENT") return [404, "not-found", t("web.mkdir_parent_missing", { path }, lang)];
3312
3590
  return [500, "internal", t("web.mkdir_failed", { path }, lang)];
3313
3591
  }
3592
+ var MAX_TEXT_BYTES = 2 * 1024 * 1024;
3593
+ var VIEWABLE = /* @__PURE__ */ new Map([
3594
+ [".png", { kind: "image", mime: "image/png" }],
3595
+ [".jpg", { kind: "image", mime: "image/jpeg" }],
3596
+ [".jpeg", { kind: "image", mime: "image/jpeg" }],
3597
+ [".gif", { kind: "image", mime: "image/gif" }],
3598
+ [".webp", { kind: "image", mime: "image/webp" }],
3599
+ [".bmp", { kind: "image", mime: "image/bmp" }],
3600
+ [".ico", { kind: "image", mime: "image/x-icon" }],
3601
+ [".avif", { kind: "image", mime: "image/avif" }],
3602
+ [".pdf", { kind: "pdf", mime: "application/pdf" }],
3603
+ [".mp3", { kind: "audio", mime: "audio/mpeg" }],
3604
+ [".wav", { kind: "audio", mime: "audio/wav" }],
3605
+ [".ogg", { kind: "audio", mime: "audio/ogg" }],
3606
+ [".m4a", { kind: "audio", mime: "audio/mp4" }],
3607
+ [".flac", { kind: "audio", mime: "audio/flac" }],
3608
+ [".mp4", { kind: "video", mime: "video/mp4" }],
3609
+ [".webm", { kind: "video", mime: "video/webm" }],
3610
+ [".mov", { kind: "video", mime: "video/quicktime" }]
3611
+ ]);
3612
+ function kindOf(stat2) {
3613
+ if (stat2.kind === "text") return "text";
3614
+ return VIEWABLE.get(extname(stat2.rel).toLowerCase())?.kind ?? "opaque";
3615
+ }
3616
+ function toWireRefusal(reason) {
3617
+ return reason;
3618
+ }
3619
+ function statusFor(refusal) {
3620
+ if (refusal === "denied") return 403;
3621
+ if (refusal === "missing") return 404;
3622
+ if (refusal === "too-large") return 413;
3623
+ return refusal === "not-a-file" ? 400 : 503;
3624
+ }
3625
+ function describeRefusal(refusal, lang) {
3626
+ if (refusal === "denied") return t("web.file_view_denied", void 0, lang);
3627
+ if (refusal === "missing") return t("web.file_view_missing", void 0, lang);
3628
+ if (refusal === "not-a-file") return t("web.file_view_not_a_file", void 0, lang);
3629
+ if (refusal === "too-large") {
3630
+ return t("web.file_view_too_large", { max: String(MAX_TEXT_BYTES) }, lang);
3631
+ }
3632
+ return t("web.file_view_unreadable", void 0, lang);
3633
+ }
3634
+ function resolveViewRoot(ctx, res, sessionId, lang) {
3635
+ if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
3636
+ sendUnknownSession(res, sessionId);
3637
+ return null;
3638
+ }
3639
+ const root = workspaceRootOf(ctx, sessionId);
3640
+ if (!root) {
3641
+ sendError(res, 409, "no-workspace", t("web.file_view_no_workspace", void 0, lang));
3642
+ return null;
3643
+ }
3644
+ return root;
3645
+ }
3646
+ function singlePath(res, rawUrl, lang) {
3647
+ const params = new URLSearchParams((rawUrl ?? "").split("?")[1] ?? "");
3648
+ const path = params.get("path")?.trim();
3649
+ if (!path) {
3650
+ sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
3651
+ return null;
3652
+ }
3653
+ return path;
3654
+ }
3655
+ function statFiles(ctx, res, sessionId, rawUrl, lang) {
3656
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3657
+ if (root === null) return;
3658
+ const params = new URLSearchParams((rawUrl ?? "").split("?")[1] ?? "");
3659
+ const asked = params.getAll("path").filter((p) => p.trim() !== "");
3660
+ const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
3661
+ const files = taken.map((path) => {
3662
+ const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3663
+ if (!stat2.ok) return { path, ok: false, refusal: toWireRefusal(stat2.reason) };
3664
+ return {
3665
+ path,
3666
+ ok: true,
3667
+ kind: kindOf(stat2),
3668
+ bytes: stat2.bytes,
3669
+ mtimeMs: stat2.mtimeMs,
3670
+ openable: ctx.sameMachine && isSystemOpenable(stat2.rel)
3671
+ };
3672
+ });
3673
+ sendJson(res, 200, {
3674
+ files,
3675
+ overLimit: asked.length - taken.length,
3676
+ root
3677
+ });
3678
+ }
3679
+ function readFileText(ctx, res, sessionId, rawUrl, lang) {
3680
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3681
+ if (root === null) return;
3682
+ const path = singlePath(res, rawUrl, lang);
3683
+ if (path === null) return;
3684
+ const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3685
+ if (!stat2.ok) {
3686
+ const refusal = toWireRefusal(stat2.reason);
3687
+ return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3688
+ }
3689
+ if (stat2.kind !== "text") {
3690
+ return sendError(res, 415, "not-text", t("web.file_view_not_text", void 0, lang));
3691
+ }
3692
+ if (stat2.bytes > MAX_TEXT_BYTES) {
3693
+ return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
3694
+ }
3695
+ const out = ctx.runtime.workspaceFiles.readFile(stat2.rel, root);
3696
+ if (!out.ok) {
3697
+ const refusal = out.reason === "binary" ? "unreadable" : out.reason;
3698
+ return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3699
+ }
3700
+ sendJson(res, 200, {
3701
+ path,
3702
+ text: out.text,
3703
+ bytes: out.bytes
3704
+ });
3705
+ }
3706
+ function readFileBytes(ctx, res, sessionId, rawUrl, headOnly, lang) {
3707
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3708
+ if (root === null) return;
3709
+ const path = singlePath(res, rawUrl, lang);
3710
+ if (path === null) return;
3711
+ const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3712
+ if (!stat2.ok) {
3713
+ const refusal = toWireRefusal(stat2.reason);
3714
+ return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3715
+ }
3716
+ const known = VIEWABLE.get(extname(stat2.rel).toLowerCase());
3717
+ const inline = known !== void 0 && stat2.kind === "binary";
3718
+ const headers = {
3719
+ "Content-Type": inline ? known.mime : "application/octet-stream",
3720
+ "X-Content-Type-Options": "nosniff",
3721
+ "Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat2.rel))}`
3722
+ };
3723
+ if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
3724
+ if (headOnly) {
3725
+ return sendEmpty(res, 200, {
3726
+ ...headers,
3727
+ "Content-Length": String(stat2.bytes)
3728
+ });
3729
+ }
3730
+ sendFile(res, stat2.absPath, headers);
3731
+ }
3732
+ function basenameOf(rel) {
3733
+ const cut = rel.lastIndexOf("/");
3734
+ return cut < 0 ? rel : rel.slice(cut + 1);
3735
+ }
3736
+ // src/workspace/file-open.ts
3737
+ function describeOpenRefusal(refusal, lang) {
3738
+ if (refusal === "extension-not-allowed") {
3739
+ return t("web.file_open_extension_not_allowed", void 0, lang);
3740
+ }
3741
+ if (refusal === "not-same-machine") return t("web.file_open_not_same_machine", void 0, lang);
3742
+ if (refusal === "launch-failed") return t("web.file_open_launch_failed", void 0, lang);
3743
+ return describeRefusal(refusal, lang);
3744
+ }
3745
+ function refuse(res, refusal, lang) {
3746
+ sendJson(res, 200, {
3747
+ ok: false,
3748
+ refusal,
3749
+ message: describeOpenRefusal(refusal, lang)
3750
+ });
3751
+ }
3752
+ async function openFileWithSystemApp(ctx, req, res, sessionId, lang) {
3753
+ if (!ctx.sameMachine) {
3754
+ return sendError(res, 403, "not-same-machine", describeOpenRefusal("not-same-machine", lang));
3755
+ }
3756
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3757
+ if (root === null) return;
3758
+ const body = await readJsonBody(req);
3759
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3760
+ const payload = asRecord(body.value);
3761
+ const path = typeof payload["path"] === "string" ? payload["path"].trim() : "";
3762
+ if (path === "") {
3763
+ return sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
3764
+ }
3765
+ const plan = ctx.runtime.workspaceFiles.planSystemOpen(path, root);
3766
+ if (!plan.ok) return refuse(res, plan.reason, lang);
3767
+ try {
3768
+ const child = spawn(plan.command, [...plan.args], { stdio: "ignore", detached: true });
3769
+ await new Promise((done, fail) => {
3770
+ child.on("error", fail);
3771
+ child.on("spawn", () => {
3772
+ child.unref();
3773
+ done();
3774
+ });
3775
+ });
3776
+ } catch {
3777
+ return refuse(res, "launch-failed", lang);
3778
+ }
3779
+ sendJson(res, 200, { ok: true });
3780
+ }
3314
3781
  var GIT_TIMEOUT_MS = 1e4;
3315
3782
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
3316
3783
  function runGit(cwd, args) {
@@ -3765,6 +4232,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
3765
4232
  if (resource === "skills" && first === "import" && second === void 0 && method === "POST") {
3766
4233
  return importSkills(ctx, req, res, lang);
3767
4234
  }
4235
+ if (resource === "skills" && first === "remove" && second === void 0 && method === "POST") {
4236
+ return removeSkill(ctx, req, res, lang);
4237
+ }
3768
4238
  if (resource === "plugins" && first === void 0 && isRead(method)) {
3769
4239
  return listPlugins(ctx, res, lang);
3770
4240
  }
@@ -3774,6 +4244,12 @@ async function handleApi(opts, req, res, method, segments, lang) {
3774
4244
  if (resource === "plugins" && first === "install" && second === void 0 && method === "POST") {
3775
4245
  return installPluginFromMarket(ctx, req, res, lang);
3776
4246
  }
4247
+ if (resource === "plugins" && first === "install" && second === "local" && detail === "preview" && method === "POST") {
4248
+ return previewLocalPluginInstall(ctx, req, res, lang);
4249
+ }
4250
+ if (resource === "plugins" && first === "install" && second === "local" && detail === void 0 && method === "POST") {
4251
+ return installLocalPlugin(ctx, req, res, lang);
4252
+ }
3777
4253
  if (resource === "plugins" && first === "update" && second === "preview" && method === "POST") {
3778
4254
  return previewPluginUpdate(ctx, req, res, lang);
3779
4255
  }
@@ -3783,18 +4259,30 @@ async function handleApi(opts, req, res, method, segments, lang) {
3783
4259
  if (resource === "plugins" && first === "uninstall" && second === void 0 && method === "POST") {
3784
4260
  return uninstallPluginByName(ctx, req, res, lang);
3785
4261
  }
4262
+ if (resource === "plugins" && first === "marketplaces" && second === "remove" && method === "POST") {
4263
+ return removePluginMarketplace(ctx, req, res, lang);
4264
+ }
4265
+ if (resource === "plugins" && first === "marketplaces" && second === void 0 && method === "POST") {
4266
+ return addPluginMarketplace(ctx, req, res, lang);
4267
+ }
3786
4268
  if (resource === "providers" && first === void 0 && isRead(method)) {
3787
4269
  return listProviders(ctx, res, lang);
3788
4270
  }
3789
4271
  if (resource === "providers" && first !== void 0 && second === "models" && method === "POST") {
3790
4272
  return discoverProviderModels(ctx, req, res, first, lang);
3791
4273
  }
4274
+ if (resource === "providers" && first !== void 0 && second === "key" && method === "POST") {
4275
+ return setProviderKey(ctx, req, res, first, lang);
4276
+ }
3792
4277
  if (resource === "workspaces" && first === "dirs" && isRead(method)) {
3793
4278
  return browseWorkspaceDirs(ctx, res, req.url, lang);
3794
4279
  }
3795
4280
  if (resource === "workspaces" && first === void 0 && method === "POST") {
3796
4281
  return createWorkspaceDir(req, res, lang);
3797
4282
  }
4283
+ if (resource === "workspaces" && first === "pick" && method === "POST") {
4284
+ return pickWorkspaceDir(ctx, req, res, lang);
4285
+ }
3798
4286
  if (resource === "schedules") {
3799
4287
  return handleSchedules(ctx, req, res, method, first, second, detail, tail, lang);
3800
4288
  }
@@ -3862,6 +4350,18 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
3862
4350
  if (action === "file-candidates" && isRead(method)) {
3863
4351
  return listFileCandidates(ctx, res, sessionId, req.url, lang);
3864
4352
  }
4353
+ if (action === "file-stat" && isRead(method)) {
4354
+ return statFiles(ctx, res, sessionId, req.url, lang);
4355
+ }
4356
+ if (action === "file" && isRead(method)) {
4357
+ return readFileText(ctx, res, sessionId, req.url, lang);
4358
+ }
4359
+ if (action === "file-bytes" && isRead(method)) {
4360
+ return readFileBytes(ctx, res, sessionId, req.url, method === "HEAD", lang);
4361
+ }
4362
+ if (action === "file-open" && method === "POST") {
4363
+ return openFileWithSystemApp(ctx, req, res, sessionId, lang);
4364
+ }
3865
4365
  if (action === "approvals" && isRead(method)) return listApprovals(ctx, res, sessionId, lang);
3866
4366
  if (action === "questions" && isRead(method)) return listQuestions(ctx, res, sessionId, lang);
3867
4367
  if (action === "capabilities" && isRead(method))
@@ -4016,13 +4516,20 @@ async function createWebServer(opts) {
4016
4516
  };
4017
4517
  }
4018
4518
  function buildHandler(opts, hub, binding, port, webRoot) {
4519
+ const machineFacts = {
4520
+ lanExposed: binding.lanExposed,
4521
+ platform: process.platform,
4522
+ env: process.env
4523
+ };
4019
4524
  return createRequestHandler({
4020
4525
  ctx: {
4021
4526
  hub,
4022
4527
  runtime: opts.runtime,
4023
4528
  version: opts.version,
4024
4529
  artifactsRoot: opts.artifactsRoot ?? opts.runtime.artifactsRoot,
4025
- lanExposed: binding.lanExposed
4530
+ lanExposed: binding.lanExposed,
4531
+ nativeDirPicker: resolveNativeDirPicker(machineFacts),
4532
+ sameMachine: isSameMachine(machineFacts)
4026
4533
  },
4027
4534
  guard: createAuthGuard({ token: binding.token, port, lanExposed: binding.lanExposed }),
4028
4535
  ...webRoot === void 0 ? {} : { webRoot },
@@ -4068,4 +4575,4 @@ function makeCloser(server, hub, runtime) {
4068
4575
  };
4069
4576
  }
4070
4577
 
4071
- export { DEFAULT_MAX_ACTIVE_SESSIONS, DEFAULT_MAX_QUEUED_MESSAGES, DEFAULT_RING_CAPACITY, DEFAULT_WEB_HOST, DEFAULT_WEB_PORT, EnvelopeRing, MAX_CANDIDATES, MAX_DIFF_BYTES, MAX_DIFF_FILES, MAX_RECORDING_FRAMES, MAX_SKILL_BODY_BYTES, SessionHub, TASK_TAIL_BYTES, UI_LANG_PARAM, UI_THEME_PARAM, attachMentions, collectCapabilities, collectSecurity, collectSettings, collectTasks, collectWorkspaceDiff, createAuthGuard, createWebServer, decideBinding, defaultWebRoot, expandUserContent, firstScreenUrl, generateToken, listFileCandidates, listReferenceCandidates, readRewindInput, resolveArtifactPath, toWireCheckpoint, toWireCommand, toWirePluginEntry, toWirePreview, toWireRewindResult, toWireRow, toWireSkillBody, toWireTask, unavailableToHttp };
4578
+ export { DEFAULT_MAX_ACTIVE_SESSIONS, DEFAULT_MAX_QUEUED_MESSAGES, DEFAULT_RING_CAPACITY, DEFAULT_WEB_HOST, DEFAULT_WEB_PORT, EnvelopeRing, MAX_CANDIDATES, MAX_DIFF_BYTES, MAX_DIFF_FILES, MAX_RECORDING_FRAMES, MAX_SKILL_BODY_BYTES, SessionHub, TASK_TAIL_BYTES, UI_LANG_PARAM, UI_THEME_PARAM, attachMentions, collectCapabilities, collectSecurity, collectSettings, collectTasks, collectWorkspaceDiff, createAuthGuard, createWebServer, decideBinding, defaultWebRoot, expandUserContent, firstScreenUrl, generateToken, isSameMachine, listFileCandidates, listReferenceCandidates, readRewindInput, resolveArtifactPath, resolveNativeDirPicker, toWireCheckpoint, toWireCommand, toWirePluginEntry, toWirePreview, toWireRewindResult, toWireRow, toWireSkillBody, toWireTask, unavailableToHttp };