/** * pi-todo — Persistent todo list for Pi * Track tasks across sessions. Priorities, tags, done/undone. * * /todo add [--pri high|med|low] [--tag ] * /todo list [--tag ] [--pri

] * /todo done * /todo rm * /todo clear */ import type { ExtensionAPI } from '@anthropic-ai/claude-code' import { readFileSync, writeFileSync, mkdirSync } from 'fs' import { join } from 'path' import { homedir } from 'os' interface Todo { id: number; text: string; done: boolean; pri: string; tag: string; created: number } const FILE = join(homedir(), '.pi', 'todos.json') let nextId = 1 function load(): Todo[] { try { const d = JSON.parse(readFileSync(FILE,'utf-8')); nextId = Math.max(...d.map((t:Todo)=>t.id),0)+1; return d } catch { return [] } } function save(todos: Todo[]) { mkdirSync(join(homedir(),'.pi'),{recursive:true}); writeFileSync(FILE, JSON.stringify(todos,null,2)) } function fmt(todos: Todo[]): string { if (!todos.length) return 'No todos. Use `/todo add ` to create one.' const priIcon: Record = { high:'🔴', med:'🟡', low:'🟢' } const lines = todos.map(t => { const check = t.done ? '✅' : '⬜' const p = priIcon[t.pri] || '⚪' const tag = t.tag ? ` \`#${t.tag}\`` : '' return `${check} **${t.id}** ${p} ${t.text}${tag}` }) const done = todos.filter(t=>t.done).length return `## Todos (${done}/${todos.length} done)\n\n${lines.join('\n')}` } export default function init(pi: ExtensionAPI) { pi.addCommand({ name: 'todo', description: 'Persistent todo list', handler: async (args) => { const todos = load() const parts = args.trim().split(/\s+/) const sub = parts[0]?.toLowerCase() if (!sub || sub === 'list') { let filtered = todos const tagIdx = parts.indexOf('--tag'); if (tagIdx>=0 && parts[tagIdx+1]) filtered = filtered.filter(t=>t.tag===parts[tagIdx+1]) const priIdx = parts.indexOf('--pri'); if (priIdx>=0 && parts[priIdx+1]) filtered = filtered.filter(t=>t.pri===parts[priIdx+1]) pi.sendMessage({content:fmt(filtered),display:true},{triggerTurn:false}); return } if (sub === 'add') { let text = args.replace(/^add\s+/,''), pri='med', tag='' const pm = text.match(/--pri\s+(\S+)/); if(pm){pri=pm[1];text=text.replace(/--pri\s+\S+/,'')} const tm = text.match(/--tag\s+(\S+)/); if(tm){tag=tm[1];text=text.replace(/--tag\s+\S+/,'')} text = text.trim() if(!text){pi.sendMessage({content:'Usage: /todo add ',display:true},{triggerTurn:false});return} todos.push({id:nextId++,text,done:false,pri,tag,created:Date.now()}) save(todos) pi.sendMessage({content:`Added #${nextId-1}: ${text}`,display:true},{triggerTurn:false}); return } if (sub === 'done') { const id=parseInt(parts[1],10); const t=todos.find(x=>x.id===id) if(!t){pi.sendMessage({content:'Not found.',display:true},{triggerTurn:false});return} t.done=true; save(todos) pi.sendMessage({content:`✅ Done: ${t.text}`,display:true},{triggerTurn:false}); return } if (sub === 'rm'||sub === 'remove') { const id=parseInt(parts[1],10); const idx=todos.findIndex(x=>x.id===id) if(idx<0){pi.sendMessage({content:'Not found.',display:true},{triggerTurn:false});return} const removed=todos.splice(idx,1)[0]; save(todos) pi.sendMessage({content:`Removed: ${removed.text}`,display:true},{triggerTurn:false}); return } if (sub === 'clear') { save([]); pi.sendMessage({content:'Cleared.',display:true},{triggerTurn:false}); return } pi.sendMessage({content:'**/todo** add|list|done|rm|clear',display:true},{triggerTurn:false}) } }) pi.addTool({ name:'todo_add', description:'Add a persistent todo item.', parameters:{type:'object',properties:{text:{type:'string'},priority:{type:'string',enum:['high','med','low']},tag:{type:'string'}},required:['text']}, handler:async(p:any)=>{const todos=load();todos.push({id:nextId++,text:p.text,done:false,pri:p.priority||'med',tag:p.tag||'',created:Date.now()});save(todos);return`Added #${nextId-1}: ${p.text}`} }) pi.addTool({ name:'todo_list', description:'List all todos.', parameters:{type:'object',properties:{}}, handler:async()=>fmt(load()) }) pi.addTool({ name:'todo_done', description:'Mark a todo as done.', parameters:{type:'object',properties:{id:{type:'number'}},required:['id']}, handler:async(p:{id:number})=>{const todos=load();const t=todos.find(x=>x.id===p.id);if(!t)return'Not found.';t.done=true;save(todos);return`Done: ${t.text}`} }) }