aicodeman 1.41.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -21
- package/dist/attachment-registry.d.ts +2 -0
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +14 -3
- package/dist/attachment-registry.js.map +1 -1
- package/dist/cli-agent.d.ts +2 -0
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +13 -9
- package/dist/cli-agent.js.map +1 -1
- package/dist/cli.d.ts +25 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +97 -11
- package/dist/cli.js.map +1 -1
- package/dist/config/buffer-limits.d.ts +9 -0
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +9 -0
- package/dist/config/buffer-limits.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +10 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +26 -0
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +89 -6
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +61 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/folder-listing.d.ts +94 -0
- package/dist/folder-listing.d.ts.map +1 -0
- package/dist/folder-listing.js +241 -0
- package/dist/folder-listing.js.map +1 -0
- package/dist/git-worktrees.d.ts +292 -0
- package/dist/git-worktrees.d.ts.map +1 -0
- package/dist/git-worktrees.js +791 -0
- package/dist/git-worktrees.js.map +1 -0
- package/dist/hooks-config.d.ts +36 -16
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +45 -20
- package/dist/hooks-config.js.map +1 -1
- package/dist/html-view-capabilities.d.ts +54 -0
- package/dist/html-view-capabilities.d.ts.map +1 -0
- package/dist/html-view-capabilities.js +103 -0
- package/dist/html-view-capabilities.js.map +1 -0
- package/dist/mcp-server.d.ts +114 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +918 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-sync.d.ts +5 -0
- package/dist/mcp-sync.d.ts.map +1 -1
- package/dist/mcp-sync.js +51 -5
- package/dist/mcp-sync.js.map +1 -1
- package/dist/mux-interface.d.ts +36 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-files.d.ts +37 -0
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +103 -0
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-pane-history.d.ts +103 -0
- package/dist/remote-pane-history.d.ts.map +1 -0
- package/dist/remote-pane-history.js +258 -0
- package/dist/remote-pane-history.js.map +1 -0
- package/dist/remote-ssh-limiter.d.ts +16 -3
- package/dist/remote-ssh-limiter.d.ts.map +1 -1
- package/dist/remote-ssh-limiter.js +37 -6
- package/dist/remote-ssh-limiter.js.map +1 -1
- package/dist/services/unified-session-service.d.ts +8 -0
- package/dist/services/unified-session-service.d.ts.map +1 -1
- package/dist/services/unified-session-service.js +21 -0
- package/dist/services/unified-session-service.js.map +1 -1
- package/dist/session-activity.d.ts +51 -0
- package/dist/session-activity.d.ts.map +1 -1
- package/dist/session-activity.js +62 -0
- package/dist/session-activity.js.map +1 -1
- package/dist/session-submit-verifier.js +1 -1
- package/dist/session.d.ts +90 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +247 -9
- package/dist/session.js.map +1 -1
- package/dist/tab-layout-legacy-order.d.ts.map +1 -1
- package/dist/tab-layout-legacy-order.js +72 -13
- package/dist/tab-layout-legacy-order.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -2
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +102 -7
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tab-layout.d.ts +9 -0
- package/dist/tab-layout.d.ts.map +1 -1
- package/dist/tab-layout.js +15 -2
- package/dist/tab-layout.js.map +1 -1
- package/dist/tmux-manager.d.ts +130 -7
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +263 -36
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/session.d.ts +53 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +44 -7
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +72 -7
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
- package/dist/utils/deepseek-cli-resolver.js +1 -1
- package/dist/utils/deepseek-cli-resolver.js.map +1 -1
- package/dist/web/approval-inbox.d.ts +24 -2
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +8 -2
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +33 -0
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +8 -1
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.f04bb2a5.js +43 -0
- package/dist/web/public/app.f04bb2a5.js.br +0 -0
- package/dist/web/public/app.f04bb2a5.js.gz +0 -0
- package/dist/web/public/approvals-ui.js +4 -0
- package/dist/web/public/approvals-ui.js.br +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.bc9c1f63.js → constants.12b0c247.js} +395 -7
- package/dist/web/public/constants.12b0c247.js.br +0 -0
- package/dist/web/public/constants.12b0c247.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/gesture/gesture-codeman.js +112 -6
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.06659676.js +2 -0
- package/dist/web/public/i18n.06659676.js.br +0 -0
- package/dist/web/public/i18n.06659676.js.gz +0 -0
- package/dist/web/public/image-input.fcdcb46c.js +462 -0
- package/dist/web/public/image-input.fcdcb46c.js.br +0 -0
- package/dist/web/public/image-input.fcdcb46c.js.gz +0 -0
- package/dist/web/public/index.html +169 -24
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.c1e7c7b7.js → keyboard-accessory.b42f539b.js} +124 -44
- package/dist/web/public/keyboard-accessory.b42f539b.js.br +0 -0
- package/dist/web/public/keyboard-accessory.b42f539b.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +11 -2
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.2dff1580.css +1 -0
- package/dist/web/public/mobile.2dff1580.css.br +0 -0
- package/dist/web/public/mobile.2dff1580.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.71846fcb.js → panels-ui.73ec0137.js} +87 -87
- package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
- package/dist/web/public/panels-ui.73ec0137.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/{respawn-ui.ff0dae4c.js → respawn-ui.d7adcf6a.js} +1 -1
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.d52ff6aa.js +100 -0
- package/dist/web/public/session-ui.d52ff6aa.js.br +0 -0
- package/dist/web/public/session-ui.d52ff6aa.js.gz +0 -0
- package/dist/web/public/settings-ui.3a22bd2a.js +97 -0
- package/dist/web/public/settings-ui.3a22bd2a.js.br +0 -0
- package/dist/web/public/settings-ui.3a22bd2a.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js +11 -0
- package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
- package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.ba6a9b84.css +1 -0
- package/dist/web/public/styles.ba6a9b84.css.br +0 -0
- package/dist/web/public/styles.ba6a9b84.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +6 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js +1 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +45 -24
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.0a7ccc29.js +2 -0
- package/dist/web/public/terminal-ui.0a7ccc29.js.br +0 -0
- package/dist/web/public/terminal-ui.0a7ccc29.js.gz +0 -0
- package/dist/web/public/tile-grid.js +439 -18
- package/dist/web/public/tile-grid.js.br +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +12 -0
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +13 -0
- package/dist/web/route-helpers.d.ts +8 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +8 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +4 -1
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/approval-routes.d.ts.map +1 -1
- package/dist/web/routes/approval-routes.js +12 -10
- package/dist/web/routes/approval-routes.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +10 -3
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts +46 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +214 -27
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +8 -1
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/html-view-routes.d.ts +60 -0
- package/dist/web/routes/html-view-routes.d.ts.map +1 -0
- package/dist/web/routes/html-view-routes.js +288 -0
- package/dist/web/routes/html-view-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +19 -14
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -2
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +8 -5
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +462 -109
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +24 -6
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts +9 -0
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +91 -12
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/worktree-routes.d.ts +52 -0
- package/dist/web/routes/worktree-routes.d.ts.map +1 -0
- package/dist/web/routes/worktree-routes.js +220 -0
- package/dist/web/routes/worktree-routes.js.map +1 -0
- package/dist/web/schemas.d.ts +45 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +105 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/screen-blocked.d.ts +80 -0
- package/dist/web/screen-blocked.d.ts.map +1 -0
- package/dist/web/screen-blocked.js +106 -0
- package/dist/web/screen-blocked.js.map +1 -0
- package/dist/web/server.d.ts +6 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +73 -14
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +1 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +20 -0
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +27 -9
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +36 -13
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/sse-events.d.ts +12 -4
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +12 -4
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +3 -2
- package/skills/codeman/SKILL.md +47 -27
- package/skills/codeman/preamble.sh +14 -11
- package/skills/codeman/reference/endpoints.md +1 -1
- package/skills/codeman/reference/recipes.md +12 -9
- package/skills/codeman/reference/verbs.md +3 -1
- package/dist/web/public/app.6f8552e1.js +0 -43
- package/dist/web/public/app.6f8552e1.js.br +0 -0
- package/dist/web/public/app.6f8552e1.js.gz +0 -0
- package/dist/web/public/constants.bc9c1f63.js.br +0 -0
- package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
- package/dist/web/public/i18n.a9840ea2.js +0 -2
- package/dist/web/public/i18n.a9840ea2.js.br +0 -0
- package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
- package/dist/web/public/image-input.e00471c6.js +0 -283
- package/dist/web/public/image-input.e00471c6.js.br +0 -0
- package/dist/web/public/image-input.e00471c6.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/mobile.111b187d.css +0 -1
- package/dist/web/public/mobile.111b187d.css.br +0 -0
- package/dist/web/public/mobile.111b187d.css.gz +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.br +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/session-ui.f731602c.js +0 -100
- package/dist/web/public/session-ui.f731602c.js.br +0 -0
- package/dist/web/public/session-ui.f731602c.js.gz +0 -0
- package/dist/web/public/settings-ui.4e33390f.js +0 -97
- package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
- package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
- package/dist/web/public/styles.81a72906.css +0 -1
- package/dist/web/public/styles.81a72906.css.br +0 -0
- package/dist/web/public/styles.81a72906.css.gz +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
- package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
- package/dist/web/public/terminal-ui.b7267005.js +0 -2
- package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
- package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
|
@@ -12,8 +12,10 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
|
|
14
14
|
import { compileFileQuery } from '../../utils/file-query.js';
|
|
15
|
+
import { describeUnknownPath, probePathKind } from '../../utils/bounded-path-probe.js';
|
|
15
16
|
import { fileStreamManager } from '../../file-stream-manager.js';
|
|
16
|
-
import { AUDIO_ATTACHMENT_EXTENSIONS, AttachmentRegistrationError, attachmentRecordToEvent, attachmentRegistry, buildFileThumbnailRoute, isSupportedAttachmentExtension, registerExternalAttachment, TEXT_ATTACHMENT_EXTENSIONS, VIDEO_ATTACHMENT_EXTENSIONS, } from '../../attachment-registry.js';
|
|
17
|
+
import { AUDIO_ATTACHMENT_EXTENSIONS, AttachmentRegistrationError, attachmentRecordToEvent, attachmentRegistry, buildFileThumbnailRoute, getAttachmentType, isSupportedAttachmentExtension, registerExternalAttachment, TEXT_ATTACHMENT_EXTENSIONS, VIDEO_ATTACHMENT_EXTENSIONS, } from '../../attachment-registry.js';
|
|
18
|
+
import { FolderListingError, folderFilePath, folderGrants, folderAllowedForUser, listFolder, validFolderFileName, } from '../../folder-listing.js';
|
|
17
19
|
import { generateFirstPageThumbnail } from '../../document-thumbnailer.js';
|
|
18
20
|
import { getOfficePreviewPdfPath, getPreviewPdfDownloadName } from '../../document-preview-cache.js';
|
|
19
21
|
import { sanitizeAttachmentHistoryItem } from '../../session-attachment-history.js';
|
|
@@ -125,8 +127,8 @@ function sendRawStream(reply, content, cleanup) {
|
|
|
125
127
|
});
|
|
126
128
|
content.pipe(reply.raw);
|
|
127
129
|
}
|
|
128
|
-
/** Byte source for a LOCAL file (the historical, only path). */
|
|
129
|
-
function localFileSource(resolvedPath) {
|
|
130
|
+
/** Byte source for a LOCAL file (the historical, only path). Shared with html-view-routes.ts. */
|
|
131
|
+
export function localFileSource(resolvedPath) {
|
|
130
132
|
return (range) => ({
|
|
131
133
|
content: createReadStream(resolvedPath, range ? { start: range.start, end: range.end } : undefined),
|
|
132
134
|
});
|
|
@@ -145,8 +147,11 @@ function localFileSource(resolvedPath) {
|
|
|
145
147
|
* `open` supplies the bytes for the (optional) range, which is what lets a remote
|
|
146
148
|
* case reuse this instead of re-implementing the 206/416 contract: same headers,
|
|
147
149
|
* same status codes, whether the file sits on this host or behind an ssh pipe.
|
|
150
|
+
*
|
|
151
|
+
* Exported for the rendered-HTML route (html-view-routes.ts), so a page's own
|
|
152
|
+
* `<video>`/`<audio>` can seek as well.
|
|
148
153
|
*/
|
|
149
|
-
function sendFileBody(reply, size, rangeHeader, open) {
|
|
154
|
+
export function sendFileBody(reply, size, rangeHeader, open) {
|
|
150
155
|
reply.header('Accept-Ranges', 'bytes');
|
|
151
156
|
const range = parseByteRange(rangeHeader, size);
|
|
152
157
|
if (range.kind === 'unsatisfiable') {
|
|
@@ -178,7 +183,9 @@ async function serveRawFile(reply, target, size, fileName, extension, download,
|
|
|
178
183
|
// would be stored XSS. SVG was always here; HTML/HTM join it now that the text
|
|
179
184
|
// family is servable, so widening what can be READ never widened what can RUN.
|
|
180
185
|
// The preview overlay reads these through `fetch()`, which ignores the
|
|
181
|
-
// disposition, so a clicked .html still
|
|
186
|
+
// disposition, so the source view of a clicked .html still works. Rendering it
|
|
187
|
+
// as a page is html-view-routes.ts's job (sandboxed, opaque origin), never this
|
|
188
|
+
// route's.
|
|
182
189
|
const markupOnly = extension === 'svg' || extension === 'html' || extension === 'htm';
|
|
183
190
|
if (download || markupOnly) {
|
|
184
191
|
reply.header('Content-Type', markupOnly ? 'application/octet-stream' : MIME_TYPES[extension] || 'application/octet-stream');
|
|
@@ -296,6 +303,32 @@ async function resolveServableRemoteAttachment(reply, record, scope) {
|
|
|
296
303
|
}
|
|
297
304
|
return { path: probe.realPath, probe };
|
|
298
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Stream a guard-checked file ({@link resolveServableAttachmentPath}) — the body of
|
|
308
|
+
* the attachment raw route, shared with the folder-grid file route.
|
|
309
|
+
*
|
|
310
|
+
* A remote file streams over ssh exactly like file-raw, with the same 200/206/416
|
|
311
|
+
* contract, and its size comes from the guard's own re-probe — so serving a remote
|
|
312
|
+
* file needs no stat the local branch would not also need.
|
|
313
|
+
*/
|
|
314
|
+
async function serveServableRaw(reply, servable, remote, fileName, extension, query) {
|
|
315
|
+
try {
|
|
316
|
+
const target = servable.probe && remote
|
|
317
|
+
? { kind: 'remote', resolvedPath: servable.path, relativePath: '', remote, probe: servable.probe }
|
|
318
|
+
: { kind: 'local', resolvedPath: servable.path, relativePath: '' };
|
|
319
|
+
const size = servable.probe ? servable.probe.size : (await fs.stat(servable.path)).size;
|
|
320
|
+
if (exceedsXlsxPreviewLimit(extension, query, size)) {
|
|
321
|
+
sendXlsxPreviewTooLarge(reply, size);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await serveRawFile(reply, target, size, fileName, extension, query.download === 'true', query.range);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
reply
|
|
328
|
+
.code(err instanceof RemoteFileAccessError ? 502 : 500)
|
|
329
|
+
.send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to read file: ${getErrorMessage(err)}`));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
299
332
|
/**
|
|
300
333
|
* Convert a DOCX/PPTX to a single-PDF preview (LibreOffice when available) and
|
|
301
334
|
* stream it inline. PDF/PNG and text formats don't need conversion — callers
|
|
@@ -350,6 +383,14 @@ function getKnownSessionWorkingDir(ctx, sessionId, reply, req) {
|
|
|
350
383
|
// wrapper exists for the callers that only need the workspace PATH.
|
|
351
384
|
return getKnownSessionFileScope(ctx, sessionId, reply, req)?.workingDir;
|
|
352
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* A Docker case is a LOCAL session for the file routes (the workspace is a
|
|
388
|
+
* bind mount, so its paths are host paths), but `~` inside the container is the
|
|
389
|
+
* container's own seeded home, not this host's. Expanding `~/x.png` against the
|
|
390
|
+
* host's home would open a different file than the one the agent printed, so
|
|
391
|
+
* the form is refused instead of guessed.
|
|
392
|
+
*/
|
|
393
|
+
export const DOCKER_HOME_PATH_ERROR = 'A ~/ path inside a Docker case is the container home, which this server cannot read; use the absolute path';
|
|
353
394
|
/**
|
|
354
395
|
* The file-access scope of a session, live or persisted — the `workingDir`-only
|
|
355
396
|
* variant of {@link getKnownSessionWorkingDir}, plus the remote metadata the routes
|
|
@@ -362,11 +403,11 @@ function getKnownSessionFileScope(ctx, sessionId, reply, req) {
|
|
|
362
403
|
const user = getAuthUser(req);
|
|
363
404
|
const liveSession = ctx.sessions.get(sessionId);
|
|
364
405
|
if (liveSession && canAccessOwned(user, liveSession.owner)) {
|
|
365
|
-
return { workingDir: liveSession.workingDir, remote: liveSession.remote };
|
|
406
|
+
return { workingDir: liveSession.workingDir, remote: liveSession.remote, docker: !!liveSession.docker };
|
|
366
407
|
}
|
|
367
408
|
const stored = ctx.store.getSession(sessionId);
|
|
368
409
|
if (stored && canAccessOwned(user, stored.owner)) {
|
|
369
|
-
return { workingDir: stored.workingDir, remote: stored.remote };
|
|
410
|
+
return { workingDir: stored.workingDir, remote: stored.remote, docker: !!stored.docker };
|
|
370
411
|
}
|
|
371
412
|
reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${sessionId} not found`));
|
|
372
413
|
return undefined;
|
|
@@ -1725,6 +1766,10 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1725
1766
|
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing attachment path'));
|
|
1726
1767
|
return;
|
|
1727
1768
|
}
|
|
1769
|
+
if (session.docker && body.path.startsWith('~/')) {
|
|
1770
|
+
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_HOME_PATH_ERROR));
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1728
1773
|
try {
|
|
1729
1774
|
// A remote case registers a path that lives on the REMOTE host: the guard and
|
|
1730
1775
|
// the reachability check happen there (#415 — this is the path a clicked
|
|
@@ -1839,26 +1884,11 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1839
1884
|
});
|
|
1840
1885
|
if (!servable)
|
|
1841
1886
|
return;
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const target = servable.probe && remote
|
|
1848
|
-
? { kind: 'remote', resolvedPath: servable.path, relativePath: '', remote, probe: servable.probe }
|
|
1849
|
-
: { kind: 'local', resolvedPath: servable.path, relativePath: '' };
|
|
1850
|
-
const size = servable.probe ? servable.probe.size : (await fs.stat(servable.path)).size;
|
|
1851
|
-
if (exceedsXlsxPreviewLimit(record.extension, { preview, download }, size)) {
|
|
1852
|
-
sendXlsxPreviewTooLarge(reply, size);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
await serveRawFile(reply, target, size, record.fileName, record.extension, download === 'true', req.headers.range);
|
|
1856
|
-
}
|
|
1857
|
-
catch (err) {
|
|
1858
|
-
reply
|
|
1859
|
-
.code(err instanceof RemoteFileAccessError ? 502 : 500)
|
|
1860
|
-
.send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to read file: ${getErrorMessage(err)}`));
|
|
1861
|
-
}
|
|
1887
|
+
await serveServableRaw(reply, servable, session.remote, record.fileName, record.extension, {
|
|
1888
|
+
download,
|
|
1889
|
+
preview,
|
|
1890
|
+
range: req.headers.range,
|
|
1891
|
+
});
|
|
1862
1892
|
});
|
|
1863
1893
|
// Serve a converted PDF preview of a registered attachment by id. Office docs
|
|
1864
1894
|
// convert server-side; PDF/PNG/text redirect to the raw route.
|
|
@@ -1910,6 +1940,163 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1910
1940
|
}
|
|
1911
1941
|
await serveThumbnail(reply, servable.path, record.extension);
|
|
1912
1942
|
});
|
|
1943
|
+
// ===== Folder grid =====
|
|
1944
|
+
// Grant + list a folder (absolute or `~/` on the session's host) for the preview
|
|
1945
|
+
// grid. Same guard as a single external attachment; the grid then fetches files by
|
|
1946
|
+
// `folderId + name`, so no browser request carries a path. See folder-listing.ts.
|
|
1947
|
+
app.post('/api/sessions/:id/folders', async (req, reply) => {
|
|
1948
|
+
const { id } = req.params;
|
|
1949
|
+
const scope = getKnownSessionFileScope(ctx, id, reply, req);
|
|
1950
|
+
if (!scope)
|
|
1951
|
+
return;
|
|
1952
|
+
const body = (req.body || {});
|
|
1953
|
+
if (!body.path || typeof body.path !== 'string') {
|
|
1954
|
+
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing folder path'));
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
// A bare `~` expands to this host's home too, not the container's.
|
|
1958
|
+
if (scope.docker && (body.path === '~' || body.path.startsWith('~/'))) {
|
|
1959
|
+
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_HOME_PATH_ERROR));
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
try {
|
|
1963
|
+
const listing = await listFolder(id, body.path, {
|
|
1964
|
+
sessionWorkingDir: scope.workingDir,
|
|
1965
|
+
remote: scope.remote,
|
|
1966
|
+
user: getAuthUser(req),
|
|
1967
|
+
});
|
|
1968
|
+
return { success: true, data: listing };
|
|
1969
|
+
}
|
|
1970
|
+
catch (err) {
|
|
1971
|
+
if (err instanceof FolderListingError) {
|
|
1972
|
+
const code = err.statusCode === 404
|
|
1973
|
+
? ApiErrorCode.NOT_FOUND
|
|
1974
|
+
: err.statusCode >= 500
|
|
1975
|
+
? ApiErrorCode.OPERATION_FAILED
|
|
1976
|
+
: ApiErrorCode.INVALID_INPUT;
|
|
1977
|
+
reply.code(err.statusCode).send(createErrorResponse(code, err.message));
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
return reply
|
|
1981
|
+
.code(500)
|
|
1982
|
+
.send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to list folder: ${getErrorMessage(err)}`));
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
/**
|
|
1986
|
+
* One file of a granted folder, guard-checked like an attachment: the name is a
|
|
1987
|
+
* bare file name of a supported type, and the joined path goes through the same
|
|
1988
|
+
* {@link resolveServableAttachmentPath} (blocklist, workspace confinement, symlink
|
|
1989
|
+
* re-resolution, remote re-probe) an attachment by id does. The multi-user rule
|
|
1990
|
+
* ({@link folderAllowedForUser}) runs again on the resolved file: a symlink inside
|
|
1991
|
+
* the granted folder must not lead into another user's space.
|
|
1992
|
+
*/
|
|
1993
|
+
async function resolveFolderFile(req, reply) {
|
|
1994
|
+
const { id, folderId, name } = req.params;
|
|
1995
|
+
const scope = getKnownSessionFileScope(ctx, id, reply, req);
|
|
1996
|
+
if (!scope)
|
|
1997
|
+
return null;
|
|
1998
|
+
const grant = folderGrants.get(id, folderId);
|
|
1999
|
+
if (!grant) {
|
|
2000
|
+
reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'Folder not found'));
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
const fileName = validFolderFileName(name);
|
|
2004
|
+
if (!fileName) {
|
|
2005
|
+
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid file name'));
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
const filePath = folderFilePath(grant.dir, fileName);
|
|
2009
|
+
if (!scope.remote) {
|
|
2010
|
+
// Bounded, like the folder itself: the grant may point at a mount that has stopped answering since.
|
|
2011
|
+
const kind = await probePathKind(filePath, { pastCap: true });
|
|
2012
|
+
if (kind === 'unknown') {
|
|
2013
|
+
reply
|
|
2014
|
+
.code(503)
|
|
2015
|
+
.send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, describeUnknownPath('File', filePath, { pastCap: true })));
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
if (kind !== 'file') {
|
|
2019
|
+
reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found'));
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
const extension = extname(fileName).toLowerCase().replace(/^\./, '');
|
|
2024
|
+
const record = {
|
|
2025
|
+
attachmentId: '',
|
|
2026
|
+
sessionId: id,
|
|
2027
|
+
filePath,
|
|
2028
|
+
fileName,
|
|
2029
|
+
extension,
|
|
2030
|
+
attachmentType: getAttachmentType(extension),
|
|
2031
|
+
size: 0,
|
|
2032
|
+
mtimeMs: 0,
|
|
2033
|
+
timestamp: 0,
|
|
2034
|
+
source: 'external',
|
|
2035
|
+
};
|
|
2036
|
+
const servable = await resolveServableAttachmentPath(reply, record, scope);
|
|
2037
|
+
if (!servable)
|
|
2038
|
+
return null;
|
|
2039
|
+
// The requested NAME only opens the door; registration judges the file a
|
|
2040
|
+
// symlink resolves to (supported extension, regular file) and so does this.
|
|
2041
|
+
// Without it `shot.png -> ../secret.bin` is served, and a FIFO or a device
|
|
2042
|
+
// behind a supported name parks a worker on an open that never returns.
|
|
2043
|
+
if (!isSupportedAttachmentExtension(extname(servable.path))) {
|
|
2044
|
+
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Unsupported attachment type'));
|
|
2045
|
+
return null;
|
|
2046
|
+
}
|
|
2047
|
+
const isRegularFile = scope.remote
|
|
2048
|
+
? servable.probe?.kind === 'file'
|
|
2049
|
+
: await fs.stat(servable.path).then((stat) => stat.isFile(), () => false);
|
|
2050
|
+
if (!isRegularFile) {
|
|
2051
|
+
reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Attachment path is not a file'));
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
if (!folderAllowedForUser(servable.path, getAuthUser(req), scope)) {
|
|
2055
|
+
reply.code(403).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'File is outside your space'));
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
return { scope, servable, name: fileName, extension };
|
|
2059
|
+
}
|
|
2060
|
+
app.get('/api/sessions/:id/folders/:folderId/files/:name/raw', async (req, reply) => {
|
|
2061
|
+
const file = await resolveFolderFile(req, reply);
|
|
2062
|
+
if (!file)
|
|
2063
|
+
return;
|
|
2064
|
+
const { download, preview } = req.query;
|
|
2065
|
+
await serveServableRaw(reply, file.servable, file.scope.remote, file.name, file.extension, {
|
|
2066
|
+
download,
|
|
2067
|
+
preview,
|
|
2068
|
+
range: req.headers.range,
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
app.get('/api/sessions/:id/folders/:folderId/files/:name/preview', async (req, reply) => {
|
|
2072
|
+
const file = await resolveFolderFile(req, reply);
|
|
2073
|
+
if (!file)
|
|
2074
|
+
return;
|
|
2075
|
+
if (file.extension !== 'docx' && file.extension !== 'pptx') {
|
|
2076
|
+
const { id, folderId } = req.params;
|
|
2077
|
+
reply.redirect(`/api/sessions/${encodeURIComponent(id)}/folders/${encodeURIComponent(folderId)}/files/${encodeURIComponent(file.name)}/raw`);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
if (file.servable.probe) {
|
|
2081
|
+
reply
|
|
2082
|
+
.code(400)
|
|
2083
|
+
.send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Office document preview is not available for files in a remote (SSH) case'));
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
await serveConvertedPreview(reply, file.servable.path, file.name, file.extension);
|
|
2087
|
+
});
|
|
2088
|
+
app.get('/api/sessions/:id/folders/:folderId/files/:name/thumbnail', async (req, reply) => {
|
|
2089
|
+
const file = await resolveFolderFile(req, reply);
|
|
2090
|
+
if (!file)
|
|
2091
|
+
return;
|
|
2092
|
+
if (file.servable.probe) {
|
|
2093
|
+
reply
|
|
2094
|
+
.code(400)
|
|
2095
|
+
.send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Thumbnails are not available for files in a remote (SSH) case'));
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
await serveThumbnail(reply, file.servable.path, file.extension);
|
|
2099
|
+
});
|
|
1913
2100
|
// Serve converted document previews for a workspace-relative path. DOCX/PPTX
|
|
1914
2101
|
// are converted to PDF via LibreOffice; PDF/PNG/text preview through file-raw.
|
|
1915
2102
|
app.get('/api/sessions/:id/file-preview', async (req, reply) => {
|