invoket 0.1.7 → 0.1.9

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
@@ -1,74 +1,91 @@
1
1
  # invoket
2
2
 
3
- A TypeScript task runner for Bun that uses type annotations to parse CLI arguments.
3
+ TypeScript task runner for Bun. Write typed methods, get a CLI for free.
4
4
 
5
- ## Features
6
-
7
- - **Type-safe CLI parsing** — TypeScript types determine how arguments are parsed
8
- - **Zero configuration** — Just write a `Tasks` class with typed methods
9
- - **JSON support** — Object and array parameters are automatically parsed from JSON
10
- - **Namespace support** — Organize tasks with `db:migrate` style namespaces
11
- - **Rest parameters** — Support for `...args` variadic parameters
12
- - **Auto-generated help** — JSDoc descriptions become CLI help text
5
+ ```typescript
6
+ import { Context } from "invoket/context";
13
7
 
14
- ## Installation
8
+ export class Tasks {
9
+ /** Deploy to an environment */
10
+ async deploy(c: Context, env: string, force: boolean = false) {
11
+ await c.run(`deploy.sh ${env}${force ? " --force" : ""}`);
12
+ }
13
+ }
14
+ ```
15
15
 
16
16
  ```bash
17
- bun link invoket
17
+ $ invt deploy prod --force
18
18
  ```
19
19
 
20
- ## Quick Start
20
+ No config files. No argument parser boilerplate. Your TypeScript types *are* the CLI definition.
21
+
22
+ ## Why not just write a script?
23
+
24
+ You could. But then you write arg parsing, help text, and error handling every time. With invoket, you write a method and get all three for free. Your `tasks.ts` becomes a growing toolbox — every task documented, discoverable via `invt --help`, and callable by name with typed arguments.
25
+
26
+ One script solves one problem. A `tasks.ts` file is a project's command centre.
27
+
28
+ ## Installation
21
29
 
22
30
  ```bash
23
- invt # Show help
24
- invt hello World 3 # Run task with args
25
- invt db:migrate up # Run namespaced task
26
- invt --version # Show version
31
+ bun add -d invoket # Add to project
32
+ bun link invoket # Or link globally for development
27
33
  ```
28
34
 
29
- ## Writing Tasks
35
+ ## Quick Start
30
36
 
31
- Create a `tasks.ts` file with a `Tasks` class:
37
+ Create `tasks.ts`:
32
38
 
33
39
  ```typescript
34
40
  import { Context } from "invoket/context";
35
41
 
36
- interface SearchParams {
37
- query: string;
38
- limit?: number;
39
- }
40
-
41
42
  /**
42
- * Project build and deployment tasks
43
+ * My project tasks
43
44
  */
44
45
  export class Tasks {
45
- /** Say hello with a name and repeat count */
46
- async hello(c: Context, name: string, count: number) {
46
+ /**
47
+ * Say hello
48
+ * @flag name -n
49
+ * @flag count -c
50
+ */
51
+ async hello(c: Context, name: string, count: number = 1) {
47
52
  for (let i = 0; i < count; i++) {
48
53
  console.log(`Hello, ${name}!`);
49
54
  }
50
55
  }
51
56
 
52
57
  /** Search with JSON parameters */
53
- async search(c: Context, entity: string, params: SearchParams) {
58
+ async search(c: Context, entity: string, params: { query: string; limit?: number }) {
54
59
  console.log(`Searching ${entity}: ${params.query}`);
55
60
  }
56
61
 
57
- /** Install packages (rest params) */
62
+ /** Install packages */
58
63
  async install(c: Context, ...packages: string[]) {
59
64
  for (const pkg of packages) {
60
- await c.run(`npm install ${pkg}`);
65
+ await c.run(`bun add ${pkg}`);
61
66
  }
62
67
  }
63
68
  }
64
69
  ```
65
70
 
71
+ Run it:
72
+
73
+ ```bash
74
+ invt # Show help
75
+ invt hello World # Positional args
76
+ invt hello -n World -c 3 # Short flags
77
+ invt hello --name=World --count=3 # Long flags
78
+ invt search users '{"query":"bob"}' # JSON params
79
+ invt install react vue angular # Rest params
80
+ invt hello -h # Task-specific help
81
+ ```
82
+
66
83
  ## Namespaces
