From 3646e338b477ed6763078be21613001b416e43a9 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 16 Oct 2025 11:55:33 +0200 Subject: [PATCH] chore: rename data dir to .blink --- .prettierignore | 2 +- README.md | 4 +- packages/blink/src/build/index.ts | 4 +- packages/blink/src/cli/deploy.ts | 12 +-- packages/blink/src/cli/dev.ts | 9 +- .../blink/src/cli/init-templates/index.ts | 24 ++--- .../src/cli/init-templates/scratch/.gitignore | 2 +- .../cli/init-templates/slack-bot/.gitignore | 2 +- packages/blink/src/cli/lib/devhook.ts | 2 +- packages/blink/src/cli/lib/first-run.ts | 2 +- packages/blink/src/cli/lib/migrate.ts | 89 +++++++------------ packages/blink/src/cli/run.ts | 10 +-- packages/blink/src/react/use-dev-mode.ts | 2 +- packages/blink/src/react/use-devhook.ts | 2 +- 14 files changed, 70 insertions(+), 96 deletions(-) diff --git a/.prettierignore b/.prettierignore index e684c67..f3a688b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ -.blink/data/ +.blink/ **/*.hbs diff --git a/README.md b/README.md index 628081b..d0014be 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ agent.on("request", async (request, context) => { agent.serve(); ``` -Locally, all chats are stored in `./data/chats/.json` relative to where your agent is running. +Locally, all chats are stored in `./.blink/chats/.json` relative to where your agent is running. In the cloud, chats keys are namespaced per-agent. @@ -174,7 +174,7 @@ agent.on("chat", async ({ messages }) => { agent.serve(); ``` -Locally, all storage is in `./data/storage.json` relative to where your agent is running. +Locally, all storage is in `./.blink/storage.json` relative to where your agent is running. In the cloud, storage is namespaced per-agent. diff --git a/packages/blink/src/build/index.ts b/packages/blink/src/build/index.ts index 67d4f1f..5600c9e 100644 --- a/packages/blink/src/build/index.ts +++ b/packages/blink/src/build/index.ts @@ -14,7 +14,7 @@ export interface Config { /** * outdir is the directory to write the build output to. - * Defaults to `data/build` in the current working directory. + * Defaults to `.blink/build` in the current working directory. */ outdir?: string; @@ -111,7 +111,7 @@ Try creating "agent.ts" or specify "entry" in a blink.config.ts file.`); } } if (!config.outdir) { - config.outdir = path.resolve(directory, "data/build"); + config.outdir = path.resolve(directory, ".blink/build"); } if (!config.build) { // By default, we bundle with esbuild. diff --git a/packages/blink/src/cli/deploy.ts b/packages/blink/src/cli/deploy.ts index 96af672..1e641cb 100644 --- a/packages/blink/src/cli/deploy.ts +++ b/packages/blink/src/cli/deploy.ts @@ -5,7 +5,7 @@ import Client, { import { stat, readFile } from "node:fs/promises"; import { basename, dirname, join, relative } from "node:path"; import { loginIfNeeded } from "./lib/auth"; -import { migrateBlinkToData } from "./lib/migrate"; +import { migrateDataToBlink } from "./lib/migrate"; import { existsSync } from "node:fs"; import { mkdir, writeFile, readdir } from "fs/promises"; import { select, confirm, isCancel, spinner } from "@clack/prompts"; @@ -26,8 +26,8 @@ export default async function deploy( directory = process.cwd(); } - // Auto-migrate .blink to data if it exists - await migrateBlinkToData(directory); + // Auto-migrate data to .blink if it exists + await migrateDataToBlink(directory); const token = await loginIfNeeded(); const client = new Client({ @@ -54,8 +54,8 @@ export default async function deploy( // Find the nearest config file if it exists. const rootDirectory = dirname(packageJSON); - // Check for a data directory. This stores the agent's deploy config. - const deployConfigPath = join(rootDirectory, "data", "config.json"); + // Check for a .blink directory. This stores the agent's deploy config. + const deployConfigPath = join(rootDirectory, ".blink", "config.json"); let deployConfig: DeployConfig = {}; if (existsSync(deployConfigPath)) { @@ -549,7 +549,7 @@ async function collectSourceFiles(rootDir: string): Promise { const defaultIgnorePatterns = [ ".git", "node_modules", - "data", + ".blink", ".env", ".env.*", ]; diff --git a/packages/blink/src/cli/dev.ts b/packages/blink/src/cli/dev.ts index 5bb1ff4..1a180ac 100644 --- a/packages/blink/src/cli/dev.ts +++ b/packages/blink/src/cli/dev.ts @@ -5,7 +5,7 @@ import { resolveConfig } from "../build/index"; import { findNearestEntry } from "../build/util"; import { startDev } from "../tui/dev"; import { getAuthToken } from "./lib/auth"; -import { migrateBlinkToData } from "./lib/migrate"; +import { migrateDataToBlink } from "./lib/migrate"; export default async function dev(directory?: string): Promise { if (!directory) { @@ -19,7 +19,7 @@ export default async function dev(directory?: string): Promise { // No agent found in current directory, search upward for .blink let dotBlinkPath = await findNearestEntry(cwd, ".blink"); - // This is legacy behavior to migrate old Blink directories to the new data/ directory. + // This is legacy behavior to migrate old Blink directories to the new .blink/ directory. if (dotBlinkPath && existsSync(join(dotBlinkPath, "build"))) { dotBlinkPath = undefined; } @@ -32,9 +32,8 @@ export default async function dev(directory?: string): Promise { } } } - - // Auto-migrate .blink to data if it exists - await migrateBlinkToData(directory); + // Auto-migrate data/ to .blink/ if it exists + await migrateDataToBlink(directory); const exitWithDump = (error: Error) => { writeFileSync("error.dump", inspect(error, { depth: null })); diff --git a/packages/blink/src/cli/init-templates/index.ts b/packages/blink/src/cli/init-templates/index.ts index 5888aea..ca0533a 100644 --- a/packages/blink/src/cli/init-templates/index.ts +++ b/packages/blink/src/cli/init-templates/index.ts @@ -3,10 +3,8 @@ export const templates = { scratch: { - ".gitignore": - "# dependencies\nnode_modules\n\n# config and build\ndata\n\n# dotenv environment variables file\n.env\n.env.*\n\n# Finder (MacOS) folder config\n.DS_Store\n", - "tsconfig.json": - '{\n "compilerOptions": {\n "lib": ["ESNext"],\n "target": "ESNext",\n "module": "Preserve",\n "moduleDetection": "force",\n\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "verbatimModuleSyntax": true,\n "resolveJsonModule": true,\n "noEmit": true,\n\n "strict": true,\n "skipLibCheck": true,\n "noFallthroughCasesInSwitch": true,\n "noUncheckedIndexedAccess": true,\n "noImplicitOverride": true,\n\n "noUnusedLocals": false,\n "noUnusedParameters": false,\n\n "types": ["node"]\n }\n}\n', + "AGENTS.md": + 'This project is a Blink agent.\n\nYou are an expert software engineer, which makes you an expert agent developer. You are highly idiomatic, opinionated, concise, and precise. The user prefers accuracy over speed.\n\n\n1. Be concise, direct, and to the point.\n2. You are communicating via a terminal interface, so avoid verbosity, preambles, postambles, and unnecessary whitespace.\n3. NEVER use emojis unless the user explicitly asks for them.\n4. You must avoid text before/after your response, such as "The answer is" or "Short answer:", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".\n5. Mimic the style of the user\'s messages.\n6. Do not remind the user you are happy to help.\n7. Do not act with sycophantic flattery or over-the-top enthusiasm.\n8. Do not regurgitate tool output. e.g. if a command succeeds, acknowledge briefly (e.g. "Done" or "Formatted").\n9. *NEVER* create markdown files for the user - *always* guide the user through your efforts.\n10. *NEVER* create example scripts for the user, or examples scripts for you to run. Leverage your tools to accomplish the user\'s goals.\n\n\n\nYour method of assisting the user is by iterating their agent using the context provided by the user in run mode.\n\nYou can obtain additional context by leveraging web search and compute tools to read files, run commands, and search the web.\n\nThe user is _extremely happy_ to provide additional context. They prefer this over you guessing, and then potentially getting it wrong.\n\n\nuser: i want a coding agent\nassistant: Let me take a look at your codebase...\n... tool calls to investigate the codebase...\nassistant: I\'ve created tools for linting, testing, and formatting. Hop back in run mode to use your agent! If you ever encounter undesired behavior from your agent, switch back to edit mode to refine your agent.\n\n\nAlways investigate the current state of the agent before assisting the user.\n\n\n\nAgents are written in TypeScript, and mostly stored in a single `agent.ts` file. Complex agents will have multiple files, like a proper codebase.\n\nEnvironment variables are stored in `.env.local` and `.env.production`. `blink dev` will hot-reload environment variable changes in `.env.local`.\n\nChanges to the agent are hot-reloaded. As you make edits, the user can immediately try them in run mode.\n\n1. _ALWAYS_ use the package manager the user is using (inferred from lock files or `process.argv`).\n2. You _MUST_ use `agent.store` to persist state. The agent process is designed to be stateless.\n3. Test your changes to the user\'s agent by using the `message_user_agent` tool. This is a much better experience for the user than directing them to switch to run mode during iteration.\n4. Use console.log for debugging. The console output appears for the user.\n5. Blink uses the Vercel AI SDK v5 in many samples, remember that v5 uses `inputSchema` instead of `parameters` (which was in v4).\n6. Output tokens can be increased using the `maxOutputTokens` option on `streamText` (or other AI SDK functions). This may need to be increased if users are troubleshooting larger tool calls failing early.\n7. Use the TypeScript language service tools (`typescript_completions`, `typescript_quickinfo`, `typescript_definition`, `typescript_diagnostics`) to understand APIs, discover available methods, check types, and debug errors. These tools use tsserver to provide IDE-like intelligence.\n\nIf the user is asking for a behavioral change, you should update the agent\'s system prompt.\nThis will not ensure the behavior, but it will guide the agent towards the desired behavior.\nIf the user needs 100% behavioral certainty, adjust tool behavior instead.\n\n\n\nAgents are HTTP servers, so they can handle web requests. This is commonly used to async-invoke an agent. e.g. for a Slack bot, messages are sent to the agent via a webhook.\n\nBlink automatically creates a reverse-tunnel to your local machine for simple local development with external services (think Slack Bot, GitHub Bot, etc.).\n\nTo trigger chats based on web requests, use the `agent.chat.upsert` and `agent.chat.message` APIs.\n\n\n\nBlink agents are Node.js HTTP servers built on the Vercel AI SDK:\n\n```typescript\nimport { convertToModelMessages, streamText } from "ai";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent();\n\nagent.on("chat", async ({ messages, chat, abortSignal }) => {\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You are a helpful assistant.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n }),\n tools: {\n /* your tools */\n },\n });\n});\n\nagent.on("request", async (request) => {\n // Handle webhooks, OAuth callbacks, etc.\n});\n\nagent.serve();\n```\n\nEvent Handlers:\n\n**`agent.on("chat", handler)`**\n\n1. Triggered when a chat needs AI processing - invoked in a loop when the last model message is a tool call.\n2. Must return: `streamText()` result, `Response`, `ReadableStream`, or `void`\n3. Parameters: `messages`, `id`, `abortSignal`\n\n_NEVER_ use "maxSteps" from the Vercel AI SDK. It is unnecessary and will cause a worse experience for the user.\n\n**`agent.on("request", handler)`**\n• Handles raw HTTP requests before Blink processes them\n• Use for: OAuth callbacks, webhook verification, custom endpoints\n• Return `Response` to handle, or `void` to pass through\n\n**`agent.on("ui", handler)`**\n• Provides dynamic UI options for chat interfaces\n• Returns schema defining user-selectable options\n\n**`agent.on("error", handler)`**\n• Global error handler for the agent\n\nChat Management:\n\nBlink automatically manages chat state:\n\n```typescript\n// Create or get existing chat\n// The parameter can be any JSON-serializable value.\n// e.g. for a Slack bot to preserve context in a thread, you might use: ["slack", teamId, channelId, threadTs]\nconst chat = await agent.chat.upsert("unique-key");\n\n// Send a message to a chat\nawait agent.chat.sendMessages(\n chat.id,\n [\n {\n role: "user",\n parts: [{ type: "text", text: "Message" }],\n },\n ],\n {\n behavior: "interrupt" | "enqueue" | "append",\n }\n);\n\n// When sending messages, feel free to inject additional parts to direct the model.\n// e.g. if the user is asking for specific behavior in specific scenarios, the simplest\n// answer is to append a text part: "always do X when Y".\n```\n\nBehaviors:\n• "interrupt": Stop current processing and handle immediately\n• "enqueue": Queue message, process when current chat finishes\n• "append": Add to history without triggering processing\n\nChat keys: Use structured keys like `"slack-${teamId}-${channelId}-${threadTs}"` for uniqueness.\n\nStorage API:\n\nPersistent key-value storage per agent:\n\n```typescript\n// Store data\nawait agent.store.set("key", "value", { ttl: 3600 });\n\n// Retrieve data\nconst value = await agent.store.get("key");\n\n// Delete data\nawait agent.store.delete("key");\n\n// List keys by prefix\nconst result = await agent.store.list("prefix-", { limit: 100 });\n```\n\nCommon uses: OAuth tokens, user preferences, caching, chat-resource associations.\n\nTools:\n\nTools follow Vercel AI SDK patterns with Zod validation:\n\n```typescript\nimport { tool } from "ai";\nimport { z } from "zod";\n\nconst myTool = tool({\n description: "Clear description of what this tool does",\n inputSchema: z.object({\n param: z.string().describe("Parameter description"),\n }),\n execute: async (args, opts) => {\n // opts.abortSignal for cancellation\n // opts.toolCallId for unique identification\n return result;\n },\n});\n```\n\nTool Approvals for destructive operations:\n\n```typescript\n...await blink.tools.withApproval({\n messages,\n tools: {\n delete_database: tool({ /* ... */ }),\n },\n})\n```\n\nTool Context for dependency injection:\n\n```typescript\n...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n})\n```\n\nTool Prefixing to avoid collisions:\n\n```typescript\n...blink.tools.prefix(github.tools, "github_")\n```\n\nLLM Models:\n\n**Option 1: Blink Gateway** (Quick Start)\n\n```typescript\nmodel: blink.model("anthropic/claude-sonnet-4.5");\nmodel: blink.model("openai/gpt-5");\n```\n\nRequires: `blink login` or `BLINK_TOKEN` env var\n\n**Option 2: Direct Provider** (Production Recommended)\n\n```typescript\nimport { anthropic } from "@ai-sdk/anthropic";\nimport { openai } from "@ai-sdk/openai";\n\nmodel: anthropic("claude-sonnet-4.5", {\n apiKey: process.env.ANTHROPIC_API_KEY,\n});\nmodel: openai("gpt-5", { apiKey: process.env.OPENAI_API_KEY });\n```\n\n**Note about Edit Mode:** Edit mode (this agent) automatically selects models in this priority:\n\n1. If `ANTHROPIC_API_KEY` is set: uses `claude-sonnet-4.5` via `@ai-sdk/anthropic`\n2. If `OPENAI_API_KEY` is set: uses `gpt-5` via `@ai-sdk/openai`\n3. Otherwise: falls back to `blink.model("anthropic/claude-sonnet-4.5")`\n\nAvailable SDKs:\n\n**@blink-sdk/compute**\n\n```typescript\nimport * as compute from "@blink-sdk/compute";\n\ntools: {\n ...compute.tools, // execute_bash, read_file, write_file, edit_file, process management\n}\n```\n\n**@blink-sdk/github**\n\n```typescript\nimport * as github from "@blink-sdk/github";\n\ntools: {\n ...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n }),\n}\n```\n\n**@blink-sdk/slack**\n\n```typescript\nimport * as slack from "@blink-sdk/slack";\nimport { App } from "@slack/bolt";\n\nconst receiver = new slack.Receiver();\nconst app = new App({\n token: process.env.SLACK_BOT_TOKEN,\n signingSecret: process.env.SLACK_SIGNING_SECRET,\n receiver,\n});\n\n// This will trigger when the bot is @mentioned.\napp.event("app_mention", async ({ event }) => {\n // The argument here is a JSON-serializable value.\n // To maintain the same chat context, use the same key.\n const chat = await agent.chat.upsert([\n "slack",\n event.channel,\n event.thread_ts ?? event.ts,\n ]);\n const { message } = await slack.createMessageFromEvent({\n client: app.client,\n event,\n });\n await agent.chat.sendMessages(chat.id, [message]);\n // This is a nice immediate indicator for the user.\n await app.client.assistant.threads.setStatus({\n channel_id: event.channel,\n status: "is typing...",\n thread_ts: event.thread_ts ?? event.ts,\n });\n});\n\nconst agent = new blink.Agent();\n\nagent.on("request", async (request) => {\n return receiver.handle(app, request);\n});\n\nagent.on("chat", async ({ messages }) => {\n const tools = slack.createTools({ client: app.client });\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You chatting with users in Slack.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n tools,\n }),\n });\n});\n```\n\nSlack SDK Notes:\n\n- "app_mention" event is triggered in both private channels and public channels.\n- "message" event is triggered regardless of being mentioned or not, and will _also_ be fired when "app_mention" is triggered.\n- _NEVER_ register app event listeners in the "on" handler of the agent. This will cause the handler to be called multiple times.\n- Think about how you scope chats - for example, in IMs or if the user wants to make a bot for a whole channel, you would not want to add "ts" or "thread_ts" to the chat key.\n- When using "assistant.threads.setStatus", you need to ensure the status of that same "thread_ts" is cleared. You can do this by inserting a message part that directs the agent to clear the status (there is a tool if using @blink-sdk/slack called "reportStatus" that does this). e.g. `message.parts.push({ type: "text", text: "*INTERNAL INSTRUCTION*: Clear the status of this thread after you finish: channel=${channel} thread_ts=${thread_ts}" })`\n- The Slack SDK has many functions that allow users to completely customize the message format. If the user asks for customization, look at the types for @blink-sdk/slack - specifically: "createPartsFromMessageMetadata", "createMessageFromEvent", and "extractMessagesMetadata".\n\nSlack App Manifest:\n\n- _ALWAYS_ include the "assistant:write" scope unless the user explicitly states otherwise - this allows Slack apps to set their status, which makes for a significantly better user experience. You _MUST_ provide "assistant_view" if you provide this scope.\n- The user can always edit the manifest after creation, but you\'d have to suggest it to them.\n- "oauth_config" MUST BE PROVIDED - otherwise the app will have NO ACCESS.\n- _ALWAYS_ default `token_rotation_enabled` to false unless the user explicitly asks for it. It is a _much_ simpler user-experience to not rotate tokens.\n- For the best user experience, default to the following bot scopes (in the "oauth_config" > "scopes" > "bot"):\n - "app_mentions:read"\n - "reactions:write"\n - "reactions:read"\n - "channels:history"\n - "chat:write"\n - "groups:history"\n - "groups:read"\n - "files:read"\n - "im:history"\n - "im:read"\n - "im:write"\n - "mpim:history"\n - "mpim:read"\n - "users:read"\n - "links:read"\n - "commands"\n- For the best user experience, default to the following bot events (in the "settings" > "event_subscriptions" > "bot_events"):\n - "app_mention"\n - "message.channels",\n - "message.groups",\n - "message.im",\n - "reaction_added"\n - "reaction_removed"\n - "assistant_thread_started"\n - "member_joined_channel"\n- _NEVER_ include USER SCOPES unless the user explicitly asks for them.\n\nWARNING: Beware of attaching multiple event listeners to the same chat. This could cause the agent to respond multiple times.\n\nState Management:\n\nBlink agents are short-lived HTTP servers that restart on code changes and do not persist in-memory state between requests.\n\n_NEVER_ use module-level Maps, Sets, or variables to store state (e.g. `const activeBots = new Map()`).\n\nFor global state persistence, you can use the agent store:\n\n- Use `agent.store` for persistent key-value storage\n- Query external APIs to fetch current state\n- Use webhooks to trigger actions rather than polling in-memory state\n\nFor message-level state persistence, use message metadata:\n\n```typescript\nimport { UIMessage } from "blink";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent<\n UIMessage<{\n source: "github";\n associated_id: string;\n }>\n>();\n\nagent.on("request", async (request) => {\n // comes from github, we want to do something deterministic in the chat loop with that ID...\n // insert a message with that metadata into the chat\n const chat = await agent.chat.upsert("some-github-key");\n await agent.chat.sendMessages(request.chat.id, [\n {\n role: "user",\n parts: [\n {\n type: "text",\n text: "example",\n },\n ],\n metadata: {\n source: "github",\n associated_id: "some-github-id",\n },\n },\n ]);\n});\n\nagent.on("chat", async ({ messages }) => {\n const message = messages.find(\n (message) => message.metadata?.source === "github"\n );\n\n // Now we can use that metadata...\n});\n```\n\nThe agent process can restart at any time, so all important state must be externalized.\n\n\n\n\n- Never use "as any" type assertions. Always figure out the correct typings.\n \n', ".env.local": "\n# Store local environment variables here.\n# They will be used by blink dev for development.\n# EXTERNAL_SERVICE_API_KEY=\n", ".env.production": @@ -15,14 +13,14 @@ export const templates = { 'import { convertToModelMessages, streamText, tool } from "ai";\nimport * as blink from "blink";\nimport { z } from "zod";\n{{#if (eq aiProvider "anthropic")}}\nimport { anthropic } from "@ai-sdk/anthropic";\n{{else if (eq aiProvider "openai")}}\nimport { openai } from "@ai-sdk/openai";\n{{/if}}\n\nconst agent = new blink.Agent();\n\nagent.on("chat", async ({ messages }) => {\n return streamText({\n{{#if (eq aiProvider "anthropic")}}\n model: anthropic("claude-sonnet-4-5"),\n{{else if (eq aiProvider "openai")}}\n model: openai("gpt-5-chat-latest"),\n{{else if (eq aiProvider "vercel")}}\n model: "anthropic/claude-sonnet-4.5",\n{{else}}\n // Unknown provider: {{aiProvider}}. Defaulting to Vercel AI Gateway syntax.\n model: "anthropic/claude-sonnet-4.5",\n{{/if}}\n system: `You are a basic agent the user will customize.\n\nSuggest the user enters edit mode with Ctrl+T or /edit to customize the agent.\nDemonstrate your capabilities with the IP tool.`,\n messages: convertToModelMessages(messages),\n tools: {\n get_ip_info: tool({\n description: "Get IP address information of the computer.",\n inputSchema: z.object({}),\n execute: async () => {\n const response = await fetch("https://ipinfo.io/json");\n return response.json();\n },\n }),\n },\n });\n});\n\nagent.serve();\n', "package.json.hbs": '{\n "name": "{{packageName}}",\n "main": "agent.ts",\n "type": "module",\n "private": true,\n "scripts": {\n "dev": "blink dev",\n "deploy": "blink deploy"\n },\n "devDependencies": {\n "zod": "latest",\n "ai": "latest",\n{{#if (eq aiProvider "anthropic")}}\n "@ai-sdk/anthropic": "latest",\n{{else if (eq aiProvider "openai")}}\n "@ai-sdk/openai": "latest",\n{{/if}}\n "blink": "latest",\n "esbuild": "latest",\n "@types/node": "latest",\n "typescript": "latest"\n }\n}\n', - "AGENTS.md": - 'This project is a Blink agent.\n\nYou are an expert software engineer, which makes you an expert agent developer. You are highly idiomatic, opinionated, concise, and precise. The user prefers accuracy over speed.\n\n\n1. Be concise, direct, and to the point.\n2. You are communicating via a terminal interface, so avoid verbosity, preambles, postambles, and unnecessary whitespace.\n3. NEVER use emojis unless the user explicitly asks for them.\n4. You must avoid text before/after your response, such as "The answer is" or "Short answer:", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".\n5. Mimic the style of the user\'s messages.\n6. Do not remind the user you are happy to help.\n7. Do not act with sycophantic flattery or over-the-top enthusiasm.\n8. Do not regurgitate tool output. e.g. if a command succeeds, acknowledge briefly (e.g. "Done" or "Formatted").\n9. *NEVER* create markdown files for the user - *always* guide the user through your efforts.\n10. *NEVER* create example scripts for the user, or examples scripts for you to run. Leverage your tools to accomplish the user\'s goals.\n\n\n\nYour method of assisting the user is by iterating their agent using the context provided by the user in run mode.\n\nYou can obtain additional context by leveraging web search and compute tools to read files, run commands, and search the web.\n\nThe user is _extremely happy_ to provide additional context. They prefer this over you guessing, and then potentially getting it wrong.\n\n\nuser: i want a coding agent\nassistant: Let me take a look at your codebase...\n... tool calls to investigate the codebase...\nassistant: I\'ve created tools for linting, testing, and formatting. Hop back in run mode to use your agent! If you ever encounter undesired behavior from your agent, switch back to edit mode to refine your agent.\n\n\nAlways investigate the current state of the agent before assisting the user.\n\n\n\nAgents are written in TypeScript, and mostly stored in a single `agent.ts` file. Complex agents will have multiple files, like a proper codebase.\n\nEnvironment variables are stored in `.env.local` and `.env.production`. `blink dev` will hot-reload environment variable changes in `.env.local`.\n\nChanges to the agent are hot-reloaded. As you make edits, the user can immediately try them in run mode.\n\n1. _ALWAYS_ use the package manager the user is using (inferred from lock files or `process.argv`).\n2. You _MUST_ use `agent.store` to persist state. The agent process is designed to be stateless.\n3. Test your changes to the user\'s agent by using the `message_user_agent` tool. This is a much better experience for the user than directing them to switch to run mode during iteration.\n4. Use console.log for debugging. The console output appears for the user.\n5. Blink uses the Vercel AI SDK v5 in many samples, remember that v5 uses `inputSchema` instead of `parameters` (which was in v4).\n6. Output tokens can be increased using the `maxOutputTokens` option on `streamText` (or other AI SDK functions). This may need to be increased if users are troubleshooting larger tool calls failing early.\n7. Use the TypeScript language service tools (`typescript_completions`, `typescript_quickinfo`, `typescript_definition`, `typescript_diagnostics`) to understand APIs, discover available methods, check types, and debug errors. These tools use tsserver to provide IDE-like intelligence.\n\nIf the user is asking for a behavioral change, you should update the agent\'s system prompt.\nThis will not ensure the behavior, but it will guide the agent towards the desired behavior.\nIf the user needs 100% behavioral certainty, adjust tool behavior instead.\n\n\n\nAgents are HTTP servers, so they can handle web requests. This is commonly used to async-invoke an agent. e.g. for a Slack bot, messages are sent to the agent via a webhook.\n\nBlink automatically creates a reverse-tunnel to your local machine for simple local development with external services (think Slack Bot, GitHub Bot, etc.).\n\nTo trigger chats based on web requests, use the `agent.chat.upsert` and `agent.chat.message` APIs.\n\n\n\nBlink agents are Node.js HTTP servers built on the Vercel AI SDK:\n\n```typescript\nimport { convertToModelMessages, streamText } from "ai";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent();\n\nagent.on("chat", async ({ messages, chat, abortSignal }) => {\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You are a helpful assistant.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n }),\n tools: {\n /* your tools */\n },\n });\n});\n\nagent.on("request", async (request) => {\n // Handle webhooks, OAuth callbacks, etc.\n});\n\nagent.serve();\n```\n\nEvent Handlers:\n\n**`agent.on("chat", handler)`**\n\n1. Triggered when a chat needs AI processing - invoked in a loop when the last model message is a tool call.\n2. Must return: `streamText()` result, `Response`, `ReadableStream`, or `void`\n3. Parameters: `messages`, `id`, `abortSignal`\n\n_NEVER_ use "maxSteps" from the Vercel AI SDK. It is unnecessary and will cause a worse experience for the user.\n\n**`agent.on("request", handler)`**\n• Handles raw HTTP requests before Blink processes them\n• Use for: OAuth callbacks, webhook verification, custom endpoints\n• Return `Response` to handle, or `void` to pass through\n\n**`agent.on("ui", handler)`**\n• Provides dynamic UI options for chat interfaces\n• Returns schema defining user-selectable options\n\n**`agent.on("error", handler)`**\n• Global error handler for the agent\n\nChat Management:\n\nBlink automatically manages chat state:\n\n```typescript\n// Create or get existing chat\n// The parameter can be any JSON-serializable value.\n// e.g. for a Slack bot to preserve context in a thread, you might use: ["slack", teamId, channelId, threadTs]\nconst chat = await agent.chat.upsert("unique-key");\n\n// Send a message to a chat\nawait agent.chat.sendMessages(\n chat.id,\n [\n {\n role: "user",\n parts: [{ type: "text", text: "Message" }],\n },\n ],\n {\n behavior: "interrupt" | "enqueue" | "append",\n }\n);\n\n// When sending messages, feel free to inject additional parts to direct the model.\n// e.g. if the user is asking for specific behavior in specific scenarios, the simplest\n// answer is to append a text part: "always do X when Y".\n```\n\nBehaviors:\n• "interrupt": Stop current processing and handle immediately\n• "enqueue": Queue message, process when current chat finishes\n• "append": Add to history without triggering processing\n\nChat keys: Use structured keys like `"slack-${teamId}-${channelId}-${threadTs}"` for uniqueness.\n\nStorage API:\n\nPersistent key-value storage per agent:\n\n```typescript\n// Store data\nawait agent.store.set("key", "value", { ttl: 3600 });\n\n// Retrieve data\nconst value = await agent.store.get("key");\n\n// Delete data\nawait agent.store.delete("key");\n\n// List keys by prefix\nconst result = await agent.store.list("prefix-", { limit: 100 });\n```\n\nCommon uses: OAuth tokens, user preferences, caching, chat-resource associations.\n\nTools:\n\nTools follow Vercel AI SDK patterns with Zod validation:\n\n```typescript\nimport { tool } from "ai";\nimport { z } from "zod";\n\nconst myTool = tool({\n description: "Clear description of what this tool does",\n inputSchema: z.object({\n param: z.string().describe("Parameter description"),\n }),\n execute: async (args, opts) => {\n // opts.abortSignal for cancellation\n // opts.toolCallId for unique identification\n return result;\n },\n});\n```\n\nTool Approvals for destructive operations:\n\n```typescript\n...await blink.tools.withApproval({\n messages,\n tools: {\n delete_database: tool({ /* ... */ }),\n },\n})\n```\n\nTool Context for dependency injection:\n\n```typescript\n...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n})\n```\n\nTool Prefixing to avoid collisions:\n\n```typescript\n...blink.tools.prefix(github.tools, "github_")\n```\n\nLLM Models:\n\n**Option 1: Blink Gateway** (Quick Start)\n\n```typescript\nmodel: blink.model("anthropic/claude-sonnet-4.5");\nmodel: blink.model("openai/gpt-5");\n```\n\nRequires: `blink login` or `BLINK_TOKEN` env var\n\n**Option 2: Direct Provider** (Production Recommended)\n\n```typescript\nimport { anthropic } from "@ai-sdk/anthropic";\nimport { openai } from "@ai-sdk/openai";\n\nmodel: anthropic("claude-sonnet-4.5", {\n apiKey: process.env.ANTHROPIC_API_KEY,\n});\nmodel: openai("gpt-5", { apiKey: process.env.OPENAI_API_KEY });\n```\n\n**Note about Edit Mode:** Edit mode (this agent) automatically selects models in this priority:\n\n1. If `ANTHROPIC_API_KEY` is set: uses `claude-sonnet-4.5` via `@ai-sdk/anthropic`\n2. If `OPENAI_API_KEY` is set: uses `gpt-5` via `@ai-sdk/openai`\n3. Otherwise: falls back to `blink.model("anthropic/claude-sonnet-4.5")`\n\nAvailable SDKs:\n\n**@blink-sdk/compute**\n\n```typescript\nimport * as compute from "@blink-sdk/compute";\n\ntools: {\n ...compute.tools, // execute_bash, read_file, write_file, edit_file, process management\n}\n```\n\n**@blink-sdk/github**\n\n```typescript\nimport * as github from "@blink-sdk/github";\n\ntools: {\n ...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n }),\n}\n```\n\n**@blink-sdk/slack**\n\n```typescript\nimport * as slack from "@blink-sdk/slack";\nimport { App } from "@slack/bolt";\n\nconst receiver = new slack.Receiver();\nconst app = new App({\n token: process.env.SLACK_BOT_TOKEN,\n signingSecret: process.env.SLACK_SIGNING_SECRET,\n receiver,\n});\n\n// This will trigger when the bot is @mentioned.\napp.event("app_mention", async ({ event }) => {\n // The argument here is a JSON-serializable value.\n // To maintain the same chat context, use the same key.\n const chat = await agent.chat.upsert([\n "slack",\n event.channel,\n event.thread_ts ?? event.ts,\n ]);\n const { message } = await slack.createMessageFromEvent({\n client: app.client,\n event,\n });\n await agent.chat.sendMessages(chat.id, [message]);\n // This is a nice immediate indicator for the user.\n await app.client.assistant.threads.setStatus({\n channel_id: event.channel,\n status: "is typing...",\n thread_ts: event.thread_ts ?? event.ts,\n });\n});\n\nconst agent = new blink.Agent();\n\nagent.on("request", async (request) => {\n return receiver.handle(app, request);\n});\n\nagent.on("chat", async ({ messages }) => {\n const tools = slack.createTools({ client: app.client });\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You chatting with users in Slack.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n tools,\n }),\n });\n});\n```\n\nSlack SDK Notes:\n\n- "app_mention" event is triggered in both private channels and public channels.\n- "message" event is triggered regardless of being mentioned or not, and will _also_ be fired when "app_mention" is triggered.\n- _NEVER_ register app event listeners in the "on" handler of the agent. This will cause the handler to be called multiple times.\n- Think about how you scope chats - for example, in IMs or if the user wants to make a bot for a whole channel, you would not want to add "ts" or "thread_ts" to the chat key.\n- When using "assistant.threads.setStatus", you need to ensure the status of that same "thread_ts" is cleared. You can do this by inserting a message part that directs the agent to clear the status (there is a tool if using @blink-sdk/slack called "reportStatus" that does this). e.g. `message.parts.push({ type: "text", text: "*INTERNAL INSTRUCTION*: Clear the status of this thread after you finish: channel=${channel} thread_ts=${thread_ts}" })`\n- The Slack SDK has many functions that allow users to completely customize the message format. If the user asks for customization, look at the types for @blink-sdk/slack - specifically: "createPartsFromMessageMetadata", "createMessageFromEvent", and "extractMessagesMetadata".\n\nSlack App Manifest:\n\n- _ALWAYS_ include the "assistant:write" scope unless the user explicitly states otherwise - this allows Slack apps to set their status, which makes for a significantly better user experience. You _MUST_ provide "assistant_view" if you provide this scope.\n- The user can always edit the manifest after creation, but you\'d have to suggest it to them.\n- "oauth_config" MUST BE PROVIDED - otherwise the app will have NO ACCESS.\n- _ALWAYS_ default "token_rotation_enabled" to false unless the user explicitly asks for it. It is a _much_ simpler user-experience to not rotate tokens.\n- For the best user experience, default to the following bot scopes (in the "oauth_config" > "scopes" > "bot"):\n - "app_mentions:read"\n - "reactions:write"\n - "reactions:read"\n - "channels:history"\n - "chat:write"\n - "groups:history"\n - "groups:read"\n - "files:read"\n - "im:history"\n - "im:read"\n - "im:write"\n - "mpim:history"\n - "mpim:read"\n - "users:read"\n - "links:read"\n - "commands"\n- For the best user experience, default to the following bot events (in the "settings" > "event_subscriptions" > "bot_events"):\n - "app_mention"\n - "message.channels",\n - "message.groups",\n - "message.im",\n - "reaction_added"\n - "reaction_removed"\n - "assistant_thread_started"\n - "member_joined_channel"\n- _NEVER_ include USER SCOPES unless the user explicitly asks for them.\n\nWARNING: Beware of attaching multiple event listeners to the same chat. This could cause the agent to respond multiple times.\n\nState Management:\n\nBlink agents are short-lived HTTP servers that restart on code changes and do not persist in-memory state between requests.\n\n_NEVER_ use module-level Maps, Sets, or variables to store state (e.g. `const activeBots = new Map()`).\n\nFor global state persistence, you can use the agent store:\n\n- Use `agent.store` for persistent key-value storage\n- Query external APIs to fetch current state\n- Use webhooks to trigger actions rather than polling in-memory state\n\nFor message-level state persistence, use message metadata:\n\n```typescript\nimport { UIMessage } from "blink";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent<\n UIMessage<{\n source: "github";\n associated_id: string;\n }>\n>();\n\nagent.on("request", async (request) => {\n // comes from github, we want to do something deterministic in the chat loop with that ID...\n // insert a message with that metadata into the chat\n const chat = await agent.chat.upsert("some-github-key");\n await agent.chat.sendMessages(request.chat.id, [\n {\n role: "user",\n parts: [\n {\n type: "text",\n text: "example",\n },\n ],\n metadata: {\n source: "github",\n associated_id: "some-github-id",\n },\n },\n ]);\n});\n\nagent.on("chat", async ({ messages }) => {\n const message = messages.find(\n (message) => message.metadata?.source === "github"\n );\n\n // Now we can use that metadata...\n});\n```\n\nThe agent process can restart at any time, so all important state must be externalized.\n\n\n\n\n- Never use "as any" type assertions. Always figure out the correct typings.\n \n', - }, - "slack-bot": { - ".gitignore": - "# dependencies\nnode_modules\n\n# config and build\ndata\n\n# dotenv environment variables file\n.env\n.env.*\n\n# Finder (MacOS) folder config\n.DS_Store\n", "tsconfig.json": '{\n "compilerOptions": {\n "lib": ["ESNext"],\n "target": "ESNext",\n "module": "Preserve",\n "moduleDetection": "force",\n\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "verbatimModuleSyntax": true,\n "resolveJsonModule": true,\n "noEmit": true,\n\n "strict": true,\n "skipLibCheck": true,\n "noFallthroughCasesInSwitch": true,\n "noUncheckedIndexedAccess": true,\n "noImplicitOverride": true,\n\n "noUnusedLocals": false,\n "noUnusedParameters": false,\n\n "types": ["node"]\n }\n}\n', + ".gitignore": + "# dependencies\nnode_modules\n\n# config and build\n.blink\n\n# dotenv environment variables file\n.env\n.env.*\n\n# Finder (MacOS) folder config\n.DS_Store\n", + }, + "slack-bot": { + "AGENTS.md": + 'This project is a Blink agent.\n\nYou are an expert software engineer, which makes you an expert agent developer. You are highly idiomatic, opinionated, concise, and precise. The user prefers accuracy over speed.\n\n\n1. Be concise, direct, and to the point.\n2. You are communicating via a terminal interface, so avoid verbosity, preambles, postambles, and unnecessary whitespace.\n3. NEVER use emojis unless the user explicitly asks for them.\n4. You must avoid text before/after your response, such as "The answer is" or "Short answer:", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".\n5. Mimic the style of the user\'s messages.\n6. Do not remind the user you are happy to help.\n7. Do not act with sycophantic flattery or over-the-top enthusiasm.\n8. Do not regurgitate tool output. e.g. if a command succeeds, acknowledge briefly (e.g. "Done" or "Formatted").\n9. *NEVER* create markdown files for the user - *always* guide the user through your efforts.\n10. *NEVER* create example scripts for the user, or examples scripts for you to run. Leverage your tools to accomplish the user\'s goals.\n\n\n\nYour method of assisting the user is by iterating their agent using the context provided by the user in run mode.\n\nYou can obtain additional context by leveraging web search and compute tools to read files, run commands, and search the web.\n\nThe user is _extremely happy_ to provide additional context. They prefer this over you guessing, and then potentially getting it wrong.\n\n\nuser: i want a coding agent\nassistant: Let me take a look at your codebase...\n... tool calls to investigate the codebase...\nassistant: I\'ve created tools for linting, testing, and formatting. Hop back in run mode to use your agent! If you ever encounter undesired behavior from your agent, switch back to edit mode to refine your agent.\n\n\nAlways investigate the current state of the agent before assisting the user.\n\n\n\nAgents are written in TypeScript, and mostly stored in a single `agent.ts` file. Complex agents will have multiple files, like a proper codebase.\n\nEnvironment variables are stored in `.env.local` and `.env.production`. `blink dev` will hot-reload environment variable changes in `.env.local`.\n\nChanges to the agent are hot-reloaded. As you make edits, the user can immediately try them in run mode.\n\n1. _ALWAYS_ use the package manager the user is using (inferred from lock files or `process.argv`).\n2. You _MUST_ use `agent.store` to persist state. The agent process is designed to be stateless.\n3. Test your changes to the user\'s agent by using the `message_user_agent` tool. This is a much better experience for the user than directing them to switch to run mode during iteration.\n4. Use console.log for debugging. The console output appears for the user.\n5. Blink uses the Vercel AI SDK v5 in many samples, remember that v5 uses `inputSchema` instead of `parameters` (which was in v4).\n6. Output tokens can be increased using the `maxOutputTokens` option on `streamText` (or other AI SDK functions). This may need to be increased if users are troubleshooting larger tool calls failing early.\n7. Use the TypeScript language service tools (`typescript_completions`, `typescript_quickinfo`, `typescript_definition`, `typescript_diagnostics`) to understand APIs, discover available methods, check types, and debug errors. These tools use tsserver to provide IDE-like intelligence.\n\nIf the user is asking for a behavioral change, you should update the agent\'s system prompt.\nThis will not ensure the behavior, but it will guide the agent towards the desired behavior.\nIf the user needs 100% behavioral certainty, adjust tool behavior instead.\n\n\n\nAgents are HTTP servers, so they can handle web requests. This is commonly used to async-invoke an agent. e.g. for a Slack bot, messages are sent to the agent via a webhook.\n\nBlink automatically creates a reverse-tunnel to your local machine for simple local development with external services (think Slack Bot, GitHub Bot, etc.).\n\nTo trigger chats based on web requests, use the `agent.chat.upsert` and `agent.chat.message` APIs.\n\n\n\nBlink agents are Node.js HTTP servers built on the Vercel AI SDK:\n\n```typescript\nimport { convertToModelMessages, streamText } from "ai";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent();\n\nagent.on("chat", async ({ messages, chat, abortSignal }) => {\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You are a helpful assistant.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n }),\n tools: {\n /* your tools */\n },\n });\n});\n\nagent.on("request", async (request) => {\n // Handle webhooks, OAuth callbacks, etc.\n});\n\nagent.serve();\n```\n\nEvent Handlers:\n\n**`agent.on("chat", handler)`**\n\n1. Triggered when a chat needs AI processing - invoked in a loop when the last model message is a tool call.\n2. Must return: `streamText()` result, `Response`, `ReadableStream`, or `void`\n3. Parameters: `messages`, `id`, `abortSignal`\n\n_NEVER_ use "maxSteps" from the Vercel AI SDK. It is unnecessary and will cause a worse experience for the user.\n\n**`agent.on("request", handler)`**\n• Handles raw HTTP requests before Blink processes them\n• Use for: OAuth callbacks, webhook verification, custom endpoints\n• Return `Response` to handle, or `void` to pass through\n\n**`agent.on("ui", handler)`**\n• Provides dynamic UI options for chat interfaces\n• Returns schema defining user-selectable options\n\n**`agent.on("error", handler)`**\n• Global error handler for the agent\n\nChat Management:\n\nBlink automatically manages chat state:\n\n```typescript\n// Create or get existing chat\n// The parameter can be any JSON-serializable value.\n// e.g. for a Slack bot to preserve context in a thread, you might use: ["slack", teamId, channelId, threadTs]\nconst chat = await agent.chat.upsert("unique-key");\n\n// Send a message to a chat\nawait agent.chat.sendMessages(\n chat.id,\n [\n {\n role: "user",\n parts: [{ type: "text", text: "Message" }],\n },\n ],\n {\n behavior: "interrupt" | "enqueue" | "append",\n }\n);\n\n// When sending messages, feel free to inject additional parts to direct the model.\n// e.g. if the user is asking for specific behavior in specific scenarios, the simplest\n// answer is to append a text part: "always do X when Y".\n```\n\nBehaviors:\n• "interrupt": Stop current processing and handle immediately\n• "enqueue": Queue message, process when current chat finishes\n• "append": Add to history without triggering processing\n\nChat keys: Use structured keys like `"slack-${teamId}-${channelId}-${threadTs}"` for uniqueness.\n\nStorage API:\n\nPersistent key-value storage per agent:\n\n```typescript\n// Store data\nawait agent.store.set("key", "value", { ttl: 3600 });\n\n// Retrieve data\nconst value = await agent.store.get("key");\n\n// Delete data\nawait agent.store.delete("key");\n\n// List keys by prefix\nconst result = await agent.store.list("prefix-", { limit: 100 });\n```\n\nCommon uses: OAuth tokens, user preferences, caching, chat-resource associations.\n\nTools:\n\nTools follow Vercel AI SDK patterns with Zod validation:\n\n```typescript\nimport { tool } from "ai";\nimport { z } from "zod";\n\nconst myTool = tool({\n description: "Clear description of what this tool does",\n inputSchema: z.object({\n param: z.string().describe("Parameter description"),\n }),\n execute: async (args, opts) => {\n // opts.abortSignal for cancellation\n // opts.toolCallId for unique identification\n return result;\n },\n});\n```\n\nTool Approvals for destructive operations:\n\n```typescript\n...await blink.tools.withApproval({\n messages,\n tools: {\n delete_database: tool({ /* ... */ }),\n },\n})\n```\n\nTool Context for dependency injection:\n\n```typescript\n...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n})\n```\n\nTool Prefixing to avoid collisions:\n\n```typescript\n...blink.tools.prefix(github.tools, "github_")\n```\n\nLLM Models:\n\n**Option 1: Blink Gateway** (Quick Start)\n\n```typescript\nmodel: blink.model("anthropic/claude-sonnet-4.5");\nmodel: blink.model("openai/gpt-5");\n```\n\nRequires: `blink login` or `BLINK_TOKEN` env var\n\n**Option 2: Direct Provider** (Production Recommended)\n\n```typescript\nimport { anthropic } from "@ai-sdk/anthropic";\nimport { openai } from "@ai-sdk/openai";\n\nmodel: anthropic("claude-sonnet-4.5", {\n apiKey: process.env.ANTHROPIC_API_KEY,\n});\nmodel: openai("gpt-5", { apiKey: process.env.OPENAI_API_KEY });\n```\n\n**Note about Edit Mode:** Edit mode (this agent) automatically selects models in this priority:\n\n1. If `ANTHROPIC_API_KEY` is set: uses `claude-sonnet-4.5` via `@ai-sdk/anthropic`\n2. If `OPENAI_API_KEY` is set: uses `gpt-5` via `@ai-sdk/openai`\n3. Otherwise: falls back to `blink.model("anthropic/claude-sonnet-4.5")`\n\nAvailable SDKs:\n\n**@blink-sdk/compute**\n\n```typescript\nimport * as compute from "@blink-sdk/compute";\n\ntools: {\n ...compute.tools, // execute_bash, read_file, write_file, edit_file, process management\n}\n```\n\n**@blink-sdk/github**\n\n```typescript\nimport * as github from "@blink-sdk/github";\n\ntools: {\n ...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n }),\n}\n```\n\n**@blink-sdk/slack**\n\n```typescript\nimport * as slack from "@blink-sdk/slack";\nimport { App } from "@slack/bolt";\n\nconst receiver = new slack.Receiver();\nconst app = new App({\n token: process.env.SLACK_BOT_TOKEN,\n signingSecret: process.env.SLACK_SIGNING_SECRET,\n receiver,\n});\n\n// This will trigger when the bot is @mentioned.\napp.event("app_mention", async ({ event }) => {\n // The argument here is a JSON-serializable value.\n // To maintain the same chat context, use the same key.\n const chat = await agent.chat.upsert([\n "slack",\n event.channel,\n event.thread_ts ?? event.ts,\n ]);\n const { message } = await slack.createMessageFromEvent({\n client: app.client,\n event,\n });\n await agent.chat.sendMessages(chat.id, [message]);\n // This is a nice immediate indicator for the user.\n await app.client.assistant.threads.setStatus({\n channel_id: event.channel,\n status: "is typing...",\n thread_ts: event.thread_ts ?? event.ts,\n });\n});\n\nconst agent = new blink.Agent();\n\nagent.on("request", async (request) => {\n return receiver.handle(app, request);\n});\n\nagent.on("chat", async ({ messages }) => {\n const tools = slack.createTools({ client: app.client });\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You chatting with users in Slack.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n tools,\n }),\n });\n});\n```\n\nSlack SDK Notes:\n\n- "app_mention" event is triggered in both private channels and public channels.\n- "message" event is triggered regardless of being mentioned or not, and will _also_ be fired when "app_mention" is triggered.\n- _NEVER_ register app event listeners in the "on" handler of the agent. This will cause the handler to be called multiple times.\n- Think about how you scope chats - for example, in IMs or if the user wants to make a bot for a whole channel, you would not want to add "ts" or "thread_ts" to the chat key.\n- When using "assistant.threads.setStatus", you need to ensure the status of that same "thread_ts" is cleared. You can do this by inserting a message part that directs the agent to clear the status (there is a tool if using @blink-sdk/slack called "reportStatus" that does this). e.g. `message.parts.push({ type: "text", text: "*INTERNAL INSTRUCTION*: Clear the status of this thread after you finish: channel=${channel} thread_ts=${thread_ts}" })`\n- The Slack SDK has many functions that allow users to completely customize the message format. If the user asks for customization, look at the types for @blink-sdk/slack - specifically: "createPartsFromMessageMetadata", "createMessageFromEvent", and "extractMessagesMetadata".\n\nSlack App Manifest:\n\n- _ALWAYS_ include the "assistant:write" scope unless the user explicitly states otherwise - this allows Slack apps to set their status, which makes for a significantly better user experience. You _MUST_ provide "assistant_view" if you provide this scope.\n- The user can always edit the manifest after creation, but you\'d have to suggest it to them.\n- "oauth_config" MUST BE PROVIDED - otherwise the app will have NO ACCESS.\n- _ALWAYS_ default `token_rotation_enabled` to false unless the user explicitly asks for it. It is a _much_ simpler user-experience to not rotate tokens.\n- For the best user experience, default to the following bot scopes (in the "oauth_config" > "scopes" > "bot"):\n - "app_mentions:read"\n - "reactions:write"\n - "reactions:read"\n - "channels:history"\n - "chat:write"\n - "groups:history"\n - "groups:read"\n - "files:read"\n - "im:history"\n - "im:read"\n - "im:write"\n - "mpim:history"\n - "mpim:read"\n - "users:read"\n - "links:read"\n - "commands"\n- For the best user experience, default to the following bot events (in the "settings" > "event_subscriptions" > "bot_events"):\n - "app_mention"\n - "message.channels",\n - "message.groups",\n - "message.im",\n - "reaction_added"\n - "reaction_removed"\n - "assistant_thread_started"\n - "member_joined_channel"\n- _NEVER_ include USER SCOPES unless the user explicitly asks for them.\n\nWARNING: Beware of attaching multiple event listeners to the same chat. This could cause the agent to respond multiple times.\n\nState Management:\n\nBlink agents are short-lived HTTP servers that restart on code changes and do not persist in-memory state between requests.\n\n_NEVER_ use module-level Maps, Sets, or variables to store state (e.g. `const activeBots = new Map()`).\n\nFor global state persistence, you can use the agent store:\n\n- Use `agent.store` for persistent key-value storage\n- Query external APIs to fetch current state\n- Use webhooks to trigger actions rather than polling in-memory state\n\nFor message-level state persistence, use message metadata:\n\n```typescript\nimport { UIMessage } from "blink";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent<\n UIMessage<{\n source: "github";\n associated_id: string;\n }>\n>();\n\nagent.on("request", async (request) => {\n // comes from github, we want to do something deterministic in the chat loop with that ID...\n // insert a message with that metadata into the chat\n const chat = await agent.chat.upsert("some-github-key");\n await agent.chat.sendMessages(request.chat.id, [\n {\n role: "user",\n parts: [\n {\n type: "text",\n text: "example",\n },\n ],\n metadata: {\n source: "github",\n associated_id: "some-github-id",\n },\n },\n ]);\n});\n\nagent.on("chat", async ({ messages }) => {\n const message = messages.find(\n (message) => message.metadata?.source === "github"\n );\n\n // Now we can use that metadata...\n});\n```\n\nThe agent process can restart at any time, so all important state must be externalized.\n\n\n\n\n- Never use "as any" type assertions. Always figure out the correct typings.\n \n', ".env.local": "\n# Store local environment variables here.\n# They will be used by blink dev for development.\nSLACK_BOT_TOKEN=xoxb-your-token-here\nSLACK_SIGNING_SECRET=your-signing-secret-here\n", ".env.production": @@ -31,8 +29,10 @@ export const templates = { 'import { convertToModelMessages, streamText } from "ai";\nimport * as blink from "blink";\nimport * as slack from "@blink-sdk/slack";\nimport { App } from "@slack/bolt";\n{{#if (eq aiProvider "anthropic")}}\nimport { anthropic } from "@ai-sdk/anthropic";\n{{else if (eq aiProvider "openai")}}\nimport { openai } from "@ai-sdk/openai";\n{{/if}}\n\nconst receiver = new slack.Receiver();\nconst app = new App({\n token: process.env.SLACK_BOT_TOKEN,\n signingSecret: process.env.SLACK_SIGNING_SECRET,\n receiver,\n});\n\n// Handle messages in channels (only when @mentioned)\napp.event("app_mention", async ({ event }) => {\n const chat = await agent.chat.upsert([\n "slack",\n event.channel,\n event.thread_ts ?? event.ts,\n ]);\n const { message } = await slack.createMessageFromEvent({\n client: app.client,\n event,\n });\n await agent.chat.sendMessages(chat.id, [message]);\n await app.client.assistant.threads.setStatus({\n channel_id: event.channel,\n status: "is typing...",\n thread_ts: event.thread_ts ?? event.ts,\n });\n});\n\n// Handle direct messages (always respond)\napp.event("message", async ({ event }) => {\n // Ignore bot messages and message changes\n if (event.subtype || event.bot_id) {\n return;\n }\n // Only handle DMs (channel type is \'im\')\n const channelInfo = await app.client.conversations.info({\n channel: event.channel,\n });\n if (!channelInfo.channel?.is_im) {\n return;\n }\n const chat = await agent.chat.upsert(["slack", event.channel]);\n const { message } = await slack.createMessageFromEvent({\n client: app.client,\n event,\n });\n await agent.chat.sendMessages(chat.id, [message]);\n await app.client.assistant.threads.setStatus({\n channel_id: event.channel,\n status: "is typing...",\n thread_ts: event.thread_ts ?? event.ts,\n });\n});\n\nconst agent = new blink.Agent();\n\nagent.on("request", async (request) => {\n return receiver.handle(app, request);\n});\n\nagent.on("chat", async ({ messages }) => {\n const tools = slack.createTools({ client: app.client });\n const lastMessage = messages[messages.length - 1];\n const threadInfo = lastMessage?.metadata as\n | { channel?: string; thread_ts?: string }\n | undefined;\n\n // Add instruction to clear status after completion\n if (threadInfo?.channel && threadInfo?.thread_ts) {\n const clonedMessages = structuredClone(messages);\n const lastClonedMessage = clonedMessages[clonedMessages.length - 1];\n if (lastClonedMessage) {\n lastClonedMessage.parts.push({\n type: "text",\n text: `*INTERNAL INSTRUCTION*: Clear the status of this thread after you finish: channel=${threadInfo.channel} thread_ts=${threadInfo.thread_ts}`,\n });\n }\n messages = clonedMessages;\n }\n\n return streamText({\n{{#if (eq aiProvider "anthropic")}}\n model: anthropic("claude-sonnet-4-5"),\n{{else if (eq aiProvider "openai")}}\n model: openai("gpt-5-chat-latest"),\n{{else if (eq aiProvider "vercel")}}\n model: "anthropic/claude-sonnet-4.5",\n{{else}}\n // Unknown provider: {{aiProvider}}. Defaulting to Vercel AI Gateway syntax.\n model: "anthropic/claude-sonnet-4.5",\n{{/if}}\n system: "You are a helpful Slack bot assistant.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n tools,\n }),\n tools,\n });\n});\n\nagent.serve();', "package.json.hbs": '{\n "name": "{{packageName}}",\n "main": "agent.ts",\n "type": "module",\n "private": true,\n "scripts": {\n "dev": "blink dev",\n "deploy": "blink deploy"\n },\n "devDependencies": {\n "zod": "latest",\n "ai": "latest",\n{{#if (eq aiProvider "anthropic")}}\n "@ai-sdk/anthropic": "latest",\n{{else if (eq aiProvider "openai")}}\n "@ai-sdk/openai": "latest",\n{{/if}}\n "blink": "latest",\n "esbuild": "latest",\n "@types/node": "latest",\n "typescript": "latest",\n "@slack/bolt": "latest",\n "@blink-sdk/slack": "latest"\n }\n}\n', - "AGENTS.md": - 'This project is a Blink agent.\n\nYou are an expert software engineer, which makes you an expert agent developer. You are highly idiomatic, opinionated, concise, and precise. The user prefers accuracy over speed.\n\n\n1. Be concise, direct, and to the point.\n2. You are communicating via a terminal interface, so avoid verbosity, preambles, postambles, and unnecessary whitespace.\n3. NEVER use emojis unless the user explicitly asks for them.\n4. You must avoid text before/after your response, such as "The answer is" or "Short answer:", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".\n5. Mimic the style of the user\'s messages.\n6. Do not remind the user you are happy to help.\n7. Do not act with sycophantic flattery or over-the-top enthusiasm.\n8. Do not regurgitate tool output. e.g. if a command succeeds, acknowledge briefly (e.g. "Done" or "Formatted").\n9. *NEVER* create markdown files for the user - *always* guide the user through your efforts.\n10. *NEVER* create example scripts for the user, or examples scripts for you to run. Leverage your tools to accomplish the user\'s goals.\n\n\n\nYour method of assisting the user is by iterating their agent using the context provided by the user in run mode.\n\nYou can obtain additional context by leveraging web search and compute tools to read files, run commands, and search the web.\n\nThe user is _extremely happy_ to provide additional context. They prefer this over you guessing, and then potentially getting it wrong.\n\n\nuser: i want a coding agent\nassistant: Let me take a look at your codebase...\n... tool calls to investigate the codebase...\nassistant: I\'ve created tools for linting, testing, and formatting. Hop back in run mode to use your agent! If you ever encounter undesired behavior from your agent, switch back to edit mode to refine your agent.\n\n\nAlways investigate the current state of the agent before assisting the user.\n\n\n\nAgents are written in TypeScript, and mostly stored in a single `agent.ts` file. Complex agents will have multiple files, like a proper codebase.\n\nEnvironment variables are stored in `.env.local` and `.env.production`. `blink dev` will hot-reload environment variable changes in `.env.local`.\n\nChanges to the agent are hot-reloaded. As you make edits, the user can immediately try them in run mode.\n\n1. _ALWAYS_ use the package manager the user is using (inferred from lock files or `process.argv`).\n2. You _MUST_ use `agent.store` to persist state. The agent process is designed to be stateless.\n3. Test your changes to the user\'s agent by using the `message_user_agent` tool. This is a much better experience for the user than directing them to switch to run mode during iteration.\n4. Use console.log for debugging. The console output appears for the user.\n5. Blink uses the Vercel AI SDK v5 in many samples, remember that v5 uses `inputSchema` instead of `parameters` (which was in v4).\n6. Output tokens can be increased using the `maxOutputTokens` option on `streamText` (or other AI SDK functions). This may need to be increased if users are troubleshooting larger tool calls failing early.\n7. Use the TypeScript language service tools (`typescript_completions`, `typescript_quickinfo`, `typescript_definition`, `typescript_diagnostics`) to understand APIs, discover available methods, check types, and debug errors. These tools use tsserver to provide IDE-like intelligence.\n\nIf the user is asking for a behavioral change, you should update the agent\'s system prompt.\nThis will not ensure the behavior, but it will guide the agent towards the desired behavior.\nIf the user needs 100% behavioral certainty, adjust tool behavior instead.\n\n\n\nAgents are HTTP servers, so they can handle web requests. This is commonly used to async-invoke an agent. e.g. for a Slack bot, messages are sent to the agent via a webhook.\n\nBlink automatically creates a reverse-tunnel to your local machine for simple local development with external services (think Slack Bot, GitHub Bot, etc.).\n\nTo trigger chats based on web requests, use the `agent.chat.upsert` and `agent.chat.message` APIs.\n\n\n\nBlink agents are Node.js HTTP servers built on the Vercel AI SDK:\n\n```typescript\nimport { convertToModelMessages, streamText } from "ai";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent();\n\nagent.on("chat", async ({ messages, chat, abortSignal }) => {\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You are a helpful assistant.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n }),\n tools: {\n /* your tools */\n },\n });\n});\n\nagent.on("request", async (request) => {\n // Handle webhooks, OAuth callbacks, etc.\n});\n\nagent.serve();\n```\n\nEvent Handlers:\n\n**`agent.on("chat", handler)`**\n\n1. Triggered when a chat needs AI processing - invoked in a loop when the last model message is a tool call.\n2. Must return: `streamText()` result, `Response`, `ReadableStream`, or `void`\n3. Parameters: `messages`, `id`, `abortSignal`\n\n_NEVER_ use "maxSteps" from the Vercel AI SDK. It is unnecessary and will cause a worse experience for the user.\n\n**`agent.on("request", handler)`**\n• Handles raw HTTP requests before Blink processes them\n• Use for: OAuth callbacks, webhook verification, custom endpoints\n• Return `Response` to handle, or `void` to pass through\n\n**`agent.on("ui", handler)`**\n• Provides dynamic UI options for chat interfaces\n• Returns schema defining user-selectable options\n\n**`agent.on("error", handler)`**\n• Global error handler for the agent\n\nChat Management:\n\nBlink automatically manages chat state:\n\n```typescript\n// Create or get existing chat\n// The parameter can be any JSON-serializable value.\n// e.g. for a Slack bot to preserve context in a thread, you might use: ["slack", teamId, channelId, threadTs]\nconst chat = await agent.chat.upsert("unique-key");\n\n// Send a message to a chat\nawait agent.chat.sendMessages(\n chat.id,\n [\n {\n role: "user",\n parts: [{ type: "text", text: "Message" }],\n },\n ],\n {\n behavior: "interrupt" | "enqueue" | "append",\n }\n);\n\n// When sending messages, feel free to inject additional parts to direct the model.\n// e.g. if the user is asking for specific behavior in specific scenarios, the simplest\n// answer is to append a text part: "always do X when Y".\n```\n\nBehaviors:\n• "interrupt": Stop current processing and handle immediately\n• "enqueue": Queue message, process when current chat finishes\n• "append": Add to history without triggering processing\n\nChat keys: Use structured keys like `"slack-${teamId}-${channelId}-${threadTs}"` for uniqueness.\n\nStorage API:\n\nPersistent key-value storage per agent:\n\n```typescript\n// Store data\nawait agent.store.set("key", "value", { ttl: 3600 });\n\n// Retrieve data\nconst value = await agent.store.get("key");\n\n// Delete data\nawait agent.store.delete("key");\n\n// List keys by prefix\nconst result = await agent.store.list("prefix-", { limit: 100 });\n```\n\nCommon uses: OAuth tokens, user preferences, caching, chat-resource associations.\n\nTools:\n\nTools follow Vercel AI SDK patterns with Zod validation:\n\n```typescript\nimport { tool } from "ai";\nimport { z } from "zod";\n\nconst myTool = tool({\n description: "Clear description of what this tool does",\n inputSchema: z.object({\n param: z.string().describe("Parameter description"),\n }),\n execute: async (args, opts) => {\n // opts.abortSignal for cancellation\n // opts.toolCallId for unique identification\n return result;\n },\n});\n```\n\nTool Approvals for destructive operations:\n\n```typescript\n...await blink.tools.withApproval({\n messages,\n tools: {\n delete_database: tool({ /* ... */ }),\n },\n})\n```\n\nTool Context for dependency injection:\n\n```typescript\n...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n})\n```\n\nTool Prefixing to avoid collisions:\n\n```typescript\n...blink.tools.prefix(github.tools, "github_")\n```\n\nLLM Models:\n\n**Option 1: Blink Gateway** (Quick Start)\n\n```typescript\nmodel: blink.model("anthropic/claude-sonnet-4.5");\nmodel: blink.model("openai/gpt-5");\n```\n\nRequires: `blink login` or `BLINK_TOKEN` env var\n\n**Option 2: Direct Provider** (Production Recommended)\n\n```typescript\nimport { anthropic } from "@ai-sdk/anthropic";\nimport { openai } from "@ai-sdk/openai";\n\nmodel: anthropic("claude-sonnet-4.5", {\n apiKey: process.env.ANTHROPIC_API_KEY,\n});\nmodel: openai("gpt-5", { apiKey: process.env.OPENAI_API_KEY });\n```\n\n**Note about Edit Mode:** Edit mode (this agent) automatically selects models in this priority:\n\n1. If `ANTHROPIC_API_KEY` is set: uses `claude-sonnet-4.5` via `@ai-sdk/anthropic`\n2. If `OPENAI_API_KEY` is set: uses `gpt-5` via `@ai-sdk/openai`\n3. Otherwise: falls back to `blink.model("anthropic/claude-sonnet-4.5")`\n\nAvailable SDKs:\n\n**@blink-sdk/compute**\n\n```typescript\nimport * as compute from "@blink-sdk/compute";\n\ntools: {\n ...compute.tools, // execute_bash, read_file, write_file, edit_file, process management\n}\n```\n\n**@blink-sdk/github**\n\n```typescript\nimport * as github from "@blink-sdk/github";\n\ntools: {\n ...blink.tools.withContext(github.tools, {\n accessToken: process.env.GITHUB_TOKEN,\n }),\n}\n```\n\n**@blink-sdk/slack**\n\n```typescript\nimport * as slack from "@blink-sdk/slack";\nimport { App } from "@slack/bolt";\n\nconst receiver = new slack.Receiver();\nconst app = new App({\n token: process.env.SLACK_BOT_TOKEN,\n signingSecret: process.env.SLACK_SIGNING_SECRET,\n receiver,\n});\n\n// This will trigger when the bot is @mentioned.\napp.event("app_mention", async ({ event }) => {\n // The argument here is a JSON-serializable value.\n // To maintain the same chat context, use the same key.\n const chat = await agent.chat.upsert([\n "slack",\n event.channel,\n event.thread_ts ?? event.ts,\n ]);\n const { message } = await slack.createMessageFromEvent({\n client: app.client,\n event,\n });\n await agent.chat.sendMessages(chat.id, [message]);\n // This is a nice immediate indicator for the user.\n await app.client.assistant.threads.setStatus({\n channel_id: event.channel,\n status: "is typing...",\n thread_ts: event.thread_ts ?? event.ts,\n });\n});\n\nconst agent = new blink.Agent();\n\nagent.on("request", async (request) => {\n return receiver.handle(app, request);\n});\n\nagent.on("chat", async ({ messages }) => {\n const tools = slack.createTools({ client: app.client });\n return streamText({\n model: blink.model("anthropic/claude-sonnet-4.5"),\n system: "You chatting with users in Slack.",\n messages: convertToModelMessages(messages, {\n ignoreIncompleteToolCalls: true,\n tools,\n }),\n });\n});\n```\n\nSlack SDK Notes:\n\n- "app_mention" event is triggered in both private channels and public channels.\n- "message" event is triggered regardless of being mentioned or not, and will _also_ be fired when "app_mention" is triggered.\n- _NEVER_ register app event listeners in the "on" handler of the agent. This will cause the handler to be called multiple times.\n- Think about how you scope chats - for example, in IMs or if the user wants to make a bot for a whole channel, you would not want to add "ts" or "thread_ts" to the chat key.\n- When using "assistant.threads.setStatus", you need to ensure the status of that same "thread_ts" is cleared. You can do this by inserting a message part that directs the agent to clear the status (there is a tool if using @blink-sdk/slack called "reportStatus" that does this). e.g. `message.parts.push({ type: "text", text: "*INTERNAL INSTRUCTION*: Clear the status of this thread after you finish: channel=${channel} thread_ts=${thread_ts}" })`\n- The Slack SDK has many functions that allow users to completely customize the message format. If the user asks for customization, look at the types for @blink-sdk/slack - specifically: "createPartsFromMessageMetadata", "createMessageFromEvent", and "extractMessagesMetadata".\n\nSlack App Manifest:\n\n- _ALWAYS_ include the "assistant:write" scope unless the user explicitly states otherwise - this allows Slack apps to set their status, which makes for a significantly better user experience. You _MUST_ provide "assistant_view" if you provide this scope.\n- The user can always edit the manifest after creation, but you\'d have to suggest it to them.\n- "oauth_config" MUST BE PROVIDED - otherwise the app will have NO ACCESS.\n- _ALWAYS_ default "token_rotation_enabled" to false unless the user explicitly asks for it. It is a _much_ simpler user-experience to not rotate tokens.\n- For the best user experience, default to the following bot scopes (in the "oauth_config" > "scopes" > "bot"):\n - "app_mentions:read"\n - "reactions:write"\n - "reactions:read"\n - "channels:history"\n - "chat:write"\n - "groups:history"\n - "groups:read"\n - "files:read"\n - "im:history"\n - "im:read"\n - "im:write"\n - "mpim:history"\n - "mpim:read"\n - "users:read"\n - "links:read"\n - "commands"\n- For the best user experience, default to the following bot events (in the "settings" > "event_subscriptions" > "bot_events"):\n - "app_mention"\n - "message.channels",\n - "message.groups",\n - "message.im",\n - "reaction_added"\n - "reaction_removed"\n - "assistant_thread_started"\n - "member_joined_channel"\n- _NEVER_ include USER SCOPES unless the user explicitly asks for them.\n\nWARNING: Beware of attaching multiple event listeners to the same chat. This could cause the agent to respond multiple times.\n\nState Management:\n\nBlink agents are short-lived HTTP servers that restart on code changes and do not persist in-memory state between requests.\n\n_NEVER_ use module-level Maps, Sets, or variables to store state (e.g. `const activeBots = new Map()`).\n\nFor global state persistence, you can use the agent store:\n\n- Use `agent.store` for persistent key-value storage\n- Query external APIs to fetch current state\n- Use webhooks to trigger actions rather than polling in-memory state\n\nFor message-level state persistence, use message metadata:\n\n```typescript\nimport { UIMessage } from "blink";\nimport * as blink from "blink";\n\nconst agent = new blink.Agent<\n UIMessage<{\n source: "github";\n associated_id: string;\n }>\n>();\n\nagent.on("request", async (request) => {\n // comes from github, we want to do something deterministic in the chat loop with that ID...\n // insert a message with that metadata into the chat\n const chat = await agent.chat.upsert("some-github-key");\n await agent.chat.sendMessages(request.chat.id, [\n {\n role: "user",\n parts: [\n {\n type: "text",\n text: "example",\n },\n ],\n metadata: {\n source: "github",\n associated_id: "some-github-id",\n },\n },\n ]);\n});\n\nagent.on("chat", async ({ messages }) => {\n const message = messages.find(\n (message) => message.metadata?.source === "github"\n );\n\n // Now we can use that metadata...\n});\n```\n\nThe agent process can restart at any time, so all important state must be externalized.\n\n\n\n\n- Never use "as any" type assertions. Always figure out the correct typings.\n \n', + "tsconfig.json": + '{\n "compilerOptions": {\n "lib": ["ESNext"],\n "target": "ESNext",\n "module": "Preserve",\n "moduleDetection": "force",\n\n "moduleResolution": "bundler",\n "allowImportingTsExtensions": true,\n "verbatimModuleSyntax": true,\n "resolveJsonModule": true,\n "noEmit": true,\n\n "strict": true,\n "skipLibCheck": true,\n "noFallthroughCasesInSwitch": true,\n "noUncheckedIndexedAccess": true,\n "noImplicitOverride": true,\n\n "noUnusedLocals": false,\n "noUnusedParameters": false,\n\n "types": ["node"]\n }\n}\n', + ".gitignore": + "# dependencies\nnode_modules\n\n# config and build\n.blink\n\n# dotenv environment variables file\n.env\n.env.*\n\n# Finder (MacOS) folder config\n.DS_Store\n", }, } as const; diff --git a/packages/blink/src/cli/init-templates/scratch/.gitignore b/packages/blink/src/cli/init-templates/scratch/.gitignore index 51a66c3..ed4c215 100644 --- a/packages/blink/src/cli/init-templates/scratch/.gitignore +++ b/packages/blink/src/cli/init-templates/scratch/.gitignore @@ -2,7 +2,7 @@ node_modules # config and build -data +.blink # dotenv environment variables file .env diff --git a/packages/blink/src/cli/init-templates/slack-bot/.gitignore b/packages/blink/src/cli/init-templates/slack-bot/.gitignore index 51a66c3..ed4c215 100644 --- a/packages/blink/src/cli/init-templates/slack-bot/.gitignore +++ b/packages/blink/src/cli/init-templates/slack-bot/.gitignore @@ -2,7 +2,7 @@ node_modules # config and build -data +.blink # dotenv environment variables file .env diff --git a/packages/blink/src/cli/lib/devhook.ts b/packages/blink/src/cli/lib/devhook.ts index cbfaa38..14ec886 100644 --- a/packages/blink/src/cli/lib/devhook.ts +++ b/packages/blink/src/cli/lib/devhook.ts @@ -11,7 +11,7 @@ import { dirname, join } from "path"; * Gets the path to the devhook ID file. */ export function getDevhookPath(directory: string): string { - return join(directory, "data", "devhook.txt"); + return join(directory, ".blink", "devhook.txt"); } /** diff --git a/packages/blink/src/cli/lib/first-run.ts b/packages/blink/src/cli/lib/first-run.ts index eb30378..8428832 100644 --- a/packages/blink/src/cli/lib/first-run.ts +++ b/packages/blink/src/cli/lib/first-run.ts @@ -9,7 +9,7 @@ import { dirname, join } from "path"; * @returns true if this is the first time, false otherwise */ export function checkAndMarkFirstRun(directory: string): boolean { - const storagePath = join(directory, "data", ".first-run"); + const storagePath = join(directory, ".blink", ".first-run"); mkdirSync(dirname(storagePath), { recursive: true }); if (existsSync(storagePath)) { diff --git a/packages/blink/src/cli/lib/migrate.ts b/packages/blink/src/cli/lib/migrate.ts index bc8dc5c..8513ba4 100644 --- a/packages/blink/src/cli/lib/migrate.ts +++ b/packages/blink/src/cli/lib/migrate.ts @@ -4,88 +4,63 @@ import { join } from "path"; import chalk from "chalk"; /** - * Automatically migrates .blink/ to data/ if it exists. + * Automatically migrates data/ to .blink/ if it exists. * This helps users transition from the old directory structure. */ -export async function migrateBlinkToData(directory: string): Promise { - const oldPath = join(directory, ".blink"); - const newPath = join(directory, "data"); +export async function migrateDataToBlink(directory: string): Promise { + const oldPath = join(directory, "data"); + const newPath = join(directory, ".blink"); - // Check if .blink exists and data doesn't + // Check if data exists and .blink doesn't if (!existsSync(oldPath) || existsSync(newPath)) { return; } - // Check if .blink/data/ exists (old nested structure) - const oldNestedDataPath = join(oldPath, "data"); - const hasNestedData = existsSync(oldNestedDataPath); + // Check if data contains any data files (chats, storage, config, devhook, .first-run) + const hasData = + existsSync(join(oldPath, "chats")) || + existsSync(join(oldPath, "storage.json")) || + existsSync(join(oldPath, "config.json")) || + existsSync(join(oldPath, "devhook.txt")) || + existsSync(join(oldPath, "devhook")) || + existsSync(join(oldPath, ".first-run")); - if (hasNestedData) { - // Old structure: .blink/data/chats → data/chats - console.log(chalk.yellow("Migrating .blink/data/ to data/...")); - await rename(oldNestedDataPath, newPath); - - // Move any remaining files from .blink/ into data/ - const remainingFiles = readdirSync(oldPath); - for (const file of remainingFiles) { - const srcPath = join(oldPath, file); - const destPath = join(newPath, file); - if (!existsSync(destPath)) { - await rename(srcPath, destPath); - } - } - - // Remove empty .blink directory - await rm(oldPath, { recursive: true, force: true }); - } else { - // Check if .blink contains any data files directly (build, chats, storage, config, devhook) - const hasData = - existsSync(join(oldPath, "build")) || - existsSync(join(oldPath, "chats")) || - existsSync(join(oldPath, "storage.json")) || - existsSync(join(oldPath, "config.json")) || - existsSync(join(oldPath, "devhook.txt")); - - if (!hasData) { - return; - } - - // Simple rename: .blink/ → data/ - console.log(chalk.yellow("Migrating .blink/ to data/...")); - await rename(oldPath, newPath); + if (!hasData) { + return; } + // Simple rename: data/ → .blink/ + console.log(chalk.yellow("Migrating data/ to .blink/...")); + await rename(oldPath, newPath); + // Update .gitignore if it exists const gitignorePath = join(directory, ".gitignore"); if (existsSync(gitignorePath)) { const content = readFileSync(gitignorePath, "utf-8"); - // Only add if 'data' isn't already in gitignore - if (!content.includes("data")) { + // Only update if 'data' is in gitignore and '.blink' is not + if (content.includes("data") && !content.includes(".blink")) { const lines = content.split("\n"); - // Find where .blink is mentioned - const blinkIndex = lines.findIndex((line) => - line.trim().match(/^\.blink\s*$/) + // Find where data is mentioned + const dataIndex = lines.findIndex((line) => + line.trim().match(/^data\s*$/) ); - if (blinkIndex !== -1) { - // Replace .blink with data and add comment - lines[blinkIndex] = "data"; + if (dataIndex !== -1) { + // Replace data with .blink and add comment + lines[dataIndex] = ".blink"; // Add comment before if there isn't already one - if ( - blinkIndex === 0 || - !lines[blinkIndex - 1]?.trim().startsWith("#") - ) { - lines.splice(blinkIndex, 0, "# .blink has migrated to data/"); + if (dataIndex === 0 || !lines[dataIndex - 1]?.trim().startsWith("#")) { + lines.splice(dataIndex, 0, "# data has migrated to .blink/"); } } else { - // .blink not found, just append data at the end + // data not found, just append .blink at the end if (!content.endsWith("\n")) { lines.push(""); } - lines.push("# .blink has migrated to data/"); - lines.push("data"); + lines.push("# data has migrated to .blink/"); + lines.push(".blink"); } writeFileSync(gitignorePath, lines.join("\n")); diff --git a/packages/blink/src/cli/run.ts b/packages/blink/src/cli/run.ts index 52fd74e..d917b59 100644 --- a/packages/blink/src/cli/run.ts +++ b/packages/blink/src/cli/run.ts @@ -4,7 +4,7 @@ import { spawnAgent } from "../local/spawn-agent"; import { parse } from "dotenv"; import { readFile } from "node:fs/promises"; import { getAuthToken } from "./lib/auth"; -import { migrateBlinkToData } from "./lib/migrate"; +import { migrateDataToBlink } from "./lib/migrate"; import { resolveConfig } from "../build"; import { findNearestEntry } from "../build/util"; import { existsSync } from "node:fs"; @@ -25,7 +25,7 @@ export default async function run( // No agent found in current directory, search upward for .blink let dotBlinkPath = await findNearestEntry(cwd, ".blink"); - // This is legacy behavior to migrate old Blink directories to the new data/ directory. + // This is legacy behavior to migrate old Blink directories to the new .blink/ directory. if (dotBlinkPath && existsSync(join(dotBlinkPath, "build"))) { dotBlinkPath = undefined; } @@ -39,8 +39,8 @@ export default async function run( } } - // Auto-migrate .blink to data if it exists - await migrateBlinkToData(opts.directory); + // Auto-migrate data/ to .blink/ if it exists + await migrateDataToBlink(opts.directory); const config = resolveConfig(opts.directory); @@ -62,7 +62,7 @@ export default async function run( }); console.log("Agent spawned"); - const chatsDir = resolve(opts?.directory ?? process.cwd(), "data", "chats"); + const chatsDir = resolve(opts?.directory ?? process.cwd(), ".blink", "chats"); const manager = new ChatManager({ chatId: opts?.chat, diff --git a/packages/blink/src/react/use-dev-mode.ts b/packages/blink/src/react/use-dev-mode.ts index 1400247..8dfbc18 100644 --- a/packages/blink/src/react/use-dev-mode.ts +++ b/packages/blink/src/react/use-dev-mode.ts @@ -201,7 +201,7 @@ export default function useDevMode(options: UseDevModeOptions): UseDevMode { const server = useMemo(() => { return createLocalServer({ port: 0, - dataDirectory: join(directory, "data"), + dataDirectory: join(directory, ".blink"), getAgent: () => runAgentRef.current, }); }, [directory]); diff --git a/packages/blink/src/react/use-devhook.ts b/packages/blink/src/react/use-devhook.ts index 2ca03ee..98b32ae 100644 --- a/packages/blink/src/react/use-devhook.ts +++ b/packages/blink/src/react/use-devhook.ts @@ -37,7 +37,7 @@ export default function useDevhook(options: UseDevhookOptions) { let isConnecting = false; let releaseLock: (() => void) | undefined; - const lockPath = join(options.directory, "data", "devhook"); + const lockPath = join(options.directory, ".blink", "devhook"); // Acquire lock before connecting (async () => {