Compare commits

..

2 Commits

Author SHA1 Message Date
Chris Farhood 856bc93e24 0.2.1 2026-04-28 06:51:21 -04:00
Chris Farhood 0e43811304 fix(ui-parser): emit dist as CJS so the sandboxed worker can load it
tsc emitted ESM `export` syntax that the UI's `new Function(...)` sandbox
can't evaluate, so parseStdoutLine never registered and the run window
fell back to dumping raw stream-json. Add an esbuild step that overwrites
dist/ui-parser.js with a CJS bundle assigning to module.exports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 06:51:18 -04:00
8 changed files with 161 additions and 167 deletions
-3
View File
@@ -30,9 +30,6 @@ jobs:
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: (github.ref == 'refs/heads/master' && github.event_name == 'push') || startsWith(github.ref, 'refs/tags/') if: (github.ref == 'refs/heads/master' && github.event_name == 'push') || startsWith(github.ref, 'refs/tags/')
concurrency:
group: publish-${{ github.sha }}
cancel-in-progress: false
permissions: permissions:
id-token: write id-token: write
steps: steps:
-2
View File
@@ -1,5 +1,3 @@
> **Abandoned** — This adapter is no longer maintained. The Kubernetes execution capability has moved to the sandbox plugin at [`farhoodlabs/paperclip-plugin-k8s`](https://github.com/farhoodlabs/paperclip-plugin-k8s) (`@farhoodlabs/paperclip-plugin-k8s` on npm).
# Claude (Kubernetes) Paperclip Adapter Plugin # Claude (Kubernetes) Paperclip Adapter Plugin
Paperclip adapter plugin that runs Claude Code agents as isolated Kubernetes Jobs instead of inside the main Paperclip process. Paperclip adapter plugin that runs Claude Code agents as isolated Kubernetes Jobs instead of inside the main Paperclip process.
+138 -142
View File
@@ -1,152 +1,148 @@
/** "use strict";
* Self-contained stdout parser for Claude stream-json output. var __defProp = Object.defineProperty;
* Zero external imports — required by the Paperclip adapter plugin UI parser contract. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
*/ var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/ui-parser.ts
var ui_parser_exports = {};
__export(ui_parser_exports, {
parseStdoutLine: () => parseStdoutLine
});
module.exports = __toCommonJS(ui_parser_exports);
function asRecord(value) { function asRecord(value) {
if (typeof value !== "object" || value === null || Array.isArray(value)) if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
return null; return value;
return value;
} }
function asNumber(value) { function asNumber(value) {
return typeof value === "number" && Number.isFinite(value) ? value : 0; return typeof value === "number" && Number.isFinite(value) ? value : 0;
} }
function errorText(value) { function errorText(value) {
if (typeof value === "string") if (typeof value === "string") return value;
return value; const rec = asRecord(value);
const rec = asRecord(value); if (!rec) return "";
if (!rec) const msg = typeof rec.message === "string" && rec.message || typeof rec.error === "string" && rec.error || typeof rec.code === "string" && rec.code || "";
return ""; if (msg) return msg;
const msg = (typeof rec.message === "string" && rec.message) || try {
(typeof rec.error === "string" && rec.error) || return JSON.stringify(rec);
(typeof rec.code === "string" && rec.code) || } catch {
""; return "";
if (msg) }
return msg;
try {
return JSON.stringify(rec);
}
catch {
return "";
}
} }
function safeJsonParse(text) { function safeJsonParse(text) {
try { try {
return JSON.parse(text); return JSON.parse(text);
} } catch {
catch { return null;
return null; }
}
} }
export function parseStdoutLine(line, ts) { function parseStdoutLine(line, ts) {
const parsed = asRecord(safeJsonParse(line)); const parsed = asRecord(safeJsonParse(line));
if (!parsed) { if (!parsed) {
return [{ kind: "stdout", ts, text: line }];
}
const type = typeof parsed.type === "string" ? parsed.type : "";
if (type === "system" && parsed.subtype === "init") {
return [
{
kind: "init",
ts,
model: typeof parsed.model === "string" ? parsed.model : "unknown",
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : "",
},
];
}
if (type === "assistant") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block)
continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text)
entries.push({ kind: "assistant", ts, text });
}
else if (blockType === "thinking") {
const text = typeof block.thinking === "string" ? block.thinking : "";
if (text)
entries.push({ kind: "thinking", ts, text });
}
else if (blockType === "tool_use") {
entries.push({
kind: "tool_call",
ts,
name: typeof block.name === "string" ? block.name : "unknown",
toolUseId: typeof block.id === "string"
? block.id
: typeof block.tool_use_id === "string"
? block.tool_use_id
: undefined,
input: block.input ?? {},
});
}
}
return entries.length > 0 ? entries : [{ kind: "stdout", ts, text: line }];
}
if (type === "user") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block)
continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text)
entries.push({ kind: "user", ts, text });
}
else if (blockType === "tool_result") {
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
const isError = block.is_error === true;
let text = "";
if (typeof block.content === "string") {
text = block.content;
}
else if (Array.isArray(block.content)) {
const parts = [];
for (const part of block.content) {
const p = asRecord(part);
if (p && typeof p.text === "string")
parts.push(p.text);
}
text = parts.join("\n");
}
entries.push({ kind: "tool_result", ts, toolUseId, content: text, isError });
}
}
if (entries.length > 0)
return entries;
}
if (type === "result") {
const usage = asRecord(parsed.usage) ?? {};
const inputTokens = asNumber(usage.input_tokens);
const outputTokens = asNumber(usage.output_tokens);
const cachedTokens = asNumber(usage.cache_read_input_tokens);
const costUsd = asNumber(parsed.total_cost_usd);
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
const isError = parsed.is_error === true;
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(errorText).filter(Boolean) : [];
const text = typeof parsed.result === "string" ? parsed.result : "";
return [{
kind: "result",
ts,
text,
inputTokens,
outputTokens,
cachedTokens,
costUsd,
subtype,
isError,
errors,
}];
}
return [{ kind: "stdout", ts, text: line }]; return [{ kind: "stdout", ts, text: line }];
}
const type = typeof parsed.type === "string" ? parsed.type : "";
if (type === "system" && parsed.subtype === "init") {
return [
{
kind: "init",
ts,
model: typeof parsed.model === "string" ? parsed.model : "unknown",
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : ""
}
];
}
if (type === "assistant") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block) continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) entries.push({ kind: "assistant", ts, text });
} else if (blockType === "thinking") {
const text = typeof block.thinking === "string" ? block.thinking : "";
if (text) entries.push({ kind: "thinking", ts, text });
} else if (blockType === "tool_use") {
entries.push({
kind: "tool_call",
ts,
name: typeof block.name === "string" ? block.name : "unknown",
toolUseId: typeof block.id === "string" ? block.id : typeof block.tool_use_id === "string" ? block.tool_use_id : void 0,
input: block.input ?? {}
});
}
}
return entries.length > 0 ? entries : [{ kind: "stdout", ts, text: line }];
}
if (type === "user") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block) continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) entries.push({ kind: "user", ts, text });
} else if (blockType === "tool_result") {
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
const isError = block.is_error === true;
let text = "";
if (typeof block.content === "string") {
text = block.content;
} else if (Array.isArray(block.content)) {
const parts = [];
for (const part of block.content) {
const p = asRecord(part);
if (p && typeof p.text === "string") parts.push(p.text);
}
text = parts.join("\n");
}
entries.push({ kind: "tool_result", ts, toolUseId, content: text, isError });
}
}
if (entries.length > 0) return entries;
}
if (type === "result") {
const usage = asRecord(parsed.usage) ?? {};
const inputTokens = asNumber(usage.input_tokens);
const outputTokens = asNumber(usage.output_tokens);
const cachedTokens = asNumber(usage.cache_read_input_tokens);
const costUsd = asNumber(parsed.total_cost_usd);
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
const isError = parsed.is_error === true;
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(errorText).filter(Boolean) : [];
const text = typeof parsed.result === "string" ? parsed.result : "";
return [{
kind: "result",
ts,
text,
inputTokens,
outputTokens,
cachedTokens,
costUsd,
subtype,
isError,
errors
}];
}
return [{ kind: "stdout", ts, text: line }];
} }
//# sourceMappingURL=ui-parser.js.map
+7 -7
View File
@@ -1,19 +1,19 @@
{ {
"name": "paperclip-adapter-claude-k8s", "name": "paperclip-adapter-claude-k8s",
"version": "0.2.5", "version": "0.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "paperclip-adapter-claude-k8s", "name": "paperclip-adapter-claude-k8s",
"version": "0.2.5", "version": "0.2.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@kubernetes/client-node": "^1.0.0", "@kubernetes/client-node": "^1.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@paperclipai/adapter-utils": "^2026.428.0", "@paperclipai/adapter-utils": "2026.415.0-canary.7",
"@types/node": "^24.6.0", "@types/node": "^24.6.0",
"@vitest/coverage-v8": "^4.1.4", "@vitest/coverage-v8": "^4.1.4",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
@@ -21,7 +21,7 @@
"vitest": "^4.1.4" "vitest": "^4.1.4"
}, },
"peerDependencies": { "peerDependencies": {
"@paperclipai/adapter-utils": ">=2026.428.0" "@paperclipai/adapter-utils": ">=2026.415.0-canary.7"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
@@ -649,9 +649,9 @@
} }
}, },
"node_modules/@paperclipai/adapter-utils": { "node_modules/@paperclipai/adapter-utils": {
"version": "2026.428.0", "version": "2026.415.0-canary.7",
"resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.428.0.tgz", "resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.415.0-canary.7.tgz",
"integrity": "sha512-kGHpE7rhePPCbnG3OwXbNuHZZuI+XyuFgNSiDnrEeiSbkI2c5XHM2WnWDCZ/NGHULfJW3lWhSxGMFoYqiy38vQ==", "integrity": "sha512-VNzIZmu1lrK6QM8Ad9WkOihZItfkj21NHKQf+artDcbwFT2hHbDAD9hdW2W9NMVxYdFvvnws3w76FI/BUbCMbQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "paperclip-adapter-claude-k8s", "name": "paperclip-adapter-claude-k8s",
"version": "0.2.5", "version": "0.2.1",
"description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs", "description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -38,10 +38,10 @@
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"@paperclipai/adapter-utils": ">=2026.428.0" "@paperclipai/adapter-utils": ">=2026.415.0-canary.7"
}, },
"devDependencies": { "devDependencies": {
"@paperclipai/adapter-utils": "^2026.428.0", "@paperclipai/adapter-utils": "2026.415.0-canary.7",
"@types/node": "^24.6.0", "@types/node": "^24.6.0",
"@vitest/coverage-v8": "^4.1.4", "@vitest/coverage-v8": "^4.1.4",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
+1 -1
View File
@@ -1234,7 +1234,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath, jobObserver).then(r => { logStopSignal.stopped = true; return r; }), waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath, jobObserver).then(r => { logStopSignal.stopped = true; return r; }),
]); ]);
stdout = tailResult.status === "fulfilled" ? tailResult.value : ""; const stdout = tailResult.status === "fulfilled" ? tailResult.value : "";
if (completionResult.status === "fulfilled") { if (completionResult.status === "fulfilled") {
jobTimedOut = completionResult.value.timedOut; jobTimedOut = completionResult.value.timedOut;
+9 -6
View File
@@ -301,7 +301,8 @@ describe("buildJobManifest", () => {
const init = job.spec?.template?.spec?.initContainers?.[0]; const init = job.spec?.template?.spec?.initContainers?.[0];
expect(init?.command?.[0]).toBe("sh"); expect(init?.command?.[0]).toBe("sh");
expect(init?.command?.[1]).toBe("-c"); expect(init?.command?.[1]).toBe("-c");
expect(init?.command?.[2]).toBe("printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"); expect(init?.command?.[2]).toContain("mkdir -p /paperclip/instances/default/run-logs/");
expect(init?.command?.[2]).toContain("printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt");
}); });
it("write-prompt mounts prompt volume", () => { it("write-prompt mounts prompt volume", () => {
@@ -807,26 +808,28 @@ describe("buildJobManifest", () => {
const { job } = buildJobManifest({ ctx, selfPod }); const { job } = buildJobManifest({ ctx, selfPod });
const cmd = job.spec?.template?.spec?.containers[0]?.command?.[2] ?? ""; const cmd = job.spec?.template?.spec?.containers[0]?.command?.[2] ?? "";
expect(cmd).toContain("| tee"); expect(cmd).toContain("| tee");
expect(cmd).toContain("/paperclip/instances/default/data/run-logs/"); expect(cmd).toContain("/paperclip/instances/default/run-logs/");
}); });
it("podLogPath is returned from buildJobManifest", () => { it("podLogPath is returned from buildJobManifest", () => {
const result = buildJobManifest({ ctx, selfPod }); const result = buildJobManifest({ ctx, selfPod });
expect(result.podLogPath).toBe( expect(result.podLogPath).toBe(
"/paperclip/instances/default/data/run-logs/co1/agent-abc/run-abc12345.pod.ndjson", "/paperclip/instances/default/run-logs/co1/agent-abc/run-abc12345.pod.ndjson",
); );
}); });
it("buildPodLogPath returns correctly formatted path", () => { it("buildPodLogPath returns correctly formatted path", () => {
expect(buildPodLogPath("co1", "agent-abc", "run-abc12345")).toBe( expect(buildPodLogPath("co1", "agent-abc", "run-abc12345")).toBe(
"/paperclip/instances/default/data/run-logs/co1/agent-abc/run-abc12345.pod.ndjson", "/paperclip/instances/default/run-logs/co1/agent-abc/run-abc12345.pod.ndjson",
); );
}); });
it("init container does not create log directory (server pre-creates it on shared PVC)", () => { it("init container creates log directory", () => {
const { job } = buildJobManifest({ ctx, selfPod }); const { job } = buildJobManifest({ ctx, selfPod });
const initCmd = job.spec?.template?.spec?.initContainers?.[0]?.command; const initCmd = job.spec?.template?.spec?.initContainers?.[0]?.command;
expect(initCmd?.[2]).not.toContain("mkdir -p /paperclip"); expect(initCmd?.[0]).toBe("sh");
expect(initCmd?.[1]).toBe("-c");
expect(initCmd?.[2]).toContain("mkdir -p /paperclip/instances/default/run-logs/");
}); });
it("sanitizes companyId with / to valid path component for log path", () => { it("sanitizes companyId with / to valid path component for log path", () => {
+3 -3
View File
@@ -23,7 +23,7 @@ function sanitizeForK8sPath(value: string): string {
} }
export function buildPodLogPath(companyId: string, agentId: string, runId: string): string { export function buildPodLogPath(companyId: string, agentId: string, runId: string): string {
return `/paperclip/instances/default/data/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`; return `/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
} }
/** Prompts above this size (bytes) are staged via a Secret instead of an /** Prompts above this size (bytes) are staged via a Secret instead of an
@@ -504,7 +504,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
name: "write-prompt", name: "write-prompt",
image: "busybox:1.36", image: "busybox:1.36",
imagePullPolicy: "IfNotPresent", imagePullPolicy: "IfNotPresent",
command: ["sh", "-c", "cp /tmp/prompt-secret/prompt.txt /tmp/prompt/prompt.txt"], command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${agent.companyId}/${agent.id} && cp /tmp/prompt-secret/prompt.txt /tmp/prompt/prompt.txt`],
volumeMounts: [ volumeMounts: [
{ name: "prompt", mountPath: "/tmp/prompt" }, { name: "prompt", mountPath: "/tmp/prompt" },
{ name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true }, { name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true },
@@ -519,7 +519,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
name: "write-prompt", name: "write-prompt",
image: "busybox:1.36", image: "busybox:1.36",
imagePullPolicy: "IfNotPresent", imagePullPolicy: "IfNotPresent",
command: ["sh", "-c", `printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`], command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${agent.companyId}/${agent.id} && printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`],
env: [{ name: "PROMPT_CONTENT", value: prompt }], env: [{ name: "PROMPT_CONTENT", value: prompt }],
volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }], volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
securityContext, securityContext,