@epoch-agent/server 0.2.0 → 0.3.1

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) {
@@ -1683,13 +1691,13 @@ function getTools(ctx, res, sessionId, lang) {
1683
1691
  sendJson(res, 200, payload);
1684
1692
  }
1685
1693
  function getSettings(ctx, res, sessionId, lang) {
1686
- if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
1694
+ if (!ctx.hub.has(sessionId)) {
1687
1695
  return sendUnknownSession(res, sessionId, lang);
1688
1696
  }
1689
1697
  sendJson(res, 200, collectSettings(ctx.runtime, sessionId));
1690
1698
  }
1691
1699
  async function writeSetting(ctx, req, res, sessionId, lang) {
1692
- if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
1700
+ if (!ctx.hub.has(sessionId)) {
1693
1701
  return sendUnknownSession(res, sessionId, lang);
1694
1702
  }
1695
1703
  const body = await readJsonBody(req, void 0, lang);
@@ -2382,9 +2390,14 @@ async function setPermission(ctx, req, res, sessionId, lang) {
2382
2390
  );
2383
2391
  }
2384
2392
  const before = perms.level();
2385
- const result = perms.setLevel(level);
2393
+ const result = perms.setLevel(level, { remember: true });
2386
2394
  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 };
2395
+ const payload = result.ok ? {
2396
+ ok: true,
2397
+ changed: state.level !== before,
2398
+ state,
2399
+ ...result.notRemembered ? { notRemembered: result.notRemembered } : {}
2400
+ } : { ok: false, refused: REFUSAL[result.reason], changed: false, state };
2388
2401
  sendJson(res, 200, payload);
2389
2402
  }
2390
2403
  // src/plan.ts
@@ -2587,7 +2600,8 @@ function toWireProvider(option, lang) {
2587
2600
  type: option.type,
2588
2601
  label: providerLabel(option, lang),
2589
2602
  envVar: option.envVar,
2590
- hasKey: option.hasKey
2603
+ hasKey: option.hasKey,
2604
+ keyHint: option.keyHint
2591
2605
  };
2592
2606
  }
2593
2607
  function toWireSuggestions(view) {
@@ -2615,6 +2629,36 @@ async function discoverProviderModels(ctx, req, res, type, lang) {
2615
2629
  suggestions: toWireSuggestions(suggestions)
2616
2630
  });
2617
2631
  }
