diff --git a/package.json b/package.json index 87067d6..d2ca2ac 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,16 @@ { "name": "@farhoodliquor/paperclip-adapter-claude-k8s", - "version": "0.1.1", + "version": "0.1.8", "description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/farhoodliquor/paperclip-adapter-claude-k8s" + }, + "bugs": { + "url": "https://github.com/farhoodliquor/paperclip-adapter-claude-k8s/issues" + }, + "homepage": "https://github.com/farhoodliquor/paperclip-adapter-claude-k8s#readme", "type": "module", "paperclip": { "adapterUiParser": "1.0.0" @@ -10,7 +18,8 @@ "exports": { ".": "./dist/index.js", "./server": "./dist/server/index.js", - "./ui-parser": "./dist/ui-parser.js" + "./ui-parser": "./dist/ui-parser.js", + "./cli": "./dist/cli/index.js" }, "files": [ "dist" @@ -18,10 +27,14 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage" }, "dependencies": { - "@kubernetes/client-node": "^1.0.0" + "@kubernetes/client-node": "^1.0.0", + "picocolors": "^1.1.1" }, "peerDependencies": { "@paperclipai/adapter-utils": ">=0.3.0" @@ -29,6 +42,8 @@ "devDependencies": { "@paperclipai/adapter-utils": "^0.3.0", "@types/node": "^24.6.0", - "typescript": "^5.7.3" + "@vitest/coverage-v8": "^4.1.4", + "typescript": "^5.7.3", + "vitest": "^4.1.4" } } diff --git a/src/index.ts b/src/index.ts index 03a5256..4af773d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,6 @@ export const models = [ { id: "claude-haiku-4-6", label: "Claude Haiku 4.6" }, { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" }, { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }, - // AWS Bedrock US inference profile IDs - { id: "us.anthropic.claude-opus-4-6-v1", label: "Bedrock Opus 4.6" }, - { id: "us.anthropic.claude-sonnet-4-6", label: "Bedrock Sonnet 4.6" }, - { id: "us.anthropic.claude-opus-4-5-20251101-v1:0", label: "Bedrock Opus 4.5" }, - { id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", label: "Bedrock Sonnet 4.5" }, - { id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", label: "Bedrock Haiku 4.5" }, - { id: "us.anthropic.claude-opus-4-1-20250805-v1:0", label: "Bedrock Opus 4.1" }, - { id: "us.anthropic.claude-sonnet-4-20250514-v1:0", label: "Bedrock Sonnet 4" }, ]; export const agentConfigurationDoc = `# claude_k8s agent configuration @@ -51,8 +43,7 @@ Operational fields: - graceSec (number, optional): additional grace before adapter gives up after Job deadline Inherited from Deployment (no config needed): -- CLAUDE_CODE_USE_BEDROCK, AWS_REGION, AWS_BEARER_TOKEN_BEDROCK -- ANTHROPIC_API_KEY, OPENAI_API_KEY +- ANTHROPIC_API_KEY, OPENAI_API_KEY, and other provider API keys - PAPERCLIP_API_URL - Container image, imagePullSecrets, DNS config, PVC mount, security context @@ -63,3 +54,4 @@ Notes: `; export { createServerAdapter } from "./server/index.js"; +export { printClaudeStreamEvent } from "./cli/index.js"; diff --git a/src/server/config-schema.ts b/src/server/config-schema.ts new file mode 100644 index 0000000..e9ce2a7 --- /dev/null +++ b/src/server/config-schema.ts @@ -0,0 +1,173 @@ +export interface AdapterConfigSchema { + sections?: AdapterConfigSection[]; +} + +export interface AdapterConfigSection { + title: string; + fields: ConfigFieldSchema[]; +} + +export type ConfigFieldSchema = + | TextFieldSchema + | NumberFieldSchema + | ToggleFieldSchema + | SelectFieldSchema + | TextareaFieldSchema; + +export interface TextFieldSchema { + type: "text"; + key: string; + label: string; + description?: string; + placeholder?: string; + helpLink?: string; +} + +export interface NumberFieldSchema { + type: "number"; + key: string; + label: string; + description?: string; + placeholder?: string; + helpLink?: string; +} + +export interface ToggleFieldSchema { + type: "toggle"; + key: string; + label: string; + description?: string; + helpLink?: string; +} + +export interface SelectFieldSchema { + type: "select"; + key: string; + label: string; + description?: string; + options: { value: string; label: string }[]; + helpLink?: string; +} + +export interface TextareaFieldSchema { + type: "textarea"; + key: string; + label: string; + description?: string; + placeholder?: string; + helpLink?: string; +} + +export function getConfigSchema(): AdapterConfigSchema { + return { + sections: [ + { + title: "Kubernetes", + fields: [ + { + type: "text", + key: "namespace", + label: "Namespace", + description: "Kubernetes namespace for Jobs. Defaults to the Deployment namespace.", + }, + { + type: "text", + key: "image", + label: "Container Image", + description: "Override the container image used for Job pods. Defaults to the running Deployment image.", + placeholder: "registry/image:tag", + }, + { + type: "select", + key: "imagePullPolicy", + label: "Image Pull Policy", + description: "Image pull policy for the container image.", + options: [ + { value: "IfNotPresent", label: "IfNotPresent" }, + { value: "Always", label: "Always" }, + { value: "Never", label: "Never" }, + ], + }, + { + type: "text", + key: "kubeconfig", + label: "Kubeconfig Path", + description: "Absolute path to a kubeconfig file on disk. Defaults to in-cluster service account auth.", + placeholder: "/path/to/kubeconfig", + }, + { + type: "number", + key: "ttlSecondsAfterFinished", + label: "TTL Seconds After Finished", + description: "Auto-cleanup delay for completed Jobs in seconds. Default: 300.", + }, + { + type: "toggle", + key: "retainJobs", + label: "Retain Jobs", + description: "Skip cleanup of completed Jobs for debugging purposes.", + }, + ], + }, + { + title: "Resource Limits", + fields: [ + { + type: "text", + key: "resources.requests.cpu", + label: "CPU Request", + description: "CPU request for Job pods (e.g. 100m, 0.5, 1).", + placeholder: "100m", + }, + { + type: "text", + key: "resources.requests.memory", + label: "Memory Request", + description: "Memory request for Job pods (e.g. 128Mi, 512Mi, 1Gi).", + placeholder: "512Mi", + }, + { + type: "text", + key: "resources.limits.cpu", + label: "CPU Limit", + description: "CPU limit for Job pods (e.g. 100m, 0.5, 1).", + placeholder: "1000m", + }, + { + type: "text", + key: "resources.limits.memory", + label: "Memory Limit", + description: "Memory limit for Job pods (e.g. 128Mi, 512Mi, 1Gi).", + placeholder: "1Gi", + }, + ], + }, + { + title: "Scheduling", + fields: [ + { + type: "textarea", + key: "nodeSelector", + label: "Node Selector", + description: "Node selector for Job pods. One key=value per line (e.g. disktype=ssd).", + placeholder: "disktype=ssd\ngpu=true", + }, + { + type: "textarea", + key: "tolerations", + label: "Tolerations", + description: "Tolerations for Job pods as JSON array.", + placeholder: '[{"key":"node-type","operator":"Equal","value":"gpu","effect":"NoSchedule"}]', + }, + { + type: "textarea", + key: "labels", + label: "Labels", + description: "Extra labels added to Job metadata. One key=value per line.", + placeholder: "team=ai\nenv=prod", + }, + ], + }, + ], + }; +} diff --git a/src/server/index.ts b/src/server/index.ts index d14ede6..e83bc86 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,6 +3,7 @@ import { type, models, agentConfigurationDoc } from "../index.js"; import { execute } from "./execute.js"; import { testEnvironment } from "./test.js"; import { sessionCodec } from "./session.js"; +import { getConfigSchema } from "./config-schema.js"; export function createServerAdapter(): ServerAdapterModule { return { @@ -13,7 +14,8 @@ export function createServerAdapter(): ServerAdapterModule { models, supportsLocalAgentJwt: true, agentConfigurationDoc, - }; + getConfigSchema, + } as ServerAdapterModule; } export { execute, testEnvironment, sessionCodec }; diff --git a/tsconfig.json b/tsconfig.json index 5ff4203..07a92d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,6 @@ "resolveJsonModule": true, "isolatedModules": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e6905f1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + include: ["src/**/*.test.ts"], + coverage: { + reporter: ["text", "lcov"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.d.ts", "src/index.ts"], + }, + }, +});