@tchayen/oru 0.0.1 → 0.0.5
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.js +77 -77
- package/dist/mcp/index.js +4 -4
- package/package.json +9 -11
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`),{...
|
|
2
|
+
var Kn=Object.defineProperty;var K=(t,e)=>()=>(t&&(e=t(t=0)),e);var Ae=(t,e)=>{for(var n in e)Kn(t,n,{get:e[n],enumerable:!0})};import le from"fs";import bt from"path";import xr from"os";import{parse as Or}from"smol-toml";function ye(){return process.env.ORU_CONFIG_DIR?bt.join(process.env.ORU_CONFIG_DIR,"config.toml"):bt.join(xr.homedir(),".oru","config.toml")}function ht(t){let e=t??ye();if(!le.existsSync(e))return{...yt};let n=le.readFileSync(e,"utf-8"),o;try{o=Or(n)}catch(s){return process.stderr.write(`Warning: Could not parse config file at ${e}: ${s instanceof Error?s.message:String(s)}. Using defaults.
|
|
3
|
+
`),{...yt}}let r={...yt};return typeof o.date_format=="string"&&$r.has(o.date_format)&&(r.date_format=o.date_format),typeof o.first_day_of_week=="string"&&Dr.has(o.first_day_of_week.toLowerCase())&&(r.first_day_of_week=o.first_day_of_week.toLowerCase()),typeof o.output_format=="string"&&Nr.has(o.output_format)&&(r.output_format=o.output_format),typeof o.next_month=="string"&&Rr.has(o.next_month)&&(r.next_month=o.next_month),typeof o.auto_update_check=="boolean"&&(r.auto_update_check=o.auto_update_check),typeof o.telemetry=="boolean"&&(r.telemetry=o.telemetry),typeof o.telemetry_notice_shown=="boolean"&&(r.telemetry_notice_shown=o.telemetry_notice_shown),typeof o.backup_path=="string"&&o.backup_path.length>0&&(r.backup_path=o.backup_path),typeof o.backup_interval=="number"&&o.backup_interval>0&&(r.backup_interval=o.backup_interval),r}function be(t,e){let n=ye(),o="";le.existsSync(n)&&(o=le.readFileSync(n,"utf-8"));let r=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),s=new RegExp(`^${r}\\s*=\\s*.*`,"m");s.test(o)?o=o.replace(s,`${t} = ${e}`):o=`${o.trimEnd()}
|
|
4
4
|
${t} = ${e}
|
|
5
|
-
`,
|
|
5
|
+
`,le.mkdirSync(bt.dirname(n),{recursive:!0}),le.writeFileSync(n,o)}var yt,$r,Dr,Nr,Rr,bn,kt=K(()=>{"use strict";yt={date_format:"mdy",first_day_of_week:"monday",output_format:"text",next_month:"same_day",auto_update_check:!0,telemetry:!0,telemetry_notice_shown:!1,backup_path:null,backup_interval:60},$r=new Set(["dmy","mdy"]),Dr=new Set(["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]),Nr=new Set(["text","json"]),Rr=new Set(["same_day","first"]);bn=`# oru configuration
|
|
6
6
|
# Docs: https://github.com/tchayen/oru#configuration
|
|
7
7
|
|
|
8
8
|
# Date input format for slash dates (e.g. 03/04/2026)
|
|
@@ -38,36 +38,36 @@ telemetry = true
|
|
|
38
38
|
|
|
39
39
|
# Minimum minutes between auto-backups (default: 60)
|
|
40
40
|
# backup_interval = 60
|
|
41
|
-
`});var
|
|
41
|
+
`});var de=K(()=>{"use strict"});var J,Mn,$e=K(()=>{"use strict";J="0.0.5",Mn="4753478"});var It={};Ae(It,{buildEvent:()=>io,detectCI:()=>Pn,extractCommandAndFlags:()=>ro,getTelemetryDisabledReason:()=>Rt,isTelemetryEnabled:()=>Nt,sendEvent:()=>so,showFirstRunNotice:()=>ao});function Nt(t){return!(process.env.DO_NOT_TRACK==="1"||process.env.ORU_TELEMETRY_DISABLED==="1"||t.telemetry===!1)}function Rt(t){return process.env.DO_NOT_TRACK==="1"?"disabled (via DO_NOT_TRACK)":process.env.ORU_TELEMETRY_DISABLED==="1"?"disabled (via ORU_TELEMETRY_DISABLED)":t.telemetry===!1?"disabled (via config)":null}function Pn(){return no.some(t=>process.env[t])}function ro(t){let e=t.slice(2),n=[],o="",r=!1;for(let s=0;s<e.length;s++){let a=e[s];if(a.startsWith("-")){let u=a.includes("=")?a.slice(0,a.indexOf("=")):a;n.push(u),!a.includes("=")&&s+1<e.length&&!e[s+1].startsWith("-")&&s++}else r?r&&!o.includes(" ")&&oo(o,a)&&(o=`${o} ${a}`):(o=a,r=!0)}return{command:o||"(unknown)",flags:n}}function oo(t,e){return{config:["init","path"],filter:["add","list","show","remove"],completions:["bash","zsh","fish"],telemetry:["status","enable","disable"],...{}}[t]?.includes(e)??!1}function so(t){let e=process.env.ORU_TELEMETRY_URL??eo;try{let n=new AbortController,o=setTimeout(()=>n.abort(),to);fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:n.signal}).catch(r=>{process.env.ORU_DEBUG==="1"&&console.error("Telemetry send failed:",r)}).finally(()=>clearTimeout(o))}catch(n){process.env.ORU_DEBUG==="1"&&console.error("Telemetry send failed:",n)}}function ao(t){try{if(t.telemetry_notice_shown)return;process.stderr.isTTY&&process.stderr.write(`
|
|
42
42
|
oru collects anonymous usage data to improve the tool.
|
|
43
43
|
To disable: oru telemetry disable (or set DO_NOT_TRACK=1)
|
|
44
44
|
|
|
45
|
-
`),
|
|
46
|
-
Update available: ${
|
|
45
|
+
`),be("telemetry_notice_shown","true")}catch(e){process.env.ORU_DEBUG==="1"&&console.error("Telemetry notice failed:",e)}}function io(t,e,n,o,r){let s={cli_version:J,command:t,flags:e,os:process.platform,arch:process.arch,node_version:process.version,is_ci:Pn(),duration_ms:n,exit_code:o};return r!==void 0&&(s.error=r),s}var eo,to,no,Qe=K(()=>{"use strict";kt();de();$e();eo="https://telemetry.oru.sh/v1/events",to=3e3;no=["CI","GITHUB_ACTIONS","GITLAB_CI","CIRCLECI","TRAVIS","JENKINS_URL","BUILDKITE","TF_BUILD"]});var jn={};Ae(jn,{autoBackup:()=>co,performBackup:()=>Ze,shouldAutoBackup:()=>Fn});import De from"fs";import Ne from"path";import Cn from"os";function lo(){return`oru-${new Date().toISOString().replace(/[:.]/g,"-").replace("Z","")}.db`}function Ze(t,e){let n=e.startsWith("~")?Ne.join(Cn.homedir(),e.slice(1)):e;De.mkdirSync(n,{recursive:!0});let o=lo(),r=o.slice(0,-3),s=Ne.join(n,o),a=1;for(;De.existsSync(s);)s=Ne.join(n,`${r}-${a}.db`),a++;return t.exec(`VACUUM INTO '${s.replace(/'/g,"''")}'`),s}function Fn(t,e){let n=t.startsWith("~")?Ne.join(Cn.homedir(),t.slice(1)):t;if(!De.existsSync(n))return!0;let o=De.readdirSync(n).filter(a=>a.startsWith("oru-")&&a.endsWith(".db")).sort();if(o.length===0)return!0;let r=De.statSync(Ne.join(n,o[o.length-1]));return(Date.now()-r.mtimeMs)/1e3/60>=e}function co(t,e,n){try{Fn(e,n)&&Ze(t,e)}catch(o){process.env.ORU_DEBUG==="1"&&console.error("Auto-backup failed:",o)}}var At=K(()=>{"use strict"});async function et(){try{let t=new AbortController,e=setTimeout(()=>t.abort(),1e4),n=await fetch("https://registry.npmjs.org/@tchayen/oru/latest",{signal:t.signal});return clearTimeout(e),n.ok?(await n.json()).version??null:null}catch{return null}}var Lt=K(()=>{"use strict"});var Ct={};Ae(Ct,{checkForUpdate:()=>go,compareVersions:()=>Re,printUpdateNotice:()=>_o});import Mt from"fs";import Pt from"path";import uo from"os";function Un(){let t=process.env.ORU_INSTALL_DIR??Pt.join(uo.homedir(),".oru");return Pt.join(t,".update-state.json")}function po(){try{let t=Mt.readFileSync(Un(),"utf-8");return JSON.parse(t)}catch{return null}}function fo(t){let e=Un();Mt.mkdirSync(Pt.dirname(e),{recursive:!0}),Mt.writeFileSync(e,JSON.stringify(t))}function Re(t,e){let n=t.split(".").map(Number),o=e.split(".").map(Number);for(let r=0;r<3;r++){let s=n[r]??0,a=o[r]??0;if(s!==a)return s-a}return 0}async function go(t){if(!t.auto_update_check||process.env.ORU_NO_UPDATE_CHECK==="1"||!process.stderr.isTTY)return null;let e=po(),n=Date.now();if(e&&n-e.lastChecked<mo)return Re(e.latestVersion,J)>0?e.latestVersion:null;let o=await et();return o?(fo({lastChecked:n,latestVersion:o}),Re(o,J)>0?o:null):null}function _o(t){process.stderr.write(`
|
|
46
|
+
Update available: ${J} \u2192 ${t}
|
|
47
47
|
Run \`oru self-update\` to upgrade.
|
|
48
|
-
`)}var
|
|
49
|
-
`)){let o=n.indexOf("=");o!==-1&&(e[n.slice(0,o).trim()]=n.slice(o+1).trim())}return e}catch{return null}}function
|
|
50
|
-
`),
|
|
51
|
-
`);let s=new AbortController,a=setTimeout(()=>s.abort(),6e4),u=await fetch(r,{signal:s.signal});if(clearTimeout(a),!u.ok)throw new Error(`Failed to download: ${u.status} from ${r}`);let p=
|
|
48
|
+
`)}var mo,tt=K(()=>{"use strict";$e();Lt();mo=1440*60*1e3});var Wn={};Ae(Wn,{performUpdate:()=>vo});import{execSync as Yn}from"child_process";import z from"fs";import pe from"path";import Ft from"os";function Bn(){let t=process.env.ORU_INSTALL_DIR??pe.join(Ft.homedir(),".oru");return pe.join(t,".install-meta")}function yo(){try{let t=z.readFileSync(Bn(),"utf-8"),e={};for(let n of t.split(`
|
|
49
|
+
`)){let o=n.indexOf("=");o!==-1&&(e[n.slice(0,o).trim()]=n.slice(o+1).trim())}return e}catch{return null}}function bo(){return yo()?.install_method==="script"?"script":"npm"}function ho(){let t=process.platform==="darwin"?"darwin":"linux",e=process.arch==="arm64"?"arm64":"x64";return`${t}-${e}`}async function ko(){process.stderr.write(`Updating via npm...
|
|
50
|
+
`),Yn("npm install -g @tchayen/oru@latest",{stdio:"inherit"})}async function So(t){let e=process.env.ORU_INSTALL_DIR??pe.join(Ft.homedir(),".oru"),n=pe.join(e,"bin"),o=ho(),r=`https://github.com/tchayen/oru/releases/download/v${t}/oru-v${t}-${o}.tar.gz`;process.stderr.write(`Downloading oru v${t}...
|
|
51
|
+
`);let s=new AbortController,a=setTimeout(()=>s.abort(),6e4),u=await fetch(r,{signal:s.signal});if(clearTimeout(a),!u.ok)throw new Error(`Failed to download: ${u.status} from ${r}`);let p=z.mkdtempSync(pe.join(Ft.tmpdir(),"oru-update-")),m=pe.join(p,"oru.tar.gz"),d=Buffer.from(await u.arrayBuffer());z.writeFileSync(m,d),z.existsSync(n)&&z.rmSync(n,{recursive:!0}),z.mkdirSync(n,{recursive:!0}),Yn(`tar -xzf "${m}" -C "${n}"`,{stdio:"pipe"}),z.rmSync(p,{recursive:!0});let _=`install_method=script
|
|
52
52
|
version=${t}
|
|
53
53
|
platform=${o}
|
|
54
54
|
installed_at=${new Date().toISOString()}
|
|
55
|
-
`;
|
|
56
|
-
`)}async function
|
|
55
|
+
`;z.writeFileSync(Bn(),_),process.stderr.write(`Updated to oru v${t}
|
|
56
|
+
`)}async function vo(t){let e=await et();if(!e)throw new Error("Failed to fetch latest version from npm registry.");let n=J;if(Re(e,n)<=0){process.stderr.write(`Already up to date (v${n})
|
|
57
57
|
`);return}if(process.stderr.write(`New version available: v${n} \u2192 v${e}
|
|
58
|
-
`),t)return;
|
|
58
|
+
`),t)return;bo()==="script"?await So(e):await ko()}var Jn=K(()=>{"use strict";tt();Lt();$e()});import{fileURLToPath as Ut}from"url";import jt from"fs";import Ie from"path";import{spawn as Hn}from"child_process";import{Command as To,Option as Q,Help as wo}from"commander";import{sql as cr}from"kysely";import{sql as V}from"kysely";import{randomBytes as Gn}from"crypto";var Wt="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",zn=new Set(Wt),Jt=11;function ot(t,e){let n=0n;for(let r of t)n=n<<8n|BigInt(r);let o=[];for(let r=0;r<e;r++)o.push(Wt[Number(n%62n)]),n/=62n;return o.reverse().join("")}function Ht(t){if(t.length!==Jt)return!1;for(let e of t)if(!zn.has(e))return!1;return!0}function re(){return ot(Gn(8),Jt)}var $=["todo","in_progress","in_review","done"],oe=new Set($),D=["low","medium","high","urgent"],se=new Set(D),Vt=["title","status","priority","owner","due_at","recurrence","blocked_by","labels","metadata"],st=new Set(Vt);var qt="todo",Xt="medium";var B=["priority","due","title","created"],Z=class extends Error{prefix;matches;constructor(e,n){super(`Prefix '${e}' is ambiguous, matches: ${n.join(", ")}`),this.name="AmbiguousPrefixError",this.prefix=e,this.matches=n}};function Le(t,e){try{return JSON.parse(t)}catch{return e}}function at(t){return{id:t.id,title:t.title,status:t.status,priority:t.priority,owner:t.owner,due_at:t.due_at,recurrence:t.recurrence,blocked_by:Le(t.blocked_by,[]),labels:Le(t.labels,[]),notes:Le(t.notes,[]),metadata:Le(t.metadata,{}),created_at:t.created_at,updated_at:t.updated_at,deleted_at:t.deleted_at}}async function it(t,e,n){let o=e.id??re(),r=n??new Date().toISOString(),s={id:o,title:e.title,status:e.status??qt,priority:e.priority??Xt,owner:e.owner??null,due_at:e.due_at??null,recurrence:e.recurrence??null,blocked_by:e.blocked_by??[],labels:e.labels??[],notes:e.notes??[],metadata:e.metadata??{},created_at:r,updated_at:r,deleted_at:null};return await t.insertInto("tasks").values({id:s.id,title:s.title,status:s.status,priority:s.priority,owner:s.owner,due_at:s.due_at,recurrence:s.recurrence,blocked_by:JSON.stringify(s.blocked_by),labels:JSON.stringify(s.labels),notes:JSON.stringify(s.notes),metadata:JSON.stringify(s.metadata),created_at:s.created_at,updated_at:s.updated_at,deleted_at:s.deleted_at}).execute(),s}async function Me(t,e){let n=t.selectFrom("tasks").selectAll().where("deleted_at","is",null);if(e?.status&&(Array.isArray(e.status)?n=n.where("status","in",e.status):n=n.where("status","=",e.status)),e?.priority&&(Array.isArray(e.priority)?n=n.where("priority","in",e.priority):n=n.where("priority","=",e.priority)),e?.owner&&(n=n.where("owner","=",e.owner)),e?.label){let s=e.label;n=n.where(V`EXISTS (SELECT 1 FROM json_each(labels) WHERE json_each.value = ${s})`)}if(e?.search){let s=e.search.replace(/[\\%_]/g,"\\$&");n=n.where(V`title LIKE '%' || ${s} || '%' ESCAPE '\\' COLLATE NOCASE`)}switch(e?.actionable&&(n=n.where("status","!=","done").where(V`NOT EXISTS (
|
|
59
59
|
SELECT 1 FROM json_each(tasks.blocked_by) AS dep
|
|
60
60
|
JOIN tasks AS blocker ON blocker.id = dep.value
|
|
61
61
|
WHERE blocker.status != 'done' AND blocker.deleted_at IS NULL
|
|
62
|
-
)`)),e?.sql&&(n=n.where(
|
|
63
|
-
`)}function
|
|
64
|
-
`)}function
|
|
65
|
-
${w('Create one with: oru add "Task title"')}`;let n=Math.max(2,...t.map(d=>d.id.length)),o=Math.max(3,...t.map(d=>d.priority.length)),r=Math.max(5,...t.map(d=>(d.owner??"").length)),s=Math.max(3,...t.map(d=>d.due_at?d.due_at.slice(11,16)==="00:00"?10:16:0)),a=Math.max(6,...t.map(d=>(d.labels.length>0?d.labels.join(", "):"").length)),u=Math.max(5,...t.map(d=>d.title.length)),p=w(` ${"ID".padEnd(n)} ${"TITLE".padEnd(u)} ${"PRI".padEnd(o)} ${"OWNER".padEnd(r)} ${"DUE".padEnd(s)} ${"LABELS".padEnd(a)} META`),m=t.map(d=>{let _=
|
|
66
|
-
`)}function
|
|
67
|
-
`)}function
|
|
68
|
-
|
|
69
|
-
`)}function
|
|
70
|
-
`),t.transaction(()=>{for(let a of o)a.up(t),t.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(String(a.version));return o.length})()}function
|
|
62
|
+
)`)),e?.sql&&(n=n.where(V`(${V.raw(e.sql)})`)),e?.sort??"priority"){case"due":n=n.orderBy(V`CASE WHEN due_at IS NULL THEN 1 ELSE 0 END`,"asc").orderBy("due_at","asc").orderBy("created_at","asc");break;case"title":n=n.orderBy(V`title COLLATE NOCASE`,"asc").orderBy("created_at","asc");break;case"created":n=n.orderBy("created_at","asc");break;default:n=n.orderBy(V`CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`).orderBy("created_at","asc");break}return(e?.limit!==void 0||e?.offset!==void 0)&&(n=n.limit(e.limit??-1)),e?.offset!==void 0&&(n=n.offset(e.offset)),(await n.execute()).map(at)}function Kt(t,e){return e.all||e.status!==void 0?t:t.filter(n=>n.status!=="done")}async function I(t,e){let n=await t.selectFrom("tasks").selectAll().where("id","=",e).where("deleted_at","is",null).executeTakeFirst();if(n)return at(n);if(!e)return null;let o=e.replace(/[\\%_]/g,"\\$&"),r=await t.selectFrom("tasks").selectAll().where(V`id LIKE ${o} || '%' ESCAPE '\\'`).where("deleted_at","is",null).execute();if(r.length===1)return at(r[0]);if(r.length>1)throw new Z(e,r.map(s=>s.id));return null}async function Pe(t,e,n,o){let r=await I(t,e);if(!r)return null;let a={updated_at:o??new Date().toISOString()};return n.title!==void 0&&(a.title=n.title),n.status!==void 0&&(a.status=n.status),n.priority!==void 0&&(a.priority=n.priority),n.owner!==void 0&&(a.owner=n.owner),n.due_at!==void 0&&(a.due_at=n.due_at),n.recurrence!==void 0&&(a.recurrence=n.recurrence),n.blocked_by!==void 0&&(a.blocked_by=JSON.stringify(n.blocked_by)),n.labels!==void 0&&(a.labels=JSON.stringify(n.labels)),n.metadata!==void 0&&(a.metadata=JSON.stringify(n.metadata)),await t.updateTable("tasks").set(a).where("id","=",r.id).execute(),I(t,r.id)}async function Ce(t,e,n,o){let r=await I(t,e);if(!r)return null;let s=n.trim();if(s.length===0||r.notes.some(p=>p.trim()===s))return r;let a=[...r.notes,s],u=o??new Date().toISOString();return await t.updateTable("tasks").set({notes:JSON.stringify(a),updated_at:u}).where("id","=",r.id).execute(),I(t,r.id)}async function Fe(t,e,n,o){let r=await I(t,e);if(!r)return null;let s=o??new Date().toISOString();return await t.updateTable("tasks").set({notes:JSON.stringify(n),updated_at:s}).where("id","=",r.id).execute(),I(t,r.id)}async function Gt(t,e,n){let o=await I(t,e);if(!o)return!1;let r=n??new Date().toISOString(),s=await t.updateTable("tasks").set({deleted_at:r,updated_at:r}).where("id","=",o.id).where("deleted_at","is",null).executeTakeFirst();return BigInt(s.numUpdatedRows)>0n}async function P(t,e,n){let o=re(),r=n??new Date().toISOString();return await t.insertInto("oplog").values({id:o,task_id:e.task_id,device_id:e.device_id,op_type:e.op_type,field:e.field,value:e.value,timestamp:r}).execute(),{id:o,task_id:e.task_id,device_id:e.device_id,op_type:e.op_type,field:e.field,value:e.value,timestamp:r}}function Qn(){return"NO_COLOR"in process.env?!1:"FORCE_COLOR"in process.env?!0:process.stdout.isTTY??!1}function je(t,e){let n=`\x1B[${t}m`,o=`\x1B[${e}m`;return r=>Qn()?`${n}${r}${o}`:r}var A=je(1,22),w=je(2,22),lt=je(3,23),fe=je(37,39);var Zn={MO:"monday",TU:"tuesday",WE:"wednesday",TH:"thursday",FR:"friday",SA:"saturday",SU:"sunday"};function ae(t){let e=t,n="";e.startsWith("after:")&&(e=e.slice(6),n=" (after completion)");let o=e.split(";"),r="",s=1,a=null,u=null;for(let d of o){let[_,b]=d.split("=");switch(_){case"FREQ":r=b;break;case"INTERVAL":s=Number(b);break;case"BYDAY":a=b.split(",");break;case"BYMONTHDAY":u=Number(b);break}}if(a&&a.length>0){let d=["MO","TU","WE","TH","FR"];if(a.length===5&&d.every(E=>a.includes(E)))return`weekdays${n}`;let b=a.map(E=>Zn[E]??E.toLowerCase());return`${s>1?`every ${s} weeks on `:"every "}${b.join(", ")}${n}`}if(u!==null){let d=er(u);return`${s>1?`every ${s} months on the `:"every "}${d}${n}`}let m={DAILY:"day",WEEKLY:"week",MONTHLY:"month",YEARLY:"year"}[r]??"year";if(s===1)switch(r){case"DAILY":return`daily${n}`;case"WEEKLY":return`weekly${n}`;case"MONTHLY":return`monthly${n}`;case"YEARLY":return`yearly${n}`}return`every ${s} ${m}s${n}`}function er(t){let e=["th","st","nd","rd"],n=t%100;return t+(e[(n-20)%10]||e[n]||e[0])}var ge={sunday:0,monday:1,tuesday:2,wednesday:3,thursday:4,friday:5,saturday:6};function ie(t,e){let n=e??new Date,o=new Date(Number(t.slice(0,4)),Number(t.slice(5,7))-1,Number(t.slice(8,10)),Number(t.slice(11,13))||0,Number(t.slice(14,16))||0);return t.slice(11,16)==="00:00"&&o.setDate(o.getDate()+1),o<n}function zt(t,e){if(ie(t,e))return!1;let n=e??new Date,o=new Date(Number(t.slice(0,4)),Number(t.slice(5,7))-1,Number(t.slice(8,10)),Number(t.slice(11,13))||0,Number(t.slice(14,16))||0);return t.slice(11,16)==="00:00"&&o.setDate(o.getDate()+1),(o.getTime()-n.getTime())/(1e3*60*60)<=48}function tr(t,e){let n=t.slice(0,10),o=t.slice(11,16),r=o==="00:00"?n:`${n} ${o}`;return ie(t,e)?A(r):r}function nr(t){let e=t.slice(0,10),n=t.slice(11,16);return n==="00:00"?e:`${e} ${n}`}function Qt(t){switch(t){case"urgent":return A(t);case"low":return w(t);default:return t}}function rr(t){switch(t){case"done":return w(t);case"in_progress":return A(t);case"in_review":return lt(t);default:return t}}function or(t){switch(t){case"done":return w("[x]");case"in_progress":return A("[~]");case"in_review":return fe("[r]");default:return w("[ ]")}}function W(t,e){let n=[];n.push(`${w(t.id)} ${A(t.title)}`);let o=` Status: ${rr(t.status)} Priority: ${Qt(t.priority)}`;if(t.due_at&&(o+=` Due: ${tr(t.due_at,e)}`),n.push(o),t.recurrence&&n.push(` Recurrence: ${ae(t.recurrence)}`),t.owner&&n.push(` Owner: ${t.owner}`),t.blocked_by.length>0&&n.push(` Blocked by: ${t.blocked_by.join(", ")}`),t.labels.length>0&&n.push(` Labels: ${t.labels.join(", ")}`),t.notes.length>0){n.push(` ${w("Notes:")}`);for(let s of t.notes)n.push(` ${w("-")} ${lt(s)}`)}let r=Object.keys(t.metadata);if(r.length>0){n.push(` ${w("Metadata:")}`);for(let s of r)n.push(` ${w(`${s}:`)} ${String(t.metadata[s])}`)}return n.join(`
|
|
63
|
+
`)}function Zt(t){return t.length===0?w("No labels found."):t.join(`
|
|
64
|
+
`)}function ct(t,e){if(t.length===0)return`${w("No tasks found.")}
|
|
65
|
+
${w('Create one with: oru add "Task title"')}`;let n=Math.max(2,...t.map(d=>d.id.length)),o=Math.max(3,...t.map(d=>d.priority.length)),r=Math.max(5,...t.map(d=>(d.owner??"").length)),s=Math.max(3,...t.map(d=>d.due_at?d.due_at.slice(11,16)==="00:00"?10:16:0)),a=Math.max(6,...t.map(d=>(d.labels.length>0?d.labels.join(", "):"").length)),u=Math.max(5,...t.map(d=>d.title.length)),p=w(` ${"ID".padEnd(n)} ${"TITLE".padEnd(u)} ${"PRI".padEnd(o)} ${"OWNER".padEnd(r)} ${"DUE".padEnd(s)} ${"LABELS".padEnd(a)} META`),m=t.map(d=>{let _=or(d.status),b=d.owner??"",N=d.due_at?nr(d.due_at):"",E=d.due_at?ie(d.due_at,e):!1,S=d.labels.length>0?d.labels.join(", "):"",x=Object.keys(d.metadata),O=x.length>0?x.map(M=>`${M}=${d.metadata[M]}`).join(", "):"",F=N.padEnd(s),g=E?A(F):F;return`${_} ${w(d.id.padEnd(n))} ${A(d.title.padEnd(u))} ${Qt(d.priority.padEnd(o))} ${b.padEnd(r)} ${g} ${S.padEnd(a)} ${O}`});return[p,...m].join(`
|
|
66
|
+
`)}function en(t){if(t.length===0)return w("No log entries found.");let e=[];for(let n of t){let o=w(n.timestamp),r=w(`(${n.device_id})`),s;switch(n.op_type){case"create":s=A("CREATE");break;case"delete":s=w("DELETE");break;case"update":s="UPDATE";break}if(n.op_type==="create"){if(e.push(`${o} ${s} ${r}`),n.value)try{let a=JSON.parse(n.value),u=[];for(let[p,m]of Object.entries(a))m!=null&&u.push(`${p} = ${JSON.stringify(m)}`);u.length>0&&e.push(` ${u.join(", ")}`)}catch{e.push(` ${n.value}`)}}else if(n.op_type==="update"){let a=n.field??"";e.push(`${o} ${s} ${a} ${r}`),n.value!==null&&e.push(` ${a} = ${JSON.stringify(n.value)}`)}else e.push(`${o} ${s} ${r}`)}return e.join(`
|
|
67
|
+
`)}function tn(t,e){let n=[["Overdue",t.overdue],["Due Soon",t.due_soon],["In Progress",t.in_progress],["Actionable",t.actionable],["Blocked",t.blocked],["Recently Completed",t.recently_completed]],o={Overdue:"overdue","Due Soon":"due soon","In Progress":"in progress",Actionable:"actionable",Blocked:"blocked","Recently Completed":"recently completed"},r=n.filter(([,p])=>p.length>0);if(r.length===0)return w("Nothing to report.");let s=r.map(([p,m])=>`${A(String(m.length))} ${o[p]}`),u=[w(s.join(", "))];for(let[p,m]of r)if(u.push(`${A(p)} ${w(`(${m.length})`)}`),u.push(ct(m,e)),p==="Blocked"&&t.blockerTitles){for(let d of m)if(d.blocked_by.length>0){let _=d.blocked_by.map(b=>{let N=t.blockerTitles.get(b);return N?`${b} (${N})`:b}).join(", ");u.push(w(` ${d.id} blocked by: ${_}`))}}return u.join(`
|
|
68
|
+
|
|
69
|
+
`)}function ut(t,e,n,o="monday"){let r=n??new Date,s=`${r.getFullYear()}-${String(r.getMonth()+1).padStart(2,"0")}-${String(r.getDate()).padStart(2,"0")}`;switch(e){case"today":return t.filter(a=>a.due_at?.slice(0,10)===s);case"this-week":{let a=r.getDay(),u=ge[o],p=(a-u+7)%7,m=new Date(r.getFullYear(),r.getMonth(),r.getDate()-p),d=new Date(m.getFullYear(),m.getMonth(),m.getDate()+6),_=`${m.getFullYear()}-${String(m.getMonth()+1).padStart(2,"0")}-${String(m.getDate()).padStart(2,"0")}`,b=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;return t.filter(N=>{if(!N.due_at)return!1;let E=N.due_at.slice(0,10);return E>=_&&E<=b})}case"overdue":return t.filter(a=>a.due_at?ie(a.due_at,r):!1)}}var sr={SU:0,MO:1,TU:2,WE:3,TH:4,FR:5,SA:6};function ar(t){let e=t.split(";"),n="",o=1,r=null,s=null;for(let a of e){let[u,p]=a.split("=");switch(u){case"FREQ":n=p;break;case"INTERVAL":o=Number(p);break;case"BYDAY":r=p.split(",");break;case"BYMONTHDAY":s=Number(p);break}}return{freq:n,interval:o,byDay:r,byMonthDay:s}}function Ue(t,e){let n=new Date(t);return n.setDate(n.getDate()+e),n}function dt(t,e){let n=new Date(t),o=n.getMonth()+e;return n.setMonth(o),n.getMonth()!==(o%12+12)%12&&n.setDate(0),n}function mt(t,e){let n=ar(t);switch(n.freq){case"DAILY":return Ue(e,n.interval);case"WEEKLY":{if(n.byDay&&n.byDay.length>0){let o=n.byDay.map(u=>sr[u]).sort((u,p)=>u-p),r=e.getDay();for(let u of o)if(u>r)return Ue(e,u-r);let s=7-r+o[0],a=(n.interval-1)*7;return Ue(e,s+a)}return Ue(e,n.interval*7)}case"MONTHLY":{if(n.byMonthDay!==null){let o=n.byMonthDay,r=new Date(e);if(e.getDate()<o){if(r.setDate(o),r.getMonth()!==e.getMonth()&&(r=new Date(e.getFullYear(),e.getMonth()+1,0)),r.getTime()<=e.getTime()){r=dt(e,n.interval);let s=r.getMonth();r.setDate(o),r.getMonth()!==s&&(r=new Date(r.getFullYear(),s+1,0))}}else{r=dt(e,n.interval);let s=r.getMonth();r.setDate(o),r.getMonth()!==s&&(r=new Date(r.getFullYear(),s+1,0))}return r}return dt(e,n.interval)}case"YEARLY":{let o=new Date(e);return o.setFullYear(o.getFullYear()+n.interval),o.getMonth()!==e.getMonth()&&o.setDate(0),o}default:throw new Error(`Unsupported FREQ: ${n.freq}`)}}import{createHash as ir}from"crypto";var lr="oru-recurrence";function Ye(t){let e=ir("sha256").update(`${lr}:${t}`).digest();return ot(e.subarray(0,8),11)}function pt(t){return t===null?null:typeof t=="string"?t:JSON.stringify(t)}function nn(t){return{title:t.title,status:t.status,priority:t.priority,owner:t.owner,due_at:t.due_at,recurrence:t.recurrence,blocked_by:t.blocked_by,labels:t.labels,notes:t.notes,metadata:t.metadata}}var Be=class{constructor(e,n){this.db=e;this.deviceId=n}async add(e){return this.db.transaction().execute(async n=>{let o=new Date().toISOString(),r={...e,owner:e.owner||null},s=await it(n,r,o);return await P(n,{task_id:s.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(nn(s))},o),s})}async _maybeSpawn(e,n,o){if(n.status!=="done"||!n.recurrence)return null;let r=Ye(n.id);if(await I(e,r))return null;let a=n.recurrence,u=a.startsWith("after:");u&&(a=a.slice(6));let p;u?p=new Date(o):n.due_at?p=new Date(n.due_at):p=new Date(o);let m=mt(a,p),d=`${m.getFullYear()}-${String(m.getMonth()+1).padStart(2,"0")}-${String(m.getDate()).padStart(2,"0")}T${String(m.getHours()).padStart(2,"0")}:${String(m.getMinutes()).padStart(2,"0")}:${String(m.getSeconds()).padStart(2,"0")}`,_={id:r,title:n.title,priority:n.priority,owner:n.owner,due_at:d,recurrence:n.recurrence,labels:[...n.labels],metadata:{...n.metadata}},b=await it(e,_,o);return await P(e,{task_id:b.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(nn(b))},o),b}async getSpawnedTask(e){let n=Ye(e);return I(this.db,n)}async validateBlockedBy(e,n){let o=null;if(e!==null){let s=await I(this.db,e);if(!s)return{valid:!1,error:`Task "${e}" not found.`};o=s.id}let r=[];for(let s of n){let a=await I(this.db,s);if(!a)return{valid:!1,error:`Task "${s}" not found.`};if(o!==null&&a.id===o)return{valid:!1,error:"A task cannot block itself."};r.push(a.id)}if(o!==null&&r.length>0){let s=await Me(this.db),a=new Map(s.map(u=>[u.id,u]));for(let u of r){let p=[u],m=new Set;for(;p.length>0;){let d=p.shift();if(d===o)return{valid:!1,error:`Setting blocked_by to "${u}" would create a circular dependency.`};if(m.has(d))continue;m.add(d);let _=a.get(d);if(_)for(let b of _.blocked_by)m.has(b)||p.push(b)}}}return{valid:!0}}async list(e){return Me(this.db,e)}async get(e){return I(this.db,e)}async update(e,n){return this.db.transaction().execute(async o=>{let r=new Date().toISOString(),s=await Pe(o,e,n,r);if(!s)return null;for(let[a,u]of Object.entries(n))a==="note"||u===void 0||await P(o,{task_id:s.id,device_id:this.deviceId,op_type:"update",field:a,value:pt(u)},r);return await this._maybeSpawn(o,s,r),s})}async addNote(e,n){return this.db.transaction().execute(async o=>{let r=new Date().toISOString(),s=await I(o,e);if(!s)return null;let a=n.trim();if(a.length===0||s.notes.some(p=>p.trim()===a))return s;let u=await Ce(o,s.id,a,r);return await P(o,{task_id:s.id,device_id:this.deviceId,op_type:"update",field:"notes",value:a},r),u})}async updateWithNote(e,n,o){return this.db.transaction().execute(async r=>{let s=new Date().toISOString(),a=await Pe(r,e,n,s);if(!a)return null;let u=a.id;for(let[m,d]of Object.entries(n))m==="note"||d===void 0||await P(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:m,value:pt(d)},s);let p=o.trim();return p.length>0&&!a.notes.some(m=>m.trim()===p)&&(a=await Ce(r,u,p,s),await P(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:"notes",value:p},s)),await this._maybeSpawn(r,a,s),a})}async clearNotes(e){return this.db.transaction().execute(async n=>{let o=new Date().toISOString(),r=await Fe(n,e,[],o);return r?(await P(n,{task_id:r.id,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},o),r):null})}async clearNotesAndUpdate(e,n,o){return this.db.transaction().execute(async r=>{let s=new Date().toISOString(),a=await Fe(r,e,[],s);if(!a)return null;let u=a.id;if(await P(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},s),o){let m=o.trim();m.length>0&&(a=await Ce(r,u,m,s),await P(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:"notes",value:m},s))}if(Object.keys(n).length>0){a=await Pe(r,u,n,s);for(let[m,d]of Object.entries(n))m==="note"||d===void 0||await P(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:m,value:pt(d)},s)}return await this._maybeSpawn(r,a,s),a})}async replaceNotes(e,n){return this.db.transaction().execute(async o=>{let r=new Date().toISOString(),s=await Fe(o,e,n,r);if(!s)return null;let a=s.id;await P(o,{task_id:a,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},r);for(let u of n)await P(o,{task_id:a,device_id:this.deviceId,op_type:"update",field:"notes",value:u},r);return s})}async listLabels(){let e=await Me(this.db),n=new Set;for(let o of e)for(let r of o.labels)n.add(r);return[...n].sort()}async getContext(e){let n=new Date,o=await this.list({sort:"priority",owner:e?.owner,label:e?.label}),r=await this.list({status:"done",sort:"priority",owner:e?.owner,label:e?.label}),s={overdue:[],due_soon:[],in_progress:[],actionable:[],blocked:[],recently_completed:[]},a=new Set(o.filter(d=>d.status!=="done").map(d=>d.id)),u=new Date(n.getTime()-1440*60*1e3).toISOString();for(let d of r)d.updated_at>=u&&s.recently_completed.push(d);for(let d of o){if(d.status==="done")continue;if(d.status==="in_progress"||d.status==="in_review"){s.in_progress.push(d);continue}if(d.due_at&&ie(d.due_at,n)){s.overdue.push(d);continue}if(d.due_at&&zt(d.due_at,n)){s.due_soon.push(d);continue}if(d.blocked_by.some(b=>a.has(b))){s.blocked.push(d);continue}if(d.status==="todo"){s.actionable.push(d);continue}}let p=new Map;for(let d of[...o,...r])p.set(d.id,d.title);s.blockerTitles=p;let m={overdue:s.overdue.length,due_soon:s.due_soon.length,in_progress:s.in_progress.length,actionable:s.actionable.length,blocked:s.blocked.length,recently_completed:s.recently_completed.length};return{sections:s,summary:m}}async log(e){let n=await I(this.db,e);return n?await this.db.selectFrom("oplog").selectAll().where("task_id","=",n.id).orderBy("timestamp","asc").orderBy(cr`rowid`,"asc").execute():null}async delete(e){return this.db.transaction().execute(async n=>{let o=new Date().toISOString(),r=await I(n,e);if(!r)return!1;let s=await Gt(n,r.id,o);return s&&await P(n,{task_id:r.id,device_id:this.deviceId,op_type:"delete",field:null,value:null},o),s})}};import{Kysely as ur,SqliteDialect as dr}from"kysely";function rn(t){return new ur({dialect:new dr({database:t})})}import mr from"better-sqlite3";import on from"path";import pr from"os";import ft from"fs";function fr(){return process.env.ORU_DB_PATH?process.env.ORU_DB_PATH:on.join(pr.homedir(),".oru","oru.db")}function sn(t){let e=t??fr(),n=on.dirname(e);ft.existsSync(n)||ft.mkdirSync(n,{recursive:!0,mode:448});let o=new mr(e);o.pragma("journal_mode = WAL"),o.pragma("foreign_keys = ON");try{ft.chmodSync(e,384)}catch{}return o}function gr(t){let e=t.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();return e?parseInt(e.value,10):0}function an(t,e){let n=gr(t),o=e.filter(a=>a.version>n).sort((a,u)=>a.version-u.version);if(o.length===0)return 0;let r=o[o.length-1].version;return process.stderr.write(`Migrating database from v${n} to v${r}...
|
|
70
|
+
`),t.transaction(()=>{for(let a of o)a.up(t),t.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(String(a.version));return o.length})()}function ln(t){t.exec(`
|
|
71
71
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
72
72
|
id TEXT PRIMARY KEY,
|
|
73
73
|
title TEXT NOT NULL,
|
|
@@ -97,7 +97,7 @@ ${w('Create one with: oru add "Task title"')}`;let n=Math.max(2,...t.map(d=>d.id
|
|
|
97
97
|
);
|
|
98
98
|
|
|
99
99
|
INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '1');
|
|
100
|
-
`),
|
|
100
|
+
`),an(t,_r)}var _r=[{version:2,up:t=>{t.exec("CREATE INDEX IF NOT EXISTS idx_oplog_task_id ON oplog(task_id)"),t.exec("CREATE INDEX IF NOT EXISTS idx_oplog_device_id ON oplog(device_id)")}},{version:3,up:t=>{t.exec("CREATE INDEX IF NOT EXISTS idx_oplog_task_timestamp ON oplog(task_id, timestamp, id)")}},{version:4,up:t=>{t.exec("ALTER TABLE tasks ADD COLUMN due_at TEXT")}},{version:5,up:t=>{t.exec("ALTER TABLE tasks ADD COLUMN blocked_by TEXT NOT NULL DEFAULT '[]'")}},{version:6,up:t=>{t.exec("ALTER TABLE tasks ADD COLUMN owner TEXT")}},{version:7,up:t=>{t.exec("ALTER TABLE tasks ADD COLUMN recurrence TEXT")}}];function cn(t){let{deleted_at:e,...n}=t;return n}function un(t){let e={};for(let[o,r]of Object.entries(t))o!=="blockerTitles"&&(e[o]=r.length);let n={summary:e};for(let[o,r]of Object.entries(t))o!=="blockerTitles"&&Array.isArray(r)&&r.length>0&&(n[o]=r.map(cn));if(t.blockerTitles&&t.blocked.length>0){let o=new Set(t.blocked.flatMap(s=>s.blocked_by)),r={};for(let s of o){let a=t.blockerTitles.get(s);a&&(r[s]=a)}Object.keys(r).length>0&&(n.blocker_titles=r)}return JSON.stringify(n,null,2)}function q(t){let{deleted_at:e,...n}=t;return JSON.stringify(n,null,2)}function dn(t){return JSON.stringify(t.map(cn),null,2)}function mn(t){return JSON.stringify(t,null,2)}function pn(t){return JSON.stringify(t,null,2)}import yr from"better-sqlite3";import fn from"fs";import br from"path";var We=class{db;constructor(e){fn.mkdirSync(br.dirname(e),{recursive:!0}),this.db=new yr(e),this.db.pragma("journal_mode = WAL"),this.db.pragma("busy_timeout = 5000"),this.db.exec(`
|
|
101
101
|
CREATE TABLE IF NOT EXISTS oplog (
|
|
102
102
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
103
|
id TEXT UNIQUE NOT NULL,
|
|
@@ -108,9 +108,9 @@ ${w('Create one with: oru add "Task title"')}`;let n=Math.max(2,...t.map(d=>d.id
|
|
|
108
108
|
value TEXT,
|
|
109
109
|
timestamp TEXT NOT NULL
|
|
110
110
|
);
|
|
111
|
-
`);try{
|
|
112
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`);this.db.transaction(r=>{for(let s of r)n.run(s.id,s.task_id,s.device_id,s.op_type,s.field,s.value,s.timestamp)})(e)}async pull(e){let n=e?parseInt(e,10):0,o=Number.isNaN(n)?0:n,r=this.db.prepare("SELECT seq, id, task_id, device_id, op_type, field, value, timestamp FROM oplog WHERE seq > ? ORDER BY seq ASC LIMIT 10000").all(o);if(r.length===0)return{entries:[],cursor:e};let s=r[r.length-1].seq;return{entries:r.map(({seq:u,...p})=>p),cursor:String(s)}}close(){this.db.close()}};import
|
|
113
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`);for(let r of e)
|
|
111
|
+
`);try{fn.chmodSync(e,384)}catch{}}async push(e){let n=this.db.prepare(`INSERT OR IGNORE INTO oplog (id, task_id, device_id, op_type, field, value, timestamp)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);this.db.transaction(r=>{for(let s of r)n.run(s.id,s.task_id,s.device_id,s.op_type,s.field,s.value,s.timestamp)})(e)}async pull(e){let n=e?parseInt(e,10):0,o=Number.isNaN(n)?0:n,r=this.db.prepare("SELECT seq, id, task_id, device_id, op_type, field, value, timestamp FROM oplog WHERE seq > ? ORDER BY seq ASC LIMIT 10000").all(o);if(r.length===0)return{entries:[],cursor:e};let s=r[r.length-1].seq;return{entries:r.map(({seq:u,...p})=>p),cursor:String(s)}}close(){this.db.close()}};import _t from"fs";import wr from"path";import Er from"os";var hr=new Set(["create","update","delete"]),kr=1e3;function Sr(t){return st.has(t)}function gt(t){try{return JSON.parse(t),!0}catch{return!1}}function _e(t){return t.filter(e=>typeof e=="string")}function gn(t,e){t.transaction(()=>{let n=t.prepare(`INSERT OR IGNORE INTO oplog (id, task_id, device_id, op_type, field, value, timestamp)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);for(let r of e)hr.has(r.op_type)&&n.run(r.id,r.task_id,r.device_id,r.op_type,r.field,r.value,r.timestamp);let o=[...new Set(e.map(r=>r.task_id))];for(let r of o)vr(t,r)})()}function vr(t,e){let n=t.prepare("SELECT * FROM oplog WHERE task_id = ? ORDER BY timestamp ASC, CASE WHEN field = 'notes_clear' THEN 0 ELSE 1 END ASC, id ASC").all(e);if(n.length===0)return;let o=n.find(g=>g.op_type==="create");if(!o||o.value===null||o.value===void 0)return;let r;try{r=JSON.parse(o.value)}catch{return}let s=typeof r.title=="string"?r.title:"Untitled",a=oe.has(r.status)?r.status:"todo",u=se.has(r.priority)?r.priority:"medium",p=typeof r.owner=="string"&&r.owner.trim().length>0?r.owner:null,m=typeof r.due_at=="string"?r.due_at:null,d=typeof r.recurrence=="string"?r.recurrence:null,_=JSON.stringify(Array.isArray(r.blocked_by)?_e(r.blocked_by):[]),b=JSON.stringify(Array.isArray(r.labels)?_e(r.labels):[]),N=JSON.stringify(r.metadata&&typeof r.metadata=="object"&&!Array.isArray(r.metadata)?r.metadata:{}),E=[...Array.isArray(r.notes)?_e(r.notes):[]],S=null,x=o.timestamp,O=null;for(let g of n)g.op_type==="update"&&(!O||g.timestamp>O)&&(O=g.timestamp);let F={};for(let g of n)if(g.op_type!=="create"){if(g.op_type==="delete"){O!==null&&O>=g.timestamp||(S=g.timestamp,g.timestamp>x&&(x=g.timestamp));continue}if(g.op_type==="update"){let M=g.field;if(!M)continue;if(M==="notes_clear"){E.length=0,g.timestamp>x&&(x=g.timestamp),S&&g.timestamp>=S&&(S=null);continue}if(M==="notes"){if(g.value&&g.value.trim().length>0){let R=g.value.trim();E.length<kr&&!E.some(c=>c.trim()===R)&&E.push(R)}g.timestamp>x&&(x=g.timestamp),S&&g.timestamp>=S&&(S=null);continue}if(!Sr(M))continue;let H=F[M];if(H&&(g.timestamp<H.timestamp||g.timestamp===H.timestamp&&g.id<H.id))continue;let j=!1;switch(M){case"title":typeof g.value=="string"&&(s=g.value,j=!0);break;case"status":g.value&&oe.has(g.value)&&(a=g.value,j=!0);break;case"priority":g.value&&se.has(g.value)&&(u=g.value,j=!0);break;case"owner":p=g.value&&g.value.trim().length>0?g.value:null,j=!0;break;case"due_at":m=g.value&&g.value.trim().length>0?g.value:null,j=!0;break;case"blocked_by":if(g.value&>(g.value)){let R=JSON.parse(g.value);Array.isArray(R)&&(_=JSON.stringify(_e(R)),j=!0)}break;case"labels":if(g.value&>(g.value)){let R=JSON.parse(g.value);Array.isArray(R)&&(b=JSON.stringify(_e(R)),j=!0)}break;case"metadata":if(g.value&>(g.value)){let R=JSON.parse(g.value);typeof R=="object"&&R!==null&&!Array.isArray(R)&&(N=g.value,j=!0)}break;case"recurrence":d=g.value&&g.value.trim().length>0?g.value:null,j=!0;break}j&&(F[M]={timestamp:g.timestamp,id:g.id}),g.timestamp>x&&(x=g.timestamp),S&&g.timestamp>=S&&(S=null)}}t.prepare(`INSERT INTO tasks (id, title, status, priority, owner, due_at, recurrence, blocked_by, labels, notes, metadata, created_at, updated_at, deleted_at)
|
|
114
114
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
115
115
|
ON CONFLICT(id) DO UPDATE SET
|
|
116
116
|
title = excluded.title,
|
|
@@ -124,19 +124,19 @@ ${w('Create one with: oru add "Task title"')}`;let n=Math.max(2,...t.map(d=>d.id
|
|
|
124
124
|
notes = excluded.notes,
|
|
125
125
|
metadata = excluded.metadata,
|
|
126
126
|
updated_at = excluded.updated_at,
|
|
127
|
-
deleted_at = excluded.deleted_at`).run(e,s,a,u,p,m,d,_,b,JSON.stringify(E)
|
|
128
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(`push_hwm_${this.deviceId}`,String(r)),o.length}async pull(){let e=0,n=0;for(;;){if(++n>this.maxPullIterations)throw new Error(`Sync pull loop exceeded ${this.maxPullIterations} iterations - aborting to prevent infinite loop. This likely indicates a bug in the remote backend.`);let r=this.db.prepare("SELECT value FROM meta WHERE key = ?").get(`pull_cursor_${this.deviceId}`)?.value??null,s=await this.remote.pull(r);if(s.entries.length===0)break;
|
|
129
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(`pull_cursor_${this.deviceId}`,s.cursor);let a=s.entries.filter(u=>u.device_id!==this.deviceId);if(e+=a.length,s.cursor===r)break}return e}async sync(){let e=await this.push(),n=await this.pull();return{pushed:e,pulled:n}}};async function
|
|
130
|
-
`;if(n+=
|
|
127
|
+
deleted_at = excluded.deleted_at`).run(e,s,a,u,p,m,d,_,b,JSON.stringify(E),N,o.timestamp,x,S)}var Tr=1e3,Je=class{constructor(e,n,o,r=Tr){this.db=e;this.remote=n;this.deviceId=o;this.maxPullIterations=r}async push(){let e=this.db.prepare("SELECT value FROM meta WHERE key = ?").get(`push_hwm_${this.deviceId}`),n=e&&parseInt(e.value,10)||0,o=this.db.prepare("SELECT rowid, * FROM oplog WHERE device_id = ? AND rowid > ? ORDER BY rowid ASC").all(this.deviceId,n);if(o.length===0)return 0;await this.remote.push(o);let r=o[o.length-1].rowid;return this.db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
128
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(`push_hwm_${this.deviceId}`,String(r)),o.length}async pull(){let e=0,n=0;for(;;){if(++n>this.maxPullIterations)throw new Error(`Sync pull loop exceeded ${this.maxPullIterations} iterations - aborting to prevent infinite loop. This likely indicates a bug in the remote backend.`);let r=this.db.prepare("SELECT value FROM meta WHERE key = ?").get(`pull_cursor_${this.deviceId}`)?.value??null,s=await this.remote.pull(r);if(s.entries.length===0)break;gn(this.db,s.entries),s.cursor&&this.db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
129
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(`pull_cursor_${this.deviceId}`,s.cursor);let a=s.entries.filter(u=>u.device_id!==this.deviceId);if(e+=a.length,s.cursor===r)break}return e}async sync(){let e=await this.push(),n=await this.pull();return{pushed:e,pulled:n}}};async function _n(t,e,n){let o=t.name,r=wr.join(Er.tmpdir(),`oru-sync-backup-${Date.now()}.db`);t.exec(`VACUUM INTO '${r.replace(/'/g,"''")}'`);try{return await new Je(t,e,n).sync()}catch(s){t.close();for(let a of["-wal","-shm"])try{_t.unlinkSync(o+a)}catch{}throw _t.copyFileSync(r,o),s}finally{e.close?.();try{_t.unlinkSync(r)}catch{}}}function yn(t){let e=t.prepare("SELECT value FROM meta WHERE key = 'device_id'").get();if(e)return e.value;let n=re();return t.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('device_id', ?)").run(n),t.prepare("SELECT value FROM meta WHERE key = 'device_id'").get().value}kt();var Ir={sunday:0,sun:0,monday:1,mon:1,tuesday:2,tue:2,wednesday:3,wed:3,thursday:4,thu:4,friday:5,fri:5,saturday:6,sat:6},hn={january:1,jan:1,february:2,feb:2,march:3,mar:3,april:4,apr:4,may:5,june:6,jun:6,july:7,jul:7,august:8,aug:8,september:9,sep:9,sept:9,october:10,oct:10,november:11,nov:11,december:12,dec:12};function St(t,e="mdy",n="monday",o="same_day",r){let s=r??new Date,a=t.trim();if(!a)return null;let{datePart:u,timePart:p}=Ar(a),m=Mr(u,e,n,o,s);if(!m)return null;if(p){let d=Pr(p);if(!d)return null;m.setHours(d.hours,d.minutes,0,0)}return Cr(m)}function Ar(t){let e=t.match(/^(\d{4}-\d{2}-\d{2})T(\d{1,2}:\d{2})$/i);if(e)return{datePart:e[1],timePart:e[2]};let n=t.split(/\s+/);if(n.length===1)return{datePart:n[0],timePart:null};let o=n[n.length-1];return Lr(o)?{datePart:n.slice(0,-1).join(" "),timePart:o}:{datePart:t,timePart:null}}function Lr(t){return/^\d{1,2}:\d{2}\s*(am?|pm?)?$/i.test(t)||/^\d{1,2}\s*(am?|pm?)$/i.test(t)}function Mr(t,e,n,o,r){let s=t.toLowerCase().trim();if(s==="today"||s==="tod")return ee(r);if(s==="tomorrow"||s==="tom"){let m=ee(r);return m.setDate(m.getDate()+1),m}if(s==="tonight"){let m=ee(r);return m.setHours(18,0,0,0),m}if(s==="next week")return Tn(r,ge[n]);if(s==="next month"){if(o==="first")return new Date(r.getFullYear(),r.getMonth()+1,1);let m=(r.getMonth()+1)%12,d=new Date(r.getFullYear(),r.getMonth()+1,r.getDate());return d.getMonth()!==m?new Date(r.getFullYear(),r.getMonth()+2,0):d}if(s==="end of month")return new Date(r.getFullYear(),r.getMonth()+1,0);if(s==="end of week"){let m=(ge[n]+6)%7,d=ee(r),_=d.getDay(),b=(m-_+7)%7;return b===0||d.setDate(d.getDate()+b),d}{let m=s;m.startsWith("next ")&&(m=m.slice(5));let d=Ir[m];if(d!==void 0)return Tn(r,d)}{let m=s.match(/^in\s+(\d+)\s+(days?|weeks?|months?)$/);if(m){let d=Number(m[1]),_=m[2],b=ee(r);if(_.startsWith("day"))b.setDate(b.getDate()+d);else if(_.startsWith("week"))b.setDate(b.getDate()+d*7);else if(_.startsWith("month")){let N=b.getDate();b.setDate(1),b.setMonth(b.getMonth()+d);let E=new Date(b.getFullYear(),b.getMonth()+1,0).getDate();b.setDate(Math.min(N,E))}return b}}{let m=s.match(/^([a-z]+)\s+(\d+)(?:st|nd|rd|th)?$/);if(m){let d=hn[m[1]];if(d!==void 0){let _=Number(m[2]);return Sn(r,d,_)}}}{let m=s.match(/^(\d+)(?:st|nd|rd|th)?\s+([a-z]+)$/);if(m){let d=hn[m[2]];if(d!==void 0){let _=Number(m[1]);return Sn(r,d,_)}}}let a=t.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(a)return te(Number(a[1]),Number(a[2]),Number(a[3]));let u=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);if(u){let m=Number(u[1]),d=Number(u[2]),_=Number(u[3]);return kn(m,d,_,e)}let p=t.match(/^(\d{1,2})\/(\d{1,2})$/);if(p){let m=Number(p[1]),d=Number(p[2]);return kn(m,d,r.getFullYear(),e)}return null}function kn(t,e,n,o){if(o==="dmy"){let s=te(n,e,t);return s||te(n,t,e)}let r=te(n,t,e);return r||te(n,e,t)}function te(t,e,n){if(e<1||e>12||n<1||n>31)return null;let o=new Date(t,e-1,n);return o.getFullYear()!==t||o.getMonth()!==e-1||o.getDate()!==n?null:o}function Sn(t,e,n){let o=t.getFullYear(),r=te(o,e,n);return r?r<ee(t)?te(o+1,e,n):r:null}function Pr(t){let e=t.trim().toLowerCase(),n=e.match(/^(\d{1,2}):(\d{2})\s*(am?|pm?)?$/);if(n){let r=Number(n[1]),s=Number(n[2]),a=n[3];return a&&(r=vn(r,a)),r<0||r>23||s<0||s>59?null:{hours:r,minutes:s}}let o=e.match(/^(\d{1,2})\s*(am?|pm?)$/);if(o){let r=Number(o[1]);return r=vn(r,o[2]),r<0||r>23?null:{hours:r,minutes:0}}return null}function vn(t,e){let n=e.startsWith("p");return n&&t<12?t+12:!n&&t===12?0:t}function Cr(t){let e=t.getFullYear(),n=String(t.getMonth()+1).padStart(2,"0"),o=String(t.getDate()).padStart(2,"0"),r=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),a=String(t.getSeconds()).padStart(2,"0");return`${e}-${n}-${o}T${r}:${s}:${a}`}function ee(t){return new Date(t.getFullYear(),t.getMonth(),t.getDate())}function Tn(t,e){let n=ee(t),o=n.getDay(),r=e-o;return r<=0&&(r+=7),n.setDate(n.getDate()+r),n}import Tt from"fs";import Ur from"path";import Yr from"os";import{spawn as Br}from"child_process";import{stringify as Wr,parse as Jr}from"smol-toml";var Fr=new Set(["DAILY","WEEKLY","MONTHLY","YEARLY"]),jr=new Set(["MO","TU","WE","TH","FR","SA","SU"]);function he(t){if(!t||t.trim().length===0)return!1;let e=t;if(e.startsWith("after:")&&(e=e.slice(6)),!e.startsWith("FREQ="))return!1;let n=e.split(";"),o=!1;for(let r of n){let[s,a]=r.split("=");if(!s||a===void 0)return!1;switch(s){case"FREQ":if(!Fr.has(a))return!1;o=!0;break;case"INTERVAL":{let u=Number(a);if(!Number.isInteger(u)||u<1)return!1;break}case"BYDAY":for(let u of a.split(","))if(!jr.has(u))return!1;break;case"BYMONTHDAY":{let u=Number(a);if(!Number.isInteger(u)||u<1||u>31)return!1;break}default:return!1}}return o}import{z as L}from"zod";var vt=/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$/,ce=1e3,ue=1e4,He=200,ke=100,Se=100,wn=100,ve=50,Te=100,we=5e3;function Ve(t){return t.replace(/[\r\n]+/g," ").trim()}function En(t,{required:e=!1}={}){return t.length===0?{valid:!1,message:e?"Title is required.":"Title cannot be empty."}:t.length>ce?{valid:!1,message:`Title exceeds maximum length of ${ce} characters.`}:{valid:!0}}function xn(t){return t.length>ue?{valid:!1,message:`Note exceeds maximum length of ${ue} characters.`}:{valid:!0}}function On(t){for(let e of t){if(e.length===0)return{valid:!1,message:"Label cannot be empty."};if(e.length>He)return{valid:!1,message:`Label exceeds maximum length of ${He} characters.`}}return{valid:!0}}var Ws=L.enum($),Js=L.enum(D),Hs=L.string().min(1,"Title is required.").max(ce,`Title exceeds maximum length of ${ce} characters.`),Vs=L.string().max(ce,`Title exceeds maximum length of ${ce} characters.`),qs=L.array(L.string().max(He,`Label exceeds maximum length of ${He} characters.`)).max(ke,`labels exceeds maximum of ${ke} items.`),Xs=L.array(L.string().max(ue,`Note exceeds maximum length of ${ue} characters.`)).max(wn,`notes exceeds maximum of ${wn} items.`),Ks=L.string().max(ue,`Note exceeds maximum length of ${ue} characters.`),Gs=L.array(L.string()).max(Se,`blocked_by exceeds maximum of ${Se} items.`),zs=L.record(L.string(),L.unknown()).refine(t=>Object.keys(t).length<=ve,`Metadata exceeds maximum of ${ve} keys.`).refine(t=>Object.keys(t).every(e=>e.length<=Te),`Metadata key exceeds maximum length of ${Te} characters.`).refine(t=>Object.values(t).every(e=>typeof e!="string"||e.length<=we),`Metadata value exceeds maximum length of ${we} characters.`),Qs=L.string().regex(vt,"Invalid date format. Expected YYYY-MM-DD, YYYY-MM-DDTHH:MM, or YYYY-MM-DDTHH:MM:SS.").nullable().optional();function $n(t){let e={title:t.title,status:t.status,priority:t.priority};t.owner&&(e.owner=t.owner),t.due_at&&(e.due=t.due_at),t.recurrence&&(e.recurrence=t.recurrence),e.blocked_by=t.blocked_by,e.labels=t.labels,Object.keys(t.metadata).length>0&&(e.metadata=t.metadata);let n=`+++
|
|
130
|
+
`;if(n+=Wr(e),n+=`
|
|
131
131
|
+++
|
|
132
132
|
`,n+=`
|
|
133
133
|
# Notes
|
|
134
134
|
`,n+=`# Add new notes below. Delete lines to remove notes.
|
|
135
135
|
`,t.notes.length>0){n+=`
|
|
136
136
|
`;for(let o of t.notes)n+=`- ${o.replace(/\r?\n/g,"\\n")}
|
|
137
|
-
`}return n}function
|
|
137
|
+
`}return n}function Dn(t,e){let n=t.match(/^\+\+\+\n([\s\S]*?)\n\+\+\+/);if(!n)throw new Error("Invalid document format: missing +++ delimiters.");let o=n[1],r=Jr(o),s={};if(typeof r.title=="string"&&r.title!==e.title&&(s.title=r.title),typeof r.status=="string"&&r.status!==e.status){if(!oe.has(r.status))throw new Error(`Invalid status: ${r.status}.`);s.status=r.status}if(typeof r.priority=="string"&&r.priority!==e.priority){if(!se.has(r.priority))throw new Error(`Invalid priority: ${r.priority}.`);s.priority=r.priority}let a=r.owner;a===void 0||a===""?e.owner!==null&&(s.owner=null):typeof a=="string"&&a!==e.owner&&(s.owner=a);let u=r.due;if(u===void 0||u==="")e.due_at!==null&&(s.due_at=null);else if(u instanceof Date){let S=u.toISOString().slice(0,10);S!==e.due_at&&(s.due_at=S)}else if(typeof u=="string"&&u!==e.due_at){if(!vt.test(u))throw new Error(`Invalid due date: ${u}. Expected format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS.`);if(isNaN(new Date(u).getTime()))throw new Error(`Invalid due date: ${u}. The date is not a valid calendar date.`);s.due_at=u}let p=r.recurrence;if(p===void 0||p==="")e.recurrence!==null&&e.recurrence!==void 0&&(s.recurrence=null);else if(typeof p=="string"&&p!==e.recurrence){if(!he(p))throw new Error(`Invalid recurrence: ${p}.`);s.recurrence=p}if(Array.isArray(r.blocked_by)){let S=r.blocked_by.filter(O=>typeof O=="string");(S.length!==e.blocked_by.length||S.some((O,F)=>O!==e.blocked_by[F]))&&(s.blocked_by=S)}if(Array.isArray(r.labels)){let S=r.labels.filter(O=>typeof O=="string");(S.length!==e.labels.length||S.some((O,F)=>O!==e.labels[F]))&&(s.labels=S)}if(r.metadata&&typeof r.metadata=="object"&&!Array.isArray(r.metadata)){let S=r.metadata,x=e.metadata;JSON.stringify(S)!==JSON.stringify(x)&&(s.metadata=S)}else!r.metadata&&Object.keys(e.metadata).length>0&&(s.metadata={});let d=t.slice(n[0].length).split(`
|
|
138
138
|
`).filter(S=>S.startsWith("- ")).map(S=>S.slice(2).replace(/\\n/g,`
|
|
139
|
-
`)),_=new Set(e.notes),b=d.filter(S=>!_.has(S))
|
|
139
|
+
`)),_=new Set(e.notes),b=d.filter(S=>!_.has(S)),N=new Set(d),E=e.notes.some(S=>!N.has(S));return{fields:s,newNotes:b,removedNotes:E}}async function Nn(t){let e=Ur.join(Yr.tmpdir(),`oru-edit-${Date.now()}.toml`);Tt.writeFileSync(e,t);let o=(process.env.EDITOR||"vi").split(/\s+/),r=o.shift();return o.push(e),await new Promise((a,u)=>{let p=Br(r,o,{stdio:"inherit"});p.on("exit",m=>{m===0?a():u(new Error(`Editor exited with code ${m}. No changes were saved.`))}),p.on("error",u)}),{edited:Tt.readFileSync(e,"utf-8"),tmpFile:e}}function Rn(t){try{Tt.unlinkSync(t)}catch{}}var Hr={monday:"MO",tuesday:"TU",wednesday:"WE",thursday:"TH",friday:"FR",saturday:"SA",sunday:"SU",mon:"MO",tue:"TU",wed:"WE",thu:"TH",fri:"FR",sat:"SA",sun:"SU"};function qe(t){let e=t.trim();if(e.length===0)throw new Error("Empty recurrence value.");let n="",o=e;if(e.toLowerCase().startsWith("after:")&&(n="after:",o=e.slice(6).trim()),o.startsWith("FREQ=")){let p=`${n}${o}`;if(!he(p))throw new Error(`Invalid RRULE: ${o}`);return p}let r=o.toLowerCase();switch(r){case"daily":return`${n}FREQ=DAILY`;case"weekly":return`${n}FREQ=WEEKLY`;case"monthly":return`${n}FREQ=MONTHLY`;case"yearly":return`${n}FREQ=YEARLY`;case"weekdays":return`${n}FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR`}let s=r.match(/^every\s+(\d+)\s+(day|week|month|year)s?$/);if(s){let p=Number(s[1]),m=s[2].toUpperCase(),_={DAY:"DAILY",WEEK:"WEEKLY",MONTH:"MONTHLY",YEAR:"YEARLY"}[m]??"YEARLY";return`${n}FREQ=${_};INTERVAL=${p}`}let a=r.match(/^every\s+([a-z,]+)$/);if(a){let p=a[1].split(",").map(d=>d.trim()),m=[];for(let d of p){let _=Hr[d];if(!_)throw new Error(`Unknown day: ${d}. Use: monday, tuesday, ..., or mon, tue, ...`);m.push(_)}return`${n}FREQ=WEEKLY;BYDAY=${m.join(",")}`}let u=r.match(/^every\s+(\d+)(?:st|nd|rd|th)$/);if(u){let p=Number(u[1]);if(p<1||p>31)throw new Error(`Invalid month day: ${p}. Must be 1-31.`);return`${n}FREQ=MONTHLY;BYMONTHDAY=${p}`}throw new Error(`Could not parse recurrence: "${e}". Try: daily, weekly, monthly, every 3 days, every monday, every mon,wed,fri, every 15th, or raw RRULE like FREQ=DAILY.`)}de();async function wt(t,e,n){if(e==="tasks")return(await t.list()).filter(r=>r.id.startsWith(n)).slice(0,50).map(r=>`${r.id} ${r.title}`);if(e==="labels"){let o=await t.list(),r=new Set;for(let s of o)for(let a of s.labels)a.startsWith(n)&&r.add(a);return[...r].sort().slice(0,50)}return[]}de();function Ee(){let t=["add","list","labels","get","update","edit","delete","done","start","review","context","log","sync","config","filter",...[],"backup","completions","self-update","telemetry"].join(" "),e=["add","list","labels","get","update","edit","delete","done","start","review","context","log","sync","config","filter",...[],"backup","completions","self-update","telemetry"].join("|"),n="";return`# oru shell completions for bash
|
|
140
140
|
# Install: oru completions bash
|
|
141
141
|
# Print: oru completions bash --print
|
|
142
142
|
|
|
@@ -149,8 +149,8 @@ _oru_completions() {
|
|
|
149
149
|
local filter_subcommands="list show add remove"${""}
|
|
150
150
|
local telemetry_subcommands="status enable disable"
|
|
151
151
|
local completion_shells="bash zsh fish"
|
|
152
|
-
local status_values="${
|
|
153
|
-
local priority_values="${
|
|
152
|
+
local status_values="${$.join(" ")}"
|
|
153
|
+
local priority_values="${D.join(" ")}"
|
|
154
154
|
local sort_values="${B.join(" ")}"
|
|
155
155
|
|
|
156
156
|
# Determine the subcommand
|
|
@@ -266,7 +266,7 @@ ${n}
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
complete -F _oru_completions oru
|
|
269
|
-
`}
|
|
269
|
+
`}de();function xe(){return`#compdef oru
|
|
270
270
|
# oru shell completions for zsh
|
|
271
271
|
# Install: oru completions zsh
|
|
272
272
|
# Print: oru completions zsh --print
|
|
@@ -296,10 +296,10 @@ _oru() {
|
|
|
296
296
|
)
|
|
297
297
|
|
|
298
298
|
local -a status_values
|
|
299
|
-
status_values=(${
|
|
299
|
+
status_values=(${$.join(" ")})
|
|
300
300
|
|
|
301
301
|
local -a priority_values
|
|
302
|
-
priority_values=(${
|
|
302
|
+
priority_values=(${D.join(" ")})
|
|
303
303
|
|
|
304
304
|
local -a sort_values
|
|
305
305
|
sort_values=(${B.join(" ")})
|
|
@@ -494,7 +494,7 @@ ${""} completions)
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
_oru "$@"
|
|
497
|
-
`}
|
|
497
|
+
`}de();function Oe(){return`# oru shell completions for fish
|
|
498
498
|
# Install: oru completions fish
|
|
499
499
|
# Print: oru completions fish --print
|
|
500
500
|
|
|
@@ -558,8 +558,8 @@ complete -c oru -n '__oru_using_command filter' -a list -d 'List all saved filte
|
|
|
558
558
|
complete -c oru -n '__oru_using_command filter' -a show -d 'Show a filter definition'
|
|
559
559
|
complete -c oru -n '__oru_using_command filter' -a add -d 'Save a new named filter'
|
|
560
560
|
complete -c oru -n '__oru_using_command filter' -a remove -d 'Delete a saved filter'
|
|
561
|
-
complete -c oru -n '__oru_using_subcommand filter add' -s s -l status -a '${
|
|
562
|
-
complete -c oru -n '__oru_using_subcommand filter add' -s p -l priority -a '${
|
|
561
|
+
complete -c oru -n '__oru_using_subcommand filter add' -s s -l status -a '${$.join(" ")}' -d 'Status' -r
|
|
562
|
+
complete -c oru -n '__oru_using_subcommand filter add' -s p -l priority -a '${D.join(" ")}' -d 'Priority' -r
|
|
563
563
|
complete -c oru -n '__oru_using_subcommand filter add' -s l -l label -a '(__oru_labels)' -d 'Label' -r
|
|
564
564
|
complete -c oru -n '__oru_using_subcommand filter add' -l owner -d 'Filter by owner' -r
|
|
565
565
|
complete -c oru -n '__oru_using_subcommand filter add' -l due -d 'Filter by due date' -r
|
|
@@ -598,14 +598,14 @@ complete -c oru -n '__oru_using_command backup' -a '(__fish_complete_directories
|
|
|
598
598
|
complete -c oru -n '__oru_using_command sync' -F
|
|
599
599
|
|
|
600
600
|
# Status flag for add, list, update, edit
|
|
601
|
-
complete -c oru -n '__oru_using_command add' -s s -l status -a '${
|
|
602
|
-
complete -c oru -n '__oru_using_command list' -s s -l status -a '${
|
|
603
|
-
complete -c oru -n '__oru_using_command update' -s s -l status -a '${
|
|
601
|
+
complete -c oru -n '__oru_using_command add' -s s -l status -a '${$.join(" ")}' -d 'Status' -r
|
|
602
|
+
complete -c oru -n '__oru_using_command list' -s s -l status -a '${$.join(" ")}' -d 'Status' -r
|
|
603
|
+
complete -c oru -n '__oru_using_command update' -s s -l status -a '${$.join(" ")}' -d 'Status' -r
|
|
604
604
|
|
|
605
605
|
# Priority flag for add, list, update, edit
|
|
606
|
-
complete -c oru -n '__oru_using_command add' -s p -l priority -a '${
|
|
607
|
-
complete -c oru -n '__oru_using_command list' -s p -l priority -a '${
|
|
608
|
-
complete -c oru -n '__oru_using_command update' -s p -l priority -a '${
|
|
606
|
+
complete -c oru -n '__oru_using_command add' -s p -l priority -a '${D.join(" ")}' -d 'Priority' -r
|
|
607
|
+
complete -c oru -n '__oru_using_command list' -s p -l priority -a '${D.join(" ")}' -d 'Priority' -r
|
|
608
|
+
complete -c oru -n '__oru_using_command update' -s p -l priority -a '${D.join(" ")}' -d 'Priority' -r
|
|
609
609
|
|
|
610
610
|
# Label flag
|
|
611
611
|
complete -c oru -n '__oru_using_command add' -s l -l label -a '(__oru_labels)' -d 'Label' -r
|
|
@@ -671,53 +671,53 @@ complete -c oru -n '__oru_using_command sync' -l plaintext -d 'Output as plain t
|
|
|
671
671
|
|
|
672
672
|
# self-update flags
|
|
673
673
|
complete -c oru -n '__oru_using_command self-update' -l check -d 'Only check if an update is available'
|
|
674
|
-
`}import
|
|
674
|
+
`}import Xe from"fs";import G from"path";import Vr from"os";import qr from"readline";function Et(){let t=process.env.SHELL;if(!t)return null;let e=G.basename(t);return e==="bash"||e==="zsh"||e==="fish"?e:null}function In(t,e=Vr.homedir()){switch(t){case"bash":return{scriptPath:G.join(e,".oru","completions.bash"),rcPath:G.join(e,".bashrc")};case"zsh":return{scriptPath:G.join(e,".oru","completions.zsh"),rcPath:G.join(e,".zshrc")};case"fish":return{scriptPath:G.join(e,".config","fish","completions","oru.fish"),rcPath:null}}}function Xr(t){switch(t){case"bash":return Ee();case"zsh":return xe();case"fish":return Oe()}}function Ke(t,e,n){let{scriptPath:o,rcPath:r}=In(t,n);Xe.mkdirSync(G.dirname(o),{recursive:!0}),Xe.writeFileSync(o,Xr(t)),e(`Wrote completions to ${o}`);let s=!1;if(r){let a=`source ${o}`,u=`source ~/.oru/completions.${t}`,p="";try{p=Xe.readFileSync(r,"utf-8")}catch{}if(!p.includes(a)&&!p.includes(u)){let m=p.length>0&&!p.endsWith(`
|
|
675
675
|
`)?`
|
|
676
|
-
`:"";
|
|
677
|
-
`),s=!0,e(`Added source line to ${r}`)}else e(`Source line already present in ${r}`)}return{shell:t,scriptPath:o,rcPath:r,sourceLineAdded:s}}function
|
|
678
|
-
Restart your shell or run: source ~/${
|
|
679
|
-
Completions will be loaded automatically on next shell start.`}import
|
|
680
|
-
`),{}}}function
|
|
676
|
+
`:"";Xe.appendFileSync(r,`${m}${a}
|
|
677
|
+
`),s=!0,e(`Added source line to ${r}`)}else e(`Source line already present in ${r}`)}return{shell:t,scriptPath:o,rcPath:r,sourceLineAdded:s}}function xt(t,e=process.stdin,n=process.stdout){return new Promise(o=>{let r=qr.createInterface({input:e,output:n});r.question(t,s=>{r.close();let a=s.trim().toLowerCase();o(a===""||a==="y"||a==="yes")})})}function Ge(t){return t.rcPath?`
|
|
678
|
+
Restart your shell or run: source ~/${G.basename(t.rcPath)}`:`
|
|
679
|
+
Completions will be loaded automatically on next shell start.`}import ze from"fs";import Ot from"path";import Kr from"os";import{parse as Gr}from"smol-toml";var $t=["status","priority","owner","label","search","sort","actionable","due","overdue","all","limit","offset"];function An(){return process.env.ORU_CONFIG_DIR?Ot.join(process.env.ORU_CONFIG_DIR,"filters.toml"):Ot.join(Kr.homedir(),".oru","filters.toml")}function me(t){let e=t??An();if(!ze.existsSync(e))return{};let n=ze.readFileSync(e,"utf-8");try{return Gr(n)}catch(o){return process.stderr.write(`Warning: Could not parse filters file at ${e}: ${o instanceof Error?o.message:String(o)}. Ignoring.
|
|
680
|
+
`),{}}}function zr(t){return Array.isArray(t)?`[${t.map(e=>`"${String(e).replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ")}]`:typeof t=="string"?`"${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`:typeof t=="boolean"||typeof t=="number"?String(t):`"${String(t)}"`}function Qr(t){return`["${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"]`}function Zr(t){let e=[];for(let[n,o]of Object.entries(t)){let r=[Qr(n)];for(let[s,a]of Object.entries(o))a!==void 0&&r.push(`${s} = ${zr(a)}`);e.push(r.join(`
|
|
681
681
|
`))}return e.join(`
|
|
682
682
|
|
|
683
683
|
`)+(e.length>0?`
|
|
684
|
-
`:"")}function
|
|
685
|
-
`).map(n=>{if(/^(Options|Commands|Arguments):$/.test(n))return e=n.slice(0,-1).toLowerCase(),
|
|
686
|
-
`)}var
|
|
687
|
-
`),n,o=r=>process.stderr.write(r)){let r=n??
|
|
684
|
+
`:"")}function Dt(t,e){let n=e??An();ze.mkdirSync(Ot.dirname(n),{recursive:!0}),ze.writeFileSync(n,Zr(t))}function Ln(t,e){let n={...t};for(let o of $t)t[o]===void 0&&e[o]!==void 0&&(n[o]=e[o]);return n}Qe();At();$e();function Vn(t){let e={};for(let n of t){let o=n.indexOf("=");if(o===-1){n.trim()&&(e[n.trim()]=null);continue}let r=n.slice(0,o).trim(),s=n.slice(o+1);r&&(e[r]=s)}return e}function qn(t){return t.replace(/\b(update -s \w+)\b/g,(e,n)=>fe(n))}function Eo(t){let e="";return t.split(`
|
|
685
|
+
`).map(n=>{if(/^(Options|Commands|Arguments):$/.test(n))return e=n.slice(0,-1).toLowerCase(),A(n);if(n.startsWith("Usage: "))return e="",`${A("Usage:")} ${n.slice(7)}`;if(n.trim()!==""&&!n.startsWith(" "))return e="",n;let o=n.match(/^(\s{2})(\S.*?)(\s{2,})(.*)/);if(o){let[,r,s,a,u]=o;return s.startsWith("-")?r+A(s)+a+u:r+fe(s)+a+w(qn(u))}return e==="commands"&&n.match(/^\s{4,}\S/)?w(qn(n)):n}).join(`
|
|
686
|
+
`)}var xo={formatHelp(t,e){let n=wo.prototype.formatHelp.call(e,t,e);return Eo(n)}};function Xn(t){t.configureHelp(xo);for(let e of t.commands)Xn(e)}function Oo(t,e=r=>process.stdout.write(`${r}
|
|
687
|
+
`),n,o=r=>process.stderr.write(r)){let r=n??ht(),s=rn(t),a=yn(t),u=new Be(s,a),p=new To("oru").description(`${A("oru")} - personal task manager that your agents can operate for you
|
|
688
688
|
|
|
689
|
-
Use --json on any command for machine-readable output (or set ORU_FORMAT=json, or output_format in config). Run 'oru config init' to create a config file. Set ORU_DEBUG=1 for verbose error output.`).version(`${
|
|
689
|
+
Use --json on any command for machine-readable output (or set ORU_FORMAT=json, or output_format in config). Run 'oru config init' to create a config file. Set ORU_DEBUG=1 for verbose error output.`).version(`${J} (${Mn})`);p.configureOutput({writeOut:e,writeErr:e}),p.exitOverride();function m(c){return c.plaintext?!1:!!(c.json||process.env.ORU_FORMAT==="json"||r.output_format==="json")}function d(c,i){e(i?JSON.stringify({error:"ambiguous_prefix",id:c.prefix,matches:c.matches}):`Prefix '${c.prefix}' is ambiguous, matches: ${c.matches.join(", ")}.`),process.exitCode=1}function _(c,i){e(c?JSON.stringify({error:"validation",message:i}):i),process.exitCode=1}function b(c,i){e(c?JSON.stringify({error:"not_found",id:i}):`Task ${i} not found.`),process.exitCode=1}function N(c,i){let l=En(c);return l.valid?!0:(_(i,l.message),!1)}function E(c,i){let l=xn(c);return l.valid?!0:(_(i,l.message),!1)}function S(c,i){if(c.length>ke)return _(i,`labels exceeds maximum of ${ke} items.`),!1;let l=On(c);return l.valid?!0:(_(i,l.message),!1)}async function x(c,i,l,f){try{let y=await l();if(!y){b(i,c);return}f(y)}catch(y){if(y instanceof Z){d(y,i);return}throw y}}function O(c,i){return c.length>Se?(_(i,`blocked_by exceeds maximum of ${Se} items.`),!1):!0}function F(c,i){if(Object.keys(c).length>ve)return _(i,`Metadata exceeds maximum of ${ve} keys.`),!1;for(let l of Object.keys(c))if(l.length>Te)return _(i,`Metadata key exceeds maximum length of ${Te} characters.`),!1;for(let l of Object.values(c))if(typeof l=="string"&&l.length>we)return _(i,`Metadata value exceeds maximum length of ${we} characters.`),!1;return!0}p.command("add <title>").description("Add a new task").option("--id <id>","Task ID (for idempotent creates)").addOption(new Q("-s, --status <status>","Initial status").choices($).default("todo")).addOption(new Q("-p, --priority <priority>","Priority level").choices(D).default("medium")).option("-d, --due <date>","Due date (e.g. 'tomorrow', 'Monday', 'mon 9am', 'in 3 days', 'end of week', '2026-03-20')").option("--assign <owner>","Assign to owner").option("-l, --label <labels...>","Add labels").option("-b, --blocked-by <ids...>","IDs of tasks that block this task").option("-n, --note <note>","Add an initial note").option("-r, --repeat <rule>","Recurrence rule (e.g. daily, weekly, 'every monday', FREQ=DAILY)").option("--meta <key=value...>","Metadata key=value pairs (key alone removes it)").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
690
690
|
Examples:
|
|
691
691
|
$ oru add "Fix login bug"
|
|
692
692
|
$ oru add "Fix login bug" -p high -d friday
|
|
693
693
|
$ oru add "Write docs" -l docs -n "Include API section"
|
|
694
694
|
$ oru add "Deploy v2" -s todo -d 2026-03-01 --assign alice
|
|
695
|
-
$ oru add "Water plants" -r "every 3 days" -d today`).action(async(c,i)=>{c=
|
|
695
|
+
$ oru add "Water plants" -r "every 3 days" -d today`).action(async(c,i)=>{c=Ve(c);let l=m(i);if(!N(c,l)||i.note&&!E(i.note,l)||i.label&&!S(i.label,l)||i.blockedBy&&!O(i.blockedBy,l))return;if(i.blockedBy){let T=await u.validateBlockedBy(null,i.blockedBy);if(!T.valid){_(l,T.error);return}}if(i.id&&!Ht(i.id)){_(l,`Invalid ID format: "${i.id}". IDs must be 11-character base62 strings.`);return}if(i.id){let T=await u.get(i.id);if(T){e(l?q(T):W(T));return}}let f;if(i.due){let T=St(i.due,r.date_format,r.first_day_of_week,r.next_month);if(!T){_(l,`Could not parse due date: ${i.due}. Try: today, tomorrow, next friday, 2026-03-01, or march 15.`);return}f=T}let y;if(i.repeat)try{y=qe(i.repeat)}catch(T){_(l,T instanceof Error?T.message:String(T));return}let v=i.meta?Vn(i.meta):void 0;if(v&&!F(v,l))return;let k=i.assign!==void 0&&i.assign.trim()===""?null:i.assign,h=await u.add({title:c,id:i.id,status:i.status,priority:i.priority,owner:k,due_at:f,recurrence:y,blocked_by:i.blockedBy,labels:i.label??void 0,notes:i.note?[i.note]:void 0,metadata:v});e(l?q(h):W(h))}),p.command("list").description("List tasks (hides done tasks by default)").option("-s, --status <status>","Filter by status (comma-separated for multiple)",c=>{let i=c.split(",");for(let l of i)if(!$.includes(l))throw new Error(`Invalid status: ${l}. Allowed: ${$.join(", ")}`);return i.length===1?i[0]:i}).option("-p, --priority <priority>","Filter by priority (comma-separated for multiple)",c=>{let i=c.split(",");for(let l of i)if(!D.includes(l))throw new Error(`Invalid priority: ${l}. Allowed: ${D.join(", ")}`);return i.length===1?i[0]:i}).option("-l, --label <label>","Filter by label").option("--owner <owner>","Filter by owner").addOption(new Q("--due <range>","Filter by due date").choices(["today","this-week"])).option("--overdue","Show only overdue tasks").addOption(new Q("--sort <field>","Sort order").choices(B)).option("--search <query>","Search tasks by title").option("-a, --all","Include done tasks").option("--actionable","Show only tasks with no incomplete blockers").option("--limit <n>","Maximum number of tasks to return",Number).option("--offset <n>","Number of tasks to skip",Number).option("--filter <name>","Apply a saved filter (see 'oru filter list')").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
696
696
|
Examples:
|
|
697
697
|
$ oru list
|
|
698
698
|
$ oru list -s in_progress -p high
|
|
699
699
|
$ oru list -l backend --sort due --actionable
|
|
700
700
|
$ oru list --search "login" --all
|
|
701
|
-
$ oru list --filter mine`).action(async c=>{let i=m(c),l=c,f;if(c.filter){let k=
|
|
701
|
+
$ oru list --filter mine`).action(async c=>{let i=m(c),l=c,f;if(c.filter){let k=me()[c.filter];if(!k){_(i,`Filter '${c.filter}' not found. Run 'oru filter list' to see available filters.`);return}l={...c,...Ln(c,k)},f=k.sql}let y=await u.list({status:l.status,priority:l.priority,owner:l.owner,label:l.label,search:l.search,sort:l.sort,actionable:l.actionable,limit:l.limit,offset:l.offset,sql:f});y=Kt(y,l),l.due&&(y=ut(y,l.due,void 0,r.first_day_of_week)),l.overdue&&(y=ut(y,"overdue")),e(i?dn(y):ct(y))}),p.command("labels").description("List all labels in use").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(async c=>{let i=await u.listLabels(),l=m(c);e(l?mn(i):Zt(i))}),p.command("get <id>").description("Get a task by ID").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
702
702
|
Examples:
|
|
703
703
|
$ oru get 019414a3
|
|
704
|
-
$ oru get 019414a3 --json`).action(async(c,i)=>{let l=m(i);await
|
|
704
|
+
$ oru get 019414a3 --json`).action(async(c,i)=>{let l=m(i);await x(c,l,()=>u.get(c),f=>{e(l?q(f):W(f))})}),p.command("update <id>").description("Update a task").option("-t, --title <title>","New title").addOption(new Q("-s, --status <status>","New status").choices($)).addOption(new Q("-p, --priority <priority>","New priority").choices(D)).option("-d, --due <date>","Due date (e.g. 'tomorrow', 'Monday', 'in 3 days', 'end of week', '2026-03-20', 'none' to clear)").option("--assign <owner>","Assign to owner ('none' to clear)").option("-l, --label <labels...>","Add labels").option("--unlabel <labels...>","Remove labels").option("-b, --blocked-by <ids...>","Set blocker task IDs (replaces full list)").option("--unblock <ids...>","Remove specific blocker task IDs").option("-n, --note <note>","Append a note").option("--clear-notes","Remove all notes").option("-r, --repeat <rule>","Recurrence rule ('none' to clear)").option("--meta <key=value...>","Metadata key=value pairs (key alone removes it)").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
705
705
|
Examples:
|
|
706
706
|
$ oru update 019414a3 -s in_progress
|
|
707
707
|
$ oru update 019414a3 -l urgent -d tomorrow
|
|
708
708
|
$ oru update 019414a3 -n "Blocked on API review"
|
|
709
709
|
$ oru update 019414a3 -t "New title" -p high
|
|
710
|
-
$ oru update 019414a3 -r "every monday"`).action(async(c,i)=>{let l=m(i);if(i.title!==void 0&&(i.title=
|
|
711
|
-
${w("Next occurrence:")} ${
|
|
710
|
+
$ oru update 019414a3 -r "every monday"`).action(async(c,i)=>{let l=m(i);if(i.title!==void 0&&(i.title=Ve(i.title)),!(i.title!==void 0&&!N(i.title,l))&&!(i.note&&!E(i.note,l))&&!(i.label&&!S(i.label,l))&&!(i.blockedBy&&!O(i.blockedBy,l))){if(i.blockedBy){let f=await u.validateBlockedBy(c,i.blockedBy);if(!f.valid){_(l,f.error);return}}try{let f={};if(i.title&&(f.title=i.title),i.status&&(f.status=i.status),i.priority&&(f.priority=i.priority),i.due!==void 0)if(i.due.toLowerCase()==="none")f.due_at=null;else{let k=St(i.due,r.date_format,r.first_day_of_week,r.next_month);if(!k){_(l,`Could not parse due date: ${i.due}. Try: today, tomorrow, next friday, 2026-03-01, or march 15.`);return}f.due_at=k}if(i.assign!==void 0&&(i.assign.toLowerCase()==="none"||i.assign.trim()===""?f.owner=null:f.owner=i.assign),i.label||i.unlabel){let k=await u.get(c);if(!k){b(l,c);return}let h=[...k.labels];if(i.label)for(let T of i.label)h.includes(T)||h.push(T);if(i.unlabel&&(h=h.filter(T=>!i.unlabel.includes(T))),!S(h,l))return;f.labels=h}if(i.blockedBy||i.unblock){let k;if(i.blockedBy)k=[...i.blockedBy];else{let h=await u.get(c);if(!h){b(l,c);return}k=[...h.blocked_by]}if(i.unblock&&(k=k.filter(h=>!i.unblock.includes(h))),!O(k,l))return;f.blocked_by=k}if(i.repeat!==void 0)if(i.repeat.toLowerCase()==="none")f.recurrence=null;else try{f.recurrence=qe(i.repeat)}catch(k){_(l,k instanceof Error?k.message:String(k));return}if(i.meta){let k=await u.get(c);if(!k){b(l,c);return}let h=Vn(i.meta),T={...k.metadata};for(let[ne,X]of Object.entries(h))X===null?delete T[ne]:T[ne]=X;if(!F(T,l))return;f.metadata=T}let y=Object.keys(f).length>0,v;if(i.clearNotes)v=await u.clearNotesAndUpdate(c,f,i.note);else if(i.note&&y)v=await u.updateWithNote(c,f,i.note);else if(i.note)v=await u.addNote(c,i.note);else if(y)v=await u.update(c,f);else{if(!l){e("No changes.");return}v=await u.get(c)}if(!v){b(l,c);return}if(e(l?q(v):W(v)),v.status==="done"&&v.recurrence){let k=await u.getSpawnedTask(v.id);k&&(l?e(JSON.stringify({spawned:k},null,2)):(e(`
|
|
711
|
+
${w("Next occurrence:")} ${ae(v.recurrence)}`),e(W(k))))}}catch(f){if(f instanceof Z){d(f,l);return}throw f}}}),p.command("edit <id>").description("Open task in $EDITOR for complex edits").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
712
712
|
Examples:
|
|
713
713
|
$ oru edit 019414a3
|
|
714
|
-
$ EDITOR=nano oru edit 019414a3`).action(async(c,i)=>{let l=m(i);try{let f=await u.get(c);if(!f){b(l,c);return}let y
|
|
715
|
-
`);return}
|
|
716
|
-
`).filter(
|
|
714
|
+
$ EDITOR=nano oru edit 019414a3`).action(async(c,i)=>{let l=m(i);try{let f=await u.get(c);if(!f){b(l,c);return}let y=$n(f),{edited:v,tmpFile:k}=await Nn(y),h,T,ne;try{({fields:h,newNotes:T,removedNotes:ne}=Dn(v,f))}catch(Y){let Yt=Y instanceof Error?Y.message:String(Y);_(l,Yt),o(`Your edits are saved at: ${k}
|
|
715
|
+
`);return}Rn(k);let X=Object.keys(h).length>0,nt=T.length>0;if(!X&&!nt&&!ne){e(l?q(f):"No changes.");return}if(h.title!==void 0&&(h.title=Ve(h.title)),h.title!==void 0&&!N(h.title,l))return;for(let Y of T)if(!E(Y,l))return;if(h.labels&&!S(h.labels,l)||h.blocked_by&&!O(h.blocked_by,l))return;if(h.blocked_by){let Y=await u.validateBlockedBy(f.id,h.blocked_by);if(!Y.valid){_(l,Y.error);return}}if(h.metadata&&!F(h.metadata,l))return;let U;if(ne){let Bt=[...v.slice(v.indexOf("+++",3)+3).split(`
|
|
716
|
+
`).filter(rt=>rt.startsWith("- ")).map(rt=>rt.slice(2)),...T];Bt.length===0?U=await u.clearNotes(c):U=await u.replaceNotes(c,Bt),X&&(U=await u.update(c,h))}else if(nt&&T.length===1&&X)U=await u.updateWithNote(c,h,T[0]);else if(nt&&T.length===1&&!X)U=await u.addNote(c,T[0]);else{X&&(U=await u.update(c,h));for(let Y of T)U=await u.addNote(c,Y)}U||(U=await u.get(c)),e(l?q(U):W(U))}catch(f){if(f instanceof Z){d(f,l);return}throw f}});function g(c,i,l,f){p.command(`${c} <id...>`).description(l).option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",f).action(async(y,v)=>{let k=m(v);for(let h of y)await x(h,k,()=>u.update(h,{status:i}),T=>{e(k?q(T):W(T))})})}p.command("done <id...>").description("Mark one or more tasks as done (shortcut for update -s done)").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
717
717
|
Examples:
|
|
718
718
|
$ oru done 019414a3
|
|
719
|
-
$ oru done 019414a3 019414b7`).action(async(c,i)=>{let l=m(i);for(let f of c)await
|
|
720
|
-
${w("Next occurrence:")} ${
|
|
719
|
+
$ oru done 019414a3 019414b7`).action(async(c,i)=>{let l=m(i);for(let f of c)await x(f,l,()=>u.update(f,{status:"done"}),async y=>{if(e(l?q(y):W(y)),y.recurrence){let v=await u.getSpawnedTask(y.id);v&&(l?e(JSON.stringify({spawned:v},null,2)):(e(`
|
|
720
|
+
${w("Next occurrence:")} ${ae(y.recurrence)}`),e(W(v))))}})}),g("start","in_progress","Start one or more tasks (shortcut for update -s in_progress)",`
|
|
721
721
|
Examples:
|
|
722
722
|
$ oru start 019414a3
|
|
723
723
|
$ oru start 019414a3 019414b7`),g("review","in_review","Mark one or more tasks as in_review (shortcut for update -s in_review)",`
|
|
@@ -727,23 +727,23 @@ Examples:
|
|
|
727
727
|
Examples:
|
|
728
728
|
$ oru context
|
|
729
729
|
$ oru context --owner alice
|
|
730
|
-
$ oru context -l backend`).action(async c=>{let{sections:i}=await u.getContext({owner:c.owner,label:c.label}),l=m(c);e(l?
|
|
730
|
+
$ oru context -l backend`).action(async c=>{let{sections:i}=await u.getContext({owner:c.owner,label:c.label}),l=m(c);e(l?un(i):tn(i,new Date))}),p.command("delete <id...>").description("Delete one or more tasks permanently").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
731
731
|
Examples:
|
|
732
732
|
$ oru delete 019414a3
|
|
733
|
-
$ oru delete 019414a3 019414b7`).action(async(c,i)=>{let l=m(i);for(let f of c)await
|
|
733
|
+
$ oru delete 019414a3 019414b7`).action(async(c,i)=>{let l=m(i);for(let f of c)await x(f,l,()=>u.delete(f),()=>{e(l?JSON.stringify({id:f,deleted:!0}):`Deleted ${f}`)})}),p.command("log <id>").description("Show change history of a task").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
734
734
|
Examples:
|
|
735
735
|
$ oru log 019414a3
|
|
736
|
-
$ oru log 019414a3 --json`).action(async(c,i)=>{let l=m(i);await
|
|
736
|
+
$ oru log 019414a3 --json`).action(async(c,i)=>{let l=m(i);await x(c,l,()=>u.log(c),f=>{e(l?pn(f):en(f))})}),p.command("sync <remote-path>").description("Sync with a filesystem remote").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
737
737
|
Examples:
|
|
738
738
|
$ oru sync /mnt/shared/oru
|
|
739
|
-
$ oru sync ~/Dropbox/oru-sync`).action(async(c,i)=>{let l=new
|
|
740
|
-
`),f}});let
|
|
741
|
-
`))}),
|
|
739
|
+
$ oru sync ~/Dropbox/oru-sync`).action(async(c,i)=>{let l=new We(c);try{let f=await _n(t,l,a),y=m(i);e(y?JSON.stringify(f,null,2):`Pushed ${f.pushed} ops, pulled ${f.pulled} ops.`)}catch(f){throw process.stderr.write(`Sync failed, database restored from backup.
|
|
740
|
+
`),f}});let M=p.command("config").description("Manage configuration");M.command("init").description("Create a default config file with documented options").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(c=>{let i=m(c),l=ye();if(jt.existsSync(l)){e(i?JSON.stringify({message:"Config file already exists.",path:l}):`Config file already exists at ${l}`);return}jt.mkdirSync(Ie.dirname(l),{recursive:!0}),jt.writeFileSync(l,bn),e(i?JSON.stringify({message:"Config file created.",path:l}):`Created ${l}`)}),M.command("path").description("Print the config file path").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(c=>{let i=m(c),l=ye();e(i?JSON.stringify({path:l}):l)});let H=p.command("filter").description("Manage saved list filters");H.command("list").description("List all saved filters").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(c=>{let i=m(c),l=me(),f=Object.keys(l);if(i){e(JSON.stringify({filters:f}));return}if(f.length===0){e("No saved filters. Use 'oru filter add <name> [flags]' to create one.");return}for(let y of f)e(y)}),H.command("show <name>").description("Show a filter's definition").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action((c,i)=>{let l=m(i),y=me()[c];if(!y){e(l?JSON.stringify({error:"not_found",name:c}):`Filter '${c}' not found.`),process.exitCode=1;return}if(l){e(JSON.stringify({name:c,...y}));return}let v=[`[${c}]`];for(let[k,h]of Object.entries(y))h!==void 0&&(Array.isArray(h)?v.push(`${k} = ${h.join(", ")}`):v.push(`${k} = ${h}`));e(v.join(`
|
|
741
|
+
`))}),H.command("remove <name>").description("Delete a saved filter").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action((c,i)=>{let l=m(i),f=me();if(!(c in f)){e(l?JSON.stringify({error:"not_found",name:c}):`Filter '${c}' not found.`),process.exitCode=1;return}delete f[c],Dt(f),e(l?JSON.stringify({message:`Removed filter '${c}'.`,name:c}):`Removed filter '${c}'.`)}),H.command("add <name>").description("Save a new named filter (accepts the same flags as 'oru list' plus --sql)").option("-s, --status <status>","Filter by status (comma-separated for multiple)",c=>{let i=c.split(",");for(let l of i)if(!$.includes(l))throw new Error(`Invalid status: ${l}. Allowed: ${$.join(", ")}`);return i.length===1?i[0]:i}).option("-p, --priority <priority>","Filter by priority (comma-separated for multiple)",c=>{let i=c.split(",");for(let l of i)if(!D.includes(l))throw new Error(`Invalid priority: ${l}. Allowed: ${D.join(", ")}`);return i.length===1?i[0]:i}).option("-l, --label <label>","Filter by label").option("--owner <owner>","Filter by owner").addOption(new Q("--due <range>","Filter by due date").choices(["today","this-week"])).option("--overdue","Show only overdue tasks").addOption(new Q("--sort <field>","Sort order").choices(B)).option("--search <query>","Search tasks by title").option("-a, --all","Include done tasks").option("--actionable","Show only tasks with no incomplete blockers").option("--limit <n>","Maximum number of tasks to return",Number).option("--offset <n>","Number of tasks to skip",Number).option("--sql <condition>",`Raw SQL WHERE condition (e.g. "priority = 'urgent'")`).option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
742
742
|
Examples:
|
|
743
743
|
$ oru filter add mine --owner tchayen --status todo
|
|
744
744
|
$ oru filter add upcoming --due this-week --sort due
|
|
745
|
-
$ oru filter add edge --sql "priority = 'urgent'"`).action((c,i)=>{let l=m(i),f={};for(let h of
|
|
745
|
+
$ oru filter add edge --sql "priority = 'urgent'"`).action((c,i)=>{let l=m(i),f={};for(let h of $t)i[h]!==void 0&&(f[h]=i[h]);if(i.sql!==void 0&&(f.sql=i.sql),Object.keys(f).length===0){e(l?JSON.stringify({error:"validation",message:"No filter fields specified."}):"No filter fields specified. Pass at least one flag."),process.exitCode=1;return}let y=me(),v=c in y;y[c]=f,Dt(y);let k=v?`Updated filter '${c}'.`:`Saved filter '${c}'.`;e(l?JSON.stringify({message:k,name:c,filter:f}):k)}),!1;let j=p.command("completions").description("Generate shell completion scripts").action(async()=>{let c=Et();if(!c){e(`Could not detect shell from $SHELL.
|
|
746
746
|
Use: oru completions bash|zsh|fish`),process.exitCode=1;return}if(!process.stdin.isTTY){e(`Detected shell: ${c}
|
|
747
|
-
Run interactively: oru completions ${c}`),process.exitCode=1;return}if(e(`Detected shell: ${c}`),!await
|
|
748
|
-
`),process.exitCode=1,f()})})}),p.command("_complete <type> [prefix]",{hidden:!0}).action(async(c,i)=>{let l=await
|
|
749
|
-
`))}),
|
|
747
|
+
Run interactively: oru completions ${c}`),process.exitCode=1;return}if(e(`Detected shell: ${c}`),!await xt(`Install completions for ${c}? [Y/n] `)){e("Aborted.");return}let l=Ke(c,e);e(Ge(l))});for(let c of["bash","zsh","fish"])j.command(c).description(`Install ${c} completions`).option("--print","Print the completion script to stdout instead of installing").action(i=>{if(i.print||!process.stdout.isTTY){e(c==="bash"?Ee():c==="zsh"?xe():Oe());return}let l=Ke(c,e);e(Ge(l))});p.command("self-update").description("Update oru to the latest version").option("--check","Only check if an update is available").action(async c=>{let{performUpdate:i}=await Promise.resolve().then(()=>(Jn(),Wn));await i(!!c.check)});let R=p.command("telemetry").description("Manage anonymous usage telemetry");return R.command("status").description("Show whether telemetry is enabled or disabled").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(c=>{let i=m(c),l=Rt(r);e(i?JSON.stringify({enabled:!l,...l?{reason:l}:{}}):l?`Telemetry: ${l}`:"Telemetry: enabled")}),R.command("enable").description("Enable anonymous usage telemetry").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(c=>{let i=m(c);be("telemetry","true"),e(i?JSON.stringify({enabled:!0}):"Telemetry enabled.")}),R.command("disable").description("Disable anonymous usage telemetry").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action(c=>{let i=m(c);be("telemetry","false"),e(i?JSON.stringify({enabled:!1}):"Telemetry disabled.")}),p.command("backup [path]").description("Create a database backup snapshot").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").action((c,i)=>{let l=m(i),f=c??r.backup_path;if(!f){e(l?JSON.stringify({error:"validation",message:"No backup path specified."}):"No backup path specified. Pass a path argument or set backup_path in config."),process.exitCode=1;return}let y=Ze(t,f);e(l?JSON.stringify({path:y}):`Backed up to ${y}`)}),p.command("mcp").description("Start the MCP (Model Context Protocol) server over stdio").action(async()=>{let c=Ie.dirname(Ut(import.meta.url)),i=Ie.join(c,"mcp","index.js"),l=Hn(process.execPath,[i],{stdio:"inherit",env:process.env});await new Promise(f=>{l.on("exit",y=>{y!==null&&(process.exitCode=y),f()}),l.on("error",y=>{process.stderr.write(`Failed to start MCP server: ${y.message}
|
|
748
|
+
`),process.exitCode=1,f()})})}),p.command("_complete <type> [prefix]",{hidden:!0}).action(async(c,i)=>{let l=await wt(u,c,i??"");l.length>0&&e(l.join(`
|
|
749
|
+
`))}),Xn(p),p}async function $o(){let t=Date.now(),e=sn();ln(e);let n=ht(),o=Oo(e,void 0,n);if(n.backup_path){let{autoBackup:a}=await Promise.resolve().then(()=>(At(),jn));a(e,n.backup_path,n.backup_interval)}try{let{showFirstRunNotice:a}=await Promise.resolve().then(()=>(Qe(),It));a(n)}catch(a){process.env.ORU_DEBUG==="1"&&console.error("Telemetry notice failed:",a)}let r;try{let{checkForUpdate:a}=await Promise.resolve().then(()=>(tt(),Ct));r=a(n)}catch(a){process.env.ORU_DEBUG==="1"&&console.error("Update check failed:",a)}let s;try{await o.parseAsync(process.argv)}catch(a){if(a instanceof Error&&"exitCode"in a)process.exitCode=a.exitCode,s="code"in a?String(a.code):"commander.error";else throw a}finally{e.close()}try{let{extractCommandAndFlags:a,buildEvent:u,sendEvent:p}=await Promise.resolve().then(()=>(Qe(),It)),{command:m,flags:d}=a(process.argv);if(Nt(n)&&!m.startsWith("telemetry")){let _=Date.now()-t,b=u(m,d,_,Number(process.exitCode??0),s);p(b)}}catch(a){process.env.ORU_DEBUG==="1"&&console.error("Telemetry send failed:",a)}if(r)try{let a=await r;if(a){let{printUpdateNotice:u}=await Promise.resolve().then(()=>(tt(),Ct));u(a)}}catch(a){process.env.ORU_DEBUG==="1"&&console.error("Update check failed:",a)}}var Do=Ut(import.meta.url),No=process.argv[1]&&Do===process.argv[1];No&&$o().catch(t=>{process.env.ORU_DEBUG==="1"?console.error(t):console.error(t instanceof Error?t.message:String(t)),process.exit(1)});export{Oo as createProgram};
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{StdioServerTransport as
|
|
3
|
-
`),r.transaction(()=>{for(let
|
|
2
|
+
import{StdioServerTransport as He}from"@modelcontextprotocol/sdk/server/stdio.js";import Se from"better-sqlite3";import V from"path";import we from"os";import M from"fs";function Ee(){return process.env.ORU_DB_PATH?process.env.ORU_DB_PATH:V.join(we.homedir(),".oru","oru.db")}function G(r){let t=r??Ee(),e=V.dirname(t);M.existsSync(e)||M.mkdirSync(e,{recursive:!0,mode:448});let s=new Se(t);s.pragma("journal_mode = WAL"),s.pragma("foreign_keys = ON");try{M.chmodSync(t,384)}catch{}return s}function xe(r){let t=r.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();return t?parseInt(t.value,10):0}function Q(r,t){let e=xe(r),s=t.filter(i=>i.version>e).sort((i,o)=>i.version-o.version);if(s.length===0)return 0;let n=s[s.length-1].version;return process.stderr.write(`Migrating database from v${e} to v${n}...
|
|
3
|
+
`),r.transaction(()=>{for(let i of s)i.up(r),r.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(String(i.version));return s.length})()}function z(r){r.exec(`
|
|
4
4
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
5
5
|
id TEXT PRIMARY KEY,
|
|
6
6
|
title TEXT NOT NULL,
|
|
@@ -30,8 +30,8 @@ import{StdioServerTransport as Ie}from"@modelcontextprotocol/sdk/server/stdio.js
|
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '1');
|
|
33
|
-
`),
|
|
33
|
+
`),Q(r,De)}var De=[{version:2,up:r=>{r.exec("CREATE INDEX IF NOT EXISTS idx_oplog_task_id ON oplog(task_id)"),r.exec("CREATE INDEX IF NOT EXISTS idx_oplog_device_id ON oplog(device_id)")}},{version:3,up:r=>{r.exec("CREATE INDEX IF NOT EXISTS idx_oplog_task_timestamp ON oplog(task_id, timestamp, id)")}},{version:4,up:r=>{r.exec("ALTER TABLE tasks ADD COLUMN due_at TEXT")}},{version:5,up:r=>{r.exec("ALTER TABLE tasks ADD COLUMN blocked_by TEXT NOT NULL DEFAULT '[]'")}},{version:6,up:r=>{r.exec("ALTER TABLE tasks ADD COLUMN owner TEXT")}},{version:7,up:r=>{r.exec("ALTER TABLE tasks ADD COLUMN recurrence TEXT")}}];import{Kysely as ve,SqliteDialect as Ie}from"kysely";function Z(r){return new ve({dialect:new Ie({database:r})})}import{randomBytes as Oe}from"crypto";var ee="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",dt=new Set(ee),Ne=11;function U(r,t){let e=0n;for(let n of r)e=e<<8n|BigInt(n);let s=[];for(let n=0;n<t;n++)s.push(ee[Number(e%62n)]),e/=62n;return s.reverse().join("")}function h(){return U(Oe(8),Ne)}function te(r){let t=r.prepare("SELECT value FROM meta WHERE key = 'device_id'").get();if(t)return t.value;let e=h();return r.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('device_id', ?)").run(e),r.prepare("SELECT value FROM meta WHERE key = 'device_id'").get().value}import{sql as je}from"kysely";import{sql as m}from"kysely";var _=["todo","in_progress","in_review","done"],Le=new Set(_),S=["low","medium","high","urgent"],Ae=new Set(S),re=["title","status","priority","owner","due_at","recurrence","blocked_by","labels","metadata"],Re=new Set(re);var ne="todo",se="medium";var F=class extends Error{prefix;matches;constructor(t,e){super(`Prefix '${t}' is ambiguous, matches: ${e.join(", ")}`),this.name="AmbiguousPrefixError",this.prefix=t,this.matches=e}};function w(r,t){try{return JSON.parse(r)}catch{return t}}function P(r){return{id:r.id,title:r.title,status:r.status,priority:r.priority,owner:r.owner,due_at:r.due_at,recurrence:r.recurrence,blocked_by:w(r.blocked_by,[]),labels:w(r.labels,[]),notes:w(r.notes,[]),metadata:w(r.metadata,{}),created_at:r.created_at,updated_at:r.updated_at,deleted_at:r.deleted_at}}async function B(r,t,e){let s=t.id??h(),n=e??new Date().toISOString(),a={id:s,title:t.title,status:t.status??ne,priority:t.priority??se,owner:t.owner??null,due_at:t.due_at??null,recurrence:t.recurrence??null,blocked_by:t.blocked_by??[],labels:t.labels??[],notes:t.notes??[],metadata:t.metadata??{},created_at:n,updated_at:n,deleted_at:null};return await r.insertInto("tasks").values({id:a.id,title:a.title,status:a.status,priority:a.priority,owner:a.owner,due_at:a.due_at,recurrence:a.recurrence,blocked_by:JSON.stringify(a.blocked_by),labels:JSON.stringify(a.labels),notes:JSON.stringify(a.notes),metadata:JSON.stringify(a.metadata),created_at:a.created_at,updated_at:a.updated_at,deleted_at:a.deleted_at}).execute(),a}async function E(r,t){let e=r.selectFrom("tasks").selectAll().where("deleted_at","is",null);if(t?.status&&(Array.isArray(t.status)?e=e.where("status","in",t.status):e=e.where("status","=",t.status)),t?.priority&&(Array.isArray(t.priority)?e=e.where("priority","in",t.priority):e=e.where("priority","=",t.priority)),t?.owner&&(e=e.where("owner","=",t.owner)),t?.label){let a=t.label;e=e.where(m`EXISTS (SELECT 1 FROM json_each(labels) WHERE json_each.value = ${a})`)}if(t?.search){let a=t.search.replace(/[\\%_]/g,"\\$&");e=e.where(m`title LIKE '%' || ${a} || '%' ESCAPE '\\' COLLATE NOCASE`)}switch(t?.actionable&&(e=e.where("status","!=","done").where(m`NOT EXISTS (
|
|
34
34
|
SELECT 1 FROM json_each(tasks.blocked_by) AS dep
|
|
35
35
|
JOIN tasks AS blocker ON blocker.id = dep.value
|
|
36
36
|
WHERE blocker.status != 'done' AND blocker.deleted_at IS NULL
|
|
37
|
-
)`)),t?.sql&&(e=e.where(f`(${f.raw(t.sql)})`)),t?.sort??"priority"){case"due":e=e.orderBy(f`CASE WHEN due_at IS NULL THEN 1 ELSE 0 END`,"asc").orderBy("due_at","asc").orderBy("created_at","asc");break;case"title":e=e.orderBy(f`title COLLATE NOCASE`,"asc").orderBy("created_at","asc");break;case"created":e=e.orderBy("created_at","asc");break;default:e=e.orderBy(f`CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`).orderBy("created_at","asc");break}return(t?.limit!==void 0||t?.offset!==void 0)&&(e=e.limit(t.limit??-1)),t?.offset!==void 0&&(e=e.offset(t.offset)),(await e.execute()).map($)}function G(r,t){return t.all||t.status!==void 0?r:r.filter(e=>e.status!=="done")}async function p(r,t){let e=await r.selectFrom("tasks").selectAll().where("id","=",t).where("deleted_at","is",null).executeTakeFirst();if(e)return $(e);if(!t)return null;let s=t.replace(/[\\%_]/g,"\\$&"),n=await r.selectFrom("tasks").selectAll().where(f`id LIKE ${s} || '%' ESCAPE '\\'`).where("deleted_at","is",null).execute();if(n.length===1)return $(n[0]);if(n.length>1)throw new L(t,n.map(i=>i.id));return null}async function E(r,t,e,s){let n=await p(r,t);if(!n)return null;let a={updated_at:s??new Date().toISOString()};return e.title!==void 0&&(a.title=e.title),e.status!==void 0&&(a.status=e.status),e.priority!==void 0&&(a.priority=e.priority),e.owner!==void 0&&(a.owner=e.owner),e.due_at!==void 0&&(a.due_at=e.due_at),e.recurrence!==void 0&&(a.recurrence=e.recurrence),e.blocked_by!==void 0&&(a.blocked_by=JSON.stringify(e.blocked_by)),e.labels!==void 0&&(a.labels=JSON.stringify(e.labels)),e.metadata!==void 0&&(a.metadata=JSON.stringify(e.metadata)),await r.updateTable("tasks").set(a).where("id","=",n.id).execute(),p(r,n.id)}async function D(r,t,e,s){let n=await p(r,t);if(!n)return null;let i=e.trim();if(i.length===0||n.notes.some(u=>u.trim()===i))return n;let a=[...n.notes,i],l=s??new Date().toISOString();return await r.updateTable("tasks").set({notes:JSON.stringify(a),updated_at:l}).where("id","=",n.id).execute(),p(r,n.id)}async function x(r,t,e,s){let n=await p(r,t);if(!n)return null;let i=s??new Date().toISOString();return await r.updateTable("tasks").set({notes:JSON.stringify(e),updated_at:i}).where("id","=",n.id).execute(),p(r,n.id)}async function z(r,t,e){let s=await p(r,t);if(!s)return!1;let n=e??new Date().toISOString(),i=await r.updateTable("tasks").set({deleted_at:n,updated_at:n}).where("id","=",s.id).where("deleted_at","is",null).executeTakeFirst();return BigInt(i.numUpdatedRows)>0n}async function g(r,t,e){let s=T(),n=e??new Date().toISOString();return await r.insertInto("oplog").values({id:s,task_id:t.task_id,device_id:t.device_id,op_type:t.op_type,field:t.field,value:t.value,timestamp:n}).execute(),{id:s,task_id:t.task_id,device_id:t.device_id,op_type:t.op_type,field:t.field,value:t.value,timestamp:n}}function ye(){return"NO_COLOR"in process.env?!1:"FORCE_COLOR"in process.env?!0:process.stdout.isTTY??!1}function I(r,t){let e=`\x1B[${r}m`,s=`\x1B[${t}m`;return n=>ye()?`${e}${n}${s}`:n}var me=I(1,22),be=I(2,22),Te=I(3,23),ke=I(37,39);function M(r,t){let e=t??new Date,s=new Date(Number(r.slice(0,4)),Number(r.slice(5,7))-1,Number(r.slice(8,10)),Number(r.slice(11,13))||0,Number(r.slice(14,16))||0);return r.slice(11,16)==="00:00"&&s.setDate(s.getDate()+1),s<e}function Z(r,t){if(M(r,t))return!1;let e=t??new Date,s=new Date(Number(r.slice(0,4)),Number(r.slice(5,7))-1,Number(r.slice(8,10)),Number(r.slice(11,13))||0,Number(r.slice(14,16))||0);return r.slice(11,16)==="00:00"&&s.setDate(s.getDate()+1),(s.getTime()-e.getTime())/(1e3*60*60)<=48}var he={SU:0,MO:1,TU:2,WE:3,TH:4,FR:5,SA:6};function _e(r){let t=r.split(";"),e="",s=1,n=null,i=null;for(let a of t){let[l,u]=a.split("=");switch(l){case"FREQ":e=u;break;case"INTERVAL":s=Number(u);break;case"BYDAY":n=u.split(",");break;case"BYMONTHDAY":i=Number(u);break}}return{freq:e,interval:s,byDay:n,byMonthDay:i}}function v(r,t){let e=new Date(r);return e.setDate(e.getDate()+t),e}function U(r,t){let e=new Date(r),s=e.getMonth()+t;return e.setMonth(s),e.getMonth()!==(s%12+12)%12&&e.setDate(0),e}function ee(r,t){let e=_e(r);switch(e.freq){case"DAILY":return v(t,e.interval);case"WEEKLY":{if(e.byDay&&e.byDay.length>0){let s=e.byDay.map(l=>he[l]).sort((l,u)=>l-u),n=t.getDay();for(let l of s)if(l>n)return v(t,l-n);let i=7-n+s[0],a=(e.interval-1)*7;return v(t,i+a)}return v(t,e.interval*7)}case"MONTHLY":{if(e.byMonthDay!==null){let s=e.byMonthDay,n=new Date(t);if(t.getDate()<s){if(n.setDate(s),n.getMonth()!==t.getMonth()&&(n=new Date(t.getFullYear(),t.getMonth()+1,0)),n.getTime()<=t.getTime()){n=U(t,e.interval);let i=n.getMonth();n.setDate(s),n.getMonth()!==i&&(n=new Date(n.getFullYear(),i+1,0))}}else{n=U(t,e.interval);let i=n.getMonth();n.setDate(s),n.getMonth()!==i&&(n=new Date(n.getFullYear(),i+1,0))}return n}return U(t,e.interval)}case"YEARLY":{let s=new Date(t);return s.setFullYear(s.getFullYear()+e.interval),s.getMonth()!==t.getMonth()&&s.setDate(0),s}default:throw new Error(`Unsupported FREQ: ${e.freq}`)}}import{createHash as we}from"crypto";var Se="oru-recurrence";function F(r){let t=we("sha256").update(`${Se}:${r}`).digest();return R(t.subarray(0,8),11)}function P(r){return r===null?null:typeof r=="string"?r:JSON.stringify(r)}function te(r){return{title:r.title,status:r.status,priority:r.priority,owner:r.owner,due_at:r.due_at,recurrence:r.recurrence,blocked_by:r.blocked_by,labels:r.labels,notes:r.notes,metadata:r.metadata}}var O=class{constructor(t,e){this.db=t;this.deviceId=e}async add(t){return this.db.transaction().execute(async e=>{let s=new Date().toISOString(),n={...t,owner:t.owner||null},i=await A(e,n,s);return await g(e,{task_id:i.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(te(i))},s),i})}async _maybeSpawn(t,e,s){if(e.status!=="done"||!e.recurrence)return null;let n=F(e.id);if(await p(t,n))return null;let a=e.recurrence,l=a.startsWith("after:");l&&(a=a.slice(6));let u;l?u=new Date(s):e.due_at?u=new Date(e.due_at):u=new Date(s);let d=ee(a,u),c=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}T${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:${String(d.getSeconds()).padStart(2,"0")}`,k={id:n,title:e.title,priority:e.priority,owner:e.owner,due_at:c,recurrence:e.recurrence,labels:[...e.labels],metadata:{...e.metadata}},y=await A(t,k,s);return await g(t,{task_id:y.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(te(y))},s),y}async getSpawnedTask(t){let e=F(t);return p(this.db,e)}async validateBlockedBy(t,e){let s=null;if(t!==null){let i=await p(this.db,t);if(!i)return{valid:!1,error:`Task "${t}" not found.`};s=i.id}let n=[];for(let i of e){let a=await p(this.db,i);if(!a)return{valid:!1,error:`Task "${i}" not found.`};if(s!==null&&a.id===s)return{valid:!1,error:"A task cannot block itself."};n.push(a.id)}if(s!==null&&n.length>0){let i=await S(this.db),a=new Map(i.map(l=>[l.id,l]));for(let l of n){let u=[l],d=new Set;for(;u.length>0;){let c=u.shift();if(c===s)return{valid:!1,error:`Setting blocked_by to "${l}" would create a circular dependency.`};if(d.has(c))continue;d.add(c);let k=a.get(c);if(k)for(let y of k.blocked_by)d.has(y)||u.push(y)}}}return{valid:!0}}async list(t){return S(this.db,t)}async get(t){return p(this.db,t)}async update(t,e){return this.db.transaction().execute(async s=>{let n=new Date().toISOString(),i=await E(s,t,e,n);if(!i)return null;for(let[a,l]of Object.entries(e))a==="note"||l===void 0||await g(s,{task_id:i.id,device_id:this.deviceId,op_type:"update",field:a,value:P(l)},n);return await this._maybeSpawn(s,i,n),i})}async addNote(t,e){return this.db.transaction().execute(async s=>{let n=new Date().toISOString(),i=await p(s,t);if(!i)return null;let a=e.trim();if(a.length===0||i.notes.some(u=>u.trim()===a))return i;let l=await D(s,i.id,a,n);return await g(s,{task_id:i.id,device_id:this.deviceId,op_type:"update",field:"notes",value:a},n),l})}async updateWithNote(t,e,s){return this.db.transaction().execute(async n=>{let i=new Date().toISOString(),a=await E(n,t,e,i);if(!a)return null;let l=a.id;for(let[d,c]of Object.entries(e))d==="note"||c===void 0||await g(n,{task_id:l,device_id:this.deviceId,op_type:"update",field:d,value:P(c)},i);let u=s.trim();return u.length>0&&!a.notes.some(d=>d.trim()===u)&&(a=await D(n,l,u,i),await g(n,{task_id:l,device_id:this.deviceId,op_type:"update",field:"notes",value:u},i)),await this._maybeSpawn(n,a,i),a})}async clearNotes(t){return this.db.transaction().execute(async e=>{let s=new Date().toISOString(),n=await x(e,t,[],s);return n?(await g(e,{task_id:n.id,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},s),n):null})}async clearNotesAndUpdate(t,e,s){return this.db.transaction().execute(async n=>{let i=new Date().toISOString(),a=await x(n,t,[],i);if(!a)return null;let l=a.id;if(await g(n,{task_id:l,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},i),s){let d=s.trim();d.length>0&&(a=await D(n,l,d,i),await g(n,{task_id:l,device_id:this.deviceId,op_type:"update",field:"notes",value:d},i))}if(Object.keys(e).length>0){a=await E(n,l,e,i);for(let[d,c]of Object.entries(e))d==="note"||c===void 0||await g(n,{task_id:l,device_id:this.deviceId,op_type:"update",field:d,value:P(c)},i)}return await this._maybeSpawn(n,a,i),a})}async replaceNotes(t,e){return this.db.transaction().execute(async s=>{let n=new Date().toISOString(),i=await x(s,t,e,n);if(!i)return null;let a=i.id;await g(s,{task_id:a,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},n);for(let l of e)await g(s,{task_id:a,device_id:this.deviceId,op_type:"update",field:"notes",value:l},n);return i})}async listLabels(){let t=await S(this.db),e=new Set;for(let s of t)for(let n of s.labels)e.add(n);return[...e].sort()}async getContext(t){let e=new Date,s=await this.list({sort:"priority",owner:t?.owner,label:t?.label}),n=await this.list({status:"done",sort:"priority",owner:t?.owner,label:t?.label}),i={overdue:[],due_soon:[],in_progress:[],actionable:[],blocked:[],recently_completed:[]},a=new Set(s.filter(c=>c.status!=="done").map(c=>c.id)),l=new Date(e.getTime()-1440*60*1e3).toISOString();for(let c of n)c.updated_at>=l&&i.recently_completed.push(c);for(let c of s){if(c.status==="done")continue;if(c.status==="in_progress"||c.status==="in_review"){i.in_progress.push(c);continue}if(c.due_at&&M(c.due_at,e)){i.overdue.push(c);continue}if(c.due_at&&Z(c.due_at,e)){i.due_soon.push(c);continue}if(c.blocked_by.some(y=>a.has(y))){i.blocked.push(c);continue}if(c.status==="todo"){i.actionable.push(c);continue}}let u=new Map;for(let c of[...s,...n])u.set(c.id,c.title);i.blockerTitles=u;let d={overdue:i.overdue.length,due_soon:i.due_soon.length,in_progress:i.in_progress.length,actionable:i.actionable.length,blocked:i.blocked.length,recently_completed:i.recently_completed.length};return{sections:i,summary:d}}async log(t){let e=await p(this.db,t);return e?await this.db.selectFrom("oplog").selectAll().where("task_id","=",e.id).orderBy("timestamp","asc").orderBy(Ee`rowid`,"asc").execute():null}async delete(t){return this.db.transaction().execute(async e=>{let s=new Date().toISOString(),n=await p(e,t);if(!n)return!1;let i=await z(e,n.id,s);return i&&await g(e,{task_id:n.id,device_id:this.deviceId,op_type:"delete",field:null,value:null},s),i})}};import{McpServer as De}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as o}from"zod";function m(r){let{deleted_at:t,...e}=r;return e}var re="0.0.1";var B=o.enum(h),C=o.enum(_);function xe(r){return r instanceof Error&&"code"in r&&typeof r.code=="string"&&r.code.startsWith("SQLITE_")}function b(r){return xe(r)||r instanceof TypeError?"An internal error occurred. Please try again.":r instanceof Error?r.message:"An internal error occurred. Please try again."}function ne(r){let t=re,e=new De({name:"oru",version:t},{capabilities:{logging:{}}});return e.registerTool("add_task",{title:"Add task",description:"Create a new task. Returns the created task. Defaults to status 'todo' and priority 'medium' if not specified. Pass an 'id' field to enable idempotent creates - if a task with that ID already exists, the existing task is returned instead of creating a duplicate.",inputSchema:o.object({title:o.string().describe("Task title, e.g. 'Fix login bug'"),id:o.string().optional().describe("Custom task ID for idempotent creates. If a task with this ID already exists, the existing task is returned. Must be a 11-character base62 string (alphabet: 0-9, A-Z, a-z)."),status:B.optional().describe("Initial status. Valid values: todo, in_progress, in_review, done. Defaults to 'todo'."),priority:C.optional().describe("Priority level. Valid values: low, medium, high, urgent. Defaults to 'medium'."),owner:o.string().optional().describe("Assign to owner, e.g. 'alice'"),due_at:o.string().optional().describe("Due date as ISO 8601 datetime string, e.g. '2026-03-01T00:00:00.000Z'"),blocked_by:o.array(o.string()).optional().describe("Array of task IDs that must be completed before this task, e.g. ['0196b8e0-...']"),labels:o.array(o.string()).optional().describe("Array of string labels to attach, e.g. ['bug', 'frontend']"),notes:o.array(o.string()).optional().describe("Initial notes to add to the task, e.g. ['Started migration']"),recurrence:o.string().nullable().optional().describe("Recurrence rule in RRULE format, e.g. 'FREQ=DAILY', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'. Prefix with 'after:' for completion-based recurrence (next due computed from completion time instead of current due date), e.g. 'after:FREQ=WEEKLY'. Set to null to remove recurrence."),metadata:o.record(o.string(),o.unknown()).optional().describe("Arbitrary JSON object for storing custom key-value data, e.g. {pr: 42}")})},async s=>{try{let n=await r.add(s);return{content:[{type:"text",text:JSON.stringify(m(n),null,2)}]}}catch(n){let i=n instanceof Error?n.message:String(n);if(s.id&&i.includes("UNIQUE constraint")){let a=await r.get(s.id);if(a)return{content:[{type:"text",text:JSON.stringify(m(a),null,2)}]}}return{content:[{type:"text",text:b(n)}],isError:!0}}}),e.registerTool("update_task",{title:"Update task",description:"Update fields on an existing task. Only send the fields you want to change - omitted fields are left unchanged. Notes are append-only: use the 'note' field to add a new note without affecting existing ones. Returns the updated task.",inputSchema:o.object({id:o.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID"),title:o.string().optional().describe("New title"),status:B.optional().describe("New status. Valid values: todo, in_progress, in_review, done."),priority:C.optional().describe("New priority. Valid values: low, medium, high, urgent."),owner:o.string().nullable().optional().describe("New owner. Set to null to unassign."),due_at:o.string().nullable().optional().describe("New due date as ISO 8601 datetime string, e.g. '2026-03-01T00:00:00.000Z'. Set to null to clear."),blocked_by:o.array(o.string()).optional().describe("Array of task IDs that block this task. Replaces the existing list."),labels:o.array(o.string()).optional().describe("Array of string labels. Replaces the existing list, e.g. ['bug', 'frontend']."),recurrence:o.string().nullable().optional().describe("Recurrence rule in RRULE format, e.g. 'FREQ=DAILY', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'. Prefix with 'after:' for completion-based (next due from completion time). Set to null to remove recurrence."),metadata:o.record(o.string(),o.unknown()).optional().describe("Arbitrary JSON object. Merged with existing metadata."),note:o.string().optional().describe("A note to append to the task. Append-only - existing notes are not affected.")})},async s=>{try{let{id:n,note:i,...a}=s;if(a.metadata!==void 0){let u=await r.get(n);a.metadata={...u?.metadata??{},...a.metadata}}let l=i?await r.updateWithNote(n,a,i):await r.update(n,a);return l?{content:[{type:"text",text:JSON.stringify(m(l),null,2)}]}:{content:[{type:"text",text:`Task not found: ${n}.`}],isError:!0}}catch(n){return{content:[{type:"text",text:b(n)}],isError:!0}}}),e.registerTool("delete_task",{title:"Delete task",description:"Soft-delete a task by ID. The task is marked as deleted and excluded from listings but retained in the oplog for sync purposes.",inputSchema:o.object({id:o.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID")})},async({id:s})=>{try{return await r.delete(s)?{content:[{type:"text",text:`Deleted ${s}.`}]}:{content:[{type:"text",text:`Task not found: ${s}.`}],isError:!0}}catch(n){return{content:[{type:"text",text:b(n)}],isError:!0}}}),e.registerTool("list_tasks",{title:"List tasks",description:"List tasks with optional filters. Returns a JSON array of tasks. Done tasks are excluded by default - pass all: true to include them, or status='done' to see only completed tasks. Use 'actionable' filter to get only tasks that are not blocked and not done. The 'search' filter performs a case-insensitive substring match on task titles.",inputSchema:o.object({status:B.optional().describe("Filter by status. Valid values: todo, in_progress, in_review, done. Pass 'done' to see completed tasks."),priority:C.optional().describe("Filter by priority. Valid values: low, medium, high, urgent."),owner:o.string().optional().describe("Filter by owner, e.g. 'alice'"),label:o.string().optional().describe("Filter by label, e.g. 'bug'"),search:o.string().optional().describe("Substring search across task titles (case-insensitive), e.g. 'login'"),sort:o.enum(["priority","due","title","created"]).optional().describe("Sort order. Valid values: priority, due, title, created."),actionable:o.boolean().optional().describe("When true, returns only actionable tasks - those with status 'todo' that are not blocked by other incomplete tasks."),all:o.boolean().optional().describe("Include done tasks (ignored when status filter is set)"),limit:o.number().optional().describe("Maximum number of results to return"),offset:o.number().optional().describe("Number of results to skip (for pagination)")})},async s=>{try{let{all:n,...i}=s,a=await r.list(i);return a=G(a,{all:n,status:i.status}),{content:[{type:"text",text:JSON.stringify(a.map(m),null,2)}]}}catch(n){return{content:[{type:"text",text:b(n)}],isError:!0}}}),e.registerTool("get_task",{title:"Get task",description:"Get a single task by its full ID or a unique ID prefix. Supports prefix matching - e.g. passing '0196b8' will match if only one task ID starts with that prefix.",inputSchema:o.object({id:o.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID")})},async({id:s})=>{try{let n=await r.get(s);return n?{content:[{type:"text",text:JSON.stringify(m(n),null,2)}]}:{content:[{type:"text",text:`Task not found: ${s}.`}],isError:!0}}catch(n){return{content:[{type:"text",text:b(n)}],isError:!0}}}),e.registerTool("get_context",{title:"Get context",description:"Get a quick status overview of what needs attention. Returns counts and full task lists for: overdue, due soon (within 48h), in progress, actionable (todo + not blocked), blocked, and recently completed (last 24h). Use this for a high-level summary before deciding what to work on next.",inputSchema:o.object({owner:o.string().optional().describe("Scope to a specific owner, e.g. 'alice'"),label:o.string().optional().describe("Filter by label, e.g. 'backend'")})},async s=>{try{let{sections:n,summary:i}=await r.getContext({owner:s.owner,label:s.label}),a={summary:i};for(let[l,u]of Object.entries(n))l==="blockerTitles"?a[l]=u:a[l]=u.map(m);return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}}catch(n){return{content:[{type:"text",text:b(n)}],isError:!0}}}),e.registerTool("add_note",{title:"Add note",description:"Append a note to an existing task. Notes are append-only and deduplicated - adding the same note text twice has no effect. Returns the updated task.",inputSchema:o.object({id:o.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID"),note:o.string().describe("Note text to append, e.g. 'Blocked on API review'")})},async({id:s,note:n})=>{try{let i=await r.addNote(s,n);return i?{content:[{type:"text",text:JSON.stringify(m(i),null,2)}]}:{content:[{type:"text",text:`Task not found: ${s}.`}],isError:!0}}catch(i){return{content:[{type:"text",text:b(i)}],isError:!0}}}),e.registerTool("list_labels",{title:"List labels",description:"List all labels currently in use across all tasks. Returns a flat JSON array of label strings. Useful for discovering available labels before filtering with list_tasks.",inputSchema:o.object({})},async()=>{try{let s=await r.listLabels();return{content:[{type:"text",text:JSON.stringify(s)}]}}catch(s){return{content:[{type:"text",text:b(s)}],isError:!0}}}),e}var Y=j();X(Y);var ve=q(Y),Oe=H(Y),Ne=new O(ve,Oe),Re=ne(Ne),Le=new Ie;await Re.connect(Le);
|
|
37
|
+
)`)),t?.sql&&(e=e.where(m`(${m.raw(t.sql)})`)),t?.sort??"priority"){case"due":e=e.orderBy(m`CASE WHEN due_at IS NULL THEN 1 ELSE 0 END`,"asc").orderBy("due_at","asc").orderBy("created_at","asc");break;case"title":e=e.orderBy(m`title COLLATE NOCASE`,"asc").orderBy("created_at","asc");break;case"created":e=e.orderBy("created_at","asc");break;default:e=e.orderBy(m`CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`).orderBy("created_at","asc");break}return(t?.limit!==void 0||t?.offset!==void 0)&&(e=e.limit(t.limit??-1)),t?.offset!==void 0&&(e=e.offset(t.offset)),(await e.execute()).map(P)}function ae(r,t){return t.all||t.status!==void 0?r:r.filter(e=>e.status!=="done")}async function p(r,t){let e=await r.selectFrom("tasks").selectAll().where("id","=",t).where("deleted_at","is",null).executeTakeFirst();if(e)return P(e);if(!t)return null;let s=t.replace(/[\\%_]/g,"\\$&"),n=await r.selectFrom("tasks").selectAll().where(m`id LIKE ${s} || '%' ESCAPE '\\'`).where("deleted_at","is",null).execute();if(n.length===1)return P(n[0]);if(n.length>1)throw new F(t,n.map(a=>a.id));return null}async function x(r,t,e,s){let n=await p(r,t);if(!n)return null;let i={updated_at:s??new Date().toISOString()};return e.title!==void 0&&(i.title=e.title),e.status!==void 0&&(i.status=e.status),e.priority!==void 0&&(i.priority=e.priority),e.owner!==void 0&&(i.owner=e.owner),e.due_at!==void 0&&(i.due_at=e.due_at),e.recurrence!==void 0&&(i.recurrence=e.recurrence),e.blocked_by!==void 0&&(i.blocked_by=JSON.stringify(e.blocked_by)),e.labels!==void 0&&(i.labels=JSON.stringify(e.labels)),e.metadata!==void 0&&(i.metadata=JSON.stringify(e.metadata)),await r.updateTable("tasks").set(i).where("id","=",n.id).execute(),p(r,n.id)}async function D(r,t,e,s){let n=await p(r,t);if(!n)return null;let a=e.trim();if(a.length===0||n.notes.some(c=>c.trim()===a))return n;let i=[...n.notes,a],o=s??new Date().toISOString();return await r.updateTable("tasks").set({notes:JSON.stringify(i),updated_at:o}).where("id","=",n.id).execute(),p(r,n.id)}async function v(r,t,e,s){let n=await p(r,t);if(!n)return null;let a=s??new Date().toISOString();return await r.updateTable("tasks").set({notes:JSON.stringify(e),updated_at:a}).where("id","=",n.id).execute(),p(r,n.id)}async function ie(r,t,e){let s=await p(r,t);if(!s)return!1;let n=e??new Date().toISOString(),a=await r.updateTable("tasks").set({deleted_at:n,updated_at:n}).where("id","=",s.id).where("deleted_at","is",null).executeTakeFirst();return BigInt(a.numUpdatedRows)>0n}async function f(r,t,e){let s=h(),n=e??new Date().toISOString();return await r.insertInto("oplog").values({id:s,task_id:t.task_id,device_id:t.device_id,op_type:t.op_type,field:t.field,value:t.value,timestamp:n}).execute(),{id:s,task_id:t.task_id,device_id:t.device_id,op_type:t.op_type,field:t.field,value:t.value,timestamp:n}}function $e(){return"NO_COLOR"in process.env?!1:"FORCE_COLOR"in process.env?!0:process.stdout.isTTY??!1}function I(r,t){let e=`\x1B[${r}m`,s=`\x1B[${t}m`;return n=>$e()?`${e}${n}${s}`:n}var Me=I(1,22),Ue=I(2,22),Fe=I(3,23),Pe=I(37,39);function Y(r,t){let e=t??new Date,s=new Date(Number(r.slice(0,4)),Number(r.slice(5,7))-1,Number(r.slice(8,10)),Number(r.slice(11,13))||0,Number(r.slice(14,16))||0);return r.slice(11,16)==="00:00"&&s.setDate(s.getDate()+1),s<e}function oe(r,t){if(Y(r,t))return!1;let e=t??new Date,s=new Date(Number(r.slice(0,4)),Number(r.slice(5,7))-1,Number(r.slice(8,10)),Number(r.slice(11,13))||0,Number(r.slice(14,16))||0);return r.slice(11,16)==="00:00"&&s.setDate(s.getDate()+1),(s.getTime()-e.getTime())/(1e3*60*60)<=48}var Be={SU:0,MO:1,TU:2,WE:3,TH:4,FR:5,SA:6};function Ye(r){let t=r.split(";"),e="",s=1,n=null,a=null;for(let i of t){let[o,c]=i.split("=");switch(o){case"FREQ":e=c;break;case"INTERVAL":s=Number(c);break;case"BYDAY":n=c.split(",");break;case"BYMONTHDAY":a=Number(c);break}}return{freq:e,interval:s,byDay:n,byMonthDay:a}}function O(r,t){let e=new Date(r);return e.setDate(e.getDate()+t),e}function C(r,t){let e=new Date(r),s=e.getMonth()+t;return e.setMonth(s),e.getMonth()!==(s%12+12)%12&&e.setDate(0),e}function le(r,t){let e=Ye(r);switch(e.freq){case"DAILY":return O(t,e.interval);case"WEEKLY":{if(e.byDay&&e.byDay.length>0){let s=e.byDay.map(o=>Be[o]).sort((o,c)=>o-c),n=t.getDay();for(let o of s)if(o>n)return O(t,o-n);let a=7-n+s[0],i=(e.interval-1)*7;return O(t,a+i)}return O(t,e.interval*7)}case"MONTHLY":{if(e.byMonthDay!==null){let s=e.byMonthDay,n=new Date(t);if(t.getDate()<s){if(n.setDate(s),n.getMonth()!==t.getMonth()&&(n=new Date(t.getFullYear(),t.getMonth()+1,0)),n.getTime()<=t.getTime()){n=C(t,e.interval);let a=n.getMonth();n.setDate(s),n.getMonth()!==a&&(n=new Date(n.getFullYear(),a+1,0))}}else{n=C(t,e.interval);let a=n.getMonth();n.setDate(s),n.getMonth()!==a&&(n=new Date(n.getFullYear(),a+1,0))}return n}return C(t,e.interval)}case"YEARLY":{let s=new Date(t);return s.setFullYear(s.getFullYear()+e.interval),s.getMonth()!==t.getMonth()&&s.setDate(0),s}default:throw new Error(`Unsupported FREQ: ${e.freq}`)}}import{createHash as Ce}from"crypto";var We="oru-recurrence";function W(r){let t=Ce("sha256").update(`${We}:${r}`).digest();return U(t.subarray(0,8),11)}function j(r){return r===null?null:typeof r=="string"?r:JSON.stringify(r)}function ce(r){return{title:r.title,status:r.status,priority:r.priority,owner:r.owner,due_at:r.due_at,recurrence:r.recurrence,blocked_by:r.blocked_by,labels:r.labels,notes:r.notes,metadata:r.metadata}}var N=class{constructor(t,e){this.db=t;this.deviceId=e}async add(t){return this.db.transaction().execute(async e=>{let s=new Date().toISOString(),n={...t,owner:t.owner||null},a=await B(e,n,s);return await f(e,{task_id:a.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(ce(a))},s),a})}async _maybeSpawn(t,e,s){if(e.status!=="done"||!e.recurrence)return null;let n=W(e.id);if(await p(t,n))return null;let i=e.recurrence,o=i.startsWith("after:");o&&(i=i.slice(6));let c;o?c=new Date(s):e.due_at?c=new Date(e.due_at):c=new Date(s);let u=le(i,c),l=`${u.getFullYear()}-${String(u.getMonth()+1).padStart(2,"0")}-${String(u.getDate()).padStart(2,"0")}T${String(u.getHours()).padStart(2,"0")}:${String(u.getMinutes()).padStart(2,"0")}:${String(u.getSeconds()).padStart(2,"0")}`,k={id:n,title:e.title,priority:e.priority,owner:e.owner,due_at:l,recurrence:e.recurrence,labels:[...e.labels],metadata:{...e.metadata}},y=await B(t,k,s);return await f(t,{task_id:y.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(ce(y))},s),y}async getSpawnedTask(t){let e=W(t);return p(this.db,e)}async validateBlockedBy(t,e){let s=null;if(t!==null){let a=await p(this.db,t);if(!a)return{valid:!1,error:`Task "${t}" not found.`};s=a.id}let n=[];for(let a of e){let i=await p(this.db,a);if(!i)return{valid:!1,error:`Task "${a}" not found.`};if(s!==null&&i.id===s)return{valid:!1,error:"A task cannot block itself."};n.push(i.id)}if(s!==null&&n.length>0){let a=await E(this.db),i=new Map(a.map(o=>[o.id,o]));for(let o of n){let c=[o],u=new Set;for(;c.length>0;){let l=c.shift();if(l===s)return{valid:!1,error:`Setting blocked_by to "${o}" would create a circular dependency.`};if(u.has(l))continue;u.add(l);let k=i.get(l);if(k)for(let y of k.blocked_by)u.has(y)||c.push(y)}}}return{valid:!0}}async list(t){return E(this.db,t)}async get(t){return p(this.db,t)}async update(t,e){return this.db.transaction().execute(async s=>{let n=new Date().toISOString(),a=await x(s,t,e,n);if(!a)return null;for(let[i,o]of Object.entries(e))i==="note"||o===void 0||await f(s,{task_id:a.id,device_id:this.deviceId,op_type:"update",field:i,value:j(o)},n);return await this._maybeSpawn(s,a,n),a})}async addNote(t,e){return this.db.transaction().execute(async s=>{let n=new Date().toISOString(),a=await p(s,t);if(!a)return null;let i=e.trim();if(i.length===0||a.notes.some(c=>c.trim()===i))return a;let o=await D(s,a.id,i,n);return await f(s,{task_id:a.id,device_id:this.deviceId,op_type:"update",field:"notes",value:i},n),o})}async updateWithNote(t,e,s){return this.db.transaction().execute(async n=>{let a=new Date().toISOString(),i=await x(n,t,e,a);if(!i)return null;let o=i.id;for(let[u,l]of Object.entries(e))u==="note"||l===void 0||await f(n,{task_id:o,device_id:this.deviceId,op_type:"update",field:u,value:j(l)},a);let c=s.trim();return c.length>0&&!i.notes.some(u=>u.trim()===c)&&(i=await D(n,o,c,a),await f(n,{task_id:o,device_id:this.deviceId,op_type:"update",field:"notes",value:c},a)),await this._maybeSpawn(n,i,a),i})}async clearNotes(t){return this.db.transaction().execute(async e=>{let s=new Date().toISOString(),n=await v(e,t,[],s);return n?(await f(e,{task_id:n.id,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},s),n):null})}async clearNotesAndUpdate(t,e,s){return this.db.transaction().execute(async n=>{let a=new Date().toISOString(),i=await v(n,t,[],a);if(!i)return null;let o=i.id;if(await f(n,{task_id:o,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},a),s){let u=s.trim();u.length>0&&(i=await D(n,o,u,a),await f(n,{task_id:o,device_id:this.deviceId,op_type:"update",field:"notes",value:u},a))}if(Object.keys(e).length>0){i=await x(n,o,e,a);for(let[u,l]of Object.entries(e))u==="note"||l===void 0||await f(n,{task_id:o,device_id:this.deviceId,op_type:"update",field:u,value:j(l)},a)}return await this._maybeSpawn(n,i,a),i})}async replaceNotes(t,e){return this.db.transaction().execute(async s=>{let n=new Date().toISOString(),a=await v(s,t,e,n);if(!a)return null;let i=a.id;await f(s,{task_id:i,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},n);for(let o of e)await f(s,{task_id:i,device_id:this.deviceId,op_type:"update",field:"notes",value:o},n);return a})}async listLabels(){let t=await E(this.db),e=new Set;for(let s of t)for(let n of s.labels)e.add(n);return[...e].sort()}async getContext(t){let e=new Date,s=await this.list({sort:"priority",owner:t?.owner,label:t?.label}),n=await this.list({status:"done",sort:"priority",owner:t?.owner,label:t?.label}),a={overdue:[],due_soon:[],in_progress:[],actionable:[],blocked:[],recently_completed:[]},i=new Set(s.filter(l=>l.status!=="done").map(l=>l.id)),o=new Date(e.getTime()-1440*60*1e3).toISOString();for(let l of n)l.updated_at>=o&&a.recently_completed.push(l);for(let l of s){if(l.status==="done")continue;if(l.status==="in_progress"||l.status==="in_review"){a.in_progress.push(l);continue}if(l.due_at&&Y(l.due_at,e)){a.overdue.push(l);continue}if(l.due_at&&oe(l.due_at,e)){a.due_soon.push(l);continue}if(l.blocked_by.some(y=>i.has(y))){a.blocked.push(l);continue}if(l.status==="todo"){a.actionable.push(l);continue}}let c=new Map;for(let l of[...s,...n])c.set(l.id,l.title);a.blockerTitles=c;let u={overdue:a.overdue.length,due_soon:a.due_soon.length,in_progress:a.in_progress.length,actionable:a.actionable.length,blocked:a.blocked.length,recently_completed:a.recently_completed.length};return{sections:a,summary:u}}async log(t){let e=await p(this.db,t);return e?await this.db.selectFrom("oplog").selectAll().where("task_id","=",e.id).orderBy("timestamp","asc").orderBy(je`rowid`,"asc").execute():null}async delete(t){return this.db.transaction().execute(async e=>{let s=new Date().toISOString(),n=await p(e,t);if(!n)return!1;let a=await ie(e,n.id,s);return a&&await f(e,{task_id:n.id,device_id:this.deviceId,op_type:"delete",field:null,value:null},s),a})}};import{McpServer as Je}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as d}from"zod";function b(r){let{deleted_at:t,...e}=r;return e}import{z as g}from"zod";var Xe=/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$/,L=1e3,A=1e4,de=200,ue=100,pe=100,ge=100,fe=50,me=100,ye=5e3;var R=g.enum(_),$=g.enum(S),be=g.string().min(1,"Title is required.").max(L,`Title exceeds maximum length of ${L} characters.`),Te=g.string().max(L,`Title exceeds maximum length of ${L} characters.`),X=g.array(g.string().max(de,`Label exceeds maximum length of ${de} characters.`)).max(ue,`labels exceeds maximum of ${ue} items.`),he=g.array(g.string().max(A,`Note exceeds maximum length of ${A} characters.`)).max(ge,`notes exceeds maximum of ${ge} items.`),J=g.string().max(A,`Note exceeds maximum length of ${A} characters.`),K=g.array(g.string()).max(pe,`blocked_by exceeds maximum of ${pe} items.`),H=g.record(g.string(),g.unknown()).refine(r=>Object.keys(r).length<=fe,`Metadata exceeds maximum of ${fe} keys.`).refine(r=>Object.keys(r).every(t=>t.length<=me),`Metadata key exceeds maximum length of ${me} characters.`).refine(r=>Object.values(r).every(t=>typeof t!="string"||t.length<=ye),`Metadata value exceeds maximum length of ${ye} characters.`),Kt=g.string().regex(Xe,"Invalid date format. Expected YYYY-MM-DD, YYYY-MM-DDTHH:MM, or YYYY-MM-DDTHH:MM:SS.").nullable().optional();var ke="0.0.5";function Ke(r){return r instanceof Error&&"code"in r&&typeof r.code=="string"&&r.code.startsWith("SQLITE_")}function T(r){return Ke(r)||r instanceof TypeError?"An internal error occurred. Please try again.":r instanceof Error?r.message:"An internal error occurred. Please try again."}function _e(r){let t=ke,e=new Je({name:"oru",version:t},{capabilities:{logging:{}}});return e.registerTool("add_task",{title:"Add task",description:"Create a new task. Returns the created task. Defaults to status 'todo' and priority 'medium' if not specified. Pass an 'id' field to enable idempotent creates - if a task with that ID already exists, the existing task is returned instead of creating a duplicate.",inputSchema:d.object({title:be.describe("Task title, e.g. 'Fix login bug'"),id:d.string().optional().describe("Custom task ID for idempotent creates. If a task with this ID already exists, the existing task is returned. Must be a 11-character base62 string (alphabet: 0-9, A-Z, a-z)."),status:R.optional().describe("Initial status. Valid values: todo, in_progress, in_review, done. Defaults to 'todo'."),priority:$.optional().describe("Priority level. Valid values: low, medium, high, urgent. Defaults to 'medium'."),owner:d.string().optional().describe("Assign to owner, e.g. 'alice'"),due_at:d.string().optional().describe("Due date as ISO 8601 datetime string, e.g. '2026-03-01T00:00:00.000Z'"),blocked_by:K.optional().describe("Array of task IDs that must be completed before this task, e.g. ['0196b8e0-...']"),labels:X.optional().describe("Array of string labels to attach, e.g. ['bug', 'frontend']"),notes:he.optional().describe("Initial notes to add to the task, e.g. ['Started migration']"),recurrence:d.string().nullable().optional().describe("Recurrence rule in RRULE format, e.g. 'FREQ=DAILY', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'. Prefix with 'after:' for completion-based recurrence (next due computed from completion time instead of current due date), e.g. 'after:FREQ=WEEKLY'. Set to null to remove recurrence."),metadata:H.optional().describe("Arbitrary JSON object for storing custom key-value data, e.g. {pr: 42}")})},async s=>{try{let n=await r.add(s);return{content:[{type:"text",text:JSON.stringify(b(n),null,2)}]}}catch(n){let a=n instanceof Error?n.message:String(n);if(s.id&&a.includes("UNIQUE constraint")){let i=await r.get(s.id);if(i)return{content:[{type:"text",text:JSON.stringify(b(i),null,2)}]}}return{content:[{type:"text",text:T(n)}],isError:!0}}}),e.registerTool("update_task",{title:"Update task",description:"Update fields on an existing task. Only send the fields you want to change - omitted fields are left unchanged. Notes are append-only: use the 'note' field to add a new note without affecting existing ones. Returns the updated task.",inputSchema:d.object({id:d.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID"),title:Te.optional().describe("New title"),status:R.optional().describe("New status. Valid values: todo, in_progress, in_review, done."),priority:$.optional().describe("New priority. Valid values: low, medium, high, urgent."),owner:d.string().nullable().optional().describe("New owner. Set to null to unassign."),due_at:d.string().nullable().optional().describe("New due date as ISO 8601 datetime string, e.g. '2026-03-01T00:00:00.000Z'. Set to null to clear."),blocked_by:K.optional().describe("Array of task IDs that block this task. Replaces the existing list."),labels:X.optional().describe("Array of string labels. Replaces the existing list, e.g. ['bug', 'frontend']."),recurrence:d.string().nullable().optional().describe("Recurrence rule in RRULE format, e.g. 'FREQ=DAILY', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'. Prefix with 'after:' for completion-based (next due from completion time). Set to null to remove recurrence."),metadata:H.optional().describe("Arbitrary JSON object. Merged with existing metadata."),note:J.optional().describe("A note to append to the task. Append-only - existing notes are not affected.")})},async s=>{try{let{id:n,note:a,...i}=s;if(i.metadata!==void 0){let c=await r.get(n);i.metadata={...c?.metadata??{},...i.metadata}}let o=a?await r.updateWithNote(n,i,a):await r.update(n,i);return o?{content:[{type:"text",text:JSON.stringify(b(o),null,2)}]}:{content:[{type:"text",text:`Task not found: ${n}.`}],isError:!0}}catch(n){return{content:[{type:"text",text:T(n)}],isError:!0}}}),e.registerTool("delete_task",{title:"Delete task",description:"Soft-delete a task by ID. The task is marked as deleted and excluded from listings but retained in the oplog for sync purposes.",inputSchema:d.object({id:d.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID")})},async({id:s})=>{try{return await r.delete(s)?{content:[{type:"text",text:`Deleted ${s}.`}]}:{content:[{type:"text",text:`Task not found: ${s}.`}],isError:!0}}catch(n){return{content:[{type:"text",text:T(n)}],isError:!0}}}),e.registerTool("list_tasks",{title:"List tasks",description:"List tasks with optional filters. Returns a JSON array of tasks. Done tasks are excluded by default - pass all: true to include them, or status='done' to see only completed tasks. Use 'actionable' filter to get only tasks that are not blocked and not done. The 'search' filter performs a case-insensitive substring match on task titles.",inputSchema:d.object({status:R.optional().describe("Filter by status. Valid values: todo, in_progress, in_review, done. Pass 'done' to see completed tasks."),priority:$.optional().describe("Filter by priority. Valid values: low, medium, high, urgent."),owner:d.string().optional().describe("Filter by owner, e.g. 'alice'"),label:d.string().optional().describe("Filter by label, e.g. 'bug'"),search:d.string().optional().describe("Substring search across task titles (case-insensitive), e.g. 'login'"),sort:d.enum(["priority","due","title","created"]).optional().describe("Sort order. Valid values: priority, due, title, created."),actionable:d.boolean().optional().describe("When true, returns only actionable tasks - those with status 'todo' that are not blocked by other incomplete tasks."),all:d.boolean().optional().describe("Include done tasks (ignored when status filter is set)"),limit:d.number().optional().describe("Maximum number of results to return"),offset:d.number().optional().describe("Number of results to skip (for pagination)")})},async s=>{try{let{all:n,...a}=s,i=await r.list(a);return i=ae(i,{all:n,status:a.status}),{content:[{type:"text",text:JSON.stringify(i.map(b),null,2)}]}}catch(n){return{content:[{type:"text",text:T(n)}],isError:!0}}}),e.registerTool("get_task",{title:"Get task",description:"Get a single task by its full ID or a unique ID prefix. Supports prefix matching - e.g. passing '0196b8' will match if only one task ID starts with that prefix.",inputSchema:d.object({id:d.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID")})},async({id:s})=>{try{let n=await r.get(s);return n?{content:[{type:"text",text:JSON.stringify(b(n),null,2)}]}:{content:[{type:"text",text:`Task not found: ${s}.`}],isError:!0}}catch(n){return{content:[{type:"text",text:T(n)}],isError:!0}}}),e.registerTool("get_context",{title:"Get context",description:"Get a quick status overview of what needs attention. Returns counts and full task lists for: overdue, due soon (within 48h), in progress, actionable (todo + not blocked), blocked, and recently completed (last 24h). Use this for a high-level summary before deciding what to work on next.",inputSchema:d.object({owner:d.string().optional().describe("Scope to a specific owner, e.g. 'alice'"),label:d.string().optional().describe("Filter by label, e.g. 'backend'")})},async s=>{try{let{sections:n,summary:a}=await r.getContext({owner:s.owner,label:s.label}),i={summary:a};for(let[o,c]of Object.entries(n))o==="blockerTitles"?i[o]=c:i[o]=c.map(b);return{content:[{type:"text",text:JSON.stringify(i,null,2)}]}}catch(n){return{content:[{type:"text",text:T(n)}],isError:!0}}}),e.registerTool("add_note",{title:"Add note",description:"Append a note to an existing task. Notes are append-only and deduplicated - adding the same note text twice has no effect. Returns the updated task.",inputSchema:d.object({id:d.string().describe("Task ID or unique ID prefix, e.g. '0196b8e0' or full UUID"),note:J.describe("Note text to append, e.g. 'Blocked on API review'")})},async({id:s,note:n})=>{try{let a=await r.addNote(s,n);return a?{content:[{type:"text",text:JSON.stringify(b(a),null,2)}]}:{content:[{type:"text",text:`Task not found: ${s}.`}],isError:!0}}catch(a){return{content:[{type:"text",text:T(a)}],isError:!0}}}),e.registerTool("list_labels",{title:"List labels",description:"List all labels currently in use across all tasks. Returns a flat JSON array of label strings. Useful for discovering available labels before filtering with list_tasks.",inputSchema:d.object({})},async()=>{try{let s=await r.listLabels();return{content:[{type:"text",text:JSON.stringify(s)}]}}catch(s){return{content:[{type:"text",text:T(s)}],isError:!0}}}),e}var q=G();z(q);var qe=Z(q),Ve=te(q),Ge=new N(qe,Ve),Qe=_e(Ge),ze=new He;await Qe.connect(ze);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tchayen/oru",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "oru - agent-friendly todo CLI with offline sync",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -31,14 +31,6 @@
|
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"test": "vitest run",
|
|
36
|
-
"test:watch": "vitest",
|
|
37
|
-
"prepack": "cp ../README.md ../LICENSE . 2>/dev/null || true",
|
|
38
|
-
"build": "tsup",
|
|
39
|
-
"dev": "tsx src/cli.ts",
|
|
40
|
-
"tsgo": "tsgo --noEmit"
|
|
41
|
-
},
|
|
42
34
|
"dependencies": {
|
|
43
35
|
"@hono/node-server": "^1.19.9",
|
|
44
36
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -52,7 +44,6 @@
|
|
|
52
44
|
"zod": "^4.3.6"
|
|
53
45
|
},
|
|
54
46
|
"devDependencies": {
|
|
55
|
-
"@oru/types": "workspace:*",
|
|
56
47
|
"@types/better-sqlite3": "^7.6.13",
|
|
57
48
|
"@types/node": "^22.13.0",
|
|
58
49
|
"@types/qrcode": "^1.5.6",
|
|
@@ -64,5 +55,12 @@
|
|
|
64
55
|
},
|
|
65
56
|
"engines": {
|
|
66
57
|
"node": ">=22"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"dev": "tsx src/cli.ts",
|
|
64
|
+
"tsgo": "tsgo --noEmit"
|
|
67
65
|
}
|
|
68
|
-
}
|
|
66
|
+
}
|