2632
+ async function setProviderKey(ctx, req, res, type, lang) {
2633
+ if (!isProviderType(type)) {
2634
+ return sendError(res, 404, "unknown-provider", t("web.provider_unknown", { name: type }, lang));
2635
+ }
2636
+ const envVar = apiKeyEnvVar(type);
2637
+ if (envVar === null) {
2638
+ return sendError(
2639
+ res,
2640
+ 400,
2641
+ "key-not-applicable",
2642
+ t("web.provider_key_not_applicable", { name: type }, lang)
2643
+ );
2644
+ }
2645
+ const body = await readJsonBody(req);
2646
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2647
+ const raw = asRecord(body.value)["apiKey"];
2648
+ const apiKey = typeof raw === "string" ? raw.trim() : "";
2649
+ if (apiKey === "") {
2650
+ return sendError(res, 400, "empty-key", t("web.provider_key_empty", void 0, lang));
2651
+ }
2652
+ const written = await ctx.runtime.modelCatalog.setKey(type, apiKey);
2653
+ sendJson(res, 200, {
2654
+ provider: type,
2655
+ hasKey: true,
2656
+ keyHint: written.keyHint,
2657
+ backend: written.backend,
2658
+ encrypted: written.encrypted,
2659
+ envPath: written.envPath
2660
+ });
2661
+ }
2618
2662
  async function addRole(ctx, req, res, lang) {
2619
2663
  if (ctx.lanExposed) {
2620
2664
  return sendError(
@@ -3014,7 +3058,7 @@ async function previewSkillImport(ctx, req, res, lang) {
3014
3058
  const body = await readJsonBody(req);
3015
3059
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3016
3060
  const source = pathOf(asRecord(body.value));
3017
- const outcome = await ctx.runtime.skillImport.preview(source, lang);
3061
+ const outcome = await ctx.runtime.skillWrite.preview(source, lang);
3018
3062
  if (!outcome.ok) {
3019
3063
  return sendJson(res, 200, {
3020
3064
  ok: false,
@@ -3045,7 +3089,7 @@ async function importSkills(ctx, req, res, lang) {
3045
3089
  const payload = asRecord(body.value);
3046
3090
  const source = pathOf(payload);
3047
3091
  const token = typeof payload["token"] === "string" ? payload["token"] : "";
3048
- const outcome = await ctx.runtime.skillImport.import(source, token, lang);
3092
+ const outcome = await ctx.runtime.skillWrite.import(source, token, lang);
3049
3093
  if (!outcome.ok) {
3050
3094
  return sendJson(res, 200, {
3051
3095
  ok: false,
@@ -3063,6 +3107,30 @@ async function importSkills(ctx, req, res, lang) {
3063
3107
  reason: null
3064
3108
  });
3065
3109
  }
3110
+ // src/skill-remove.ts
3111
+ async function removeSkill(ctx, req, res, lang) {
3112
+ const body = await readJsonBody(req);
3113
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3114
+ const payload = asRecord(body.value);
3115
+ const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
3116
+ const outcome = await ctx.runtime.skillWrite.remove(name, lang);
3117
+ if (!outcome.ok) {
3118
+ return sendJson(res, 200, {
3119
+ ok: false,
3120
+ name: "",
3121
+ path: null,
3122
+ detail: outcome.detail,
3123
+ reason: outcome.reason
3124
+ });
3125
+ }
3126
+ sendJson(res, 200, {
3127
+ ok: true,
3128
+ name: outcome.name,
3129
+ path: outcome.path,
3130
+ detail: null,
3131
+ reason: null
3132
+ });
3133
+ }
3066
3134
  // src/sse.ts
3067
3135
  var DEFAULT_HEARTBEAT_MS = 15e3;
3068
3136
  function parseLastEventId(req) {
@@ -3134,6 +3202,9 @@ function getTasks(ctx, res, sessionId, _lang, registry = LIVE_TASKS) {
3134
3202
  sendJson(res, 200, collectTasks(sessionId, registry));
3135
3203
  }
3136
3204
  var DIR_PAGE_LIMIT = 1e3;
3205
+ function workspaceHome() {
3206
+ return homedir();
3207
+ }
3137
3208
  function workspaceAnchors(workspaces) {
3138
3209
  const seen = /* @__PURE__ */ new Set();
3139
3210
  const entries = [];
@@ -3227,10 +3298,127 @@ function parentOf(path) {
3227
3298
  function displayName2(path) {
3228
3299
  return basename(path) || path;
3229
3300
  }
3301
+ function present(value) {
3302
+ return value !== void 0 && value !== "";
3303
+ }
3304
+ function resolveNativeDirPicker(facts) {
3305
+ return isSameMachine(facts);
3306
+ }
3307
+ function isSameMachine(facts) {
3308
+ if (facts.lanExposed) return false;
3309
+ if (present(facts.env.SSH_CONNECTION) || present(facts.env.SSH_TTY)) return false;
3310
+ return facts.platform === "darwin" || facts.platform === "win32";
3311
+ }
3312
+ function nativePickerCommands(platform, prompt) {
3313
+ if (platform === "darwin") {
3314
+ return [
3315
+ {
3316
+ command: "osascript",
3317
+ args: [
3318
+ "-e",
3319
+ `set epochChosenFolder to choose folder with prompt ${asAppleScriptString(prompt)}`,
3320
+ "-e",
3321
+ "POSIX path of epochChosenFolder"
3322
+ ]
3323
+ }
3324
+ ];
3325
+ }
3326
+ if (platform === "win32") {
3327
+ const script = win32PickScript(prompt);
3328
+ return [
3329
+ { command: "pwsh", args: ["-NoProfile", "-STA", "-Command", script] },
3330
+ { command: "powershell.exe", args: ["-NoProfile", "-STA", "-Command", script] }
3331
+ ];
3332
+ }
3333
+ return null;
3334
+ }
3335
+ function win32PickScript(prompt) {
3336
+ return [
3337
+ "Add-Type -AssemblyName System.Windows.Forms",
3338
+ "$d = New-Object System.Windows.Forms.FolderBrowserDialog",
3339
+ `$d.Description = ${asPowerShellString(prompt)}`,
3340
+ "$d.ShowNewFolderButton = $true",
3341
+ "if ($d.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { exit 1 }",
3342
+ "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($d.SelectedPath))"
3343
+ ].join("; ");
3344
+ }
3345
+ function asAppleScriptString(text) {
3346
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
3347
+ }
3348
+ function asPowerShellString(text) {
3349
+ return `'${text.replace(/'/g, "''")}'`;
3350
+ }
3351
+ function decodeNativePick(platform, stdout) {
3352
+ const raw = stdout.trim();
3353
+ if (platform !== "win32") return raw;
3354
+ return Buffer.from(raw, "base64").toString("utf-8").trim();
3355
+ }
3356
+ function isPickCanceled(platform, code, stderr) {
3357
+ if (code !== 1) return false;
3358
+ if (platform === "win32") return true;
3359
+ return /-128|user canceled/i.test(stderr);
3360
+ }
3361
+ var inFlight = false;
3362
+ async function pickNativeDirectory(opts) {
3363
+ const platform = opts.platform ?? process.platform;
3364
+ const candidates = nativePickerCommands(platform, opts.prompt);
3365
+ if (!candidates) {
3366
+ return { ok: false, reason: "unsupported", message: `unsupported platform: ${platform}` };
3367
+ }
3368
+ if (inFlight) {
3369
+ return { ok: false, reason: "busy", message: "a directory picker is already open" };
3370
+ }
3371
+ const run = opts.runner ?? execFileRunner;
3372
+ inFlight = true;
3373
+ try {
3374
+ let last = "";
3375
+ for (const [index, candidate] of candidates.entries()) {
3376
+ const attempt = await run(candidate.command, candidate.args, opts.signal);
3377
+ if (attempt.missing) {
3378
+ last = attempt.message || `${candidate.command} not found`;
3379
+ if (index < candidates.length - 1) continue;
3380
+ return { ok: false, reason: "failed", message: last };
3381
+ }
3382
+ if (!attempt.code) {
3383
+ const path = decodeNativePick(platform, attempt.stdout);
3384
+ return path ? { ok: true, path } : { ok: false, reason: "failed", message: "picker returned an empty path" };
3385
+ }
3386
+ if (isPickCanceled(platform, attempt.code, attempt.stderr)) {
3387
+ return { ok: true, canceled: true };
3388
+ }
3389
+ return { ok: false, reason: "failed", message: attempt.stderr.trim() || attempt.message };
3390
+ }
3391
+ return { ok: false, reason: "failed", message: last };
3392
+ } finally {
3393
+ inFlight = false;
3394
+ }
3395
+ }
3396
+ var execFileRunner = (command, args, signal) => new Promise((done) => {
3397
+ execFile(
3398
+ command,
3399
+ [...args],
3400
+ {
3401
+ ...signal ? { signal } : {},
3402
+ encoding: "utf-8",
3403
+ windowsHide: true,
3404
+ maxBuffer: 1024 * 1024
3405
+ },
3406
+ (error, stdout, stderr) => {
3407
+ const err = error;
3408
+ done({
3409
+ code: err?.code ?? 0,
3410
+ stdout,
3411
+ stderr,
3412
+ missing: err?.code === "ENOENT",
3413
+ message: err?.message ?? ""
3414
+ });
3415
+ }
3416
+ );
3417
+ });
3230
3418
  // src/workspace/dirs.ts
3231
3419
  function browseWorkspaceDirs(ctx, res, rawUrl, lang) {
3232
3420
  const query = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams;
3233
- const path = dirsQuery(query);
3421
+ const path = dirsQuery(query) ?? atQuery(query);
3234
3422
  if (path === void 0) {
3235
3423
  return sendJson(res, 200, workspaceAnchors(ctx.runtime.workspaces));
3236
3424
  }
@@ -3260,6 +3448,9 @@ function dirsQuery(query) {
3260
3448
  const value = query.get("path")?.trim();
3261
3449
  return value ? value : void 0;
3262
3450
  }
3451
+ function atQuery(query) {
3452
+ return query.get("at")?.trim().toLowerCase() === "home" ? workspaceHome() : void 0;
3453
+ }
3263
3454
  function hiddenQuery(query) {
3264
3455
  const value = query.get("hidden")?.trim().toLowerCase();
3265
3456
  return value === "1" || value === "true";
@@ -3297,6 +3488,34 @@ async function createWorkspaceDir(req, res, lang) {
3297
3488
  }
3298
3489
  });
3299
3490
  }
3491
+ async function pickWorkspaceDir(ctx, req, res, lang) {
3492
+ if (!ctx.nativeDirPicker) {
3493
+ return sendError(res, 403, "native-picker-unavailable", t("web.pick_unavailable", {}, lang));
3494
+ }
3495
+ const abort = new AbortController();
3496
+ req.on("close", () => abort.abort());
3497
+ const outcome = await pickNativeDirectory({
3498
+ prompt: t("web.pick_prompt", {}, lang),
3499
+ signal: abort.signal
3500
+ });
3501
+ if (res.writableEnded || abort.signal.aborted) return;
3502
+ if (outcome.ok) {
3503
+ return sendJson(
3504
+ res,
3505
+ 200,
3506
+ outcome.path === void 0 ? { canceled: true } : { path: outcome.path }
3507
+ );
3508
+ }
3509
+ if (outcome.reason === "busy") {
3510
+ return sendError(res, 409, "native-picker-busy", t("web.pick_busy", {}, lang));
3511
+ }
3512
+ sendError(
3513
+ res,
3514
+ 500,
3515
+ "native-picker-failed",
3516
+ t("web.pick_failed", { detail: outcome.message }, lang)
3517
+ );
3518
+ }
3300
3519
  function isSingleSegment(name) {
3301
3520
  if (name.length === 0) return false;
3302
3521
  if (name === "." || name === "..") return false;
@@ -3311,6 +3530,195 @@ function mkdirFailure(cause, path, lang) {
3311
3530
  if (code === "ENOENT") return [404, "not-found", t("web.mkdir_parent_missing", { path }, lang)];
3312
3531
  return [500, "internal", t("web.mkdir_failed", { path }, lang)];
3313
3532
  }
3533
+ var MAX_TEXT_BYTES = 2 * 1024 * 1024;
3534
+ var VIEWABLE = /* @__PURE__ */ new Map([
3535
+ [".png", { kind: "image", mime: "image/png" }],
3536
+ [".jpg", { kind: "image", mime: "image/jpeg" }],
3537
+ [".jpeg", { kind: "image", mime: "image/jpeg" }],
3538
+ [".gif", { kind: "image", mime: "image/gif" }],
3539
+ [".webp", { kind: "image", mime: "image/webp" }],
3540
+ [".bmp", { kind: "image", mime: "image/bmp" }],
3541
+ [".ico", { kind: "image", mime: "image/x-icon" }],
3542
+ [".avif", { kind: "image", mime: "image/avif" }],
3543
+ [".pdf", { kind: "pdf", mime: "application/pdf" }],
3544
+ [".mp3", { kind: "audio", mime: "audio/mpeg" }],
3545
+ [".wav", { kind: "audio", mime: "audio/wav" }],
3546
+ [".ogg", { kind: "audio", mime: "audio/ogg" }],
3547
+ [".m4a", { kind: "audio", mime: "audio/mp4" }],
3548
+ [".flac", { kind: "audio", mime: "audio/flac" }],
3549
+ [".mp4", { kind: "video", mime: "video/mp4" }],
3550
+ [".webm", { kind: "video", mime: "video/webm" }],
3551
+ [".mov", { kind: "video", mime: "video/quicktime" }]
3552
+ ]);
3553
+ function kindOf(stat2) {
3554
+ if (stat2.kind === "text") return "text";
3555
+ return VIEWABLE.get(extname(stat2.rel).toLowerCase())?.kind ?? "opaque";
3556
+ }
3557
+ function toWireRefusal(reason) {
3558
+ return reason;
3559
+ }
3560
+ function statusFor(refusal) {
3561
+ if (refusal === "denied") return 403;
3562
+ if (refusal === "missing") return 404;
3563
+ if (refusal === "too-large") return 413;
3564
+ return refusal === "not-a-file" ? 400 : 503;
3565
+ }
3566
+ function describeRefusal(refusal, lang) {
3567
+ if (refusal === "denied") return t("web.file_view_denied", void 0, lang);
3568
+ if (refusal === "missing") return t("web.file_view_missing", void 0, lang);
3569
+ if (refusal === "not-a-file") return t("web.file_view_not_a_file", void 0, lang);
3570
+ if (refusal === "too-large") {
3571
+ return t("web.file_view_too_large", { max: String(MAX_TEXT_BYTES) }, lang);
3572
+ }
3573
+ return t("web.file_view_unreadable", void 0, lang);
3574
+ }
3575
+ function resolveViewRoot(ctx, res, sessionId, lang) {
3576
+ if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
3577
+ sendUnknownSession(res, sessionId);
3578
+ return null;
3579
+ }
3580
+ const root = workspaceRootOf(ctx, sessionId);
3581
+ if (!root) {
3582
+ sendError(res, 409, "no-workspace", t("web.file_view_no_workspace", void 0, lang));
3583
+ return null;
3584
+ }
3585
+ return root;
3586
+ }
3587
+ function singlePath(res, rawUrl, lang) {
3588
+ const params = new URLSearchParams((rawUrl ?? "").split("?")[1] ?? "");
3589
+ const path = params.get("path")?.trim();
3590
+ if (!path) {
3591
+ sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
3592
+ return null;
3593
+ }
3594
+ return path;
3595
+ }
3596
+ function statFiles(ctx, res, sessionId, rawUrl, lang) {
3597
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3598
+ if (root === null) return;
3599
+ const params = new URLSearchParams((rawUrl ?? "").split("?")[1] ?? "");
3600
+ const asked = params.getAll("path").filter((p) => p.trim() !== "");
3601
+ const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
3602
+ const files = taken.map((path) => {
3603
+ const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3604
+ if (!stat2.ok) return { path, ok: false, refusal: toWireRefusal(stat2.reason) };
3605
+ return {
3606
+ path,
3607
+ ok: true,
3608
+ kind: kindOf(stat2),
3609
+ bytes: stat2.bytes,
3610
+ mtimeMs: stat2.mtimeMs,
3611
+ openable: ctx.sameMachine && isSystemOpenable(stat2.rel)
3612
+ };
3613
+ });
3614
+ sendJson(res, 200, {
3615
+ files,
3616
+ overLimit: asked.length - taken.length,
3617
+ root
3618
+ });
3619
+ }
3620
+ function readFileText(ctx, res, sessionId, rawUrl, lang) {
3621
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3622
+ if (root === null) return;
3623
+ const path = singlePath(res, rawUrl, lang);
3624
+ if (path === null) return;
3625
+ const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3626
+ if (!stat2.ok) {
3627
+ const refusal = toWireRefusal(stat2.reason);
3628
+ return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3629
+ }
3630
+ if (stat2.kind !== "text") {
3631
+ return sendError(res, 415, "not-text", t("web.file_view_not_text", void 0, lang));
3632
+ }
3633
+ if (stat2.bytes > MAX_TEXT_BYTES) {
3634
+ return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
3635
+ }
3636
+ const out = ctx.runtime.workspaceFiles.readFile(stat2.rel, root);
3637
+ if (!out.ok) {
3638
+ const refusal = out.reason === "binary" ? "unreadable" : out.reason;
3639
+ return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3640
+ }
3641
+ sendJson(res, 200, {
3642
+ path,
3643
+ text: out.text,
3644
+ bytes: out.bytes
3645
+ });
3646
+ }
3647
+ function readFileBytes(ctx, res, sessionId, rawUrl, headOnly, lang) {
3648
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3649
+ if (root === null) return;
3650
+ const path = singlePath(res, rawUrl, lang);
3651
+ if (path === null) return;
3652
+ const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3653
+ if (!stat2.ok) {
3654
+ const refusal = toWireRefusal(stat2.reason);
3655
+ return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3656
+ }
3657
+ const known = VIEWABLE.get(extname(stat2.rel).toLowerCase());
3658
+ const inline = known !== void 0 && stat2.kind === "binary";
3659
+ const headers = {
3660
+ "Content-Type": inline ? known.mime : "application/octet-stream",
3661
+ "X-Content-Type-Options": "nosniff",
3662
+ "Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat2.rel))}`
3663
+ };
3664
+ if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
3665
+ if (headOnly) {
3666
+ return sendEmpty(res, 200, {
3667
+ ...headers,
3668
+ "Content-Length": String(stat2.bytes)
3669
+ });
3670
+ }
3671
+ sendFile(res, stat2.absPath, headers);
3672
+ }
3673
+ function basenameOf(rel) {
3674
+ const cut = rel.lastIndexOf("/");
3675
+ return cut < 0 ? rel : rel.slice(cut + 1);
3676
+ }
3677
+ // src/workspace/file-open.ts
3678
+ function describeOpenRefusal(refusal, lang) {
3679
+ if (refusal === "extension-not-allowed") {
3680
+ return t("web.file_open_extension_not_allowed", void 0, lang);
3681
+ }
3682
+ if (refusal === "not-same-machine") return t("web.file_open_not_same_machine", void 0, lang);
3683
+ if (refusal === "launch-failed") return t("web.file_open_launch_failed", void 0, lang);
3684
+ return describeRefusal(refusal, lang);
3685
+ }
3686
+ function refuse(res, refusal, lang) {
3687
+ sendJson(res, 200, {
3688
+ ok: false,
3689
+ refusal,
3690
+ message: describeOpenRefusal(refusal, lang)
3691
+ });
3692
+ }
3693
+ async function openFileWithSystemApp(ctx, req, res, sessionId, lang) {
3694
+ if (!ctx.sameMachine) {
3695
+ return sendError(res, 403, "not-same-machine", describeOpenRefusal("not-same-machine", lang));
3696
+ }
3697
+ const root = resolveViewRoot(ctx, res, sessionId, lang);
3698
+ if (root === null) return;
3699
+ const body = await readJsonBody(req);
3700
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3701
+ const payload = asRecord(body.value);
3702
+ const path = typeof payload["path"] === "string" ? payload["path"].trim() : "";
3703
+ if (path === "") {
3704
+ return sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
3705
+ }
3706
+ const plan = ctx.runtime.workspaceFiles.planSystemOpen(path, root);
3707
+ if (!plan.ok) return refuse(res, plan.reason, lang);
3708
+ try {
3709
+ const child = spawn(plan.command, [...plan.args], { stdio: "ignore", detached: true });
3710
+ await new Promise((done, fail) => {
3711
+ child.on("error", fail);
3712
+ child.on("spawn", () => {
3713
+ child.unref();
3714
+ done();
3715
+ });
3716
+ });
3717
+ } catch {
3718
+ return refuse(res, "launch-failed", lang);
3719
+ }
3720
+ sendJson(res, 200, { ok: true });
3721
+ }
3314
3722
  var GIT_TIMEOUT_MS = 1e4;
3315
3723
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
3316
3724
  function runGit(cwd, args) {
@@ -3765,6 +4173,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
3765
4173
  if (resource === "skills" && first === "import" && second === void 0 && method === "POST") {
3766
4174
  return importSkills(ctx, req, res, lang);
3767
4175
  }
4176
+ if (resource === "skills" && first === "remove" && second === void 0 && method === "POST") {
4177
+ return removeSkill(ctx, req, res, lang);
4178
+ }
3768
4179
  if (resource === "plugins" && first === void 0 && isRead(method)) {
3769
4180
  return listPlugins(ctx, res, lang);
3770
4181
  }
@@ -3789,12 +4200,18 @@ async function handleApi(opts, req, res, method, segments, lang) {
3789
4200
  if (resource === "providers" && first !== void 0 && second === "models" && method === "POST") {
3790
4201
  return discoverProviderModels(ctx, req, res, first, lang);
3791
4202
  }
4203
+ if (resource === "providers" && first !== void 0 && second === "key" && method === "POST") {
4204
+ return setProviderKey(ctx, req, res, first, lang);
4205
+ }
3792
4206
  if (resource === "workspaces" && first === "dirs" && isRead(method)) {
3793
4207
  return browseWorkspaceDirs(ctx, res, req.url, lang);
3794
4208
  }
3795
4209
  if (resource === "workspaces" && first === void 0 && method === "POST") {
3796
4210
  return createWorkspaceDir(req, res, lang);
3797
4211
  }
4212
+ if (resource === "workspaces" && first === "pick" && method === "POST") {
4213
+ return pickWorkspaceDir(ctx, req, res, lang);
4214
+ }
3798
4215
  if (resource === "schedules") {
3799
4216
  return handleSchedules(ctx, req, res, method, first, second, detail, tail, lang);
3800
4217
  }
@@ -3862,6 +4279,18 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
3862
4279
  if (action === "file-candidates" && isRead(method)) {
3863
4280
  return listFileCandidates(ctx, res, sessionId, req.url, lang);
3864
4281
  }
4282
+ if (action === "file-stat" && isRead(method)) {
4283
+ return statFiles(ctx, res, sessionId, req.url, lang);
4284
+ }
4285
+ if (action === "file" && isRead(method)) {
4286
+ return readFileText(ctx, res, sessionId, req.url, lang);
4287
+ }
4288
+ if (action === "file-bytes" && isRead(method)) {
4289
+ return readFileBytes(ctx, res, sessionId, req.url, method === "HEAD", lang);
4290
+ }
4291
+ if (action === "file-open" && method === "POST") {
4292
+ return openFileWithSystemApp(ctx, req, res, sessionId, lang);
4293
+ }
3865
4294
  if (action === "approvals" && isRead(method)) return listApprovals(ctx, res, sessionId, lang);
3866
4295
  if (action === "questions" && isRead(method)) return listQuestions(ctx, res, sessionId, lang);
3867
4296
  if (action === "capabilities" && isRead(method))
@@ -4016,13 +4445,20 @@ async function createWebServer(opts) {
4016
4445
  };
4017
4446
  }
4018
4447
  function buildHandler(opts, hub, binding, port, webRoot) {
4448
+ const machineFacts = {
4449
+ lanExposed: binding.lanExposed,
4450
+ platform: process.platform,
4451
+ env: process.env
4452
+ };
4019
4453
  return createRequestHandler({
4020
4454
  ctx: {
4021
4455
  hub,
4022
4456
  runtime: opts.runtime,
4023
4457
  version: opts.version,
4024
4458
  artifactsRoot: opts.artifactsRoot ?? opts.runtime.artifactsRoot,
4025
- lanExposed: binding.lanExposed
4459
+ lanExposed: binding.lanExposed,
4460
+ nativeDirPicker: resolveNativeDirPicker(machineFacts),
4461
+ sameMachine: isSameMachine(machineFacts)
4026
4462
  },
4027
4463
  guard: createAuthGuard({ token: binding.token, port, lanExposed: binding.lanExposed }),
4028
4464
  ...webRoot === void 0 ? {} : { webRoot },
@@ -4068,4 +4504,4 @@ function makeCloser(server, hub, runtime) {
4068
4504
  };
4069
4505
  }
4070
4506
 
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 };
4507
+ 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 };