create-walle 0.9.30 → 0.9.32
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/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +410 -5
- package/template/claude-task-manager/api-reviews.js +251 -3
- package/template/claude-task-manager/approval-agent.js +322 -24
- package/template/claude-task-manager/db.js +77 -13
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +35 -1
- package/template/claude-task-manager/docs/postgres-concurrency-dgx-spark.html +407 -0
- package/template/claude-task-manager/docs/prompt-manager-redesign-proposal.html +667 -0
- package/template/claude-task-manager/docs/resume-ux-redesign.html +493 -0
- package/template/claude-task-manager/docs/review-redesign-proposal.html +892 -0
- package/template/claude-task-manager/docs/unified-auto-approver-proposal.html +520 -0
- package/template/claude-task-manager/git-utils.js +164 -26
- package/template/claude-task-manager/lib/agent-capabilities.js +26 -5
- package/template/claude-task-manager/lib/agent-cli-cache.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +12 -0
- package/template/claude-task-manager/lib/agent-version-service.js +650 -0
- package/template/claude-task-manager/lib/approval-drift.js +57 -0
- package/template/claude-task-manager/lib/approval-hook.js +13 -0
- package/template/claude-task-manager/lib/auth-rules.js +6 -0
- package/template/claude-task-manager/lib/broadcast-payload-memo.js +109 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +89 -8
- package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +10 -1
- package/template/claude-task-manager/lib/document-review.js +75 -1
- package/template/claude-task-manager/lib/escalation-review.js +33 -3
- package/template/claude-task-manager/lib/headless-term-service.js +54 -9
- package/template/claude-task-manager/lib/mcp-risk.js +128 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +51 -2
- package/template/claude-task-manager/lib/native-agent-model-args.js +149 -0
- package/template/claude-task-manager/lib/opencode-conversation-store.js +269 -0
- package/template/claude-task-manager/lib/permission-match.js +77 -5
- package/template/claude-task-manager/lib/read-pool-client.js +15 -6
- package/template/claude-task-manager/lib/restore-backoff.js +82 -0
- package/template/claude-task-manager/lib/runtime-registry.js +1 -0
- package/template/claude-task-manager/lib/scheduled-wake.js +42 -0
- package/template/claude-task-manager/lib/session-capture.js +33 -6
- package/template/claude-task-manager/lib/session-history.js +60 -3
- package/template/claude-task-manager/lib/session-jobs.js +19 -0
- package/template/claude-task-manager/lib/session-restore.js +5 -4
- package/template/claude-task-manager/lib/session-standup.js +2 -0
- package/template/claude-task-manager/lib/session-stream.js +9 -0
- package/template/claude-task-manager/lib/state-sync/cell-diff.js +31 -4
- package/template/claude-task-manager/lib/state-sync/restore-frame-hold.js +39 -0
- package/template/claude-task-manager/lib/tui-input-modes.js +40 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +103 -36
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +7 -1
- package/template/claude-task-manager/lib/walle-token-chip.js +65 -0
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/providers/claude-code.js +20 -1
- package/template/claude-task-manager/providers/codex-mcp-extract.js +33 -0
- package/template/claude-task-manager/providers/codex-mcp.js +23 -20
- package/template/claude-task-manager/providers/codex.js +65 -1
- package/template/claude-task-manager/public/css/prompts.css +345 -0
- package/template/claude-task-manager/public/css/reviews.css +248 -40
- package/template/claude-task-manager/public/css/walle-session.css +90 -26
- package/template/claude-task-manager/public/index.html +2031 -428
- package/template/claude-task-manager/public/js/document-review-links.js +255 -13
- package/template/claude-task-manager/public/js/file-context-menu.js +185 -0
- package/template/claude-task-manager/public/js/message-renderer.js +46 -1
- package/template/claude-task-manager/public/js/mobile-review-core.js +54 -0
- package/template/claude-task-manager/public/js/prompt-diff.js +55 -0
- package/template/claude-task-manager/public/js/prompt-editor-chrome.js +108 -0
- package/template/claude-task-manager/public/js/prompt-editor-modes.js +87 -0
- package/template/claude-task-manager/public/js/prompt-organize.js +179 -0
- package/template/claude-task-manager/public/js/prompt-reuse.js +77 -0
- package/template/claude-task-manager/public/js/prompts.js +683 -215
- package/template/claude-task-manager/public/js/resume-state.js +186 -0
- package/template/claude-task-manager/public/js/reviews.js +402 -121
- package/template/claude-task-manager/public/js/screenshot-router.js +91 -0
- package/template/claude-task-manager/public/js/session-search-utils.js +81 -0
- package/template/claude-task-manager/public/js/state-sync-client.js +134 -17
- package/template/claude-task-manager/public/js/stream-view.js +93 -20
- package/template/claude-task-manager/public/js/walle-session.js +183 -32
- package/template/claude-task-manager/public/js/walle.js +21 -9
- package/template/claude-task-manager/public/m/app.css +117 -0
- package/template/claude-task-manager/public/m/app.js +309 -3
- package/template/claude-task-manager/public/m/index.html +56 -1
- package/template/claude-task-manager/server.js +724 -305
- package/template/claude-task-manager/workers/db-owner-worker.js +8 -0
- package/template/claude-task-manager/workers/read-pool-worker.js +4 -0
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +10 -1
- package/template/docs/proposals/2026-06-23-local-model-right-path.html +340 -0
- package/template/docs/proposals/2026-06-24-qlora-done-right.md +255 -0
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +21 -3
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +27 -4
- package/template/wall-e/brain.js +50 -5
- package/template/wall-e/chat/force-compact.js +73 -0
- package/template/wall-e/chat.js +137 -9
- package/template/wall-e/coding/action-memory-policy.js +120 -1
- package/template/wall-e/coding/compaction-service.js +10 -3
- package/template/wall-e/coding/model-router.js +116 -0
- package/template/wall-e/coding-orchestrator.js +303 -11
- package/template/wall-e/coding-prompts.js +1 -1
- package/template/wall-e/docs/skill-self-heal-design.html +429 -0
- package/template/wall-e/llm/client.js +115 -6
- package/template/wall-e/llm/codex-cli.js +71 -23
- package/template/wall-e/llm/codex-cli.plugin.json +1 -0
- package/template/wall-e/llm/default-fallback.js +71 -11
- package/template/wall-e/llm/mlx-worker.js +0 -0
- package/template/wall-e/llm/mlx.js +46 -12
- package/template/wall-e/llm/provider-error.js +9 -3
- package/template/wall-e/llm/provider-health-state.js +561 -11
- package/template/wall-e/llm/registry.js +6 -0
- package/template/wall-e/llm/routing-policy.js +255 -25
- package/template/wall-e/llm/tool-call-validator.js +156 -0
- package/template/wall-e/skills/skill-dispatch-decision.js +82 -2
- package/template/wall-e/skills/skill-planner.js +72 -5
- package/template/wall-e/tools/command-registry.js +34 -0
- package/template/wall-e/training/gemma-e4b-qlora.js +197 -1
- package/template/wall-e/training/mlx_lora_launch.py +196 -0
- package/template/wall-e/training/training-coexistence.sh +87 -0
- package/template/wall-e/training/training-mem-guard.sh +76 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const db = require('./db');
|
|
5
6
|
const gitUtils = require('./git-utils');
|
|
6
7
|
const documentReview = require('./lib/document-review');
|
|
@@ -23,6 +24,132 @@ function isValidProjectPath(p) {
|
|
|
23
24
|
return true;
|
|
24
25
|
} catch { return false; }
|
|
25
26
|
}
|
|
27
|
+
// Resolve `filePath` within `project` and enforce containment AFTER following
|
|
28
|
+
// symlinks. A lexical `resolved.startsWith(root + sep)` check is not enough: a
|
|
29
|
+
// symlink that lives inside the repo can point at /etc/passwd (or ~/.ssh) and the
|
|
30
|
+
// lexical path still looks in-bounds. realpath canonicalizes the link target, so
|
|
31
|
+
// containment is checked against where the file ACTUALLY is.
|
|
32
|
+
// Returns the safe absolute path to read, or null if it escapes the project.
|
|
33
|
+
// A path that doesn't exist yet (ENOENT) can't be a symlink, so the lexical check
|
|
34
|
+
// already guarantees safety — return it so the caller can produce its own 404.
|
|
35
|
+
// The repo's worktree roots, main-style checkouts first (work usually lands there), capped.
|
|
36
|
+
function htmlWorktreeRoots(projectRoot, cap = 20) {
|
|
37
|
+
try {
|
|
38
|
+
const { execFileSync } = require('child_process');
|
|
39
|
+
const out = execFileSync('git', ['worktree', 'list', '--porcelain'],
|
|
40
|
+
{ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
41
|
+
const roots = [];
|
|
42
|
+
for (const line of out.split('\n')) {
|
|
43
|
+
if (line.startsWith('worktree ')) roots.push(line.slice('worktree '.length).trim());
|
|
44
|
+
}
|
|
45
|
+
const isWorktreeSubdir = (pth) => /\/\.(?:claude|walle)\/worktrees\/|\/\.worktrees\//.test(pth);
|
|
46
|
+
roots.sort((a, b) => (isWorktreeSubdir(a) ? 1 : 0) - (isWorktreeSubdir(b) ? 1 : 0));
|
|
47
|
+
return roots.slice(0, Math.max(1, cap));
|
|
48
|
+
} catch { return []; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Find an .html file (tracked + untracked) within a single worktree `root` by exact relative
|
|
52
|
+
// path, suffix, or basename.
|
|
53
|
+
function findHtmlInRoot(root, want, base) {
|
|
54
|
+
try {
|
|
55
|
+
const { execFileSync } = require('child_process');
|
|
56
|
+
const out = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '*.html', '*.htm'],
|
|
57
|
+
{ cwd: root, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 }).split('\0').filter(Boolean);
|
|
58
|
+
let hit = out.find(f => f === want || f.endsWith('/' + want));
|
|
59
|
+
if (!hit) hit = out.find(f => path.basename(f) === base);
|
|
60
|
+
return hit ? path.join(root, hit) : null;
|
|
61
|
+
} catch { return null; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Best-effort fallback when an html path doesn't resolve at the given root — e.g. a path
|
|
65
|
+
// relative to the repo root while the session cwd is a subdir, OR a file that lives in a
|
|
66
|
+
// SIBLING worktree of the same repo (work landed on main while the session runs in a feature
|
|
67
|
+
// worktree). Searches the own worktree first, then the repo's others; returns a path under
|
|
68
|
+
// the user's HOME or null.
|
|
69
|
+
function findHtmlInRepo(root, relPath) {
|
|
70
|
+
const want = String(relPath || '').replace(/^\.?\//, '');
|
|
71
|
+
if (!want) return null;
|
|
72
|
+
const base = path.basename(want);
|
|
73
|
+
const home = process.env.HOME || '';
|
|
74
|
+
const underHome = (pth) => {
|
|
75
|
+
if (!home) return false;
|
|
76
|
+
const r = path.resolve(pth);
|
|
77
|
+
return r === home || r.startsWith(home + path.sep);
|
|
78
|
+
};
|
|
79
|
+
const own = findHtmlInRoot(root, want, base);
|
|
80
|
+
if (own && underHome(own)) return own;
|
|
81
|
+
let realRoot = '';
|
|
82
|
+
try { realRoot = fs.realpathSync(root); } catch { realRoot = path.resolve(root); }
|
|
83
|
+
for (const wt of htmlWorktreeRoots(root)) {
|
|
84
|
+
let realWt = '';
|
|
85
|
+
try { realWt = fs.realpathSync(wt); } catch { realWt = path.resolve(wt); }
|
|
86
|
+
if (realWt === realRoot) continue; // own worktree already searched
|
|
87
|
+
const hit = findHtmlInRoot(wt, want, base);
|
|
88
|
+
if (hit && underHome(hit)) return hit;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function htmlNotFoundPage(filePath) {
|
|
94
|
+
const safe = String(filePath || '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
95
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>File not found</title>
|
|
96
|
+
<style>body{font-family:-apple-system,system-ui,sans-serif;background:#1a1b26;color:#c0caf5;display:grid;place-items:center;height:100vh;margin:0}
|
|
97
|
+
.box{text-align:center;max-width:34rem;padding:2rem}h1{font-size:1.1rem;color:#f7768e}code{background:rgba(122,162,247,.12);padding:.15rem .4rem;border-radius:4px;font-size:.85em;word-break:break-all}p{color:#8b94bd;line-height:1.6;font-size:.9rem}</style>
|
|
98
|
+
</head><body><div class="box"><h1>File not found</h1>
|
|
99
|
+
<p><code>${safe}</code> isn't in this checkout. It may live in a different worktree or branch than the one this session is running in.</p></div></body></html>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveWithinProject(project, filePath) {
|
|
103
|
+
if (!project || typeof filePath !== 'string') return null;
|
|
104
|
+
const root = path.resolve(project);
|
|
105
|
+
const resolved = path.resolve(root, filePath);
|
|
106
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null; // lexical ../ escape
|
|
107
|
+
let realRoot;
|
|
108
|
+
try { realRoot = fs.realpathSync(root); }
|
|
109
|
+
catch { return null; }
|
|
110
|
+
let realFile;
|
|
111
|
+
try { realFile = fs.realpathSync(resolved); }
|
|
112
|
+
catch (e) {
|
|
113
|
+
if (e && e.code === 'ENOENT') return resolved; // lexically in-bounds, not yet on disk
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (realFile !== realRoot && !realFile.startsWith(realRoot + path.sep)) return null; // symlink escape
|
|
117
|
+
return realFile;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The per-session scratchpad lives OUTSIDE the project root AND outside HOME:
|
|
121
|
+
// <tmp>/claude-<uid>/<project-slug>/<session>/scratchpad/<file>.html
|
|
122
|
+
// (e.g. <tmpdir>/claude-<uid>/.../scratchpad/plan.html). Agents author HTML there and
|
|
123
|
+
// print the absolute path into session output; clicking it built a serve-html URL that
|
|
124
|
+
// 403'd because resolveWithinProject (project- and HOME-scoped) rejected it. Serve these
|
|
125
|
+
// too — same trust level as project HTML (same uid, mode-0700 temp dir), behind the same
|
|
126
|
+
// .html + sandbox-CSP guards, with realpath containment to block a symlink escaping the
|
|
127
|
+
// scratchpad. Returns the safe absolute path to read, or null.
|
|
128
|
+
function scratchpadRoots() {
|
|
129
|
+
let uid = '';
|
|
130
|
+
try { if (typeof process.getuid === 'function') uid = String(process.getuid()); } catch {}
|
|
131
|
+
if (!uid) return [];
|
|
132
|
+
const roots = new Set();
|
|
133
|
+
// os.tmpdir() (launchd gives a per-user /var/folders/... TMPDIR) plus the /tmp aliases
|
|
134
|
+
// Claude Code actually uses; realpath collapses /tmp → /private/tmp on macOS and dedupes.
|
|
135
|
+
for (const base of [os.tmpdir(), '/tmp', '/private/tmp']) {
|
|
136
|
+
try { roots.add(fs.realpathSync(path.join(base, 'claude-' + uid))); } catch {}
|
|
137
|
+
}
|
|
138
|
+
return [...roots];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveWithinScratchpad(filePath) {
|
|
142
|
+
if (typeof filePath !== 'string' || !path.isAbsolute(filePath)) return null;
|
|
143
|
+
const roots = scratchpadRoots();
|
|
144
|
+
if (!roots.length) return null;
|
|
145
|
+
let realFile;
|
|
146
|
+
try { realFile = fs.realpathSync(filePath); } // must exist; no pre-create 404 for scratchpad
|
|
147
|
+
catch { return null; }
|
|
148
|
+
for (const root of roots) {
|
|
149
|
+
if (realFile === root || realFile.startsWith(root + path.sep)) return realFile;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
26
153
|
function jsonResponse(res, code, data) {
|
|
27
154
|
const body = JSON.stringify(data);
|
|
28
155
|
res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
|
@@ -234,6 +361,22 @@ function handleReviewApi(req, res, url) {
|
|
|
234
361
|
return true;
|
|
235
362
|
}
|
|
236
363
|
|
|
364
|
+
// POST /api/reviews/verify-commits — given a repo and a list of candidate SHAs
|
|
365
|
+
// (mined from session output), return only the ones that are real commits, with
|
|
366
|
+
// their short hash + subject. Lets the client linkify commit references safely.
|
|
367
|
+
if (p === '/api/reviews/verify-commits' && m === 'POST') {
|
|
368
|
+
readBody(req).then(body => {
|
|
369
|
+
const project = body && body.project;
|
|
370
|
+
const shas = (body && Array.isArray(body.shas)) ? body.shas.slice(0, 50) : [];
|
|
371
|
+
if (!project) return jsonResponse(res, 400, { error: 'project required' });
|
|
372
|
+
if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
|
|
373
|
+
gitUtils.verifyCommits(project, shas)
|
|
374
|
+
.then(commits => jsonResponse(res, 200, { commits }))
|
|
375
|
+
.catch(e => jsonResponse(res, 500, { error: e.message }));
|
|
376
|
+
}).catch(e => jsonResponse(res, 400, { error: e.message }));
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
237
380
|
// GET /api/reviews/diff-stat?project=...&base=...
|
|
238
381
|
if (p === '/api/reviews/diff-stat' && m === 'GET') {
|
|
239
382
|
const project = url.searchParams.get('project');
|
|
@@ -364,9 +507,9 @@ function handleReviewApi(req, res, url) {
|
|
|
364
507
|
const ext = path.extname(filePath).toLowerCase();
|
|
365
508
|
const imageExts = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon', '.bmp': 'image/bmp' };
|
|
366
509
|
if (!imageExts[ext]) return jsonResponse(res, 400, { error: 'Only image files are supported' });
|
|
367
|
-
// Prevent path traversal
|
|
368
|
-
const resolved =
|
|
369
|
-
if (!resolved
|
|
510
|
+
// Prevent path traversal (incl. symlinks escaping the project — see resolveWithinProject)
|
|
511
|
+
const resolved = resolveWithinProject(project, filePath);
|
|
512
|
+
if (!resolved) return jsonResponse(res, 403, { error: 'Invalid file path' });
|
|
370
513
|
try {
|
|
371
514
|
const data = fs.readFileSync(resolved);
|
|
372
515
|
res.writeHead(200, { 'Content-Type': imageExts[ext], 'Content-Length': data.length, 'Cache-Control': 'no-cache' });
|
|
@@ -462,6 +605,108 @@ function handleReviewApi(req, res, url) {
|
|
|
462
605
|
return jsonResponse(res, 200, { prompt });
|
|
463
606
|
}
|
|
464
607
|
|
|
608
|
+
// GET /api/reviews/documents?project=... — list reviewable markdown/text files in a
|
|
609
|
+
// project (powers the Review Hub "Documents" discovery column). Tracked + untracked-
|
|
610
|
+
// not-ignored, bounded, sorted most-recently-modified first.
|
|
611
|
+
if (p === '/api/reviews/documents' && m === 'GET') {
|
|
612
|
+
const project = url.searchParams.get('project');
|
|
613
|
+
if (!project) return jsonResponse(res, 400, { error: 'project required' });
|
|
614
|
+
if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
|
|
615
|
+
try {
|
|
616
|
+
const { execFileSync } = require('child_process');
|
|
617
|
+
const root = path.resolve(project);
|
|
618
|
+
const globs = ['*.md', '*.markdown', '*.mdown', '*.txt'];
|
|
619
|
+
// -z → NUL-separated so paths with spaces/specials survive (no core.quotePath issues).
|
|
620
|
+
const run = (args) => {
|
|
621
|
+
try {
|
|
622
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 })
|
|
623
|
+
.split('\0').filter(Boolean);
|
|
624
|
+
} catch { return []; }
|
|
625
|
+
};
|
|
626
|
+
const set = new Set([
|
|
627
|
+
...run(['ls-files', '-z', ...globs]),
|
|
628
|
+
...run(['ls-files', '-z', '--others', '--exclude-standard', ...globs]),
|
|
629
|
+
]);
|
|
630
|
+
let items = [...set].slice(0, 2000).map(rel => {
|
|
631
|
+
let mtime = 0;
|
|
632
|
+
try { mtime = fs.statSync(path.join(root, rel)).mtimeMs; } catch {}
|
|
633
|
+
return { path: rel, name: path.basename(rel), dir: path.dirname(rel), mtime };
|
|
634
|
+
});
|
|
635
|
+
items.sort((a, b) => b.mtime - a.mtime);
|
|
636
|
+
return jsonResponse(res, 200, { documents: items.slice(0, 300), total: set.size });
|
|
637
|
+
} catch (e) {
|
|
638
|
+
return jsonResponse(res, 500, { error: e.message });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// GET /api/reviews/serve-html?project=...&path=... — serve a project .html file so the
|
|
643
|
+
// file-router can open it in a new browser tab. Path-validated; no traversal outside project.
|
|
644
|
+
if (p === '/api/reviews/serve-html' && m === 'GET') {
|
|
645
|
+
const project = url.searchParams.get('project');
|
|
646
|
+
const filePath = url.searchParams.get('path');
|
|
647
|
+
if (!project || !filePath) return jsonResponse(res, 400, { error: 'project and path required' });
|
|
648
|
+
if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
|
|
649
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
650
|
+
if (ext !== '.html' && ext !== '.htm') return jsonResponse(res, 400, { error: 'Only .html files are supported' });
|
|
651
|
+
let resolved = resolveWithinProject(project, filePath);
|
|
652
|
+
// The file may be agent-authored HTML in the session scratchpad (absolute /tmp path,
|
|
653
|
+
// outside the project and HOME). realpath-validated to the user's claude-<uid> temp dir.
|
|
654
|
+
if (!resolved) resolved = resolveWithinScratchpad(filePath);
|
|
655
|
+
if (!resolved) return jsonResponse(res, 403, { error: 'Invalid file path' });
|
|
656
|
+
// Fallback: the path may be relative to the repo root while the session cwd is a
|
|
657
|
+
// subdir/worktree — search the repo for the file before giving up.
|
|
658
|
+
if (!fs.existsSync(resolved)) {
|
|
659
|
+
const found = findHtmlInRepo(path.resolve(project), filePath);
|
|
660
|
+
if (found) resolved = found;
|
|
661
|
+
}
|
|
662
|
+
// Sandbox the served file: arbitrary repo HTML rendered on the CTM origin (which holds
|
|
663
|
+
// the auth cookie). A bare `sandbox` blocks scripting AND assigns an opaque origin, so it
|
|
664
|
+
// can't run JS, read CTM storage, or make cookie-authed calls — inline CSS/images still render.
|
|
665
|
+
const htmlHeaders = (len) => ({
|
|
666
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
667
|
+
'Content-Length': len,
|
|
668
|
+
'Cache-Control': 'no-cache',
|
|
669
|
+
'X-Content-Type-Options': 'nosniff',
|
|
670
|
+
'Content-Security-Policy': 'sandbox',
|
|
671
|
+
});
|
|
672
|
+
let data;
|
|
673
|
+
try { data = fs.readFileSync(resolved); }
|
|
674
|
+
catch {
|
|
675
|
+
// Friendly in-tab page instead of a scary "unreachable file" / bare text 404.
|
|
676
|
+
const body = Buffer.from(htmlNotFoundPage(filePath));
|
|
677
|
+
res.writeHead(404, htmlHeaders(body.length));
|
|
678
|
+
res.end(body);
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
res.writeHead(200, htmlHeaders(data.length));
|
|
682
|
+
res.end(data);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// POST /api/reviews/reveal {project, path} — reveal a project file in Finder (macOS
|
|
687
|
+
// `open -R`). Path-validated; best-effort (the file-router menu's "Reveal" action).
|
|
688
|
+
if (p === '/api/reviews/reveal' && m === 'POST') {
|
|
689
|
+
readBody(req).then(body => {
|
|
690
|
+
const project = String(body.project || '');
|
|
691
|
+
const filePath = String(body.path || '');
|
|
692
|
+
if (!project || !filePath) return jsonResponse(res, 400, { error: 'project and path required' });
|
|
693
|
+
if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
|
|
694
|
+
const resolved = resolveWithinProject(project, filePath);
|
|
695
|
+
if (!resolved) return jsonResponse(res, 403, { error: 'Invalid file path' });
|
|
696
|
+
if (!fs.existsSync(resolved)) return jsonResponse(res, 404, { error: 'File not found' });
|
|
697
|
+
try {
|
|
698
|
+
const { execFile } = require('child_process');
|
|
699
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
700
|
+
const args = process.platform === 'darwin' ? ['-R', resolved] : [path.dirname(resolved)];
|
|
701
|
+
execFile(cmd, args, () => {});
|
|
702
|
+
jsonResponse(res, 200, { ok: true });
|
|
703
|
+
} catch (e) {
|
|
704
|
+
jsonResponse(res, 500, { error: e.message });
|
|
705
|
+
}
|
|
706
|
+
}).catch(e => jsonResponse(res, 400, { error: e.message }));
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
|
|
465
710
|
return false; // Not handled
|
|
466
711
|
}
|
|
467
712
|
|
|
@@ -508,4 +753,7 @@ module.exports = {
|
|
|
508
753
|
_sanitizeReviewCommentInput: sanitizeReviewCommentInput,
|
|
509
754
|
_sanitizeReviewCommentUpdate: sanitizeReviewCommentUpdate,
|
|
510
755
|
_documentReview: documentReview,
|
|
756
|
+
_resolveWithinProject: resolveWithinProject,
|
|
757
|
+
_resolveWithinScratchpad: resolveWithinScratchpad,
|
|
758
|
+
_findHtmlInRepo: findHtmlInRepo,
|
|
511
759
|
};
|