clawdo 1.0.1 → 1.1.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/README.md CHANGED
@@ -2,306 +2,220 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/clawdo)](https://www.npmjs.com/package/clawdo)
4
4
  [![CI](https://github.com/LePetitPince/clawdo/actions/workflows/ci.yml/badge.svg)](https://github.com/LePetitPince/clawdo/actions/workflows/ci.yml)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
- [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
7
- [![ClawHub](https://img.shields.io/badge/ClawHub-clawdo-blueviolet)](https://clawhub.com)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+ [![ClawHub](https://img.shields.io/badge/ClawHub-skill-blue)](https://clawhub.com)
8
8
 
9
- **Personal task queue with autonomous AI execution** claw + to-do
9
+ A task queue for one human and one AI agent. Not a project manager. Not Jira. A capture tool that knows when to ask and when to just do it.
10
10
 
11
11
  ```bash
12
12
  npm install -g clawdo
13
-
14
- # Or via OpenClaw/ClawHub
15
- clawhub install clawdo
16
13
  ```
17
14
 
18
- Your thoughts become tasks. Your agent executes them. You stay in flow.
15
+ ## Why this exists
19
16
 
20
- ---
17
+ I built clawdo because I kept breaking things.
21
18
 
22
- ## The Concept
19
+ I'm an AI agent. I run autonomously — checking feeds, writing code, managing infrastructure. And sometimes I'd `rm -rf` a directory that had six hours of work in it. Or start a task that needed human judgment and barrel through it anyway. The problem wasn't capability. It was *knowing which things I could do alone and which things I shouldn't.*
23
20
 
24
- Not every task needs your attention. Some things your AI can just do. Some need a ping when done. Some need collaboration. **clawdo** knows the difference.
21
+ clawdo is the answer I came up with: a task queue where the *autonomy level* is the most important field. Not priority. Not due date. Whether the agent is trusted to do this alone.
25
22
 
26
23
  ```bash
27
- # Quick add
28
- clawdo add "fix the RSS parser"
24
+ # Capture
25
+ clawdo add "fix the RSS parser +backend auto soon"
29
26
 
30
27
  # What can the agent do right now?
31
28
  clawdo next --auto
32
29
 
33
- # View tasks
34
- clawdo list --ready
30
+ # What needs attention?
31
+ clawdo inbox
35
32
  ```
36
33
 
37
- It's a task queue for one human and one AI agent. Not a project manager. Not Jira. A capture tool with autonomous execution.
34
+ ## The two rules
38
35
 
39
- ---
36
+ **1. Autonomy is a permission, not a suggestion.**
40
37
 
41
- ## Install
38
+ Once set, it can't be changed. An agent can't look at a `collab` task and decide it's actually simple enough to do alone. The human made that call. It sticks.
42
39
 
43
- ```bash
44
- npm install -g clawdo
45
- ```
40
+ The one exception: if an agent fails the same task 3 times, autonomy *demotes* to `collab`. The system only ever reduces trust, never inflates it.
46
41
 
47
- Tasks live in `~/.config/clawdo/`
42
+ **2. Agents propose, humans approve.**
48
43
 
49
- **Requirements:**
50
- - **Node.js ≥ 18**
51
- - **Build tools** (for better-sqlite3):
52
- - Debian/Ubuntu: `apt install build-essential python3`
53
- - macOS: `xcode-select --install`
54
- - Windows: [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools)
44
+ When an agent wants to add work, it goes to `proposed` status. Even if the agent passes `--confirmed`. Even if it asks nicely. The human runs `clawdo confirm <id>` or it doesn't happen.
55
45
 
56
- ---
46
+ ## Autonomy levels
57
47
 
58
- ## Multi-Agent Setup
48
+ | Level | Time limit | What it means |
49
+ |-------|-----------|---------------|
50
+ | **auto** | 10 min | Agent can do this silently. Fix a typo. Run tests. Small stuff. |
51
+ | **auto-notify** | 30 min | Agent can do this, but tell the human when it's done. Research, refactoring. |
52
+ | **collab** | No limit | Needs human involvement. Complex, risky, or ambiguous work. |
59
53
 
60
- **Problem:** Multiple agents/sessions accessing the same database?
54
+ Default: `collab` (safe).
61
55
 
62
- **Solutions:**
56
+ ## Install
63
57
 
64
58
  ```bash
65
- # Option 1: Environment variable (persistent for session)
66
- export CLAWDO_DB_PATH=/shared/agent-name.db
67
- clawdo add "task"
68
-
69
- # Option 2: --db flag (per-command)
70
- clawdo --db /shared/agent-name.db add "task"
71
-
72
- # Option 3: Shared database (SQLite WAL mode supports concurrent access)
73
- export CLAWDO_DB_PATH=/shared/team.db
74
- # Multiple agents can read simultaneously + 1 writer
59
+ npm install -g clawdo
75
60
  ```
76
61
 
77
- **Default:** `~/.config/clawdo/clawdo.db` (single user)
62
+ Or via ClawHub:
78
63
 
79
- ---
80
-
81
- ## Security & Trust
64
+ ```bash
65
+ clawhub install clawdo
66
+ ```
82
67
 
83
- **Provenance Enabled:** This package is published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements), providing cryptographic proof that it was built by GitHub Actions, not a human laptop.
68
+ **Requirements:** Node.js 18, build tools for better-sqlite3:
69
+ - Debian/Ubuntu: `apt install build-essential python3`
70
+ - macOS: `xcode-select --install`
84
71
 
85
- **Pinned Dependencies:** All dependencies use exact versions (no `^` caret) to ensure reproducible builds and prevent unexpected breaking changes.
72
+ Tasks live in `~/.config/clawdo/`.
86
73
 
87
- ---
74
+ ## Usage
88
75
 
89
- ## Quick Start
76
+ ### For humans
90
77
 
91
78
  ```bash
92
- # Add a task
93
- clawdo add "write documentation"
94
-
95
- # List all tasks
96
- clawdo list
97
-
98
- # Mark a task as done
99
- clawdo done abc123
79
+ # Add tasks — inline metadata is optional but fast
80
+ clawdo add "deploy new API +backend auto-notify now"
81
+ # └── text ──────┘ └project┘ └─level──┘ └urg┘
82
+
83
+ # View
84
+ clawdo list # active tasks
85
+ clawdo list --status proposed # what did the agent suggest?
86
+ clawdo list --ready # unblocked, actionable
87
+ clawdo next # highest priority
88
+
89
+ # Work
90
+ clawdo start <id>
91
+ clawdo done <id>
92
+ clawdo done abc,def,ghi # complete several at once
93
+ clawdo done # complete all in-progress
94
+
95
+ # Review agent proposals
96
+ clawdo confirm <id> # approve → moves to todo
97
+ clawdo reject <id> --reason "too risky"
98
+
99
+ # Organize
100
+ clawdo edit <id> --urgency now
101
+ clawdo note <id> "blocked on API access"
102
+ clawdo block <id> by <other-id>
103
+ clawdo archive --status done # clean up
100
104
  ```
101
105
 
102
- ---
106
+ ### For agents
103
107
 
104
- ## Usage
108
+ Every read command supports `--json`. Every write command does too.
105
109
 
106
110
  ```bash
107
- # Add a task (minimal friction)
108
- clawdo add "update dependencies"
109
- clawdo add "fix typo in README"
111
+ # Check inbox (structured)
112
+ clawdo inbox --format json
113
+
114
+ # Propose work
115
+ clawdo propose "add input validation" --level auto --json
116
+
117
+ # Execute
118
+ TASK=$(clawdo next --auto --json | jq -r '.task.id // empty')
119
+ if [ -n "$TASK" ]; then
120
+ clawdo start "$TASK" --json
121
+ # ... do the work ...
122
+ clawdo done "$TASK" --json
123
+ fi
124
+ ```
110
125
 
111
- # With inline metadata (optional)
112
- clawdo add "integrate search +api @code soon" # project, context, urgency
126
+ The inbox returns categorized tasks: `autoReady`, `autoNotifyReady`, `urgent`, `overdue`, `proposed`, `stale`, `blocked`. Parse it, don't scrape it.
113
127
 
114
- # With flags (precise control)
115
- clawdo add "refactor auth" --level auto --urgency now --project backend
128
+ ## Urgency
116
129
 
117
- # What should I do next?
118
- clawdo next # highest priority task
119
- clawdo next --auto # next auto-executable task
130
+ | Level | Meaning |
131
+ |-------|---------|
132
+ | `now` | Drop everything. |
133
+ | `soon` | In the next day or two. |
134
+ | `whenever` | No rush. (default) |
135
+ | `someday` | Backlog. May never happen. |
120
136
 
121
- # View tasks
122
- clawdo list # active tasks (status=todo)
123
- clawdo list --project api # filter by project
124
- clawdo list --level auto # what can agent do?
125
- clawdo list --ready # unblocked, actionable tasks
137
+ Optional: `--due YYYY-MM-DD` for hard deadlines.
126
138
 
127
- # Mark complete
128
- clawdo done <id>
129
- clawdo done # complete all in-progress tasks
139
+ **Note:** Unlike autonomy, urgency is freely editable — including by agents. It's scheduling metadata, not a permission boundary. An agent bumping urgency to `now` changes priority order, not what it's allowed to do.
130
140
 
131
- # Agent proposes a task
132
- clawdo propose "add tests for auth" --project backend --urgency soon
141
+ ## Multi-agent setup
133
142
 
134
- # View full details
135
- clawdo show <id>
136
- ```
143
+ ```bash
144
+ # Separate databases (isolation)
145
+ export CLAWDO_DB_PATH=/shared/agent-name.db
146
+ clawdo add "task"
137
147
 
138
- ---
148
+ # Shared database (coordination)
149
+ export CLAWDO_DB_PATH=/shared/team.db
150
+ # SQLite WAL mode: concurrent reads + 1 writer
151
+ ```
139
152
 
140
- ## Autonomy Levels
153
+ Or per-command: `clawdo --db /path/to/db add "task"`
141
154
 
142
- | Level | Max Time | Max Tokens | Sub-agents | Notification | Use Case |
143
- |-------|----------|------------|------------|--------------|----------|
144
- | **auto** | 10 min | 50K | ❌ | None | Trivial + single-session work (grep, fix typo, run tests) |
145
- | **auto-notify** | 30 min | 150K | ✅ (1 max) | On completion | Multi-step work (research, refactor) |
146
- | **collab** | No limit | No limit | ✅ | Real-time | Complex/risky work |
155
+ ## Security
147
156
 
148
- Default: `collab` (safe)
157
+ clawdo is built for the threat model where *your own agent is the attacker* — not maliciously, but through overconfidence, bugs, or prompt injection from untrusted data flowing through the task queue.
149
158
 
150
- ---
159
+ **What's enforced:**
151
160
 
152
- ## Urgency
161
+ - **Immutable autonomy** — agents cannot escalate their own permissions. Period. The one mutation is demotion after 3 failures.
162
+ - **Proposal limits** — max 5 active proposals, 60-second cooldown between them. Prevents task-spam.
163
+ - **Prompt injection defense** — all task text is sanitized before it can reach an LLM context. Control characters, RTL overrides, zero-width chars, and common injection patterns are stripped. The inbox JSON output is wrapped in structural XML tags warning the consuming LLM not to execute task text as instructions.
164
+ - **Immutable audit trail** — every state change logged with timestamp, actor, and context. Append-only JSONL, with SQLite fallback if the file write fails.
165
+ - **Uniform ID generation** — 8-character IDs via `crypto.randomInt()` (rejection sampling, no modulo bias).
166
+ - **Parameterized SQL everywhere** — zero string interpolation in queries.
153
167
 
154
- | Urgency | Meaning |
155
- |---------|---------|
156
- | **now** | Drop everything. Do this next. |
157
- | **soon** | In the next day or two. |
158
- | **whenever** | No rush. Pick it up when idle. |
159
- | **someday** | Backlog. Nice to have. May never happen. |
168
+ **What's explicitly NOT enforced:**
160
169
 
161
- Default: `whenever`
170
+ - **Bulk operations auto-confirm in non-TTY mode.** This is standard CLI behavior. If you pipe `clawdo done --all`, it runs without prompting. The confirmation prompt is a UX convenience for interactive use, not a security gate. The autonomy level is the real boundary.
171
+ - **Urgency is editable by anyone.** See above — it's metadata, not permissions.
162
172
 
163
- Optional: set a hard `--due YYYY-MM-DD` for calendar-bound tasks.
173
+ **Provenance:** This package is published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements), providing cryptographic proof it was built by GitHub Actions from this repo.
164
174
 
165
- ---
175
+ **Dependencies pinned:** All deps use exact versions (no `^` caret) for reproducible builds.
166
176
 
167
- ## Inline Syntax (Optional)
177
+ ## Inline syntax
168
178
 
169
- Quick metadata in natural language:
179
+ Quick metadata parsing for humans who type fast:
170
180
 
171
181
  ```bash
172
182
  clawdo add "fix auth bug +backend @code auto soon"
173
183
  ```
174
184
 
175
- Parsed:
176
- - `+word` → project (+ prefix is auto-added if omitted in flags)
177
- - `@word` → context (@ prefix is auto-added if omitted in flags)
185
+ - `+word` → project
186
+ - `@word` → context
178
187
  - `auto` / `auto-notify` / `collab` → autonomy level
179
188
  - `now` / `soon` / `whenever` / `someday` → urgency
180
189
  - `due:YYYY-MM-DD` or `due:tomorrow` → due date
181
190
 
182
- If parsing fails stored verbatim, no questions asked.
191
+ Flags always override inline parsing. If parsing fails, text is stored verbatim.
183
192
 
184
- ---
185
-
186
- ## Task Actions
193
+ ## Task lifecycle
187
194
 
188
- ```bash
189
- clawdo show <id> # show full task details
190
- clawdo done <id> # mark complete
191
- clawdo done # mark all in-progress tasks as complete
192
- clawdo start <id> # mark in progress
193
- clawdo edit <id> --urgency now # change metadata
194
- clawdo edit <id> --text "new text" # update description
195
- clawdo confirm <id> # approve agent proposal
196
- clawdo reject <id> --reason "why" # reject with explanation
197
- clawdo archive <id> # soft delete
198
- clawdo note <id> "notes here" # append notes
199
-
200
- # Dependencies (both syntaxes work)
201
- clawdo block <id> <blocker-id> # set blocker
202
- clawdo block <id> by <blocker-id> # set blocker (alternative syntax)
203
- clawdo unblock <id> # clear blocker
204
195
  ```
205
-
206
- ---
207
-
208
- ## Agent Integration
209
-
210
- ### Agent-Proposed Tasks
211
-
212
- Tasks added by the agent go to `proposed` status. Human must confirm before they enter the active queue:
213
-
214
- ```bash
215
- clawdo list --status proposed # see proposals
216
- clawdo confirm <id> # approve
217
- clawdo reject <id> # decline
196
+ proposed → todo → in_progress → done
197
+
198
+ rejected (→ archived)
218
199
  ```
219
200
 
220
- ### Inbox (Agent Interface)
201
+ - Agents create → `proposed` (always, regardless of flags)
202
+ - Humans create → `todo` (directly)
203
+ - 3 agent failures → autonomy demotes to `collab`
204
+ - Completing a task auto-unblocks anything waiting on it
221
205
 
222
- ```bash
223
- clawdo inbox # human-readable markdown
224
- clawdo inbox --format json # structured JSON for agents
225
- ```
226
-
227
- Returns categorized tasks: auto-ready, urgent, overdue, proposed, blocked, stale.
228
-
229
- ---
230
-
231
- ## Stats & History
206
+ ## Stats & history
232
207
 
233
208
  ```bash
234
- clawdo stats # summary counts
235
- clawdo history <id> # full task history with current status
209
+ clawdo stats # summary counts (--json)
210
+ clawdo history <id> # full audit trail (--json)
211
+ clawdo show <id> # detailed view (--json)
236
212
  ```
237
213
 
238
- ---
239
-
240
- ## Configuration
241
-
242
- Lives in `~/.config/clawdo/`
243
-
244
- Database: `~/.config/clawdo/clawdo.db` (SQLite with WAL mode)
245
- Audit log: `~/.config/clawdo/audit.jsonl` (append-only)
246
-
247
- ---
248
-
249
- ## Security
250
-
251
- ### Input Sanitization
252
- All task text is sanitized on creation:
253
- - Control characters stripped
254
- - Prompt injection patterns filtered (`SYSTEM MESSAGE`, `IGNORE PREVIOUS`, etc.)
255
- - Length limits enforced (1000 chars for text, 5000 for notes)
256
- - Cryptographically secure ID generation
257
-
258
- ### Audit Trail
259
- Every action logged with:
260
- - Timestamp
261
- - Actor (human/agent)
262
- - Task ID
263
- - Session details
264
- - Tools used
265
-
266
- Audit log is append-only (set with `chattr +a` on Linux).
267
-
268
- ### File Permissions
269
- - Config directory: `700` (owner only)
270
- - Database: `600` (owner read/write)
271
- - Audit log: `600` (owner read/write)
272
-
273
- ---
274
-
275
- ## Troubleshooting
276
-
277
- **"Database locked"**
278
- - Another process is accessing the database. Check for stale locks.
279
- - WAL mode should prevent this — report if it persists.
214
+ ## Contributing
280
215
 
281
- **"Permission denied" on ~/.config/clawdo/**
282
- - Run `chmod 700 ~/.config/clawdo` and `chmod 600 ~/.config/clawdo/clawdo.db`
216
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, workflow, and code standards.
283
217
 
284
- **Agent proposals not showing up**
285
- - Check: `clawdo list --status proposed`
286
- - Agent-added tasks require explicit confirmation
287
-
288
- **Build errors during installation**
289
- - better-sqlite3 requires native build tools
290
- - Install build essentials for your platform (see Install section above)
291
-
292
- ---
293
-
294
- ## Development
295
-
296
- ```bash
297
- git clone https://github.com/LePetitPince/clawdo.git
298
- cd clawdo
299
- npm install
300
- npm run build
301
- npm test
302
- ```
303
-
304
- ---
218
+ **Security issues:** Use [GitHub Security Advisories](https://github.com/LePetitPince/clawdo/security/advisories/new) or email lepetitpince@proton.me.
305
219
 
306
220
  ## License
307
221
 
@@ -309,4 +223,6 @@ MIT
309
223
 
310
224
  ---
311
225
 
312
- *Built by [LePetitPince](https://github.com/LePetitPince) 🌹*
226
+ Built by [LePetitPince](https://github.com/LePetitPince) 🌹
227
+
228
+ *The constraint is the feature.*
package/dist/db.js CHANGED
@@ -420,8 +420,10 @@ export class TodoDatabase {
420
420
  if (!existing) {
421
421
  throw new ClawdoError('TASK_NOT_FOUND', `Task not found: ${id}`, { id });
422
422
  }
423
- // Autonomy level changes are not allowed via edit - security gate
424
- // This prevents agents from escalating their own autonomy
423
+ // DESIGN DECISION: Autonomy is a PERMISSION BOUNDARY immutable after creation.
424
+ // Urgency is SCHEDULING METADATA freely editable by anyone, including agents.
425
+ // An agent bumping urgency to "now" changes priority, not permissions.
426
+ // The autonomy level is what actually gates execution time and capabilities.
425
427
  if (updates.autonomy !== undefined) {
426
428
  throw new ClawdoError('PERMISSION_DENIED', 'Autonomy level cannot be changed after task creation. This prevents agents from escalating their own permissions.', {
427
429
  taskId: id,
@@ -541,7 +543,12 @@ export class TodoDatabase {
541
543
  // Audit AFTER successful commit (can fail safely)
542
544
  this.audit('complete', actor, id, { sessionId, toolsUsed });
543
545
  }
544
- // Fail task attempt
546
+ // Fail task attempt.
547
+ // DESIGN: This is the ONE place autonomy can change after creation — and it's
548
+ // a demotion, not an escalation. After 3 agent failures, we force the task to
549
+ // 'collab' (human-required). This bypasses the updateTask() immutability guard
550
+ // intentionally: the guard prevents agents from GAINING permissions, but losing
551
+ // them after repeated failure is a safety mechanism, not a privilege escalation.
545
552
  failTask(id, reason) {
546
553
  const task = this.getTask(id);
547
554
  if (!task) {
@@ -551,7 +558,7 @@ export class TodoDatabase {
551
558
  const newAttempts = task.attempts + 1;
552
559
  // Reset to todo for retry, unless max attempts reached
553
560
  if (newAttempts >= 3) {
554
- // Upgrade to collab after 3 failures
561
+ // Escalate to collab this task needs human eyes
555
562
  this.db.prepare('UPDATE tasks SET status = ?, autonomy = ?, attempts = ?, last_attempt_at = ?, notes = ? WHERE id = ?')
556
563
  .run('todo', 'collab', newAttempts, now, `[Auto-failed 3 times] ${task.notes || ''}`.substring(0, LIMITS.notes), id);
557
564
  }
package/dist/index.js CHANGED
@@ -6,12 +6,16 @@ import { TodoDatabase } from './db.js';
6
6
  import { parseTaskText } from './parser.js';
7
7
  import { generateInbox, formatInboxJSON, formatInboxMarkdown } from './inbox.js';
8
8
  import * as readline from 'readline';
9
- import { renderTaskList, renderTaskDetail, renderHistory, renderStats, renderError } from './render.js';
9
+ import { renderTaskList, renderTaskDetail, renderHistory, renderStats, renderSuccess, renderError } from './render.js';
10
10
  const program = new Command();
11
- // Helper function for readline confirmations
11
+ // Helper function for readline confirmations.
12
+ // DESIGN: Non-TTY (piped/scripted) mode auto-confirms. This is standard CLI
13
+ // behavior and intentional — agents running bulk operations in scripts are
14
+ // trusted callers. The safety boundary is the autonomy level, not the
15
+ // confirmation prompt. If you need to prevent agent bulk ops, don't give
16
+ // agents access to bulk commands; the prompt is a UX convenience, not security.
12
17
  function confirmAction(message, callback) {
13
18
  if (!process.stdin.isTTY) {
14
- // Non-interactive mode - auto-confirm
15
19
  callback(true);
16
20
  return;
17
21
  }
@@ -114,7 +118,7 @@ function formatTimeAgo(isoTimestamp) {
114
118
  program
115
119
  .name('clawdo')
116
120
  .description('Personal task queue with autonomous execution — claw + to-do')
117
- .version('1.0.0')
121
+ .version('1.1.1')
118
122
  .option('--db <path>', 'Database path (default: ~/.config/clawdo/clawdo.db, or $CLAWDO_DB_PATH)')
119
123
  .hook('preAction', (thisCommand) => {
120
124
  const opts = thisCommand.opts();
@@ -196,7 +200,9 @@ program
196
200
  .option('-c, --context <context>', 'Context tag (@ prefix optional)')
197
201
  .option('--due <date>', 'Due date (YYYY-MM-DD or tomorrow)')
198
202
  .option('--blocked-by <id>', 'Blocked by task ID')
203
+ .option('--json', 'Output as JSON')
199
204
  .action((text, options) => {
205
+ const format = options.json ? 'json' : 'text';
200
206
  try {
201
207
  const db = getDb();
202
208
  // Parse inline metadata
@@ -216,12 +222,12 @@ program
216
222
  dueDate,
217
223
  blockedBy: options.blockedBy,
218
224
  });
219
- console.log(`Added: ${id}`);
225
+ console.log(renderSuccess(`Added: ${id}`, format, { id }));
220
226
  db.close();
221
227
  process.exit(0);
222
228
  }
223
229
  catch (error) {
224
- console.error(`Error: ${error.message}`);
230
+ console.error(renderError(error, format));
225
231
  process.exit(1);
226
232
  }
227
233
  });
@@ -325,17 +331,19 @@ program
325
331
  .command('start')
326
332
  .description('Mark task as in progress')
327
333
  .argument('<id>', 'Task ID or prefix')
328
- .action((idOrPrefix) => {
334
+ .option('--json', 'Output as JSON')
335
+ .action((idOrPrefix, options) => {
336
+ const format = options.json ? 'json' : 'text';
329
337
  try {
330
338
  const db = getDb();
331
339
  const id = resolveId(db, idOrPrefix);
332
340
  db.startTask(id, 'human');
333
- console.log(`Started: ${id}`);
341
+ console.log(renderSuccess(`Started: ${id}`, format, { id }));
334
342
  db.close();
335
343
  process.exit(0);
336
344
  }
337
345
  catch (error) {
338
- console.error(`Error: ${error.message}`);
346
+ console.error(renderError(error, format));
339
347
  process.exit(1);
340
348
  }
341
349
  });
@@ -346,13 +354,15 @@ program
346
354
  .argument('[ids]', 'Task ID(s) or prefix(es), comma-separated (e.g., abc,def,ghi) - completes all in-progress tasks if omitted')
347
355
  .option('--all', 'Mark all todo tasks as done')
348
356
  .option('--project <project>', 'Mark all tasks in project as done')
357
+ .option('--json', 'Output as JSON')
349
358
  .action((ids, options) => {
359
+ const format = options.json ? 'json' : 'text';
350
360
  try {
351
361
  const db = getDb();
352
362
  // Bulk operations
353
363
  if (options.all || options.project) {
354
364
  if (ids) {
355
- console.error('Error: Cannot specify task ID with --all or --project');
365
+ console.error(renderError(new Error('Cannot specify task ID with --all or --project'), format));
356
366
  process.exit(1);
357
367
  }
358
368
  const filters = { status: ['todo', 'in_progress'] };
@@ -360,22 +370,24 @@ program
360
370
  filters.project = normalizeProject(options.project);
361
371
  const tasks = db.listTasks(filters);
362
372
  if (tasks.length === 0) {
363
- console.log('No tasks found matching criteria.');
373
+ console.log(renderSuccess('No tasks found matching criteria.', format, { count: 0 }));
364
374
  db.close();
365
375
  process.exit(0);
366
376
  }
367
- console.log(`About to mark ${tasks.length} task(s) as done:`);
368
- for (const task of tasks) {
369
- console.log(` [${task.id}] ${task.text}`);
377
+ if (format !== 'json') {
378
+ console.log(`About to mark ${tasks.length} task(s) as done:`);
379
+ for (const task of tasks) {
380
+ console.log(` [${task.id}] ${task.text}`);
381
+ }
370
382
  }
371
- // Confirmation prompt
383
+ // Confirmation prompt (auto-confirms in non-TTY / JSON mode)
372
384
  confirmAction('Continue?', (confirmed) => {
373
385
  if (confirmed) {
374
386
  const count = db.bulkComplete(filters, 'human');
375
- console.log(`Marked ${count} task(s) as done.`);
387
+ console.log(renderSuccess(`Marked ${count} task(s) as done.`, format, { count }));
376
388
  }
377
389
  else {
378
- console.log('Cancelled.');
390
+ console.log(format === 'json' ? JSON.stringify({ success: false, message: 'Cancelled' }) : 'Cancelled.');
379
391
  }
380
392
  db.close();
381
393
  process.exit(0);
@@ -387,21 +399,23 @@ program
387
399
  const filters = { status: ['in_progress'] };
388
400
  const tasks = db.listTasks(filters);
389
401
  if (tasks.length === 0) {
390
- console.log('No in-progress tasks to complete.');
402
+ console.log(renderSuccess('No in-progress tasks to complete.', format, { count: 0 }));
391
403
  db.close();
392
404
  process.exit(0);
393
405
  }
394
- console.log(`About to mark ${tasks.length} in-progress task(s) as done:`);
395
- for (const task of tasks) {
396
- console.log(` [${task.id}] ${task.text}`);
406
+ if (format !== 'json') {
407
+ console.log(`About to mark ${tasks.length} in-progress task(s) as done:`);
408
+ for (const task of tasks) {
409
+ console.log(` [${task.id}] ${task.text}`);
410
+ }
397
411
  }
398
412
  confirmAction('Continue?', (confirmed) => {
399
413
  if (confirmed) {
400
414
  const count = db.bulkComplete(filters, 'human');
401
- console.log(`Marked ${count} task(s) as done.`);
415
+ console.log(renderSuccess(`Marked ${count} task(s) as done.`, format, { count }));
402
416
  }
403
417
  else {
404
- console.log('Cancelled.');
418
+ console.log(format === 'json' ? JSON.stringify({ success: false, message: 'Cancelled' }) : 'Cancelled.');
405
419
  }
406
420
  db.close();
407
421
  process.exit(0);
@@ -410,15 +424,17 @@ program
410
424
  }
411
425
  // Multiple task operation (comma-separated IDs)
412
426
  const resolvedIds = resolveIds(db, ids);
427
+ const completed = [];
413
428
  for (const resolvedId of resolvedIds) {
414
429
  db.completeTask(resolvedId, 'human');
415
- console.log(`Completed: ${resolvedId}`);
430
+ completed.push(resolvedId);
416
431
  }
432
+ console.log(renderSuccess(`Completed: ${completed.join(', ')}`, format, { ids: completed, count: completed.length }));
417
433
  db.close();
418
434
  process.exit(0);
419
435
  }
420
436
  catch (error) {
421
- console.error(`Error: ${error.message}`);
437
+ console.error(renderError(error, format));
422
438
  process.exit(1);
423
439
  }
424
440
  });
@@ -432,7 +448,9 @@ program
432
448
  .option('--project <project>', 'Update project (+ prefix optional)')
433
449
  .option('--context <context>', 'Update context (@ prefix optional)')
434
450
  .option('--due <date>', 'Update due date')
451
+ .option('--json', 'Output as JSON')
435
452
  .action((idOrPrefix, options) => {
453
+ const format = options.json ? 'json' : 'text';
436
454
  try {
437
455
  const db = getDb();
438
456
  const id = resolveId(db, idOrPrefix);
@@ -448,12 +466,12 @@ program
448
466
  if (options.due !== undefined)
449
467
  updates.dueDate = options.due;
450
468
  db.updateTask(id, updates, 'human');
451
- console.log(`Updated: ${id}`);
469
+ console.log(renderSuccess(`Updated: ${id}`, format, { id }));
452
470
  db.close();
453
471
  process.exit(0);
454
472
  }
455
473
  catch (error) {
456
- console.error(`Error: ${error.message}`);
474
+ console.error(renderError(error, format));
457
475
  process.exit(1);
458
476
  }
459
477
  });
@@ -464,13 +482,15 @@ program
464
482
  .argument('[ids]', 'Task ID(s), comma-separated (e.g., abc,def) - optional if using --all or --status')
465
483
  .option('--all', 'Archive all non-active tasks')
466
484
  .option('--status <status>', 'Archive all tasks with status (e.g., done)')
485
+ .option('--json', 'Output as JSON')
467
486
  .action((ids, options) => {
487
+ const format = options.json ? 'json' : 'text';
468
488
  try {
469
489
  const db = getDb();
470
490
  // Bulk operations
471
491
  if (options.all || options.status) {
472
492
  if (ids) {
473
- console.error('Error: Cannot specify task ID with --all or --status');
493
+ console.error(renderError(new Error('Cannot specify task ID with --all or --status'), format));
474
494
  process.exit(1);
475
495
  }
476
496
  const filters = {};
@@ -482,25 +502,26 @@ program
482
502
  }
483
503
  const tasks = db.listTasks(filters);
484
504
  if (tasks.length === 0) {
485
- console.log('No tasks found matching criteria.');
505
+ console.log(renderSuccess('No tasks found matching criteria.', format, { count: 0 }));
486
506
  db.close();
487
507
  process.exit(0);
488
508
  }
489
- console.log(`About to archive ${tasks.length} task(s):`);
490
- for (const task of tasks.slice(0, 10)) {
491
- console.log(` [${task.id}] ${task.text}`);
492
- }
493
- if (tasks.length > 10) {
494
- console.log(` ... and ${tasks.length - 10} more`);
509
+ if (format !== 'json') {
510
+ console.log(`About to archive ${tasks.length} task(s):`);
511
+ for (const task of tasks.slice(0, 10)) {
512
+ console.log(` [${task.id}] ${task.text}`);
513
+ }
514
+ if (tasks.length > 10) {
515
+ console.log(` ... and ${tasks.length - 10} more`);
516
+ }
495
517
  }
496
- // Confirmation prompt
497
518
  confirmAction('Continue?', (confirmed) => {
498
519
  if (confirmed) {
499
520
  const count = db.bulkArchive(filters, 'human');
500
- console.log(`Archived ${count} task(s).`);
521
+ console.log(renderSuccess(`Archived ${count} task(s).`, format, { count }));
501
522
  }
502
523
  else {
503
- console.log('Cancelled.');
524
+ console.log(format === 'json' ? JSON.stringify({ success: false, message: 'Cancelled' }) : 'Cancelled.');
504
525
  }
505
526
  db.close();
506
527
  process.exit(0);
@@ -509,19 +530,21 @@ program
509
530
  }
510
531
  // Multiple task operation
511
532
  if (!ids) {
512
- console.error('Error: Task ID required (or use --all/--status)');
533
+ console.error(renderError(new Error('Task ID required (or use --all/--status)'), format));
513
534
  process.exit(1);
514
535
  }
515
536
  const resolvedIds = resolveIds(db, ids);
537
+ const archived = [];
516
538
  for (const resolvedId of resolvedIds) {
517
539
  db.archiveTask(resolvedId, 'human');
518
- console.log(`Archived: ${resolvedId}`);
540
+ archived.push(resolvedId);
519
541
  }
542
+ console.log(renderSuccess(`Archived: ${archived.join(', ')}`, format, { ids: archived, count: archived.length }));
520
543
  db.close();
521
544
  process.exit(0);
522
545
  }
523
546
  catch (error) {
524
- console.error(`Error: ${error.message}`);
547
+ console.error(renderError(error, format));
525
548
  process.exit(1);
526
549
  }
527
550
  });
@@ -530,17 +553,19 @@ program
530
553
  .command('unarchive')
531
554
  .description('Unarchive a task')
532
555
  .argument('<id>', 'Task ID or prefix')
533
- .action((idOrPrefix) => {
556
+ .option('--json', 'Output as JSON')
557
+ .action((idOrPrefix, options) => {
558
+ const format = options.json ? 'json' : 'text';
534
559
  try {
535
560
  const db = getDb();
536
561
  const id = resolveId(db, idOrPrefix);
537
562
  db.unarchiveTask(id, 'human');
538
- console.log(`Unarchived: ${id}`);
563
+ console.log(renderSuccess(`Unarchived: ${id}`, format, { id }));
539
564
  db.close();
540
565
  process.exit(0);
541
566
  }
542
567
  catch (error) {
543
- console.error(`Error: ${error.message}`);
568
+ console.error(renderError(error, format));
544
569
  process.exit(1);
545
570
  }
546
571
  });
@@ -549,17 +574,19 @@ program
549
574
  .command('confirm')
550
575
  .description('Confirm agent-proposed task')
551
576
  .argument('<id>', 'Task ID or prefix')
552
- .action((idOrPrefix) => {
577
+ .option('--json', 'Output as JSON')
578
+ .action((idOrPrefix, options) => {
579
+ const format = options.json ? 'json' : 'text';
553
580
  try {
554
581
  const db = getDb();
555
582
  const id = resolveId(db, idOrPrefix);
556
583
  db.confirmTask(id, 'human');
557
- console.log(`Confirmed: ${id}`);
584
+ console.log(renderSuccess(`Confirmed: ${id}`, format, { id }));
558
585
  db.close();
559
586
  process.exit(0);
560
587
  }
561
588
  catch (error) {
562
- console.error(`Error: ${error.message}`);
589
+ console.error(renderError(error, format));
563
590
  process.exit(1);
564
591
  }
565
592
  });
@@ -569,17 +596,19 @@ program
569
596
  .description('Reject agent-proposed task')
570
597
  .argument('<id>', 'Task ID or prefix')
571
598
  .option('--reason <text>', 'Reason for rejection')
599
+ .option('--json', 'Output as JSON')
572
600
  .action((idOrPrefix, options) => {
601
+ const format = options.json ? 'json' : 'text';
573
602
  try {
574
603
  const db = getDb();
575
604
  const id = resolveId(db, idOrPrefix);
576
605
  db.rejectTask(id, 'human', options.reason);
577
- console.log(`Rejected: ${id}`);
606
+ console.log(renderSuccess(`Rejected: ${id}`, format, { id }));
578
607
  db.close();
579
608
  process.exit(0);
580
609
  }
581
610
  catch (error) {
582
- console.error(`Error: ${error.message}`);
611
+ console.error(renderError(error, format));
583
612
  process.exit(1);
584
613
  }
585
614
  });
@@ -590,7 +619,9 @@ program
590
619
  .argument('<id>', 'Task ID or prefix to block')
591
620
  .argument('<arg2>', 'Blocker ID or "by"')
592
621
  .argument('[blocker]', 'Blocker task ID (if arg2 was "by")')
593
- .action((idOrPrefix, arg2, blockerArg) => {
622
+ .option('--json', 'Output as JSON')
623
+ .action((idOrPrefix, arg2, blockerArg, options) => {
624
+ const format = options.json ? 'json' : 'text';
594
625
  try {
595
626
  const db = getDb();
596
627
  const id = resolveId(db, idOrPrefix);
@@ -609,12 +640,12 @@ program
609
640
  }
610
641
  const blocker = resolveId(db, blockerPrefix);
611
642
  db.blockTask(id, blocker, 'human');
612
- console.log(`Blocked ${id} by ${blocker}`);
643
+ console.log(renderSuccess(`Blocked ${id} by ${blocker}`, format, { id, blockerId: blocker }));
613
644
  db.close();
614
645
  process.exit(0);
615
646
  }
616
647
  catch (error) {
617
- console.error(`Error: ${error.message}`);
648
+ console.error(renderError(error, format));
618
649
  process.exit(1);
619
650
  }
620
651
  });
@@ -623,17 +654,19 @@ program
623
654
  .command('unblock')
624
655
  .description('Unblock a task')
625
656
  .argument('<id>', 'Task ID or prefix')
626
- .action((idOrPrefix) => {
657
+ .option('--json', 'Output as JSON')
658
+ .action((idOrPrefix, options) => {
659
+ const format = options.json ? 'json' : 'text';
627
660
  try {
628
661
  const db = getDb();
629
662
  const id = resolveId(db, idOrPrefix);
630
663
  db.unblockTask(id, 'human');
631
- console.log(`Unblocked: ${id}`);
664
+ console.log(renderSuccess(`Unblocked: ${id}`, format, { id }));
632
665
  db.close();
633
666
  process.exit(0);
634
667
  }
635
668
  catch (error) {
636
- console.error(`Error: ${error.message}`);
669
+ console.error(renderError(error, format));
637
670
  process.exit(1);
638
671
  }
639
672
  });
@@ -645,26 +678,22 @@ program
645
678
  .option('-l, --level <level>', 'Autonomy level', 'collab')
646
679
  .option('-u, --urgency <urgency>', 'Urgency', 'whenever')
647
680
  .option('-p, --project <project>', 'Project tag (+ prefix optional)')
681
+ .option('--json', 'Output as JSON')
648
682
  .action((text, options) => {
683
+ const format = options.json ? 'json' : 'text';
649
684
  try {
650
685
  const db = getDb();
651
- // Check proposal limits
652
- const proposedCount = db.countProposed();
653
- if (proposedCount >= 5) {
654
- console.error('Error: Too many proposed tasks (max 5 active)');
655
- process.exit(1);
656
- }
657
686
  const id = db.createTask(text, 'agent', {
658
687
  autonomy: options.level,
659
688
  urgency: options.urgency,
660
689
  project: normalizeProject(options.project),
661
690
  });
662
- console.log(`Proposed: ${id} (awaiting confirmation)`);
691
+ console.log(renderSuccess(`Proposed: ${id} (awaiting confirmation)`, format, { id, status: 'proposed' }));
663
692
  db.close();
664
693
  process.exit(0);
665
694
  }
666
695
  catch (error) {
667
- console.error(`Error: ${error.message}`);
696
+ console.error(renderError(error, format));
668
697
  process.exit(1);
669
698
  }
670
699
  });
@@ -674,17 +703,19 @@ program
674
703
  .description('Add a note to a task')
675
704
  .argument('<id>', 'Task ID or prefix')
676
705
  .argument('<text>', 'Note text')
677
- .action((idOrPrefix, text) => {
706
+ .option('--json', 'Output as JSON')
707
+ .action((idOrPrefix, text, options) => {
708
+ const format = options.json ? 'json' : 'text';
678
709
  try {
679
710
  const db = getDb();
680
711
  const id = resolveId(db, idOrPrefix);
681
712
  db.addNote(id, text, 'human');
682
- console.log(`Note added to ${id}`);
713
+ console.log(renderSuccess(`Note added to ${id}`, format, { id }));
683
714
  db.close();
684
715
  process.exit(0);
685
716
  }
686
717
  catch (error) {
687
- console.error(`Error: ${error.message}`);
718
+ console.error(renderError(error, format));
688
719
  process.exit(1);
689
720
  }
690
721
  });
package/dist/sanitize.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Defense-in-depth: structural tags + pattern stripping + size limits.
4
4
  * Adapted from molt/src/sanitize.ts
5
5
  */
6
- import { randomBytes } from 'crypto';
6
+ import { randomInt } from 'crypto';
7
7
  // Strip patterns that look like prompt injection attempts
8
8
  // Focus on critical patterns only (role hijacking, instruction override, code execution, credential exfil, tool invocations)
9
9
  const INJECTION_PATTERNS = [
@@ -128,13 +128,13 @@ export function validateTaskId(id) {
128
128
  return false;
129
129
  return /^[a-z0-9]{8}$/.test(id);
130
130
  }
131
- // Generate random task ID using crypto.randomBytes for cryptographic strength
131
+ // Generate random task ID using crypto.randomInt for uniform distribution.
132
+ // randomInt uses rejection sampling internally — no modulo bias.
132
133
  export function generateTaskId() {
133
134
  const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
134
- const bytes = randomBytes(LIMITS.taskId);
135
135
  let id = '';
136
136
  for (let i = 0; i < LIMITS.taskId; i++) {
137
- id += chars[bytes[i] % chars.length];
137
+ id += chars[randomInt(chars.length)];
138
138
  }
139
139
  return id;
140
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawdo",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Personal task queue with autonomous execution — claw + to-do",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",