67
84
 
68
- Organize related tasks into namespaces:
85
+ Group related tasks:
69
86
 
70
87
  ```typescript
71
- class DbNamespace {
88
+ class Db {
72
89
  /** Run database migrations */
73
90
  async migrate(c: Context, direction: string = "up") {
74
91
  await c.run(`prisma migrate ${direction}`);
@@ -81,128 +98,349 @@ class DbNamespace {
81
98
  }
82
99
 
83
100
  export class Tasks {
84
- db = new DbNamespace();
101
+ db = new Db();
85
102
  }
86
103
  ```
87
104
 
88
- Call with `invt db:migrate up` or `invt db.seed`.
105
+ ```bash
106
+ invt db:migrate up # colon separator
107
+ invt db.seed # dot separator also works
108
+ ```
109
+
110
+ ## Arguments
89
111
 
90
- ## Type Mapping
112
+ ### Type Mapping
91
113
 
92
- | TypeScript | CLI Display | Example Input |
93
- |------------|-------------|---------------|
94
- | `name: string` | `<name>` | `hello` |
95
- | `name: string = "default"` | `[name]` | `hello` (optional) |
114
+ | TypeScript | CLI | Example |
115
+ |------------|-----|---------|
116
+ | `name: string` | `<name>` (required) | `hello` |
117
+ | `name: string = "default"` | `[name]` (optional) | `hello` |
96
118
  | `count: number` | `<count>` | `42` |
97
- | `force: boolean` | `<force>` | `true` or `1` |
119
+ | `force: boolean` | `<force>` | `true`, `1`, `false`, `0` |
98
120
  | `params: SomeInterface` | `<params>` | `'{"key": "value"}'` |
99
- | `items: string[]` | `<items>` | `'["a", "b", "c"]'` |
100
- | `...args: string[]` | `[args...]` | `a b c` (variadic) |
121
+ | `items: string[]` | `<items>` | `'["a", "b"]'` |
122
+ | `...args: string[]` | `[args...]` (variadic) | `a b c` |
101
123
 
102
- ## CLI Flags
124
+ ### Flags
103
125
 
104
- | Flag | Description |
105
- |------|-------------|
106
- | `-h`, `--help` | Show help with all tasks |
107
- | `<task> -h` | Show help for a specific task |
108
- | `-l`, `--list` | List available tasks |
109
- | `--version` | Show version |
110
-
111
- ### Task-Specific Help
126
+ Every parameter automatically gets a `--long` flag. Add `@flag` annotations for short flags and aliases:
112
127
 
113
- Get detailed help for any task:
128
+ ```typescript
129
+ /**
130
+ * @flag env -e --environment
131
+ * @flag force -f
132
+ */
133
+ async deploy(c: Context, env: string, force: boolean = false) {}
134
+ ```
114
135
 
115
136
  ```bash
116
- invt hello -h
117
- # Usage: invt hello <name> <count>
118
- #
119
- # Say hello with a name and repeat count
120
- #
121
- # Arguments:
122
- # name string (required)
123
- # count number (required)
124
-
125
- invt db:migrate --help
126
- # Usage: invt db:migrate [direction]
127
- #
128
- # Run database migrations
129
- #
130
- # Arguments:
131
- # direction string (optional)
137
+ invt deploy prod # positional
138
+ invt deploy --env=prod --force # long flags
139
+ invt deploy -e prod -f # short flags
140
+ invt deploy --environment=prod # alias
141
+ invt deploy --no-force # boolean negation
142
+ invt deploy --force=false # explicit boolean
143
+ invt install -- --not-a-flag # -- stops flag parsing
132
144
  ```
133
145
 
146
+ Flags and positional args can be freely mixed in any order.
147
+
148
+ ### CLI Flags
149
+
150
+ | Flag | Description |
151
+ |------|-------------|
152
+ | `-h`, `--help` | Show all tasks |
153
+ | `<task> -h` | Help for a specific task |
154
+ | `-l`, `--list` | List tasks |
155
+ | `--version` | Show version |
156
+
134
157
  ## Context API
135
158
 
136
- Every task receives a `Context` object as the first parameter:
159
+ Every task receives a `Context` for shell execution:
137
160
 
