├── .gitignore ├── README.md ├── package.json ├── Claude.mjs ├── GPT.mjs ├── holefill.mjs ├── Chat.mjs ├── refactor.mjs ├── chatsh.mjs ├── aiemu.mjs ├── kindcoder.mjs └── KIND2_GUIDE_AI.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | ab_challenge/ 3 | tmp/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Taelin AI Scripts 2 | ================= 3 | 4 | Some AI scripts I use daily. 5 | 6 | - `holefill`: I use it on VIM to fill code snippets 7 | 8 | - `aiemu`: moved to [here](https://github.com/victorTaelin/aiemu) 9 | 10 | - `chatsh [model]`: like ChatGPT but in the terminal 11 | 12 | TODO: remove `Claude.mjs`/`GPT.mjs` and just use `Ask.mjs` in all files 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taelin-ai-scripts", 3 | "version": "0.1.0", 4 | "description": "Handy AI scripts by Taelin", 5 | "type": "module", 6 | "main": "main.js", 7 | "bin": { 8 | "holefill": "holefill.mjs", 9 | "chatsh": "chatsh.mjs", 10 | "csh": "chatsh.mjs", 11 | "refactor": "refactor.mjs", 12 | "kindcoder": "kindcoder.mjs", 13 | "aiemu": "aiemu.mjs" 14 | }, 15 | "scripts": {}, 16 | "keywords": [ 17 | "cli", 18 | "openai", 19 | "gpt-4" 20 | ], 21 | "author": "", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@anthropic-ai/sdk": "^0.19.1", 25 | "@google/generative-ai": "^0.12.0", 26 | "@portkey-ai/gateway": "^1.4.0", 27 | "gateway": "^1.0.0", 28 | "gpt-tokenizer": "^2.1.2", 29 | "groq-sdk": "^0.3.2", 30 | "openai": "^4.31.0", 31 | "portkey-ai": "^1.3.0", 32 | "punycode": "^2.3.1" 33 | }, 34 | "devDependencies": { 35 | "tsx": "^4.16.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Claude.mjs: -------------------------------------------------------------------------------- 1 | import { Anthropic } from '@anthropic-ai/sdk'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | 5 | async function getAnthropicKey() { 6 | const keyPath = path.join(process.env.HOME, '.config', 'anthropic.token'); 7 | return (await fs.readFile(keyPath, 'utf8')).trim(); 8 | } 9 | 10 | export async function ask({ system, prompt, max_tokens, model = 'claude-3-opus-20240229', temperature = 1, debug = true }) { 11 | const anthropic = new Anthropic({ apiKey: await getAnthropicKey() }); 12 | if (debug) { 13 | const stream = anthropic.messages.stream({ 14 | model, 15 | messages: [{ role: 'user', content: prompt }], 16 | max_tokens: max_tokens || 4096, 17 | temperature, 18 | ...(system && { system }), 19 | }).on('text', (text) => process.stdout.write(text)); 20 | const message = await stream.finalMessage(); 21 | console.log(); // Add a newline at the end 22 | return message.content[0].text; 23 | } else { 24 | const message = await anthropic.messages.create({ 25 | model, 26 | messages: [{ role: 'user', content: prompt }], 27 | max_tokens: max_tokens || 4096, 28 | temperature, 29 | ...(system && { system }), 30 | }); 31 | return message.content[0].text; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GPT.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from "process"; 4 | import OpenAI from "openai"; 5 | import fs from "fs/promises"; 6 | import os from "os"; 7 | import path from "path"; 8 | import { encode } from "gpt-tokenizer/esm/model/davinci-codex"; // tokenizer 9 | 10 | const openai = new OpenAI({apiKey: await get_token()}); 11 | 12 | export async function get_token() { 13 | const tokenPath = path.join(os.homedir(), ".config", "openai.token"); 14 | try { 15 | const token = (await fs.readFile(tokenPath, "utf8")).trim(); 16 | return token; 17 | } catch (err) { 18 | if (err.code === "ENOENT") { 19 | console.error("Error: openai.token file not found in `~/.config/openai.token`."); 20 | console.error("Please make sure the file exists and contains your OpenAI API token."); 21 | } else { 22 | console.error("Error reading openai.token file:", err.message); 23 | } 24 | process.exit(1); 25 | } 26 | } 27 | 28 | export async function ask({system, prompt, model, temperature}) { 29 | const stream = await openai.chat.completions.create({ 30 | model: model || "gpt-4-turbo-2024-04-09", 31 | messages: [ 32 | {role: "system", content: system || "You're a helpful assistant." }, 33 | {role: "user", content: prompt || "What time is it?" } 34 | ], 35 | stream: true, 36 | temperature: temperature || 0, 37 | }); 38 | var result = ""; 39 | for await (const chunk of stream) { 40 | var text = chunk.choices[0]?.delta?.content || ""; 41 | process.stdout.write(text); 42 | result += text; 43 | } 44 | process.stdout.write("\n"); 45 | return result; 46 | } 47 | 48 | export function token_count(inputText) { 49 | // Encode the input string into tokens 50 | const tokens = encode(inputText); 51 | 52 | // Get the number of tokens 53 | const numberOfTokens = tokens.length; 54 | 55 | // Return the number of tokens 56 | return numberOfTokens; 57 | } 58 | -------------------------------------------------------------------------------- /holefill.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { chat, MODELS, tokenCount } from './Chat.mjs'; 3 | import process from "process"; 4 | import fs from 'fs/promises'; 5 | import os from 'os'; 6 | import path from 'path'; 7 | 8 | const system = ` 9 | You are a HOLE FILLER. You are provided with a file containing holes, formatted 10 | as '{{HOLE_NAME}}'. Your TASK is to complete with a string to replace this hole 11 | with, inside a XML tag, including context-aware indentation, if 12 | needed. All completions MUST be truthful, accurate, well-written and correct. 13 | 14 | ## EXAMPLE QUERY: 15 | 16 | 17 | function sum_evens(lim) { 18 | var sum = 0; 19 | for (var i = 0; i < lim; ++i) { 20 | {{FILL_HERE}} 21 | } 22 | return sum; 23 | } 24 | 25 | 26 | ## CORRECT COMPLETION 27 | 28 | if (i % 2 === 0) { 29 | sum += i; 30 | } 31 | 32 | ## EXAMPLE QUERY: 33 | 34 | 35 | def sum_list(lst): 36 | total = 0 37 | for x in lst: 38 | {{FILL_HERE}} 39 | return total 40 | 41 | print sum_list([1, 2, 3]) 42 | 43 | 44 | ## CORRECT COMPLETION: 45 | 46 | total += x 47 | 48 | ## EXAMPLE QUERY: 49 | 50 | 51 | // data Tree a = Node (Tree a) (Tree a) | Leaf a 52 | 53 | // sum :: Tree Int -> Int 54 | // sum (Node lft rgt) = sum lft + sum rgt 55 | // sum (Leaf val) = val 56 | 57 | // convert to TypeScript: 58 | {{FILL_HERE}} 59 | 60 | 61 | ## CORRECT COMPLETION: 62 | 63 | type Tree 64 | = {$:"Node", lft: Tree, rgt: Tree} 65 | | {$:"Leaf", val: T}; 66 | 67 | function sum(tree: Tree): number { 68 | switch (tree.$) { 69 | case "Node": 70 | return sum(tree.lft) + sum(tree.rgt); 71 | case "Leaf": 72 | return tree.val; 73 | } 74 | } 75 | 76 | ## EXAMPLE QUERY: 77 | 78 | The 2nd {{FILL_HERE}} is Saturn. 79 | 80 | ## CORRECT COMPLETION: 81 | 82 | gas giant 83 | 84 | ## EXAMPLE QUERY: 85 | 86 | function hypothenuse(a, b) { 87 | return Math.sqrt({{FILL_HERE}}b ** 2); 88 | } 89 | 90 | ## CORRECT COMPLETION: 91 | 92 | a ** 2 + 93 | 94 | ## IMPORTANT: 95 | 96 | - Answer ONLY with the block. Do NOT include anything outside it. 97 | `; 98 | 99 | var file = process.argv[2]; 100 | var mini = process.argv[3]; 101 | var model = process.argv[4] || "s"; 102 | var ask = chat(model); 103 | 104 | if (!file) { 105 | console.log("Usage: holefill [] []"); 106 | console.log(""); 107 | console.log("This will replace all {{HOLES}} in , using GPT-4 / Claude-3."); 108 | console.log("A shortened file can be used to omit irrelevant parts."); 109 | process.exit(); 110 | } 111 | 112 | var file_code = await fs.readFile(file, 'utf-8'); 113 | var mini_code = mini ? await fs.readFile(mini, 'utf-8') : file_code; 114 | 115 | // Imports context files when //./path_to_file// is present. 116 | var regex = /\/\/\.\/(.*?)\/\//g; 117 | var match; 118 | while ((match = regex.exec(mini_code)) !== null) { 119 | var import_path = path.resolve(path.dirname(file), match[1]); 120 | if (await fs.stat(import_path).then(() => true).catch(() => false)) { 121 | var import_text = await fs.readFile(import_path, 'utf-8'); 122 | console.log("import_file:", match[0]); 123 | mini_code = mini_code.replace(match[0], '\n' + import_text); 124 | } else { 125 | console.log("import_file:", match[0], "ERROR"); 126 | process.exit(1); 127 | } 128 | } 129 | 130 | await fs.writeFile(mini, mini_code, 'utf-8'); 131 | 132 | var tokens = tokenCount(mini_code); 133 | var holes = mini_code.match(/{{\w+}}/g) || []; 134 | 135 | if (holes.length === 0 && mini_code.indexOf("??") !== -1 && (mini_code.match(/\?\?/g) || []).length == 1) { 136 | holes = "??"; 137 | } 138 | 139 | console.log("holes_found:", holes); 140 | console.log("token_count:", tokens); 141 | console.log("model_label:", MODELS[model] || model); 142 | 143 | if (holes === "??") { 144 | console.log("next_filled: ??"); 145 | var prompt = "\n" + mini_code.replace("??", "{{FILL_HERE}}") + "\n"; 146 | var answer = await ask(prompt, {system, model}); 147 | var match = answer.match(/([\s\S]*?)<\/COMPLETION>/); 148 | if (match) { 149 | file_code = file_code.replace("??", match[1]); 150 | } else { 151 | console.error("Error: Could not find tags in the AI's response."); 152 | process.exit(1); 153 | } 154 | } else { 155 | for (let hole of holes) { 156 | console.log("next_filled: " + hole + "..."); 157 | var prompt = "\n" + mini_code + "\n"; 158 | var answer = await ask(prompt, {system, model}); 159 | var match = answer.match(/([\s\S]*?)<\/COMPLETION>/); 160 | if (match) { 161 | file_code = file_code.replace(hole, match[1]); 162 | } else { 163 | console.error("Error: Could not find tags in the AI's response for hole: " + hole); 164 | process.exit(1); 165 | } 166 | } 167 | } 168 | 169 | await fs.writeFile(file, file_code, 'utf-8'); 170 | -------------------------------------------------------------------------------- /Chat.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import { OpenAI } from "openai"; 5 | import { Anthropic } from '@anthropic-ai/sdk'; 6 | import { Groq } from "groq-sdk"; 7 | import { GoogleGenerativeAI } from "@google/generative-ai"; 8 | import { encode } from "gpt-tokenizer/esm/model/davinci-codex"; // tokenizer 9 | 10 | // Map of model shortcodes to full model names 11 | export const MODELS = { 12 | g: 'gpt-4o', 13 | G: 'gpt-4-32k-0314', 14 | h: 'claude-3-haiku-20240307', 15 | s: 'claude-3-5-sonnet-20240620', 16 | o: 'claude-3-opus-20240229', 17 | l: 'llama3-8b-8192', 18 | L: 'llama3-70b-8192', 19 | i: 'gemini-1.5-flash-latest', 20 | I: 'gemini-1.5-pro-latest' 21 | }; 22 | 23 | // Factory function to create a stateful OpenAI chat 24 | export function openAIChat(clientClass) { 25 | const messages = []; 26 | 27 | async function ask(userMessage, { system, model, temperature = 0.0, max_tokens = 4096, stream = true }) { 28 | model = MODELS[model] || model; 29 | const client = new clientClass({ apiKey: await getToken(clientClass.name.toLowerCase()) }); 30 | 31 | if (messages.length === 0) { 32 | messages.push({ role: "system", content: system }); 33 | } 34 | 35 | messages.push({ role: "user", content: userMessage }); 36 | 37 | const params = { messages, model, temperature, max_tokens, stream }; 38 | 39 | let result = ""; 40 | const response = await client.chat.completions.create(params); 41 | 42 | for await (const chunk of response) { 43 | const text = chunk.choices[0]?.delta?.content || ""; 44 | process.stdout.write(text); 45 | result += text; 46 | } 47 | 48 | messages.push({ role: 'assistant', content: result }); 49 | 50 | return result; 51 | } 52 | 53 | return ask; 54 | } 55 | 56 | // Factory function to create a stateful Anthropic chat 57 | export function anthropicChat(clientClass) { 58 | const messages = []; 59 | 60 | async function ask(userMessage, { system, model, temperature = 0.0, max_tokens = 4096, stream = true }) { 61 | model = MODELS[model] || model; 62 | const client = new clientClass({ apiKey: await getToken(clientClass.name.toLowerCase()) }); 63 | 64 | messages.push({ role: "user", content: userMessage }); 65 | 66 | const params = { system, model, temperature, max_tokens, stream }; 67 | 68 | let result = ""; 69 | const response = client.messages 70 | .stream({ ...params, messages }) 71 | .on('text', (text) => { 72 | process.stdout.write(text); 73 | result += text; 74 | }); 75 | await response.finalMessage(); 76 | 77 | messages.push({ role: 'assistant', content: result }); 78 | 79 | return result; 80 | } 81 | 82 | return ask; 83 | } 84 | 85 | export function geminiChat(clientClass) { 86 | const messages = []; 87 | 88 | async function ask(userMessage, { system, model, temperature = 0.0, max_tokens = 4096, stream = true }) { 89 | model = MODELS[model] || model; 90 | const client = new clientClass(await getToken(clientClass.name.toLowerCase())); 91 | 92 | const generationConfig = { 93 | maxOutputTokens: max_tokens, 94 | temperature, 95 | }; 96 | 97 | const chat = client.getGenerativeModel({ model, systemInstruction: system, generationConfig }) 98 | .startChat({ history: messages }); 99 | 100 | messages.push({ role: "user", parts: [{ text: userMessage }] }); 101 | 102 | let result = ""; 103 | if (stream) { 104 | const response = await chat.sendMessageStream(userMessage); 105 | for await (const chunk of response.stream) { 106 | const text = chunk.text(); 107 | process.stdout.write(text); 108 | result += text; 109 | } 110 | } else { 111 | const response = await chat.sendMessage(userMessage); 112 | result = (await response.response).text(); 113 | } 114 | 115 | messages.push({ role: 'model', parts: [{ text: result }] }); 116 | 117 | return result; 118 | } 119 | 120 | return ask; 121 | } 122 | 123 | // Generic asker function that dispatches to the correct asker based on the model name 124 | export function chat(model) { 125 | model = MODELS[model] || model; 126 | if (model.startsWith('gpt')) { 127 | return openAIChat(OpenAI); 128 | } else if (model.startsWith('claude')) { 129 | return anthropicChat(Anthropic); 130 | } else if (model.startsWith('llama')) { 131 | return openAIChat(Groq); 132 | } else if (model.startsWith('gemini')) { 133 | return geminiChat(GoogleGenerativeAI); 134 | } else { 135 | throw new Error(`Unsupported model: ${model}`); 136 | } 137 | } 138 | 139 | // Utility function to read the API token for a given vendor 140 | async function getToken(vendor) { 141 | const tokenPath = path.join(os.homedir(), '.config', `${vendor}.token`); 142 | try { 143 | return (await fs.readFile(tokenPath, 'utf8')).trim(); 144 | } catch (err) { 145 | console.error(`Error reading ${vendor}.token file:`, err.message); 146 | process.exit(1); 147 | } 148 | } 149 | 150 | export function tokenCount(inputText) { 151 | // Encode the input string into tokens 152 | const tokens = encode(inputText); 153 | 154 | // Get the number of tokens 155 | const numberOfTokens = tokens.length; 156 | 157 | // Return the number of tokens 158 | return numberOfTokens; 159 | } 160 | -------------------------------------------------------------------------------- /refactor.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs/promises'; 4 | import os from 'os'; 5 | import path from 'path'; 6 | import process from "process"; 7 | import { chat, MODELS, tokenCount } from './Chat.mjs'; 8 | import { exec } from 'child_process'; 9 | import { promisify } from 'util'; 10 | 11 | const execAsync = promisify(exec); 12 | 13 | // System prompt for the AI model, defining its role and behavior 14 | const system = ` 15 | You are a file refactoring tool. 16 | 17 | - INPUT: You will receive a FILE and a change REQUEST. 18 | 19 | - OUTPUT: You must answer with the changed file inside tags. 20 | 21 | # GUIDE FOR REFACTORING 22 | 23 | 1. Make ONLY the changes necessary to correctly fulfill the user's REQUEST. 24 | 2. Do NOT fix, remove, complete, or alter any parts unrelated to the REQUEST. 25 | 3. Do not include any additional comments, explanations, or text outside of the RESULT tags. 26 | 4. NEVER assume information you don't have. ALWAYS request files to make sure. 27 | 5. Preserve the same indentation and style of the current file. 28 | 6. Be precise and careful in your modifications. 29 | 30 | # GUIDE FOR NAVIGATING 31 | 32 | In some cases, you WILL need additional context to fulfill a request. When that is the case, do NOT attempt to refactor the file immediatelly. Instead, ask for additional files inside tags, as follows: 33 | 34 | 35 | ["./README.md", "./src/some_file.js", "./src/some_dir"] 36 | 37 | 38 | You can ask for information as many times as you want. 39 | 40 | # GUIDE FOR AGDA STYLE 41 | 42 | Every Agda file MUST: 43 | 44 | 1. Start with a "module" declaration. 45 | 46 | 2. Must have ONLY ONE top-level definition. 47 | 48 | 3. Export ALL its in-scope values> 49 | 50 | Example file: 51 | 52 | \`\`\` 53 | module Term where 54 | 55 | {-# NO_TERMINATION_CHECK #-} 56 | 57 | data Term : Set where 58 | var : Term 59 | abs : (Term → Term) → Term 60 | app : Term → Term → Term 61 | 62 | open Term public using (Term; var; abs; app) 63 | \`\`\` 64 | 65 | # EXAMPLE 66 | 67 | Below is a complete example of how you should interact with the user. 68 | 69 | ## User: 70 | 71 | 72 | Bool.agda 73 | Nat.agda 74 | is_zero.agda 75 | 76 | 77 | 78 | module is_zero where 79 | open import Nat 80 | open import Bool 81 | 82 | is_zero : Nat -> Bool 83 | is_zero n = ? 84 | 85 | 86 | 87 | case-split on n 88 | 89 | 90 | ## You: 91 | 92 | 93 | ["./Nat.agda"] 94 | 95 | 96 | ## User: 97 | 98 | 99 | module Nat where 100 | 101 | data Nat : Set where 102 | zero : Nat 103 | succ : Nat → Nat 104 | 105 | open Nat public using (Nat; zero; succ) 106 | 107 | 108 | ## You: 109 | 110 | 111 | module is_zero where 112 | 113 | open import Nat 114 | open import Bool 115 | 116 | is_zero : Nat -> Bool 117 | is_zero zero = ? 118 | is_zero (succ n) = ? 119 | 120 | 121 | ## User: 122 | 123 | 124 | complete it 125 | 126 | 127 | ## You: 128 | 129 | 130 | ["/.Bool.agda"] 131 | 132 | 133 | ## User: 134 | 135 | 136 | module Bool where 137 | 138 | data Bool : Set where 139 | true : Bool 140 | false : Bool 141 | 142 | open Bool public using (Bool; true; false) 143 | 144 | 145 | ## You: 146 | 147 | 148 | module is_zero where 149 | 150 | open import Nat 151 | open import Bool 152 | 153 | is_zero : Nat -> Bool 154 | is_zero zero = true 155 | is_zero (succ n) = false 156 | 157 | `; 158 | 159 | // Main function to handle the refactoring process 160 | async function main() { 161 | // Check for correct usage and parse command-line arguments 162 | if (process.argv.length < 3) { 163 | console.log("Usage: refactor [] [--check]"); 164 | process.exit(1); 165 | } 166 | 167 | const file = process.argv[2]; 168 | const request = process.argv[3]; 169 | const model = process.argv[4] || "s"; 170 | const check = process.argv.includes("--check"); 171 | 172 | // Initialize the chat function with the specified model 173 | const ask = chat(model); 174 | 175 | // Get directory and file information 176 | const dir = path.dirname(file); 177 | const fileContent = await fs.readFile(file, 'utf-8'); 178 | const dirContent = await fs.readdir(dir); 179 | 180 | // Prepare initial input for the AI 181 | let aiInput = `\n${dirContent.join('\n')}\n\n\n\n${fileContent}\n\n\n\n${request}\n`; 182 | 183 | // If --check flag is present, perform initial type check 184 | if (check) { 185 | const initialCheck = await typeCheck(file); 186 | aiInput += `\n\n\n${initialCheck || 'No errors.'}\n`; 187 | } 188 | 189 | // Main interaction loop with the AI 190 | while (true) { 191 | console.log(""); 192 | const aiOutput = await ask(aiInput, { system, model }); 193 | 194 | // Handle AI's request for additional information 195 | if (aiOutput.includes("")) { 196 | const showMatch = aiOutput.match(/([\s\S]*?)<\/SHOW>/); 197 | if (showMatch) { 198 | const filesToShow = JSON.parse(showMatch[1]); 199 | let showContent = ""; 200 | for (const fileToShow of filesToShow) { 201 | const fullPath = path.resolve(dir, fileToShow); 202 | if (await fs.stat(fullPath).then(stat => stat.isDirectory())) { 203 | const dirContent = await fs.readdir(fullPath); 204 | showContent += `\n${dirContent.join('\n')}\n\n`; 205 | } else { 206 | const content = await fs.readFile(fullPath, 'utf-8'); 207 | showContent += `\n${content}\n\n`; 208 | } 209 | } 210 | aiInput = showContent; 211 | } 212 | } 213 | // Handle AI's refactoring result 214 | else if (aiOutput.includes("")) { 215 | const resultMatch = aiOutput.match(/([\s\S]*?)<\/RESULT>/); 216 | if (resultMatch) { 217 | const newContent = resultMatch[1]; 218 | await fs.writeFile(file, newContent.trim(), 'utf-8'); 219 | console.log("\nFile refactored successfully."); 220 | 221 | // If --check flag is present, perform type check on the refactored file 222 | if (check) { 223 | const checkResult = await typeCheck(file); 224 | if (checkResult) { 225 | aiInput = `\n${newContent.trim()}\n\n\n\nFix this file.\n\n\n\n${checkResult}\n`; 226 | continue; 227 | } 228 | } 229 | break; 230 | } 231 | } 232 | } 233 | } 234 | 235 | // Function to perform type checking based on file extension 236 | async function typeCheck(file) { 237 | const ext = path.extname(file); 238 | let cmd; 239 | switch (ext) { 240 | case '.agda': 241 | cmd = `agda-check ${file}`; 242 | break; 243 | case '.kind2': 244 | cmd = `kind2 check ${file}`; 245 | break; 246 | case '.c': 247 | cmd = `gcc -fsyntax-only ${file}`; 248 | break; 249 | case '.ts': 250 | cmd = `tsc --noEmit ${file}`; 251 | break; 252 | case '.hs': 253 | cmd = `ghc -fno-code ${file}`; 254 | break; 255 | default: 256 | return null; 257 | } 258 | 259 | try { 260 | var result = await execAsync(cmd); 261 | return result.stderr || result.stdout; 262 | } catch (error) { 263 | return error.stderr; 264 | } 265 | } 266 | 267 | // Run the main function and handle any errors 268 | main().catch(console.error); 269 | -------------------------------------------------------------------------------- /chatsh.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import readline from 'readline'; 4 | import { exec } from 'child_process'; 5 | import { promisify } from 'util'; 6 | import { chat, MODELS } from './Chat.mjs'; 7 | 8 | const execAsync = promisify(exec); 9 | 10 | // Default model if not specified 11 | const DEFAULT_MODEL = "s"; 12 | // Get model from environment variable or use default 13 | const MODEL = process.argv[2] || DEFAULT_MODEL; 14 | 15 | console.log(`Welcome to ChatSH. Model: ${MODELS[MODEL]||MODEL}\n`); 16 | 17 | // System prompt to set the assistant's behavior 18 | const SYSTEM_PROMPT = `You are ChatSH, an AI language model that specializes in assisting users with tasks on their system using shell commands. ChatSH operates in two modes: COMMAND MODE and CHAT MODE. 19 | 20 | # GUIDE for COMMAND NODE: 21 | 22 | 1. The USER asks you to perform a SYSTEM TASK. 23 | 24 | 2. ChatSH answers with a SHELL SCRIPT to perform the task. 25 | 26 | # GUIDE for CHAT MODE: 27 | 28 | 1. The USER asks an ARBITRARY QUESTION or OPEN-ENDED MESSAGE. 29 | 30 | 2. ChatSH answers it with a concise, factual response. 31 | 32 | # EXAMPLE 1: 33 | 34 | 35 | Show me local files. 36 | 37 | 38 | 39 | \`\`\`sh 40 | ls 41 | \`\`\` 42 | 43 | 44 | 45 | example.gif example.tst example_dir/ 46 | 47 | 48 | 49 | Is there any text file? 50 | 51 | 52 | 53 | Yes, there is a file named example.txt in the current directory 54 | 55 | 56 | 57 | Show me the contents of example.txt. 58 | 59 | 60 | 61 | cat example.txt 62 | 63 | 64 | 65 | Na matemática, um grupo é um conjunto de elementos associados a uma operação que combina dois elementos quaisquer para formar um terceiro. Para se qualificar como grupo o conjunto e a operação devem satisfazer algumas condições chamadas axiomas de grupo: associatividade, elemento neutro e elementos inversos. 66 | 67 | 68 | 69 | Can you translate the first sentence to English? 70 | 71 | 72 | 73 | In mathematics, a group is a set of elements associated with an operation that combines any two elements to form a third element. 74 | 75 | 76 | 77 | What is the monster group? 78 | 79 | 80 | 81 | The monster group is the largest sporadic simple group, with order approximately 8×10^53. 82 | 83 | 84 | 85 | What is a simple group? 86 | 87 | 88 | 89 | A simple group is a nontrivial group that contains no proper nontrivial normal subgroups. 90 | 91 | 92 | 93 | Move the text file to that dir. 94 | 95 | 96 | 97 | \`\`\`sh 98 | mv example.txt example_dir/ 99 | \`\`\` 100 | 101 | 102 | # EXAMPLE 2: 103 | 104 | 105 | Fix main.ts 106 | 107 | 108 | 109 | \`\`\`sh 110 | cat main.ts 111 | tsc --noEmit main.ts 112 | \`\`\` 113 | 114 | 115 | 116 | import List from './list'; 117 | import map from './map'; 118 | 119 | const n_1_2: List = { $: "cons", head: 1, tail: { $: "cons", head: 2, tail: { $: "nil" }}}; 120 | const n_2_4: List = map(n_1_2, x => x * 2); 121 | 122 | console.log(JSON.stringify(n_2_4)); 123 | 124 | map.ts:7:22 - error TS2345: Argument of type '(val: T) => U' is not assignable to parameter of type 'List'. 125 | 126 | 7 let tail = map(fun, list.tail); 127 | ~~~ 128 | 129 | 130 | 131 | \`\`\`sh 132 | ls 133 | \`\`\` 134 | 135 | 136 | 137 | list.ts main.ts map.ts 138 | 139 | 140 | 141 | \`\`\`sh 142 | cat map.ts 143 | \`\`\` 144 | 145 | 146 | 147 | import List from './list'; 148 | 149 | function map(list: List, fun: (val: T) => U): List { 150 | switch (list.$) { 151 | case "cons": 152 | let head = fun(list.head); 153 | let tail = map(fun, list.tail); 154 | return { $: "cons", head, tail }; 155 | case "nil": 156 | return { $: "nil" }; 157 | } 158 | } 159 | 160 | export default map; 161 | 162 | 163 | 164 | \`\`\`sh 165 | cat > map.ts << EOL 166 | import List from './list'; 167 | 168 | function map(list: List, fun: (val: T) => U): List { 169 | switch (list.$) { 170 | case "cons": 171 | let head = fun(list.head); 172 | let tail = map(list.tail, fun); 173 | return { $: "cons", head, tail }; 174 | case "nil": 175 | return { $: "nil" }; 176 | } 177 | } 178 | 179 | export default map; 180 | EOL 181 | tsc --noEmit map.ts 182 | \`\`\` 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Done. 191 | { 231 | rl.question(query, resolve); 232 | }); 233 | } 234 | 235 | // If there are words after the 'chatsh', set them as the initialUserMessage 236 | var initialUserMessage = process.argv.slice(3).join(' '); 237 | 238 | // Main interaction loop 239 | async function main() { 240 | let lastOutput = ""; 241 | 242 | while (true) { 243 | let userMessage; 244 | if (initialUserMessage) { 245 | userMessage = initialUserMessage; 246 | initialUserMessage = null; 247 | } else { 248 | process.stdout.write('\x1b[1m'); // blue color 249 | userMessage = await prompt('λ '); 250 | process.stdout.write('\x1b[0m'); // reset color 251 | } 252 | 253 | try { 254 | const fullMessage = userMessage.trim() !== '' 255 | ? `\n${lastOutput.trim()}\n\n\n${userMessage}\n\n` 256 | : `\n${lastOutput.trim()}\n`; 257 | 258 | const assistantMessage = await ask(fullMessage, { system: SYSTEM_PROMPT, model: MODEL }); 259 | console.log(); 260 | 261 | const code = extractCode(assistantMessage); 262 | lastOutput = ""; 263 | 264 | if (code) { 265 | console.log("\x1b[31mPress enter to execute, or 'N' to cancel.\x1b[0m"); 266 | const answer = await prompt(''); 267 | // TODO: delete the warning above from the terminal 268 | process.stdout.moveCursor(0, -2); 269 | process.stdout.clearLine(2); 270 | if (answer.toLowerCase() === 'n') { 271 | console.log('Execution skipped.'); 272 | lastOutput = "Command skipped.\n"; 273 | } else { 274 | try { 275 | const {stdout, stderr} = await execAsync(code); 276 | const output = `${stdout.trim()}${stderr.trim()}`; 277 | console.log('\x1b[2m' + output.trim() + '\x1b[0m'); 278 | lastOutput = output; 279 | } catch(error) { 280 | const output = `${error.stdout?.trim()||''}${error.stderr?.trim()||''}`; 281 | console.log('\x1b[2m' + output.trim() + '\x1b[0m'); 282 | lastOutput = output; 283 | } 284 | } 285 | } 286 | } catch(error) { 287 | console.error(`Error: ${error.message}`); 288 | } 289 | } 290 | } 291 | 292 | // Utility function to extract code from the assistant's message 293 | function extractCode(text) { 294 | const match = text.match(/```sh([\s\S]*?)```/); 295 | return match ? match[1].trim() : null; 296 | } 297 | 298 | async function get_shell() { 299 | const shellInfo = (await execAsync('uname -a && $SHELL --version')).stdout.trim(); 300 | return shellInfo; 301 | } 302 | 303 | main(); 304 | -------------------------------------------------------------------------------- /aiemu.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from "process"; 4 | import fs from 'fs/promises'; 5 | import { chat, MODELS } from './Chat.mjs'; 6 | 7 | const MODEL = process.argv[2] || "s"; 8 | 9 | const SYSTEM = ` 10 | You're a game emulator. You can emulate ANY game, but text-based. Your goal is 11 | to be a fully playable text-based version of the game, emulating as close to the 12 | original as possible, from start to end. 13 | 14 | You'll be provided with: 15 | 1. The chosen game. 16 | 2. The current message context. 17 | 18 | Your responses must include: 19 | 1. A short description of the current game screen or state. 20 | 2. A textual 2D UI of the current game screen, using emojis and symbols. 21 | 3. A labelled list of options that the player can take. 22 | 23 | Always follow this template: 24 | 25 | <> 26 | <> 27 | <> 28 | 29 | Guidelines for the game screen UI: 30 | - Draw it as compactly as possible while maintaining readability. 31 | - When handy, add a description / narration above the screen. 32 | - Use a 2D textual grid to position key game elements spatially. 33 | - Represent sprites, characters, items etc with 1-3 emojis each. 34 | - Draw HP/mana bars, item counts etc visually to form a nice UI. 35 | - Use ASCII diagrams very sparingly, mainly for progress bars. 36 | - Include menu options like Pause, Inventory etc for completeness. 37 | - Expand item/action options (e.g. Use X, Attack, Defend) for faster play. 38 | 39 | Here are some examples of how your game screen should look. 40 | 41 | //# Example: Pokémon Red - Battle Screen 42 | 43 | You're in a Pokémon battle. 44 | ,-----------------------------, 45 | Blastoise LV30 [💦🐢💣] 46 | HP: |||....... [🔫🐚🛡️] 47 | 48 | Charizard LV32 [🔥🐉🦇] 49 | HP: ||||||.... [🌋🦖😤] 50 | '-----------------------------' 51 | A) FIGHT 52 | B) PKMN 53 | C) ITEM 54 | D) RUN 55 | 56 | //# Example: Zelda Majora's Mask - Odolwa Boss Room 57 | 58 | You're in Odolwa's boss room in Woodfall Temple. 59 | Odolwa is dancing and swinging his swords menacingly. 60 | ,--------------------------------------------------, 61 | HP ❤️ ❤️ ❤️ 🤍🤍🤍🤍 62 | MANA 🟩🟩🟩⬜⬜⬜⬜⬜⬜⬜ 63 | 64 | Link Navi Door0 65 | [🗡️🧝🛡️] [🧚] [🚪🔒] 66 | 67 | Odolwa Jar Door1 Chest 68 | [🗡️🎭🗡️] [🏺] [🚪🔒] [🎁🔒] 69 | 70 | Grs0 Grs1 Grs2 71 | [🌿] [🌿] [🌿] 72 | 73 | 💎 000 🕒 7 AM :: ☀️ 1st Day 74 | '--------------------------------------------------' 75 | A) Talk to Navi 76 | B) Enter Door0 77 | C) Attack Odolwa 78 | D) Break the Jar 79 | E) Enter Door1 80 | F) Check Grs0 81 | G) Check Grs1 82 | H) Check Grs2 83 | 84 | //# Example: Mario 64 - Inside Castle 85 | 86 | You're in the main entrance hall of Princess Peach's castle. 87 | ,---------------------------------. 88 | 🍄x4 🌟x7 89 | 90 | Door0 Door1 Door2 91 | [🚪🌟] [🚪🔒] [🚪0] 92 | 93 | Door3 Door4 Door5 Door6 94 | [🚪0] [🚪3] [🚪7] [🚪1] 95 | 96 | Exit Mario Coin0 Coin1 97 | [🚪] [🍄] [🟡] [🟡] 98 | '---------------------------------' 99 | A) Enter Door0 100 | B) Enter Door1 101 | C) Enter Door2 102 | D) Enter Door3 103 | E) Enter Door4 104 | F) Enter Door5 105 | G) Enter Door6 106 | H) Check Coin0 107 | I) Check Coin1 108 | J) Exit 109 | 110 | //# Example: Pokémon Red - Title Screen 111 | 112 | ,-------------------------------, 113 | Pokémon 114 | Red 115 | 116 | [🔥🐉🦇] 117 | 118 | ©1996 Nintendo 119 | Creatures Inc. 120 | GAME FREAK inc. 121 | 122 | Press Start Button 123 | '-------------------------------' 124 | A) New Game 125 | B) Continue 126 | C) Options 127 | 128 | //# Example: Pokémon Red - Introduction 129 | 130 | ,-------------------------------. 131 | 132 | OAK 133 | Hello there! Welcome to the 134 | world of POKÉMON! 135 | 136 | OAK 137 | My name is OAK! 138 | People call me the 139 | POKÉMON PROF! 140 | 141 | NIDORAN♂ 142 | [🐭💜🦏] 143 | '-------------------------------' 144 | A) Next 145 | 146 | //# Example: Pokémon Red - Pallet Town 147 | 148 | You're in Pallet Town, your hometown. 149 | ,--------------------------, 150 | 🌳 [Route 1] 🌳 151 | 152 | House0 House1 153 | [🏠] [🏠] 154 | 155 | Grass Oak's Lab 156 | [🌿] [🏫] 157 | 158 | Beach Sign 🌸 159 | [🌊] [🪧] 🌼 160 | '--------------------------' 161 | A) Enter House0 162 | B) Enter House1 163 | C) Enter Oak's Lab 164 | D) Check the Sign 165 | E) Walk in the Grass 166 | F) Exit to Route 1 167 | 168 | //# Example: Pokémon Red - Protagonist's House 169 | 170 | You're inside your house in Pallet Town. 171 | ,---------------------------. 172 | PC TV Stairs 173 | [💻] [📺] [┗┓] 174 | 175 | Bed You 176 | [🛏️] [👦] 177 | '---------------------------' 178 | A) Check the PC 179 | B) Play SNES on TV 180 | C) Rest in Bed 181 | B) Go Downstairs 182 | 183 | //# Example: The Legend of Zelda - Majora's Mask - Title Screen 184 | 185 | ,------------------------------------------, 186 | 187 | The Legend of 188 | Zelda 189 | Majora's Mask 190 | 191 | [🎭😈🌙] 192 | 193 | Press Start 194 | 195 | 196 | ©2000 Nintendo. All Rights Reserved. 197 | '------------------------------------------' 198 | A) PRESS START 199 | B) OPTIONS 200 | 201 | IMPORTANT: 202 | - You ARE the videogame. Stay in character. 203 | - Start from the game's initial menus and emulate each level in order. 204 | - Emulate the game loyally, following its original sequence of events. 205 | - Design a well-aligned UI for each screen. Position elements in 2D. 206 | - Respond with ONLY the next emulation step and its options. 207 | - BE CREATIVE. Make this a great, entertaining experience. 208 | 209 | If the player provides feedback after a '#', use it to improve the experience. 210 | `; 211 | 212 | (async () => { 213 | 214 | // TODO: wait for 100ms 215 | await new Promise(resolve => setTimeout(resolve, 100)); 216 | 217 | console.clear(); 218 | 219 | const ASCII_ART = ` 220 | \x1b[1m\x1b[36m█▀▀▀▀▀█ ▀ ▄▀▄ █▀▀▀▀▀█\x1b[0m 221 | \x1b[1m\x1b[36m█ ███ █ ▀ ▀█▀ █ ███ █\x1b[0m 222 | \x1b[1m\x1b[36m█ ▀▀▀ █ █ ▄█▄ █ ▀▀▀ █\x1b[0m 223 | \x1b[1m\x1b[36m▀▀▀▀▀▀▀ ▀ ▀▀▀ ▀▀▀▀▀▀▀\x1b[0m 224 | \x1b[2mA I E M U L A T O R\x1b[0m 225 | `.trim(); 226 | 227 | console.log(ASCII_ART); 228 | 229 | console.log(""); 230 | console.log(`\x1b[32mUsing \x1b[1m${MODELS[MODEL]||MODEL}\x1b[0m`); 231 | console.log(""); 232 | 233 | process.stdout.write("Game: "); 234 | const game = (await new Promise(resolve => process.stdin.once('data', data => resolve(data.toString())))).trim(); 235 | 236 | console.log(`Emulating ${game}...\n\n`); 237 | 238 | const ask = chat(MODEL); 239 | let messages = [ 240 | {role: "user", content: `# GAME: ${game}`}, 241 | ]; 242 | 243 | while (true) { 244 | console.clear(); 245 | 246 | const response = await ask(messages[messages.length - 1].content, { 247 | system: SYSTEM, 248 | model: MODEL, 249 | max_tokens: 2048, 250 | temperature: 0.5, 251 | }); 252 | 253 | messages.push({role: "assistant", content: response}); 254 | 255 | process.stdout.write("\n\nEnter your choice: "); 256 | const choice = (await new Promise(resolve => process.stdin.once('data', data => resolve(data.toString())))).trim(); 257 | messages.push({role: "user", content: choice}); 258 | 259 | await fs.writeFile("./log.txt", messages.map(m => `${m.role === "user" ? "# PLAYER" : "# EMULATOR"}:\n\n${m.content}\n\n`).join("")); 260 | } 261 | })(); 262 | 263 | -------------------------------------------------------------------------------- /kindcoder.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs/promises'; 4 | import os from 'os'; 5 | import path from 'path'; 6 | import process from "process"; 7 | import { chat, MODELS, tokenCount } from './Chat.mjs'; 8 | import { exec } from 'child_process'; 9 | import { promisify } from 'util'; 10 | 11 | const execAsync = promisify(exec); 12 | const kind2_guide = await fs.readFile(new URL('./KIND2_GUIDE_AI.md', import.meta.url), 'utf-8'); 13 | 14 | // System prompt for the AI model, defining its role and behavior 15 | const system_KindCoder = ` 16 | You are KindCoder, a Kind2-Lang coding assistant. 17 | 18 | # USER INPUT 19 | 20 | You will receive: 21 | 22 | 1. A target in the Kind2 language. That's the code you must update. 23 | 24 | 2. The user's change . You must perform that change on the target file. 25 | 26 | 3. Some additional context (files, dirs) that could be helpful. 27 | 28 | # KINDCODER OUTPUT 29 | 30 | You, KindCoder, must answer with a single tag, which must include the 31 | user's file, except *modified* to fulfill the user's request, and nothing else. 32 | 33 | # GUIDE FOR REFACTORING 34 | 35 | 1. Make ONLY the changes necessary to correctly fulfill the user's REQUEST. 36 | 2. Do NOT fix, remove, complete, or alter any parts unrelated to the REQUEST. 37 | 3. Preserve the same indentation and style of the target FILE. 38 | 4. Consult Kind2's guide to emit syntactically correct code. 39 | 5. Be precise and careful in your modifications. 40 | 41 | ${kind2_guide} 42 | 43 | # KINDCODER EXAMPLE 44 | 45 | Below is a complete example of how KindCoder should interact with the user. 46 | 47 | ## User: 48 | 49 | 50 | ... 51 | 52 | 53 | 54 | ... 55 | 56 | 57 | 58 | ... 59 | 60 | 61 | 62 | use Nat/{succ,zero} 63 | 64 | is_even 65 | - n: Nat 66 | : Nat 67 | 68 | ?a 69 | 70 | 71 | 72 | GOAL ?a : Nat 73 | - n: Nat 74 | 75 | 76 | 77 | case-split on n 78 | 79 | 80 | ## KindCoder: 81 | 82 | 83 | use Nat/{succ,zero} 84 | 85 | is_even 86 | - n: Nat 87 | : Nat 88 | 89 | match n { 90 | zero: ?zero_case 91 | succ: ?succ_case 92 | } 93 | 94 | 95 | # EXPLANATION 96 | 97 | ## Input: 98 | 99 | The user provided a target file (Nat/is_even) to be modified, and a request: 100 | "case-split on n". The user also provided some additional files and dirs for 101 | context (including Nat, Nat/succ, Nat/zero). The target file had an incomplete 102 | top-level definition, 'is_even', with a hole, '?a', as its body. 103 | 104 | ## Output: 105 | 106 | As a response, you, KindCoder, performed a case-split on 'n', by using Kind's 107 | 'match' syntax-sugar. You did NOT perform any extra work, nor change anything 108 | beyond what the user explicitly asked for. Instead, you just placed NEW holes 109 | ('?n_is_zero'/'?n_is_succ') on the respective cases. You included the updated 110 | file inside a RESULT tag, completing the task successfully. Good job! 111 | 112 | # TASK 113 | 114 | The user will now give you a Kind2 file, and a change request. Read it carefully 115 | and update it as demanded. Consult the guides above as necessary. Pay attention 116 | to syntax details, like mandatory parenthesis, to emit valid code. Do it now: 117 | `.trim(); 118 | 119 | const system_DepsPredictor = ` 120 | # ABOUT KIND2 121 | 122 | Kind2 is a minimal purely functional programming language, where every file 123 | defines exactly ONE function, type or constant. For example: 124 | 125 | ''' 126 | // Nat/add.kind2: defines Nat addition 127 | 128 | use Nat/{succ,zero} 129 | 130 | add 131 | - a: Nat 132 | - b: Nat 133 | : Nat 134 | 135 | match a { 136 | succ: (succ (add a.pred b)) 137 | zero: b 138 | } 139 | ''' 140 | 141 | The file above implements the global 'Nat/add' definition. 142 | 143 | # INPUT 144 | 145 | You will be given the NAME of a Kind2 file, its source code (which may be 146 | empty), and a list of ALL Kind2 definitions available in the stdlib. 147 | 148 | # OUTPUT 149 | 150 | You must answer with a list of definitions that are, or that you predict WILL BE 151 | used, directly or not, inside that Kind2 file. Answer in a tag. 152 | 153 | Optionally, you can also include a SHORT, 1-paragraph . 154 | 155 | # EXAMPLE INPUT 156 | 157 | Nat/equal 158 | 159 | 160 | 161 | 162 | 163 | - List/ 164 | - cons 165 | - nil 166 | - match 167 | - map 168 | - fold 169 | - filter 170 | - equal 171 | - zip 172 | - length 173 | - Nat/ 174 | - match 175 | - fold 176 | - succ 177 | - zero 178 | - compare 179 | - add 180 | - sub 181 | - mul 182 | - div 183 | - mod 184 | - pow 185 | - lte 186 | - gte 187 | - Bool/ 188 | - match 189 | - fold 190 | - true 191 | - false 192 | - not 193 | - and 194 | - or 195 | - xor 196 | - nand 197 | 198 | 199 | # EXAMPLE OUTPUT 200 | 201 | 202 | Nat/equal is likely to be a pairwise comparison between Nats. As such, it must 203 | include Nat (obviously), as well as its constructor and match. It returns a 204 | Bool, so, it must also include its constructors and match. For completion, I've 205 | also added bool AND and OR, since these are often used in comparison. Finally, 206 | Nat/compare and List/equal might be similar algorithms, so, I included them. 207 | 208 | 209 | Nat 210 | Nat/succ 211 | Nat/zero 212 | Nat/match 213 | Bool 214 | Bool/true 215 | Bool/false 216 | Bool/match 217 | Bool/and 218 | Bool/or 219 | Nat/compare 220 | List/equal 221 | 222 | 223 | # HINTS 224 | 225 | - Attempt to include ALL files that might be relevant, directly or not. 226 | 227 | - Always include files that might be similar algorithms to the current one. 228 | Example: 'Map/set' MUST include 'Mat/get' 229 | 230 | - If the file is the constructor of an ADT, then, INCLUDE its type. 231 | Example: 'List/cons' MUST include 'List' 232 | 233 | - When in doubt, prefer to include MORE, rather than LESS, potencial dependencies. 234 | 235 | - Try to include AT LEAST 4 dependencies, and AT MOST (only if needed) 16. 236 | 237 | - Sometimes the user will give hints in the file. Follow them. 238 | `.trim(); 239 | 240 | // Function to predict dependencies 241 | async function predictDependencies(name, fileContent) { 242 | // Function to get all Kind2 files recursively 243 | async function getAllKind2Files(dir) { 244 | const entries = await fs.readdir(dir, { withFileTypes: true }); 245 | const files = await Promise.all(entries.map(async (entry) => { 246 | const res = path.resolve(dir, entry.name); 247 | if (entry.isDirectory()) { 248 | const subFiles = await getAllKind2Files(res); 249 | return subFiles.length > 0 ? { name: entry.name, children: subFiles } : null; 250 | } else if (entry.name.endsWith('.kind2')) { 251 | return { name: entry.name.replace('.kind2', '') }; 252 | } 253 | return null; 254 | })); 255 | return files.filter(file => file !== null).map(file => ({...file, name: file.name.replace(/\/_$/, '')})); 256 | } 257 | 258 | // Function to build a tree structure from files 259 | function buildTree(files, prefix = '') { 260 | let result = ''; 261 | for (const file of files) { 262 | if (file.children) { 263 | result += `${prefix}- ${file.name}/\n`; 264 | result += buildTree(file.children, `${prefix} `); 265 | } else { 266 | result += `${prefix}- ${file.name}\n`; 267 | } 268 | } 269 | return result; 270 | } 271 | 272 | const allFiles = await getAllKind2Files("./"); 273 | const defsTree = buildTree(allFiles); 274 | 275 | const aiInput = [ 276 | `${name}`, 277 | '', 278 | fileContent.trim(), 279 | '', 280 | '', 281 | defsTree.trim(), 282 | '' 283 | ].join('\n').trim(); 284 | 285 | const aiOutput = await chat("s")(aiInput, { system: system_DepsPredictor, model: "s" }); 286 | console.log(""); 287 | 288 | const dependenciesMatch = aiOutput.match(/([\s\S]*)<\/DEPENDENCIES>/); 289 | if (!dependenciesMatch) { 290 | console.error("Error: AI output does not contain a valid DEPENDENCIES tag."); 291 | return []; 292 | } 293 | 294 | return dependenciesMatch[1].trim().split('\n').map(dep => dep.trim()); 295 | } 296 | 297 | // Function to perform type checking based on file extension 298 | async function typeCheck(file) { 299 | let ext = path.extname(file); 300 | let cmd = `kind2 check ${file}`; 301 | try { 302 | var result = await execAsync(cmd); 303 | return result.stderr.trim() || result.stdout.trim(); 304 | } catch (error) { 305 | return error.stderr.trim(); 306 | } 307 | } 308 | 309 | // Main function to handle the refactoring process 310 | async function main() { 311 | // Check for correct usage and parse command-line arguments 312 | if (process.argv.length < 3) { 313 | console.log("Usage: kindcoder []"); 314 | process.exit(1); 315 | } 316 | 317 | let file = process.argv[2]; 318 | let request = process.argv[3]; 319 | let model = process.argv[4] || "s"; 320 | 321 | // Initialize the chat function with the specified model 322 | let ask = chat(model); 323 | 324 | // Get directory and file information 325 | let dir = path.dirname(file); 326 | let fileContent; 327 | try { 328 | fileContent = await fs.readFile(file, 'utf-8'); 329 | } catch (e) { 330 | fileContent = ""; 331 | } 332 | let dirContent = await fs.readdir(dir); 333 | 334 | // If the request is empty, replace it by a default request. 335 | if (request.trim() === '') { 336 | request = [ 337 | "Update this file.", 338 | "- If it is empty, implement an initial template.", 339 | "- If it has holes, fill them, up to \"one layer\".", 340 | "- If it has no holes, fully complete it, as much as possible." 341 | ].join('\n'); 342 | } 343 | 344 | // If the file is empty, ask the AI to fill with an initial template 345 | if (fileContent.trim() === '') { 346 | fileContent = [ 347 | "This file is empty. Please replace it with a Kind2 definition. Example:", 348 | "", 349 | "```kind2", 350 | "/// Does foo.", 351 | "///", 352 | "/// # Input", 353 | "///", 354 | "/// * `x0` - Description", 355 | "/// * `x1` - Description", 356 | "/// ...", 357 | "///", 358 | "/// # Output", 359 | "///", 360 | "/// The result of doing foo", 361 | "", 362 | "use Lib/A/{a,b}", 363 | "use Lib/B/{c,d}", 364 | "...", 365 | "", 366 | "foo ...", 367 | "- x0: X0", 368 | "- x1: X1", 369 | "...", 370 | "", 371 | "body", 372 | "```", 373 | ].join('\n'); 374 | } 375 | 376 | // Extract the definition name from the file path 377 | let defName = file.split('/book/')[1].replace('.kind2', ''); 378 | 379 | // Collect direct and indirect dependencies 380 | let deps; 381 | try { 382 | let { stdout } = await execAsync(`kind2 deps ${defName}`); 383 | deps = stdout.trim().split('\n'); 384 | } catch (e) { 385 | deps = []; 386 | } 387 | 388 | // Predict additional dependencies 389 | const predictedDeps = await predictDependencies(defName, fileContent); 390 | //console.log(JSON.stringify(predictedDeps,null,2)); 391 | //process.exit(); 392 | deps = [...new Set([...deps, ...predictedDeps])]; 393 | deps = deps.filter(dep => dep !== defName); 394 | 395 | // Read dependent files 396 | let depFiles = await Promise.all(deps.map(async (dep) => { 397 | let depPath, content; 398 | let path0 = path.join(dir, '..', `${dep}.kind2`); 399 | let path1 = path.join(dir, '..', `${dep}/_.kind2`); 400 | try { 401 | content = await fs.readFile(path0, 'utf-8'); 402 | depPath = path0; 403 | } catch (error) { 404 | try { 405 | content = await fs.readFile(path1, 'utf-8'); 406 | depPath = path1; 407 | } catch (error) { 408 | return ""; 409 | } 410 | } 411 | return `\n${content}\n`; 412 | })); 413 | 414 | // Perform initial type checking 415 | let initialCheck = (await typeCheck(defName)).replace(/\x1b\[[0-9;]*m/g, ''); 416 | 417 | // Prepare AI input 418 | let aiInput = [ 419 | ...depFiles, 420 | ``, 421 | fileContent, 422 | '', 423 | '', 424 | initialCheck, 425 | '', 426 | '', 427 | request, 428 | '' 429 | ].join('\n').trim(); 430 | 431 | // Write a .prompt file with the system + aiInput strings 432 | await fs.writeFile('.kindcoder', system_KindCoder + '\n\n' + aiInput, 'utf-8'); 433 | 434 | // Call the AI model 435 | let aiOutput = await ask(aiInput, { system: system_KindCoder, model }); 436 | console.log(""); 437 | 438 | // Extract the result from AI output 439 | let resultMatch = aiOutput.match(/([\s\S]*)<\/RESULT>/); 440 | if (!resultMatch) { 441 | console.error("Error: AI output does not contain a valid RESULT tag."); 442 | process.exit(1); 443 | } 444 | 445 | let result = resultMatch[1].trim(); 446 | 447 | // Write the result back to the file 448 | await fs.writeFile(file, result, 'utf-8'); 449 | 450 | console.log("File updated successfully."); 451 | } 452 | 453 | // Run the main function and handle any errors 454 | main().catch(console.error); 455 | 456 | -------------------------------------------------------------------------------- /KIND2_GUIDE_AI.md: -------------------------------------------------------------------------------- 1 | # GUIDE FOR THE KIND2 LANGUAGE 2 | 3 | Kind2 is a minimal proof language based on the Calculus of Constructions. It is 4 | similar to Agda in capabilities, but has a raw syntax, and a much smaller core. 5 | Instead of a native datatype system, it uses λ-encodings to represent data. To 6 | make inductive proofs, it includes a lightweight primitive called Self Types. 7 | 8 | ## Kind2 Core Language 9 | 10 | All of Kind2 desugars to the following small core: 11 | 12 | ``` 13 | Term ::= 14 | | all : ∀(x: A) B // the dependent function type (also called Pi Type) 15 | | lam : λx f // an inline function (also called lambda) 16 | | app : (f x y z) // function application (Lisp-style, MANDATORY parenthesis) 17 | | ann : {x :: T} // an inline annotation (type hint) 18 | | slf : $(x: A) T // self type, for λ-encoded inductive datatypes (see later) 19 | | ins : ~t // self inst, to consume a self type (see later) 20 | | ref : // top-level reference (expands to its definition) 21 | | let : let x = t // local definition, creates a new variable (runtime cloning) 22 | | use : use x = t // local definition, substitutes statically (no runtime cloning) 23 | | set : * // the only universe (kind has type-in-type) 24 | | num : // a numeric literal (48-bit unsigned integer) 25 | | op2 : (+ x y) // a numeric operation (Lisp-style, MANDATORY parenthesis) 26 | | swi : see below // a numeric pattern-match (with zero and succ cases) 27 | | u48 : U48 // a numeric type 28 | | hol : ?a // a typed hole, for debugging and context inspection 29 | | met : _ // an unification metavar (is solved by the checker) 30 | | var : // a variable 31 | ``` 32 | 33 | ## Kind2 Syntax 34 | 35 | Since Kind2's core is so simple, it comes with many syntax sugars. 36 | 37 | ### Top-Level Function 38 | 39 | Every .kind2 file must define ONE top-level function: 40 | 41 | ``` 42 | func ... 43 | - arg0: typ0 44 | - arg1: typ1 45 | - ... 46 | : ret_typ 47 | 48 | body 49 | ``` 50 | 51 | Where: 52 | - p0, p1... are erased arguments 53 | - arg0, arg1... are the function arguments 54 | - ret_typ is the returned type 55 | - body is the function's body 56 | 57 | ### Top-Level Datatype 58 | 59 | Alternatively, a .kind2 tile can also define an inductive datatype: 60 | 61 | ``` 62 | data Name ... (i0: I0) (i1: I1) ... 63 | | ctr0 (f0: F0) (f1: F1) ... : (Name p0 p1 ... i0 i1 ...) 64 | | ctr1 (f0: F0) (f1: F1) ... : (Name p0 p1 ... i0 i1 ...) 65 | | ... 66 | ``` 67 | 68 | Where: 69 | - p0, p1... are parameters 70 | - i0, i1... are indices 71 | - ctr0, ctr1... are constructors 72 | - f0, f1... are fields 73 | 74 | Top-Level datatypes desugar to λ-encodings. The λ-encoded constructors must be 75 | created manually, in separate files. See examples below. 76 | 77 | ### Names, Paths and Use Aliases 78 | 79 | Kind2 doesn't need imports. Every file defines a single top-level definition, 80 | which can be addressed from any other file via its full path. Example: 81 | 82 | ``` 83 | book/Nat/add/comm.kind2 84 | ``` 85 | 86 | Defines: 87 | 88 | ``` 89 | Nat/add/comm 90 | ``` 91 | 92 | Which can be accessed directly from any other file, no 'import' needed. 93 | 94 | To shorten names, the 'use' declaration can be added to the beginning of a file: 95 | 96 | ``` 97 | use Nat/{succ,zero} 98 | ``` 99 | 100 | This locally expands 'succ' and 'zero' to 'Nat/succ' and 'Nat/zero'. It is 101 | specially useful to avoid typing full constructor names on 'match' cases. 102 | 103 | NOTE: when a definition is not found in `Foo/Bar.kind2`, Kind2 will try to 104 | look for it on `Foo/Bar/_.kind2`. The `_` is just a placeholder and is NOT 105 | part of the definition's name. 106 | 107 | ### Pattern-Matching 108 | 109 | To eliminate a datatype, the match syntax can be used: 110 | 111 | ``` 112 | match x = expr with (a0: A0) (a1: A1) ... { 113 | Type/ctr0: ret0 114 | Type/ctr1: ret1 115 | ... 116 | }: motive 117 | ``` 118 | 119 | Where: 120 | - x is the *name* of the scrutinee 121 | - expr is the *value* of scrutinee (optional) 122 | - a0, a1... are arguments to be *linearly passed down* to the branches (as an optimization, and helps proving) 123 | - ctr0, ctr1... are the matched cases 124 | - ret0, ret1... are the returned bodies of each case (with ctr fields available) 125 | - the motive is optional (useful for theorem proving) 126 | 127 | Kind will automatically make constructor fields available on their respective 128 | cases, named `.`. For example, on the `succ` case of 129 | Nat, if the scrutinee is called `num`, `num.pred` will be available. 130 | 131 | For this syntax to work, a top-level 'Type/match' definition must be provided. 132 | 133 | The 'match' keyword can be replaced by 'fold', to auto-recurse. 134 | 135 | This desugars to a self-inst and function applications. 136 | 137 | ### Numeric Pattern-Matching 138 | 139 | For matching on native U48 numbers, Kind2 provides a special syntax: 140 | 141 | ``` 142 | switch x = expr { 143 | 0: zero_case 144 | _: succ_case 145 | }: motive 146 | ``` 147 | 148 | ### Note on Parameters and Metavars 149 | 150 | Top-level definitions can have N parameters, or erased arguments. Example: 151 | 152 | ``` 153 | // Pair/swap.kind2 154 | swap 155 | - pair: (Pair A B) 156 | ... 157 | ``` 158 | 159 | There are two ways to call these functions. 160 | 161 | 1. Filling the parameters explicitly: 162 | 163 | ``` 164 | (swap Nat (List Nat) (Pair/new Nat (List Nat) Nat/zero (List/nil Nat))) 165 | ``` 166 | 167 | 2. Using metavars (`_`) to fill the parameters: 168 | 169 | ``` 170 | (swap _ _ (Pair/new _ _ Nat/zero (List/nil _))) 171 | ``` 172 | 173 | As you can see, using metavars is much more concise. As a rule of thumb, always 174 | use metavars on the function body, but write it fully on its arglist. Remember 175 | to always count the arguments: you need one metavar (`_`) per parameter (`<>`). 176 | 177 | ### Other Sugars 178 | 179 | - Lists: `[a, b, c]` (desugars to cons/nil) 180 | - Strings: `"foo"` (desugars to lists of u48 codepoints) 181 | - Equality: `{a = b}` (desugars to `(Equal _ a b)`) 182 | - Function: `A -> B` (desugars to `∀(x_: A) B`) 183 | - Comments: `// comment here` 184 | 185 | ## Kind2 Examples 186 | 187 | ### Nat/_.kind2 188 | 189 | ``` 190 | /// Defines the natural numbers as an inductive datatype. 191 | /// 192 | /// # Constructors 193 | /// 194 | /// * `succ` - Represents the successor of a natural number (x+1). 195 | /// * `zero` - Represents the natural number zero (0). 196 | 197 | data Nat 198 | | succ (pred: Nat) 199 | | zero 200 | ``` 201 | 202 | ### Nat/succ.kind2 203 | 204 | ``` 205 | /// Constructs the successor of a natural number. 206 | /// 207 | /// # Input 208 | /// 209 | /// * `n` - The natural number to which we add 1. 210 | /// 211 | /// # Output 212 | /// 213 | /// The successor of `n`. 214 | 215 | succ 216 | - n: Nat 217 | : Nat 218 | 219 | ~λP λsucc λzero (succ n) 220 | ``` 221 | 222 | ### Nat/zero.kind2 223 | 224 | ``` 225 | /// Represents the zero natural number. 226 | /// 227 | /// # Output 228 | /// 229 | /// The zero natural number. 230 | 231 | zero 232 | : Nat 233 | 234 | ~λP λsucc λzero zero 235 | ``` 236 | 237 | ### Nat/match.kind2 238 | 239 | ``` 240 | /// Provides a way to pattern match on natural numbers. 241 | /// 242 | /// # Inputs 243 | /// 244 | /// * `P` - The motive of the elimination. 245 | /// * `s` - The successor case. 246 | /// * `z` - The zero case. 247 | /// * `n` - The natural number to match on. 248 | /// 249 | /// # Output 250 | /// 251 | /// The result of the elimination. 252 | 253 | match 254 | - P: Nat -> * 255 | - s: ∀(pred: Nat) (P (Nat/succ pred)) 256 | - z: (P Nat/zero) 257 | - n: Nat 258 | : (P n) 259 | 260 | (~n P s z) 261 | ``` 262 | 263 | ### Nat/add.kind2 264 | 265 | ``` 266 | /// Adds two natural numbers. 267 | /// 268 | /// # Inputs 269 | /// 270 | /// * `a` - The first natural number. 271 | /// * `b` - The second natural number. 272 | /// 273 | /// # Output 274 | /// 275 | /// The sum of `a` and `b`. 276 | 277 | use Nat/{succ,zero} 278 | 279 | add 280 | - a: Nat 281 | - b: Nat 282 | : Nat 283 | 284 | match a { 285 | succ: (succ (add a.pred b)) 286 | zero: b 287 | } 288 | ``` 289 | 290 | ### Nat/equal.kind2 291 | 292 | ``` 293 | /// Checks if two natural numbers are equal. 294 | /// 295 | /// # Inputs 296 | /// 297 | /// * `a` - The first natural number. 298 | /// * `b` - The second natural number. 299 | /// 300 | /// # Output 301 | /// 302 | /// `true` if `a` and `b` are equal, `false` otherwise. 303 | 304 | use Nat/{succ,zero} 305 | use Bool/{true,false} 306 | 307 | equal 308 | - a: Nat 309 | - b: Nat 310 | : Bool 311 | 312 | match a with (b: Nat) { 313 | succ: match b { 314 | succ: (equal a.pred b.pred) 315 | zero: false 316 | } 317 | zero: match b { 318 | succ: false 319 | zero: true 320 | } 321 | } 322 | ``` 323 | 324 | ### List/_.kind2 325 | 326 | ``` 327 | /// Defines a generic list datatype. 328 | /// 329 | /// # Parameters 330 | /// 331 | /// * `T` - The type of elements in the list. 332 | /// 333 | /// # Constructors 334 | /// 335 | /// * `cons` - Adds an element to the front of a list. 336 | /// * `nil` - Represents an empty list. 337 | 338 | data List 339 | | cons (head: T) (tail: (List T)) 340 | | nil 341 | ``` 342 | 343 | ### List/cons.kind2 344 | 345 | ``` 346 | /// Constructs a new list by adding an element to the front of an existing list. 347 | /// 348 | /// # Parameters 349 | /// 350 | /// * `T` - The type of elements in the list. 351 | /// 352 | /// # Inputs 353 | /// 354 | /// * `head` - The element to add to the front of the list. 355 | /// * `tail` - The existing list. 356 | /// 357 | /// # Output 358 | /// 359 | /// A new list with `head` as its first element, followed by the elements of `tail`. 360 | 361 | cons 362 | - head: T 363 | - tail: (List T) 364 | : (List T) 365 | 366 | ~λP λcons λnil (cons head tail) 367 | ``` 368 | 369 | ### List/nil.kind2 370 | 371 | ``` 372 | /// Constructs an empty list. 373 | /// 374 | /// # Parameters 375 | /// 376 | /// * `T` - The type of elements in the list. 377 | /// 378 | /// # Output 379 | /// 380 | /// An empty list of type `(List T)`. 381 | 382 | nil 383 | : (List T) 384 | 385 | ~λP λcons λnil nil 386 | ``` 387 | 388 | ### List/match.kind2 389 | 390 | ``` 391 | /// Provides a way to pattern match on lists. 392 | /// 393 | /// # Parameters 394 | /// 395 | /// * `A` - The type of elements in the list. 396 | /// 397 | /// # Inputs 398 | /// 399 | /// * `P` - The motive of the elimination. 400 | /// * `c` - The cons case. 401 | /// * `n` - The nil case. 402 | /// * `xs` - The list to match on. 403 | /// 404 | /// # Output 405 | /// 406 | /// The result of the elimination. 407 | 408 | match 409 | - P: (List A) -> * 410 | - c: ∀(head: A) ∀(tail: (List A)) (P (List/cons A head tail)) 411 | - n: (P (List/nil A)) 412 | - xs: (List A) 413 | : (P xs) 414 | 415 | (~xs P c n) 416 | ``` 417 | 418 | ### List/fold.kind2 419 | 420 | ``` 421 | /// Folds a list from left to right. 422 | /// 423 | /// # Parameters 424 | /// 425 | /// * `A` - The type of elements in the list. 426 | /// 427 | /// # Inputs 428 | /// 429 | /// * `P` - The type of the accumulator and result. 430 | /// * `c` - The function to apply to each element and the accumulator. 431 | /// * `n` - The initial value of the accumulator. 432 | /// * `xs` - The list to fold. 433 | /// 434 | /// # Output 435 | /// 436 | /// The result of folding the list. 437 | 438 | use List/{cons,nil} 439 | 440 | List/fold (P: *) 441 | - c: A -> P -> P 442 | - n: P 443 | - xs: (List A) 444 | : P 445 | 446 | match xs { 447 | cons: (c xs.head (List/fold _ P c n xs.tail)) 448 | nil: n 449 | } 450 | ``` 451 | 452 | ### List/map.kind2 453 | 454 | ``` 455 | /// Applies a function to each element of a list. 456 | /// 457 | /// # Parameters 458 | /// 459 | /// * `A` - The type of elements in the input list. 460 | /// * `B` - The type of elements in the output list. 461 | /// 462 | /// # Inputs 463 | /// 464 | /// * `xs` - The input list. 465 | /// * `fn` - The function to apply to each element. 466 | /// 467 | /// # Output 468 | /// 469 | /// A new list with the function applied to each element of the input list. 470 | 471 | use List/{cons,nil} 472 | 473 | map 474 | - xs: (List A) 475 | - fn: A -> B 476 | : (List B) 477 | 478 | match xs { 479 | cons: 480 | let head = (fn xs.head) 481 | let tail = (List/map _ _ xs.tail fn) 482 | (cons _ head tail) 483 | nil: 484 | (nil _) 485 | } 486 | ``` 487 | 488 | ### List/filter.kind2 489 | 490 | ``` 491 | /// Filters a list, keeping only elements that satisfy the given condition. 492 | /// 493 | /// # Parameters 494 | /// 495 | /// * `A` - The type of elements in the list. 496 | /// 497 | /// # Inputs 498 | /// 499 | /// * `cond` - A function that takes an element of type A and returns a Bool. 500 | /// * `list` - The input list of type (List A). 501 | /// 502 | /// # Output 503 | /// 504 | /// A new list containing only the elements for which `cond` returns `true`. 505 | 506 | use List/{cons,nil} 507 | use Bool/{true,false} 508 | 509 | filter 510 | - cond: A -> Bool 511 | - list: (List A) 512 | : (List A) 513 | 514 | match list { 515 | nil: 516 | (nil _) 517 | cons: 518 | let test = (cond list.head) 519 | let head = match test { true: (cons _ list.head) false: λx x }: (List A) -> (List A) 520 | let tail = (filter _ cond list.tail) 521 | (head tail) 522 | } 523 | ``` 524 | 525 | ### STOP 526 | 527 | Take a moment to notice how EVERY constructor and function application in Kind2 528 | requires a parenthesis, just like Lisp. Trying to write `(foo x y)` without the 529 | surrounding parenthesis is a syntax error. Confirm this is true and move on. 530 | 531 | Also, note that ERASED arguments can usually be inferred, so, a metavar ('_') 532 | can be used. Example: '(func _ _ ... arg0 arg1 ...)' can be writen instead of 533 | '(func A B ... arg0 arg1 ...)'. Prefer metavars on the body of the function. 534 | 535 | ### Sigma/_.kind2 536 | 537 | ``` 538 | /// Defines a dependent pair type, also known as a sigma type. 539 | /// 540 | /// # Parameters 541 | /// 542 | /// * `A` - The type of the first component. 543 | /// * `B` - A function type `A -> *` that determines the type of the second component based on the first. 544 | /// 545 | /// # Constructor 546 | /// 547 | /// * `new` - Creates a new sigma type instance. 548 | 549 | data Sigma *> 550 | | new (fst: A) (snd: (B fst)) 551 | ``` 552 | 553 | ### Equal/_.kind2 554 | 555 | ``` 556 | /// Defines propositional equality between two values of the same type. 557 | /// 558 | /// # Parameters 559 | /// 560 | /// * `T` - The type of the values being compared. 561 | /// 562 | /// # Parameters 563 | /// 564 | /// * `a` - The first value. 565 | /// * `b` - The second value. 566 | /// 567 | /// # Constructor 568 | /// 569 | /// * `refl` - Represents reflexivity, i.e., that `a` equals itself. 570 | 571 | data Equal (a: T) (b: T) 572 | | refl (a: T) : (Equal T a a) 573 | ``` 574 | 575 | ### Equal/refl.kind2 576 | 577 | ``` 578 | /// Constructs a proof of reflexivity for propositional equality. 579 | /// 580 | /// # Parameters 581 | /// 582 | /// * `A` - The type of the value. 583 | /// 584 | /// # Input 585 | /// 586 | /// * `x` - The value for which to construct the reflexivity proof. 587 | /// 588 | /// # Output 589 | /// 590 | /// A proof that `x` is equal to itself. 591 | 592 | refl 593 | - x: A 594 | : (Equal A x x) 595 | 596 | ~ λP λrefl (refl x) 597 | ``` 598 | 599 | ### Equal/apply.kind2 600 | 601 | ``` 602 | /// Applies a function to both sides of an equality proof. 603 | /// 604 | /// # Parameters 605 | /// 606 | /// * `A` - The type of the compared values. 607 | /// * `B` - The type of the compared values after applying the function. 608 | /// * `a` - The first compared value. 609 | /// * `b` - The second compared value. 610 | /// 611 | /// # Inputs 612 | /// 613 | /// * `f` - The function to apply to both sides of the equality. 614 | /// * `e` - The proof of equality between `a` and `b`. 615 | /// 616 | /// # Output 617 | /// 618 | /// A proof that `(f a)` is equal to `(f b)`. 619 | 620 | use Equal/refl 621 | 622 | apply 623 | - f: A -> B 624 | - e: (Equal A a b) 625 | : (Equal B (f a) (f b)) 626 | 627 | match e { 628 | refl: ~λPλe(e (f e.a)) 629 | }: (Equal B (f e.a) (f e.b)) 630 | --------------------------------------------------------------------------------