@tchayen/oru 0.0.1
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/cli.js +749 -0
- package/dist/mcp/index.js +37 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tomasz Czajęcki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
```
|
|
2
|
+
██████╗ ██████╗ ██╗ ██╗
|
|
3
|
+
██╔═══██╗██╔══██╗██║ ██║
|
|
4
|
+
██║ ██║██████╔╝██║ ██║
|
|
5
|
+
██║ ██║██╔══██╗██║ ██║
|
|
6
|
+
╚██████╔╝██║ ██║╚██████╔╝
|
|
7
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═════╝
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Personal task manager for the terminal. Designed to be operated by your AI agent.
|
|
11
|
+
|
|
12
|
+
SQLite database on your machine. No accounts. No cloud. Pass `--json` to any command for machine-readable output.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
curl -fsSL https://oru.sh/install.sh | bash
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node 22+.
|
|
21
|
+
|
|
22
|
+
Or install via npm:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @tchayen/oru
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
oru add "Write README for release"
|
|
32
|
+
oru add "Fix login bug" -p high -d friday -l backend
|
|
33
|
+
oru add "Water plants" -r "every 3 days" -d today
|
|
34
|
+
oru list
|
|
35
|
+
oru context # what needs your attention right now
|
|
36
|
+
oru done <id> # recurring tasks auto-spawn the next occurrence
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
43
|
+
| `add <title>` | Add a new task |
|
|
44
|
+
| `list` | List tasks (hides done by default) |
|
|
45
|
+
| `get <id>` | Get a task by ID |
|
|
46
|
+
| `update <id>` | Update a task |
|
|
47
|
+
| `edit <id>` | Open task in `$EDITOR` |
|
|
48
|
+
| `done <id...>` | Mark tasks as done |
|
|
49
|
+
| `start <id...>` | Mark tasks as in_progress |
|
|
50
|
+
| `review <id...>` | Mark tasks as in_review |
|
|
51
|
+
| `delete <id...>` | Delete tasks |
|
|
52
|
+
| `context` | Summary of overdue, due soon, in progress, and actionable tasks |
|
|
53
|
+
| `labels` | List all labels in use |
|
|
54
|
+
| `log <id>` | Show change history of a task |
|
|
55
|
+
| `filter add <name>` | Save a named filter (same flags as `list`) |
|
|
56
|
+
| `filter list` | List saved filters |
|
|
57
|
+
| `filter show <name>` | Show a filter's definition |
|
|
58
|
+
| `filter remove` | Delete a saved filter |
|
|
59
|
+
| `sync <path>` | Sync with a filesystem remote |
|
|
60
|
+
| `backup [path]` | Create a database backup snapshot |
|
|
61
|
+
| `config init` | Create a default config file |
|
|
62
|
+
| `completions` | Generate shell completions (bash, zsh, fish) |
|
|
63
|
+
| `self-update` | Update oru to the latest version |
|
|
64
|
+
|
|
65
|
+
## Agent usage
|
|
66
|
+
|
|
67
|
+
oru is built to be operated by AI agents. Pass `--json` to any command for structured output. Pass `--id` to `add` for idempotent task creation. Attach `--meta key=value` pairs for agent-specific data.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Agent creates a task with a known ID (idempotent)
|
|
71
|
+
oru add "Refactor auth module" --id A1b2C3d4E5f \
|
|
72
|
+
-p high -l backend --meta agent=claude --json
|
|
73
|
+
|
|
74
|
+
# Agent reads what needs attention
|
|
75
|
+
oru context --json
|
|
76
|
+
|
|
77
|
+
# Agent updates a task (prefix match on IDs)
|
|
78
|
+
oru update A1b -s in_progress --meta pr=142 --json
|
|
79
|
+
|
|
80
|
+
# Create a recurring task (auto-spawns next occurrence when done)
|
|
81
|
+
oru add "Weekly standup" -r "every monday" -d "next monday" --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Set `ORU_FORMAT=json` or `output_format = "json"` in config to default to JSON output.
|
|
85
|
+
|
|
86
|
+
## MCP server
|
|
87
|
+
|
|
88
|
+
oru ships with an [MCP](https://modelcontextprotocol.io/) server so AI agents can manage tasks through the standardized protocol.
|
|
89
|
+
|
|
90
|
+
### Claude Desktop / Claude Code
|
|
91
|
+
|
|
92
|
+
Add to your MCP config:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"oru": {
|
|
98
|
+
"command": "npx",
|
|
99
|
+
"args": ["-p", "@tchayen/oru@latest", "oru-mcp"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or if oru is installed globally (`npm install -g @tchayen/oru`):
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"oru": {
|
|
111
|
+
"command": "oru-mcp"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Available tools
|
|
118
|
+
|
|
119
|
+
| Tool | Description |
|
|
120
|
+
| ------------- | --------------------------------- |
|
|
121
|
+
| `add_task` | Create a new task |
|
|
122
|
+
| `update_task` | Update fields on an existing task |
|
|
123
|
+
| `delete_task` | Delete a task by ID |
|
|
124
|
+
| `list_tasks` | List tasks with optional filters |
|
|
125
|
+
| `get_task` | Get a single task by ID |
|
|
126
|
+
| `get_context` | Summary of what needs attention |
|
|
127
|
+
| `add_note` | Append a note to a task |
|
|
128
|
+
| `list_labels` | List all labels in use |
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
oru config init # creates ~/.oru/config.toml with documented options
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Key options:
|
|
137
|
+
|
|
138
|
+
- `date_format` - `"mdy"` (US) or `"dmy"` (international)
|
|
139
|
+
- `first_day_of_week` - `"monday"` or `"sunday"`
|
|
140
|
+
- `output_format` - `"text"` or `"json"`
|
|
141
|
+
- `backup_path` - directory for automatic backups
|
|
142
|
+
- `backup_interval` - minutes between auto-backups (default: 60)
|
|
143
|
+
|
|
144
|
+
## Sync
|
|
145
|
+
|
|
146
|
+
oru syncs between machines via a shared filesystem (Dropbox, iCloud Drive, NAS, etc.):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
oru sync ~/Dropbox/oru-sync
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Conflict resolution is automatic: last-write-wins per field, updates beat deletes, notes append with dedup. The oplog is the source of truth.
|
|
153
|
+
|
|
154
|
+
## Data
|
|
155
|
+
|
|
156
|
+
Everything is stored locally in `~/.oru/oru.db` (SQLite). Override with `ORU_DB_PATH`.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var Yn=Object.defineProperty;var q=(t,e)=>()=>(t&&(e=t(t=0)),e);var Ee=(t,e)=>{for(var n in e)Yn(t,n,{get:e[n],enumerable:!0})};import ie from"fs";import lt from"path";import br from"os";import{parse as hr}from"smol-toml";function fe(){return process.env.ORU_CONFIG_DIR?lt.join(process.env.ORU_CONFIG_DIR,"config.toml"):lt.join(br.homedir(),".oru","config.toml")}function ct(t){let e=t??fe();if(!ie.existsSync(e))return{...it};let n=ie.readFileSync(e,"utf-8"),o;try{o=hr(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
|
+
`),{...it}}let r={...it};return typeof o.date_format=="string"&&kr.has(o.date_format)&&(r.date_format=o.date_format),typeof o.first_day_of_week=="string"&&Sr.has(o.first_day_of_week.toLowerCase())&&(r.first_day_of_week=o.first_day_of_week.toLowerCase()),typeof o.output_format=="string"&&vr.has(o.output_format)&&(r.output_format=o.output_format),typeof o.next_month=="string"&&Tr.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 ge(t,e){let n=fe(),o="";ie.existsSync(n)&&(o=ie.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
|
+
${t} = ${e}
|
|
5
|
+
`,ie.mkdirSync(lt.dirname(n),{recursive:!0}),ie.writeFileSync(n,o)}var it,kr,Sr,vr,Tr,sn,ut=q(()=>{"use strict";it={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},kr=new Set(["dmy","mdy"]),Sr=new Set(["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]),vr=new Set(["text","json"]),Tr=new Set(["same_day","first"]);sn=`# oru configuration
|
|
6
|
+
# Docs: https://github.com/tchayen/oru#configuration
|
|
7
|
+
|
|
8
|
+
# Date input format for slash dates (e.g. 03/04/2026)
|
|
9
|
+
# "mdy" = MM/DD/YYYY (US)
|
|
10
|
+
# "dmy" = DD/MM/YYYY (EU/international)
|
|
11
|
+
date_format = "mdy"
|
|
12
|
+
|
|
13
|
+
# First day of the week, used by "next week" and "end of week"
|
|
14
|
+
# Options: "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"
|
|
15
|
+
first_day_of_week = "monday"
|
|
16
|
+
|
|
17
|
+
# Default output format for CLI commands
|
|
18
|
+
# "text" = human-readable (default)
|
|
19
|
+
# "json" = machine-readable (overridable per-command with --json / --plaintext)
|
|
20
|
+
output_format = "text"
|
|
21
|
+
|
|
22
|
+
# What "next month" means for due dates
|
|
23
|
+
# "same_day" = same day number next month (Feb 15 -> Mar 15, Jan 31 -> Feb 28)
|
|
24
|
+
# "first" = first day of next month (Feb 15 -> Mar 1)
|
|
25
|
+
next_month = "same_day"
|
|
26
|
+
|
|
27
|
+
# Check for new versions on startup (once per 24h)
|
|
28
|
+
# Set to false to disable
|
|
29
|
+
auto_update_check = true
|
|
30
|
+
|
|
31
|
+
# Send anonymous usage data to improve oru
|
|
32
|
+
# Set to false to disable (or set DO_NOT_TRACK=1)
|
|
33
|
+
telemetry = true
|
|
34
|
+
|
|
35
|
+
# Auto-backup: copy the database to this directory on each CLI run
|
|
36
|
+
# (at most once per backup_interval minutes). Disabled by default.
|
|
37
|
+
# backup_path = "~/Dropbox/oru-backup"
|
|
38
|
+
|
|
39
|
+
# Minimum minutes between auto-backups (default: 60)
|
|
40
|
+
# backup_interval = 60
|
|
41
|
+
`});var le=q(()=>{"use strict"});var W,Tn,ke=q(()=>{"use strict";W="0.0.1",Tn="80ce484"});var St={};Ee(St,{buildEvent:()=>Zr,detectCI:()=>wn,extractCommandAndFlags:()=>Kr,getTelemetryDisabledReason:()=>kt,isTelemetryEnabled:()=>ht,sendEvent:()=>zr,showFirstRunNotice:()=>Qr});function ht(t){return!(process.env.DO_NOT_TRACK==="1"||process.env.ORU_TELEMETRY_DISABLED==="1"||t.telemetry===!1)}function kt(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 wn(){return qr.some(t=>process.env[t])}function Kr(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(" ")&&Gr(o,a)&&(o=`${o} ${a}`):(o=a,r=!0)}return{command:o||"(unknown)",flags:n}}function Gr(t,e){return{config:["init","path"],filter:["add","list","show","remove"],completions:["bash","zsh","fish"],telemetry:["status","enable","disable"],...{}}[t]?.includes(e)??!1}function zr(t){let e=process.env.ORU_TELEMETRY_URL??Vr;try{let n=new AbortController,o=setTimeout(()=>n.abort(),Xr);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 Qr(t){try{if(t.telemetry_notice_shown)return;process.stderr.isTTY&&process.stderr.write(`
|
|
42
|
+
oru collects anonymous usage data to improve the tool.
|
|
43
|
+
To disable: oru telemetry disable (or set DO_NOT_TRACK=1)
|
|
44
|
+
|
|
45
|
+
`),ge("telemetry_notice_shown","true")}catch(e){process.env.ORU_DEBUG==="1"&&console.error("Telemetry notice failed:",e)}}function Zr(t,e,n,o,r){let s={cli_version:W,command:t,flags:e,os:process.platform,arch:process.arch,node_version:process.version,is_ci:wn(),duration_ms:n,exit_code:o};return r!==void 0&&(s.error=r),s}var Vr,Xr,qr,We=q(()=>{"use strict";ut();le();ke();Vr="https://telemetry.oru.sh/v1/events",Xr=3e3;qr=["CI","GITHUB_ACTIONS","GITLAB_CI","CIRCLECI","TRAVIS","JENKINS_URL","BUILDKITE","TF_BUILD"]});var xn={};Ee(xn,{autoBackup:()=>to,performBackup:()=>Je,shouldAutoBackup:()=>On});import Se from"fs";import ve from"path";import En from"os";function eo(){return`oru-${new Date().toISOString().replace(/[:.]/g,"-").replace("Z","")}.db`}function Je(t,e){let n=e.startsWith("~")?ve.join(En.homedir(),e.slice(1)):e;Se.mkdirSync(n,{recursive:!0});let o=eo(),r=o.slice(0,-3),s=ve.join(n,o),a=1;for(;Se.existsSync(s);)s=ve.join(n,`${r}-${a}.db`),a++;return t.exec(`VACUUM INTO '${s.replace(/'/g,"''")}'`),s}function On(t,e){let n=t.startsWith("~")?ve.join(En.homedir(),t.slice(1)):t;if(!Se.existsSync(n))return!0;let o=Se.readdirSync(n).filter(a=>a.startsWith("oru-")&&a.endsWith(".db")).sort();if(o.length===0)return!0;let r=Se.statSync(ve.join(n,o[o.length-1]));return(Date.now()-r.mtimeMs)/1e3/60>=e}function to(t,e,n){try{On(e,n)&&Je(t,e)}catch(o){process.env.ORU_DEBUG==="1"&&console.error("Auto-backup failed:",o)}}var vt=q(()=>{"use strict"});async function He(){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 Tt=q(()=>{"use strict"});var Ot={};Ee(Ot,{checkForUpdate:()=>ao,compareVersions:()=>Te,printUpdateNotice:()=>io});import wt from"fs";import Et from"path";import no from"os";function $n(){let t=process.env.ORU_INSTALL_DIR??Et.join(no.homedir(),".oru");return Et.join(t,".update-state.json")}function oo(){try{let t=wt.readFileSync($n(),"utf-8");return JSON.parse(t)}catch{return null}}function so(t){let e=$n();wt.mkdirSync(Et.dirname(e),{recursive:!0}),wt.writeFileSync(e,JSON.stringify(t))}function Te(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 ao(t){if(!t.auto_update_check||process.env.ORU_NO_UPDATE_CHECK==="1"||!process.stderr.isTTY)return null;let e=oo(),n=Date.now();if(e&&n-e.lastChecked<ro)return Te(e.latestVersion,W)>0?e.latestVersion:null;let o=await He();return o?(so({lastChecked:n,latestVersion:o}),Te(o,W)>0?o:null):null}function io(t){process.stderr.write(`
|
|
46
|
+
Update available: ${W} \u2192 ${t}
|
|
47
|
+
Run \`oru self-update\` to upgrade.
|
|
48
|
+
`)}var ro,Ve=q(()=>{"use strict";ke();Tt();ro=1440*60*1e3});var Rn={};Ee(Rn,{performUpdate:()=>fo});import{execSync as Dn}from"child_process";import G from"fs";import ue from"path";import xt from"os";function Nn(){let t=process.env.ORU_INSTALL_DIR??ue.join(xt.homedir(),".oru");return ue.join(t,".install-meta")}function lo(){try{let t=G.readFileSync(Nn(),"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 co(){return lo()?.install_method==="script"?"script":"npm"}function uo(){let t=process.platform==="darwin"?"darwin":"linux",e=process.arch==="arm64"?"arm64":"x64";return`${t}-${e}`}async function mo(){process.stderr.write(`Updating via npm...
|
|
50
|
+
`),Dn("npm install -g @tchayen/oru@latest",{stdio:"inherit"})}async function po(t){let e=process.env.ORU_INSTALL_DIR??ue.join(xt.homedir(),".oru"),n=ue.join(e,"bin"),o=uo(),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=G.mkdtempSync(ue.join(xt.tmpdir(),"oru-update-")),m=ue.join(p,"oru.tar.gz"),d=Buffer.from(await u.arrayBuffer());G.writeFileSync(m,d),G.existsSync(n)&&G.rmSync(n,{recursive:!0}),G.mkdirSync(n,{recursive:!0}),Dn(`tar -xzf "${m}" -C "${n}"`,{stdio:"pipe"}),G.rmSync(p,{recursive:!0});let _=`install_method=script
|
|
52
|
+
version=${t}
|
|
53
|
+
platform=${o}
|
|
54
|
+
installed_at=${new Date().toISOString()}
|
|
55
|
+
`;G.writeFileSync(Nn(),_),process.stderr.write(`Updated to oru v${t}
|
|
56
|
+
`)}async function fo(t){let e=await He();if(!e)throw new Error("Failed to fetch latest version from npm registry.");let n=W;if(Te(e,n)<=0){process.stderr.write(`Already up to date (v${n})
|
|
57
|
+
`);return}if(process.stderr.write(`New version available: v${n} \u2192 v${e}
|
|
58
|
+
`),t)return;co()==="script"?await po(e):await mo()}var In=q(()=>{"use strict";Ve();Tt();ke()});import{fileURLToPath as Dt}from"url";import $t from"fs";import we from"path";import{spawn as Fn}from"child_process";import{Command as go,Option as z,Help as _o}from"commander";import{sql as nr}from"kysely";import{sql as H}from"kysely";import{randomBytes as Wn}from"crypto";var It="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",Jn=new Set(It),Lt=11;function Ke(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(It[Number(n%62n)]),n/=62n;return o.reverse().join("")}function At(t){if(t.length!==Lt)return!1;for(let e of t)if(!Jn.has(e))return!1;return!0}function ne(){return Ke(Wn(8),Lt)}var D=["todo","in_progress","in_review","done"],re=new Set(D),N=["low","medium","high","urgent"],oe=new Set(N);var Mt="todo",Pt="medium";var B=["priority","due","title","created"],Q=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 Oe(t,e){try{return JSON.parse(t)}catch{return e}}function Ge(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:Oe(t.blocked_by,[]),labels:Oe(t.labels,[]),notes:Oe(t.notes,[]),metadata:Oe(t.metadata,{}),created_at:t.created_at,updated_at:t.updated_at,deleted_at:t.deleted_at}}async function ze(t,e,n){let o=e.id??ne(),r=n??new Date().toISOString(),s={id:o,title:e.title,status:e.status??Mt,priority:e.priority??Pt,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 xe(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(H`EXISTS (SELECT 1 FROM json_each(labels) WHERE json_each.value = ${s})`)}if(e?.search){let s=e.search.replace(/[\\%_]/g,"\\$&");n=n.where(H`title LIKE '%' || ${s} || '%' ESCAPE '\\' COLLATE NOCASE`)}switch(e?.actionable&&(n=n.where("status","!=","done").where(H`NOT EXISTS (
|
|
59
|
+
SELECT 1 FROM json_each(tasks.blocked_by) AS dep
|
|
60
|
+
JOIN tasks AS blocker ON blocker.id = dep.value
|
|
61
|
+
WHERE blocker.status != 'done' AND blocker.deleted_at IS NULL
|
|
62
|
+
)`)),e?.sql&&(n=n.where(H`(${H.raw(e.sql)})`)),e?.sort??"priority"){case"due":n=n.orderBy(H`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(H`title COLLATE NOCASE`,"asc").orderBy("created_at","asc");break;case"created":n=n.orderBy("created_at","asc");break;default:n=n.orderBy(H`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(Ge)}function Ct(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 Ge(n);if(!e)return null;let o=e.replace(/[\\%_]/g,"\\$&"),r=await t.selectFrom("tasks").selectAll().where(H`id LIKE ${o} || '%' ESCAPE '\\'`).where("deleted_at","is",null).execute();if(r.length===1)return Ge(r[0]);if(r.length>1)throw new Q(e,r.map(s=>s.id));return null}async function $e(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 De(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 Ne(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 Ft(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 A(t,e,n){let o=ne(),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 Hn(){return"NO_COLOR"in process.env?!1:"FORCE_COLOR"in process.env?!0:process.stdout.isTTY??!1}function Re(t,e){let n=`\x1B[${t}m`,o=`\x1B[${e}m`;return r=>Hn()?`${n}${r}${o}`:r}var L=Re(1,22),w=Re(2,22),Qe=Re(3,23),de=Re(37,39);var Vn={MO:"monday",TU:"tuesday",WE:"wednesday",TH:"thursday",FR:"friday",SA:"saturday",SU:"sunday"};function se(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=>Vn[E]??E.toLowerCase());return`${s>1?`every ${s} weeks on `:"every "}${b.join(", ")}${n}`}if(u!==null){let d=Xn(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 Xn(t){let e=["th","st","nd","rd"],n=t%100;return t+(e[(n-20)%10]||e[n]||e[0])}var me={sunday:0,monday:1,tuesday:2,wednesday:3,thursday:4,friday:5,saturday:6};function ae(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 jt(t,e){if(ae(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 qn(t,e){let n=t.slice(0,10),o=t.slice(11,16),r=o==="00:00"?n:`${n} ${o}`;return ae(t,e)?L(r):r}function Kn(t){let e=t.slice(0,10),n=t.slice(11,16);return n==="00:00"?e:`${e} ${n}`}function Ut(t){switch(t){case"urgent":return L(t);case"low":return w(t);default:return t}}function Gn(t){switch(t){case"done":return w(t);case"in_progress":return L(t);case"in_review":return Qe(t);default:return t}}function zn(t){switch(t){case"done":return w("[x]");case"in_progress":return L("[~]");case"in_review":return de("[r]");default:return w("[ ]")}}function Y(t,e){let n=[];n.push(`${w(t.id)} ${L(t.title)}`);let o=` Status: ${Gn(t.status)} Priority: ${Ut(t.priority)}`;if(t.due_at&&(o+=` Due: ${qn(t.due_at,e)}`),n.push(o),t.recurrence&&n.push(` Recurrence: ${se(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("-")} ${Qe(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 Bt(t){return t.length===0?w("No labels found."):t.join(`
|
|
64
|
+
`)}function Ze(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 _=zn(d.status),b=d.owner??"",$=d.due_at?Kn(d.due_at):"",E=d.due_at?ae(d.due_at,e):!1,S=d.labels.length>0?d.labels.join(", "):"",O=Object.keys(d.metadata),x=O.length>0?O.map(C=>`${C}=${d.metadata[C]}`).join(", "):"",P=$.padEnd(s),g=E?L(P):P;return`${_} ${w(d.id.padEnd(n))} ${L(d.title.padEnd(u))} ${Ut(d.priority.padEnd(o))} ${b.padEnd(r)} ${g} ${S.padEnd(a)} ${x}`});return[p,...m].join(`
|
|
66
|
+
`)}function Yt(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=L("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 Wt(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])=>`${L(String(m.length))} ${o[p]}`),u=[w(s.join(", "))];for(let[p,m]of r)if(u.push(`${L(p)} ${w(`(${m.length})`)}`),u.push(Ze(m,e)),p==="Blocked"&&t.blockerTitles){for(let d of m)if(d.blocked_by.length>0){let _=d.blocked_by.map(b=>{let $=t.blockerTitles.get(b);return $?`${b} (${$})`:b}).join(", ");u.push(w(` ${d.id} blocked by: ${_}`))}}return u.join(`
|
|
68
|
+
|
|
69
|
+
`)}function et(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=me[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($=>{if(!$.due_at)return!1;let E=$.due_at.slice(0,10);return E>=_&&E<=b})}case"overdue":return t.filter(a=>a.due_at?ae(a.due_at,r):!1)}}var Qn={SU:0,MO:1,TU:2,WE:3,TH:4,FR:5,SA:6};function Zn(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 Ie(t,e){let n=new Date(t);return n.setDate(n.getDate()+e),n}function tt(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 nt(t,e){let n=Zn(t);switch(n.freq){case"DAILY":return Ie(e,n.interval);case"WEEKLY":{if(n.byDay&&n.byDay.length>0){let o=n.byDay.map(u=>Qn[u]).sort((u,p)=>u-p),r=e.getDay();for(let u of o)if(u>r)return Ie(e,u-r);let s=7-r+o[0],a=(n.interval-1)*7;return Ie(e,s+a)}return Ie(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=tt(e,n.interval);let s=r.getMonth();r.setDate(o),r.getMonth()!==s&&(r=new Date(r.getFullYear(),s+1,0))}}else{r=tt(e,n.interval);let s=r.getMonth();r.setDate(o),r.getMonth()!==s&&(r=new Date(r.getFullYear(),s+1,0))}return r}return tt(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 er}from"crypto";var tr="oru-recurrence";function Le(t){let e=er("sha256").update(`${tr}:${t}`).digest();return Ke(e.subarray(0,8),11)}function rt(t){return t===null?null:typeof t=="string"?t:JSON.stringify(t)}function Jt(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 Ae=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 ze(n,r,o);return await A(n,{task_id:s.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(Jt(s))},o),s})}async _maybeSpawn(e,n,o){if(n.status!=="done"||!n.recurrence)return null;let r=Le(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=nt(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 ze(e,_,o);return await A(e,{task_id:b.id,device_id:this.deviceId,op_type:"create",field:null,value:JSON.stringify(Jt(b))},o),b}async getSpawnedTask(e){let n=Le(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 xe(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 xe(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 $e(o,e,n,r);if(!s)return null;for(let[a,u]of Object.entries(n))a==="note"||u===void 0||await A(o,{task_id:s.id,device_id:this.deviceId,op_type:"update",field:a,value:rt(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 De(o,s.id,a,r);return await A(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 $e(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 A(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:m,value:rt(d)},s);let p=o.trim();return p.length>0&&!a.notes.some(m=>m.trim()===p)&&(a=await De(r,u,p,s),await A(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 Ne(n,e,[],o);return r?(await A(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 Ne(r,e,[],s);if(!a)return null;let u=a.id;if(await A(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 De(r,u,m,s),await A(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:"notes",value:m},s))}if(Object.keys(n).length>0){a=await $e(r,u,n,s);for(let[m,d]of Object.entries(n))m==="note"||d===void 0||await A(r,{task_id:u,device_id:this.deviceId,op_type:"update",field:m,value:rt(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 Ne(o,e,n,r);if(!s)return null;let a=s.id;await A(o,{task_id:a,device_id:this.deviceId,op_type:"update",field:"notes_clear",value:""},r);for(let u of n)await A(o,{task_id:a,device_id:this.deviceId,op_type:"update",field:"notes",value:u},r);return s})}async listLabels(){let e=await xe(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&&ae(d.due_at,n)){s.overdue.push(d);continue}if(d.due_at&&jt(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(nr`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 Ft(n,r.id,o);return s&&await A(n,{task_id:r.id,device_id:this.deviceId,op_type:"delete",field:null,value:null},o),s})}};import{Kysely as rr,SqliteDialect as or}from"kysely";function Ht(t){return new rr({dialect:new or({database:t})})}import sr from"better-sqlite3";import Vt from"path";import ar from"os";import ot from"fs";function ir(){return process.env.ORU_DB_PATH?process.env.ORU_DB_PATH:Vt.join(ar.homedir(),".oru","oru.db")}function Xt(t){let e=t??ir(),n=Vt.dirname(e);ot.existsSync(n)||ot.mkdirSync(n,{recursive:!0,mode:448});let o=new sr(e);o.pragma("journal_mode = WAL"),o.pragma("foreign_keys = ON");try{ot.chmodSync(e,384)}catch{}return o}function lr(t){let e=t.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();return e?parseInt(e.value,10):0}function qt(t,e){let n=lr(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 Kt(t){t.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
title TEXT NOT NULL,
|
|
74
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
75
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
76
|
+
labels TEXT NOT NULL DEFAULT '[]',
|
|
77
|
+
notes TEXT NOT NULL DEFAULT '[]',
|
|
78
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
updated_at TEXT NOT NULL,
|
|
81
|
+
deleted_at TEXT
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS oplog (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
task_id TEXT NOT NULL,
|
|
87
|
+
device_id TEXT NOT NULL,
|
|
88
|
+
op_type TEXT NOT NULL,
|
|
89
|
+
field TEXT,
|
|
90
|
+
value TEXT,
|
|
91
|
+
timestamp TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
95
|
+
key TEXT PRIMARY KEY,
|
|
96
|
+
value TEXT NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '1');
|
|
100
|
+
`),qt(t,cr)}var cr=[{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 Gt(t){let{deleted_at:e,...n}=t;return n}function zt(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(Gt));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 V(t){let{deleted_at:e,...n}=t;return JSON.stringify(n,null,2)}function Qt(t){return JSON.stringify(t.map(Gt),null,2)}function Zt(t){return JSON.stringify(t,null,2)}function en(t){return JSON.stringify(t,null,2)}import ur from"better-sqlite3";import tn from"fs";import dr from"path";var Me=class{db;constructor(e){tn.mkdirSync(dr.dirname(e),{recursive:!0}),this.db=new ur(e),this.db.pragma("journal_mode = WAL"),this.db.pragma("busy_timeout = 5000"),this.db.exec(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS oplog (
|
|
102
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
id TEXT UNIQUE NOT NULL,
|
|
104
|
+
task_id TEXT NOT NULL,
|
|
105
|
+
device_id TEXT NOT NULL,
|
|
106
|
+
op_type TEXT NOT NULL,
|
|
107
|
+
field TEXT,
|
|
108
|
+
value TEXT,
|
|
109
|
+
timestamp TEXT NOT NULL
|
|
110
|
+
);
|
|
111
|
+
`);try{tn.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 at from"fs";import _r from"path";import yr from"os";var mr=new Set(["create","update","delete"]),pr=1e3;function st(t){try{return JSON.parse(t),!0}catch{return!1}}function pe(t){return t.filter(e=>typeof e=="string")}function nn(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)mr.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)fr(t,r)})()}function fr(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=re.has(r.status)?r.status:"todo",u=oe.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)?pe(r.blocked_by):[]),b=JSON.stringify(Array.isArray(r.labels)?pe(r.labels):[]),$=JSON.stringify(r.metadata&&typeof r.metadata=="object"&&!Array.isArray(r.metadata)?r.metadata:{}),E=[...Array.isArray(r.notes)?pe(r.notes):[]],S=null,O=o.timestamp,x=null;for(let g of n)g.op_type==="update"&&(!x||g.timestamp>x)&&(x=g.timestamp);let P={};for(let g of n)if(g.op_type!=="create"){if(g.op_type==="delete"){x!==null&&x>=g.timestamp||(S=g.timestamp,g.timestamp>O&&(O=g.timestamp));continue}if(g.op_type==="update"){let C=g.field;if(!C)continue;if(C==="notes_clear"){E.length=0,g.timestamp>O&&(O=g.timestamp),S&&g.timestamp>=S&&(S=null);continue}if(C==="notes"){if(g.value&&g.value.trim().length>0){let R=g.value.trim();E.length<pr&&!E.some(c=>c.trim()===R)&&E.push(R)}g.timestamp>O&&(O=g.timestamp),S&&g.timestamp>=S&&(S=null);continue}let J=P[C];if(J&&(g.timestamp<J.timestamp||g.timestamp===J.timestamp&&g.id<J.id))continue;let F=!1;switch(C){case"title":typeof g.value=="string"&&(s=g.value,F=!0);break;case"status":g.value&&re.has(g.value)&&(a=g.value,F=!0);break;case"priority":g.value&&oe.has(g.value)&&(u=g.value,F=!0);break;case"owner":p=g.value&&g.value.trim().length>0?g.value:null,F=!0;break;case"due_at":m=g.value&&g.value.trim().length>0?g.value:null,F=!0;break;case"blocked_by":if(g.value&&st(g.value)){let R=JSON.parse(g.value);Array.isArray(R)&&(_=JSON.stringify(pe(R)),F=!0)}break;case"labels":if(g.value&&st(g.value)){let R=JSON.parse(g.value);Array.isArray(R)&&(b=JSON.stringify(pe(R)),F=!0)}break;case"metadata":if(g.value&&st(g.value)){let R=JSON.parse(g.value);typeof R=="object"&&R!==null&&!Array.isArray(R)&&($=g.value,F=!0)}break;case"recurrence":d=g.value&&g.value.trim().length>0?g.value:null,F=!0;break}F&&(P[C]={timestamp:g.timestamp,id:g.id}),g.timestamp>O&&(O=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
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
115
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
116
|
+
title = excluded.title,
|
|
117
|
+
status = excluded.status,
|
|
118
|
+
priority = excluded.priority,
|
|
119
|
+
owner = excluded.owner,
|
|
120
|
+
due_at = excluded.due_at,
|
|
121
|
+
recurrence = excluded.recurrence,
|
|
122
|
+
blocked_by = excluded.blocked_by,
|
|
123
|
+
labels = excluded.labels,
|
|
124
|
+
notes = excluded.notes,
|
|
125
|
+
metadata = excluded.metadata,
|
|
126
|
+
updated_at = excluded.updated_at,
|
|
127
|
+
deleted_at = excluded.deleted_at`).run(e,s,a,u,p,m,d,_,b,JSON.stringify(E),$,o.timestamp,O,S)}var gr=1e3,Pe=class{constructor(e,n,o,r=gr){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;nn(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 rn(t,e,n){let o=t.name,r=_r.join(yr.tmpdir(),`oru-sync-backup-${Date.now()}.db`);t.exec(`VACUUM INTO '${r.replace(/'/g,"''")}'`);try{return await new Pe(t,e,n).sync()}catch(s){t.close();for(let a of["-wal","-shm"])try{at.unlinkSync(o+a)}catch{}throw at.copyFileSync(r,o),s}finally{e.close?.();try{at.unlinkSync(r)}catch{}}}function on(t){let e=t.prepare("SELECT value FROM meta WHERE key = 'device_id'").get();if(e)return e.value;let n=ne();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}ut();var wr={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},an={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 dt(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}=Er(a),m=xr(u,e,n,o,s);if(!m)return null;if(p){let d=$r(p);if(!d)return null;m.setHours(d.hours,d.minutes,0,0)}return Dr(m)}function Er(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 Or(o)?{datePart:n.slice(0,-1).join(" "),timePart:o}:{datePart:t,timePart:null}}function Or(t){return/^\d{1,2}:\d{2}\s*(am?|pm?)?$/i.test(t)||/^\d{1,2}\s*(am?|pm?)$/i.test(t)}function xr(t,e,n,o,r){let s=t.toLowerCase().trim();if(s==="today"||s==="tod")return Z(r);if(s==="tomorrow"||s==="tom"){let m=Z(r);return m.setDate(m.getDate()+1),m}if(s==="tonight"){let m=Z(r);return m.setHours(18,0,0,0),m}if(s==="next week")return dn(r,me[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=(me[n]+6)%7,d=Z(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=wr[m];if(d!==void 0)return dn(r,d)}{let m=s.match(/^in\s+(\d+)\s+(days?|weeks?|months?)$/);if(m){let d=Number(m[1]),_=m[2],b=Z(r);if(_.startsWith("day"))b.setDate(b.getDate()+d);else if(_.startsWith("week"))b.setDate(b.getDate()+d*7);else if(_.startsWith("month")){let $=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($,E))}return b}}{let m=s.match(/^([a-z]+)\s+(\d+)(?:st|nd|rd|th)?$/);if(m){let d=an[m[1]];if(d!==void 0){let _=Number(m[2]);return cn(r,d,_)}}}{let m=s.match(/^(\d+)(?:st|nd|rd|th)?\s+([a-z]+)$/);if(m){let d=an[m[2]];if(d!==void 0){let _=Number(m[1]);return cn(r,d,_)}}}let a=t.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(a)return ee(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 ln(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 ln(m,d,r.getFullYear(),e)}return null}function ln(t,e,n,o){if(o==="dmy"){let s=ee(n,e,t);return s||ee(n,t,e)}let r=ee(n,t,e);return r||ee(n,e,t)}function ee(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 cn(t,e,n){let o=t.getFullYear(),r=ee(o,e,n);return r?r<Z(t)?ee(o+1,e,n):r:null}function $r(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=un(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=un(r,o[2]),r<0||r>23?null:{hours:r,minutes:0}}return null}function un(t,e){let n=e.startsWith("p");return n&&t<12?t+12:!n&&t===12?0:t}function Dr(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 Z(t){return new Date(t.getFullYear(),t.getMonth(),t.getDate())}function dn(t,e){let n=Z(t),o=n.getDay(),r=e-o;return r<=0&&(r+=7),n.setDate(n.getDate()+r),n}import mt from"fs";import Ir from"path";import Lr from"os";import{spawn as Ar}from"child_process";import{stringify as Mr,parse as Pr}from"smol-toml";var Nr=new Set(["DAILY","WEEKLY","MONTHLY","YEARLY"]),Rr=new Set(["MO","TU","WE","TH","FR","SA","SU"]);function _e(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(!Nr.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(!Rr.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}var mn=/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$/;function Ce(t){return t.replace(/[\r\n]+/g," ").trim()}function pn(t,{required:e=!1}={}){return t.length===0?{valid:!1,message:e?"Title is required.":"Title cannot be empty."}:t.length>1e3?{valid:!1,message:"Title exceeds maximum length of 1000 characters."}:{valid:!0}}function fn(t){return t.length>1e4?{valid:!1,message:"Note exceeds maximum length of 10000 characters."}:{valid:!0}}function gn(t){for(let e of t){if(e.length===0)return{valid:!1,message:"Label cannot be empty."};if(e.length>200)return{valid:!1,message:"Label exceeds maximum length of 200 characters."}}return{valid:!0}}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+=Mr(e),n+=`
|
|
131
|
+
+++
|
|
132
|
+
`,n+=`
|
|
133
|
+
# Notes
|
|
134
|
+
`,n+=`# Add new notes below. Delete lines to remove notes.
|
|
135
|
+
`,t.notes.length>0){n+=`
|
|
136
|
+
`;for(let o of t.notes)n+=`- ${o.replace(/\r?\n/g,"\\n")}
|
|
137
|
+
`}return n}function yn(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=Pr(o),s={};if(typeof r.title=="string"&&r.title!==e.title&&(s.title=r.title),typeof r.status=="string"&&r.status!==e.status){if(!re.has(r.status))throw new Error(`Invalid status: ${r.status}.`);s.status=r.status}if(typeof r.priority=="string"&&r.priority!==e.priority){if(!oe.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(!mn.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(!_e(p))throw new Error(`Invalid recurrence: ${p}.`);s.recurrence=p}if(Array.isArray(r.blocked_by)){let S=r.blocked_by.filter(x=>typeof x=="string");(S.length!==e.blocked_by.length||S.some((x,P)=>x!==e.blocked_by[P]))&&(s.blocked_by=S)}if(Array.isArray(r.labels)){let S=r.labels.filter(x=>typeof x=="string");(S.length!==e.labels.length||S.some((x,P)=>x!==e.labels[P]))&&(s.labels=S)}if(r.metadata&&typeof r.metadata=="object"&&!Array.isArray(r.metadata)){let S=r.metadata,O=e.metadata;JSON.stringify(S)!==JSON.stringify(O)&&(s.metadata=S)}else!r.metadata&&Object.keys(e.metadata).length>0&&(s.metadata={});let d=t.slice(n[0].length).split(`
|
|
138
|
+
`).filter(S=>S.startsWith("- ")).map(S=>S.slice(2).replace(/\\n/g,`
|
|
139
|
+
`)),_=new Set(e.notes),b=d.filter(S=>!_.has(S)),$=new Set(d),E=e.notes.some(S=>!$.has(S));return{fields:s,newNotes:b,removedNotes:E}}async function bn(t){let e=Ir.join(Lr.tmpdir(),`oru-edit-${Date.now()}.toml`);mt.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=Ar(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:mt.readFileSync(e,"utf-8"),tmpFile:e}}function hn(t){try{mt.unlinkSync(t)}catch{}}var Cr={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 Fe(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(!_e(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 _=Cr[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.`)}le();async function pt(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[]}le();function ye(){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
|
+
# Install: oru completions bash
|
|
141
|
+
# Print: oru completions bash --print
|
|
142
|
+
|
|
143
|
+
_oru_completions() {
|
|
144
|
+
local cur prev words cword
|
|
145
|
+
_init_completion || return
|
|
146
|
+
|
|
147
|
+
local commands="${t}"
|
|
148
|
+
local config_subcommands="init path"
|
|
149
|
+
local filter_subcommands="list show add remove"${""}
|
|
150
|
+
local telemetry_subcommands="status enable disable"
|
|
151
|
+
local completion_shells="bash zsh fish"
|
|
152
|
+
local status_values="${D.join(" ")}"
|
|
153
|
+
local priority_values="${N.join(" ")}"
|
|
154
|
+
local sort_values="${B.join(" ")}"
|
|
155
|
+
|
|
156
|
+
# Determine the subcommand
|
|
157
|
+
local subcmd=""
|
|
158
|
+
local i
|
|
159
|
+
for ((i = 1; i < cword; i++)); do
|
|
160
|
+
case "\${words[i]}" in
|
|
161
|
+
${e})
|
|
162
|
+
subcmd="\${words[i]}"
|
|
163
|
+
break
|
|
164
|
+
;;
|
|
165
|
+
esac
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# Handle flag values
|
|
169
|
+
case "$prev" in
|
|
170
|
+
-s|--status)
|
|
171
|
+
COMPREPLY=($(compgen -W "$status_values" -- "$cur"))
|
|
172
|
+
return
|
|
173
|
+
;;
|
|
174
|
+
-p|--priority)
|
|
175
|
+
COMPREPLY=($(compgen -W "$priority_values" -- "$cur"))
|
|
176
|
+
return
|
|
177
|
+
;;
|
|
178
|
+
-l|--label|--unlabel)
|
|
179
|
+
local labels
|
|
180
|
+
labels=$(oru _complete labels "$cur" 2>/dev/null)
|
|
181
|
+
COMPREPLY=($(compgen -W "$labels" -- "$cur"))
|
|
182
|
+
return
|
|
183
|
+
;;
|
|
184
|
+
--sort)
|
|
185
|
+
COMPREPLY=($(compgen -W "$sort_values" -- "$cur"))
|
|
186
|
+
return
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
|
|
190
|
+
case "$subcmd" in
|
|
191
|
+
"")
|
|
192
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
193
|
+
;;
|
|
194
|
+
config)
|
|
195
|
+
COMPREPLY=($(compgen -W "$config_subcommands" -- "$cur"))
|
|
196
|
+
;;
|
|
197
|
+
${n}
|
|
198
|
+
completions)
|
|
199
|
+
if [[ "$cur" == -* ]]; then
|
|
200
|
+
COMPREPLY=($(compgen -W "--print" -- "$cur"))
|
|
201
|
+
else
|
|
202
|
+
COMPREPLY=($(compgen -W "$completion_shells" -- "$cur"))
|
|
203
|
+
fi
|
|
204
|
+
;;
|
|
205
|
+
list)
|
|
206
|
+
if [[ "$cur" == -* ]]; then
|
|
207
|
+
COMPREPLY=($(compgen -W "-s --status -p --priority -l --label --owner --due --overdue --sort --search -a --all --actionable --limit --offset --filter --json --plaintext" -- "$cur"))
|
|
208
|
+
fi
|
|
209
|
+
;;
|
|
210
|
+
filter)
|
|
211
|
+
COMPREPLY=($(compgen -W "$filter_subcommands" -- "$cur"))
|
|
212
|
+
;;
|
|
213
|
+
labels)
|
|
214
|
+
if [[ "$cur" == -* ]]; then
|
|
215
|
+
COMPREPLY=($(compgen -W "--json --plaintext" -- "$cur"))
|
|
216
|
+
fi
|
|
217
|
+
;;
|
|
218
|
+
add)
|
|
219
|
+
if [[ "$cur" == -* ]]; then
|
|
220
|
+
COMPREPLY=($(compgen -W "--id -s --status -p --priority -d --due --assign -l --label -b --blocked-by -n --note -r --repeat --meta --json --plaintext" -- "$cur"))
|
|
221
|
+
fi
|
|
222
|
+
;;
|
|
223
|
+
update)
|
|
224
|
+
if [[ "$cur" != -* ]]; then
|
|
225
|
+
local tasks
|
|
226
|
+
tasks=$(oru _complete tasks "$cur" 2>/dev/null | cut -f1)
|
|
227
|
+
COMPREPLY=($(compgen -W "$tasks" -- "$cur"))
|
|
228
|
+
else
|
|
229
|
+
COMPREPLY=($(compgen -W "-t --title -s --status -p --priority -d --due --assign -l --label --unlabel -b --blocked-by --unblock -n --note --clear-notes -r --repeat --meta --json --plaintext" -- "$cur"))
|
|
230
|
+
fi
|
|
231
|
+
;;
|
|
232
|
+
edit)
|
|
233
|
+
if [[ "$cur" != -* ]]; then
|
|
234
|
+
local tasks
|
|
235
|
+
tasks=$(oru _complete tasks "$cur" 2>/dev/null | cut -f1)
|
|
236
|
+
COMPREPLY=($(compgen -W "$tasks" -- "$cur"))
|
|
237
|
+
fi
|
|
238
|
+
;;
|
|
239
|
+
context)
|
|
240
|
+
if [[ "$cur" == -* ]]; then
|
|
241
|
+
COMPREPLY=($(compgen -W "--owner -l --label --json --plaintext" -- "$cur"))
|
|
242
|
+
fi
|
|
243
|
+
;;
|
|
244
|
+
get|delete|done|start|review|log)
|
|
245
|
+
if [[ "$cur" != -* ]]; then
|
|
246
|
+
local tasks
|
|
247
|
+
tasks=$(oru _complete tasks "$cur" 2>/dev/null | cut -f1)
|
|
248
|
+
COMPREPLY=($(compgen -W "$tasks" -- "$cur"))
|
|
249
|
+
fi
|
|
250
|
+
;;
|
|
251
|
+
self-update)
|
|
252
|
+
if [[ "$cur" == -* ]]; then
|
|
253
|
+
COMPREPLY=($(compgen -W "--check" -- "$cur"))
|
|
254
|
+
fi
|
|
255
|
+
;;
|
|
256
|
+
telemetry)
|
|
257
|
+
COMPREPLY=($(compgen -W "$telemetry_subcommands" -- "$cur"))
|
|
258
|
+
;;
|
|
259
|
+
sync)
|
|
260
|
+
_filedir
|
|
261
|
+
;;
|
|
262
|
+
backup)
|
|
263
|
+
_filedir -d
|
|
264
|
+
;;
|
|
265
|
+
esac
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
complete -F _oru_completions oru
|
|
269
|
+
`}le();function be(){return`#compdef oru
|
|
270
|
+
# oru shell completions for zsh
|
|
271
|
+
# Install: oru completions zsh
|
|
272
|
+
# Print: oru completions zsh --print
|
|
273
|
+
|
|
274
|
+
_oru() {
|
|
275
|
+
local -a commands
|
|
276
|
+
commands=(
|
|
277
|
+
'add:Add a new task'
|
|
278
|
+
'list:List tasks'
|
|
279
|
+
'labels:List all labels in use'
|
|
280
|
+
'get:Get a task by ID'
|
|
281
|
+
'update:Update a task'
|
|
282
|
+
'edit:Open task in $EDITOR for complex edits'
|
|
283
|
+
'delete:Delete one or more tasks'
|
|
284
|
+
'done:Mark one or more tasks as done'
|
|
285
|
+
'start:Start one or more tasks'
|
|
286
|
+
'review:Mark one or more tasks as in_review'
|
|
287
|
+
'context:Show a summary of what needs your attention'
|
|
288
|
+
'log:Show change history of a task'
|
|
289
|
+
'sync:Sync with a filesystem remote'
|
|
290
|
+
'config:Manage configuration'
|
|
291
|
+
'filter:Manage saved list filters'${""}
|
|
292
|
+
'completions:Generate shell completions'
|
|
293
|
+
'backup:Create a database backup snapshot'
|
|
294
|
+
'self-update:Update oru to the latest version'
|
|
295
|
+
'telemetry:Manage anonymous usage telemetry'
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
local -a status_values
|
|
299
|
+
status_values=(${D.join(" ")})
|
|
300
|
+
|
|
301
|
+
local -a priority_values
|
|
302
|
+
priority_values=(${N.join(" ")})
|
|
303
|
+
|
|
304
|
+
local -a sort_values
|
|
305
|
+
sort_values=(${B.join(" ")})
|
|
306
|
+
|
|
307
|
+
_arguments -C \\
|
|
308
|
+
'1:command:->command' \\
|
|
309
|
+
'*::arg:->args'
|
|
310
|
+
|
|
311
|
+
case $state in
|
|
312
|
+
command)
|
|
313
|
+
_describe -t commands 'oru command' commands
|
|
314
|
+
;;
|
|
315
|
+
args)
|
|
316
|
+
case $words[1] in
|
|
317
|
+
add)
|
|
318
|
+
_arguments \\
|
|
319
|
+
'--id[Task ID]:id:' \\
|
|
320
|
+
'(-s --status)'{-s,--status}'[Initial status]:status:('"$status_values"')' \\
|
|
321
|
+
'(-p --priority)'{-p,--priority}'[Priority level]:priority:('"$priority_values"')' \\
|
|
322
|
+
'(-d --due)'{-d,--due}'[Due date]:date:' \\
|
|
323
|
+
'--assign[Assign to owner]:owner:' \\
|
|
324
|
+
'*'{-l,--label}'[Add labels]:label:->labels' \\
|
|
325
|
+
'(-b --blocked-by)'{-b,--blocked-by}'[Blocked by task ID]:task:' \\
|
|
326
|
+
'(-n --note)'{-n,--note}'[Add a note]:note:' \\
|
|
327
|
+
'(-r --repeat)'{-r,--repeat}'[Recurrence rule]:rule:' \\
|
|
328
|
+
'--meta[Metadata key=value]:meta:' \\
|
|
329
|
+
'--json[Output as JSON]' \\
|
|
330
|
+
'--plaintext[Output as plain text]' \\
|
|
331
|
+
'1:title:'
|
|
332
|
+
;;
|
|
333
|
+
list)
|
|
334
|
+
_arguments \\
|
|
335
|
+
'(-s --status)'{-s,--status}'[Filter by status]:status:('"$status_values"')' \\
|
|
336
|
+
'(-p --priority)'{-p,--priority}'[Filter by priority]:priority:('"$priority_values"')' \\
|
|
337
|
+
'(-l --label)'{-l,--label}'[Filter by label]:label:->labels' \\
|
|
338
|
+
'--owner[Filter by owner]:owner:' \\
|
|
339
|
+
'--due[Filter by due date]:date:' \\
|
|
340
|
+
'--overdue[Show only overdue tasks]' \\
|
|
341
|
+
'--sort[Sort order]:sort:('"$sort_values"')' \\
|
|
342
|
+
'--search[Search by title]:query:' \\
|
|
343
|
+
'(-a --all)'{-a,--all}'[Include done tasks]' \\
|
|
344
|
+
'--actionable[Show only actionable tasks]' \\
|
|
345
|
+
'--limit[Maximum number of tasks to return]:number:' \\
|
|
346
|
+
'--offset[Number of tasks to skip]:number:' \\
|
|
347
|
+
'--filter[Apply a saved filter]:name:' \\
|
|
348
|
+
'--json[Output as JSON]' \\
|
|
349
|
+
'--plaintext[Output as plain text]'
|
|
350
|
+
;;
|
|
351
|
+
filter)
|
|
352
|
+
local -a filter_commands
|
|
353
|
+
filter_commands=(
|
|
354
|
+
'list:List all saved filters'
|
|
355
|
+
'show:Show a filter definition'
|
|
356
|
+
'add:Save a new named filter'
|
|
357
|
+
'remove:Delete a saved filter'
|
|
358
|
+
)
|
|
359
|
+
_describe -t commands 'filter command' filter_commands
|
|
360
|
+
;;
|
|
361
|
+
labels)
|
|
362
|
+
_arguments \\
|
|
363
|
+
'--json[Output as JSON]' \\
|
|
364
|
+
'--plaintext[Output as plain text]'
|
|
365
|
+
;;
|
|
366
|
+
get)
|
|
367
|
+
_arguments \\
|
|
368
|
+
'--json[Output as JSON]' \\
|
|
369
|
+
'--plaintext[Output as plain text]' \\
|
|
370
|
+
'1:task:->tasks'
|
|
371
|
+
;;
|
|
372
|
+
update)
|
|
373
|
+
_arguments \\
|
|
374
|
+
'(-t --title)'{-t,--title}'[New title]:title:' \\
|
|
375
|
+
'(-s --status)'{-s,--status}'[New status]:status:('"$status_values"')' \\
|
|
376
|
+
'(-p --priority)'{-p,--priority}'[New priority]:priority:('"$priority_values"')' \\
|
|
377
|
+
'(-d --due)'{-d,--due}'[Due date]:date:' \\
|
|
378
|
+
'--assign[Assign to owner]:owner:' \\
|
|
379
|
+
'*'{-l,--label}'[Add labels]:label:->labels' \\
|
|
380
|
+
'*--unlabel[Remove labels]:label:->labels' \\
|
|
381
|
+
'(-b --blocked-by)'{-b,--blocked-by}'[Blocked by task ID]:task:' \\
|
|
382
|
+
'*--unblock[Remove blocker task IDs]:task:' \\
|
|
383
|
+
'(-n --note)'{-n,--note}'[Append a note]:note:' \\
|
|
384
|
+
'--clear-notes[Remove all notes]' \\
|
|
385
|
+
'(-r --repeat)'{-r,--repeat}'[Recurrence rule]:rule:' \\
|
|
386
|
+
'--meta[Metadata key=value]:meta:' \\
|
|
387
|
+
'--json[Output as JSON]' \\
|
|
388
|
+
'--plaintext[Output as plain text]' \\
|
|
389
|
+
'1:task:->tasks'
|
|
390
|
+
;;
|
|
391
|
+
edit)
|
|
392
|
+
_arguments \\
|
|
393
|
+
'--json[Output as JSON]' \\
|
|
394
|
+
'--plaintext[Output as plain text]' \\
|
|
395
|
+
'1:task:->tasks'
|
|
396
|
+
;;
|
|
397
|
+
delete)
|
|
398
|
+
_arguments \\
|
|
399
|
+
'--json[Output as JSON]' \\
|
|
400
|
+
'--plaintext[Output as plain text]' \\
|
|
401
|
+
'*:task:->tasks'
|
|
402
|
+
;;
|
|
403
|
+
done)
|
|
404
|
+
_arguments \\
|
|
405
|
+
'--json[Output as JSON]' \\
|
|
406
|
+
'--plaintext[Output as plain text]' \\
|
|
407
|
+
'*:task:->tasks'
|
|
408
|
+
;;
|
|
409
|
+
start)
|
|
410
|
+
_arguments \\
|
|
411
|
+
'--json[Output as JSON]' \\
|
|
412
|
+
'--plaintext[Output as plain text]' \\
|
|
413
|
+
'*:task:->tasks'
|
|
414
|
+
;;
|
|
415
|
+
review)
|
|
416
|
+
_arguments \\
|
|
417
|
+
'--json[Output as JSON]' \\
|
|
418
|
+
'--plaintext[Output as plain text]' \\
|
|
419
|
+
'*:task:->tasks'
|
|
420
|
+
;;
|
|
421
|
+
context)
|
|
422
|
+
_arguments \\
|
|
423
|
+
'--owner[Scope to a specific owner]:owner:' \\
|
|
424
|
+
'(-l --label)'{-l,--label}'[Filter by labels]:label:' \\
|
|
425
|
+
'--json[Output as JSON]' \\
|
|
426
|
+
'--plaintext[Output as plain text]'
|
|
427
|
+
;;
|
|
428
|
+
log)
|
|
429
|
+
_arguments \\
|
|
430
|
+
'--json[Output as JSON]' \\
|
|
431
|
+
'--plaintext[Output as plain text]' \\
|
|
432
|
+
'1:task:->tasks'
|
|
433
|
+
;;
|
|
434
|
+
sync)
|
|
435
|
+
_arguments \\
|
|
436
|
+
'--json[Output as JSON]' \\
|
|
437
|
+
'--plaintext[Output as plain text]' \\
|
|
438
|
+
'1:remote path:_files'
|
|
439
|
+
;;
|
|
440
|
+
config)
|
|
441
|
+
local -a config_commands
|
|
442
|
+
config_commands=(
|
|
443
|
+
'init:Create a default config file'
|
|
444
|
+
'path:Print the config file path'
|
|
445
|
+
)
|
|
446
|
+
_describe -t commands 'config command' config_commands
|
|
447
|
+
;;
|
|
448
|
+
${""} completions)
|
|
449
|
+
_arguments \\
|
|
450
|
+
'--print[Print completion script to stdout]' \\
|
|
451
|
+
'1:shell:(bash zsh fish)'
|
|
452
|
+
;;
|
|
453
|
+
backup)
|
|
454
|
+
_arguments \\
|
|
455
|
+
'1:backup directory:_directories'
|
|
456
|
+
;;
|
|
457
|
+
self-update)
|
|
458
|
+
_arguments \\
|
|
459
|
+
'--check[Only check if an update is available]'
|
|
460
|
+
;;
|
|
461
|
+
telemetry)
|
|
462
|
+
local -a telemetry_commands
|
|
463
|
+
telemetry_commands=(
|
|
464
|
+
'status:Show whether telemetry is enabled or disabled'
|
|
465
|
+
'enable:Enable anonymous usage telemetry'
|
|
466
|
+
'disable:Disable anonymous usage telemetry'
|
|
467
|
+
)
|
|
468
|
+
_describe -t commands 'telemetry command' telemetry_commands
|
|
469
|
+
;;
|
|
470
|
+
esac
|
|
471
|
+
|
|
472
|
+
case $state in
|
|
473
|
+
tasks)
|
|
474
|
+
local -a task_ids
|
|
475
|
+
task_ids=(\${(f)"$(oru _complete tasks "$words[CURRENT]" 2>/dev/null)"})
|
|
476
|
+
if (( \${#task_ids} )); then
|
|
477
|
+
local -a descriptions
|
|
478
|
+
for entry in $task_ids; do
|
|
479
|
+
local id=\${entry%%$'\\t'*}
|
|
480
|
+
local title=\${entry#*$'\\t'}
|
|
481
|
+
descriptions+=("\${id}:\${title}")
|
|
482
|
+
done
|
|
483
|
+
_describe -t tasks 'task' descriptions
|
|
484
|
+
fi
|
|
485
|
+
;;
|
|
486
|
+
labels)
|
|
487
|
+
local -a label_list
|
|
488
|
+
label_list=(\${(f)"$(oru _complete labels "$words[CURRENT]" 2>/dev/null)"})
|
|
489
|
+
compadd -a label_list
|
|
490
|
+
;;
|
|
491
|
+
esac
|
|
492
|
+
;;
|
|
493
|
+
esac
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_oru "$@"
|
|
497
|
+
`}le();function he(){return`# oru shell completions for fish
|
|
498
|
+
# Install: oru completions fish
|
|
499
|
+
# Print: oru completions fish --print
|
|
500
|
+
|
|
501
|
+
function __oru_task_ids
|
|
502
|
+
oru _complete tasks (commandline -ct) 2>/dev/null | while read -l line
|
|
503
|
+
set -l parts (string split \\t $line)
|
|
504
|
+
echo $parts[1]\\t$parts[2]
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
function __oru_labels
|
|
509
|
+
oru _complete labels (commandline -ct) 2>/dev/null
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
function __oru_needs_command
|
|
513
|
+
set -l cmd (commandline -opc)
|
|
514
|
+
test (count $cmd) -eq 1
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
function __oru_using_command
|
|
518
|
+
set -l cmd (commandline -opc)
|
|
519
|
+
test (count $cmd) -ge 2; and test "$cmd[2]" = "$argv[1]"
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
function __oru_using_subcommand
|
|
523
|
+
set -l cmd (commandline -opc)
|
|
524
|
+
test (count $cmd) -ge 3; and test "$cmd[2]" = "$argv[1]"; and test "$cmd[3]" = "$argv[2]"
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Disable file completions by default
|
|
528
|
+
complete -c oru -f
|
|
529
|
+
|
|
530
|
+
# Top-level commands
|
|
531
|
+
complete -c oru -n __oru_needs_command -a add -d 'Add a new task'
|
|
532
|
+
complete -c oru -n __oru_needs_command -a list -d 'List tasks'
|
|
533
|
+
complete -c oru -n __oru_needs_command -a labels -d 'List all labels in use'
|
|
534
|
+
complete -c oru -n __oru_needs_command -a get -d 'Get a task by ID'
|
|
535
|
+
complete -c oru -n __oru_needs_command -a update -d 'Update a task'
|
|
536
|
+
complete -c oru -n __oru_needs_command -a edit -d 'Open task in \\$EDITOR for complex edits'
|
|
537
|
+
complete -c oru -n __oru_needs_command -a delete -d 'Delete one or more tasks'
|
|
538
|
+
complete -c oru -n __oru_needs_command -a done -d 'Mark one or more tasks as done'
|
|
539
|
+
complete -c oru -n __oru_needs_command -a start -d 'Start one or more tasks'
|
|
540
|
+
complete -c oru -n __oru_needs_command -a review -d 'Mark one or more tasks as in_review'
|
|
541
|
+
complete -c oru -n __oru_needs_command -a context -d 'Show a summary of what needs your attention'
|
|
542
|
+
complete -c oru -n __oru_needs_command -a log -d 'Show change history of a task'
|
|
543
|
+
complete -c oru -n __oru_needs_command -a sync -d 'Sync with a filesystem remote'
|
|
544
|
+
complete -c oru -n __oru_needs_command -a backup -d 'Create a database backup snapshot'
|
|
545
|
+
complete -c oru -n __oru_needs_command -a config -d 'Manage configuration'
|
|
546
|
+
complete -c oru -n __oru_needs_command -a filter -d 'Manage saved list filters'
|
|
547
|
+
${""}
|
|
548
|
+
complete -c oru -n __oru_needs_command -a completions -d 'Generate shell completions'
|
|
549
|
+
complete -c oru -n __oru_needs_command -a self-update -d 'Update oru to the latest version'
|
|
550
|
+
complete -c oru -n __oru_needs_command -a telemetry -d 'Manage anonymous usage telemetry'
|
|
551
|
+
|
|
552
|
+
# config subcommands
|
|
553
|
+
complete -c oru -n '__oru_using_command config' -a init -d 'Create a default config file'
|
|
554
|
+
complete -c oru -n '__oru_using_command config' -a path -d 'Print the config file path'
|
|
555
|
+
|
|
556
|
+
# filter subcommands
|
|
557
|
+
complete -c oru -n '__oru_using_command filter' -a list -d 'List all saved filters'
|
|
558
|
+
complete -c oru -n '__oru_using_command filter' -a show -d 'Show a filter definition'
|
|
559
|
+
complete -c oru -n '__oru_using_command filter' -a add -d 'Save a new named filter'
|
|
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 '${D.join(" ")}' -d 'Status' -r
|
|
562
|
+
complete -c oru -n '__oru_using_subcommand filter add' -s p -l priority -a '${N.join(" ")}' -d 'Priority' -r
|
|
563
|
+
complete -c oru -n '__oru_using_subcommand filter add' -s l -l label -a '(__oru_labels)' -d 'Label' -r
|
|
564
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l owner -d 'Filter by owner' -r
|
|
565
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l due -d 'Filter by due date' -r
|
|
566
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l overdue -d 'Show only overdue tasks'
|
|
567
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l sort -a '${B.join(" ")}' -d 'Sort order' -r
|
|
568
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l search -d 'Search by title' -r
|
|
569
|
+
complete -c oru -n '__oru_using_subcommand filter add' -s a -l all -d 'Include done tasks'
|
|
570
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l actionable -d 'Show only actionable tasks'
|
|
571
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l limit -d 'Maximum number of tasks' -r
|
|
572
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l offset -d 'Number of tasks to skip' -r
|
|
573
|
+
complete -c oru -n '__oru_using_subcommand filter add' -l sql -d 'Raw SQL WHERE condition' -r
|
|
574
|
+
|
|
575
|
+
${""}# completions subcommands
|
|
576
|
+
complete -c oru -n '__oru_using_command completions' -a 'bash zsh fish'
|
|
577
|
+
complete -c oru -n '__oru_using_command completions' -l print -d 'Print completion script to stdout'
|
|
578
|
+
|
|
579
|
+
# telemetry subcommands
|
|
580
|
+
complete -c oru -n '__oru_using_command telemetry' -a status -d 'Show telemetry status'
|
|
581
|
+
complete -c oru -n '__oru_using_command telemetry' -a enable -d 'Enable telemetry'
|
|
582
|
+
complete -c oru -n '__oru_using_command telemetry' -a disable -d 'Disable telemetry'
|
|
583
|
+
|
|
584
|
+
# Task ID completions for get, update, edit, delete, done, start
|
|
585
|
+
complete -c oru -n '__oru_using_command get' -a '(__oru_task_ids)'
|
|
586
|
+
complete -c oru -n '__oru_using_command update' -a '(__oru_task_ids)'
|
|
587
|
+
complete -c oru -n '__oru_using_command edit' -a '(__oru_task_ids)'
|
|
588
|
+
complete -c oru -n '__oru_using_command delete' -a '(__oru_task_ids)'
|
|
589
|
+
complete -c oru -n '__oru_using_command done' -a '(__oru_task_ids)'
|
|
590
|
+
complete -c oru -n '__oru_using_command start' -a '(__oru_task_ids)'
|
|
591
|
+
complete -c oru -n '__oru_using_command review' -a '(__oru_task_ids)'
|
|
592
|
+
complete -c oru -n '__oru_using_command log' -a '(__oru_task_ids)'
|
|
593
|
+
|
|
594
|
+
# backup gets directory completions
|
|
595
|
+
complete -c oru -n '__oru_using_command backup' -a '(__fish_complete_directories)'
|
|
596
|
+
|
|
597
|
+
# sync gets file completions
|
|
598
|
+
complete -c oru -n '__oru_using_command sync' -F
|
|
599
|
+
|
|
600
|
+
# Status flag for add, list, update, edit
|
|
601
|
+
complete -c oru -n '__oru_using_command add' -s s -l status -a '${D.join(" ")}' -d 'Status' -r
|
|
602
|
+
complete -c oru -n '__oru_using_command list' -s s -l status -a '${D.join(" ")}' -d 'Status' -r
|
|
603
|
+
complete -c oru -n '__oru_using_command update' -s s -l status -a '${D.join(" ")}' -d 'Status' -r
|
|
604
|
+
|
|
605
|
+
# Priority flag for add, list, update, edit
|
|
606
|
+
complete -c oru -n '__oru_using_command add' -s p -l priority -a '${N.join(" ")}' -d 'Priority' -r
|
|
607
|
+
complete -c oru -n '__oru_using_command list' -s p -l priority -a '${N.join(" ")}' -d 'Priority' -r
|
|
608
|
+
complete -c oru -n '__oru_using_command update' -s p -l priority -a '${N.join(" ")}' -d 'Priority' -r
|
|
609
|
+
|
|
610
|
+
# Label flag
|
|
611
|
+
complete -c oru -n '__oru_using_command add' -s l -l label -a '(__oru_labels)' -d 'Label' -r
|
|
612
|
+
complete -c oru -n '__oru_using_command list' -s l -l label -a '(__oru_labels)' -d 'Label' -r
|
|
613
|
+
complete -c oru -n '__oru_using_command update' -s l -l label -a '(__oru_labels)' -d 'Label' -r
|
|
614
|
+
complete -c oru -n '__oru_using_command update' -l unlabel -a '(__oru_labels)' -d 'Remove label' -r
|
|
615
|
+
|
|
616
|
+
# Common flags
|
|
617
|
+
complete -c oru -n '__oru_using_command labels' -l json -d 'Output as JSON'
|
|
618
|
+
complete -c oru -n '__oru_using_command labels' -l plaintext -d 'Output as plain text'
|
|
619
|
+
complete -c oru -n '__oru_using_command add' -l json -d 'Output as JSON'
|
|
620
|
+
complete -c oru -n '__oru_using_command add' -l plaintext -d 'Output as plain text'
|
|
621
|
+
complete -c oru -n '__oru_using_command add' -s d -l due -d 'Due date' -r
|
|
622
|
+
complete -c oru -n '__oru_using_command add' -s n -l note -d 'Add a note' -r
|
|
623
|
+
complete -c oru -n '__oru_using_command add' -l id -d 'Task ID' -r
|
|
624
|
+
complete -c oru -n '__oru_using_command add' -l assign -d 'Assign to owner' -r
|
|
625
|
+
complete -c oru -n '__oru_using_command add' -s b -l blocked-by -d 'Blocked by task ID' -r
|
|
626
|
+
complete -c oru -n '__oru_using_command add' -l meta -d 'Metadata key=value' -r
|
|
627
|
+
complete -c oru -n '__oru_using_command add' -s r -l repeat -d 'Recurrence rule' -r
|
|
628
|
+
complete -c oru -n '__oru_using_command list' -l json -d 'Output as JSON'
|
|
629
|
+
complete -c oru -n '__oru_using_command list' -l plaintext -d 'Output as plain text'
|
|
630
|
+
complete -c oru -n '__oru_using_command list' -l search -d 'Search by title' -r
|
|
631
|
+
complete -c oru -n '__oru_using_command list' -l owner -d 'Filter by owner' -r
|
|
632
|
+
complete -c oru -n '__oru_using_command list' -l due -d 'Filter by due date' -r
|
|
633
|
+
complete -c oru -n '__oru_using_command list' -l overdue -d 'Show only overdue tasks'
|
|
634
|
+
complete -c oru -n '__oru_using_command list' -l sort -a '${B.join(" ")}' -d 'Sort order' -r
|
|
635
|
+
complete -c oru -n '__oru_using_command list' -s a -l all -d 'Include done tasks'
|
|
636
|
+
complete -c oru -n '__oru_using_command list' -l actionable -d 'Show only actionable tasks'
|
|
637
|
+
complete -c oru -n '__oru_using_command list' -l limit -d 'Maximum number of tasks' -r
|
|
638
|
+
complete -c oru -n '__oru_using_command list' -l offset -d 'Number of tasks to skip' -r
|
|
639
|
+
complete -c oru -n '__oru_using_command list' -l filter -d 'Apply a saved filter' -r
|
|
640
|
+
complete -c oru -n '__oru_using_command get' -l json -d 'Output as JSON'
|
|
641
|
+
complete -c oru -n '__oru_using_command get' -l plaintext -d 'Output as plain text'
|
|
642
|
+
complete -c oru -n '__oru_using_command update' -l json -d 'Output as JSON'
|
|
643
|
+
complete -c oru -n '__oru_using_command update' -l plaintext -d 'Output as plain text'
|
|
644
|
+
complete -c oru -n '__oru_using_command update' -s t -l title -d 'New title' -r
|
|
645
|
+
complete -c oru -n '__oru_using_command update' -s d -l due -d 'Due date' -r
|
|
646
|
+
complete -c oru -n '__oru_using_command update' -s n -l note -d 'Append a note' -r
|
|
647
|
+
complete -c oru -n '__oru_using_command update' -l assign -d 'Assign to owner' -r
|
|
648
|
+
complete -c oru -n '__oru_using_command update' -l clear-notes -d 'Remove all notes'
|
|
649
|
+
complete -c oru -n '__oru_using_command update' -s b -l blocked-by -d 'Blocked by task ID' -r
|
|
650
|
+
complete -c oru -n '__oru_using_command update' -l unblock -d 'Remove blocker task ID' -r
|
|
651
|
+
complete -c oru -n '__oru_using_command update' -l meta -d 'Metadata key=value' -r
|
|
652
|
+
complete -c oru -n '__oru_using_command update' -s r -l repeat -d 'Recurrence rule' -r
|
|
653
|
+
complete -c oru -n '__oru_using_command edit' -l json -d 'Output as JSON'
|
|
654
|
+
complete -c oru -n '__oru_using_command edit' -l plaintext -d 'Output as plain text'
|
|
655
|
+
complete -c oru -n '__oru_using_command delete' -l json -d 'Output as JSON'
|
|
656
|
+
complete -c oru -n '__oru_using_command delete' -l plaintext -d 'Output as plain text'
|
|
657
|
+
complete -c oru -n '__oru_using_command done' -l json -d 'Output as JSON'
|
|
658
|
+
complete -c oru -n '__oru_using_command done' -l plaintext -d 'Output as plain text'
|
|
659
|
+
complete -c oru -n '__oru_using_command start' -l json -d 'Output as JSON'
|
|
660
|
+
complete -c oru -n '__oru_using_command start' -l plaintext -d 'Output as plain text'
|
|
661
|
+
complete -c oru -n '__oru_using_command review' -l json -d 'Output as JSON'
|
|
662
|
+
complete -c oru -n '__oru_using_command review' -l plaintext -d 'Output as plain text'
|
|
663
|
+
complete -c oru -n '__oru_using_command context' -l owner -d 'Scope to a specific owner' -r
|
|
664
|
+
complete -c oru -n '__oru_using_command context' -s l -l label -d 'Filter by labels' -r
|
|
665
|
+
complete -c oru -n '__oru_using_command context' -l json -d 'Output as JSON'
|
|
666
|
+
complete -c oru -n '__oru_using_command context' -l plaintext -d 'Output as plain text'
|
|
667
|
+
complete -c oru -n '__oru_using_command log' -l json -d 'Output as JSON'
|
|
668
|
+
complete -c oru -n '__oru_using_command log' -l plaintext -d 'Output as plain text'
|
|
669
|
+
complete -c oru -n '__oru_using_command sync' -l json -d 'Output as JSON'
|
|
670
|
+
complete -c oru -n '__oru_using_command sync' -l plaintext -d 'Output as plain text'
|
|
671
|
+
|
|
672
|
+
# self-update flags
|
|
673
|
+
complete -c oru -n '__oru_using_command self-update' -l check -d 'Only check if an update is available'
|
|
674
|
+
`}import je from"fs";import K from"path";import Fr from"os";import jr from"readline";function ft(){let t=process.env.SHELL;if(!t)return null;let e=K.basename(t);return e==="bash"||e==="zsh"||e==="fish"?e:null}function kn(t,e=Fr.homedir()){switch(t){case"bash":return{scriptPath:K.join(e,".oru","completions.bash"),rcPath:K.join(e,".bashrc")};case"zsh":return{scriptPath:K.join(e,".oru","completions.zsh"),rcPath:K.join(e,".zshrc")};case"fish":return{scriptPath:K.join(e,".config","fish","completions","oru.fish"),rcPath:null}}}function Ur(t){switch(t){case"bash":return ye();case"zsh":return be();case"fish":return he()}}function Ue(t,e,n){let{scriptPath:o,rcPath:r}=kn(t,n);je.mkdirSync(K.dirname(o),{recursive:!0}),je.writeFileSync(o,Ur(t)),e(`Wrote completions to ${o}`);let s=!1;if(r){let a=`source ${o}`,u=`source ~/.oru/completions.${t}`,p="";try{p=je.readFileSync(r,"utf-8")}catch{}if(!p.includes(a)&&!p.includes(u)){let m=p.length>0&&!p.endsWith(`
|
|
675
|
+
`)?`
|
|
676
|
+
`:"";je.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 gt(t,e=process.stdin,n=process.stdout){return new Promise(o=>{let r=jr.createInterface({input:e,output:n});r.question(t,s=>{r.close();let a=s.trim().toLowerCase();o(a===""||a==="y"||a==="yes")})})}function Be(t){return t.rcPath?`
|
|
678
|
+
Restart your shell or run: source ~/${K.basename(t.rcPath)}`:`
|
|
679
|
+
Completions will be loaded automatically on next shell start.`}import Ye from"fs";import _t from"path";import Br from"os";import{parse as Yr}from"smol-toml";var yt=["status","priority","owner","label","search","sort","actionable","due","overdue","all","limit","offset"];function Sn(){return process.env.ORU_CONFIG_DIR?_t.join(process.env.ORU_CONFIG_DIR,"filters.toml"):_t.join(Br.homedir(),".oru","filters.toml")}function ce(t){let e=t??Sn();if(!Ye.existsSync(e))return{};let n=Ye.readFileSync(e,"utf-8");try{return Yr(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 Wr(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 Jr(t){return`["${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"]`}function Hr(t){let e=[];for(let[n,o]of Object.entries(t)){let r=[Jr(n)];for(let[s,a]of Object.entries(o))a!==void 0&&r.push(`${s} = ${Wr(a)}`);e.push(r.join(`
|
|
681
|
+
`))}return e.join(`
|
|
682
|
+
|
|
683
|
+
`)+(e.length>0?`
|
|
684
|
+
`:"")}function bt(t,e){let n=e??Sn();Ye.mkdirSync(_t.dirname(n),{recursive:!0}),Ye.writeFileSync(n,Hr(t))}function vn(t,e){let n={...t};for(let o of yt)t[o]===void 0&&e[o]!==void 0&&(n[o]=e[o]);return n}We();vt();ke();function jn(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 Un(t){return t.replace(/\b(update -s \w+)\b/g,(e,n)=>de(n))}function yo(t){let e="";return t.split(`
|
|
685
|
+
`).map(n=>{if(/^(Options|Commands|Arguments):$/.test(n))return e=n.slice(0,-1).toLowerCase(),L(n);if(n.startsWith("Usage: "))return e="",`${L("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+L(s)+a+u:r+de(s)+a+w(Un(u))}return e==="commands"&&n.match(/^\s{4,}\S/)?w(Un(n)):n}).join(`
|
|
686
|
+
`)}var bo={formatHelp(t,e){let n=_o.prototype.formatHelp.call(e,t,e);return yo(n)}};function Bn(t){t.configureHelp(bo);for(let e of t.commands)Bn(e)}function ho(t,e=r=>process.stdout.write(`${r}
|
|
687
|
+
`),n,o=r=>process.stderr.write(r)){let r=n??ct(),s=Ht(t),a=on(t),u=new Ae(s,a),p=new go("oru").description(`${L("oru")} - personal task manager that your agents can operate for you
|
|
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(`${W} (${Tn})`);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 $(c,i){let l=pn(c);return l.valid?!0:(_(i,l.message),!1)}function E(c,i){let l=fn(c);return l.valid?!0:(_(i,l.message),!1)}function S(c,i){if(c.length>100)return _(i,`labels exceeds maximum of ${100} items.`),!1;let l=gn(c);return l.valid?!0:(_(i,l.message),!1)}async function O(c,i,l,f){try{let y=await l();if(!y){b(i,c);return}f(y)}catch(y){if(y instanceof Q){d(y,i);return}throw y}}function x(c,i){return c.length>100?(_(i,`blocked_by exceeds maximum of ${100} items.`),!1):!0}function P(c,i){if(Object.keys(c).length>50)return _(i,`Metadata exceeds maximum of ${50} keys.`),!1;for(let l of Object.keys(c))if(l.length>100)return _(i,`Metadata key exceeds maximum length of ${100} characters.`),!1;for(let l of Object.values(c))if(typeof l=="string"&&l.length>5e3)return _(i,`Metadata value exceeds maximum length of ${5e3} characters.`),!1;return!0}p.command("add <title>").description("Add a new task").option("--id <id>","Task ID (for idempotent creates)").addOption(new z("-s, --status <status>","Initial status").choices(D).default("todo")).addOption(new z("-p, --priority <priority>","Priority level").choices(N).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
|
+
Examples:
|
|
691
|
+
$ oru add "Fix login bug"
|
|
692
|
+
$ oru add "Fix login bug" -p high -d friday
|
|
693
|
+
$ oru add "Write docs" -l docs -n "Include API section"
|
|
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=Ce(c);let l=m(i);if(!$(c,l)||i.note&&!E(i.note,l)||i.label&&!S(i.label,l)||i.blockedBy&&!x(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&&!At(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?V(T):Y(T));return}}let f;if(i.due){let T=dt(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=Fe(i.repeat)}catch(T){_(l,T instanceof Error?T.message:String(T));return}let v=i.meta?jn(i.meta):void 0;if(v&&!P(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?V(h):Y(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(!D.includes(l))throw new Error(`Invalid status: ${l}. Allowed: ${D.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(!N.includes(l))throw new Error(`Invalid priority: ${l}. Allowed: ${N.join(", ")}`);return i.length===1?i[0]:i}).option("-l, --label <label>","Filter by label").option("--owner <owner>","Filter by owner").addOption(new z("--due <range>","Filter by due date").choices(["today","this-week"])).option("--overdue","Show only overdue tasks").addOption(new z("--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
|
+
Examples:
|
|
697
|
+
$ oru list
|
|
698
|
+
$ oru list -s in_progress -p high
|
|
699
|
+
$ oru list -l backend --sort due --actionable
|
|
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=ce()[c.filter];if(!k){_(i,`Filter '${c.filter}' not found. Run 'oru filter list' to see available filters.`);return}l={...c,...vn(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=Ct(y,l),l.due&&(y=et(y,l.due,void 0,r.first_day_of_week)),l.overdue&&(y=et(y,"overdue")),e(i?Qt(y):Ze(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?Zt(i):Bt(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
|
+
Examples:
|
|
703
|
+
$ oru get 019414a3
|
|
704
|
+
$ oru get 019414a3 --json`).action(async(c,i)=>{let l=m(i);await O(c,l,()=>u.get(c),f=>{e(l?V(f):Y(f))})}),p.command("update <id>").description("Update a task").option("-t, --title <title>","New title").addOption(new z("-s, --status <status>","New status").choices(D)).addOption(new z("-p, --priority <priority>","New priority").choices(N)).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
|
+
Examples:
|
|
706
|
+
$ oru update 019414a3 -s in_progress
|
|
707
|
+
$ oru update 019414a3 -l urgent -d tomorrow
|
|
708
|
+
$ oru update 019414a3 -n "Blocked on API review"
|
|
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=Ce(i.title)),!(i.title!==void 0&&!$(i.title,l))&&!(i.note&&!E(i.note,l))&&!(i.label&&!S(i.label,l))&&!(i.blockedBy&&!x(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=dt(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))),!x(k,l))return;f.blocked_by=k}if(i.repeat!==void 0)if(i.repeat.toLowerCase()==="none")f.recurrence=null;else try{f.recurrence=Fe(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=jn(i.meta),T={...k.metadata};for(let[te,X]of Object.entries(h))X===null?delete T[te]:T[te]=X;if(!P(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?V(v):Y(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:")} ${se(v.recurrence)}`),e(Y(k))))}}catch(f){if(f instanceof Q){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
|
+
Examples:
|
|
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=_n(f),{edited:v,tmpFile:k}=await bn(y),h,T,te;try{({fields:h,newNotes:T,removedNotes:te}=yn(v,f))}catch(U){let Nt=U instanceof Error?U.message:String(U);_(l,Nt),o(`Your edits are saved at: ${k}
|
|
715
|
+
`);return}hn(k);let X=Object.keys(h).length>0,Xe=T.length>0;if(!X&&!Xe&&!te){e(l?V(f):"No changes.");return}if(h.title!==void 0&&(h.title=Ce(h.title)),h.title!==void 0&&!$(h.title,l))return;for(let U of T)if(!E(U,l))return;if(h.labels&&!S(h.labels,l)||h.blocked_by&&!x(h.blocked_by,l))return;if(h.blocked_by){let U=await u.validateBlockedBy(f.id,h.blocked_by);if(!U.valid){_(l,U.error);return}}if(h.metadata&&!P(h.metadata,l))return;let j;if(te){let Rt=[...v.slice(v.indexOf("+++",3)+3).split(`
|
|
716
|
+
`).filter(qe=>qe.startsWith("- ")).map(qe=>qe.slice(2)),...T];Rt.length===0?j=await u.clearNotes(c):j=await u.replaceNotes(c,Rt),X&&(j=await u.update(c,h))}else if(Xe&&T.length===1&&X)j=await u.updateWithNote(c,h,T[0]);else if(Xe&&T.length===1&&!X)j=await u.addNote(c,T[0]);else{X&&(j=await u.update(c,h));for(let U of T)j=await u.addNote(c,U)}j||(j=await u.get(c)),e(l?V(j):Y(j))}catch(f){if(f instanceof Q){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 O(h,k,()=>u.update(h,{status:i}),T=>{e(k?V(T):Y(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
|
+
Examples:
|
|
718
|
+
$ oru done 019414a3
|
|
719
|
+
$ oru done 019414a3 019414b7`).action(async(c,i)=>{let l=m(i);for(let f of c)await O(f,l,()=>u.update(f,{status:"done"}),async y=>{if(e(l?V(y):Y(y)),y.recurrence){let v=await u.getSpawnedTask(y.id);v&&(l?e(JSON.stringify({spawned:v},null,2)):(e(`
|
|
720
|
+
${w("Next occurrence:")} ${se(y.recurrence)}`),e(Y(v))))}})}),g("start","in_progress","Start one or more tasks (shortcut for update -s in_progress)",`
|
|
721
|
+
Examples:
|
|
722
|
+
$ oru start 019414a3
|
|
723
|
+
$ oru start 019414a3 019414b7`),g("review","in_review","Mark one or more tasks as in_review (shortcut for update -s in_review)",`
|
|
724
|
+
Examples:
|
|
725
|
+
$ oru review 019414a3
|
|
726
|
+
$ oru review 019414a3 019414b7`),p.command("context").description("Show a summary of what needs your attention (overdue, due soon, in progress, actionable, blocked, recently completed)").option("--owner <owner>","Scope to a specific owner").option("-l, --label <label>","Filter by label").option("--json","Output as JSON").option("--plaintext","Output as plain text (overrides config)").addHelpText("after",`
|
|
727
|
+
Examples:
|
|
728
|
+
$ oru context
|
|
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?zt(i):Wt(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
|
+
Examples:
|
|
732
|
+
$ oru delete 019414a3
|
|
733
|
+
$ oru delete 019414a3 019414b7`).action(async(c,i)=>{let l=m(i);for(let f of c)await O(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
|
+
Examples:
|
|
735
|
+
$ oru log 019414a3
|
|
736
|
+
$ oru log 019414a3 --json`).action(async(c,i)=>{let l=m(i);await O(c,l,()=>u.log(c),f=>{e(l?en(f):Yt(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
|
+
Examples:
|
|
738
|
+
$ oru sync /mnt/shared/oru
|
|
739
|
+
$ oru sync ~/Dropbox/oru-sync`).action(async(c,i)=>{let l=new Me(c);try{let f=await rn(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 C=p.command("config").description("Manage configuration");C.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=fe();if($t.existsSync(l)){e(i?JSON.stringify({message:"Config file already exists.",path:l}):`Config file already exists at ${l}`);return}$t.mkdirSync(we.dirname(l),{recursive:!0}),$t.writeFileSync(l,sn),e(i?JSON.stringify({message:"Config file created.",path:l}):`Created ${l}`)}),C.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=fe();e(i?JSON.stringify({path:l}):l)});let J=p.command("filter").description("Manage saved list filters");J.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=ce(),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)}),J.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=ce()[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
|
+
`))}),J.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=ce();if(!(c in f)){e(l?JSON.stringify({error:"not_found",name:c}):`Filter '${c}' not found.`),process.exitCode=1;return}delete f[c],bt(f),e(l?JSON.stringify({message:`Removed filter '${c}'.`,name:c}):`Removed filter '${c}'.`)}),J.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(!D.includes(l))throw new Error(`Invalid status: ${l}. Allowed: ${D.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(!N.includes(l))throw new Error(`Invalid priority: ${l}. Allowed: ${N.join(", ")}`);return i.length===1?i[0]:i}).option("-l, --label <label>","Filter by label").option("--owner <owner>","Filter by owner").addOption(new z("--due <range>","Filter by due date").choices(["today","this-week"])).option("--overdue","Show only overdue tasks").addOption(new z("--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
|
+
Examples:
|
|
743
|
+
$ oru filter add mine --owner tchayen --status todo
|
|
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 yt)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=ce(),v=c in y;y[c]=f,bt(y);let k=v?`Updated filter '${c}'.`:`Saved filter '${c}'.`;e(l?JSON.stringify({message:k,name:c,filter:f}):k)}),!1;let F=p.command("completions").description("Generate shell completion scripts").action(async()=>{let c=ft();if(!c){e(`Could not detect shell from $SHELL.
|
|
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 gt(`Install completions for ${c}? [Y/n] `)){e("Aborted.");return}let l=Ue(c,e);e(Be(l))});for(let c of["bash","zsh","fish"])F.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"?ye():c==="zsh"?be():he());return}let l=Ue(c,e);e(Be(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(()=>(In(),Rn));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=kt(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);ge("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);ge("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=Je(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=we.dirname(Dt(import.meta.url)),i=we.join(c,"mcp","index.js"),l=Fn(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 pt(u,c,i??"");l.length>0&&e(l.join(`
|
|
749
|
+
`))}),Bn(p),p}async function ko(){let t=Date.now(),e=Xt();Kt(e);let n=ct(),o=ho(e,void 0,n);if(n.backup_path){let{autoBackup:a}=await Promise.resolve().then(()=>(vt(),xn));a(e,n.backup_path,n.backup_interval)}try{let{showFirstRunNotice:a}=await Promise.resolve().then(()=>(We(),St));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(()=>(Ve(),Ot));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(()=>(We(),St)),{command:m,flags:d}=a(process.argv);if(ht(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(()=>(Ve(),Ot));u(a)}}catch(a){process.env.ORU_DEBUG==="1"&&console.error("Update check failed:",a)}}var So=Dt(import.meta.url),vo=process.argv[1]&&So===process.argv[1];vo&&ko().catch(t=>{process.env.ORU_DEBUG==="1"?console.error(t):console.error(t instanceof Error?t.message:String(t)),process.exit(1)});export{ho as createProgram};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{StdioServerTransport as Ie}from"@modelcontextprotocol/sdk/server/stdio.js";import se from"better-sqlite3";import W from"path";import ie from"os";import N from"fs";function ae(){return process.env.ORU_DB_PATH?process.env.ORU_DB_PATH:W.join(ie.homedir(),".oru","oru.db")}function j(r){let t=r??ae(),e=W.dirname(t);N.existsSync(e)||N.mkdirSync(e,{recursive:!0,mode:448});let s=new se(t);s.pragma("journal_mode = WAL"),s.pragma("foreign_keys = ON");try{N.chmodSync(t,384)}catch{}return s}function oe(r){let t=r.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();return t?parseInt(t.value,10):0}function J(r,t){let e=oe(r),s=t.filter(a=>a.version>e).sort((a,l)=>a.version-l.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 a of s)a.up(r),r.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(String(a.version));return s.length})()}function X(r){r.exec(`
|
|
4
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
title TEXT NOT NULL,
|
|
7
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
8
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
9
|
+
labels TEXT NOT NULL DEFAULT '[]',
|
|
10
|
+
notes TEXT NOT NULL DEFAULT '[]',
|
|
11
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
deleted_at TEXT
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS oplog (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
task_id TEXT NOT NULL,
|
|
20
|
+
device_id TEXT NOT NULL,
|
|
21
|
+
op_type TEXT NOT NULL,
|
|
22
|
+
field TEXT,
|
|
23
|
+
value TEXT,
|
|
24
|
+
timestamp TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
28
|
+
key TEXT PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '1');
|
|
33
|
+
`),J(r,le)}var le=[{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 ce,SqliteDialect as ue}from"kysely";function q(r){return new ce({dialect:new ue({database:r})})}import{randomBytes as de}from"crypto";var K="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",Je=new Set(K),pe=11;function R(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(K[Number(e%62n)]),e/=62n;return s.reverse().join("")}function T(){return R(de(8),pe)}function H(r){let t=r.prepare("SELECT value FROM meta WHERE key = 'device_id'").get();if(t)return t.value;let e=T();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 Ee}from"kysely";import{sql as f}from"kysely";var h=["todo","in_progress","in_review","done"],ge=new Set(h),_=["low","medium","high","urgent"],fe=new Set(_);var V="todo",Q="medium";var L=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 $(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 A(r,t,e){let s=t.id??T(),n=e??new Date().toISOString(),i={id:s,title:t.title,status:t.status??V,priority:t.priority??Q,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:i.id,title:i.title,status:i.status,priority:i.priority,owner:i.owner,due_at:i.due_at,recurrence:i.recurrence,blocked_by:JSON.stringify(i.blocked_by),labels:JSON.stringify(i.labels),notes:JSON.stringify(i.notes),metadata:JSON.stringify(i.metadata),created_at:i.created_at,updated_at:i.updated_at,deleted_at:i.deleted_at}).execute(),i}async function S(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 i=t.label;e=e.where(f`EXISTS (SELECT 1 FROM json_each(labels) WHERE json_each.value = ${i})`)}if(t?.search){let i=t.search.replace(/[\\%_]/g,"\\$&");e=e.where(f`title LIKE '%' || ${i} || '%' ESCAPE '\\' COLLATE NOCASE`)}switch(t?.actionable&&(e=e.where("status","!=","done").where(f`NOT EXISTS (
|
|
34
|
+
SELECT 1 FROM json_each(tasks.blocked_by) AS dep
|
|
35
|
+
JOIN tasks AS blocker ON blocker.id = dep.value
|
|
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);
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tchayen/oru",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "oru - agent-friendly todo CLI with offline sync",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"agentic",
|
|
8
|
+
"cli",
|
|
9
|
+
"mcp",
|
|
10
|
+
"offline-first",
|
|
11
|
+
"tasks",
|
|
12
|
+
"todo"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://oru.sh",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Tomasz Czajęcki",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/tchayen/oru"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"oru": "./dist/cli.js",
|
|
23
|
+
"oru-mcp": "./dist/mcp/index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
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
|
+
"dependencies": {
|
|
43
|
+
"@hono/node-server": "^1.19.9",
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
45
|
+
"better-sqlite3": "^12.6.2",
|
|
46
|
+
"cloudflared": "^0.7.1",
|
|
47
|
+
"commander": "^14.0.3",
|
|
48
|
+
"hono": "^4.11.10",
|
|
49
|
+
"kysely": "^0.28.11",
|
|
50
|
+
"qrcode": "^1.5.4",
|
|
51
|
+
"smol-toml": "^1.6.0",
|
|
52
|
+
"zod": "^4.3.6"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@oru/types": "workspace:*",
|
|
56
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
57
|
+
"@types/node": "^22.13.0",
|
|
58
|
+
"@types/qrcode": "^1.5.6",
|
|
59
|
+
"@typescript/native-preview": "^7.0.0-dev.20260218.1",
|
|
60
|
+
"tsup": "^8.5.1",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22"
|
|
67
|
+
}
|
|
68
|
+
}
|