779ca4872b
- Removed Prettier configuration files (.prettierignore and .prettierrc) and migrated formatting to Biome. - Updated Biome configuration from version 2.3.11 to 2.3.12 and changed arrowParentheses rule from 'always' to 'asNeeded'. - Pinned @biomejs/biome dependency to exact version 2.3.12 in package.json and bun.lock. - Applied consistent arrow function formatting across 489 files by removing unnecessary parentheses around single parameters. - Removed blank lines after comment blocks and reorganized imports for consistency across the codebase.
98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
/**
|
|
* Protocol handler for artifact:// URLs.
|
|
*
|
|
* Resolves artifact IDs to files in the session artifacts directory.
|
|
* Unlike agent://, artifacts are raw text with no JSON extraction.
|
|
*
|
|
* URL form:
|
|
* - artifact://<id> - Full artifact content
|
|
*
|
|
* Pagination is handled by the read tool via offset/limit parameters.
|
|
*/
|
|
import * as fs from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
|
|
export interface ArtifactProtocolOptions {
|
|
/**
|
|
* Returns the artifacts directory path, or null if no session.
|
|
*/
|
|
getArtifactsDir: () => string | null;
|
|
}
|
|
|
|
/**
|
|
* List available artifact IDs in the directory.
|
|
*/
|
|
async function listAvailableArtifacts(artifactsDir: string): Promise<string[]> {
|
|
try {
|
|
const files = await fs.readdir(artifactsDir);
|
|
return files
|
|
.filter(f => /^\d+\./.test(f))
|
|
.map(f => f.split(".")[0])
|
|
.sort((a, b) => Number(a) - Number(b));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for artifact:// URLs.
|
|
*
|
|
* Resolves numeric artifact IDs to their text content.
|
|
* Artifacts are created by tools when output is truncated.
|
|
*/
|
|
export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
readonly scheme = "artifact";
|
|
|
|
constructor(private readonly options: ArtifactProtocolOptions) {}
|
|
|
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
const artifactsDir = this.options.getArtifactsDir();
|
|
if (!artifactsDir) {
|
|
throw new Error("No session - artifacts unavailable");
|
|
}
|
|
|
|
// Extract artifact ID from host
|
|
const id = url.rawHost || url.hostname;
|
|
if (!id) {
|
|
throw new Error("artifact:// URL requires a numeric ID: artifact://0");
|
|
}
|
|
|
|
// Validate ID is numeric
|
|
if (!/^\d+$/.test(id)) {
|
|
throw new Error(`artifact:// ID must be numeric, got: ${id}`);
|
|
}
|
|
|
|
// Check directory exists and find file matching ID prefix
|
|
let files: string[];
|
|
try {
|
|
files = await fs.readdir(artifactsDir);
|
|
} catch (err) {
|
|
if (isEnoent(err)) {
|
|
throw new Error("No artifacts directory found");
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
const match = files.find(f => f.startsWith(`${id}.`));
|
|
|
|
if (!match) {
|
|
const available = await listAvailableArtifacts(artifactsDir);
|
|
const availableStr = available.length > 0 ? available.join(", ") : "none";
|
|
throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
|
|
}
|
|
|
|
const filePath = path.join(artifactsDir, match);
|
|
const content = await Bun.file(filePath).text();
|
|
|
|
return {
|
|
url: url.href,
|
|
content,
|
|
contentType: "text/plain",
|
|
size: Buffer.byteLength(content, "utf-8"),
|
|
sourcePath: filePath,
|
|
};
|
|
}
|
|
}
|