aicodeman 0.9.14 → 1.1.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 +5 -5
- package/dist/attachment-magic.d.ts +5 -0
- package/dist/attachment-magic.d.ts.map +1 -0
- package/dist/attachment-magic.js +34 -0
- package/dist/attachment-magic.js.map +1 -0
- package/dist/attachment-registry.d.ts +72 -0
- package/dist/attachment-registry.d.ts.map +1 -0
- package/dist/attachment-registry.js +166 -0
- package/dist/attachment-registry.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +85 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/attachment-guard.d.ts +71 -0
- package/dist/config/attachment-guard.d.ts.map +1 -0
- package/dist/config/attachment-guard.js +118 -0
- package/dist/config/attachment-guard.js.map +1 -0
- package/dist/document-conversion-limiter.d.ts +27 -0
- package/dist/document-conversion-limiter.d.ts.map +1 -0
- package/dist/document-conversion-limiter.js +64 -0
- package/dist/document-conversion-limiter.js.map +1 -0
- package/dist/document-preview-cache.d.ts +15 -0
- package/dist/document-preview-cache.d.ts.map +1 -0
- package/dist/document-preview-cache.js +261 -0
- package/dist/document-preview-cache.js.map +1 -0
- package/dist/document-thumbnailer.d.ts +9 -0
- package/dist/document-thumbnailer.d.ts.map +1 -0
- package/dist/document-thumbnailer.js +70 -0
- package/dist/document-thumbnailer.js.map +1 -0
- package/dist/hooks-config.d.ts +17 -0
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +164 -73
- package/dist/hooks-config.js.map +1 -1
- package/dist/image-watcher.d.ts +6 -1
- package/dist/image-watcher.d.ts.map +1 -1
- package/dist/image-watcher.js +52 -7
- package/dist/image-watcher.js.map +1 -1
- package/dist/session-attachment-history.d.ts +19 -0
- package/dist/session-attachment-history.d.ts.map +1 -0
- package/dist/session-attachment-history.js +63 -0
- package/dist/session-attachment-history.js.map +1 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +45 -0
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +6 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/session.d.ts +35 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/tools.d.ts +35 -1
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/types/tools.js +2 -1
- package/dist/types/tools.js.map +1 -1
- package/dist/usage-telemetry.d.ts +102 -0
- package/dist/usage-telemetry.d.ts.map +1 -0
- package/dist/usage-telemetry.js +130 -0
- package/dist/usage-telemetry.js.map +1 -0
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +4 -3
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/plan-usage-latest.d.ts +15 -0
- package/dist/web/plan-usage-latest.d.ts.map +1 -0
- package/dist/web/plan-usage-latest.js +20 -0
- package/dist/web/plan-usage-latest.js.map +1 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.9daf49ad.js +37 -0
- package/dist/web/public/app.9daf49ad.js.br +0 -0
- package/dist/web/public/app.9daf49ad.js.gz +0 -0
- package/dist/web/public/{constants.8fa1a65f.js → constants.00fa5405.js} +2 -0
- package/dist/web/public/constants.00fa5405.js.br +0 -0
- package/dist/web/public/constants.00fa5405.js.gz +0 -0
- package/dist/web/public/fonts/jetbrains-mono-variable.woff2 +0 -0
- package/dist/web/public/fonts/manrope-variable.woff2 +0 -0
- package/dist/web/public/image-input.0ea86695.js.gz +0 -0
- package/dist/web/public/index.html +59 -35
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.b8686b5e.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.bc753cc7.js.gz +0 -0
- package/dist/web/public/mobile-handlers.763a7439.js.gz +0 -0
- package/dist/web/public/mobile.06b38d3a.css +1 -0
- package/dist/web/public/mobile.06b38d3a.css.br +0 -0
- package/dist/web/public/mobile.06b38d3a.css.gz +0 -0
- package/dist/web/public/notification-manager.9c984ac2.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.6bb3169f.js → panels-ui.2f467969.js} +141 -87
- package/dist/web/public/panels-ui.2f467969.js.br +0 -0
- package/dist/web/public/panels-ui.2f467969.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/session-ui.1463b824.js +36 -0
- package/dist/web/public/session-ui.1463b824.js.br +0 -0
- package/dist/web/public/session-ui.1463b824.js.gz +0 -0
- package/dist/web/public/settings-ui.44b99ce0.js +55 -0
- package/dist/web/public/settings-ui.44b99ce0.js.br +0 -0
- package/dist/web/public/settings-ui.44b99ce0.js.gz +0 -0
- package/dist/web/public/styles.c13845d5.css +1 -0
- package/dist/web/public/styles.c13845d5.css.br +0 -0
- package/dist/web/public/styles.c13845d5.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.5bf97f7e.js +3 -0
- package/dist/web/public/terminal-ui.5bf97f7e.js.br +0 -0
- package/dist/web/public/terminal-ui.5bf97f7e.js.gz +0 -0
- package/dist/web/public/upload.html.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 +2 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.br +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-zerolag-input.137ad9f0.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.085e9e73.js.gz +0 -0
- package/dist/web/routes/file-routes.d.ts +2 -2
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +457 -17
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +31 -2
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts +17 -0
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -0
- package/dist/web/routes/status-telemetry-routes.js +57 -0
- package/dist/web/routes/status-telemetry-routes.js.map +1 -0
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +21 -1
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +30 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +52 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts +24 -0
- package/dist/web/sensitive-path.d.ts.map +1 -0
- package/dist/web/sensitive-path.js +39 -0
- package/dist/web/sensitive-path.js.map +1 -0
- package/dist/web/server.d.ts +13 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +77 -2
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +5 -1
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +9 -1
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/sse-events.d.ts +6 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +6 -0
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.js +3 -0
- package/dist/web/public/app.a23f8bf6.js +0 -35
- package/dist/web/public/app.a23f8bf6.js.br +0 -0
- package/dist/web/public/app.a23f8bf6.js.gz +0 -0
- package/dist/web/public/constants.8fa1a65f.js.br +0 -0
- package/dist/web/public/constants.8fa1a65f.js.gz +0 -0
- package/dist/web/public/mobile.c7513aed.css +0 -1
- package/dist/web/public/mobile.c7513aed.css.br +0 -0
- package/dist/web/public/mobile.c7513aed.css.gz +0 -0
- package/dist/web/public/panels-ui.6bb3169f.js.br +0 -0
- package/dist/web/public/panels-ui.6bb3169f.js.gz +0 -0
- package/dist/web/public/session-ui.34f25fdf.js +0 -36
- package/dist/web/public/session-ui.34f25fdf.js.br +0 -0
- package/dist/web/public/session-ui.34f25fdf.js.gz +0 -0
- package/dist/web/public/settings-ui.3a341938.js +0 -55
- package/dist/web/public/settings-ui.3a341938.js.br +0 -0
- package/dist/web/public/settings-ui.3a341938.js.gz +0 -0
- package/dist/web/public/styles.c14ecd8a.css +0 -1
- package/dist/web/public/styles.c14ecd8a.css.br +0 -0
- package/dist/web/public/styles.c14ecd8a.css.gz +0 -0
- package/dist/web/public/terminal-ui.1f5b45cf.js +0 -3
- package/dist/web/public/terminal-ui.1f5b45cf.js.br +0 -0
- package/dist/web/public/terminal-ui.1f5b45cf.js.gz +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Attachment path-guard configuration (COD-53).
|
|
3
|
+
*
|
|
4
|
+
* Governs which host files may be registered as cross-workspace attachments
|
|
5
|
+
* and served to the browser. Two operator-facing knobs, both with safe
|
|
6
|
+
* defaults:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Blocked-path blocklist (DEFAULT, configurable).** Pre-populated with the
|
|
9
|
+
* shared secret-location blocklist (`isSensitivePath`) PLUS the directory
|
|
10
|
+
* trees `/root` and `/etc` (anything under them is blocked). The operator
|
|
11
|
+
* EXTENDS — never shrinks — this set with additional absolute directory
|
|
12
|
+
* trees via the settings key `attachmentBlockedPaths: string[]` and/or the
|
|
13
|
+
* env var `CODEMAN_ATTACHMENT_BLOCKED_PATHS` (comma-separated).
|
|
14
|
+
*
|
|
15
|
+
* 2. **Workspace confinement (OPTIONAL, default OFF).** When enabled, an
|
|
16
|
+
* attachment must resolve INSIDE the registering session's workingDir
|
|
17
|
+
* (reusing `validateSessionFilePath` containment semantics). This is
|
|
18
|
+
* strictly more restrictive than the blocklist and breaks intentional
|
|
19
|
+
* cross-workspace attachment (codeman-publish, the ~/.codeman review-card
|
|
20
|
+
* loop), so it is OFF by default. Toggle via settings
|
|
21
|
+
* `attachmentConfineToWorkspace: boolean` and/or env
|
|
22
|
+
* `CODEMAN_ATTACHMENT_CONFINE` (`1`/`true`).
|
|
23
|
+
*
|
|
24
|
+
* All paths passed to the predicates here MUST be absolute and symlink-resolved
|
|
25
|
+
* (realpath) by the caller, mirroring `isSensitivePath`'s contract.
|
|
26
|
+
*
|
|
27
|
+
* @module config/attachment-guard
|
|
28
|
+
*/
|
|
29
|
+
import { sep } from 'node:path';
|
|
30
|
+
import { isSensitivePath } from '../web/sensitive-path.js';
|
|
31
|
+
import { readJsonConfig, SETTINGS_PATH } from '../web/route-helpers.js';
|
|
32
|
+
/**
|
|
33
|
+
* Directory trees blocked by default, IN ADDITION to the secret-location
|
|
34
|
+
* blocklist in `isSensitivePath`. Anything resolving under one of these trees
|
|
35
|
+
* is rejected. Pre-populated with the root account home and the system config
|
|
36
|
+
* tree (which already partially overlaps `isSensitivePath`'s `/etc/shadow`
|
|
37
|
+
* etc., but here we block the WHOLE tree).
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_BLOCKED_TREES = ['/root', '/etc'];
|
|
40
|
+
/** Settings key carrying extra blocked directory trees (extends the defaults). */
|
|
41
|
+
export const ATTACHMENT_BLOCKED_PATHS_SETTING = 'attachmentBlockedPaths';
|
|
42
|
+
/** Settings key carrying the workspace-confinement toggle. */
|
|
43
|
+
export const ATTACHMENT_CONFINE_SETTING = 'attachmentConfineToWorkspace';
|
|
44
|
+
/** Normalizes a tree prefix: trim, drop trailing separators (but keep root). */
|
|
45
|
+
function normalizeTree(raw) {
|
|
46
|
+
const trimmed = raw.trim();
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
return '';
|
|
49
|
+
// Strip trailing slashes so '/etc/' and '/etc' behave the same; never reduce
|
|
50
|
+
// a bare separator to empty.
|
|
51
|
+
const stripped = trimmed.replace(/[/\\]+$/, '');
|
|
52
|
+
return stripped || trimmed[0];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if `absPath` (absolute, symlink-resolved) is the tree itself or
|
|
56
|
+
* lives under it. Uses path-separator-aware matching so `/etc` does NOT block
|
|
57
|
+
* an unrelated `/etcetera/notes.md`.
|
|
58
|
+
*/
|
|
59
|
+
export function isUnderTree(absPath, tree) {
|
|
60
|
+
const t = normalizeTree(tree);
|
|
61
|
+
if (!t)
|
|
62
|
+
return false;
|
|
63
|
+
if (absPath === t)
|
|
64
|
+
return true;
|
|
65
|
+
return absPath.startsWith(t.endsWith(sep) ? t : t + sep);
|
|
66
|
+
}
|
|
67
|
+
/** Parses the comma-separated env override into a list of normalized trees. */
|
|
68
|
+
function parseEnvBlockedTrees() {
|
|
69
|
+
const raw = process.env.CODEMAN_ATTACHMENT_BLOCKED_PATHS;
|
|
70
|
+
if (!raw)
|
|
71
|
+
return [];
|
|
72
|
+
return raw
|
|
73
|
+
.split(',')
|
|
74
|
+
.map(normalizeTree)
|
|
75
|
+
.filter((t) => t.length > 0);
|
|
76
|
+
}
|
|
77
|
+
/** Parses the env confinement toggle (`1`/`true`/`yes`/`on`, case-insensitive). */
|
|
78
|
+
function parseEnvConfine() {
|
|
79
|
+
const raw = process.env.CODEMAN_ATTACHMENT_CONFINE;
|
|
80
|
+
if (raw === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
return /^(1|true|yes|on)$/i.test(raw.trim());
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Loads the effective attachment-guard config by merging the pre-populated
|
|
86
|
+
* defaults with settings.json and env overrides. Env wins over settings for the
|
|
87
|
+
* confinement toggle; blocked-tree extras from BOTH sources are unioned on top
|
|
88
|
+
* of the defaults (operators can only EXTEND, never shrink, the blocked set).
|
|
89
|
+
*/
|
|
90
|
+
export async function loadAttachmentGuardConfig() {
|
|
91
|
+
const settings = await readJsonConfig(SETTINGS_PATH, 'settings.json', {});
|
|
92
|
+
const settingsTrees = Array.isArray(settings[ATTACHMENT_BLOCKED_PATHS_SETTING])
|
|
93
|
+
? settings[ATTACHMENT_BLOCKED_PATHS_SETTING]
|
|
94
|
+
.filter((v) => typeof v === 'string')
|
|
95
|
+
.map(normalizeTree)
|
|
96
|
+
.filter((t) => t.length > 0)
|
|
97
|
+
: [];
|
|
98
|
+
const blockedTrees = Array.from(new Set([...DEFAULT_BLOCKED_TREES, ...settingsTrees, ...parseEnvBlockedTrees()]));
|
|
99
|
+
const envConfine = parseEnvConfine();
|
|
100
|
+
const settingsConfine = settings[ATTACHMENT_CONFINE_SETTING] === true;
|
|
101
|
+
const confineToWorkspace = envConfine ?? settingsConfine;
|
|
102
|
+
return { blockedTrees, confineToWorkspace };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Attachment-specific blocklist check. Builds on the shared `isSensitivePath`
|
|
106
|
+
* base (secret locations, shared with `/api/download`) and ADDS the configured
|
|
107
|
+
* directory trees (`/root`, `/etc`, plus operator extras). `absPath` must be
|
|
108
|
+
* absolute and symlink-resolved.
|
|
109
|
+
*
|
|
110
|
+
* NOTE: this is intentionally a SUPERSET of `isSensitivePath` so `/api/download`
|
|
111
|
+
* behavior is NOT changed — only attachment registration/serving uses this.
|
|
112
|
+
*/
|
|
113
|
+
export function isBlockedAttachmentPath(absPath, blockedTrees) {
|
|
114
|
+
if (isSensitivePath(absPath))
|
|
115
|
+
return true;
|
|
116
|
+
return blockedTrees.some((tree) => isUnderTree(absPath, tree));
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=attachment-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-guard.js","sourceRoot":"","sources":["../../src/config/attachment-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Global concurrency limiter for spawning external document
|
|
3
|
+
* converters (pdftoppm / LibreOffice `soffice` / Word-COM `powershell.exe`).
|
|
4
|
+
*
|
|
5
|
+
* Without a cap, N simultaneous thumbnail/preview requests for *distinct*
|
|
6
|
+
* documents fork N converter processes at once — each held open for up to the
|
|
7
|
+
* multi-minute conversion timeout. That is a localhost resource-exhaustion
|
|
8
|
+
* (fork-bomb-shaped) vector: a handful of large PDFs detected at once can pin
|
|
9
|
+
* CPU and RAM. This module serializes converter spawns down to a small fixed
|
|
10
|
+
* pool; excess spawns queue (FIFO) until a slot frees. The in-flight cache in
|
|
11
|
+
* `document-preview-cache.ts` already de-dups *identical* inputs; this bounds
|
|
12
|
+
* the *distinct* case the cache can't.
|
|
13
|
+
*
|
|
14
|
+
* Permit accounting transfers the slot directly to the next waiter on release
|
|
15
|
+
* (rather than decrement-then-reacquire) so the active count can never exceed
|
|
16
|
+
* the cap even under interleaved async resumption.
|
|
17
|
+
*
|
|
18
|
+
* NOT re-entrant: never call `runWithConversionLimit` from inside a task that is
|
|
19
|
+
* already holding a slot — a nested acquire under a full pool would deadlock.
|
|
20
|
+
* The converter call sites only ever acquire once per request (the office path
|
|
21
|
+
* acquires for `soffice` and `pdftoppm` sequentially, not nested).
|
|
22
|
+
*/
|
|
23
|
+
/** Test/diagnostic hook: converters currently holding a slot. */
|
|
24
|
+
export declare function getActiveConversionCount(): number;
|
|
25
|
+
/** Run `task` once a converter slot is free, releasing the slot afterward. */
|
|
26
|
+
export declare function runWithConversionLimit<T>(task: () => Promise<T>): Promise<T>;
|
|
27
|
+
//# sourceMappingURL=document-conversion-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-conversion-limiter.d.ts","sourceRoot":"","sources":["../src/document-conversion-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAcH,iEAAiE;AACjE,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAoBD,8EAA8E;AAC9E,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOlF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Global concurrency limiter for spawning external document
|
|
3
|
+
* converters (pdftoppm / LibreOffice `soffice` / Word-COM `powershell.exe`).
|
|
4
|
+
*
|
|
5
|
+
* Without a cap, N simultaneous thumbnail/preview requests for *distinct*
|
|
6
|
+
* documents fork N converter processes at once — each held open for up to the
|
|
7
|
+
* multi-minute conversion timeout. That is a localhost resource-exhaustion
|
|
8
|
+
* (fork-bomb-shaped) vector: a handful of large PDFs detected at once can pin
|
|
9
|
+
* CPU and RAM. This module serializes converter spawns down to a small fixed
|
|
10
|
+
* pool; excess spawns queue (FIFO) until a slot frees. The in-flight cache in
|
|
11
|
+
* `document-preview-cache.ts` already de-dups *identical* inputs; this bounds
|
|
12
|
+
* the *distinct* case the cache can't.
|
|
13
|
+
*
|
|
14
|
+
* Permit accounting transfers the slot directly to the next waiter on release
|
|
15
|
+
* (rather than decrement-then-reacquire) so the active count can never exceed
|
|
16
|
+
* the cap even under interleaved async resumption.
|
|
17
|
+
*
|
|
18
|
+
* NOT re-entrant: never call `runWithConversionLimit` from inside a task that is
|
|
19
|
+
* already holding a slot — a nested acquire under a full pool would deadlock.
|
|
20
|
+
* The converter call sites only ever acquire once per request (the office path
|
|
21
|
+
* acquires for `soffice` and `pdftoppm` sequentially, not nested).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Max converter processes allowed to run concurrently across the whole process.
|
|
25
|
+
* Override with CODEMAN_MAX_DOCUMENT_CONVERSIONS (clamped to >= 1).
|
|
26
|
+
*/
|
|
27
|
+
const MAX_CONCURRENT_DOCUMENT_CONVERSIONS = (() => {
|
|
28
|
+
const raw = Number(process.env.CODEMAN_MAX_DOCUMENT_CONVERSIONS);
|
|
29
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 3;
|
|
30
|
+
})();
|
|
31
|
+
let active = 0;
|
|
32
|
+
const waiters = [];
|
|
33
|
+
/** Test/diagnostic hook: converters currently holding a slot. */
|
|
34
|
+
export function getActiveConversionCount() {
|
|
35
|
+
return active;
|
|
36
|
+
}
|
|
37
|
+
function acquire() {
|
|
38
|
+
if (active < MAX_CONCURRENT_DOCUMENT_CONVERSIONS) {
|
|
39
|
+
active++;
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
43
|
+
}
|
|
44
|
+
function release() {
|
|
45
|
+
const next = waiters.shift();
|
|
46
|
+
if (next) {
|
|
47
|
+
// Hand the slot straight to the next waiter — `active` stays at the cap.
|
|
48
|
+
next();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
active--;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Run `task` once a converter slot is free, releasing the slot afterward. */
|
|
55
|
+
export async function runWithConversionLimit(task) {
|
|
56
|
+
await acquire();
|
|
57
|
+
try {
|
|
58
|
+
return await task();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
release();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=document-conversion-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-conversion-limiter.js","sourceRoot":"","sources":["../src/document-conversion-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;GAGG;AACH,MAAM,mCAAmC,GAAG,CAAC,GAAG,EAAE;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC,CAAC,EAAE,CAAC;AAEL,IAAI,MAAM,GAAG,CAAC,CAAC;AACf,MAAM,OAAO,GAAsB,EAAE,CAAC;AAEtC,iEAAiE;AACjE,MAAM,UAAU,wBAAwB;IACtC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,OAAO;IACd,IAAI,MAAM,GAAG,mCAAmC,EAAE,CAAC;QACjD,MAAM,EAAE,CAAC;QACT,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,OAAO;IACd,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAC7B,IAAI,IAAI,EAAE,CAAC;QACT,yEAAyE;QACzE,IAAI,EAAE,CAAC;IACT,CAAC;SAAM,CAAC;QACN,MAAM,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAI,IAAsB;IACpE,MAAM,OAAO,EAAE,CAAC;IAChB,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared disk cache for expensive Office document previews.
|
|
3
|
+
*/
|
|
4
|
+
export declare function clearDocumentPreviewCache(): void;
|
|
5
|
+
/**
|
|
6
|
+
* Best-effort LRU-ish eviction for the persistent converted-PDF cache: keeps at
|
|
7
|
+
* most MAX_PREVIEW_CACHE_FILES `*.pdf` files in `cacheDir`, deleting the oldest
|
|
8
|
+
* by mtime once over the cap. Never throws — a pruning failure must not fail the
|
|
9
|
+
* conversion that triggered it. Only `*.pdf` files are considered, so the
|
|
10
|
+
* transient `work-*` mkdtemp dirs are ignored.
|
|
11
|
+
*/
|
|
12
|
+
export declare function pruneDocumentPreviewCache(cacheDir: string): Promise<void>;
|
|
13
|
+
export declare function getOfficePreviewPdfPath(filePath: string, extension: string): Promise<string | null>;
|
|
14
|
+
export declare function getPreviewPdfDownloadName(fileName: string, extension: string): string;
|
|
15
|
+
//# sourceMappingURL=document-preview-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-preview-cache.d.ts","sourceRoot":"","sources":["../src/document-preview-cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAyDH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB/E;AAED,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyCzG;AAmKD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAErF"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared disk cache for expensive Office document previews.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { runWithConversionLimit } from './document-conversion-limiter.js';
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const OFFICE_CONVERSION_TIMEOUT_MS = 5 * 60_000;
|
|
14
|
+
const DOCUMENT_PREVIEW_CACHE_DIR = join(tmpdir(), 'codeman-document-preview-cache');
|
|
15
|
+
/**
|
|
16
|
+
* Cap on persistent converted-PDF files kept in DOCUMENT_PREVIEW_CACHE_DIR.
|
|
17
|
+
* The cache key embeds the source mtime, so every edit to a doc orphans its
|
|
18
|
+
* prior PDF; without a cap the dir grows unbounded across long-running sessions.
|
|
19
|
+
* Override with CODEMAN_MAX_PREVIEW_CACHE_FILES (clamped to >= 1).
|
|
20
|
+
*/
|
|
21
|
+
const MAX_PREVIEW_CACHE_FILES = (() => {
|
|
22
|
+
const raw = Number(process.env.CODEMAN_MAX_PREVIEW_CACHE_FILES);
|
|
23
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 100;
|
|
24
|
+
})();
|
|
25
|
+
function buildWordExportPdfScript(sourcePath, outputPath) {
|
|
26
|
+
return `
|
|
27
|
+
$ErrorActionPreference = "Stop"
|
|
28
|
+
$source = ${toPowerShellSingleQuotedString(sourcePath)}
|
|
29
|
+
$output = ${toPowerShellSingleQuotedString(outputPath)}
|
|
30
|
+
$word = $null
|
|
31
|
+
$doc = $null
|
|
32
|
+
try {
|
|
33
|
+
$word = New-Object -ComObject Word.Application
|
|
34
|
+
$word.Visible = $false
|
|
35
|
+
$word.DisplayAlerts = 0
|
|
36
|
+
$doc = $word.Documents.Open($source)
|
|
37
|
+
$doc.ExportAsFixedFormat($output, 17)
|
|
38
|
+
} finally {
|
|
39
|
+
if ($null -ne $doc) {
|
|
40
|
+
$doc.Close($false) | Out-Null
|
|
41
|
+
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc) | Out-Null
|
|
42
|
+
}
|
|
43
|
+
if ($null -ne $word) {
|
|
44
|
+
$word.Quit() | Out-Null
|
|
45
|
+
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($word) | Out-Null
|
|
46
|
+
}
|
|
47
|
+
[System.GC]::Collect()
|
|
48
|
+
[System.GC]::WaitForPendingFinalizers()
|
|
49
|
+
}
|
|
50
|
+
`.trim();
|
|
51
|
+
}
|
|
52
|
+
const inFlightOfficeConversions = new Map();
|
|
53
|
+
export function clearDocumentPreviewCache() {
|
|
54
|
+
inFlightOfficeConversions.clear();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort LRU-ish eviction for the persistent converted-PDF cache: keeps at
|
|
58
|
+
* most MAX_PREVIEW_CACHE_FILES `*.pdf` files in `cacheDir`, deleting the oldest
|
|
59
|
+
* by mtime once over the cap. Never throws — a pruning failure must not fail the
|
|
60
|
+
* conversion that triggered it. Only `*.pdf` files are considered, so the
|
|
61
|
+
* transient `work-*` mkdtemp dirs are ignored.
|
|
62
|
+
*/
|
|
63
|
+
export async function pruneDocumentPreviewCache(cacheDir) {
|
|
64
|
+
try {
|
|
65
|
+
const entries = await fs.readdir(cacheDir);
|
|
66
|
+
const pdfs = entries.filter((name) => name.toLowerCase().endsWith('.pdf'));
|
|
67
|
+
if (pdfs.length <= MAX_PREVIEW_CACHE_FILES)
|
|
68
|
+
return;
|
|
69
|
+
const stats = await Promise.all(pdfs.map(async (name) => {
|
|
70
|
+
const fullPath = join(cacheDir, name);
|
|
71
|
+
try {
|
|
72
|
+
const stat = await fs.stat(fullPath);
|
|
73
|
+
return { fullPath, mtimeMs: stat.mtimeMs ?? 0 };
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
const sorted = stats.filter((s) => s !== null);
|
|
80
|
+
sorted.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
|
|
81
|
+
const toRemove = sorted.slice(0, Math.max(0, sorted.length - MAX_PREVIEW_CACHE_FILES));
|
|
82
|
+
await Promise.all(toRemove.map((entry) => fs.rm(entry.fullPath, { force: true }).catch(() => { })));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Best-effort: pruning must never break a conversion.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function getOfficePreviewPdfPath(filePath, extension) {
|
|
89
|
+
const ext = extension.toLowerCase().replace(/^\./, '');
|
|
90
|
+
if (ext !== 'docx' && ext !== 'pptx')
|
|
91
|
+
return null;
|
|
92
|
+
let sourceStat;
|
|
93
|
+
try {
|
|
94
|
+
sourceStat = await fs.stat(filePath);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
for (const converter of getOfficePreviewConverters(filePath, ext)) {
|
|
100
|
+
const cacheKey = createDocumentPreviewCacheKey(filePath, ext, sourceStat.size, sourceStat.mtimeMs ?? 0, converter);
|
|
101
|
+
const cachePath = getOfficePreviewCachePath(filePath, cacheKey, converter);
|
|
102
|
+
if (await fileExists(cachePath)) {
|
|
103
|
+
return cachePath;
|
|
104
|
+
}
|
|
105
|
+
const inFlightKey = `${converter}:${cacheKey}`;
|
|
106
|
+
const inFlight = inFlightOfficeConversions.get(inFlightKey);
|
|
107
|
+
if (inFlight) {
|
|
108
|
+
const converted = await inFlight;
|
|
109
|
+
if (converted)
|
|
110
|
+
return converted;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const conversion = converter === 'msword'
|
|
114
|
+
? convertWordDocumentToCachedPdf(filePath, cachePath)
|
|
115
|
+
: convertLibreOfficeDocumentToCachedPdf(filePath, cachePath);
|
|
116
|
+
inFlightOfficeConversions.set(inFlightKey, conversion);
|
|
117
|
+
try {
|
|
118
|
+
const converted = await conversion;
|
|
119
|
+
if (converted)
|
|
120
|
+
return converted;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
inFlightOfficeConversions.delete(inFlightKey);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function getOfficePreviewConverters(filePath, extension) {
|
|
129
|
+
if (extension === 'docx' && wslMountPathToWindowsPath(filePath)) {
|
|
130
|
+
return ['msword', 'libreoffice'];
|
|
131
|
+
}
|
|
132
|
+
return ['libreoffice'];
|
|
133
|
+
}
|
|
134
|
+
function createDocumentPreviewCacheKey(filePath, extension, size, mtimeMs, converter) {
|
|
135
|
+
return createHash('sha256')
|
|
136
|
+
.update(JSON.stringify({ cacheVersion: 2, converter, filePath, extension, size, mtimeMs }))
|
|
137
|
+
.digest('hex')
|
|
138
|
+
.slice(0, 32);
|
|
139
|
+
}
|
|
140
|
+
function getOfficePreviewCachePath(filePath, cacheKey, converter) {
|
|
141
|
+
if (converter === 'msword') {
|
|
142
|
+
const windowsCacheDir = getWindowsUserTempCacheDir(filePath);
|
|
143
|
+
if (windowsCacheDir) {
|
|
144
|
+
return join(windowsCacheDir, `${cacheKey}.pdf`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return join(DOCUMENT_PREVIEW_CACHE_DIR, `${cacheKey}.pdf`);
|
|
148
|
+
}
|
|
149
|
+
async function fileExists(filePath) {
|
|
150
|
+
try {
|
|
151
|
+
const stat = await fs.stat(filePath);
|
|
152
|
+
return typeof stat.isFile !== 'function' || stat.isFile();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function convertWordDocumentToCachedPdf(filePath, cachePath) {
|
|
159
|
+
const outputPath = wslMountPathToWindowsPath(cachePath);
|
|
160
|
+
if (!outputPath)
|
|
161
|
+
return null;
|
|
162
|
+
let sourceCopyPath;
|
|
163
|
+
try {
|
|
164
|
+
await fs.mkdir(dirname(cachePath), { recursive: true });
|
|
165
|
+
sourceCopyPath = join(dirname(cachePath), `${basename(cachePath, '.pdf')}.docx`);
|
|
166
|
+
await fs.copyFile(filePath, sourceCopyPath);
|
|
167
|
+
const sourcePath = wslMountPathToWindowsPath(sourceCopyPath);
|
|
168
|
+
if (!sourcePath)
|
|
169
|
+
return null;
|
|
170
|
+
await runWithConversionLimit(() => execFileAsync('powershell.exe', [
|
|
171
|
+
'-NoProfile',
|
|
172
|
+
'-NonInteractive',
|
|
173
|
+
'-ExecutionPolicy',
|
|
174
|
+
'Bypass',
|
|
175
|
+
'-EncodedCommand',
|
|
176
|
+
encodePowerShellCommand(buildWordExportPdfScript(sourcePath, outputPath)),
|
|
177
|
+
], {
|
|
178
|
+
timeout: OFFICE_CONVERSION_TIMEOUT_MS,
|
|
179
|
+
maxBuffer: 1024 * 1024,
|
|
180
|
+
}));
|
|
181
|
+
if (await fileExists(cachePath)) {
|
|
182
|
+
await pruneDocumentPreviewCache(dirname(cachePath));
|
|
183
|
+
return cachePath;
|
|
184
|
+
}
|
|
185
|
+
console.warn(`[DocumentPreviewCache] Microsoft Word did not produce PDF output for ${filePath}`);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
console.warn(`[DocumentPreviewCache] Failed to convert DOCX with Microsoft Word (${filePath}):`, getCacheErrorMessage(err));
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
if (sourceCopyPath) {
|
|
194
|
+
await fs.rm(sourceCopyPath, { force: true }).catch(() => { });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function convertLibreOfficeDocumentToCachedPdf(filePath, cachePath) {
|
|
199
|
+
let workDir;
|
|
200
|
+
try {
|
|
201
|
+
await fs.mkdir(DOCUMENT_PREVIEW_CACHE_DIR, { recursive: true });
|
|
202
|
+
const outDir = await fs.mkdtemp(join(DOCUMENT_PREVIEW_CACHE_DIR, 'work-'));
|
|
203
|
+
workDir = outDir;
|
|
204
|
+
const profileDir = join(outDir, 'profile');
|
|
205
|
+
await fs.mkdir(profileDir, { recursive: true });
|
|
206
|
+
await runWithConversionLimit(() => execFileAsync('soffice', [
|
|
207
|
+
'--headless',
|
|
208
|
+
'--nologo',
|
|
209
|
+
'--nofirststartwizard',
|
|
210
|
+
`-env:UserInstallation=${pathToFileURL(profileDir).href}`,
|
|
211
|
+
'--convert-to',
|
|
212
|
+
'pdf',
|
|
213
|
+
'--outdir',
|
|
214
|
+
outDir,
|
|
215
|
+
filePath,
|
|
216
|
+
], {
|
|
217
|
+
timeout: OFFICE_CONVERSION_TIMEOUT_MS,
|
|
218
|
+
maxBuffer: 1024 * 1024,
|
|
219
|
+
}));
|
|
220
|
+
const converted = (await fs.readdir(outDir)).find((name) => name.toLowerCase().endsWith('.pdf'));
|
|
221
|
+
if (!converted)
|
|
222
|
+
return null;
|
|
223
|
+
await fs.rename(join(workDir, converted), cachePath);
|
|
224
|
+
await pruneDocumentPreviewCache(DOCUMENT_PREVIEW_CACHE_DIR);
|
|
225
|
+
return cachePath;
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.warn(`[DocumentPreviewCache] Failed to convert Office file to PDF (${filePath}):`, getCacheErrorMessage(err));
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
if (workDir) {
|
|
233
|
+
await fs.rm(workDir, { recursive: true, force: true }).catch(() => { });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function wslMountPathToWindowsPath(filePath) {
|
|
238
|
+
const match = filePath.match(/^\/mnt\/([a-zA-Z])\/(.+)$/);
|
|
239
|
+
if (!match)
|
|
240
|
+
return null;
|
|
241
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, '\\')}`;
|
|
242
|
+
}
|
|
243
|
+
function getWindowsUserTempCacheDir(filePath) {
|
|
244
|
+
const match = filePath.match(/^\/mnt\/([a-zA-Z])\/Users\/([^/]+)\//);
|
|
245
|
+
if (!match)
|
|
246
|
+
return null;
|
|
247
|
+
return `/mnt/${match[1].toLowerCase()}/Users/${match[2]}/AppData/Local/Temp/codeman-document-preview-cache`;
|
|
248
|
+
}
|
|
249
|
+
function toPowerShellSingleQuotedString(value) {
|
|
250
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
251
|
+
}
|
|
252
|
+
function encodePowerShellCommand(script) {
|
|
253
|
+
return Buffer.from(script, 'utf16le').toString('base64');
|
|
254
|
+
}
|
|
255
|
+
export function getPreviewPdfDownloadName(fileName, extension) {
|
|
256
|
+
return `${basename(fileName, extname(fileName) || `.${extension}`)}.pdf`;
|
|
257
|
+
}
|
|
258
|
+
function getCacheErrorMessage(err) {
|
|
259
|
+
return err instanceof Error ? err.message : String(err);
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=document-preview-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-preview-cache.js","sourceRoot":"","sources":["../src/document-preview-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Best-effort first-page thumbnails for attachment cards.
|
|
3
|
+
*/
|
|
4
|
+
export interface ThumbnailResult {
|
|
5
|
+
content: Buffer;
|
|
6
|
+
contentType: 'image/png';
|
|
7
|
+
}
|
|
8
|
+
export declare function generateFirstPageThumbnail(filePath: string, extension: string): Promise<ThumbnailResult | null>;
|
|
9
|
+
//# sourceMappingURL=document-thumbnailer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-thumbnailer.d.ts","sourceRoot":"","sources":["../src/document-thumbnailer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAaH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAuBrH"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Best-effort first-page thumbnails for attachment cards.
|
|
3
|
+
*/
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { basename, extname, join } from 'node:path';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { getOfficePreviewPdfPath } from './document-preview-cache.js';
|
|
10
|
+
import { runWithConversionLimit } from './document-conversion-limiter.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const THUMBNAIL_CONVERSION_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
export async function generateFirstPageThumbnail(filePath, extension) {
|
|
14
|
+
const ext = extension.toLowerCase().replace(/^\./, '');
|
|
15
|
+
try {
|
|
16
|
+
await fs.stat(filePath);
|
|
17
|
+
if (ext === 'png') {
|
|
18
|
+
return { content: await fs.readFile(filePath), contentType: 'image/png' };
|
|
19
|
+
}
|
|
20
|
+
if (ext === 'pdf') {
|
|
21
|
+
return renderPdfFirstPage(filePath);
|
|
22
|
+
}
|
|
23
|
+
if (ext === 'docx' || ext === 'pptx') {
|
|
24
|
+
return renderOfficeFirstPage(filePath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.warn(`[Thumbnailer] Failed to generate ${ext} thumbnail for ${filePath}:`, getThumbnailErrorMessage(err));
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
async function renderOfficeFirstPage(filePath) {
|
|
34
|
+
try {
|
|
35
|
+
const previewPdfPath = await getOfficePreviewPdfPath(filePath, extname(filePath).toLowerCase().replace(/^\./, ''));
|
|
36
|
+
if (!previewPdfPath)
|
|
37
|
+
return null;
|
|
38
|
+
return await renderPdfFirstPage(previewPdfPath);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.warn(`[Thumbnailer] Failed to convert Office file to PDF for thumbnail (${filePath}):`, getThumbnailErrorMessage(err));
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function renderPdfFirstPage(filePath) {
|
|
46
|
+
let previewDir;
|
|
47
|
+
try {
|
|
48
|
+
previewDir = await fs.mkdtemp(join(tmpdir(), 'codeman-thumb-pdf-'));
|
|
49
|
+
const prefix = join(previewDir, basename(filePath, extname(filePath)));
|
|
50
|
+
await runWithConversionLimit(() => execFileAsync('pdftoppm', ['-png', '-singlefile', '-f', '1', '-l', '1', '-scale-to', '520', filePath, prefix], {
|
|
51
|
+
timeout: THUMBNAIL_CONVERSION_TIMEOUT_MS,
|
|
52
|
+
maxBuffer: 1024 * 1024,
|
|
53
|
+
}));
|
|
54
|
+
const content = await fs.readFile(`${prefix}.png`);
|
|
55
|
+
return { content, contentType: 'image/png' };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.warn(`[Thumbnailer] Failed to render PDF first page for thumbnail (${filePath}):`, getThumbnailErrorMessage(err));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (previewDir) {
|
|
63
|
+
await fs.rm(previewDir, { recursive: true, force: true }).catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function getThumbnailErrorMessage(err) {
|
|
68
|
+
return err instanceof Error ? err.message : String(err);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=document-thumbnailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-thumbnailer.js","sourceRoot":"","sources":["../src/document-thumbnailer.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks-config.d.ts
CHANGED
|
@@ -57,4 +57,21 @@ export declare function updateCaseModel(casePath: string, model: string | null):
|
|
|
57
57
|
* Merges with existing file content, only touching the `hooks` key.
|
|
58
58
|
*/
|
|
59
59
|
export declare function writeHooksConfig(casePath: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* The plan-usage statusLine exporter command. Mirrors the hook `curlCmd` pattern:
|
|
62
|
+
* reads Claude Code's statusline stdin JSON, POSTs `{sessionId,data}` to Codeman,
|
|
63
|
+
* and prints the response body (a compact "⟳ 5h 15% · 7d 34%" footer) back to
|
|
64
|
+
* stdout so the in-terminal statusline stays useful. Env vars resolve at runtime
|
|
65
|
+
* (present in every managed session via tmux setenv), so the config is static.
|
|
66
|
+
*/
|
|
67
|
+
export declare function generateStatusLineCommand(): string;
|
|
68
|
+
/**
|
|
69
|
+
* Add or remove Codeman's plan-usage statusLine exporter in
|
|
70
|
+
* `.claude/settings.local.json`. Only ever touches a statusLine that is OURS
|
|
71
|
+
* (command targets `/api/status-telemetry`), so a user's hand-authored
|
|
72
|
+
* statusLine is never removed OR overwritten — on both the enable and disable
|
|
73
|
+
* paths we bail out when an existing statusLine isn't ours. Callers gate on
|
|
74
|
+
* Claude mode. Merges, preserving all other keys (hooks, env, model).
|
|
75
|
+
*/
|
|
76
|
+
export declare function applyStatusLineConfig(casePath: string, enabled: boolean): Promise<void>;
|
|
60
77
|
//# sourceMappingURL=hooks-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-config.d.ts","sourceRoot":"","sources":["../src/hooks-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"hooks-config.d.ts","sourceRoot":"","sources":["../src/hooks-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAiCH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;CAAE,CAmD1E;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BvG;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BxG;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB3F;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBtE;AAKD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAalD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7F"}
|