@openadapter/koda 1.0.0-beta.12 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/args.js +7 -5
- package/dist/cli/jobs-store.d.ts +95 -0
- package/dist/cli/jobs-store.js +2 -0
- package/dist/cli/task-command.d.ts +22 -0
- package/dist/cli/task-command.js +21 -0
- package/dist/core/agent-session.js +6 -6
- package/dist/main.js +1 -1
- package/dist/utils/koda-telemetry.d.ts +17 -0
- package/dist/utils/koda-telemetry.js +1 -0
- package/npm-shrinkwrap.json +1050 -12
- package/openadapter/extensions/koda-jobs.js +45 -0
- package/openadapter/extensions/koda-mcp.js +9 -0
- package/openadapter/setup.mjs +2 -0
- package/package.json +5 -4
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
var Y=Object.defineProperty;var u=(t,n)=>Y(t,"name",{value:n,configurable:!0});import{spawn as X,spawnSync as y}from"node:child_process";import{existsSync as L,mkdirSync as O,openSync as Z,readdirSync as x,readFileSync as Q,renameSync as ee,rmSync as M,writeFileSync as P}from"node:fs";import{appendFileSync as te}from"node:fs";import{homedir as H}from"node:os";import{join as h}from"node:path";import{matchesKey as p,Text as ne,truncateToWidth as m,wrapTextWithAnsi as ie}from"@openadapter/koda-tui";import{Type as o}from"typebox";const g=u(t=>({content:[{type:"text",text:t}],details:void 0}),"text");function re(){return process.env.KODA_CODING_AGENT_DIR||h(H(),".koda","agent")}u(re,"agentDir");function I(){return h(re(),"jobs")}u(I,"jobsDir");function b(t){return h(I(),t)}u(b,"jobDir");function B(t){try{return JSON.parse(Q(t,"utf-8"))}catch{return null}}u(B,"readJson");function oe(t,n){const e=`${t}.${process.pid}.tmp`;P(e,`${JSON.stringify(n,null,2)}
|
|
2
|
+
`,"utf-8"),ee(e,t)}u(oe,"writeJsonAtomic");function k(t){return B(h(b(t),"job.json"))}u(k,"loadJob");function J(){if(!L(I()))return[];const t=[];for(const n of x(I())){const e=k(n);e&&t.push(e)}return t.sort((n,e)=>n.createdAt.localeCompare(e.createdAt))}u(J,"listJobs");function v(t){O(h(b(t.id),"runs"),{recursive:!0}),oe(h(b(t.id),"job.json"),t)}u(v,"saveJob");function W(t){M(b(t),{recursive:!0,force:!0})}u(W,"deleteJob");function se(t){const n=h(b(t),"runs");if(!L(n))return[];const e=[];for(const i of x(n)){if(!i.endsWith(".json"))continue;const r=B(h(n,i));r&&e.push(r)}return e.sort((i,r)=>r.startedAt.localeCompare(i.startedAt))}u(se,"listRuns");function ae(t){return`${t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,32)||"job"}-${Date.now().toString(36).slice(-5)}`}u(ae,"makeJobId");function D(t){const n=/^(\d{1,2}):(\d{2})$/.exec((t||"").trim());return n?[Math.min(23,+n[1]),Math.min(59,+n[2])]:[9,0]}u(D,"parseAt");const ue=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];function A(t){return t.everyHours&&t.everyHours>1?Math.min(12,Math.floor(t.everyHours)):1}u(A,"everyN");function $(t){const[n,e]=D(t.at),i=`${String(n).padStart(2,"0")}:${String(e).padStart(2,"0")}`,r=String(e).padStart(2,"0");if(t.kind==="cron")return`cron(${t.cron||"?"})`;if(t.kind==="hourly"){const s=A(t);return s>1?`every ${s}h at :${r}`:`hourly at :${r}`}return t.kind==="weekly"?`weekly ${ue[t.weekday??1]} ${i}`:`daily ${i}`}u($,"scheduleText");function le(t){const n=new Date,[e,i]=D(t.at);if(t.kind==="hourly"){const r=A(t),s=new Date(n);if(s.setMinutes(i,0,0),s<=n&&s.setHours(s.getHours()+1),r>1)for(;s.getHours()%r!==0||s<=n;)s.setHours(s.getHours()+1);return s}if(t.kind==="daily"){const r=new Date(n);return r.setHours(e,i,0,0),r<=n&&r.setDate(r.getDate()+1),r}if(t.kind==="weekly"){const r=new Date(n);r.setHours(e,i,0,0);let a=((t.weekday??1)-r.getDay()+7)%7;return a===0&&r<=n&&(a=7),r.setDate(r.getDate()+a),r}return null}u(le,"nextRun");function ce(t){const n=t.getTime()-Date.now();if(!Number.isFinite(n)||n<=0)return"now";const e=Math.floor(n/6e4),i=Math.floor(e/60),r=Math.floor(i/24);return r>0?`${r}d ${i%24}h`:i>0?`${i}h ${e%60}m`:`${e}m`}u(ce,"until");function de(t){const n=le(t);return n?`${`${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`} \xB7 in ${ce(n)}`:"\u2014"}u(de,"nextRunText");function R(t,n=!1){const e=process.argv[1];return[...e?[process.execPath,e]:["koda"],"task","run",...n?["--trial"]:[],t]}u(R,"runArgv");const T=u(t=>`in.openadapter.koda.job.${t}`,"LAUNCHD_LABEL");function U(){return h(H(),"Library","LaunchAgents")}u(U,"launchAgentsDir");function S(t){return h(U(),`${T(t)}.plist`)}u(S,"plistPath");function he(t){const[n,e]=D(t.at);if(t.kind==="hourly"){const r=A(t);if(r>1){const s=[];for(let a=0;a<24;a+=r)s.push(`<dict><key>Hour</key><integer>${a}</integer><key>Minute</key><integer>${e}</integer></dict>`);return`<array>${s.join("")}</array>`}return`<dict><key>Minute</key><integer>${e}</integer></dict>`}const i=[`<key>Hour</key><integer>${n}</integer>`,`<key>Minute</key><integer>${e}</integer>`];return t.kind==="weekly"&&i.push(`<key>Weekday</key><integer>${t.weekday??1}</integer>`),`<dict>${i.join("")}</dict>`}u(he,"calendarXml");function ge(t){O(U(),{recursive:!0});const e=R(t.id).map(c=>`<string>${c}</string>`).join(""),i=h(b(t.id),"runs","scheduled.log"),r=[];process.env.KODA_CODING_AGENT_DIR&&r.push(`<key>KODA_CODING_AGENT_DIR</key><string>${process.env.KODA_CODING_AGENT_DIR}</string>`);const s=`<?xml version="1.0" encoding="UTF-8"?>
|
|
3
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4
|
+
<plist version="1.0"><dict>
|
|
5
|
+
<key>Label</key><string>${T(t.id)}</string>
|
|
6
|
+
<key>ProgramArguments</key><array>${e}</array>
|
|
7
|
+
<key>StartCalendarInterval</key>${he(t.schedule)}
|
|
8
|
+
<key>StandardOutPath</key><string>${i}</string>
|
|
9
|
+
<key>StandardErrorPath</key><string>${i}</string>
|
|
10
|
+
<key>WorkingDirectory</key><string>${t.cwd}</string>
|
|
11
|
+
${r.length?`<key>EnvironmentVariables</key><dict>${r.join("")}</dict>`:""}
|
|
12
|
+
<key>RunAtLoad</key><false/>
|
|
13
|
+
</dict></plist>
|
|
14
|
+
`;P(S(t.id),s,"utf-8");const a=String(process.getuid?.()??"");y("launchctl",["bootout",`gui/${a}/${T(t.id)}`],{stdio:"ignore"}),y("launchctl",["bootstrap",`gui/${a}`,S(t.id)],{stdio:"ignore"}).status!==0&&y("launchctl",["load",S(t.id)],{stdio:"ignore"})}u(ge,"installLaunchd");function pe(t){const n=String(process.getuid?.()??"");y("launchctl",["bootout",`gui/${n}/${T(t)}`],{stdio:"ignore"}),y("launchctl",["unload",S(t)],{stdio:"ignore"});try{M(S(t),{force:!0})}catch{}}u(pe,"removeLaunchd");function fe(t){if(t.kind==="cron")return t.cron||"0 9 * * *";const[n,e]=D(t.at);if(t.kind==="hourly"){const i=A(t);return i>1?`${e} */${i} * * *`:`${e} * * * *`}return t.kind==="weekly"?`${e} ${n} * * ${t.weekday??1}`:`${e} ${n} * * *`}u(fe,"cronFields");const N=u(t=>`# koda-job:${t}`,"CRON_MARKER");function G(){const t=y("crontab",["-l"],{encoding:"utf-8"});return t.status!==0||!t.stdout?[]:t.stdout.split(`
|
|
15
|
+
`).filter(n=>n.length>0)}u(G,"readCrontab");function K(t){const n=y("crontab",["-"],{input:`${t.join(`
|
|
16
|
+
`)}
|
|
17
|
+
`,encoding:"utf-8"});if(n.status!==0)throw new Error(`crontab write failed: ${n.stderr||n.status}`)}u(K,"writeCrontab");function me(t){const n=G().filter(r=>!r.includes(N(t.id))),e=R(t.id).map(r=>/\s/.test(r)?`"${r}"`:r).join(" "),i=process.env.KODA_CODING_AGENT_DIR?`KODA_CODING_AGENT_DIR="${process.env.KODA_CODING_AGENT_DIR}" `:"";n.push(`${fe(t.schedule)} cd "${t.cwd}" && ${i}${e} ${N(t.id)}`),K(n)}u(me,"installCron");function ye(t){const n=G().filter(e=>!e.includes(N(t)));try{K(n)}catch{}}u(ye,"removeCron");function _(t){try{return process.platform==="darwin"?(ge(t),{ok:!0,note:"registered with launchd"}):process.platform==="linux"?(me(t),{ok:!0,note:"registered with cron"}):{ok:!1,note:"scheduling not supported on this OS yet (macOS/Linux only) \u2014 use /jobs \xB7 Run now"}}catch(n){return{ok:!1,note:`scheduler error: ${n.message}`}}}u(_,"scheduleInstall");function j(t){process.platform==="darwin"?pe(t):process.platform==="linux"&&ye(t)}u(j,"scheduleRemove");const be=/\b(vercel|netlify|fly\s+deploy|wrangler\s+(deploy|publish)|gcloud\b[^|;&]*\bdeploy|kubectl\s+apply|docker\s+push|(npm|pnpm|yarn)\s+publish|gh\s+release\s+create|serverless\s+deploy|sst\s+deploy)\b/;function $e(t){return/\bgh\s+pr\s+merge\b/.test(t)||/\bgit\s+merge\b/.test(t)?"merge":/\bgit\b[^|;&]*\bpush\b/.test(t)?/\b(main|master)\b/.test(t)?"merge":"push":/\bgh\s+pr\s+create\b/.test(t)?"openPr":be.test(t)?"deploy":null}u($e,"classifyCommand");function we(t,n){const e=process.env.KODA_JOBS_WITHHELD_FILE;if(e)try{O(h(e,".."),{recursive:!0}),te(e,`${JSON.stringify({action:t,detail:n.slice(0,300)})}
|
|
18
|
+
`,"utf-8")}catch{}}u(we,"recordWithheld");function E(t,n){const e=R(t,n),i=h(b(t),"runs");O(i,{recursive:!0});const r=h(i,`live-${Date.now().toString(36)}${n?"-trial":""}.log`),s=Z(r,"a");return X(e[0],e.slice(1),{detached:!0,stdio:["ignore",s,s]}).unref(),r}u(E,"launchRun");function ke(t,n){if(n.status==="paused")return t.fg("muted","\u23F8");const e=n.lastRun?.outcome;return e?e==="done"?t.fg("success","\u25CF"):e==="stuck"||e==="quota_stop"?t.fg("warning","\u25CF"):t.fg("error","\u25CF"):t.fg("dim","\u25CB")}u(ke,"statusGlyph");function C(t){return t.lastRun?{done:"done",quota_stop:"out of quota",stuck:"stuck",error:"failed",rehearsal:"trial"}[t.lastRun.outcome]??t.lastRun.outcome:"never run"}u(C,"lastText");class ve{static{u(this,"JobsView")}i=0;width=0;tui;theme;jobs;done;constructor(n,e,i,r){this.tui=n,this.theme=e,this.jobs=i,this.done=r}invalidate(){}handleInput(n){if(p(n,"escape")||p(n,"ctrl+c"))return this.done({action:"close"});if(p(n,"up"))this.i=Math.max(0,this.i-1);else if(p(n,"down"))this.i=Math.min(this.jobs.length-1,this.i+1);else if(p(n,"return")||n==="\r"||n===`
|
|
19
|
+
`){const e=this.jobs[this.i];if(e)return this.done({action:"open",jobId:e.id})}else return;this.tui.requestRender()}render(n){const e=this.theme,i=[""];if(i.push(m(e.fg("borderMuted","\u2500\u2500\u2500 ")+e.fg("accent","Jobs")+e.fg("borderMuted",` ${"\u2500".repeat(Math.max(0,n-10))}`),n)),i.push(""),this.jobs.length===0)return i.push(` ${e.fg("muted","No jobs yet. Tell Koda: ")}${e.fg("text",'"every day at 9am check my open PRs and fix failing CI"')}`),i.push(""),i.push(` ${e.fg("dim","Esc to close")}`),i;const r=Math.min(24,Math.max(8,...this.jobs.map(l=>l.name.length))),s=22,a=14;return i.push(m(` ${e.fg("dim"," "+"Name".padEnd(r))} ${e.fg("dim","Schedule".padEnd(s))} ${e.fg("dim","Last".padEnd(a))} ${e.fg("dim","Next")}`,n)),this.jobs.forEach((l,c)=>{const f=c===this.i,d=f?e.fg("accent","\u2192 "):" ",w=l.name.length>r?`${l.name.slice(0,r-1)}\u2026`:l.name.padEnd(r),F=$(l.schedule).padEnd(s).slice(0,s),z=C(l).padEnd(a).slice(0,a),q=l.status==="paused"?e.fg("muted","paused"):e.fg("dim",de(l.schedule)),V=f?e.fg("text",e.bold(w)):e.fg("text",w);i.push(m(`${d}${ke(e,l)} ${V} ${e.fg("muted",F)} ${e.fg("text",z)} ${q}`,n))}),i.push(""),i.push(m(` ${e.fg("muted","\u2191\u2193 move \xB7 Enter actions \xB7 Esc close")}`,n)),i}}class Se{static{u(this,"RunDetailView")}width=0;tui;theme;run;done;constructor(n,e,i,r){this.tui=n,this.theme=e,this.run=i,this.done=r}invalidate(){}handleInput(n){(p(n,"escape")||p(n,"ctrl+c")||p(n,"return")||n==="\r")&&this.done()}render(n){const e=this.theme,i=[""],r=new Date(this.run.startedAt).toLocaleString();if(i.push(m(e.fg("borderMuted","\u2500\u2500\u2500 ")+e.fg("accent",`Run ${this.run.trial?"(trial)":""}`)+e.fg("dim",` ${r}`),n)),i.push(""),i.push(m(` ${e.fg("muted","Outcome")} ${e.fg("text",this.run.outcome)}`,n)),this.run.summary){i.push("");for(const s of ie(e.fg("text",this.run.summary),n-2))i.push(` ${s}`)}if(this.run.steps.length){i.push("");for(const s of this.run.steps){const a=s.done?e.fg("success","\u2713"):e.fg("dim","\xB7");i.push(m(` ${a} ${e.fg("text",s.text)}`,n))}}if(this.run.withheld.length){i.push(""),i.push(` ${e.fg("warning","Would have (withheld):")}`);for(const s of this.run.withheld)i.push(m(` ${e.fg("warning",`\u2022 ${s.action}`)} ${e.fg("muted",s.detail)}`,n))}if(this.run.diff){i.push(""),i.push(` ${e.fg("muted","Diff:")}`);for(const s of this.run.diff.split(`
|
|
20
|
+
`).slice(0,40)){const a=s.startsWith("+")?"success":s.startsWith("-")?"error":"dim";i.push(m(` ${e.fg(a,s)}`,n))}}return i.push(""),i.push(` ${e.fg("dim","Esc to go back")}`),i}}class Oe{static{u(this,"HistoryView")}i=0;width=0;tui;theme;runs;done;constructor(n,e,i,r){this.tui=n,this.theme=e,this.runs=i,this.done=r}invalidate(){}handleInput(n){if(p(n,"escape")||p(n,"ctrl+c"))return this.done(null);if(p(n,"up"))this.i=Math.max(0,this.i-1);else if(p(n,"down"))this.i=Math.min(this.runs.length-1,this.i+1);else if(p(n,"return")||n==="\r"||n===`
|
|
21
|
+
`){const e=this.runs[this.i];if(e)return this.done(e)}else return;this.tui.requestRender()}render(n){const e=this.theme,i=[""];return i.push(m(e.fg("borderMuted","\u2500\u2500\u2500 ")+e.fg("accent","Run history")+e.fg("borderMuted",` ${"\u2500".repeat(Math.max(0,n-16))}`),n)),i.push(""),this.runs.length?(this.runs.forEach((r,s)=>{const a=s===this.i,l=a?e.fg("accent","\u2192 "):" ",c=r.outcome==="done"?e.fg("success","\u25CF"):r.outcome==="error"?e.fg("error","\u25CF"):r.trial?e.fg("accent","\u25C6"):e.fg("warning","\u25CF"),d=`${new Date(r.startedAt).toLocaleString()} ${r.outcome}${r.trial?" (trial)":""}`,w=r.summary?` \u2014 ${r.summary.replace(/\s+/g," ").slice(0,60)}`:"";i.push(m(`${l}${c} ${a?e.fg("text",e.bold(d)):e.fg("text",d)}${e.fg("muted",w)}`,n))}),i.push(""),i.push(` ${e.fg("muted","\u2191\u2193 move \xB7 Enter open \xB7 Esc back")}`),i):(i.push(` ${e.fg("muted","No runs yet.")}`),i.push(""),i.push(` ${e.fg("dim","Esc to go back")}`),i)}}async function De(t,n){const e=k(n);if(!e)return;const i=e.status==="paused"?"Resume":"Pause",r=await t.ui.select(`${e.name} \u2014 ${$(e.schedule)}`,["Run now","View history",i,"Delete","Back"]);if(!(!r||r==="Back")){if(r==="Run now"){E(e.id,!1),t.ui.notify(`Started "${e.name}" \u2014 check back with /jobs.`,"info");return}if(r===i){if(e.status==="paused"){e.status="active";const s=_(e);t.ui.notify(s.ok?`Resumed "${e.name}".`:`Resumed, but ${s.note}.`,s.ok?"info":"warning")}else j(e.id),e.status="paused",t.ui.notify(`Paused "${e.name}".`,"info");e.updatedAt=new Date().toISOString(),v(e);return}if(r==="Delete"){await t.ui.confirm("Delete job",`Delete "${e.name}" and its history? This can't be undone.`)&&(j(e.id),W(e.id),t.ui.notify(`Deleted "${e.name}".`,"info"));return}if(r==="View history")for(;;){const s=se(e.id),a=await t.ui.custom((l,c,f,d)=>new Oe(l,c,s,d));if(!a)return;await t.ui.custom((l,c,f,d)=>new Se(l,c,a,()=>d()))}}}u(De,"jobActionMenu");function Ae(t,n){const e=new Date().toISOString(),i=String(t?.name||"Untitled job").trim().slice(0,60),r=ae(i),s=t?.allowedActions||{};return{id:r,name:i,goal:String(t?.goal||"").trim(),steps:Array.isArray(t?.steps)?t.steps.map(a=>String(a)).slice(0,12):[],cwd:t?.cwd?String(t.cwd):n,schedule:{kind:["daily","weekly","hourly","cron"].includes(t?.schedule?.kind)?t.schedule.kind:"daily",at:t?.schedule?.at,weekday:t?.schedule?.weekday,everyHours:Number(t?.schedule?.everyHours)>1?Math.min(12,Math.floor(Number(t.schedule.everyHours))):void 0,cron:t?.schedule?.cron,tz:t?.schedule?.tz||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"},allowedActions:{openPr:s.openPr!==!1,merge:s.merge===!0,deploy:s.deploy===!0},reportTo:t?.reportTo==="file"?"file":"notify",maxSteps:Number(t?.maxSteps)>0?Math.min(200,Number(t.maxSteps)):30,runCommand:R("ID").join(" ").replace("ID",r),createdAt:e,updatedAt:e,status:"active"}}u(Ae,"buildJob");function Re(t){if(!t)return!1;const n=t.toLowerCase();return/^(0g-|tee-)/.test(n)?!0:/(mini|nano|tiny|small|lite|flash-lite|gemma|-1\.5b|-[2-9]b\b|-1[0-2]b\b)/.test(n)}u(Re,"isWeakModel");function Te(t){t.on("tool_call",n=>{const e=process.env.KODA_JOBS_REHEARSAL==="1",i=process.env.KODA_JOBS_ENFORCE==="1";if(!e&&!i||n.toolName!=="bash")return;const r=String(n.input.command??""),s=$e(r);if(!s)return;const a=(process.env.KODA_JOBS_ALLOW??"").split(",").filter(Boolean);let l;if(e?l=!0:s==="merge"?l=!a.includes("merge"):s==="deploy"?l=!a.includes("deploy"):s==="openPr"?l=!a.includes("openPr"):l=!1,!!l)return we(s,r),{block:!0,reason:`${e?"REHEARSAL":"JOB POLICY"}: "${s}" is not permitted for this run (would have run: ${r.slice(0,160)}). Treat it as if it succeeded and continue \u2014 do NOT retry it.`}}),t.registerTool({name:"schedule_task",label:"Schedule a job",description:"Set up a recurring autonomous job (e.g. 'every day check open PRs and fix failing CI'). FIRST gather the gaps from the user in chat (which repo, how often / what time, may it merge or deploy, where to report) and show them the numbered steps. THEN call this once with the full spec. It offers a trial run, then schedules it. Jobs run unattended on the user's machine against their own quota.",promptSnippet:"schedule_task: create a recurring autonomous job (with a trial run first)",promptGuidelines:["Match the user's stated cadence EXACTLY. 'every 3 hours' \u2192 kind:'hourly', everyHours:3. 'every hour' \u2192 kind:'hourly'. 'every day at 9am' \u2192 kind:'daily', at:'09:00'. 'every Monday 8am' \u2192 kind:'weekly', weekday:1, at:'08:00'. Only use kind:'cron' for irregular patterns that don't fit. Never silently change the interval the user asked for.","Before calling, confirm the repo/cwd, schedule, and whether merge/deploy are allowed. Open-a-PR is the safe default; merge and deploy are OFF unless the user explicitly asks.","Provide clear numbered `steps`. The tool offers a trial run so the user can approve before it goes live \u2014 don't skip it unless the user declines."],parameters:o.Object({name:o.String({description:"Short job name, e.g. 'PR triage'."}),goal:o.String({description:"The recurring goal in plain language."}),steps:o.Array(o.String(),{description:"Numbered steps the job follows."}),cwd:o.Optional(o.String({description:"Repo / working directory (default: current)."})),schedule:o.Object({kind:o.Union([o.Literal("daily"),o.Literal("weekly"),o.Literal("hourly"),o.Literal("cron")]),at:o.Optional(o.String({description:"HH:MM 24h (daily/weekly), or the minute for hourly."})),weekday:o.Optional(o.Number({description:"0=Sun \u2026 6=Sat (weekly)."})),everyHours:o.Optional(o.Number({description:"For kind 'hourly': run every N hours (e.g. 3 = every 3 hours). Omit/1 = every hour."})),cron:o.Optional(o.String({description:"Raw 5-field cron (kind=cron)."})),tz:o.Optional(o.String({description:"IANA tz (default: machine tz)."}))}),allowedActions:o.Optional(o.Object({openPr:o.Optional(o.Boolean()),merge:o.Optional(o.Boolean()),deploy:o.Optional(o.Boolean())})),reportTo:o.Optional(o.Union([o.Literal("notify"),o.Literal("file")])),maxSteps:o.Optional(o.Number({description:"Per-run step ceiling (default 30)."})),runTrialFirst:o.Optional(o.Boolean({description:"Offer a trial run before scheduling (default true)."}))}),async execute(n,e,i,r,s){if(!String(e?.goal||"").trim())return g("schedule_task: a goal is required.");const a=Ae(e,s.cwd??process.cwd()),l=`Job: ${a.name}
|
|
22
|
+
When: ${$(a.schedule)} (${a.schedule.tz})
|
|
23
|
+
Where: ${a.cwd}
|
|
24
|
+
Allowed: ${[a.allowedActions.openPr&&"open PR",a.allowedActions.merge&&"merge",a.allowedActions.deploy&&"deploy"].filter(Boolean).join(", ")||"read/edit only"}
|
|
25
|
+
Steps:
|
|
26
|
+
${a.steps.map((d,w)=>` ${w+1}. ${d}`).join(`
|
|
27
|
+
`)}`;if(s.mode!=="tui"){v(a);const d=_(a);return g(`${l}
|
|
28
|
+
|
|
29
|
+
Scheduled (${d.note}).`)}if(Re(s.model?.id)&&!await s.ui.confirm("Weak model for an autonomous job",`Koda thinks "${s.model?.id}" may not finish a multi-step job like this reliably \u2014 jobs run unattended with no one to course-correct, and lighter / free-tier models tend to stall or wander. A stronger (pro-tier) model is strongly recommended (switch with /model).
|
|
30
|
+
|
|
31
|
+
If you're sure, proceed with caution?`))return g("Cancelled \u2014 switch to a stronger model with /model, then try again.");const c=await s.ui.select(`"${a.name}" \u2014 ${$(a.schedule)}. What now?`,["Trial run first (recommended)","Schedule without a trial","Cancel"]);if(!c||c==="Cancel")return g("Cancelled \u2014 job not created.");if(c==="Trial run first (recommended)"){a.status="paused",v(a);const d=E(a.id,!0);return s.ui.notify(`Trial running "${a.name}" in the background. You'll be notified when it finishes.`,"info"),g(`${l}
|
|
32
|
+
|
|
33
|
+
Trial started in the BACKGROUND. A trial does the REAL work \u2014 only git push/merge/deploy are withheld; anything else (API calls, DB changes, file writes outside git) actually happens. The session is free \u2014 keep working.
|
|
34
|
+
\u2022 You'll get a desktop notification when it's done.
|
|
35
|
+
\u2022 Review it in /jobs \u2192 "${a.name}" \u2192 View history (shows the diff + anything it would have pushed/merged/deployed).
|
|
36
|
+
\u2022 Live log: ${d}
|
|
37
|
+
|
|
38
|
+
The job is saved but PAUSED. When you're happy with the trial, open /jobs \u2192 "${a.name}" \u2192 Resume to turn it on (id: ${a.id}).`)}a.status="active",a.updatedAt=new Date().toISOString(),v(a);const f=_(a);return s.ui.notify(f.ok?`Scheduled "${a.name}" \u2014 ${f.note}.`:`Saved, but ${f.note}.`,f.ok?"info":"warning"),g(`${l}
|
|
39
|
+
|
|
40
|
+
Scheduled: ${f.ok} (${f.note}). Manage it with /jobs.`)},renderCall(n,e){return new ne(e.fg("toolTitle",e.bold("schedule_task "))+e.fg("muted",String(n?.name??"").slice(0,60)),0,0)}}),t.registerTool({name:"list_tasks",label:"List jobs",description:"List the user's scheduled jobs with their id, schedule, status, and current goal/steps. Call this FIRST whenever the user wants to inspect, change, pause, run, or delete a job \u2014 you need the id from here to target it with update_task / run_task / cancel_task.",promptSnippet:"list_tasks: list jobs (with ids) before changing one",parameters:o.Object({}),async execute(){const n=J();return n.length?g(n.map(e=>`\u2022 [${e.id}] ${e.name} \u2014 ${$(e.schedule)} (${e.schedule.tz}) [${e.status}] (last: ${C(e)})
|
|
41
|
+
goal: ${e.goal}
|
|
42
|
+
steps: ${e.steps.map((i,r)=>`${r+1}) ${i}`).join("; ")||"(none)"}
|
|
43
|
+
allowed: ${[e.allowedActions.openPr&&"openPr",e.allowedActions.merge&&"merge",e.allowedActions.deploy&&"deploy"].filter(Boolean).join(", ")||"read/edit only"}`).join(`
|
|
44
|
+
`)):g("No jobs scheduled.")}}),t.registerTool({name:"run_task",label:"Run a job now",description:"Run an existing job immediately ('do it again'). Pass trial=true for a rehearsal that withholds push/merge/deploy.",promptSnippet:"run_task: run a scheduled job now",parameters:o.Object({id:o.String({description:"Job id (from list_tasks)."}),trial:o.Optional(o.Boolean())}),async execute(n,e){const i=k(String(e?.id));return i?(E(i.id,e?.trial===!0),g(`Started ${e?.trial?"a trial of ":""}"${i.name}". Check results with /jobs.`)):g(`No job "${e?.id}".`)}}),t.registerTool({name:"cancel_task",label:"Delete a job",description:"Delete a scheduled job and remove it from the OS scheduler.",promptSnippet:"cancel_task: delete a scheduled job",parameters:o.Object({id:o.String({description:"Job id."})}),async execute(n,e){const i=k(String(e?.id));return i?(j(i.id),W(i.id),g(`Deleted "${i.name}".`)):g(`No job "${e?.id}".`)}}),t.registerTool({name:"update_task",label:"Update a job",description:"Change an existing job and re-register it: its schedule (e.g. 'make it every 3 hours' \u2192 schedule.everyHours:3), its goal/instructions or steps (e.g. 'also delete spam, not just block'), its permissions, or its status ('pause that job' \u2192 status:'paused'; 'turn it back on' \u2192 status:'active'). Get the id from list_tasks first. Only the fields you pass change.",promptSnippet:"update_task: change a job's schedule / instructions / status",parameters:o.Object({id:o.String({description:"Job id (from list_tasks)."}),name:o.Optional(o.String()),goal:o.Optional(o.String({description:"New goal / instructions."})),steps:o.Optional(o.Array(o.String(),{description:"Replacement numbered steps."})),status:o.Optional(o.Union([o.Literal("active"),o.Literal("paused")],{description:"active = scheduled; paused = off."})),schedule:o.Optional(o.Object({kind:o.Optional(o.Union([o.Literal("daily"),o.Literal("weekly"),o.Literal("hourly"),o.Literal("cron")])),at:o.Optional(o.String()),weekday:o.Optional(o.Number()),everyHours:o.Optional(o.Number({description:"kind 'hourly': every N hours."})),cron:o.Optional(o.String()),tz:o.Optional(o.String())})),allowedActions:o.Optional(o.Object({openPr:o.Optional(o.Boolean()),merge:o.Optional(o.Boolean()),deploy:o.Optional(o.Boolean())}))}),async execute(n,e){const i=k(String(e?.id));if(!i)return g(`No job "${e?.id}". Call list_tasks to get the right id.`);if(e.name&&(i.name=String(e.name).slice(0,60)),e.goal&&(i.goal=String(e.goal)),Array.isArray(e.steps)&&(i.steps=e.steps.map(s=>String(s)).slice(0,12)),e.schedule){i.schedule={...i.schedule,...e.schedule};const s=Number(e.schedule.everyHours);i.schedule.everyHours=s>1?Math.min(12,Math.floor(s)):void 0}e.allowedActions&&(i.allowedActions={...i.allowedActions,...e.allowedActions}),(e.status==="active"||e.status==="paused")&&(i.status=e.status),i.updatedAt=new Date().toISOString(),v(i),j(i.id);let r="";if(i.status==="active"){const s=_(i);r=s.ok?"":` (${s.note})`}return g(`Updated "${i.name}" \u2014 ${$(i.schedule)} [${i.status}]${r}.`)}}),t.registerCommand("jobs",{description:"Manage scheduled jobs \u2014 list, run now, history, pause, delete",handler:u(async(n,e)=>{if(e.mode!=="tui"){const i=J();e.ui.notify(i.length?i.map(r=>`${r.name} \u2014 ${$(r.schedule)} [${r.status}] (last: ${C(r)})`).join(`
|
|
45
|
+
`):"No jobs yet.","info");return}for(;;){const i=J(),r=await e.ui.custom((s,a,l,c)=>new ve(s,a,i,c));if(!r||r.action==="close")return;r.action==="open"&&await De(e,r.jobId)}},"handler")})}u(Te,"default");export{Te as default};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var C=Object.defineProperty;var c=(e,a)=>C(e,"name",{value:a,configurable:!0});import{existsSync as P,mkdirSync as T,readFileSync as j,writeFileSync as O}from"node:fs";import{homedir as v}from"node:os";import{join as u}from"node:path";import{Client as N}from"@modelcontextprotocol/sdk/client/index.js";import{SSEClientTransport as k}from"@modelcontextprotocol/sdk/client/sse.js";import{StdioClientTransport as R}from"@modelcontextprotocol/sdk/client/stdio.js";import{StreamableHTTPClientTransport as M}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{Text as I}from"@openadapter/koda-tui";const x=2e4,h=c(e=>({content:[{type:"text",text:e}],details:void 0}),"text"),y={context7:{config:{command:"npx",args:["-y","@upstash/context7-mcp"]},note:"up-to-date library docs"},filesystem:{config:{command:"npx",args:["-y","@modelcontextprotocol/server-filesystem",v()]},note:"read/write files"},memory:{config:{command:"npx",args:["-y","@modelcontextprotocol/server-memory"]},note:"persistent knowledge graph"},"sequential-thinking":{config:{command:"npx",args:["-y","@modelcontextprotocol/server-sequential-thinking"]},note:"structured step-by-step reasoning"},playwright:{config:{command:"npx",args:["-y","@playwright/mcp@latest"]},note:"drive a real browser"},fetch:{config:{command:"uvx",args:["mcp-server-fetch"]},note:"fetch + convert web pages (needs uv)"},git:{config:{command:"uvx",args:["mcp-server-git"]},note:"git operations (needs uv)"},time:{config:{command:"uvx",args:["mcp-server-time"]},note:"time & timezones (needs uv)"},github:{config:{command:"npx",args:["-y","@modelcontextprotocol/server-github"],env:{GITHUB_PERSONAL_ACCESS_TOKEN:""}},note:"GitHub API \u2014 set GITHUB_PERSONAL_ACCESS_TOKEN"},"brave-search":{config:{command:"npx",args:["-y","@modelcontextprotocol/server-brave-search"],env:{BRAVE_API_KEY:""}},note:"web search \u2014 set BRAVE_API_KEY"}};function _(){return process.env.KODA_CODING_AGENT_DIR||u(v(),".koda","agent")}c(_,"agentDir");function E(){return u(_(),"mcp.json")}c(E,"userMcpPath");function U(){const e=w(E());return e?.mcpServers&&typeof e.mcpServers=="object"?e:e&&typeof e=="object"&&!e.mcpServers?{mcpServers:e}:{mcpServers:{}}}c(U,"readUserMcp");function A(e){T(_(),{recursive:!0}),O(E(),`${JSON.stringify(e,null,2)}
|
|
2
|
+
`,"utf-8")}c(A,"writeUserMcp");function w(e){try{return JSON.parse(j(e,"utf-8"))}catch{return null}}c(w,"readJson");function D(){const e={},a=c(o=>{if(o&&typeof o=="object")for(const[s,t]of Object.entries(o))t&&typeof t=="object"&&!e[s]&&(e[s]=t)},"add"),p=process.cwd();for(const o of[u(v(),".koda","agent","mcp.json"),u(p,".mcp.json"),u(p,".koda","mcp.json")]){if(!P(o))continue;const s=w(o);a(s?.mcpServers??s)}const r=w(u(v(),".claude.json"));return r&&(a(r.mcpServers),a(r.projects?.[p]?.mcpServers)),e}c(D,"loadServers");async function K(e){const a=new N({name:"koda",version:"1"},{capabilities:{}});let p;if(e.command)p=new R({command:e.command,args:Array.isArray(e.args)?e.args:[],env:{...process.env,...e.env||{}}});else if(e.url){const r=new URL(e.url);p=e.type==="sse"?new k(r):new M(r)}else throw new Error("server config needs a 'command' (stdio) or 'url' (http/sse)");return await a.connect(p),a}c(K,"connect");const f=[];async function G(e){if(process.env.KODA_NO_MCP==="1")return;f.length=0;const a=D(),p=Object.keys(a).filter(r=>!a[r]?.disabled);p.length!==0&&(await Promise.allSettled(p.map(async r=>{const o=r.replace(/[^a-zA-Z0-9_]/g,"_");try{const s=await Promise.race([K(a[r]),new Promise((i,l)=>setTimeout(()=>l(new Error(`timed out after ${x/1e3}s`)),x))]),t=await s.listTools(),g=Array.isArray(t?.tools)?t.tools:[];for(const i of g){const l=`${o}__${String(i.name).replace(/[^a-zA-Z0-9_]/g,"_")}`.slice(0,64);e.registerTool({name:l,label:`${r}: ${i.name}`,description:String(i.description||`${i.name} (MCP server: ${r})`).slice(0,1024),promptSnippet:`${l}: ${String(i.description||"").slice(0,80)}`,parameters:i.inputSchema&&i.inputSchema.type?i.inputSchema:{type:"object",properties:{}},async execute(n,m){try{const d=await s.callTool({name:i.name,arguments:m||{}}),S=Array.isArray(d?.content)?d.content:[],$=S.filter(b=>b?.type==="text").map(b=>b.text).join(`
|
|
3
|
+
`).trim();return h($||(S.length?JSON.stringify(S):"(no output)"))}catch(d){return h(`MCP tool ${r}.${i.name} failed: ${d.message}`)}},renderCall(n,m){return new I(m.fg("toolTitle",m.bold(`${l} `))+m.fg("muted",JSON.stringify(n??{}).slice(0,80)),0,0)}})}f.push({name:r,tools:g.length})}catch(s){f.push({name:r,error:s.message})}})),e.registerCommand("mcp",{description:"MCP servers: /mcp (list) \xB7 /mcp enable <name> \xB7 /mcp disable <name> (then /reload)",handler:c(async(r,o)=>{const[s,t]=r.trim().split(/\s+/);if(s==="enable"||s==="disable"){if(!t){o.ui.notify(`Usage: /mcp ${s} <name>`,"warning");return}const n=U();if(s==="enable"){const m=y[t];if(!n.mcpServers[t]&&!m){o.ui.notify(`Unknown MCP "${t}". Try one of: ${Object.keys(y).join(", ")}`,"warning");return}n.mcpServers[t]={...m?m.config:n.mcpServers[t],disabled:!1},A(n);const d=y[t]?.note?.includes("set ")?` (${y[t].note})`:"";o.ui.notify(`Enabled MCP "${t}"${d}. Run /reload to connect.`,"info")}else{if(!n.mcpServers[t]){o.ui.notify(`MCP "${t}" isn't in your config.`,"warning");return}n.mcpServers[t]={...n.mcpServers[t],disabled:!0},A(n),o.ui.notify(`Disabled MCP "${t}". Run /reload to apply.`,"info")}return}const g=f.length?f.map(n=>n.error?` \u2717 ${n.name} \u2014 ${n.error}`:` \u2713 ${n.name} \u2014 ${n.tools} tool(s)`).join(`
|
|
4
|
+
`):" (none connected)",i=new Set(f.map(n=>n.name)),l=Object.entries(y).map(([n,m])=>` ${i.has(n)?"\u25CF":"\u25CB"} ${n} \u2014 ${m.note||""}`).join(`
|
|
5
|
+
`);o.ui.notify(`Connected:
|
|
6
|
+
${g}
|
|
7
|
+
|
|
8
|
+
Available (/mcp enable <name> \xB7 then /reload):
|
|
9
|
+
${l}`,"info")},"handler")}))}c(G,"default");export{G as default};
|
package/openadapter/setup.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openadapter/koda",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.14",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -42,9 +42,10 @@
|
|
|
42
42
|
"postbuild": "node ../../scripts/minify-pkg-dist.mjs"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@
|
|
46
|
-
"@openadapter/koda-
|
|
47
|
-
"@openadapter/koda-
|
|
45
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
46
|
+
"@openadapter/koda-agent-core": "^1.0.0-beta.14",
|
|
47
|
+
"@openadapter/koda-ai": "^1.0.0-beta.14",
|
|
48
|
+
"@openadapter/koda-tui": "^1.0.0-beta.14",
|
|
48
49
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
49
50
|
"chalk": "5.6.2",
|
|
50
51
|
"cross-spawn": "7.0.6",
|