138
161
  ```typescript
139
162
  async deploy(c: Context, env: string) {
140
- // Run shell commands
141
- await c.run("npm run build");
142
-
143
- // Capture output
144
- const { stdout } = await c.run("git rev-parse HEAD", { hide: true });
145
-
146
- // Ignore errors
147
- await c.run("rm -f temp.txt", { warn: true });
148
-
149
- // Echo command before running
150
- await c.run("npm test", { echo: true });
151
-
152
- // Change directory temporarily
153
- for await (const _ of c.cd("subdir")) {
163
+ await c.run("npm run build"); // run command
164
+ const { stdout } = await c.run("git rev-parse HEAD", { hide: true }); // capture output
165
+ await c.run("rm -f temp.txt", { warn: true }); // ignore errors
166
+ await c.run("npm test", { echo: true }); // echo before running
167
+ await c.run("make", { stream: true }); // stream output in real-time
168
+
169
+ for await (const _ of c.cd("subdir")) { // temporary cd
154
170
  await c.run("ls");
155
171
  }
156
-
157
- // Sudo
158
- await c.sudo("apt update");
159
-
160
- // Access config
161
- console.log(c.config); // { echo: false, warn: false, ... }
162
-
163
- // local() is alias for run()
164
- await c.local("echo hello");
172
+
173
+ await c.sudo("apt update"); // sudo prefix
174
+ await c.local("echo hello"); // alias for run()
165
175
  }
166
176
  ```
167
177
 
168
- ### Context Options
178
+ ### Options
169
179
 
170
180
  | Option | Type | Default | Description |
171
181
  |--------|------|---------|-------------|
172
182
  | `echo` | boolean | false | Print command before execution |
173
183
  | `warn` | boolean | false | Don't throw on non-zero exit |
174
184
  | `hide` | boolean | false | Capture output instead of printing |
175
- | `cwd` | string | process.cwd() | Working directory |
185
+ | `stream` | boolean | false | Stream output in real-time |
186
+ | `cwd` | string | `process.cwd()` | Working directory |
176
187
 
177
188
  ### RunResult
178
189
 
179
190
  ```typescript
180
191
  interface RunResult {
181
- stdout: string;
192
+ stdout: string; // captured output (empty when streaming)
182
193
  stderr: string;
183
194
  code: number;
184
- ok: boolean; // code === 0
185
- failed: boolean; // code !== 0
195
+ ok: boolean; // code === 0
196
+ failed: boolean; // code !== 0
197
+ }
198
+ ```
199
+
200
+ ### Error Handling
201
+
202
+ Failed commands throw `CommandError`:
203
+
204
+ ```typescript
205
+ import { CommandError } from "invoket/context";
206
+
207
+ try {
208
+ await c.run("exit 1");
209
+ } catch (e) {
210
+ if (e instanceof CommandError) {
211
+ console.log(e.result.code); // 1
212
+ console.log(e.result.stderr);
213
+ }
186
214
  }
187
215
  ```
188
216
 
217
+ Use `{ warn: true }` to suppress throws and inspect the result instead.
218
+
189
219
  ## Private Methods
190
220
 
191
- Methods starting with `_` are private and won't appear in help or be callable:
221
+ Prefix with `_` to hide from CLI:
192
222
 
193
223
  ```typescript
194
224
  export class Tasks {
195
- async publicTask(c: Context) { }
196
- async _privateHelper(c: Context) { } // Hidden
225
+ async publicTask(c: Context) {
226
+ this._helper();
227
+ }
228
+ async _helper() { } // not discoverable, not callable via CLI
229
+ }
230
+ ```
231
+
232
+ ## Patterns
233
+
234
+ ### Project setup
235
+
236
+ ```typescript
237
+ /** Bootstrap dev environment */
238
+ async setup(c: Context) {
239
+ await c.run("bun install");
240
+ await c.run("cp .env.example .env", { warn: true });
241
+ await c.run("bun run db:migrate");
242
+ console.log("Ready to go!");
197
243
  }
198
244
  ```
199
245
 
