invoket 0.1.7 → 0.1.8
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 +373 -101
- package/package.json +1 -1
- package/src/cli.ts +13 -594
- package/src/context.ts +12 -3
- package/src/parser.ts +560 -0
package/README.md
CHANGED
|
@@ -1,74 +1,91 @@
|
|
|
1
1
|
# invoket
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript task runner for Bun. Write typed methods, get a CLI for free.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
$ invt deploy prod --force
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
##
|
|
35
|
+
## Quick Start
|
|
30
36
|
|
|
31
|
-
Create
|
|
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
|
-
*
|
|
43
|
+
* My project tasks
|
|
43
44
|
*/
|
|
44
45
|
export class Tasks {
|
|
45
|
-
/**
|
|
46
|
-
|
|
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:
|
|
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
|
|
62
|
+
/** Install packages */
|
|
58
63
|
async install(c: Context, ...packages: string[]) {
|
|
59
64
|
for (const pkg of packages) {
|
|
60
|
-
await c.run(`
|
|
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
|
-
|
|
85
|
+
Group related tasks:
|
|
69
86
|
|
|
70
87
|
```typescript
|
|
71
|
-
class
|
|
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,383 @@ class DbNamespace {
|
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
export class Tasks {
|
|
84
|
-
db = new
|
|
101
|
+
db = new Db();
|
|
85
102
|
}
|
|
86
103
|
```
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
```bash
|
|
106
|
+
invt db:migrate up # colon separator
|
|
107
|
+
invt db.seed # dot separator also works
|
|
108
|
+
```
|
|
89
109
|
|
|
90
|
-
##
|
|
110
|
+
## Arguments
|
|
91
111
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
112
|
+
### Type Mapping
|
|
113
|
+
|
|
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`
|
|
119
|
+
| `force: boolean` | `<force>` | `true`, `1`, `false`, `0` |
|
|
98
120
|
| `params: SomeInterface` | `<params>` | `'{"key": "value"}'` |
|
|
99
|
-
| `items: string[]` | `<items>` | `'["a", "b"
|
|
100
|
-
| `...args: string[]` | `[args...]` | `a b c`
|
|
101
|
-
|
|
102
|
-
## CLI Flags
|
|
121
|
+
| `items: string[]` | `<items>` | `'["a", "b"]'` |
|
|
122
|
+
| `...args: string[]` | `[args...]` (variadic) | `a b c` |
|
|
103
123
|
|
|
104
|
-
|
|
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 |
|
|
124
|
+
### Flags
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
Every parameter automatically gets a `--long` flag. Add `@flag` annotations for short flags and aliases:
|
|
112
127
|
|
|
113
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
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`
|
|
159
|
+
Every task receives a `Context` for shell execution:
|
|
137
160
|
|
|
138
161
|
```typescript
|
|
139
162
|
async deploy(c: Context, env: string) {
|
|
140
|
-
//
|
|
141
|
-
await c.run("
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
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
|
-
//
|
|
158
|
-
await c.
|
|
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
|
-
###
|
|
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
|
-
| `
|
|
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;
|
|
185
|
-
failed: boolean;
|
|
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
|
-
|
|
221
|
+
Prefix with `_` to hide from CLI:
|
|
192
222
|
|
|
193
223
|
```typescript
|
|
194
224
|
export class Tasks {
|
|
195
|
-
async publicTask(c: Context) {
|
|
196
|
-
|
|
225
|
+
async publicTask(c: Context) {
|
|
226
|
+
this._helper();
|
|
227
|
+
}
|
|
228
|
+
async _helper() { } // not discoverable, not callable via CLI
|
|
197
229
|
}
|
|
198
230
|
```
|
|
199
231
|
|
|
200
|
-
##
|
|
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!");
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
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
|
+
invoket shines as a toolbox for AI agents. Instead of writing ad-hoc scripts each session, the agent adds methods to `tasks.ts` that persist across sessions. `invt --help` shows what tools are available. The file becomes the project's growing command centre.
|
|
304
|
+
|
|
305
|
+
### Memory — persist context across sessions
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
309
|
+
|
|
310
|
+
class Memory {
|
|
311
|
+
private dir = ".memory";
|
|
312
|
+
|
|
313
|
+
private ensure() {
|
|
314
|
+
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Store a value by key */
|
|
318
|
+
async store(c: Context, key: string, ...value: string[]) {
|
|
319
|
+
this.ensure();
|
|
320
|
+
writeFileSync(`${this.dir}/${key}.md`, value.join(" "));
|
|
321
|
+
console.log(`Stored: ${key}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Recall a value by key */
|
|
325
|
+
async recall(c: Context, key: string) {
|
|
326
|
+
const path = `${this.dir}/${key}.md`;
|
|
327
|
+
if (!existsSync(path)) { console.log(`Not found: ${key}`); return; }
|
|
328
|
+
console.log(readFileSync(path, "utf-8"));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** List all stored keys */
|
|
332
|
+
async list(c: Context) {
|
|
333
|
+
this.ensure();
|
|
334
|
+
const { stdout } = await c.run(`ls ${this.dir}`, { hide: true, warn: true });
|
|
335
|
+
console.log(stdout || "(empty)");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export class Tasks {
|
|
340
|
+
memory = new Memory();
|
|
341
|
+
}
|
|
342
|
+
```
|
|
201
343
|
|
|
202
344
|
```bash
|
|
203
|
-
|
|
345
|
+
invt memory:store arch "Monorepo with packages/api and packages/web"
|
|
346
|
+
invt memory:recall arch
|
|
347
|
+
invt memory:list
|
|
204
348
|
```
|
|
205
349
|
|
|
350
|
+
### Task planning — break work into steps
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
354
|
+
|
|
355
|
+
class Plan {
|
|
356
|
+
private file = ".plan.json";
|
|
357
|
+
|
|
358
|
+
private load(): { task: string; done: boolean }[] {
|
|
359
|
+
if (!existsSync(this.file)) return [];
|
|
360
|
+
return JSON.parse(readFileSync(this.file, "utf-8"));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private save(tasks: { task: string; done: boolean }[]) {
|
|
364
|
+
writeFileSync(this.file, JSON.stringify(tasks, null, 2));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Add a step to the plan */
|
|
368
|
+
async add(c: Context, ...task: string[]) {
|
|
369
|
+
const tasks = this.load();
|
|
370
|
+
tasks.push({ task: task.join(" "), done: false });
|
|
371
|
+
this.save(tasks);
|
|
372
|
+
console.log(`Added step ${tasks.length}: ${task.join(" ")}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Mark step as done */
|
|
376
|
+
async done(c: Context, step: number) {
|
|
377
|
+
const tasks = this.load();
|
|
378
|
+
tasks[step - 1].done = true;
|
|
379
|
+
this.save(tasks);
|
|
380
|
+
console.log(`Done: ${tasks[step - 1].task}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Show the plan */
|
|
384
|
+
async show(c: Context) {
|
|
385
|
+
const tasks = this.load();
|
|
386
|
+
if (!tasks.length) { console.log("No plan yet."); return; }
|
|
387
|
+
for (const [i, t] of tasks.entries()) {
|
|
388
|
+
console.log(`${t.done ? "✓" : " "} ${i + 1}. ${t.task}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Clear the plan */
|
|
393
|
+
async clear(c: Context) {
|
|
394
|
+
this.save([]);
|
|
395
|
+
console.log("Plan cleared.");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export class Tasks {
|
|
400
|
+
plan = new Plan();
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
invt plan:add "Set up database schema"
|
|
406
|
+
invt plan:add "Write API endpoints"
|
|
407
|
+
invt plan:add "Add tests"
|
|
408
|
+
invt plan:show
|
|
409
|
+
invt plan:done 1
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Session journal — log decisions
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { appendFileSync, existsSync, readFileSync } from "fs";
|
|
416
|
+
|
|
417
|
+
class Journal {
|
|
418
|
+
private file = ".journal.md";
|
|
419
|
+
|
|
420
|
+
/** Log a decision or finding */
|
|
421
|
+
async log(c: Context, ...entry: string[]) {
|
|
422
|
+
const ts = new Date().toISOString().slice(0, 16);
|
|
423
|
+
appendFileSync(this.file, `\n## ${ts}\n\n${entry.join(" ")}\n`);
|
|
424
|
+
console.log("Logged.");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Show recent entries */
|
|
428
|
+
async show(c: Context) {
|
|
429
|
+
if (!existsSync(this.file)) { console.log("No journal yet."); return; }
|
|
430
|
+
console.log(readFileSync(this.file, "utf-8"));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export class Tasks {
|
|
435
|
+
journal = new Journal();
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
invt journal:log "Chose Postgres over SQLite for concurrent writes"
|
|
441
|
+
invt journal:show
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Codebase search — structured context gathering
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
class Search {
|
|
448
|
+
/** Find files matching a pattern */
|
|
449
|
+
async files(c: Context, pattern: string) {
|
|
450
|
+
await c.run(`find . -name "${pattern}" -not -path "*/node_modules/*"`, { stream: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Search code for a pattern */
|
|
454
|
+
async code(c: Context, pattern: string, ...glob: string[]) {
|
|
455
|
+
const g = glob.length ? `--glob '${glob.join("' --glob '")}'` : "";
|
|
456
|
+
await c.run(`rg "${pattern}" ${g} --type-not binary`, { stream: true, warn: true });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Summarise project structure */
|
|
460
|
+
async tree(c: Context) {
|
|
461
|
+
await c.run("find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | head -50", { stream: true });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export class Tasks {
|
|
466
|
+
search = new Search();
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
invt search:code "async.*Context" "*.ts"
|
|
472
|
+
invt search:files "*.test.ts"
|
|
473
|
+
invt search:tree
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
These tasks persist in the project. Every session, the agent starts with `invt --help` and has its full toolbox ready.
|
|
477
|
+
|
|
206
478
|
## Requirements
|
|
207
479
|
|
|
208
480
|
- Bun >= 1.0.0
|