From e0b8d99c096774ca7682c04cdb3c29d3817176de Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 10:48:38 +0000 Subject: [PATCH 1/7] feat(ask-project): capture and upload Claude Code session transcript at deploy time After each managed deploy, the Claude Code session transcript is automatically uploaded to R2 as a deployment artifact. This gives the ask feature rich context about what was being worked on when the code shipped. Two paths capture the transcript: - CLI deploy (jack deploy/ship): reads CLAUDE_TRANSCRIPT_PATH from env, which is exported by a new SessionStart hook via CLAUDE_ENV_FILE - MCP deploy (deploy_project tool): a new PostToolUse hook fires after the tool call and uploads the transcript directly New pieces: - PUT /v1/projects/:projectId/deployments/:deploymentId/session-transcript endpoint - jack _internal session-start: SessionStart hook handler (exports transcript path) - jack _internal post-deploy: PostToolUse hook handler (uploads after MCP deploy) - installClaudeCodeHooks() now also installs the two new hooks - DB migration 0031: has_session_transcript column on deployments https://claude.ai/code/session_01Naw4TaqKjHM1m2HFpxs23y --- apps/cli/src/commands/internal.ts | 117 ++++++++++++++++++ apps/cli/src/index.ts | 6 + apps/cli/src/lib/claude-hooks-installer.ts | 56 +++++++-- apps/cli/src/lib/managed-deploy.ts | 12 ++ apps/cli/src/lib/session-transcript.ts | 103 +++++++++++++++ .../0031_add_session_transcript.sql | 4 + apps/control-plane/src/index.ts | 57 +++++++++ 7 files changed, 344 insertions(+), 11 deletions(-) create mode 100644 apps/cli/src/commands/internal.ts create mode 100644 apps/cli/src/lib/session-transcript.ts create mode 100644 apps/control-plane/migrations/0031_add_session_transcript.sql diff --git a/apps/cli/src/commands/internal.ts b/apps/cli/src/commands/internal.ts new file mode 100644 index 0000000..4efd8a7 --- /dev/null +++ b/apps/cli/src/commands/internal.ts @@ -0,0 +1,117 @@ +import { appendFile } from "node:fs/promises"; +import { readProjectLink } from "../lib/project-link.ts"; +import { uploadSessionTranscript } from "../lib/session-transcript.ts"; + +/** + * Internal commands used by Claude Code hooks. + * Not exposed in the help text — these are implementation details. + */ +export default async function internal(subcommand?: string): Promise { + if (subcommand === "session-start") { + await handleSessionStart(); + return; + } + + if (subcommand === "post-deploy") { + await handlePostDeploy(); + return; + } + + // Unknown subcommand — exit silently (hooks must never error visibly) + process.exit(0); +} + +/** + * SessionStart hook handler. + * + * Claude Code passes JSON via stdin: + * { session_id, transcript_path, cwd, ... } + * + * We write CLAUDE_TRANSCRIPT_PATH to $CLAUDE_ENV_FILE so that subsequent + * Bash tool calls (e.g. `jack deploy`) can find the transcript and upload it. + */ +async function handleSessionStart(): Promise { + try { + const raw = await readStdin(); + if (!raw) return; + + const payload = JSON.parse(raw) as Record; + const transcriptPath = payload.transcript_path as string | undefined; + const envFile = process.env.CLAUDE_ENV_FILE; + + if (transcriptPath && envFile) { + await appendFile(envFile, `export CLAUDE_TRANSCRIPT_PATH='${transcriptPath}'\n`); + } + } catch { + // Never surface errors from hooks + } +} + +/** + * PostToolUse hook handler for deploy_project. + * + * Claude Code passes JSON via stdin: + * { hook_event_name, tool_name, tool_input, tool_response, transcript_path, cwd, session_id } + * + * We extract the deployment_id + project_id and upload the transcript. + */ +async function handlePostDeploy(): Promise { + try { + const raw = await readStdin(); + if (!raw) return; + + const payload = JSON.parse(raw) as Record; + + // Only handle deploy_project tool calls + if (payload.tool_name !== "deploy_project") return; + + const transcriptPath = payload.transcript_path as string | undefined; + if (!transcriptPath) return; + + // Parse the tool response to get deploymentId + const toolResponse = payload.tool_response as string | undefined; + if (!toolResponse) return; + + let deploymentId: string | undefined; + try { + const parsed = JSON.parse(toolResponse) as Record; + // Response shape: { success, data: { deploymentId, ... }, meta } + const data = parsed.data as Record | undefined; + deploymentId = data?.deploymentId as string | undefined; + } catch { + return; + } + if (!deploymentId) return; + + // Get project_id from the project link in the working directory + const cwd = (payload.cwd as string | undefined) ?? process.cwd(); + const projectPath = + (payload.tool_input as Record | undefined)?.project_path as + | string + | undefined; + const resolvedPath = projectPath ?? cwd; + + const link = await readProjectLink(resolvedPath).catch(() => null); + if (!link || link.deploy_mode !== "managed") return; + + await uploadSessionTranscript({ + projectId: link.project_id, + deploymentId, + transcriptPath, + }); + } catch { + // Never surface errors from hooks + } +} + +async function readStdin(): Promise { + try { + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) { + chunks.push(chunk as Buffer); + } + return Buffer.concat(chunks).toString("utf8").trim() || null; + } catch { + return null; + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index baaa6e3..29ace31 100755 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -547,6 +547,12 @@ try { await withTelemetry("skills", skills, { subcommand: args[0] })(args[0], args.slice(1)); break; } + case "_internal": { + // Internal commands used by Claude Code hooks — not shown in help + const { default: internal } = await import("./commands/internal.ts"); + await internal(args[0]); + break; + } default: { // No command provided - show interactive picker if TTY, else help if (!command) { diff --git a/apps/cli/src/lib/claude-hooks-installer.ts b/apps/cli/src/lib/claude-hooks-installer.ts index aa8cad1..a7e740e 100644 --- a/apps/cli/src/lib/claude-hooks-installer.ts +++ b/apps/cli/src/lib/claude-hooks-installer.ts @@ -2,11 +2,18 @@ import { existsSync } from "node:fs"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; -const HOOK_COMMAND = "jack mcp context 2>/dev/null || true"; +const MCP_CONTEXT_COMMAND = "jack mcp context 2>/dev/null || true"; +const SESSION_START_COMMAND = "jack _internal session-start 2>/dev/null || true"; +const POST_DEPLOY_COMMAND = "jack _internal post-deploy 2>/dev/null || true"; /** - * Install a Claude Code SessionStart hook to the project's .claude/settings.json. - * Only fires when Claude Code is opened in this project directory. + * Install Claude Code hooks to the project's .claude/settings.json: + * + * - SessionStart: runs `jack mcp context` (project context) + `jack _internal session-start` + * (exports CLAUDE_TRANSCRIPT_PATH via CLAUDE_ENV_FILE so `jack deploy` can read it) + * - PostToolUse(deploy_project): runs `jack _internal post-deploy` to upload the session + * transcript to the control plane after an MCP-triggered deploy + * * Non-destructive: preserves existing hooks and deduplicates. */ export async function installClaudeCodeHooks(projectPath: string): Promise { @@ -29,22 +36,49 @@ export async function installClaudeCodeHooks(projectPath: string): Promise) ?? {}; + + // --- SessionStart hooks --- const sessionStart = (hooks.SessionStart as Array>) ?? []; - // Check if jack hook is already installed - for (const entry of sessionStart) { + const hasMcpContext = sessionStart.some((entry) => { const entryHooks = entry.hooks as Array> | undefined; - if (entryHooks?.some((h) => h.command?.includes("jack mcp context"))) { - return true; - } + return entryHooks?.some((h) => h.command?.includes("jack mcp context")); + }); + if (!hasMcpContext) { + sessionStart.push({ + matcher: "", + hooks: [{ type: "command", command: MCP_CONTEXT_COMMAND }], + }); } - sessionStart.push({ - matcher: "", - hooks: [{ type: "command", command: HOOK_COMMAND }], + const hasSessionStart = sessionStart.some((entry) => { + const entryHooks = entry.hooks as Array> | undefined; + return entryHooks?.some((h) => h.command?.includes("jack _internal session-start")); }); + if (!hasSessionStart) { + sessionStart.push({ + matcher: "", + hooks: [{ type: "command", command: SESSION_START_COMMAND }], + }); + } hooks.SessionStart = sessionStart; + + // --- PostToolUse hooks --- + const postToolUse = (hooks.PostToolUse as Array>) ?? []; + + const hasPostDeploy = postToolUse.some((entry) => { + const entryHooks = entry.hooks as Array> | undefined; + return entryHooks?.some((h) => h.command?.includes("jack _internal post-deploy")); + }); + if (!hasPostDeploy) { + postToolUse.push({ + matcher: "deploy_project", + hooks: [{ type: "command", command: POST_DEPLOY_COMMAND }], + }); + } + + hooks.PostToolUse = postToolUse; settings.hooks = hooks; await Bun.write(settingsPath, JSON.stringify(settings, null, 2)); diff --git a/apps/cli/src/lib/managed-deploy.ts b/apps/cli/src/lib/managed-deploy.ts index 36d83ed..3766cf8 100644 --- a/apps/cli/src/lib/managed-deploy.ts +++ b/apps/cli/src/lib/managed-deploy.ts @@ -18,6 +18,7 @@ import { formatSize } from "./format.ts"; import { createFileCountProgress, createUploadProgress } from "./progress.ts"; import type { OperationReporter } from "./project-operations.ts"; import { getProjectTags } from "./tags.ts"; +import { uploadSessionTranscript } from "./session-transcript.ts"; import { Events, track, trackActivationIfFirst } from "./telemetry.ts"; import { findWranglerConfig } from "./wrangler-config.ts"; import { packageForDeploy } from "./zip-packager.ts"; @@ -225,6 +226,17 @@ export async function deployCodeToManagedProject( // Source snapshot for forking is now derived from deployment artifacts on the control plane. // No separate upload needed — clone/fork reads from the latest live deployment's source.zip. + // Fire-and-forget: upload Claude Code session transcript if running under Claude Code. + // CLAUDE_TRANSCRIPT_PATH is exported by the SessionStart hook via CLAUDE_ENV_FILE. + const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH; + if (transcriptPath) { + void uploadSessionTranscript({ + projectId, + deploymentId: result.id, + transcriptPath, + }); + } + return { deploymentId: result.id, status: result.status, diff --git a/apps/cli/src/lib/session-transcript.ts b/apps/cli/src/lib/session-transcript.ts new file mode 100644 index 0000000..f8d29bd --- /dev/null +++ b/apps/cli/src/lib/session-transcript.ts @@ -0,0 +1,103 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { authFetch } from "./auth/index.ts"; +import { getControlApiUrl } from "./control-plane.ts"; + +// Keep last N messages and cap at MAX_BYTES to stay well under the 1MB server limit +const MAX_MESSAGES = 200; +const MAX_BYTES = 800_000; + +interface TranscriptLine { + type?: string; + [key: string]: unknown; +} + +/** + * Read a Claude Code session JSONL file, keep only user/assistant messages, + * truncate to MAX_MESSAGES from the end, and cap at MAX_BYTES. + */ +export async function readAndTruncateTranscript(transcriptPath: string): Promise { + if (!existsSync(transcriptPath)) { + return null; + } + + let raw: string; + try { + raw = await readFile(transcriptPath, "utf8"); + } catch { + return null; + } + + const lines = raw + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + + // Keep only conversation turns (skip summary/metadata lines) + const turns: string[] = []; + for (const line of lines) { + try { + const parsed = JSON.parse(line) as TranscriptLine; + if (parsed.type === "user" || parsed.type === "assistant") { + turns.push(line); + } + } catch { + // Skip malformed lines + } + } + + // Take the last MAX_MESSAGES turns + const recent = turns.slice(-MAX_MESSAGES); + + // Build output, trimming from the front if over MAX_BYTES + let output = recent.join("\n"); + if (new TextEncoder().encode(output).length > MAX_BYTES) { + // Drop oldest lines until under limit + let i = 0; + while (i < recent.length) { + const candidate = recent.slice(i).join("\n"); + if (new TextEncoder().encode(candidate).length <= MAX_BYTES) { + output = candidate; + break; + } + i++; + } + if (i >= recent.length) { + return null; // Nothing fits + } + } + + return output || null; +} + +/** + * Upload a session transcript to the control plane for a given deployment. + * Silent on failure — transcript upload is best-effort and must never block a deploy. + */ +export async function uploadSessionTranscript(opts: { + projectId: string; + deploymentId: string; + transcriptPath: string; +}): Promise { + const { projectId, deploymentId, transcriptPath } = opts; + + try { + const transcript = await readAndTruncateTranscript(transcriptPath); + if (!transcript) { + return; + } + + const url = `${getControlApiUrl()}/v1/projects/${projectId}/deployments/${deploymentId}/session-transcript`; + const response = await authFetch(url, { + method: "PUT", + headers: { "Content-Type": "application/x-ndjson" }, + body: transcript, + }); + + if (!response.ok) { + // Silent — best-effort + } + } catch { + // Never surface transcript errors to the user + } +} diff --git a/apps/control-plane/migrations/0031_add_session_transcript.sql b/apps/control-plane/migrations/0031_add_session_transcript.sql new file mode 100644 index 0000000..cff0688 --- /dev/null +++ b/apps/control-plane/migrations/0031_add_session_transcript.sql @@ -0,0 +1,4 @@ +-- Migration: Add session transcript support to deployments +-- Description: Store Claude Code session transcript captured at deploy time + +ALTER TABLE deployments ADD COLUMN has_session_transcript INTEGER DEFAULT 0; diff --git a/apps/control-plane/src/index.ts b/apps/control-plane/src/index.ts index 07bdf3b..21451eb 100644 --- a/apps/control-plane/src/index.ts +++ b/apps/control-plane/src/index.ts @@ -3709,6 +3709,63 @@ api.post("/projects/:projectId/deployments/upload", async (c) => { } }); +// Upload session transcript for a deployment +api.put("/projects/:projectId/deployments/:deploymentId/session-transcript", async (c) => { + const auth = c.get("auth"); + const projectId = c.req.param("projectId"); + const deploymentId = c.req.param("deploymentId"); + const provisioning = new ProvisioningService(c.env); + + const project = await provisioning.getProject(projectId); + if (!project) { + return c.json({ error: "not_found", message: "Project not found" }, 404); + } + + const membership = await c.env.DB.prepare( + "SELECT 1 FROM org_memberships WHERE org_id = ? AND user_id = ?", + ) + .bind(project.org_id, auth.userId) + .first(); + if (!membership) { + return c.json({ error: "not_found", message: "Project not found" }, 404); + } + + // Verify deployment belongs to this project + const deployment = await c.env.DB.prepare( + "SELECT id FROM deployments WHERE id = ? AND project_id = ?", + ) + .bind(deploymentId, projectId) + .first(); + if (!deployment) { + return c.json({ error: "not_found", message: "Deployment not found" }, 404); + } + + const body = await c.req.text(); + if (!body) { + return c.json({ error: "invalid_request", message: "Empty body" }, 400); + } + + // Limit to 1MB to avoid unbounded storage + const MAX_TRANSCRIPT_BYTES = 1_000_000; + const bodyBytes = new TextEncoder().encode(body).length; + if (bodyBytes > MAX_TRANSCRIPT_BYTES) { + return c.json({ error: "payload_too_large", message: "Transcript exceeds 1MB limit" }, 413); + } + + const r2Key = `projects/${projectId}/deployments/${deploymentId}/session-transcript.jsonl`; + await c.env.CODE_BUCKET.put(r2Key, body, { + httpMetadata: { contentType: "application/x-ndjson" }, + }); + + await c.env.DB.prepare( + "UPDATE deployments SET has_session_transcript = 1 WHERE id = ?", + ) + .bind(deploymentId) + .run(); + + return c.json({ ok: true }); +}); + // Secrets endpoints - never stores secrets in D1, passes directly to Cloudflare api.post("/projects/:projectId/secrets", async (c) => { const auth = c.get("auth"); From 7683f204c4cc7ca4b2566cff5bfcea9c2d9a61be Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 10:49:20 +0000 Subject: [PATCH 2/7] docs: add implementation plan for ask feature improvements https://claude.ai/code/session_01Naw4TaqKjHM1m2HFpxs23y --- PLAN.md | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..79c137e --- /dev/null +++ b/PLAN.md @@ -0,0 +1,290 @@ +# Plan: Ask Feature 10x + +## Overview + +Four improvements to the post-deployment `ask` feature: + +1. **LLM Synthesis** — replace deterministic `pickAnswer()` with Claude on the server side +2. **Session Transcript at Deploy** — automatically capture and upload the Claude Code session context when deploying +3. **Git/JJ Diff at Deploy** — capture actual VCS diff in the CLI and store it with the deployment +4. **Real AST Parsing** — replace regex symbol extraction with `acorn` + `acorn-typescript` + +--- + +## Feature 1: LLM Synthesis + +Replace the heuristic `pickAnswer()` in `ask-project.ts` with a real Claude API call. + +### What changes + +**`apps/control-plane/src/types.ts`** +- Add `ANTHROPIC_API_KEY?: string` to `Bindings` + +**`apps/control-plane/wrangler.toml`** +- Add comment: `# - ANTHROPIC_API_KEY # Anthropic API key for ask_project synthesis` + +**`apps/control-plane/package.json`** +- Add `@anthropic-ai/sdk` dependency + +**`apps/control-plane/src/ask-project.ts`** +- Add `synthesizeWithLLM(evidence, question, opts?)` function: + - Formats evidence as a structured markdown block for Claude + - Includes session transcript excerpt (if available) and VCS diff stat (if available) + - Calls `claude-haiku-4-5-20251001` via Anthropic SDK + - Returns `{ answer: string, root_cause?: string, suggested_fix?: string, confidence: "high" | "medium" | "low" }` + - Falls back to `pickAnswer()` if `ANTHROPIC_API_KEY` is not set or the call fails +- Update `answerProjectQuestion()` to call `synthesizeWithLLM()` instead of `pickAnswer()` +- Keep `pickAnswer()` as the fallback +- Extend API response shape to include `root_cause?`, `suggested_fix?`, `confidence` + +### Prompt structure for Claude + +``` +You are a debugging assistant for deployed Cloudflare Workers projects. +Given evidence collected about a project, answer the user's question concisely. + +Question: + +Evidence: + + +[If session transcript available]: +Context from deploy session (last 30 messages): + + +[If VCS diff available]: +Code changed in this deployment: + + +Respond with JSON: +{ + "answer": "...", + "root_cause": "..." | null, + "suggested_fix": "..." | null, + "confidence": "high" | "medium" | "low" +} +``` + +--- + +## Feature 2: Session Transcript at Deploy + +Automatically capture the Claude Code session transcript at deploy time and store it as a deployment artifact. The `ask` feature then uses it as context. + +### Mechanism + +Two paths, both automatic (no agent opt-in): + +**Path A — CLI deploy** (`jack deploy` run as a Bash tool by Claude Code): +- Extend the existing `SessionStart` hook in `installClaudeCodeHooks()` to also export `CLAUDE_TRANSCRIPT_PATH` via `CLAUDE_ENV_FILE` +- After a successful deploy, `jack deploy` checks for `CLAUDE_TRANSCRIPT_PATH` env var and uploads the transcript + +**Path B — MCP deploy** (`deploy_project` MCP tool called by Claude Code): +- Add a `PostToolUse` hook in `installClaudeCodeHooks()` that fires when `tool_name === "deploy_project"` +- Hook reads `transcript_path` and `tool_response` from stdin +- Extracts `deploymentId` and `projectId` from tool response JSON +- Calls `jack _internal upload-session-transcript --project --deployment --transcript-path ` + +### What changes + +**`apps/cli/src/lib/claude-hooks-installer.ts`** +- Extend `installClaudeCodeHooks()` to also install: + - Updated `SessionStart` hook command that exports `CLAUDE_TRANSCRIPT_PATH` and `CLAUDE_SESSION_ID` via `CLAUDE_ENV_FILE` + - New `PostToolUse` hook entry with `matcher: "deploy_project"` that runs `jack _internal upload-session-transcript ...` +- Keep existing hook deduplication logic + +**`apps/cli/src/lib/session-transcript.ts`** (new file) +- `readAndTruncateTranscript(path: string): Promise`: + - Reads JSONL file from `transcript_path` + - Keeps only `type: "user" | "assistant"` lines + - Truncates to last 200 messages or 100KB, whichever is smaller + - Returns truncated JSONL string +- `uploadSessionTranscript(opts: { projectId, deploymentId, transcriptPath, authToken, baseUrl })`: + - Calls `readAndTruncateTranscript()` + - PUTs to `/v1/projects/:projectId/deployments/:deploymentId/session-transcript` + +**`apps/cli/src/commands/internal.ts`** (new command or extend existing) +- Add `jack _internal upload-session-transcript` subcommand +- Reads `--project-id`, `--deployment-id`, `--transcript-path` args +- Calls `uploadSessionTranscript()` using saved auth token +- Silent: exits 0 even on failure (hook errors must not block the user) + +**`apps/cli/src/lib/managed-deploy.ts`** (or wherever deploy completes) +- After successful deploy: check `process.env.CLAUDE_TRANSCRIPT_PATH` +- If set, call `uploadSessionTranscript()` asynchronously (fire-and-forget, silent) + +**`apps/control-plane/src/index.ts`** +- New endpoint: `PUT /v1/projects/:projectId/deployments/:deploymentId/session-transcript` + - Auth: same JWT org membership check + - Body: raw text (JSONL) + - Stores to R2: `projects/{projectId}/deployments/{deploymentId}/session-transcript.jsonl` + - Updates `deployments SET has_session_transcript = 1 WHERE id = ?` + +**`apps/control-plane/migrations/0031_add_deployment_context.sql`** (new) +```sql +ALTER TABLE deployments ADD COLUMN has_session_transcript INTEGER DEFAULT 0; +ALTER TABLE deployments ADD COLUMN vcs_type TEXT; +ALTER TABLE deployments ADD COLUMN vcs_sha TEXT; +ALTER TABLE deployments ADD COLUMN vcs_message TEXT; +ALTER TABLE deployments ADD COLUMN vcs_diff_stat TEXT; +ALTER TABLE deployments ADD COLUMN vcs_diff TEXT; +``` + +**`apps/control-plane/src/ask-project.ts`** +- Before calling `synthesizeWithLLM()`: if `deployment.has_session_transcript`, fetch from R2 and pass last 30 messages as context +- Add `"session_transcript"` evidence type — summary says e.g. "Deploy session had 47 turns. Agent was working on: ..." + +--- + +## Feature 3: Git/JJ Diff at Deploy + +Capture the actual VCS diff in the CLI at deploy time and store it with the deployment. + +### What changes + +**`apps/cli/src/lib/vcs.ts`** (new file) +```typescript +interface VcsDiff { + vcs: "git" | "jj"; + sha: string; + message: string; // commit/change description + diff_stat: string; // max 2KB + diff: string; // max 15KB, truncated with notice if over +} + +async function captureVcsDiff(projectDir: string): Promise +``` + +- Detects VCS by checking for `.git/` (git) or `.jj/` (jj) in `projectDir` and parents +- Git commands: + - `git -C rev-parse HEAD` → sha + - `git -C log -1 --pretty=%s` → message + - `git -C diff HEAD~1 --stat` → diff_stat (if no parent commit, diff against empty tree) + - `git -C diff HEAD~1 --unified=3` → diff (truncated to 15KB) +- JJ commands: + - `jj --no-pager log -r @ --no-graph --template 'commit_id.short()'` → sha + - `jj --no-pager log -r @ --no-graph --template 'description.first_line()'` → message + - `jj --no-pager diff --stat` → diff_stat + - `jj --no-pager diff` → diff (truncated to 15KB) +- Silent fallback: catches all errors, returns `null` if anything fails or VCS not found + +**`apps/cli/src/lib/managed-deploy.ts`** (or deploy request builder) +- Call `captureVcsDiff(projectDir)` before deploy request +- Include `vcs` field in request body if non-null + +**`apps/control-plane/src/index.ts`** (deploy endpoint) +- Accept optional `vcs?: VcsDiff` in deploy request body +- Pass through to `createCodeDeployment()` + +**`apps/control-plane/src/deployment-service.ts`** +- `createCodeDeployment()` accepts optional `vcs` field +- Stores `vcs_type`, `vcs_sha`, `vcs_message`, `vcs_diff_stat`, `vcs_diff` on deployment record + +**`apps/control-plane/src/ask-project.ts`** +- For "what changed" questions: pull `vcs_diff_stat` + `vcs_sha` + `vcs_message` from deployment +- Add `"vcs_diff"` evidence type with summary like "Deployed from commit abc1234: 'fix auth middleware'. Changed 3 files, +47 -12 lines." +- Pass `vcs_diff` to `synthesizeWithLLM()` for full diff context + +--- + +## Feature 4: Real AST Parsing + +Replace regex-based symbol extraction with a proper parser to get accurate symbols, imports, and a lightweight call graph. + +### Parser choice: `acorn` + `acorn-walk` + `acorn-typescript` + +- Pure JS, ~300KB total — no Wasm, works in Cloudflare Workers +- Handles JS, JSX, TS, TSX (TypeScript stripped before parse via lightweight type stripping) +- Acorn is battle-tested and used by many JS tools + +Alternative considered: `tree-sitter` Wasm (more accurate but 2-5MB Wasm bundle per language, cold start cost). Skip for now. + +### What changes + +**`apps/control-plane/package.json`** +- Add `acorn`, `acorn-walk`, `acorn-typescript` + +**`apps/control-plane/src/ask-code-index.ts`** +- Replace `jsTsAdapter` regex implementation with AST-based extraction +- New/improved symbol extraction: + +| Kind | Before (regex) | After (AST) | +|------|---------------|-------------| +| `route` | basic `app.get("/x")` pattern | + object router `{ GET: handler }`, `pathname === "/x"` chained routes | +| `function` | name only | name + param count + async/generator in signature | +| `class` | name only | name + method names listed in signature | +| `export` | any `export` keyword | distinguishes named/default/re-export | +| `env_binding` | `env.UPPER_SNAKE` | same, but accurate (no false positives in strings/comments) | +| `sql_ref` | SQL keyword match | same, more accurate (avoids matches in comments) | +| `import` | not extracted | **new**: `from` module path + imported names | +| `interface` | not extracted | **new**: TypeScript interface names | +| `type_alias` | not extracted | **new**: TypeScript `type X = ...` | + +- New `meta` column on symbol rows (stored as JSON string) holds: + - For `function`: `{ params: string[], async: bool, callees: string[] }` — lightweight call graph + - For `import`: `{ from: string, names: string[] }` + - For `class`: `{ methods: string[] }` + +**`apps/control-plane/migrations/0031_add_deployment_context.sql`** (same migration as above) +```sql +ALTER TABLE ask_code_symbols_latest ADD COLUMN meta TEXT; +ALTER TABLE ask_code_index_runs ADD COLUMN parser_version TEXT; +``` + +- Bump `PARSER_VERSION` constant in `ask-code-index.ts` to trigger re-index on next deploy + +--- + +## Migration summary + +One new migration file: `0031_add_deployment_context.sql` + +```sql +-- Deployment context columns +ALTER TABLE deployments ADD COLUMN has_session_transcript INTEGER DEFAULT 0; +ALTER TABLE deployments ADD COLUMN vcs_type TEXT; +ALTER TABLE deployments ADD COLUMN vcs_sha TEXT; +ALTER TABLE deployments ADD COLUMN vcs_message TEXT; +ALTER TABLE deployments ADD COLUMN vcs_diff_stat TEXT; +ALTER TABLE deployments ADD COLUMN vcs_diff TEXT; + +-- AST parser meta column +ALTER TABLE ask_code_symbols_latest ADD COLUMN meta TEXT; +``` + +--- + +## New files + +| File | Purpose | +|------|---------| +| `apps/cli/src/lib/vcs.ts` | Git/JJ diff capture | +| `apps/cli/src/lib/session-transcript.ts` | Transcript read, truncate, upload | +| `apps/control-plane/migrations/0031_add_deployment_context.sql` | Schema additions | + +--- + +## Modified files + +| File | Change | +|------|--------| +| `apps/control-plane/src/types.ts` | Add `ANTHROPIC_API_KEY?` to Bindings | +| `apps/control-plane/wrangler.toml` | Add secret comment | +| `apps/control-plane/package.json` | Add `@anthropic-ai/sdk`, `acorn`, `acorn-walk`, `acorn-typescript` | +| `apps/control-plane/src/ask-project.ts` | Add `synthesizeWithLLM()`, load transcript + VCS diff context | +| `apps/control-plane/src/ask-code-index.ts` | Replace regex with AST parser | +| `apps/control-plane/src/index.ts` | New `PUT .../session-transcript` endpoint; accept `vcs` in deploy | +| `apps/control-plane/src/deployment-service.ts` | Store VCS fields on deployment | +| `apps/cli/src/lib/claude-hooks-installer.ts` | Add SessionStart env export + PostToolUse hook | +| `apps/cli/src/lib/managed-deploy.ts` | Capture VCS diff + upload transcript after deploy | +| `apps/cli/src/commands/` | Add `jack _internal upload-session-transcript` | + +--- + +## Open questions (no blockers) + +1. **Codex CLI**: no hooks API exists yet (open issue #2765). Transcript capture for Codex is deferred — the `vcs` diff path will still work for Codex users since it's CLI-side. + +2. **Model for LLM synthesis**: plan uses `claude-haiku-4-5-20251001`. If response quality is insufficient, swap to `claude-sonnet-4-5-20250929` — just a one-line constant change. + +3. **PostToolUse hook `tool_response` parsing**: the `deploy_project` MCP tool returns `{ deploymentId, projectId, ... }` wrapped in `formatSuccessResponse`. The hook script will parse the text content JSON. If the shape changes, the hook silently fails (no user impact — transcript upload is always best-effort). From fb79b8362850f5c7b382c4d98230209e3d3fa07b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 11:00:20 +0000 Subject: [PATCH 3/7] feat(ask-project): LLM synthesis + session transcript evidence Replaces heuristic pickAnswer() with Claude (haiku) synthesis. Evidence is passed as structured context; session transcript excerpt from the deploy is included when available. Falls back to pickAnswer() if ANTHROPIC_API_KEY is not set or the API call fails. New pieces: - synthesizeAnswer(): calls claude-haiku-4-5-20251001, returns answer + root_cause + suggested_fix + confidence; returns null when key absent - fetchSessionTranscript(): reads session-transcript.jsonl from R2, parses Claude Code JSONL format (type/message/content), returns last 30 turns - AskProjectResponse extended with root_cause?, suggested_fix?, confidence? - AskProjectEvidence.type union gains "session_transcript" - ANTHROPIC_API_KEY? added to Bindings; @anthropic-ai/sdk added to deps - scripts/test-ask-e2e.sh: 7-step E2E test suite for transcript + ask endpoints - scripts/curl-ask-examples.sh: quick curl reference Run `wrangler secret put ANTHROPIC_API_KEY --cwd apps/control-plane` to enable LLM synthesis; feature degrades gracefully without it. https://claude.ai/code/session_01Naw4TaqKjHM1m2HFpxs23y --- apps/control-plane/package.json | 1 + apps/control-plane/src/ask-project.ts | 145 +++++++++++++++++++++- apps/control-plane/src/types.ts | 2 + apps/control-plane/wrangler.toml | 1 + bun.lock | 49 +++++++- scripts/curl-ask-examples.sh | 21 ++++ scripts/test-ask-e2e.sh | 168 ++++++++++++++++++++++++++ 7 files changed, 382 insertions(+), 5 deletions(-) create mode 100755 scripts/curl-ask-examples.sh create mode 100755 scripts/test-ask-e2e.sh diff --git a/apps/control-plane/package.json b/apps/control-plane/package.json index 96135af..450c53c 100644 --- a/apps/control-plane/package.json +++ b/apps/control-plane/package.json @@ -14,6 +14,7 @@ "cronstrue": "^2.50.0", "fflate": "^0.8.2", "hono": "^4.6.0", + "@anthropic-ai/sdk": "^0.36.3", "stripe": "^17.5.0" }, "devDependencies": { diff --git a/apps/control-plane/src/ask-project.ts b/apps/control-plane/src/ask-project.ts index 4367b02..73b0bf0 100644 --- a/apps/control-plane/src/ask-project.ts +++ b/apps/control-plane/src/ask-project.ts @@ -8,6 +8,7 @@ import { CloudflareClient } from "./cloudflare-api"; import { DeploymentService } from "./deployment-service"; import { ProvisioningService } from "./provisioning"; import type { Bindings, Deployment } from "./types"; +import Anthropic from "@anthropic-ai/sdk"; export interface AskProjectHints { endpoint?: string; @@ -30,7 +31,8 @@ export interface AskProjectEvidence { | "env_snapshot" | "code_chunk" | "code_symbol" - | "index_status"; + | "index_status" + | "session_transcript"; source: string; summary: string; timestamp: string; @@ -41,6 +43,9 @@ export interface AskProjectEvidence { export interface AskProjectResponse { answer: string; evidence: AskProjectEvidence[]; + root_cause?: string; + suggested_fix?: string; + confidence?: "high" | "medium" | "low"; } interface AskProjectInput { @@ -220,6 +225,114 @@ function summarizeDeploymentMessage(deployment: Deployment): string { return `Deployment ${deployment.id} (${deployment.status}) at ${createdAt} has no deploy message`; } +async function fetchSessionTranscript( + env: Bindings, + projectId: string, + deploymentId: string, +): Promise { + try { + const r2Key = `projects/${projectId}/deployments/${deploymentId}/session-transcript.jsonl`; + const obj = await env.CODE_BUCKET.get(r2Key); + if (!obj) return null; + + const raw = await obj.text(); + const lines = raw.split("\n").filter((l) => l.trim().length > 0); + + // Parse Claude Code JSONL format: { type: "user"|"assistant", message: { content: string | ContentBlock[] } } + const turns: string[] = []; + for (const line of lines) { + try { + const parsed = JSON.parse(line) as Record; + if (parsed.type !== "user" && parsed.type !== "assistant") continue; + const msg = parsed.message as Record | undefined; + if (!msg) continue; + const content = msg.content; + let text = ""; + if (typeof content === "string") { + text = content; + } else if (Array.isArray(content)) { + // Extract text blocks only (skip tool_use, tool_result, etc.) + text = content + .filter((b): b is { type: "text"; text: string } => + typeof b === "object" && b !== null && (b as Record).type === "text", + ) + .map((b) => b.text) + .join(" "); + } + if (text.trim()) { + turns.push(`[${parsed.type as string}]: ${redactText(text).slice(0, 500)}`); + } + } catch { + // skip malformed lines + } + } + + const lastTurns = turns.slice(-30); + return lastTurns.length > 0 ? lastTurns.join("\n\n") : null; + } catch { + return null; + } +} + +// Returns null when ANTHROPIC_API_KEY is absent; throws on API failure (caller catches and falls back) +async function synthesizeAnswer( + env: Bindings, + question: string, + evidence: AskProjectEvidence[], + sessionExcerpt: string | null, +): Promise | null> { + if (!env.ANTHROPIC_API_KEY) return null; + + const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); + + const evidenceBlock = evidence + .map((e) => `- [${e.id}] type=${e.type} source=${e.source} relation=${e.relation}: ${e.summary}`) + .join("\n"); + + const transcriptBlock = sessionExcerpt + ? `\n\n## Deploy Session Context (last 30 turns)\n\n${sessionExcerpt}` + : ""; + + const prompt = `You are a debugging assistant for a Cloudflare Workers deployment platform. Answer the user's question about their deployed project based only on the collected evidence below. + +## Question +${question} + +## Evidence +${evidenceBlock}${transcriptBlock} + +Respond with only valid JSON (no markdown fences): +{ + "answer": "Concise direct answer (2-4 sentences)", + "root_cause": "Specific root cause or null", + "suggested_fix": "Concrete fix or null", + "confidence": "high" | "medium" | "low" +}`; + + const message = await client.messages.create({ + model: "claude-haiku-4-5-20251001", + max_tokens: 512, + messages: [{ role: "user", content: prompt }], + }); + + const text = message.content[0]?.type === "text" ? message.content[0].text : ""; + + try { + const parsed = JSON.parse(text) as Record; + return { + answer: typeof parsed.answer === "string" ? parsed.answer : text.slice(0, 800), + root_cause: typeof parsed.root_cause === "string" ? parsed.root_cause : undefined, + suggested_fix: typeof parsed.suggested_fix === "string" ? parsed.suggested_fix : undefined, + confidence: + parsed.confidence === "high" || parsed.confidence === "medium" || parsed.confidence === "low" + ? parsed.confidence + : "low", + }; + } catch { + return { answer: text.slice(0, 800), confidence: "low" }; + } +} + function pickAnswer(params: { question: string; latestDeployment: Deployment | null; @@ -566,8 +679,23 @@ export async function answerProjectQuestion(input: AskProjectInput): Promise).has_session_transcript === 1) { + sessionExcerpt = await fetchSessionTranscript(env, project.id, targetDeployment.id); + if (sessionExcerpt) { + evidence.add( + "session_transcript", + "deploy_session", + `Deploy session context: ${sessionExcerpt.slice(0, 200)}`, + "supports", + { deployment_id: targetDeployment.id }, + ); + } + } + const hasInsufficientEvidence = evidence.values().some((e) => e.relation === "gap"); - const answer = pickAnswer({ + const pickAnswerParams = { question, latestDeployment, endpointPath, @@ -577,10 +705,19 @@ export async function answerProjectQuestion(input: AskProjectInput): Promise { + console.error("ask_project: LLM synthesis failed, falling back to pickAnswer:", err); + return null; + }, + ); + + const synthesis = synthesized ?? { answer: pickAnswer(pickAnswerParams) }; return { - answer, + ...synthesis, evidence: evidence.values(), }; } diff --git a/apps/control-plane/src/types.ts b/apps/control-plane/src/types.ts index 9631ca1..fd296a2 100644 --- a/apps/control-plane/src/types.ts +++ b/apps/control-plane/src/types.ts @@ -30,6 +30,7 @@ export type Bindings = { // Optional PostHog server-side capture key for control-plane events POSTHOG_API_KEY?: string; POSTHOG_HOST?: string; + ANTHROPIC_API_KEY?: string; }; // Project status enum @@ -114,6 +115,7 @@ export interface Deployment { worker_version_id: string | null; error_message: string | null; message: string | null; + has_session_transcript: number; // SQLite boolean (0 or 1) created_at: string; updated_at: string; } diff --git a/apps/control-plane/wrangler.toml b/apps/control-plane/wrangler.toml index 960de56..0147e96 100644 --- a/apps/control-plane/wrangler.toml +++ b/apps/control-plane/wrangler.toml @@ -81,3 +81,4 @@ dataset = "jack_control_usage" # - DAIMO_WEBHOOK_SECRET # Daimo webhook basic auth token # - DAIMO_RECEIVER_ADDRESS # Wallet address to receive USDC on Base # - POSTHOG_API_KEY # Optional: server-side product analytics capture +# - ANTHROPIC_API_KEY # Optional: Anthropic API key for LLM answer synthesis in ask_project diff --git a/bun.lock b/bun.lock index 8b7d127..30364c7 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,7 @@ }, "apps/cli": { "name": "@getjack/jack", - "version": "0.1.34", + "version": "0.1.36", "bin": { "jack": "./src/index.ts", }, @@ -62,6 +62,7 @@ "apps/control-plane": { "name": "@getjack/control-plane", "dependencies": { + "@anthropic-ai/sdk": "^0.36.3", "@getjack/auth": "workspace:*", "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", @@ -137,6 +138,8 @@ "packages": { "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.36.3", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-+c0mMLxL/17yFZ4P5+U6bTWiCSFZUKJddrv01ud2aFBWnTPLdRncYV76D3q1tqfnL7aCnhRtykFnoCFzvr4U3Q=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -713,6 +716,8 @@ "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/readdir-glob": ["@types/readdir-glob@1.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg=="], @@ -751,6 +756,8 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -769,6 +776,8 @@ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], @@ -847,6 +856,8 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], @@ -981,6 +992,8 @@ "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -1027,6 +1040,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -1099,8 +1114,14 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -1137,6 +1158,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hast-util-classnames": ["hast-util-classnames@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-tI3JjoGDEBVorMAWK4jNRsfLMYmih1BUOG3VV36pH36njs1IEl7xkNrVTD2mD2yYHmQCa5R/fj61a8IAF4bRaQ=="], @@ -1183,6 +1206,8 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1455,6 +1480,10 @@ "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1715,6 +1744,8 @@ "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1807,6 +1838,12 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "workerd": ["workerd@1.20251217.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251217.0", "@cloudflare/workerd-darwin-arm64": "1.20251217.0", "@cloudflare/workerd-linux-64": "1.20251217.0", "@cloudflare/workerd-linux-arm64": "1.20251217.0", "@cloudflare/workerd-windows-64": "1.20251217.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA=="], @@ -1843,6 +1880,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -1921,6 +1960,8 @@ "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], @@ -1937,6 +1978,8 @@ "hast-util-to-jsx-runtime/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "humanize-ms/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -1983,6 +2026,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2065,6 +2110,8 @@ "finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "hast-util-from-dom/hastscript/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], diff --git a/scripts/curl-ask-examples.sh b/scripts/curl-ask-examples.sh new file mode 100755 index 0000000..fe52313 --- /dev/null +++ b/scripts/curl-ask-examples.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Quick curl examples for ask feature endpoints +# Set: JACK_API_TOKEN, PROJECT_ID, DEPLOYMENT_ID, CONTROL_URL + +CONTROL_URL="${CONTROL_URL:-https://control.getjack.org}" + +# Upload a session transcript +curl -X PUT \ + -H "Authorization: Bearer $JACK_API_TOKEN" \ + -H "Content-Type: application/x-ndjson" \ + --data-binary '{"type":"user","message":{"role":"user","content":"why is /api broken?"},"sessionId":"s1","timestamp":"2026-01-01T00:00:00.000Z"}' \ + "$CONTROL_URL/v1/projects/$PROJECT_ID/deployments/$DEPLOYMENT_ID/session-transcript" + +echo "" + +# Ask a question +curl -X POST \ + -H "Authorization: Bearer $JACK_API_TOKEN" \ + -H "Content-Type: application/json" \ + --data '{"question":"Why did my endpoint break?","hints":{"endpoint":"/api/hello"}}' \ + "$CONTROL_URL/v1/projects/$PROJECT_ID/ask" | jq . diff --git a/scripts/test-ask-e2e.sh b/scripts/test-ask-e2e.sh new file mode 100755 index 0000000..b735b49 --- /dev/null +++ b/scripts/test-ask-e2e.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# E2E test: session transcript upload + ask endpoint +# +# Usage: +# JACK_API_TOKEN=jkt_xxx PROJECT_ID=proj_xxx DEPLOYMENT_ID=dep_xxx bash scripts/test-ask-e2e.sh +# +# Optional: +# CONTROL_URL=https://control.getjack.org (default) +# +# How to get IDs: +# PROJECT_ID: jack info --json | jq -r '.project.id' +# DEPLOYMENT_ID: jack deploys --json | jq -r '.[0].id' + +set -euo pipefail + +CONTROL_URL="${CONTROL_URL:-https://control.getjack.org}" +PASS=0 +FAIL=0 + +# ── helpers ────────────────────────────────────────────────────────────────── + +check() { + local label="$1" + local result="$2" + if [[ "$result" == "true" ]]; then + echo " PASS $label" + PASS=$((PASS + 1)) + else + echo " FAIL $label" + FAIL=$((FAIL + 1)) + fi +} + +require_var() { + if [[ -z "${!1:-}" ]]; then + echo "ERROR: $1 is required. See usage at the top of this script." + exit 1 + fi +} + +# ── preflight ───────────────────────────────────────────────────────────────── + +require_var JACK_API_TOKEN +require_var PROJECT_ID +require_var DEPLOYMENT_ID + +echo "" +echo "Control URL : $CONTROL_URL" +echo "Project : $PROJECT_ID" +echo "Deployment : $DEPLOYMENT_ID" +echo "" + +# ── test 1: upload session transcript ──────────────────────────────────────── + +echo "[ 1 ] Upload session transcript" + +# Minimal valid Claude Code JSONL (two turns) +TRANSCRIPT_BODY='{"type":"user","message":{"role":"user","content":"Why did the /api/hello endpoint break after deploy?"},"sessionId":"test","timestamp":"2026-01-01T00:00:00.000Z"} +{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"The error is likely a missing D1 table. Check your schema migrations."}]},"sessionId":"test","timestamp":"2026-01-01T00:00:01.000Z"}' + +UPLOAD_RESP=$(curl -s -w "\n%{http_code}" \ + -X PUT \ + -H "Authorization: Bearer ${JACK_API_TOKEN}" \ + -H "Content-Type: application/x-ndjson" \ + --data-binary "${TRANSCRIPT_BODY}" \ + "${CONTROL_URL}/v1/projects/${PROJECT_ID}/deployments/${DEPLOYMENT_ID}/session-transcript") + +UPLOAD_STATUS=$(echo "$UPLOAD_RESP" | tail -1) +UPLOAD_BODY=$(echo "$UPLOAD_RESP" | head -1) + +check "PUT /session-transcript returns 200" "$([[ "$UPLOAD_STATUS" == "200" ]] && echo true || echo false)" +check "Response body contains ok:true" "$(echo "$UPLOAD_BODY" | grep -qc '"ok":true' && echo true || echo false)" + +echo " Response: $UPLOAD_BODY (HTTP $UPLOAD_STATUS)" + +# ── test 2: upload idempotency ──────────────────────────────────────────────── + +echo "" +echo "[ 2 ] Upload again (idempotency)" + +UPLOAD2_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -X PUT \ + -H "Authorization: Bearer ${JACK_API_TOKEN}" \ + -H "Content-Type: application/x-ndjson" \ + --data-binary "${TRANSCRIPT_BODY}" \ + "${CONTROL_URL}/v1/projects/${PROJECT_ID}/deployments/${DEPLOYMENT_ID}/session-transcript") + +check "Second upload also returns 200" "$([[ "$UPLOAD2_STATUS" == "200" ]] && echo true || echo false)" + +# ── test 3: payload too large ───────────────────────────────────────────────── + +echo "" +echo "[ 3 ] Reject oversized payload (>1MB)" + +# Generate ~1.1MB of text +BIG_BODY=$(python3 -c "print('x' * 1_100_000)" 2>/dev/null || dd if=/dev/urandom bs=1100000 count=1 2>/dev/null | base64) + +OVERSIZE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -X PUT \ + -H "Authorization: Bearer ${JACK_API_TOKEN}" \ + -H "Content-Type: application/x-ndjson" \ + --data-binary "${BIG_BODY}" \ + "${CONTROL_URL}/v1/projects/${PROJECT_ID}/deployments/${DEPLOYMENT_ID}/session-transcript") + +check "Oversized payload returns 413" "$([[ "$OVERSIZE_STATUS" == "413" ]] && echo true || echo false)" + +# ── test 4: ask endpoint returns answer + evidence ─────────────────────────── + +echo "" +echo "[ 4 ] Ask endpoint — basic response shape" + +ASK_RESP=$(curl -s \ + -X POST \ + -H "Authorization: Bearer ${JACK_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"question\":\"Why did the api endpoint break?\",\"hints\":{\"deployment_id\":\"${DEPLOYMENT_ID}\"}}" \ + "${CONTROL_URL}/v1/projects/${PROJECT_ID}/ask") + +echo " Response: $(echo "$ASK_RESP" | head -c 300)..." + +check "ask response has 'answer' field" "$(echo "$ASK_RESP" | grep -qc '"answer"' && echo true || echo false)" +check "ask response has 'evidence' field" "$(echo "$ASK_RESP" | grep -qc '"evidence"' && echo true || echo false)" + +# ── test 5: session_transcript evidence is present ─────────────────────────── + +echo "" +echo "[ 5 ] Ask evidence includes session_transcript entry" + +check "Evidence contains session_transcript type" "$(echo "$ASK_RESP" | grep -qc '"session_transcript"' && echo true || echo false)" + +# ── test 6: LLM synthesis fields (present when ANTHROPIC_API_KEY is set) ───── + +echo "" +echo "[ 6 ] LLM synthesis fields (optional — only present when ANTHROPIC_API_KEY is configured)" + +HAS_CONFIDENCE=$(echo "$ASK_RESP" | grep -c '"confidence"' || true) +if [[ "$HAS_CONFIDENCE" -gt 0 ]]; then + check "confidence field present (LLM synthesis active)" "true" + check "confidence is valid value" "$(echo "$ASK_RESP" | grep -qE '"confidence":\s*"(high|medium|low)"' && echo true || echo false)" + check "root_cause field present" "$(echo "$ASK_RESP" | grep -qc '"root_cause"' && echo true || echo false)" +else + echo " SKIP confidence/root_cause fields — ANTHROPIC_API_KEY not set, using fallback pickAnswer" +fi + +# ── test 7: unauthorized request is rejected ───────────────────────────────── + +echo "" +echo "[ 7 ] Unauthorized upload is rejected" + +UNAUTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -X PUT \ + -H "Authorization: Bearer bad_token" \ + -H "Content-Type: application/x-ndjson" \ + --data-binary "${TRANSCRIPT_BODY}" \ + "${CONTROL_URL}/v1/projects/${PROJECT_ID}/deployments/${DEPLOYMENT_ID}/session-transcript") + +check "Unauthorized request returns 401 or 403" "$([[ "$UNAUTH_STATUS" == "401" || "$UNAUTH_STATUS" == "403" ]] && echo true || echo false)" + +# ── summary ─────────────────────────────────────────────────────────────────── + +echo "" +echo "──────────────────────────────" +echo " $PASS passed / $FAIL failed" +echo "──────────────────────────────" + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi From fd164d80a7e07845e796b4b7c0cd1d9896ad1d88 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 18 Feb 2026 13:19:41 +0100 Subject: [PATCH 4/7] feat(ask-project): switch to Workers AI GLM-4.7-Flash, add session digest - Replace Anthropic SDK with Cloudflare Workers AI binding for LLM calls - Use @cf/glm4/glm-4.7-flash for both digest generation and ask synthesis - Generate prose session digest at transcript upload time via waitUntil - Store digest in session_digest column on deployments (migration 0032) - Remove @anthropic-ai/sdk dependency and ANTHROPIC_API_KEY references - Update @cloudflare/workers-types to 4.20260218.0 --- .../migrations/0032_add_session_digest.sql | 4 + apps/control-plane/package.json | 3 +- apps/control-plane/src/ask-project.ts | 194 +++++++++++------- apps/control-plane/src/index.ts | 28 ++- apps/control-plane/src/types.ts | 4 +- apps/control-plane/wrangler.toml | 4 +- bun.lock | 63 ++---- 7 files changed, 161 insertions(+), 139 deletions(-) create mode 100644 apps/control-plane/migrations/0032_add_session_digest.sql diff --git a/apps/control-plane/migrations/0032_add_session_digest.sql b/apps/control-plane/migrations/0032_add_session_digest.sql new file mode 100644 index 0000000..9cfceff --- /dev/null +++ b/apps/control-plane/migrations/0032_add_session_digest.sql @@ -0,0 +1,4 @@ +-- Migration: Add session digest column to deployments +-- Description: Store a short LLM-generated summary of the deploy session transcript + +ALTER TABLE deployments ADD COLUMN session_digest TEXT; diff --git a/apps/control-plane/package.json b/apps/control-plane/package.json index 450c53c..83f6adf 100644 --- a/apps/control-plane/package.json +++ b/apps/control-plane/package.json @@ -14,11 +14,10 @@ "cronstrue": "^2.50.0", "fflate": "^0.8.2", "hono": "^4.6.0", - "@anthropic-ai/sdk": "^0.36.3", "stripe": "^17.5.0" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20241205.0", + "@cloudflare/workers-types": "^4.20260218.0", "typescript": "^5.0.0" } } diff --git a/apps/control-plane/src/ask-project.ts b/apps/control-plane/src/ask-project.ts index 73b0bf0..abb15b4 100644 --- a/apps/control-plane/src/ask-project.ts +++ b/apps/control-plane/src/ask-project.ts @@ -8,8 +8,6 @@ import { CloudflareClient } from "./cloudflare-api"; import { DeploymentService } from "./deployment-service"; import { ProvisioningService } from "./provisioning"; import type { Bindings, Deployment } from "./types"; -import Anthropic from "@anthropic-ai/sdk"; - export interface AskProjectHints { endpoint?: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; @@ -225,68 +223,102 @@ function summarizeDeploymentMessage(deployment: Deployment): string { return `Deployment ${deployment.id} (${deployment.status}) at ${createdAt} has no deploy message`; } -async function fetchSessionTranscript( - env: Bindings, - projectId: string, - deploymentId: string, -): Promise { - try { - const r2Key = `projects/${projectId}/deployments/${deploymentId}/session-transcript.jsonl`; - const obj = await env.CODE_BUCKET.get(r2Key); - if (!obj) return null; - - const raw = await obj.text(); - const lines = raw.split("\n").filter((l) => l.trim().length > 0); - - // Parse Claude Code JSONL format: { type: "user"|"assistant", message: { content: string | ContentBlock[] } } - const turns: string[] = []; - for (const line of lines) { - try { - const parsed = JSON.parse(line) as Record; - if (parsed.type !== "user" && parsed.type !== "assistant") continue; - const msg = parsed.message as Record | undefined; - if (!msg) continue; - const content = msg.content; - let text = ""; - if (typeof content === "string") { - text = content; - } else if (Array.isArray(content)) { - // Extract text blocks only (skip tool_use, tool_result, etc.) - text = content - .filter((b): b is { type: "text"; text: string } => +/** + * Parse raw JSONL transcript into redacted conversation turns. + * Shared between the digest generator and the fallback read path. + */ +export function parseTranscriptTurns(raw: string): string[] { + const lines = raw.split("\n").filter((l) => l.trim().length > 0); + const turns: string[] = []; + for (const line of lines) { + try { + const parsed = JSON.parse(line) as Record; + if (parsed.type !== "user" && parsed.type !== "assistant") continue; + const msg = parsed.message as Record | undefined; + if (!msg) continue; + const content = msg.content; + let text = ""; + if (typeof content === "string") { + text = content; + } else if (Array.isArray(content)) { + text = content + .filter( + (b): b is { type: "text"; text: string } => typeof b === "object" && b !== null && (b as Record).type === "text", - ) - .map((b) => b.text) - .join(" "); - } - if (text.trim()) { - turns.push(`[${parsed.type as string}]: ${redactText(text).slice(0, 500)}`); - } - } catch { - // skip malformed lines + ) + .map((b) => b.text) + .join(" "); + } + if (text.trim()) { + turns.push(`[${parsed.type as string}]: ${redactText(text).slice(0, 500)}`); } + } catch { + // skip malformed lines } + } + return turns; +} - const lastTurns = turns.slice(-30); - return lastTurns.length > 0 ? lastTurns.join("\n\n") : null; - } catch { - return null; +/** + * Generate a prose digest of a session transcript using Workers AI. + * Called via waitUntil at upload time — failures are silent. + * Writes the result to the session_digest column on the deployment. + */ +export async function generateSessionDigest( + env: Bindings, + deploymentId: string, + rawTranscript: string, +): Promise { + try { + const turns = parseTranscriptTurns(rawTranscript); + if (turns.length === 0) return; + + // Take last 50 turns — recent context matters most for debugging + const recentTurns = turns.slice(-50).join("\n\n"); + + const response = await env.AI.run("@cf/glm4/glm-4.7-flash" as keyof AiModels, { + messages: [ + { + role: "system", + content: + "You summarize coding session transcripts. Be concise (3-8 sentences). Focus on: problems encountered and how they were resolved, key decisions or trade-offs made, workarounds applied, and anything that could break later. Skip pleasantries, tool invocations, and routine file edits. If the session is trivial (a quick fix with no complications), say so in one sentence.", + }, + { + role: "user", + content: `Summarize this coding session transcript:\n\n${recentTurns}`, + }, + ], + max_tokens: 400, + }); + + const text = + typeof response === "object" && response !== null && "response" in response + ? String((response as { response: unknown }).response).trim() + : ""; + + if (!text) return; + + await env.DB.prepare("UPDATE deployments SET session_digest = ? WHERE id = ?") + .bind(text, deploymentId) + .run(); + } catch (err) { + console.error("generateSessionDigest failed:", err); } } -// Returns null when ANTHROPIC_API_KEY is absent; throws on API failure (caller catches and falls back) async function synthesizeAnswer( env: Bindings, question: string, evidence: AskProjectEvidence[], sessionExcerpt: string | null, -): Promise | null> { - if (!env.ANTHROPIC_API_KEY) return null; - - const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); - +): Promise | null> { const evidenceBlock = evidence - .map((e) => `- [${e.id}] type=${e.type} source=${e.source} relation=${e.relation}: ${e.summary}`) + .map( + (e) => `- [${e.id}] type=${e.type} source=${e.source} relation=${e.relation}: ${e.summary}`, + ) .join("\n"); const transcriptBlock = sessionExcerpt @@ -309,13 +341,23 @@ Respond with only valid JSON (no markdown fences): "confidence": "high" | "medium" | "low" }`; - const message = await client.messages.create({ - model: "claude-haiku-4-5-20251001", + const response = await env.AI.run("@cf/glm4/glm-4.7-flash" as keyof AiModels, { + messages: [ + { + role: "system", + content: "You are a concise debugging assistant. Respond with only valid JSON.", + }, + { role: "user", content: prompt }, + ], max_tokens: 512, - messages: [{ role: "user", content: prompt }], }); - const text = message.content[0]?.type === "text" ? message.content[0].text : ""; + const text = + typeof response === "object" && response !== null && "response" in response + ? String((response as { response: unknown }).response) + : ""; + + if (!text) return null; try { const parsed = JSON.parse(text) as Record; @@ -324,7 +366,9 @@ Respond with only valid JSON (no markdown fences): root_cause: typeof parsed.root_cause === "string" ? parsed.root_cause : undefined, suggested_fix: typeof parsed.suggested_fix === "string" ? parsed.suggested_fix : undefined, confidence: - parsed.confidence === "high" || parsed.confidence === "medium" || parsed.confidence === "low" + parsed.confidence === "high" || + parsed.confidence === "medium" || + parsed.confidence === "low" ? parsed.confidence : "low", }; @@ -679,19 +723,16 @@ export async function answerProjectQuestion(input: AskProjectInput): Promise).has_session_transcript === 1) { - sessionExcerpt = await fetchSessionTranscript(env, project.id, targetDeployment.id); - if (sessionExcerpt) { - evidence.add( - "session_transcript", - "deploy_session", - `Deploy session context: ${sessionExcerpt.slice(0, 200)}`, - "supports", - { deployment_id: targetDeployment.id }, - ); - } + // Use pre-computed session digest if available + const sessionExcerpt = targetDeployment.session_digest; + if (sessionExcerpt) { + evidence.add( + "session_transcript", + "deploy_session", + `Deploy session context: ${sessionExcerpt.slice(0, 300)}`, + "supports", + { deployment_id: targetDeployment.id }, + ); } const hasInsufficientEvidence = evidence.values().some((e) => e.relation === "gap"); @@ -707,12 +748,15 @@ export async function answerProjectQuestion(input: AskProjectInput): Promise { - console.error("ask_project: LLM synthesis failed, falling back to pickAnswer:", err); - return null; - }, - ); + const synthesized = await synthesizeAnswer( + env, + question, + evidence.values(), + sessionExcerpt, + ).catch((err: unknown) => { + console.error("ask_project: LLM synthesis failed, falling back to pickAnswer:", err); + return null; + }); const synthesis = synthesized ?? { answer: pickAnswer(pickAnswerParams) }; diff --git a/apps/control-plane/src/index.ts b/apps/control-plane/src/index.ts index 21451eb..ee3fb97 100644 --- a/apps/control-plane/src/index.ts +++ b/apps/control-plane/src/index.ts @@ -4,6 +4,17 @@ import { unzipSync } from "fflate"; import { Hono } from "hono"; import { cors } from "hono/cors"; import type Stripe from "stripe"; +import { indexLatestDeploymentSource } from "./ask-code-index"; +import { + type AskIndexQueueMessage, + consumeAskIndexBatch, + enqueueAskIndexJob, +} from "./ask-index-queue"; +import { + type AskProjectRequest, + answerProjectQuestion, + generateSessionDigest, +} from "./ask-project"; import { BillingService } from "./billing-service"; import { type AIUsageByModel, @@ -21,13 +32,6 @@ import { DaimoBillingService } from "./daimo-billing-service"; import { DeploymentService, validateManifest } from "./deployment-service"; import { getDoEnforcementStatus, processDoMetering } from "./do-metering"; import { REFERRAL_CAP, TIER_LIMITS, computeLimits } from "./entitlements-config"; -import { indexLatestDeploymentSource } from "./ask-code-index"; -import { - type AskIndexQueueMessage, - consumeAskIndexBatch, - enqueueAskIndexJob, -} from "./ask-index-queue"; -import { answerProjectQuestion, type AskProjectRequest } from "./ask-project"; import { ProvisioningService, normalizeSlug, validateSlug } from "./provisioning"; import { ProjectCacheService } from "./repositories/project-cache-service"; import { validateReadOnly } from "./sql-utils"; @@ -182,7 +186,8 @@ function inferAskSource(c: { req: { header: (name: string) => string | undefined const userAgent = c.req.header("user-agent")?.toLowerCase() ?? ""; if (userAgent.includes("mozilla")) return "web"; if (userAgent.includes("curl")) return "api"; - if (userAgent.includes("jack") || userAgent.includes("bun") || userAgent.includes("node")) return "cli"; + if (userAgent.includes("jack") || userAgent.includes("bun") || userAgent.includes("node")) + return "cli"; return "api"; } @@ -3757,12 +3762,13 @@ api.put("/projects/:projectId/deployments/:deploymentId/session-transcript", asy httpMetadata: { contentType: "application/x-ndjson" }, }); - await c.env.DB.prepare( - "UPDATE deployments SET has_session_transcript = 1 WHERE id = ?", - ) + await c.env.DB.prepare("UPDATE deployments SET has_session_transcript = 1 WHERE id = ?") .bind(deploymentId) .run(); + // Generate a prose digest in the background — never blocks the response + c.executionCtx.waitUntil(generateSessionDigest(c.env, deploymentId, body)); + return c.json({ ok: true }); }); diff --git a/apps/control-plane/src/types.ts b/apps/control-plane/src/types.ts index fd296a2..cdb442c 100644 --- a/apps/control-plane/src/types.ts +++ b/apps/control-plane/src/types.ts @@ -30,7 +30,8 @@ export type Bindings = { // Optional PostHog server-side capture key for control-plane events POSTHOG_API_KEY?: string; POSTHOG_HOST?: string; - ANTHROPIC_API_KEY?: string; + // Workers AI binding for LLM synthesis in ask_project + AI: Ai; }; // Project status enum @@ -116,6 +117,7 @@ export interface Deployment { error_message: string | null; message: string | null; has_session_transcript: number; // SQLite boolean (0 or 1) + session_digest: string | null; created_at: string; updated_at: string; } diff --git a/apps/control-plane/wrangler.toml b/apps/control-plane/wrangler.toml index 0147e96..d84fa37 100644 --- a/apps/control-plane/wrangler.toml +++ b/apps/control-plane/wrangler.toml @@ -2,6 +2,9 @@ name = "jack-control" main = "src/index.ts" compatibility_date = "2024-12-01" +[ai] +binding = "AI" + routes = [ { pattern = "control.getjack.org", custom_domain = true } ] @@ -81,4 +84,3 @@ dataset = "jack_control_usage" # - DAIMO_WEBHOOK_SECRET # Daimo webhook basic auth token # - DAIMO_RECEIVER_ADDRESS # Wallet address to receive USDC on Base # - POSTHOG_API_KEY # Optional: server-side product analytics capture -# - ANTHROPIC_API_KEY # Optional: Anthropic API key for LLM answer synthesis in ask_project diff --git a/bun.lock b/bun.lock index 30364c7..7b683e6 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,6 @@ "apps/control-plane": { "name": "@getjack/control-plane", "dependencies": { - "@anthropic-ai/sdk": "^0.36.3", "@getjack/auth": "workspace:*", "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", @@ -71,7 +70,7 @@ "stripe": "^17.5.0", }, "devDependencies": { - "@cloudflare/workers-types": "^4.20241205.0", + "@cloudflare/workers-types": "^4.20260218.0", "typescript": "^5.0.0", }, }, @@ -138,8 +137,6 @@ "packages": { "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.36.3", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-+c0mMLxL/17yFZ4P5+U6bTWiCSFZUKJddrv01ud2aFBWnTPLdRncYV76D3q1tqfnL7aCnhRtykFnoCFzvr4U3Q=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -232,7 +229,7 @@ "@cloudflare/workers-oauth-provider": ["@cloudflare/workers-oauth-provider@0.2.3", "", {}, "sha512-86a5eJZR+kXoRBWVYEf47RmHM4FjqETOtJSBv7MpzmkcfHU27XhahhwVOO4i4OQDNIPGOOubVcbWDVDLydfD7g=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260218.0", "", {}, "sha512-E28uJNJb9J9pca3RaxjXm1JxAjp8td9/cudkY+IT8rio71NlshN7NKMe2Cr/6GN+RufbSnp+N3ZKP74xgUaL0A=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -716,8 +713,6 @@ "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], - "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], - "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/readdir-glob": ["@types/readdir-glob@1.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg=="], @@ -756,8 +751,6 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], - "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -776,8 +769,6 @@ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], @@ -856,8 +847,6 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], @@ -992,8 +981,6 @@ "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -1040,8 +1027,6 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -1114,14 +1099,8 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - - "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], - "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], - "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -1158,8 +1137,6 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hast-util-classnames": ["hast-util-classnames@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-tI3JjoGDEBVorMAWK4jNRsfLMYmih1BUOG3VV36pH36njs1IEl7xkNrVTD2mD2yYHmQCa5R/fj61a8IAF4bRaQ=="], @@ -1206,8 +1183,6 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1480,10 +1455,6 @@ "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1744,8 +1715,6 @@ "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1838,12 +1807,6 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "workerd": ["workerd@1.20251217.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251217.0", "@cloudflare/workerd-darwin-arm64": "1.20251217.0", "@cloudflare/workerd-linux-64": "1.20251217.0", "@cloudflare/workerd-linux-arm64": "1.20251217.0", "@cloudflare/workerd-windows-64": "1.20251217.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA=="], @@ -1880,8 +1843,6 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -1894,8 +1855,20 @@ "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@getjack/auth-worker/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + + "@getjack/binding-proxy-worker/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + + "@getjack/dispatch-worker/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + "@getjack/jack/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@getjack/log-worker/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + + "@getjack/mcp-worker/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + + "@getjack/telemetry-worker/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251221.0", "", {}, "sha512-VVTEhY29TtwIwjBjpRrdT51Oqu0JlXijc5TiEKFCjwouUSn+5VhzoTSaz7UBjVOu4vfvcQmjqt/dzwBUR7c95w=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -1960,8 +1933,6 @@ "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], @@ -1978,8 +1949,6 @@ "hast-util-to-jsx-runtime/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "humanize-ms/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -2026,8 +1995,6 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2110,8 +2077,6 @@ "finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "hast-util-from-dom/hastscript/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], From a2b61ad3dea3fdcf697e29c9073366ce4c86ded4 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 18 Feb 2026 13:26:09 +0100 Subject: [PATCH 5/7] fix(mcp): surface build stderr and include HOME/TMPDIR in env config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP deploys failed with "Build failed" across projects because: 1. Claude Code/Desktop replaces the entire process env with only what's in the MCP config — missing HOME/TMPDIR broke wrangler subprocess resolution 2. The actual build error (stderr) was dropped by formatErrorResponse(), making failures undiagnosable Add HOME, TMPDIR, USER to MCP env config. Surface JackError.meta.stderr as error.details in MCP responses. Users need `jack mcp install` to pick up the new env config. --- apps/cli/src/lib/mcp-config.ts | 5 ++++- apps/cli/src/mcp/utils.ts | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/lib/mcp-config.ts b/apps/cli/src/lib/mcp-config.ts index a533202..ba622fe 100644 --- a/apps/cli/src/lib/mcp-config.ts +++ b/apps/cli/src/lib/mcp-config.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import { homedir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import { platform } from "node:os"; import { dirname, join } from "node:path"; import { CONFIG_DIR } from "./config.ts"; @@ -86,6 +86,9 @@ export function getJackMcpConfig(): McpServerConfig { args: ["mcp", "serve"], env: { PATH: defaultPaths.join(":"), + HOME: homedir(), + TMPDIR: tmpdir(), + ...(process.env.USER && { USER: process.env.USER }), }, }; } diff --git a/apps/cli/src/mcp/utils.ts b/apps/cli/src/mcp/utils.ts index c6d0ee8..6118cfd 100644 --- a/apps/cli/src/mcp/utils.ts +++ b/apps/cli/src/mcp/utils.ts @@ -27,12 +27,18 @@ export function formatErrorResponse(error: unknown, startTime: number): McpToolR ? (error.suggestion ?? getSuggestionForError(code)) : getSuggestionForError(code); + // Surface subprocess stderr so AI agents can see actual build/deploy errors + const details = isJackError(error) && error.meta?.stderr + ? String(error.meta.stderr) + : undefined; + return { success: false, error: { code, message, suggestion, + ...(details && { details }), }, meta: { duration_ms: Date.now() - startTime, From f8a3ae592bf8808125813a275585616cbaacfb27 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 18 Feb 2026 13:44:29 +0100 Subject: [PATCH 6/7] feat(docs): apply design system theme to vocs config Map oklch color tokens, JetBrains Mono, forced dark mode, and transparent shadows to match the landing page design system. --- vocs.config.tsx | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/vocs.config.tsx b/vocs.config.tsx index 0ee4c6a..3076d54 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -25,6 +25,61 @@ export default defineConfig({ }, iconUrl: "/jack-logo.png", ogImageUrl: "/og.png", + font: { + default: { google: "Inter" }, + mono: { google: "JetBrains Mono" }, + }, + theme: { + accentColor: "oklch(0.72 0.15 220)", + colorScheme: "dark", + variables: { + color: { + // Background hierarchy + background: "oklch(0.06 0.01 250)", + background2: "oklch(0.10 0.012 250)", + background3: "oklch(0.10 0.012 250)", + background4: "oklch(0.14 0.013 250)", + background5: "oklch(0.18 0.015 250)", + backgroundDark: "oklch(0.06 0.01 250)", + backgroundDarkTint: "oklch(0.10 0.012 250)", + + // Text hierarchy + heading: "oklch(0.95 0.008 250)", + title: "oklch(0.95 0.008 250)", + text: "oklch(0.60 0.015 250)", + text2: "oklch(0.55 0.015 250)", + text3: "oklch(0.45 0.015 250)", + text4: "oklch(0.35 0.015 250)", + textHover: "oklch(0.95 0.008 250)", + + // Borders + border: "oklch(0.25 0.015 250)", + border2: "oklch(0.30 0.015 250)", + + // Accent (blue primary) + backgroundAccent: "oklch(0.72 0.15 220)", + backgroundAccentHover: "oklch(0.65 0.15 220)", + backgroundAccentText: "oklch(0.06 0.01 250)", + textAccent: "oklch(0.72 0.15 220)", + textAccentHover: "oklch(0.65 0.15 220)", + borderAccent: "oklch(0.72 0.15 220)", + + // Green (brand accent) + textGreen: "oklch(0.72 0.18 150)", + textGreenHover: "oklch(0.65 0.18 150)", + + // Kill shadows + shadow: "transparent", + shadow2: "transparent", + + // Inverted for buttons + inverted: "oklch(0.95 0.008 250)", + }, + // Code block & inline code (semantic) + // codeBlockBackground → card bg + // codeInlineBackground → muted bg + }, + }, topNav: [ { text: "Quickstart", link: "/quickstart" }, { text: "Templates", link: "/templates" }, From aa14e00bf5f97b847bb18612d6cedbc878c1c189 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 18 Feb 2026 14:13:34 +0100 Subject: [PATCH 7/7] fix(mcp): use cloud-authoritative data for managed project name and URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three hardening fixes for wrong project name/URL in MCP responses: - Fix E: sync wrangler config name with cloud slug after project creation (4 call sites in project-operations.ts). Adds updateWranglerConfigName() utility that preserves comments/formatting for JSONC and TOML. - Fix C: getProjectStatus() uses overview response (slug, url) instead of reading wrangler config name for managed projects. Falls back to local data if cloud is unreachable. - Fix D: reject deploy if wrangler name is literally "jack-template" — this is always a bug from un-rendered template files. --- apps/cli/src/lib/project-operations.ts | 69 +++++++++++++++++++++----- apps/cli/src/lib/wrangler-config.ts | 24 +++++++++ 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/apps/cli/src/lib/project-operations.ts b/apps/cli/src/lib/project-operations.ts index 39e700c..9442add 100644 --- a/apps/cli/src/lib/project-operations.ts +++ b/apps/cli/src/lib/project-operations.ts @@ -72,7 +72,7 @@ import { applySchema, getD1Bindings, getD1DatabaseName, hasD1Config } from "./sc import { getSavedSecrets, saveSecrets } from "./secrets.ts"; import { getProjectNameFromDir, getRemoteManifest } from "./storage/index.ts"; import { Events, track, trackActivationIfFirst } from "./telemetry.ts"; -import { findWranglerConfig, hasWranglerConfig } from "./wrangler-config.ts"; +import { findWranglerConfig, hasWranglerConfig, updateWranglerConfigName } from "./wrangler-config.ts"; // ============================================================================ // Type Definitions @@ -644,6 +644,14 @@ async function runAutoDetectFlow( usePrebuilt: false, }); + // Sync wrangler config name with cloud slug if they differ + if (remoteResult.projectSlug !== projectName) { + const configPath = findWranglerConfig(projectPath); + if (configPath) { + await updateWranglerConfigName(configPath, remoteResult.projectSlug); + } + } + // Link project locally (include username for correct URL display) await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername ?? undefined); await registerPath(remoteResult.projectId, projectPath); @@ -651,7 +659,7 @@ async function runAutoDetectFlow( track(Events.AUTO_DETECT_SUCCESS, { type: detection.type }); return { - projectName, + projectName: remoteResult.projectSlug, projectId: remoteResult.projectId, deployMode: "managed", }; @@ -723,6 +731,14 @@ async function runAutoDetectFlow( usePrebuilt: false, }); + // Sync wrangler config name with cloud slug if they differ + if (remoteResult.projectSlug !== slugifiedName) { + const configPath = findWranglerConfig(projectPath); + if (configPath) { + await updateWranglerConfigName(configPath, remoteResult.projectSlug); + } + } + // Link project locally (include username for correct URL display) await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername ?? undefined); await registerPath(remoteResult.projectId, projectPath); @@ -730,7 +746,7 @@ async function runAutoDetectFlow( track(Events.AUTO_DETECT_SUCCESS, { type: detection.type }); return { - projectName: slugifiedName, + projectName: remoteResult.projectSlug, projectId: remoteResult.projectId, deployMode: "managed", }; @@ -1243,6 +1259,15 @@ export async function createProject( }, }); remoteResult = result.remoteResult; + + // Sync wrangler config name with cloud slug if they differ + if (remoteResult && remoteResult.projectSlug !== projectName) { + const configPath = findWranglerConfig(targetDir); + if (configPath) { + await updateWranglerConfigName(configPath, remoteResult.projectSlug); + } + } + timings.push({ label: "Parallel setup", duration: timerEnd("parallel-setup") }); reporter.stop(); if (urlShownEarly) { @@ -1620,6 +1645,15 @@ export async function deployProject(options: DeployOptions = {}): Promise r.resource_type === "d1"); dbName = d1?.resource_name || null; @@ -2037,7 +2079,8 @@ export async function getProjectStatus( lastDeployMessage = latest.message; } } catch { - // Silent fail — supplementary data + // Fallback to local data if cloud is unreachable + workerUrl = await buildManagedUrl(projectName, link.owner_username, resolvedPath); } } else if (localExists) { // For BYO, parse from wrangler config diff --git a/apps/cli/src/lib/wrangler-config.ts b/apps/cli/src/lib/wrangler-config.ts index 93133e1..46fe39b 100644 --- a/apps/cli/src/lib/wrangler-config.ts +++ b/apps/cli/src/lib/wrangler-config.ts @@ -42,6 +42,30 @@ export function hasWranglerConfig(projectDir: string): boolean { return findWranglerConfig(projectDir) !== null; } +// ============================================================================ +// Wrangler Config Name Updates +// ============================================================================ + +/** + * Update the "name" field in a wrangler config file. + * Uses text replacement to preserve comments and formatting. + * Supports both JSONC/JSON ("name": "value") and TOML (name = "value"). + * + * @returns true if the name was updated, false if unchanged or not found + */ +export async function updateWranglerConfigName(configPath: string, newName: string): Promise { + const content = await Bun.file(configPath).text(); + let updated: string; + if (configPath.endsWith(".toml")) { + updated = content.replace(/^name\s*=\s*"[^"]*"/m, `name = "${newName}"`); + } else { + updated = content.replace(/"name"\s*:\s*"[^"]*"/, `"name": "${newName}"`); + } + if (updated === content) return false; + await Bun.write(configPath, updated); + return true; +} + // ============================================================================ // D1 Binding Config // ============================================================================