200
- ## Testing
246
+ ### Git workflow
247
+
248
+ ```typescript
249
+ /**
250
+ * Commit and push current branch
251
+ * @flag message -m
252
+ */
253
+ async ship(c: Context, message: string) {
254
+ const { stdout } = await c.run("git branch --show-current", { hide: true });
255
+ await c.run("git add -A");
256
+ await c.run(`git commit -m "${message}"`);
257
+ await c.run(`git push -u origin ${stdout.trim()}`);
258
+ }
259
+ ```
260
+
261
+ ### Run with fallback
262
+
263
+ ```typescript
264
+ /** Lint and fix */
265
+ async lint(c: Context) {
266
+ const result = await c.run("eslint . --fix", { warn: true, hide: true });
267
+ if (result.failed) {
268
+ console.log("Lint errors remain:");
269
+ console.log(result.stdout);
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### Capture and transform
275
+
276
+ ```typescript
277
+ /** Show outdated deps */
278
+ async deps(c: Context) {
279
+ const { stdout } = await c.run("bun outdated --json", { hide: true, warn: true });
280
+ const deps = JSON.parse(stdout || "[]");
281
+ for (const d of deps) console.log(`${d.name}: ${d.current} → ${d.latest}`);
282
+ }
283
+ ```
284
+
285
+ ### Scaffold files
286
+
287
+ ```typescript
288
+ /** @flag name -n */
289
+ async component(c: Context, name: string) {
290
+ const upper = name[0].toUpperCase() + name.slice(1);
291
+ await c.run(`mkdir -p src/components/${name}`);
292
+ await c.run(`cat > src/components/${name}/index.tsx << 'EOF'
293
+ export function ${upper}() {
294
+ return <div>${upper}</div>;
295
+ }
296
+ EOF`);
297
+ console.log(`Created src/components/${name}/index.tsx`);
298
+ }
299
+ ```
300
+
301
+ ## Agentic Tools
302
+
303
+ AI agents like Claude Code have built-in tools for searching files, reading code, and running commands. What they lack is **project context** — the state of migrations, the history of decisions, the shape of your API, which tests are flaky and why. That knowledge lives in developers' heads, scattered across commits, issues, and Slack threads.
304
+
305
+ invoket lets you build a **structured, queryable project knowledge base** that agents can read and write through the same CLI interface humans use. Bun's built-in SQLite makes this trivial — no external database, no setup, just a `.ctx.db` file that travels with the project.
306
+
307
+ ### Project context — a SQLite-backed knowledge base
308
+
309
+ ```typescript
310
+ import { Context } from "invoket/context";
311
+ import { Database } from "bun:sqlite";
312
+
313
+ class Ctx {
314
+ private db: Database;
315
+
316
+ constructor() {
317
+ this.db = new Database(".ctx.db", { create: true });
318
+ this.db.run(`CREATE TABLE IF NOT EXISTS context (
319
+ key TEXT PRIMARY KEY,
320
+ value TEXT NOT NULL,
321
+ updated_at TEXT DEFAULT (datetime('now'))
322
+ )`);
323
+ this.db.run(`CREATE TABLE IF NOT EXISTS decisions (
324
+ id INTEGER PRIMARY KEY,
325
+ subject TEXT NOT NULL,
326
+ decision TEXT NOT NULL,
327
+ rationale TEXT,
328
+ status TEXT DEFAULT 'active',
329
+ created_at TEXT DEFAULT (datetime('now'))
330
+ )`);
331
+ }
332
+
333
+ /** Store a key-value fact about the project */
334
+ async set(c: Context, key: string, ...value: string[]) {
335
+ this.db.run(
336
+ `INSERT OR REPLACE INTO context (key, value, updated_at)
337
+ VALUES (?, ?, datetime('now'))`,
338
+ [key, value.join(" ")]
339
+ );
340
+ console.log(`Set: ${key}`);
341
+ }
342
+
343
+ /** Retrieve a fact */
344
+ async get(c: Context, key: string) {
345
+ const row = this.db.query("SELECT value, updated_at FROM context WHERE key = ?").get(key) as any;
346
+ if (!row) { console.log(`Not found: ${key}`); return; }
347
+ console.log(`${row.value} (${row.updated_at})`);
348
+ }
349
+
350
+ /** Search facts by keyword */
351
+ async search(c: Context, ...terms: string[]) {
352
+ const pattern = `%${terms.join(" ")}%`;
353
+ const rows = this.db.query(
354
+ "SELECT key, value FROM context WHERE key LIKE ? OR value LIKE ?"
355
+ ).all(pattern, pattern) as any[];
356
+ for (const r of rows) console.log(`${r.key}: ${r.value}`);
357
+ }
358
+
359
+ /** Record an architectural decision */
360
+ async decide(c: Context, subject: string, decision: string, ...rationale: string[]) {
361
+ this.db.run(
362
+ "INSERT INTO decisions (subject, decision, rationale) VALUES (?, ?, ?)",
363
+ [subject, decision, rationale.join(" ")]
364
+ );
365
+ console.log(`Recorded: ${subject}`);
366
+ }
367
+
368
+ /** List active decisions */
369
+ async decisions(c: Context) {
370
+ const rows = this.db.query(
371
+ "SELECT id, subject, decision, rationale FROM decisions WHERE status = 'active' ORDER BY created_at DESC"
372
+ ).all() as any[];
373
+ for (const r of rows) {
374
+ console.log(`#${r.id} ${r.subject}: ${r.decision}`);
375
+ if (r.rationale) console.log(` ${r.rationale}`);
376
+ }
377
+ }
378
+
379
+ /** Dump all context as JSON */
380
+ async dump(c: Context) {
381
+ const facts = this.db.query("SELECT key, value FROM context ORDER BY key").all();
382
+ const decisions = this.db.query("SELECT * FROM decisions WHERE status = 'active'").all();
383
+ console.log(JSON.stringify({ facts, decisions }, null, 2));
384
+ }
385
+ }
386
+
387
+ export class Tasks {
388
+ ctx = new Ctx();
389
+ }
390
+ ```
201
391
 
202
392
  ```bash
