mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
- Pass the attachment request `source` through the server deps lambda and make it a required param on SessionListenerDeps.registerAttachment + the wiring event type (the 2-arg lambda silently dropped `source`, force-confining every codex-generated artifact — the feature never worked outside the workspace); new test/session-listener-wiring.test.ts asserts the pass-through - Gate the Codex `Saved to: file://` scanner on mode === 'codex' via a codexArtifacts option threaded from the session call site; magic links stay mode-agnostic; tests assert claude/shell sessions never emit codex-generated requests - Decide the generated-artifact trust policy on the realpath-RESOLVED path (unresolvable → force-confined) and anchor the ~/.codex marker dirs to os.homedir() prefixes with startsWith instead of substring matching; symlink escape + unanchored-marker regression tests added - Run the Codex scanner on stripAnsi'd data so trailing SGR sequences don't ride into the captured URL; styled 'Saved to:' test added - Extend generateFirstPageThumbnail with jpg/jpeg/gif/webp passthrough and per-extension content types (mirrors the png passthrough) so the PR's new image formats render real thumbnails instead of 204 letter-tiles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
/**
|
|
* @fileoverview Parses terminal magic links that request attachment cards.
|
|
*/
|
|
|
|
import { isAbsolute } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { isSupportedAttachmentExtension } from './attachment-registry.js';
|
|
import { stripAnsi } from './utils/index.js';
|
|
|
|
const MAGIC_LINK_RE = /codeman:\/\/attach\?([^\s<>"']+)/g;
|
|
const CODEX_SAVED_FILE_RE = /\bSaved to:\s*(file:\/\/[^\s<>"']+)/gi;
|
|
|
|
export interface TerminalAttachmentRequest {
|
|
path: string;
|
|
source: 'external' | 'codex-generated';
|
|
}
|
|
|
|
export interface ParseTerminalAttachmentOptions {
|
|
/**
|
|
* Enable the Codex `Saved to: file://...` scanner. Only codex-mode sessions
|
|
* may set this — the relaxed codex-generated trust policy must never be
|
|
* reachable from other modes' (prompt-injectable) terminal output.
|
|
*/
|
|
codexArtifacts?: boolean;
|
|
}
|
|
|
|
export function parseAttachmentMagicLinks(data: string): string[] {
|
|
return parseMagicAttachmentRequests(data).map((request) => request.path);
|
|
}
|
|
|
|
export function parseTerminalAttachmentRequests(
|
|
data: string,
|
|
options: ParseTerminalAttachmentOptions = {}
|
|
): TerminalAttachmentRequest[] {
|
|
const results: TerminalAttachmentRequest[] = [];
|
|
const seen = new Set<string>();
|
|
const requests = options.codexArtifacts
|
|
? [...parseMagicAttachmentRequests(data), ...parseCodexGeneratedArtifactRequests(data)]
|
|
: parseMagicAttachmentRequests(data);
|
|
|
|
for (const request of requests) {
|
|
const key = `${request.source}:${request.path}`;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
results.push(request);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function parseMagicAttachmentRequests(data: string): TerminalAttachmentRequest[] {
|
|
const results: string[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
for (const match of data.matchAll(MAGIC_LINK_RE)) {
|
|
const query = trimTrailingPunctuation(match[1] || '');
|
|
try {
|
|
const params = new URLSearchParams(query);
|
|
const filePath = params.get('path');
|
|
if (!filePath || !isAbsolute(filePath)) continue;
|
|
const extension = filePath.split('.').pop()?.toLowerCase() || '';
|
|
if (!isSupportedAttachmentExtension(extension)) continue;
|
|
if (seen.has(filePath)) continue;
|
|
seen.add(filePath);
|
|
results.push(filePath);
|
|
} catch {
|
|
// Ignore malformed terminal text. Magic links are advisory.
|
|
}
|
|
}
|
|
|
|
return results.map((path) => ({ path, source: 'external' }));
|
|
}
|
|
|
|
function parseCodexGeneratedArtifactRequests(data: string): TerminalAttachmentRequest[] {
|
|
const results: TerminalAttachmentRequest[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
// Codex styles its TUI output — strip ANSI first so a trailing SGR reset
|
|
// (e.g. `...mockup.png\x1b[0m`) doesn't ride into the captured URL and break
|
|
// the extension allowlist check.
|
|
for (const match of stripAnsi(data).matchAll(CODEX_SAVED_FILE_RE)) {
|
|
const rawUrl = trimTrailingPunctuation(match[1] || '');
|
|
try {
|
|
const filePath = fileURLToPath(rawUrl);
|
|
if (!isAbsolute(filePath)) continue;
|
|
const extension = filePath.split('.').pop()?.toLowerCase() || '';
|
|
if (!isSupportedAttachmentExtension(extension)) continue;
|
|
if (seen.has(filePath)) continue;
|
|
seen.add(filePath);
|
|
results.push({ path: filePath, source: 'codex-generated' });
|
|
} catch {
|
|
// Ignore malformed terminal text. Generated-artifact links are advisory.
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function trimTrailingPunctuation(value: string): string {
|
|
return value.replace(/[),.;:]+$/g, '');
|
|
}
|