@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/README.md +204 -76
- package/dist/index.d.ts +322 -16
- package/dist/index.js +451 -15
- package/dist/web/assets/index-C8_G1o8e.js +373 -0
- package/dist/web/assets/index-DtAA7lvj.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-Bj3SdU3h.js +0 -366
- package/dist/web/assets/index-cRd3MeBE.css +0 -1
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)
|
|
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)
|
|
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 ? {
|
|
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.
|
|
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.
|
|
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 };
|