@xopcai/xopc 0.0.229 → 0.0.231
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/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-CsFAJTIN.js → agent-avatar-dicebear-CBqUnlvL.js} +1 -1
- package/dist/gateway/static/root/assets/agents-7YO6znTT.js +68 -0
- package/dist/gateway/static/root/assets/{app-management-settings-panel-7KcMWuah.js → app-management-settings-panel-DP7YVRPn.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-Cqlw7Fif.js → appearance-settings-CVQ5n7y6.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-CsnhdGZ2.js → apps-page-DSCia0DT.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-q4pDpqkF.js → attachment-load-mmr_mHRr.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-BuTqjKGX.js → automations-page-DfY-T6Ga.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-BpvD54D6.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-DUce1Bsc.js → binary-plugins-CO9QMEh2.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-D8Ejp-QR.js → block-editor-CE14CVOy.js} +3 -3
- package/dist/gateway/static/root/assets/browser-settings-page-CeESA4Oj.js +8 -0
- package/dist/gateway/static/root/assets/{browser-workflows-page-BDvZbR9v.js → browser-workflows-page-1wgXyoSO.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-B_OXTojB.js +7 -0
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-D5nZecxV.js +3 -0
- package/dist/gateway/static/root/assets/channels-settings-BzmnWx6X.js +1 -0
- package/dist/gateway/static/root/assets/{chat-terminal-dock-h1CKexNz.js → chat-terminal-dock-CdRW6Euc.js} +2 -2
- package/dist/gateway/static/root/assets/cloud-C4-yfnnk.js +1 -0
- package/dist/gateway/static/root/assets/connectors-page-DX6OkwFQ.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-DlaQQOId.js → cron-expression-DYSMZsms.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-8R1sXJKb.js → date-picker-DGCfX4tK.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-CWur0821.js → dependency-picker-DHDa4ht1.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-Knl-zNDU.js → desktop-pet-CMO-J3cG.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-CAQMLVtd.js → desktop-pet-settings-MmOR4A3u.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-Caaz4Rw5.js → directory-picker-path-field-B9qoxDcn.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-B965Mc15.js → extension-debug-page-DdH0dG2_.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-C0UnkBi5.js → extension-page-Clg-KktN.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BUSIAZCR.js → extension-settings-page-eP3fJ2M6.js} +1 -1
- package/dist/gateway/static/root/assets/files-Dp8_uGTO.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-config-api-DkH0eEgy.js → gateway-config-api-DEHnF4Wm.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-BBgLDm6I.js → gateway-settings-DKkEXXj9.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-BtEHVXWD.js → heartbeat-settings-DlBPNubY.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-BnOrLRfs.js +1 -0
- package/dist/gateway/static/root/assets/index-0xQO5B_Q.js +70 -0
- package/dist/gateway/static/root/assets/index-CCyIdGuQ.css +2 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-CsRz8NrS.js → keyboard-shortcuts-settings-BvpEeHnm.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-Ch1rdUo8.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-CTBNUG4a.js → local-apps-page-Bzvn99eH.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-zxx2ExlY.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-YqUAEwQo.js → management-settings-BQaeyOYm.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-split-dxpSCXb8.js → markdown-split-D9tmIiEm.js} +1 -1
- package/dist/gateway/static/root/assets/markdown-view-B1bGSQqt.css +1 -0
- package/dist/gateway/static/root/assets/{media-plugins-CJ00i1Lg.js → media-plugins-D8I7OGnT.js} +1 -1
- package/dist/gateway/static/root/assets/models-hub-cache-0RjEwafI.js +1 -0
- package/dist/gateway/static/root/assets/{notes-api-BFsOGYkd.js → notes-api-EpgG-OQc.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-DEnbnkIt.js → notes-page-CCvjGMhN.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-9cgh77VH.js +4 -0
- package/dist/gateway/static/root/assets/oauth-api-BwNLwbI8.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-BHxo9U95.js → product-open-page-MmMswmOU.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-hf6pmH4p.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-By2U-Wdh.js +1 -0
- package/dist/gateway/static/root/assets/remote-access-hub-CUcpp7CQ.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-BpQ2GI-0.js → runtime-tools-settings-panel-Dx6o7sLu.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-HE1G26Lx.js → schema-form-Cdd9c4vt.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-ByI82xS1.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-BCoI5IJ8.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-CXAn0Xhy.js → settings-form-section-D0YxMnhm.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-DsFu7Bcv.js → settings-loading-skeleton-y53gEKsF.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-C4iib9nO.js → settings-page-BoE_tToG.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-BA83Wr1d.js → setup-status-panel-BQmyb5KC.js} +1 -1
- package/dist/gateway/static/root/assets/share-preview-page-DiqBn-oE.js +2 -0
- package/dist/gateway/static/root/assets/shares-settings-Bf3VpZ-R.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-CXHlbuGS.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-CdaFtNw9.js → skills-page-CZwhWiRl.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-DYaAddUL.js → skills-page.utils-kBGr8opZ.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-Co7gilRd.js → system-settings-panel-CPBs6lWi.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-Bo0KSZ2v.js +1 -0
- package/dist/gateway/static/root/assets/theme-store-Qgvo0Ga7.js +1 -0
- package/dist/gateway/static/root/assets/{use-autosave-DnnZtMpA.js → use-autosave-C4YcEiP9.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-Cnj7gMX2.js +1 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-wetUpi3i.js → voice-api-key-field-BML8zCwZ.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-tbTWASJJ.js → work-discovery-overlay-CKIdPEen.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-Dy0fDs8w.js → workflow-run-setup-panel-BK44Vm5W.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-CB98MTwG.js +5 -0
- package/dist/gateway/static/root/index.html +10 -6
- package/dist/package.js +1 -1
- package/dist/src/agent/inbound/turn-dispatcher.js +1 -1
- package/dist/src/agent/service/process-direct-streaming.js +4 -1
- package/dist/src/agent/service/webchat-tts.d.ts +2 -0
- package/dist/src/agent/service/webchat-tts.js +15 -2
- package/dist/src/agent/service.d.ts +1 -0
- package/dist/src/agent/service.js +5 -2
- package/dist/src/agent/skills/skill-manage-ops.js +1 -1
- package/dist/src/agent/tools/tts-tool.js +19 -13
- package/dist/src/agent/tools/write.js +9 -12
- package/dist/src/agent/tools/xopc-use-tool.js +2 -2
- package/dist/src/auth/oauth/index.js +2 -2
- package/dist/src/auth/oauth/registry.d.ts +1 -1
- package/dist/src/auth/oauth/registry.js +5 -1
- package/dist/src/auth/oauth/xopc-cloud.d.ts +1 -0
- package/dist/src/auth/oauth/xopc-cloud.js +8 -2
- package/dist/src/files/file-service.d.ts +36 -0
- package/dist/src/files/file-service.js +282 -0
- package/dist/src/gateway/chat-stream/mapper.js +11 -0
- package/dist/src/gateway/heartbeat/service.js +1 -1
- package/dist/src/gateway/hono/app.js +2 -2
- package/dist/src/gateway/hono/routes/files.d.ts +3 -0
- package/dist/src/gateway/hono/routes/files.js +172 -0
- package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
- package/dist/src/gateway/hono/routes/projects.js +0 -508
- package/dist/src/gateway/hono/routes/shares.js +410 -21
- package/dist/src/gateway/hono/routes/workspace.js +15 -974
- package/dist/src/gateway/service.js +9 -9
- package/dist/src/heartbeat/index.js +1 -1
- package/dist/src/share/hosted-session-share.d.ts +104 -0
- package/dist/src/share/hosted-session-share.js +276 -0
- package/dist/src/share/note-share-service.js +4 -24
- package/dist/src/share/session-share-projector.d.ts +31 -0
- package/dist/src/share/session-share-projector.js +79 -0
- package/dist/src/share/session-share-service.d.ts +89 -0
- package/dist/src/share/session-share-service.js +325 -0
- package/dist/src/share/share-asset-ticket.d.ts +7 -0
- package/dist/src/share/share-asset-ticket.js +30 -0
- package/dist/src/share/share-landing.d.ts +4 -3
- package/dist/src/share/share-landing.js +5 -5
- package/dist/src/share/share-store.d.ts +27 -2
- package/dist/src/share/share-store.js +71 -8
- package/dist/src/share/share-types.d.ts +17 -2
- package/dist/src/tui/backends/embedded-backend.js +1 -1
- package/dist/src/tui/backends/gateway-realtime-backend.js +11 -3
- package/package.json +1 -1
- package/dist/gateway/static/root/assets/agents-DA4b6_0C.js +0 -68
- package/dist/gateway/static/root/assets/automations-workspace-CD0E2ThS.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-CY1k3MMd.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-D1VfotHQ.js +0 -7
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-DXHqRLpU.js +0 -3
- package/dist/gateway/static/root/assets/channels-settings-DrFL57M_.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-DUAyfFCF.js +0 -2
- package/dist/gateway/static/root/assets/home-page-5SU2TNJj.js +0 -1
- package/dist/gateway/static/root/assets/index-BNWhgmjs.js +0 -70
- package/dist/gateway/static/root/assets/index-D_hrEKXM.css +0 -2
- package/dist/gateway/static/root/assets/local-app-workbench-page-DYPEUgpP.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-B0P7rG2B.js +0 -2
- package/dist/gateway/static/root/assets/markdown-view-CLzTdVLW.css +0 -1
- package/dist/gateway/static/root/assets/notes-workbench-DmVYu_Oc.js +0 -4
- package/dist/gateway/static/root/assets/project-detail-page-DV2nGmsq.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-BNIFwFfc.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-DLXqKiA_.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-DWaXf_25.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-JFCbnCL2.js +0 -1
- package/dist/gateway/static/root/assets/share-preview-page-BOGl3s6M.js +0 -2
- package/dist/gateway/static/root/assets/shares-settings-Q2MdGaBi.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-cFtfVeFU.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-5B1pswu8.js +0 -1
- package/dist/gateway/static/root/assets/theme-store-DOdCjyuw.js +0 -1
- package/dist/gateway/static/root/assets/user-context-page-Dr1FKjXv.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-IVP3GSkI.js +0 -5
- package/dist/src/gateway/file-path-classifier.d.ts +0 -30
- package/dist/src/gateway/file-path-classifier.js +0 -105
- package/dist/src/gateway/file-reference-errors.d.ts +0 -6
- package/dist/src/gateway/file-reference-errors.js +0 -21
- package/dist/src/gateway/file-reference-registry.d.ts +0 -34
- package/dist/src/gateway/file-reference-registry.js +0 -45
- /package/dist/gateway/static/root/assets/{markdown-view-DvM5PWEH.js → markdown-view-RxNHwlNo.js} +0 -0
|
@@ -1,152 +1,21 @@
|
|
|
1
|
-
import { init_agent_scope, listAgentEntries, normalizeAgentId, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../../agent/agent-scope.js";
|
|
2
|
-
import { validateWritePath } from "../../../agent/sandbox/path-policy.js";
|
|
3
|
-
import { isPathUnderWorkspace, resolveWorkspaceSafePath, toWorkspaceRelativePosix } from "../../workspace-editor-path.js";
|
|
4
|
-
import { getWorkspacePath } from "../../../config/workspace-path-helpers.js";
|
|
5
|
-
import { buildFilePathClassifierContext, classifyFileLocation, displayNameForPath, fileRefSessionKeysMatch, resolveFileReferenceCandidate } from "../../file-path-classifier.js";
|
|
6
|
-
import { classifyFileReferenceFsError } from "../../file-reference-errors.js";
|
|
7
|
-
import { fileReferenceRegistry } from "../../file-reference-registry.js";
|
|
8
1
|
import { resolveHeartbeatMdPath } from "../../workspace-heartbeat-path.js";
|
|
9
2
|
import { createGatewayRouteLogger } from "../lib/route-logger.js";
|
|
10
|
-
import {
|
|
11
|
-
import { fuzzySearchWorkspaceFiles } from "../../workspace-file-search.js";
|
|
12
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
13
|
-
import { constants } from "node:fs";
|
|
14
|
-
import { randomUUID } from "node:crypto";
|
|
15
|
-
import { copyFile, link, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
16
4
|
//#region src/gateway/hono/routes/workspace.ts
|
|
17
|
-
init_agent_scope();
|
|
18
5
|
const log = createGatewayRouteLogger("Workspace");
|
|
19
|
-
function isKnownEditorAgentId(cfg, id) {
|
|
20
|
-
const n = normalizeAgentId(id);
|
|
21
|
-
if (n === resolveDefaultAgentId(cfg)) return true;
|
|
22
|
-
return listAgentEntries(cfg).some((e) => normalizeAgentId(e.id) === n);
|
|
23
|
-
}
|
|
24
|
-
function resolveEditorWorkspaceRoot(cfg, agentIdRaw) {
|
|
25
|
-
const trimmed = typeof agentIdRaw === "string" ? agentIdRaw.trim() : "";
|
|
26
|
-
if (!trimmed) {
|
|
27
|
-
const root = getWorkspacePath(cfg);
|
|
28
|
-
if (!root) return {
|
|
29
|
-
ok: false,
|
|
30
|
-
message: "Workspace not configured"
|
|
31
|
-
};
|
|
32
|
-
return {
|
|
33
|
-
ok: true,
|
|
34
|
-
root
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
const id = normalizeAgentId(trimmed);
|
|
38
|
-
if (!isKnownEditorAgentId(cfg, id)) return {
|
|
39
|
-
ok: false,
|
|
40
|
-
message: "Unknown agent"
|
|
41
|
-
};
|
|
42
|
-
return {
|
|
43
|
-
ok: true,
|
|
44
|
-
root: resolveAgentWorkspaceDir(cfg, id)
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/** Prefer `sessionKey` (per-session workspace override) over `agentId`. */
|
|
48
|
-
async function resolveEditorWorkspaceRootAsync(service, cfg, sessionKeyRaw, agentIdRaw) {
|
|
49
|
-
const sk = typeof sessionKeyRaw === "string" ? sessionKeyRaw.trim() : "";
|
|
50
|
-
if (sk) try {
|
|
51
|
-
return {
|
|
52
|
-
ok: true,
|
|
53
|
-
root: await service.sessions.getEffectiveWorkspacePath(sk)
|
|
54
|
-
};
|
|
55
|
-
} catch (err) {
|
|
56
|
-
const em = err instanceof Error ? err.message : String(err);
|
|
57
|
-
log.warn({
|
|
58
|
-
err,
|
|
59
|
-
sessionKey: sk
|
|
60
|
-
}, "Session workspace root resolution failed");
|
|
61
|
-
return {
|
|
62
|
-
ok: false,
|
|
63
|
-
message: em || "Session workspace resolution failed"
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return resolveEditorWorkspaceRoot(cfg, agentIdRaw);
|
|
67
|
-
}
|
|
68
|
-
function resolveWorkspaceImportConfig(cfg) {
|
|
69
|
-
const raw = cfg.workspace?.import;
|
|
70
|
-
return {
|
|
71
|
-
targetDir: raw?.targetDir?.trim() || "imports",
|
|
72
|
-
maxBytes: raw?.maxBytes ?? 104857600,
|
|
73
|
-
allowOverwrite: raw?.allowOverwrite ?? true
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
/** Strip path separators, NULs and control chars from a basename so it stays in the destination dir. */
|
|
77
|
-
function sanitizeImportBasename(name) {
|
|
78
|
-
return name.replace(/[\\/]/g, "").replace(/[\x00-\x1f\x7f]/g, "").trim();
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Race-safe target picker for the `rename`-on-conflict strategy. Uses `link(tmp, target)`
|
|
82
|
-
* which atomically fails with EEXIST when the candidate is taken; on success the tmp
|
|
83
|
-
* file is left in place for the caller to unlink. Returns the linked target path.
|
|
84
|
-
*/
|
|
85
|
-
async function pickAvailableTargetWithLink(tmpAbs, initialDestAbs, maxAttempts = 1e3) {
|
|
86
|
-
const dir = dirname(initialDestAbs);
|
|
87
|
-
const original = basename(initialDestAbs);
|
|
88
|
-
const dotIdx = original.lastIndexOf(".");
|
|
89
|
-
const stem = dotIdx > 0 ? original.slice(0, dotIdx) : original;
|
|
90
|
-
const ext = dotIdx > 0 ? original.slice(dotIdx) : "";
|
|
91
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
92
|
-
const candidate = attempt === 1 ? initialDestAbs : join(dir, `${stem}-${attempt}${ext}`);
|
|
93
|
-
try {
|
|
94
|
-
await link(tmpAbs, candidate);
|
|
95
|
-
return {
|
|
96
|
-
ok: true,
|
|
97
|
-
path: candidate,
|
|
98
|
-
attempts: attempt
|
|
99
|
-
};
|
|
100
|
-
} catch (err) {
|
|
101
|
-
if (err?.code !== "EEXIST") throw err;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
ok: false,
|
|
106
|
-
attempts: maxAttempts
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function fileReferenceCapabilities(scope, isDirectory, locationKind) {
|
|
110
|
-
if (scope === "workspace") return isDirectory ? [
|
|
111
|
-
"openExternal",
|
|
112
|
-
"revealInFolder",
|
|
113
|
-
"copyPath"
|
|
114
|
-
] : [
|
|
115
|
-
"preview",
|
|
116
|
-
"edit",
|
|
117
|
-
"openExternal",
|
|
118
|
-
"revealInFolder",
|
|
119
|
-
"copyPath"
|
|
120
|
-
];
|
|
121
|
-
if (scope === "external" || scope === "agent-profile" || scope === "session-artifact") {
|
|
122
|
-
const base = [
|
|
123
|
-
"openExternal",
|
|
124
|
-
"revealInFolder",
|
|
125
|
-
"copyPath"
|
|
126
|
-
];
|
|
127
|
-
if (!isDirectory && locationKind !== "xopc-config") base.push("importToWorkspace");
|
|
128
|
-
return base;
|
|
129
|
-
}
|
|
130
|
-
if (scope === "missing") return ["copyPath"];
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
function isFileReferenceAction(action) {
|
|
134
|
-
return action === "openExternal" || action === "revealInFolder";
|
|
135
|
-
}
|
|
136
6
|
function registerWorkspaceRoutes(authenticated, deps) {
|
|
137
7
|
const { service } = deps;
|
|
138
8
|
authenticated.get("/api/workspace/heartbeat-md", async (c) => {
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
9
|
+
const path = resolveHeartbeatMdPath(service.currentConfig);
|
|
10
|
+
if (!path) return c.json({
|
|
141
11
|
ok: false,
|
|
142
12
|
error: { message: "Workspace not configured" }
|
|
143
13
|
}, 400);
|
|
144
14
|
try {
|
|
145
|
-
const content = await readFile(abs, "utf-8");
|
|
146
15
|
return c.json({
|
|
147
16
|
ok: true,
|
|
148
17
|
payload: {
|
|
149
|
-
content,
|
|
18
|
+
content: await readFile(path, "utf8"),
|
|
150
19
|
file: "HEARTBEAT.md"
|
|
151
20
|
}
|
|
152
21
|
});
|
|
@@ -161,861 +30,33 @@ function registerWorkspaceRoutes(authenticated, deps) {
|
|
|
161
30
|
}
|
|
162
31
|
});
|
|
163
32
|
authenticated.put("/api/workspace/heartbeat-md", async (c) => {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
33
|
+
const path = resolveHeartbeatMdPath(service.currentConfig);
|
|
34
|
+
if (!path) return c.json({
|
|
166
35
|
ok: false,
|
|
167
36
|
error: { message: "Workspace not configured" }
|
|
168
37
|
}, 400);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
body = await c.req.json();
|
|
172
|
-
} catch {
|
|
173
|
-
return c.json({
|
|
174
|
-
ok: false,
|
|
175
|
-
error: { message: "Invalid JSON" }
|
|
176
|
-
}, 400);
|
|
177
|
-
}
|
|
178
|
-
const content = typeof body === "object" && body !== null && "content" in body && typeof body.content === "string" ? body.content : "";
|
|
179
|
-
try {
|
|
180
|
-
await writeFile(abs, content, "utf-8");
|
|
181
|
-
return c.json({
|
|
182
|
-
ok: true,
|
|
183
|
-
payload: { file: "HEARTBEAT.md" }
|
|
184
|
-
});
|
|
185
|
-
} catch (err) {
|
|
186
|
-
log.error({
|
|
187
|
-
err,
|
|
188
|
-
path: abs
|
|
189
|
-
}, "Failed to write HEARTBEAT.md");
|
|
190
|
-
return c.json({
|
|
191
|
-
ok: false,
|
|
192
|
-
error: { message: "Write failed" }
|
|
193
|
-
}, 500);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
authenticated.get("/api/workspace/editor/list", async (c) => {
|
|
197
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
198
|
-
if (ws.ok === false) return c.json({
|
|
199
|
-
ok: false,
|
|
200
|
-
error: { message: ws.message }
|
|
201
|
-
}, 400);
|
|
202
|
-
const workspaceRoot = ws.root;
|
|
203
|
-
const absDir = resolveWorkspaceSafePath(workspaceRoot, typeof c.req.query("dir") === "string" ? c.req.query("dir") : "");
|
|
204
|
-
if (!absDir) return c.json({
|
|
205
|
-
ok: false,
|
|
206
|
-
error: { message: "Invalid path" }
|
|
207
|
-
}, 400);
|
|
208
|
-
let st;
|
|
209
|
-
try {
|
|
210
|
-
st = await stat(absDir);
|
|
211
|
-
} catch {
|
|
212
|
-
return c.json({
|
|
213
|
-
ok: false,
|
|
214
|
-
error: { message: "Not found" }
|
|
215
|
-
}, 404);
|
|
216
|
-
}
|
|
217
|
-
if (!st.isDirectory()) return c.json({
|
|
218
|
-
ok: false,
|
|
219
|
-
error: { message: "Not a directory" }
|
|
220
|
-
}, 400);
|
|
221
|
-
const dirents = await readdir(absDir, { withFileTypes: true });
|
|
222
|
-
const entries = [];
|
|
223
|
-
for (const entry of dirents) {
|
|
224
|
-
if (entry.name.startsWith(".")) continue;
|
|
225
|
-
const fullPath = join(absDir, entry.name);
|
|
226
|
-
if (entry.isDirectory()) entries.push({
|
|
227
|
-
name: entry.name,
|
|
228
|
-
path: toWorkspaceRelativePosix(workspaceRoot, fullPath),
|
|
229
|
-
absolutePath: fullPath,
|
|
230
|
-
isDirectory: true
|
|
231
|
-
});
|
|
232
|
-
else entries.push({
|
|
233
|
-
name: entry.name,
|
|
234
|
-
path: toWorkspaceRelativePosix(workspaceRoot, fullPath),
|
|
235
|
-
absolutePath: fullPath,
|
|
236
|
-
isDirectory: false
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
entries.sort((a, b) => {
|
|
240
|
-
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
|
241
|
-
return a.name.localeCompare(b.name);
|
|
242
|
-
});
|
|
243
|
-
return c.json({
|
|
244
|
-
ok: true,
|
|
245
|
-
payload: {
|
|
246
|
-
root: workspaceRoot,
|
|
247
|
-
entries
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
authenticated.get("/api/workspace/editor/read", async (c) => {
|
|
252
|
-
const pathRel = typeof c.req.query("path") === "string" ? c.req.query("path") : "";
|
|
253
|
-
if (!pathRel.trim()) return c.json({
|
|
254
|
-
ok: false,
|
|
255
|
-
error: { message: "Missing path" }
|
|
256
|
-
}, 400);
|
|
257
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
258
|
-
if (ws.ok === false) return c.json({
|
|
259
|
-
ok: false,
|
|
260
|
-
error: { message: ws.message }
|
|
261
|
-
}, 400);
|
|
262
|
-
const workspaceRoot = ws.root;
|
|
263
|
-
const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
|
|
264
|
-
if (!abs) return c.json({
|
|
265
|
-
ok: false,
|
|
266
|
-
error: { message: "Invalid path" }
|
|
267
|
-
}, 400);
|
|
268
|
-
let st;
|
|
269
|
-
try {
|
|
270
|
-
st = await stat(abs);
|
|
271
|
-
} catch {
|
|
272
|
-
return c.json({
|
|
273
|
-
ok: false,
|
|
274
|
-
error: { message: "Not found" }
|
|
275
|
-
}, 404);
|
|
276
|
-
}
|
|
277
|
-
if (!st.isFile()) return c.json({
|
|
278
|
-
ok: false,
|
|
279
|
-
error: { message: "Not a file" }
|
|
280
|
-
}, 400);
|
|
281
|
-
try {
|
|
282
|
-
const content = await readFile(abs, "utf-8");
|
|
283
|
-
return c.json({
|
|
284
|
-
ok: true,
|
|
285
|
-
payload: {
|
|
286
|
-
content,
|
|
287
|
-
path: toWorkspaceRelativePosix(workspaceRoot, abs),
|
|
288
|
-
absolutePath: abs,
|
|
289
|
-
mtimeMs: st.mtimeMs
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
} catch {
|
|
293
|
-
return c.json({
|
|
294
|
-
ok: false,
|
|
295
|
-
error: { message: "Read failed" }
|
|
296
|
-
}, 500);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
/** Read file as raw bytes and return base64 (for PDF/images in workspace preview — avoids UTF-8 corruption). */
|
|
300
|
-
authenticated.get("/api/workspace/editor/read-base64", async (c) => {
|
|
301
|
-
const pathRel = typeof c.req.query("path") === "string" ? c.req.query("path") : "";
|
|
302
|
-
if (!pathRel.trim()) return c.json({
|
|
303
|
-
ok: false,
|
|
304
|
-
error: { message: "Missing path" }
|
|
305
|
-
}, 400);
|
|
306
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
307
|
-
if (ws.ok === false) return c.json({
|
|
308
|
-
ok: false,
|
|
309
|
-
error: { message: ws.message }
|
|
310
|
-
}, 400);
|
|
311
|
-
const workspaceRoot = ws.root;
|
|
312
|
-
const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
|
|
313
|
-
if (!abs) return c.json({
|
|
314
|
-
ok: false,
|
|
315
|
-
error: { message: "Invalid path" }
|
|
316
|
-
}, 400);
|
|
317
|
-
let st;
|
|
318
|
-
try {
|
|
319
|
-
st = await stat(abs);
|
|
320
|
-
} catch {
|
|
321
|
-
return c.json({
|
|
322
|
-
ok: false,
|
|
323
|
-
error: { message: "Not found" }
|
|
324
|
-
}, 404);
|
|
325
|
-
}
|
|
326
|
-
if (!st.isFile()) return c.json({
|
|
327
|
-
ok: false,
|
|
328
|
-
error: { message: "Not a file" }
|
|
329
|
-
}, 400);
|
|
330
|
-
try {
|
|
331
|
-
const buf = await readFile(abs);
|
|
332
|
-
return c.json({
|
|
333
|
-
ok: true,
|
|
334
|
-
payload: {
|
|
335
|
-
contentBase64: buf.toString("base64"),
|
|
336
|
-
path: toWorkspaceRelativePosix(workspaceRoot, abs),
|
|
337
|
-
/** Host absolute path — Electron can open with the default app (shell.openPath). */
|
|
338
|
-
absolutePath: abs,
|
|
339
|
-
mtimeMs: st.mtimeMs
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
} catch {
|
|
343
|
-
return c.json({
|
|
344
|
-
ok: false,
|
|
345
|
-
error: { message: "Read failed" }
|
|
346
|
-
}, 500);
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
/** Map an absolute host path to a workspace-relative path (if under this session’s workspace). */
|
|
350
|
-
authenticated.get("/api/workspace/editor/resolve-path", async (c) => {
|
|
351
|
-
const raw = c.req.query("absolutePath");
|
|
352
|
-
if (!raw || typeof raw !== "string" || !raw.trim()) return c.json({
|
|
353
|
-
ok: false,
|
|
354
|
-
error: { message: "Missing absolutePath" }
|
|
355
|
-
}, 400);
|
|
356
|
-
const absolutePath = raw.trim();
|
|
357
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
358
|
-
if (ws.ok === false) return c.json({
|
|
359
|
-
ok: false,
|
|
360
|
-
error: { message: ws.message }
|
|
361
|
-
}, 400);
|
|
362
|
-
const workspaceRoot = ws.root;
|
|
363
|
-
const normalized = resolve(absolutePath);
|
|
364
|
-
if (!isPathUnderWorkspace(workspaceRoot, normalized)) return c.json({
|
|
365
|
-
ok: false,
|
|
366
|
-
error: { message: "Path not under workspace" }
|
|
367
|
-
}, 403);
|
|
368
|
-
const rel = toWorkspaceRelativePosix(workspaceRoot, normalized);
|
|
369
|
-
return c.json({
|
|
370
|
-
ok: true,
|
|
371
|
-
payload: { workspaceRelativePath: rel }
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
authenticated.get("/api/workspace/editor/resolve-reference", async (c) => {
|
|
375
|
-
const rawPath = typeof c.req.query("path") === "string" ? c.req.query("path").trim() : "";
|
|
376
|
-
if (!rawPath) return c.json({
|
|
377
|
-
ok: false,
|
|
378
|
-
error: {
|
|
379
|
-
code: "INVALID_PATH",
|
|
380
|
-
message: "Missing path"
|
|
381
|
-
}
|
|
382
|
-
}, 400);
|
|
383
|
-
const sessionKey = typeof c.req.query("sessionKey") === "string" ? c.req.query("sessionKey").trim() : "";
|
|
384
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, sessionKey, c.req.query("agentId"));
|
|
385
|
-
if (ws.ok === false) return c.json({
|
|
386
|
-
ok: false,
|
|
387
|
-
error: {
|
|
388
|
-
code: "WORKSPACE_RESOLUTION_FAILED",
|
|
389
|
-
message: ws.message
|
|
390
|
-
}
|
|
391
|
-
}, 400);
|
|
392
|
-
const workspaceRoot = ws.root;
|
|
393
|
-
const classifierCtx = {
|
|
394
|
-
...buildFilePathClassifierContext(service.currentConfig, sessionKey),
|
|
395
|
-
workspaceRoot
|
|
396
|
-
};
|
|
397
|
-
const displayName = displayNameForPath(rawPath);
|
|
398
|
-
const { candidate, invalid } = await resolveFileReferenceCandidate(rawPath, workspaceRoot, classifierCtx);
|
|
399
|
-
if (!candidate || invalid) return c.json({
|
|
400
|
-
ok: true,
|
|
401
|
-
payload: {
|
|
402
|
-
inputPath: rawPath,
|
|
403
|
-
displayName,
|
|
404
|
-
scope: "invalid",
|
|
405
|
-
exists: false,
|
|
406
|
-
capabilities: [],
|
|
407
|
-
errorCode: "INVALID_PATH"
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
let st = null;
|
|
411
|
-
let statError = null;
|
|
412
|
-
try {
|
|
413
|
-
st = await stat(candidate);
|
|
414
|
-
} catch (err) {
|
|
415
|
-
statError = err;
|
|
416
|
-
}
|
|
417
|
-
if (!st) {
|
|
418
|
-
const failure = classifyFileReferenceFsError(statError);
|
|
419
|
-
if (failure.code !== "FILE_NOT_FOUND") {
|
|
420
|
-
log.warn({
|
|
421
|
-
err: statError,
|
|
422
|
-
sessionKey,
|
|
423
|
-
workspaceRoot,
|
|
424
|
-
candidate,
|
|
425
|
-
errorCode: failure.code
|
|
426
|
-
}, `Workspace file reference access failed: ${failure.message}`);
|
|
427
|
-
return c.json({
|
|
428
|
-
ok: false,
|
|
429
|
-
error: {
|
|
430
|
-
code: failure.code,
|
|
431
|
-
message: failure.message
|
|
432
|
-
}
|
|
433
|
-
}, failure.status === 403 ? 403 : 500);
|
|
434
|
-
}
|
|
435
|
-
return c.json({
|
|
436
|
-
ok: true,
|
|
437
|
-
payload: {
|
|
438
|
-
inputPath: rawPath,
|
|
439
|
-
displayName,
|
|
440
|
-
scope: "missing",
|
|
441
|
-
exists: false,
|
|
442
|
-
absolutePath: candidate,
|
|
443
|
-
capabilities: fileReferenceCapabilities("missing", false),
|
|
444
|
-
errorCode: "FILE_NOT_FOUND"
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
const { scope, locationKind, manageRoute } = classifyFileLocation(candidate, classifierCtx);
|
|
449
|
-
const inWorkspace = scope === "workspace";
|
|
450
|
-
const isDirectory = st.isDirectory();
|
|
451
|
-
const capabilities = fileReferenceCapabilities(scope, isDirectory, locationKind);
|
|
452
|
-
const ref = fileReferenceRegistry.register({
|
|
453
|
-
absolutePath: candidate,
|
|
454
|
-
sessionKey: sessionKey || void 0,
|
|
455
|
-
scope,
|
|
456
|
-
locationKind,
|
|
457
|
-
capabilities
|
|
458
|
-
});
|
|
459
|
-
return c.json({
|
|
460
|
-
ok: true,
|
|
461
|
-
payload: {
|
|
462
|
-
fileRefId: ref.id,
|
|
463
|
-
inputPath: rawPath,
|
|
464
|
-
displayName,
|
|
465
|
-
scope,
|
|
466
|
-
locationKind,
|
|
467
|
-
manageRoute,
|
|
468
|
-
exists: true,
|
|
469
|
-
isDirectory,
|
|
470
|
-
absolutePath: candidate,
|
|
471
|
-
workspaceRelativePath: inWorkspace ? toWorkspaceRelativePosix(workspaceRoot, candidate) : void 0,
|
|
472
|
-
capabilities,
|
|
473
|
-
mtimeMs: st.mtimeMs
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
});
|
|
477
|
-
authenticated.post("/api/workspace/file-ref/:id/resolve-action", async (c) => {
|
|
478
|
-
const id = c.req.param("id")?.trim() ?? "";
|
|
479
|
-
if (!id) return c.json({
|
|
480
|
-
ok: false,
|
|
481
|
-
error: {
|
|
482
|
-
code: "INVALID_FILE_REF",
|
|
483
|
-
message: "Missing file reference"
|
|
484
|
-
}
|
|
485
|
-
}, 400);
|
|
486
|
-
const ref = fileReferenceRegistry.resolve(id);
|
|
487
|
-
if (!ref) return c.json({
|
|
488
|
-
ok: false,
|
|
489
|
-
error: {
|
|
490
|
-
code: "FILE_REF_EXPIRED",
|
|
491
|
-
message: "File reference expired"
|
|
492
|
-
}
|
|
493
|
-
}, 404);
|
|
494
|
-
const sessionKey = typeof c.req.query("sessionKey") === "string" ? c.req.query("sessionKey").trim() : "";
|
|
495
|
-
if (!fileRefSessionKeysMatch(ref.sessionKey, sessionKey)) return c.json({
|
|
496
|
-
ok: false,
|
|
497
|
-
error: {
|
|
498
|
-
code: "FILE_REF_FORBIDDEN",
|
|
499
|
-
message: "File reference forbidden"
|
|
500
|
-
}
|
|
501
|
-
}, 403);
|
|
502
|
-
const action = (await c.req.json().catch(() => ({}))).action;
|
|
503
|
-
if (!isFileReferenceAction(action)) return c.json({
|
|
504
|
-
ok: false,
|
|
505
|
-
error: {
|
|
506
|
-
code: "INVALID_ACTION",
|
|
507
|
-
message: "Invalid action"
|
|
508
|
-
}
|
|
509
|
-
}, 400);
|
|
510
|
-
if (!ref.capabilities.includes(action)) return c.json({
|
|
511
|
-
ok: false,
|
|
512
|
-
error: {
|
|
513
|
-
code: "ACTION_NOT_ALLOWED",
|
|
514
|
-
message: "Action not allowed"
|
|
515
|
-
}
|
|
516
|
-
}, 403);
|
|
517
|
-
let st;
|
|
518
|
-
try {
|
|
519
|
-
st = await stat(ref.absolutePath);
|
|
520
|
-
} catch {
|
|
521
|
-
return c.json({
|
|
522
|
-
ok: false,
|
|
523
|
-
error: {
|
|
524
|
-
code: "FILE_NOT_FOUND",
|
|
525
|
-
message: "File not found"
|
|
526
|
-
}
|
|
527
|
-
}, 404);
|
|
528
|
-
}
|
|
529
|
-
return c.json({
|
|
530
|
-
ok: true,
|
|
531
|
-
payload: {
|
|
532
|
-
absolutePath: ref.absolutePath,
|
|
533
|
-
isDirectory: st.isDirectory()
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
authenticated.post("/api/workspace/import-file-ref/:id", async (c) => {
|
|
538
|
-
const id = c.req.param("id")?.trim() ?? "";
|
|
539
|
-
if (!id) return c.json({
|
|
540
|
-
ok: false,
|
|
541
|
-
error: {
|
|
542
|
-
code: "INVALID_FILE_REF",
|
|
543
|
-
message: "Missing file reference"
|
|
544
|
-
}
|
|
545
|
-
}, 400);
|
|
546
|
-
const ref = fileReferenceRegistry.resolve(id);
|
|
547
|
-
if (!ref) return c.json({
|
|
548
|
-
ok: false,
|
|
549
|
-
error: {
|
|
550
|
-
code: "FILE_REF_EXPIRED",
|
|
551
|
-
message: "File reference expired"
|
|
552
|
-
}
|
|
553
|
-
}, 404);
|
|
554
|
-
const sessionKey = typeof c.req.query("sessionKey") === "string" ? c.req.query("sessionKey").trim() : "";
|
|
555
|
-
if (!fileRefSessionKeysMatch(ref.sessionKey, sessionKey)) return c.json({
|
|
556
|
-
ok: false,
|
|
557
|
-
error: {
|
|
558
|
-
code: "FILE_REF_FORBIDDEN",
|
|
559
|
-
message: "File reference forbidden"
|
|
560
|
-
}
|
|
561
|
-
}, 403);
|
|
562
|
-
if (!ref.capabilities.includes("importToWorkspace")) return c.json({
|
|
563
|
-
ok: false,
|
|
564
|
-
error: {
|
|
565
|
-
code: "IMPORT_NOT_ALLOWED",
|
|
566
|
-
message: "Import not allowed for this file"
|
|
567
|
-
}
|
|
568
|
-
}, 403);
|
|
569
|
-
let sourceStat;
|
|
570
|
-
try {
|
|
571
|
-
sourceStat = await stat(ref.absolutePath);
|
|
572
|
-
} catch {
|
|
573
|
-
fileReferenceRegistry.expireById(id);
|
|
574
|
-
return c.json({
|
|
575
|
-
ok: false,
|
|
576
|
-
error: {
|
|
577
|
-
code: "SOURCE_NOT_FOUND",
|
|
578
|
-
message: "Source file no longer exists"
|
|
579
|
-
}
|
|
580
|
-
}, 404);
|
|
581
|
-
}
|
|
582
|
-
if (!sourceStat.isFile()) return c.json({
|
|
583
|
-
ok: false,
|
|
584
|
-
error: {
|
|
585
|
-
code: "SOURCE_NOT_FILE",
|
|
586
|
-
message: "Source is not a regular file"
|
|
587
|
-
}
|
|
588
|
-
}, 400);
|
|
589
|
-
const importCfg = resolveWorkspaceImportConfig(service.currentConfig);
|
|
590
|
-
if (sourceStat.size > importCfg.maxBytes) return c.json({
|
|
591
|
-
ok: false,
|
|
592
|
-
error: {
|
|
593
|
-
code: "SOURCE_TOO_LARGE",
|
|
594
|
-
message: `Source exceeds maximum import size (${importCfg.maxBytes} bytes)`
|
|
595
|
-
}
|
|
596
|
-
}, 413);
|
|
597
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, sessionKey, void 0);
|
|
598
|
-
if (ws.ok === false) return c.json({
|
|
599
|
-
ok: false,
|
|
600
|
-
error: {
|
|
601
|
-
code: "WORKSPACE_RESOLUTION_FAILED",
|
|
602
|
-
message: ws.message
|
|
603
|
-
}
|
|
604
|
-
}, 400);
|
|
605
|
-
const workspaceRoot = ws.root;
|
|
606
|
-
let body;
|
|
607
|
-
try {
|
|
608
|
-
body = await c.req.json().catch(() => ({}));
|
|
609
|
-
} catch {
|
|
610
|
-
body = {};
|
|
611
|
-
}
|
|
612
|
-
const requestedDestRaw = typeof body.destination === "string" ? body.destination.trim() : "";
|
|
613
|
-
const onConflictRaw = typeof body.onConflict === "string" ? body.onConflict : "rename";
|
|
614
|
-
if (onConflictRaw !== "rename" && onConflictRaw !== "overwrite" && onConflictRaw !== "error") return c.json({
|
|
615
|
-
ok: false,
|
|
616
|
-
error: {
|
|
617
|
-
code: "INVALID_CONFLICT_MODE",
|
|
618
|
-
message: "Invalid onConflict value"
|
|
619
|
-
}
|
|
620
|
-
}, 400);
|
|
621
|
-
const onConflict = onConflictRaw;
|
|
622
|
-
if (onConflict === "overwrite" && !importCfg.allowOverwrite) return c.json({
|
|
623
|
-
ok: false,
|
|
624
|
-
error: {
|
|
625
|
-
code: "OVERWRITE_DISABLED",
|
|
626
|
-
message: "Overwrite is disabled by config"
|
|
627
|
-
}
|
|
628
|
-
}, 403);
|
|
629
|
-
const sourceBasename = sanitizeImportBasename(basename(ref.absolutePath)) || "imported-file";
|
|
630
|
-
let requestedRel;
|
|
631
|
-
if (!requestedDestRaw) requestedRel = `${importCfg.targetDir}/${sourceBasename}`;
|
|
632
|
-
else {
|
|
633
|
-
const trimmedDest = requestedDestRaw.replace(/\\/g, "/");
|
|
634
|
-
requestedRel = trimmedDest.endsWith("/") ? `${trimmedDest}${sourceBasename}` : trimmedDest;
|
|
635
|
-
}
|
|
636
|
-
let initialDestAbs = resolveWorkspaceSafePath(workspaceRoot, requestedRel);
|
|
637
|
-
if (!initialDestAbs) return c.json({
|
|
38
|
+
const body = await c.req.json().catch(() => null);
|
|
39
|
+
if (!body || typeof body.content !== "string") return c.json({
|
|
638
40
|
ok: false,
|
|
639
|
-
error: {
|
|
640
|
-
code: "INVALID_DESTINATION",
|
|
641
|
-
message: "Invalid destination path"
|
|
642
|
-
}
|
|
41
|
+
error: { message: "content is required" }
|
|
643
42
|
}, 400);
|
|
644
|
-
const writePolicy = validateWritePath(initialDestAbs, workspaceRoot);
|
|
645
|
-
if (!writePolicy.allowed) return c.json({
|
|
646
|
-
ok: false,
|
|
647
|
-
error: {
|
|
648
|
-
code: "DESTINATION_BLOCKED",
|
|
649
|
-
message: writePolicy.reason ?? "Destination blocked"
|
|
650
|
-
}
|
|
651
|
-
}, 403);
|
|
652
|
-
if (resolve(ref.absolutePath) === resolve(initialDestAbs)) return c.json({
|
|
653
|
-
ok: false,
|
|
654
|
-
error: {
|
|
655
|
-
code: "SAME_LOCATION",
|
|
656
|
-
message: "Destination is the same as source"
|
|
657
|
-
}
|
|
658
|
-
}, 400);
|
|
659
|
-
const destDir = dirname(initialDestAbs);
|
|
660
43
|
try {
|
|
661
|
-
await
|
|
662
|
-
} catch (err) {
|
|
663
|
-
log.warn({
|
|
664
|
-
err,
|
|
665
|
-
destDir
|
|
666
|
-
}, "Failed to create import destination directory");
|
|
667
|
-
return c.json({
|
|
668
|
-
ok: false,
|
|
669
|
-
error: {
|
|
670
|
-
code: "IMPORT_FAILED",
|
|
671
|
-
message: "Failed to prepare destination"
|
|
672
|
-
}
|
|
673
|
-
}, 500);
|
|
674
|
-
}
|
|
675
|
-
const tmpAbs = join(destDir, `.${basename(initialDestAbs)}.import-${randomUUID()}.tmp`);
|
|
676
|
-
const started = Date.now();
|
|
677
|
-
let renamed = false;
|
|
678
|
-
let overwrote = false;
|
|
679
|
-
let finalDestAbs = initialDestAbs;
|
|
680
|
-
try {
|
|
681
|
-
try {
|
|
682
|
-
await copyFile(ref.absolutePath, tmpAbs, constants.COPYFILE_FICLONE);
|
|
683
|
-
} catch (err) {
|
|
684
|
-
await unlink(tmpAbs).catch(() => {});
|
|
685
|
-
log.warn({
|
|
686
|
-
err,
|
|
687
|
-
source: ref.absolutePath,
|
|
688
|
-
tmpAbs
|
|
689
|
-
}, "Failed to copy source into staging tmp");
|
|
690
|
-
return c.json({
|
|
691
|
-
ok: false,
|
|
692
|
-
error: {
|
|
693
|
-
code: "IMPORT_FAILED",
|
|
694
|
-
message: "Failed to copy source file"
|
|
695
|
-
}
|
|
696
|
-
}, 500);
|
|
697
|
-
}
|
|
698
|
-
if (onConflict === "overwrite") {
|
|
699
|
-
overwrote = await stat(initialDestAbs).then(() => true).catch(() => false);
|
|
700
|
-
try {
|
|
701
|
-
await rename(tmpAbs, initialDestAbs);
|
|
702
|
-
} catch (err) {
|
|
703
|
-
await unlink(tmpAbs).catch(() => {});
|
|
704
|
-
log.warn({
|
|
705
|
-
err,
|
|
706
|
-
target: initialDestAbs
|
|
707
|
-
}, "Atomic rename failed");
|
|
708
|
-
return c.json({
|
|
709
|
-
ok: false,
|
|
710
|
-
error: {
|
|
711
|
-
code: "IMPORT_FAILED",
|
|
712
|
-
message: "Failed to finalize import"
|
|
713
|
-
}
|
|
714
|
-
}, 500);
|
|
715
|
-
}
|
|
716
|
-
} else if (onConflict === "error") {
|
|
717
|
-
if (await stat(initialDestAbs).then(() => true).catch(() => false)) {
|
|
718
|
-
await unlink(tmpAbs).catch(() => {});
|
|
719
|
-
return c.json({
|
|
720
|
-
ok: false,
|
|
721
|
-
error: {
|
|
722
|
-
code: "DESTINATION_EXISTS",
|
|
723
|
-
message: "Destination already exists"
|
|
724
|
-
}
|
|
725
|
-
}, 409);
|
|
726
|
-
}
|
|
727
|
-
try {
|
|
728
|
-
await link(tmpAbs, initialDestAbs);
|
|
729
|
-
await unlink(tmpAbs).catch(() => {});
|
|
730
|
-
} catch (err) {
|
|
731
|
-
await unlink(tmpAbs).catch(() => {});
|
|
732
|
-
log.warn({
|
|
733
|
-
err,
|
|
734
|
-
target: initialDestAbs
|
|
735
|
-
}, "Hard link to destination failed");
|
|
736
|
-
return c.json({
|
|
737
|
-
ok: false,
|
|
738
|
-
error: {
|
|
739
|
-
code: "IMPORT_FAILED",
|
|
740
|
-
message: "Failed to finalize import"
|
|
741
|
-
}
|
|
742
|
-
}, 500);
|
|
743
|
-
}
|
|
744
|
-
} else {
|
|
745
|
-
const picked = await pickAvailableTargetWithLink(tmpAbs, initialDestAbs);
|
|
746
|
-
if (!picked.ok) {
|
|
747
|
-
await unlink(tmpAbs).catch(() => {});
|
|
748
|
-
log.warn({
|
|
749
|
-
target: initialDestAbs,
|
|
750
|
-
attempts: picked.attempts
|
|
751
|
-
}, "Failed to find free import target name");
|
|
752
|
-
return c.json({
|
|
753
|
-
ok: false,
|
|
754
|
-
error: {
|
|
755
|
-
code: "IMPORT_FAILED",
|
|
756
|
-
message: "No free destination name available"
|
|
757
|
-
}
|
|
758
|
-
}, 500);
|
|
759
|
-
}
|
|
760
|
-
await unlink(tmpAbs).catch(() => {});
|
|
761
|
-
finalDestAbs = picked.path;
|
|
762
|
-
renamed = picked.path !== initialDestAbs;
|
|
763
|
-
}
|
|
764
|
-
} catch (err) {
|
|
765
|
-
await unlink(tmpAbs).catch(() => {});
|
|
766
|
-
log.error({ err }, "Import file unexpected failure");
|
|
767
|
-
return c.json({
|
|
768
|
-
ok: false,
|
|
769
|
-
error: {
|
|
770
|
-
code: "IMPORT_FAILED",
|
|
771
|
-
message: "Import failed"
|
|
772
|
-
}
|
|
773
|
-
}, 500);
|
|
774
|
-
}
|
|
775
|
-
let finalMtime;
|
|
776
|
-
try {
|
|
777
|
-
finalMtime = (await stat(finalDestAbs)).mtimeMs;
|
|
778
|
-
} catch {
|
|
779
|
-
finalMtime = Date.now();
|
|
780
|
-
}
|
|
781
|
-
const workspaceRel = toWorkspaceRelativePosix(workspaceRoot, finalDestAbs);
|
|
782
|
-
fileReferenceRegistry.expireById(id);
|
|
783
|
-
const newRef = fileReferenceRegistry.register({
|
|
784
|
-
absolutePath: finalDestAbs,
|
|
785
|
-
sessionKey: sessionKey || void 0,
|
|
786
|
-
scope: "workspace",
|
|
787
|
-
capabilities: fileReferenceCapabilities("workspace", false)
|
|
788
|
-
});
|
|
789
|
-
const sourceScope = ref.scope;
|
|
790
|
-
const sourceLocationKind = ref.locationKind;
|
|
791
|
-
service.emit("workspace.file-imported", {
|
|
792
|
-
sessionKey: sessionKey || void 0,
|
|
793
|
-
workspaceRelativePath: workspaceRel,
|
|
794
|
-
absolutePath: finalDestAbs,
|
|
795
|
-
bytes: sourceStat.size,
|
|
796
|
-
sourceScope,
|
|
797
|
-
sourceLocationKind
|
|
798
|
-
});
|
|
799
|
-
log.info({
|
|
800
|
-
sessionKey,
|
|
801
|
-
fileRefId: id,
|
|
802
|
-
sourceAbsolutePath: ref.absolutePath,
|
|
803
|
-
sourceScope,
|
|
804
|
-
sourceLocationKind,
|
|
805
|
-
destWorkspaceRelativePath: workspaceRel,
|
|
806
|
-
bytes: sourceStat.size,
|
|
807
|
-
renamed,
|
|
808
|
-
overwrote,
|
|
809
|
-
durationMs: Date.now() - started
|
|
810
|
-
}, "Workspace file import succeeded");
|
|
811
|
-
return c.json({
|
|
812
|
-
ok: true,
|
|
813
|
-
payload: {
|
|
814
|
-
workspaceRelativePath: workspaceRel,
|
|
815
|
-
absolutePath: finalDestAbs,
|
|
816
|
-
bytesCopied: sourceStat.size,
|
|
817
|
-
sourceAbsolutePath: ref.absolutePath,
|
|
818
|
-
sourceScope,
|
|
819
|
-
sourceLocationKind,
|
|
820
|
-
renamed,
|
|
821
|
-
overwrote,
|
|
822
|
-
mtimeMs: finalMtime,
|
|
823
|
-
newFileRefId: newRef.id
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
});
|
|
827
|
-
/**
|
|
828
|
-
* Serve a workspace file as raw bytes (e.g. <img> after auth fetch + blob URL).
|
|
829
|
-
* Path is workspace-relative; scope via sessionKey / agentId like other editor routes.
|
|
830
|
-
*/
|
|
831
|
-
authenticated.get("/api/workspace/editor/raw", async (c) => {
|
|
832
|
-
const pathRel = typeof c.req.query("path") === "string" ? c.req.query("path") : "";
|
|
833
|
-
if (!pathRel.trim()) return c.json({
|
|
834
|
-
ok: false,
|
|
835
|
-
error: { message: "Missing path" }
|
|
836
|
-
}, 400);
|
|
837
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
838
|
-
if (ws.ok === false) return c.json({
|
|
839
|
-
ok: false,
|
|
840
|
-
error: { message: ws.message }
|
|
841
|
-
}, 400);
|
|
842
|
-
const workspaceRoot = ws.root;
|
|
843
|
-
const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
|
|
844
|
-
if (!abs) return c.json({
|
|
845
|
-
ok: false,
|
|
846
|
-
error: { message: "Invalid path" }
|
|
847
|
-
}, 400);
|
|
848
|
-
let st;
|
|
849
|
-
try {
|
|
850
|
-
st = await stat(abs);
|
|
851
|
-
} catch {
|
|
852
|
-
return c.json({
|
|
853
|
-
ok: false,
|
|
854
|
-
error: { message: "Not found" }
|
|
855
|
-
}, 404);
|
|
856
|
-
}
|
|
857
|
-
if (!st.isFile()) return c.json({
|
|
858
|
-
ok: false,
|
|
859
|
-
error: { message: "Not a file" }
|
|
860
|
-
}, 400);
|
|
861
|
-
const contentType = {
|
|
862
|
-
png: "image/png",
|
|
863
|
-
jpg: "image/jpeg",
|
|
864
|
-
jpeg: "image/jpeg",
|
|
865
|
-
gif: "image/gif",
|
|
866
|
-
webp: "image/webp",
|
|
867
|
-
bmp: "image/bmp",
|
|
868
|
-
svg: "image/svg+xml",
|
|
869
|
-
pdf: "application/pdf",
|
|
870
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
871
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
872
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
873
|
-
txt: "text/plain",
|
|
874
|
-
md: "text/markdown",
|
|
875
|
-
json: "application/json",
|
|
876
|
-
html: "text/html",
|
|
877
|
-
css: "text/css",
|
|
878
|
-
js: "text/javascript",
|
|
879
|
-
ts: "text/typescript",
|
|
880
|
-
mp3: "audio/mpeg",
|
|
881
|
-
wav: "audio/wav",
|
|
882
|
-
ogg: "audio/ogg",
|
|
883
|
-
webm: "video/webm",
|
|
884
|
-
mp4: "video/mp4",
|
|
885
|
-
mov: "video/quicktime"
|
|
886
|
-
}[pathRel.split(".").pop()?.toLowerCase() ?? ""] || "application/octet-stream";
|
|
887
|
-
try {
|
|
888
|
-
const buf = await readFile(abs);
|
|
889
|
-
return new Response(buf, { headers: {
|
|
890
|
-
"Content-Type": contentType,
|
|
891
|
-
"Cache-Control": "private, max-age=3600"
|
|
892
|
-
} });
|
|
893
|
-
} catch {
|
|
894
|
-
return c.json({
|
|
895
|
-
ok: false,
|
|
896
|
-
error: { message: "Read failed" }
|
|
897
|
-
}, 500);
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
authenticated.put("/api/workspace/editor/write", async (c) => {
|
|
901
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
902
|
-
if (ws.ok === false) return c.json({
|
|
903
|
-
ok: false,
|
|
904
|
-
error: { message: ws.message }
|
|
905
|
-
}, 400);
|
|
906
|
-
const workspaceRoot = ws.root;
|
|
907
|
-
let body;
|
|
908
|
-
try {
|
|
909
|
-
body = await c.req.json();
|
|
910
|
-
} catch {
|
|
911
|
-
return c.json({
|
|
912
|
-
ok: false,
|
|
913
|
-
error: { message: "Invalid JSON" }
|
|
914
|
-
}, 400);
|
|
915
|
-
}
|
|
916
|
-
const pathRel = typeof body === "object" && body !== null && "path" in body && typeof body.path === "string" ? body.path : "";
|
|
917
|
-
const content = typeof body === "object" && body !== null && "content" in body && typeof body.content === "string" ? body.content : "";
|
|
918
|
-
if (!pathRel.trim()) return c.json({
|
|
919
|
-
ok: false,
|
|
920
|
-
error: { message: "Missing path" }
|
|
921
|
-
}, 400);
|
|
922
|
-
const abs = resolveWorkspaceSafePath(workspaceRoot, pathRel);
|
|
923
|
-
if (!abs) return c.json({
|
|
924
|
-
ok: false,
|
|
925
|
-
error: { message: "Invalid path" }
|
|
926
|
-
}, 400);
|
|
927
|
-
let st;
|
|
928
|
-
try {
|
|
929
|
-
st = await stat(abs);
|
|
930
|
-
} catch {
|
|
931
|
-
st = void 0;
|
|
932
|
-
}
|
|
933
|
-
if (st && !st.isFile()) return c.json({
|
|
934
|
-
ok: false,
|
|
935
|
-
error: { message: "Not a file" }
|
|
936
|
-
}, 400);
|
|
937
|
-
try {
|
|
938
|
-
await writeFile(abs, content, "utf-8");
|
|
939
|
-
let mtimeMs;
|
|
940
|
-
try {
|
|
941
|
-
mtimeMs = (await stat(abs)).mtimeMs;
|
|
942
|
-
} catch {
|
|
943
|
-
mtimeMs = Date.now();
|
|
944
|
-
}
|
|
44
|
+
await writeFile(path, body.content, "utf8");
|
|
945
45
|
return c.json({
|
|
946
46
|
ok: true,
|
|
947
|
-
payload: {
|
|
948
|
-
path: toWorkspaceRelativePosix(workspaceRoot, abs),
|
|
949
|
-
mtimeMs
|
|
950
|
-
}
|
|
47
|
+
payload: { file: "HEARTBEAT.md" }
|
|
951
48
|
});
|
|
952
|
-
} catch (
|
|
49
|
+
} catch (error) {
|
|
953
50
|
log.error({
|
|
954
|
-
err,
|
|
955
|
-
path
|
|
956
|
-
}, "
|
|
51
|
+
err: error,
|
|
52
|
+
path
|
|
53
|
+
}, "Failed to write HEARTBEAT.md");
|
|
957
54
|
return c.json({
|
|
958
55
|
ok: false,
|
|
959
56
|
error: { message: "Write failed" }
|
|
960
57
|
}, 500);
|
|
961
58
|
}
|
|
962
59
|
});
|
|
963
|
-
authenticated.get("/api/workspace/editor/search", async (c) => {
|
|
964
|
-
const q = typeof c.req.query("q") === "string" ? c.req.query("q").trim() : "";
|
|
965
|
-
const dirRel = typeof c.req.query("dir") === "string" ? c.req.query("dir") : "";
|
|
966
|
-
if (!q) return c.json({
|
|
967
|
-
ok: true,
|
|
968
|
-
payload: { results: [] }
|
|
969
|
-
});
|
|
970
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
971
|
-
if (ws.ok === false) return c.json({
|
|
972
|
-
ok: false,
|
|
973
|
-
error: { message: ws.message }
|
|
974
|
-
}, 400);
|
|
975
|
-
const workspaceRoot = ws.root;
|
|
976
|
-
const absDir = resolveWorkspaceSafePath(workspaceRoot, dirRel);
|
|
977
|
-
if (!absDir) return c.json({
|
|
978
|
-
ok: false,
|
|
979
|
-
error: { message: "Invalid path" }
|
|
980
|
-
}, 400);
|
|
981
|
-
let st;
|
|
982
|
-
try {
|
|
983
|
-
st = await stat(absDir);
|
|
984
|
-
} catch {
|
|
985
|
-
return c.json({
|
|
986
|
-
ok: false,
|
|
987
|
-
error: { message: "Not found" }
|
|
988
|
-
}, 404);
|
|
989
|
-
}
|
|
990
|
-
if (!st.isDirectory()) return c.json({
|
|
991
|
-
ok: false,
|
|
992
|
-
error: { message: "Not a directory" }
|
|
993
|
-
}, 400);
|
|
994
|
-
const results = (await runRipgrepInDirectory(q, absDir)).filter((r) => isPathUnderWorkspace(workspaceRoot, r.filePath)).map((r) => ({
|
|
995
|
-
...r,
|
|
996
|
-
filePath: toWorkspaceRelativePosix(workspaceRoot, resolve(r.filePath))
|
|
997
|
-
}));
|
|
998
|
-
return c.json({
|
|
999
|
-
ok: true,
|
|
1000
|
-
payload: { results }
|
|
1001
|
-
});
|
|
1002
|
-
});
|
|
1003
|
-
/** Fuzzy filename / path search over the session workspace (ripgrep `--files` + subsequence scoring). */
|
|
1004
|
-
authenticated.get("/api/workspace/editor/files/search", async (c) => {
|
|
1005
|
-
const q = typeof c.req.query("q") === "string" ? c.req.query("q").trim() : "";
|
|
1006
|
-
const limitRaw = c.req.query("limit");
|
|
1007
|
-
const limit = Math.min(Math.max(parseInt(typeof limitRaw === "string" ? limitRaw : "15", 10) || 15, 1), 50);
|
|
1008
|
-
const ws = await resolveEditorWorkspaceRootAsync(service, service.currentConfig, c.req.query("sessionKey"), c.req.query("agentId"));
|
|
1009
|
-
if (ws.ok === false) return c.json({
|
|
1010
|
-
ok: false,
|
|
1011
|
-
error: { message: ws.message }
|
|
1012
|
-
}, 400);
|
|
1013
|
-
const entries = await fuzzySearchWorkspaceFiles(ws.root, q, limit);
|
|
1014
|
-
return c.json({
|
|
1015
|
-
ok: true,
|
|
1016
|
-
payload: { entries }
|
|
1017
|
-
});
|
|
1018
|
-
});
|
|
1019
60
|
}
|
|
1020
61
|
//#endregion
|
|
1021
62
|
export { registerWorkspaceRoutes };
|