203
- bun test
393
+ # Store project facts
394
+ invt ctx:set db "Postgres 16 on Supabase, migrations in prisma/"
395
+ invt ctx:set api "REST with /api/v2 prefix, auth via JWT middleware"
396
+ invt ctx:set deploy "Fly.io, auto-deploy on push to main"
397
+
398
+ # Record decisions with rationale
399
+ invt ctx:decide auth "JWT in httpOnly cookies" "Chose over localStorage for XSS protection"
400
+ invt ctx:decide orm "Prisma over Drizzle" "Team familiarity, existing migrations"
401
+
402
+ # Query context
403
+ invt ctx:get db
404
+ invt ctx:search auth
405
+ invt ctx:decisions
406
+
407
+ # Dump everything for agent context
408
+ invt ctx:dump
409
+ ```
410
+
411
+ An agent starts a session with `invt ctx:dump` and immediately has structured project knowledge — not flat markdown, not grep results, but queryable facts and decisions with timestamps.
412
+
413
+ ### Why SQLite?
414
+
415
+ Bun bundles SQLite natively — `import { Database } from "bun:sqlite"` just works. No dependencies, no server, no config. The `.ctx.db` file is a single file you can `.gitignore` or commit. You can extend the schema as the project grows: add tables for endpoints, test history, deployment logs, whatever your project needs.
416
+
417
+ ### Growing the schema
418
+
419
+ The example above is a starting point. A real project might track more:
420
+
421
+ ```typescript
422
+ // Track API endpoints and their status
423
+ this.db.run(`CREATE TABLE IF NOT EXISTS endpoints (
424
+ path TEXT PRIMARY KEY,
425
+ method TEXT,
426
+ handler TEXT,
427
+ auth TEXT DEFAULT 'required',
428
+ status TEXT DEFAULT 'active'
429
+ )`);
430
+
431
+ // Track test health
432
+ this.db.run(`CREATE TABLE IF NOT EXISTS test_runs (
433
+ id INTEGER PRIMARY KEY,
434
+ suite TEXT,
435
+ passed INTEGER,
436
+ failed INTEGER,
437
+ skipped INTEGER,
438
+ ran_at TEXT DEFAULT (datetime('now'))
439
+ )`);
204
440
  ```
205
441
 
442
+ The namespace class is the interface. The schema is yours to shape around what your project actually needs to remember.
443
+
206
444
  ## Requirements
207
445
 
208
446
  - Bun >= 1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "invoket",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "TypeScript task runner for Bun - uses type annotations to parse CLI arguments",
6
6
  "bin": {