diff --git a/pkg/create/templates.go b/pkg/create/templates.go index fb5845f..f5090b3 100644 --- a/pkg/create/templates.go +++ b/pkg/create/templates.go @@ -17,8 +17,9 @@ const ( TemplateBrowserUse = "browser-use" TemplateStagehand = "stagehand" TemplateOpenAGIComputerUse = "openagi-computer-use" - TemplateClaudeAgentSDK = "claude-agent-sdk" - TemplateYutoriComputerUse = "yutori" + TemplateClaudeAgentSDK = "claude-agent-sdk" + TemplateYutoriComputerUse = "yutori" + TemplateCloudglueSessionRecap = "cloudglue-session-recap" ) type TemplateInfo struct { @@ -90,6 +91,11 @@ var Templates = map[string]TemplateInfo{ Description: "Implements a Yutori n1 computer use agent", Languages: []string{LanguageTypeScript, LanguagePython}, }, + TemplateCloudglueSessionRecap: { + Name: "Cloudglue Session Recap", + Description: "Analyze browser session recordings with Cloudglue video AI", + Languages: []string{LanguageTypeScript}, + }, } // GetSupportedTemplatesForLanguage returns a list of all supported template names for a given language @@ -213,6 +219,11 @@ var Commands = map[string]map[string]DeployConfig{ NeedsEnvFile: true, InvokeCommand: `kernel invoke ts-yutori-cua cua-task --payload '{"query": "Navigate to https://example.com and describe the page"}'`, }, + TemplateCloudglueSessionRecap: { + EntryPoint: "index.ts", + NeedsEnvFile: true, + InvokeCommand: `kernel invoke ts-cloudglue-session-recap session-recap --payload '{"recording_url": "https://your-recording-url.com/replay.mp4"}'`, + }, }, LanguagePython: { TemplateSampleApp: { diff --git a/pkg/templates/typescript/cloudglue-session-recap/.env.example b/pkg/templates/typescript/cloudglue-session-recap/.env.example new file mode 100644 index 0000000..dc43fbb --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/.env.example @@ -0,0 +1,2 @@ +# Get your API key at https://app.cloudglue.dev +CLOUDGLUE_API_KEY=your_cloudglue_api_key_here diff --git a/pkg/templates/typescript/cloudglue-session-recap/README.md b/pkg/templates/typescript/cloudglue-session-recap/README.md new file mode 100644 index 0000000..6e96fb9 --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/README.md @@ -0,0 +1,88 @@ +# Cloudglue Session Recap + +Analyze browser session recordings with [Cloudglue](https://cloudglue.dev) to get a detailed scene-by-scene recap — thumbnails, timestamps, user actions, and screen descriptions. + +## What it does + +The **`session-recap`** action takes any video URL (a Kernel session recording, screen capture, or video file) and produces: + +- A generated title and summary +- A thumbnail grid overview (4 columns) +- A detailed scene breakdown with timestamps, descriptions, user actions, URLs, and screen state +- A complete markdown document combining all of the above + +Uses Cloudglue describe (for visual timeline + thumbnails) and segment-level extract (for structured user actions) in parallel. + +## Input + +```json +{ + "recording_url": "https://example.com/recordings/replay.mp4", + "title": "Login Flow Test", + "max_seconds": 8 +} +``` + +Only `recording_url` is required. `title` defaults to "Session Recap of \" and `max_seconds` defaults to 8. + +## Output + +```json +{ + "title": "Login Flow Test", + "recording_url": "https://example.com/recordings/replay.mp4", + "summary": "A user navigates to the login page, enters credentials, and is redirected to the dashboard.", + "duration_seconds": 22.5, + "scene_count": 4, + "scenes": [ + { + "timestamp": "00:00", + "thumbnail_url": "https://...", + "description": "Login page loaded with email and password input fields.", + "user_action": "Navigated to the login page", + "screen_description": "Login form with email field, password field, and Sign In button." + }, + { + "timestamp": "00:06", + "thumbnail_url": "https://...", + "description": "User enters email and password into the login form.", + "user_action": "Typed credentials into the email and password fields", + "screen_description": "Login form with filled-in fields and cursor in the password input." + } + ], + "markdown": "# Login Flow Test\n\n- **Recording:** ..." +} +``` + +## Setup + +Create a `.env` file: + +``` +CLOUDGLUE_API_KEY=your-cloudglue-api-key +``` + +Get your API key at [app.cloudglue.dev](https://app.cloudglue.dev). + +## Deploy + +```bash +kernel login +kernel deploy index.ts --env-file .env +``` + +## Invoke + +```bash +# Basic — auto-generates title from URL +kernel invoke ts-cloudglue-session-recap session-recap \ + --payload '{"recording_url": "https://your-recording-url.com/replay.mp4"}' + +# With custom title +kernel invoke ts-cloudglue-session-recap session-recap \ + --payload '{"recording_url": "https://...", "title": "Login Flow Test"}' + +# With longer scene windows (default is 8s, max 60s) +kernel invoke ts-cloudglue-session-recap session-recap \ + --payload '{"recording_url": "https://...", "max_seconds": 15}' +``` diff --git a/pkg/templates/typescript/cloudglue-session-recap/_gitignore b/pkg/templates/typescript/cloudglue-session-recap/_gitignore new file mode 100644 index 0000000..095f573 --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/_gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ +package-lock.json + +# TypeScript +*.tsbuildinfo +dist/ +build/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Misc +.cache/ +.temp/ +.tmp/ diff --git a/pkg/templates/typescript/cloudglue-session-recap/cloudglue.ts b/pkg/templates/typescript/cloudglue-session-recap/cloudglue.ts new file mode 100644 index 0000000..c83c575 --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/cloudglue.ts @@ -0,0 +1,151 @@ +import { Cloudglue } from "@cloudglue/cloudglue-js"; +import type { SegmentationConfig } from "@cloudglue/cloudglue-js"; + +const CLOUDGLUE_API_KEY = process.env.CLOUDGLUE_API_KEY; + +if (!CLOUDGLUE_API_KEY) { + throw new Error("CLOUDGLUE_API_KEY is not set"); +} + +const client = new Cloudglue({ apiKey: CLOUDGLUE_API_KEY }); + +export interface SegmentationOptions { + maxSeconds?: number; +} + +/** Build segmentation config with configurable max_seconds. */ +function buildSegmentation(opts?: SegmentationOptions): SegmentationConfig { + return { + strategy: "shot-detector", + shot_detector_config: { + detector: "adaptive", + min_seconds: 1, + max_seconds: opts?.maxSeconds ?? 8, + fill_gaps: true, + }, + }; +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + +/** + * Retry a function up to maxRetries times with backoff. + */ +async function withRetry( + fn: () => Promise, + label: string, + maxRetries = 2, + backoffMs = 20_000 +): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (err: unknown) { + const msg = + err instanceof Error ? err.message : String(err); + console.error( + `${label}: attempt ${attempt}/${maxRetries} failed: ${msg}` + ); + if (attempt < maxRetries) { + console.log(`${label}: waiting ${backoffMs / 1000}s before retry...`); + await sleep(backoffMs); + console.log(`${label}: retrying (attempt ${attempt + 1})...`); + } else { + console.error(`${label}: all ${maxRetries} attempts exhausted.`); + throw err; + } + } + } + // Unreachable, but TypeScript needs it + throw new Error(`${label} failed`); +} + +/** + * Poll a describe job until complete, fetching full data with thumbnails. + */ +async function pollDescribe( + jobId: string, + intervalMs = 5000 +): Promise> { + while (true) { + const job = await client.describe.getDescribe(jobId, { + include_thumbnails: true, + include_shots: true, + }); + if (job.status === "completed") return job as Record; + if (job.status === "failed" || job.status === "not_applicable") { + throw new Error(`Describe job ${jobId} failed: ${JSON.stringify(job)}`); + } + await new Promise((r) => setTimeout(r, intervalMs)); + } +} + +/** + * Poll an extract job until complete, fetching thumbnails. + */ +async function pollExtract( + jobId: string, + intervalMs = 5000 +): Promise> { + while (true) { + const job = await client.extract.getExtract(jobId, { + include_thumbnails: true, + include_shots: true, + }); + if (job.status === "completed") return job as Record; + if (job.status === "failed" || job.status === "not_applicable") { + throw new Error(`Extract job ${jobId} failed: ${JSON.stringify(job)}`); + } + await new Promise((r) => setTimeout(r, intervalMs)); + } +} + +/** + * Describe a video recording via Cloudglue. + * Retries up to 2 times with 20s backoff if the video isn't ready yet. + */ +export async function describeRecording( + url: string, + opts?: SegmentationOptions +): Promise> { + return withRetry(async () => { + const job = await client.describe.createDescribe(url, { + enable_visual_scene_description: true, + enable_scene_text: true, + enable_speech: true, + enable_summary: true, + segmentation_config: buildSegmentation(opts), + include_shots: true, + }); + + console.log(`Describe job created: ${job.job_id}`); + return await pollDescribe(job.job_id); + }, "Describe"); +} + +/** + * Extract structured data from a video at the segment level. + * Retries up to 2 times with 20s backoff if the video isn't ready yet. + */ +export async function extractSegmentLevel( + url: string, + schema: Record, + prompt: string, + opts?: SegmentationOptions +): Promise> { + return withRetry(async () => { + const job = await client.extract.createExtract(url, { + url, + schema, + prompt, + enable_segment_level_entities: true, + segmentation_config: buildSegmentation(opts), + include_shots: true, + }); + + console.log(`Extract job created: ${job.job_id} (segment-level)`); + return await pollExtract(job.job_id); + }, "Extract"); +} diff --git a/pkg/templates/typescript/cloudglue-session-recap/index.ts b/pkg/templates/typescript/cloudglue-session-recap/index.ts new file mode 100644 index 0000000..d540800 --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/index.ts @@ -0,0 +1,457 @@ +import { Kernel, type KernelContext } from "@onkernel/sdk"; +import { chromium } from "playwright-core"; +import { describeRecording, extractSegmentLevel } from "./cloudglue.js"; + +const kernel = new Kernel(); + +const app = kernel.app("ts-cloudglue-session-recap"); + +// --- Types --- + +interface Scene { + timestamp: string; + thumbnail_url: string | null; + description: string; + user_action: string; + screen_description: string; +} + +interface RecapInput { + /** URL of the video recording to analyze */ + recording_url: string; + /** Custom title for the recap (auto-generated if not provided) */ + title?: string; + /** Maximum length in seconds for each scene analyzed (2-60, defaults to 8) */ + max_seconds?: number; +} + +interface RecapOutput { + title: string; + recording_url: string; + summary: string; + duration_seconds: number | null; + scene_count: number; + scenes: Scene[]; + markdown: string; +} + +interface BrowseAndRecapInput { + /** URL to open in the browser */ + url: string; + /** Placeholder for template — replace with your browser agent input/logic */ + task?: string; + /** Custom title for the recap (auto-generated if not provided) */ + title?: string; + /** Maximum length in seconds for each scene analyzed (2-60, defaults to 8) */ + max_seconds?: number; +} + +interface BrowseAndRecapOutput extends RecapOutput { + replay_view_url: string; +} + +// --- Extract schema --- + +const SCENE_SCHEMA = { + type: "object", + properties: { + user_action: { + type: "string", + description: + "What the user did in this segment, e.g. 'Clicked the Sign In button', 'Scrolled down the page', 'Typed search query'. Use 'N/A' if no clear user action.", + }, + screen_description: { + type: "string", + description: + "Brief description of what is visible on screen: the page layout, key UI elements, content shown. Do NOT invent or guess text that is too small to read.", + }, + }, + required: ["user_action", "screen_description"], +}; + +const EXTRACT_PROMPT = + "For each segment of this recording, extract the user action and a description of what is on screen. " + + "Be specific about user interactions (clicks, typing, scrolling, navigating). " + + "For user_action, put 'N/A' if the user is not actively doing anything (e.g. a static screen). " + + "IMPORTANT: Do NOT guess or invent text you cannot clearly read on screen. " + + "Do NOT guess URLs — only report what you can confidently read."; + +// --- Helpers --- + +function formatTimestamp(seconds: number): string { + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; +} + +function trimUrl(url: string, maxLen = 60): string { + if (url.length <= maxLen) return url; + try { + const u = new URL(url); + const trimmed = u.hostname + u.pathname; + return trimmed.length > maxLen + ? trimmed.slice(0, maxLen - 3) + "..." + : trimmed; + } catch { + return url.slice(0, maxLen - 3) + "..."; + } +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + + + +function buildScenes( + describeResult: Record, + extractResult: Record +): Scene[] { + const descData = describeResult.data as Record | undefined; + const segmentSummary = + (descData?.segment_summary as Array>) ?? []; + const shots = + (describeResult.shots as Array>) ?? []; + const descSegments = segmentSummary.length ? segmentSummary : shots; + + const extData = extractResult.data as Record | undefined; + const segmentEntities = + (extData?.segment_entities as Array>) ?? []; + + const maxLen = Math.max(descSegments.length, segmentEntities.length); + if (maxLen === 0) return []; + + const raw: Scene[] = []; + + for (let i = 0; i < maxLen; i++) { + const desc = descSegments[i]; + const ext = segmentEntities[i]; + const entities = (ext?.entities as Record) ?? {}; + + const description = + (desc?.summary as string) || + (desc?.description as string) || + (desc?.title as string) || + ""; + + raw.push({ + timestamp: formatTimestamp( + Number(desc?.start_time ?? ext?.start_time ?? 0) + ), + thumbnail_url: + (desc?.thumbnail_url as string) ?? + (ext?.thumbnail_url as string) ?? + null, + description, + user_action: (entities.user_action as string) ?? "N/A", + screen_description: (entities.screen_description as string) ?? "", + }); + } + + // Deduplicate scenes with the same timestamp + const scenes: Scene[] = []; + for (const scene of raw) { + const prev = scenes[scenes.length - 1]; + if (prev && prev.timestamp === scene.timestamp) { + if (!prev.description && scene.description) { + prev.description = scene.description; + } + if (prev.user_action === "N/A" && scene.user_action !== "N/A") { + prev.user_action = scene.user_action; + } else if (prev.user_action !== "N/A" && scene.user_action !== "N/A") { + prev.user_action += "; " + scene.user_action; + } + if (!prev.screen_description && scene.screen_description) { + prev.screen_description = scene.screen_description; + } + if (!prev.thumbnail_url && scene.thumbnail_url) { + prev.thumbnail_url = scene.thumbnail_url; + } + } else { + scenes.push({ ...scene }); + } + } + + return scenes; +} + +function buildMarkdown( + title: string, + generatedTitle: string, + recordingUrl: string, + summary: string, + durationSeconds: number | null, + scenes: Scene[] +): string { + const lines: string[] = []; + + lines.push(`# ${title}`); + lines.push(""); + lines.push(`- **Recording:** ${recordingUrl}`); + if (durationSeconds != null) { + lines.push(`- **Duration:** ${formatTimestamp(durationSeconds)}`); + } + lines.push(`- **Scenes:** ${scenes.length}`); + lines.push(""); + + lines.push("## Summary"); + lines.push(""); + lines.push(`### ${generatedTitle}`); + lines.push(""); + lines.push(summary); + lines.push(""); + + if (scenes.some((s) => s.thumbnail_url)) { + lines.push("## Thumbnail Preview"); + lines.push(""); + + for (let i = 0; i < scenes.length; i += 4) { + const row = scenes.slice(i, i + 4); + lines.push( + "| " + + row + .map( + (s) => + `[${s.timestamp}](#scene-${i + row.indexOf(s) + 1})` + ) + .join(" | ") + + " |" + ); + lines.push("| " + row.map(() => "---").join(" | ") + " |"); + lines.push( + "| " + + row + .map((s) => + s.thumbnail_url + ? `` + : "*no thumbnail*" + ) + .join(" | ") + + " |" + ); + lines.push(""); + } + } + + lines.push("## Scene Breakdown"); + lines.push(""); + + scenes.forEach((scene, i) => { + const anchor = `scene-${i + 1}`; + lines.push( + `### Scene ${i + 1} — ${scene.timestamp}` + ); + lines.push(""); + if (scene.thumbnail_url) { + lines.push(`![Scene ${i + 1}](${scene.thumbnail_url})`); + lines.push(""); + } + lines.push(`**Description:** ${scene.description}`); + lines.push(""); + if (scene.user_action !== "N/A") { + lines.push(`**User Action:** ${scene.user_action}`); + lines.push(""); + } + if (scene.screen_description) { + lines.push(`**Screen:** ${scene.screen_description}`); + lines.push(""); + } + lines.push("---"); + lines.push(""); + }); + + return lines.join("\n"); +} + +/** + * Shared analysis logic: describe + extract a recording and build the recap. + */ +async function analyzeRecording( + recordingUrl: string, + opts: { title?: string; maxSeconds: number } +): Promise { + // Run describe and extract in parallel (each retries independently) + const [describeResult, extractResult] = await Promise.all([ + describeRecording(recordingUrl, { maxSeconds: opts.maxSeconds }), + extractSegmentLevel(recordingUrl, SCENE_SCHEMA, EXTRACT_PROMPT, { + maxSeconds: opts.maxSeconds, + }), + ]); + + const data = describeResult.data as Record | undefined; + const summary = (data?.summary as string) ?? ""; + const durationSeconds = + (describeResult.duration_seconds as number) ?? null; + const generatedTitle = (data?.title as string) || "Session Recap"; + const title = + opts.title || `Session Recap of ${trimUrl(recordingUrl)}`; + + const scenes = buildScenes(describeResult, extractResult); + const markdown = buildMarkdown( + title, + generatedTitle, + recordingUrl, + summary, + durationSeconds, + scenes + ); + + return { + title, + recording_url: recordingUrl, + summary, + duration_seconds: durationSeconds, + scene_count: scenes.length, + scenes, + markdown, + }; +} + +/** + * Analyze a recording and return a visual scene-by-scene recap. + * + * Pass any video URL — a Kernel session recording, a screen capture, or any video file. + * Returns a structured recap with thumbnails, descriptions, user actions, + * and a complete markdown document. + * + * Parameters: + * recording_url (required) — URL of the video to analyze + * title (optional) — custom title; defaults to "Session Recap of " + * max_seconds (optional) — max length per scene in seconds (2-60, default 8) + * + * Invoke: + * kernel invoke ts-cloudglue-session-recap session-recap \ + * --payload '{"recording_url": "https://..."}' + * + * kernel invoke ts-cloudglue-session-recap session-recap \ + * --payload '{"recording_url": "https://...", "title": "Login Flow Test", "max_seconds": 15}' + */ +app.action( + "session-recap", + async (_ctx: KernelContext, payload?: RecapInput): Promise => { + if (!payload?.recording_url) { + throw new Error("recording_url is required"); + } + + const maxSeconds = payload.max_seconds ?? 8; + if (maxSeconds < 2 || maxSeconds > 60) { + throw new Error("max_seconds must be between 2 and 60"); + } + + console.log( + `Analyzing recording: ${payload.recording_url} (max_seconds: ${maxSeconds})` + ); + + return analyzeRecording(payload.recording_url, { + title: payload.title, + maxSeconds, + }); + } +); + +/** + * Open a URL in a Kernel browser, record the session, then analyze the recording. + * + * Creates a cloud browser, navigates to the URL, waits 10 seconds to capture + * the page, then stops the recording and passes it to Cloudglue for analysis. + * + * The `task` parameter is a placeholder for future agent integration (e.g. + * connecting an AI computer-use agent to execute the task). Currently the + * browser only navigates to the URL and waits. + * + * Parameters: + * url (required) — URL to open in the browser + * task (optional) — task description (placeholder, not executed) + * title (optional) — custom title; defaults to "Session Recap of " + * max_seconds (optional) — max length per scene in seconds (2-60, default 8) + * + * Invoke: + * kernel invoke ts-cloudglue-session-recap browse-and-recap \ + * --payload '{"url": "https://news.ycombinator.com"}' + * + * kernel invoke ts-cloudglue-session-recap browse-and-recap \ + * --payload '{"url": "https://example.com", "task": "Click sign in", "title": "Sign In Flow"}' + */ +app.action( + "browse-and-recap", + async ( + ctx: KernelContext, + payload?: BrowseAndRecapInput + ): Promise => { + if (!payload?.url) throw new Error("url is required"); + + const maxSeconds = payload.max_seconds ?? 8; + if (maxSeconds < 2 || maxSeconds > 60) { + throw new Error("max_seconds must be between 2 and 60"); + } + + // Create a Kernel browser + const kernelBrowser = await kernel.browsers.create({ + invocation_id: ctx.invocation_id, + stealth: true, + }); + + const sessionId = kernelBrowser.session_id; + console.log("Live view:", kernelBrowser.browser_live_view_url); + + // Start replay recording + const replay = await kernel.browsers.replays.start(sessionId); + const replayId = replay.replay_id; + console.log(`Replay recording started: ${replayId}`); + + // Connect Playwright to the Kernel browser + const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url); + const context = browser.contexts()[0] || (await browser.newContext()); + const page = context.pages()[0] || (await context.newPage()); + + try { + console.log(`Navigating to ${payload.url}...`); + await page.goto(payload.url, { waitUntil: "networkidle" }); + + // --------------------------------------------------------------- + // DEMO ONLY: This just waits 10 seconds to capture the page. + // Replace this section with your browser automation logic. + // --------------------------------------------------------------- + await sleep(10_000); + } finally { + await browser.close(); + } + + // Stop recording and wait for replay URL + await sleep(2000); + await kernel.browsers.replays.stop(replayId, { id: sessionId }); + console.log("Replay recording stopped. Waiting for processing..."); + + let replayViewUrl = ""; + const maxWait = 60_000; + const start = Date.now(); + while (Date.now() - start < maxWait) { + const replays = await kernel.browsers.replays.list(sessionId); + const found = replays.find((r) => r.replay_id === replayId); + if (found?.replay_view_url) { + replayViewUrl = found.replay_view_url; + break; + } + await sleep(2000); + } + + if (!replayViewUrl) { + throw new Error("Replay URL not available after processing."); + } + + console.log(`Replay view URL: ${replayViewUrl}`); + console.log("Analyzing recording with Cloudglue..."); + + const recap = await analyzeRecording(replayViewUrl, { + title: payload.title || `Session Recap of ${trimUrl(payload.url)}`, + maxSeconds, + }); + + await kernel.browsers.deleteByID(sessionId); + + return { + ...recap, + replay_view_url: replayViewUrl, + }; + } +); diff --git a/pkg/templates/typescript/cloudglue-session-recap/package.json b/pkg/templates/typescript/cloudglue-session-recap/package.json new file mode 100644 index 0000000..a60211a --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/package.json @@ -0,0 +1,15 @@ +{ + "name": "ts-cloudglue-session-recap", + "module": "index.ts", + "type": "module", + "private": true, + "dependencies": { + "@cloudglue/cloudglue-js": "^0.7.7", + "@onkernel/sdk": "^0.35.0", + "playwright-core": "^1.57.0" + }, + "devDependencies": { + "@types/node": "^22.15.17", + "typescript": "^5.9.3" + } +} diff --git a/pkg/templates/typescript/cloudglue-session-recap/pnpm-lock.yaml b/pkg/templates/typescript/cloudglue-session-recap/pnpm-lock.yaml new file mode 100644 index 0000000..39fa231 --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/pnpm-lock.yaml @@ -0,0 +1,278 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@cloudglue/cloudglue-js': + specifier: ^0.7.7 + version: 0.7.7(@zodios/core@10.9.6(axios@1.14.0)(zod@3.25.76)) + '@onkernel/sdk': + specifier: ^0.35.0 + version: 0.35.0 + playwright-core: + specifier: ^1.57.0 + version: 1.59.1 + devDependencies: + '@types/node': + specifier: ^22.15.17 + version: 22.19.15 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@cloudglue/cloudglue-js@0.7.7': + resolution: {integrity: sha512-RA+5o6BSSPPCiGfI1zoPANHjJK3vnWYq1/uy9fpLHxB2X4VfuIdxXAcj5opgyQg2dfDILoxlWEoPLYvTRjn1eg==} + peerDependencies: + '@zodios/core': ^10.0.0 + + '@onkernel/sdk@0.35.0': + resolution: {integrity: sha512-EnTEyTm85WwOOXZziDTySNHl46ZO+DSJjVDJDJNarwkD+kv623TzXDLpgH7vwy4LfQjQ4DzOQe0hHKgCYrAv5A==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@zodios/core@10.9.6': + resolution: {integrity: sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==} + peerDependencies: + axios: ^0.x || ^1.0.0 + zod: ^3.x + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.14.0: + resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@cloudglue/cloudglue-js@0.7.7(@zodios/core@10.9.6(axios@1.14.0)(zod@3.25.76))': + dependencies: + '@zodios/core': 10.9.6(axios@1.14.0)(zod@3.25.76) + axios: 1.14.0 + zod: 3.25.76 + transitivePeerDependencies: + - debug + + '@onkernel/sdk@0.35.0': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@zodios/core@10.9.6(axios@1.14.0)(zod@3.25.76)': + dependencies: + axios: 1.14.0 + zod: 3.25.76 + + asynckit@0.4.0: {} + + axios@1.14.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + follow-redirects@1.15.11: {} + + 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.35 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + playwright-core@1.59.1: {} + + proxy-from-env@2.1.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + zod@3.25.76: {} diff --git a/pkg/templates/typescript/cloudglue-session-recap/tsconfig.json b/pkg/templates/typescript/cloudglue-session-recap/tsconfig.json new file mode 100644 index 0000000..bfc01a2 --- /dev/null +++ b/pkg/templates/typescript/cloudglue-session-recap/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules", "dist"] +}