incur 0.0.2 → 0.1.0
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 +326 -10
- package/SKILL.md +34 -0
- package/package.json +5 -1
- package/src/e2e.test.ts +40 -7
- package/examples/presto/cli.ts +0 -246
- package/examples/presto/node_modules/.bin/incur.src +0 -21
- package/examples/presto/node_modules/.bin/tsx +0 -21
- package/examples/presto/package.json +0 -14
- package/examples/presto/tsconfig.json +0 -9
package/README.md
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/logo-dark.svg">
|
|
3
|
+
<source media="(prefers-color-scheme: light)" srcset=".github/logo-light.svg">
|
|
4
|
+
<img alt="incur" src=".github/logo-light.svg" width="100%" height="140px">
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
7
|
+
<br/>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="#features">Features</a> · <a href="#quickprompt">Quickprompt</a> · <a href="#install">Install</a> · <a href="#usage">Usage</a> · <a href="#walkthrough">Walkthrough</a> · <a href="#license">License</a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- [**Agent discovery**](#agent-discovery): built-in Skills and MCP sync (`skills add`, `mcp add`) so agents find your CLI automatically
|
|
16
|
+
- [**Session savings**](#session-savings): up to **3× fewer tokens** per session vs. MCP or skill alternatives
|
|
17
|
+
- [**Call-to-actions**](#call-to-actions): suggest next commands to agents and humans after a run
|
|
18
|
+
- [**TOON output**](#toon-output): token-efficient default format that agents parse easily, with JSON, YAML, Markdown, and JSONL alternatives
|
|
19
|
+
- [**`--llms` flag**](#agent-discovery): token-efficient command manifest in Markdown or JSON schema
|
|
20
|
+
- [**Well-formed I/O**](#well-formed-io): Schemas schemas for arguments, options, environment variables, and output
|
|
21
|
+
- [**Inferred types**](#inferred-types): generic type flow from schemas to `run` callbacks with zero manual annotations
|
|
22
|
+
- [**Global options**](#global-options): `--format`, `--json`, `--verbose`, `--help`, `--version` on every CLI for free
|
|
23
|
+
- [**Light API surface**](#light-api-surface): `Cli.create()`, `.command()`, `.serve()` – that's it
|
|
4
24
|
|
|
5
25
|
## Quickprompt
|
|
6
26
|
|
|
7
27
|
Prompt your agent:
|
|
8
28
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
**Skills (recommended – lighter on tokens)**
|
|
30
|
+
|
|
31
|
+
```txt
|
|
32
|
+
Run `npx incur skills add`, then show me how to build CLIs with incur.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**MCP**
|
|
12
36
|
|
|
13
|
-
|
|
14
|
-
|
|
37
|
+
```txt
|
|
38
|
+
Run `npx incur mcp add`, then show me how to build CLIs with incur.
|
|
15
39
|
```
|
|
16
40
|
|
|
17
41
|
## Install
|
|
@@ -32,6 +56,8 @@ bun i incur
|
|
|
32
56
|
|
|
33
57
|
### Single-command CLI
|
|
34
58
|
|
|
59
|
+
Pass `run` directly to `Cli.create()` for CLIs that do one thing.
|
|
60
|
+
|
|
35
61
|
```ts
|
|
36
62
|
import { Cli, z } from 'incur'
|
|
37
63
|
|
|
@@ -51,8 +77,32 @@ $ greet world
|
|
|
51
77
|
# → message: hello world
|
|
52
78
|
```
|
|
53
79
|
|
|
80
|
+
```sh
|
|
81
|
+
$ greet --help
|
|
82
|
+
# greet – A greeting CLI
|
|
83
|
+
#
|
|
84
|
+
# Usage: greet <name>
|
|
85
|
+
#
|
|
86
|
+
# Arguments:
|
|
87
|
+
# name Name to greet
|
|
88
|
+
#
|
|
89
|
+
# Built-in Commands:
|
|
90
|
+
# mcp add Register as an MCP server
|
|
91
|
+
# skills add Sync skill files to your agent
|
|
92
|
+
#
|
|
93
|
+
# Global Options:
|
|
94
|
+
# --format <toon|json|yaml|md|jsonl> Output format
|
|
95
|
+
# --help Show help
|
|
96
|
+
# --llms Print LLM-readable manifest
|
|
97
|
+
# --mcp Start as MCP stdio server
|
|
98
|
+
# --verbose Show full output envelope
|
|
99
|
+
# --version Show version
|
|
100
|
+
```
|
|
101
|
+
|
|
54
102
|
### Multi-command CLI
|
|
55
103
|
|
|
104
|
+
Chain `.command()` calls to register subcommands.
|
|
105
|
+
|
|
56
106
|
```ts
|
|
57
107
|
import { Cli, z } from 'incur'
|
|
58
108
|
|
|
@@ -90,8 +140,33 @@ $ my-cli install express -D
|
|
|
90
140
|
# → packages: 451
|
|
91
141
|
```
|
|
92
142
|
|
|
143
|
+
```sh
|
|
144
|
+
$ my-cli --help
|
|
145
|
+
# my-cli – My CLI
|
|
146
|
+
#
|
|
147
|
+
# Usage: my-cli <command>
|
|
148
|
+
#
|
|
149
|
+
# Commands:
|
|
150
|
+
# install Install a package
|
|
151
|
+
# status Show repo status
|
|
152
|
+
#
|
|
153
|
+
# Built-in Commands:
|
|
154
|
+
# mcp add Register as an MCP server
|
|
155
|
+
# skills add Sync skill files to your agent
|
|
156
|
+
#
|
|
157
|
+
# Global Options:
|
|
158
|
+
# --format <toon|json|yaml|md|jsonl> Output format
|
|
159
|
+
# --help Show help
|
|
160
|
+
# --llms Print LLM-readable manifest
|
|
161
|
+
# --mcp Start as MCP stdio server
|
|
162
|
+
# --verbose Show full output envelope
|
|
163
|
+
# --version Show version
|
|
164
|
+
```
|
|
165
|
+
|
|
93
166
|
### Sub-command CLI
|
|
94
167
|
|
|
168
|
+
Create a separate `Cli` and mount it with `.command(cli)` to nest command groups.
|
|
169
|
+
|
|
95
170
|
```ts
|
|
96
171
|
const cli = Cli.create('my-cli', { description: 'My CLI' })
|
|
97
172
|
|
|
@@ -117,12 +192,36 @@ $ my-cli pr list --state closed
|
|
|
117
192
|
# → state: closed
|
|
118
193
|
```
|
|
119
194
|
|
|
195
|
+
```sh
|
|
196
|
+
$ my-cli --help
|
|
197
|
+
# my-cli – My CLI
|
|
198
|
+
#
|
|
199
|
+
# Usage: my-cli <command>
|
|
200
|
+
#
|
|
201
|
+
# Commands:
|
|
202
|
+
# pr Pull request commands
|
|
203
|
+
#
|
|
204
|
+
# Built-in Commands:
|
|
205
|
+
# mcp add Register as an MCP server
|
|
206
|
+
# skills add Sync skill files to your agent
|
|
207
|
+
#
|
|
208
|
+
# Global Options:
|
|
209
|
+
# --format <toon|json|yaml|md|jsonl> Output format
|
|
210
|
+
# --help Show help
|
|
211
|
+
# --llms Print LLM-readable manifest
|
|
212
|
+
# --mcp Start as MCP stdio server
|
|
213
|
+
# --verbose Show full output envelope
|
|
214
|
+
# --version Show version
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Walkthrough
|
|
218
|
+
|
|
120
219
|
### Agent discovery
|
|
121
220
|
|
|
122
|
-
|
|
221
|
+
Agents can only use your CLI if they know it exists. incur solves this with three built-in discovery mechanisms – no manual config, no copy-pasting tool definitions:
|
|
123
222
|
|
|
124
223
|
```sh
|
|
125
|
-
# Auto-generate and install agent skill files (recommended
|
|
224
|
+
# Auto-generate and install agent skill files (recommended – lighter on tokens)
|
|
126
225
|
my-cli skills add
|
|
127
226
|
|
|
128
227
|
# Register as an MCP server for your agents
|
|
@@ -132,6 +231,223 @@ my-cli mcp add
|
|
|
132
231
|
my-cli --llms
|
|
133
232
|
```
|
|
134
233
|
|
|
234
|
+
### Session savings
|
|
235
|
+
|
|
236
|
+
Most CLIs expose tools via MCP or a single monolithic skill file. incur combines on-demand skill loading with TOON output to cut token usage across the entire session – from discovery through invocation and response.
|
|
237
|
+
|
|
238
|
+
The table below models a session with a 20-command CLI producing verbose output.
|
|
239
|
+
|
|
240
|
+
- **Session start** – tokens consumed just by having the tool available. _MCP injects all tool schemas into every turn; skills only load frontmatter (name + description)._
|
|
241
|
+
- **Discovery** – tokens to learn what commands exist and how to call them. _MCP gets this at session start; skills load the full skill file on demand; incur splits by command group so only relevant commands are loaded._
|
|
242
|
+
- **Invocation (×5)** – tokens per tool call.
|
|
243
|
+
- **Response (×5)** – tokens in CLI output. _MCP and skills return JSON; incur defaults to TOON which strips braces, quotes, and keys._
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
┌─────────────────┬────────────┬──────────────────┬─────────┬───────────────┐
|
|
247
|
+
│ │ MCP + JSON │ One Skill + JSON │ incur │ vs. incur │
|
|
248
|
+
├─────────────────┼────────────┼──────────────────┼─────────┼───────────────┤
|
|
249
|
+
│ Session start │ 6,747 │ 624 │ 805 │ ↓8.4× │
|
|
250
|
+
│ Discovery │ 0 │ 11,489 │ 387 │ ↓29.7× │
|
|
251
|
+
│ Invocation (×5) │ 110 │ 65 │ 65 │ ↓1.7× │
|
|
252
|
+
│ Response (×5) │ 10,940 │ 10,800 │ 5,790 │ ↓1.9× │
|
|
253
|
+
├─────────────────┼────────────┼──────────────────┼─────────┼───────────────┤
|
|
254
|
+
│ Cost │ $0.0325 │ $0.0410 │ $0.0131 │ ↓3.1× │
|
|
255
|
+
└─────────────────┴────────────┴──────────────────┴─────────┴───────────────┘
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Call-to-actions
|
|
259
|
+
|
|
260
|
+
Without CTAs, agents have to guess what to do next or ask the user. With CTAs, your CLI tells the agent exactly which commands are relevant after each run, so it can chain operations without extra prompting.
|
|
261
|
+
|
|
262
|
+
Return CTAs from `ok()` or `error()` to suggest next steps. `cta` parameters are also fully type-inferred, so agents get valid command names, arguments, and options for free.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
cli.command('list', {
|
|
266
|
+
args: z.object({ state: z.enum(['open', 'closed']).default('open') }),
|
|
267
|
+
run({ args, ok }) {
|
|
268
|
+
const items = [{ id: 1, title: 'Fix bug' }]
|
|
269
|
+
return ok({ items }, {
|
|
270
|
+
cta: {
|
|
271
|
+
commands: [
|
|
272
|
+
{ command: 'get 1', description: 'View item' },
|
|
273
|
+
{ command: 'list', args: { state: 'closed' }, description: 'View closed' },
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```sh
|
|
282
|
+
$ my-cli list
|
|
283
|
+
# → items:
|
|
284
|
+
# → - id: 1
|
|
285
|
+
# → title: Fix bug
|
|
286
|
+
# Next:
|
|
287
|
+
# my-cli get 1 – View item
|
|
288
|
+
# my-cli list closed – View closed
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Light API surface
|
|
292
|
+
|
|
293
|
+
A small API means agents can build entire CLIs in a single pass without needing to learn framework abstractions. Three functions: `create`, `command`, `serve`, and everything else (parsing, help, validation, output formatting, agent discovery) is handled automatically:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { Cli, z } from 'incur'
|
|
297
|
+
|
|
298
|
+
// Define sub-command groups
|
|
299
|
+
const db = Cli.create('db', { description: 'Database commands' })
|
|
300
|
+
.command('migrate', { description: 'Run migrations', run: () => ({ migrated: true }) })
|
|
301
|
+
|
|
302
|
+
// Create the root CLI
|
|
303
|
+
Cli.create('tool', { description: 'A tool' })
|
|
304
|
+
// Register commands
|
|
305
|
+
.command('run', { description: 'Run a task', run: () => ({ ok: true }) })
|
|
306
|
+
// Mount sub-command groups
|
|
307
|
+
.command(db)
|
|
308
|
+
// Serve the CLI
|
|
309
|
+
.serve()
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
$ tool --help
|
|
314
|
+
# Usage: tool <command>
|
|
315
|
+
#
|
|
316
|
+
# Commands:
|
|
317
|
+
# run Run a task
|
|
318
|
+
# db Database commands
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### TOON output
|
|
322
|
+
|
|
323
|
+
Every token an agent spends reading CLI output is a token it can’t spend reasoning. incur defaults to [TOON](https://github.com/toon-format/toon) – a format that’s as readable as YAML but with no quoting, no braces, and no redundant syntax. Agents parse it easily and use up to **60% fewer tokens compared to JSON**.
|
|
324
|
+
|
|
325
|
+
```sh
|
|
326
|
+
$ my-cli hikes --location Boulder --season spring_2025
|
|
327
|
+
# → context:
|
|
328
|
+
# → task: Our favorite hikes together
|
|
329
|
+
# → location: Boulder
|
|
330
|
+
# → season: spring_2025
|
|
331
|
+
# → friends[3]: ana,luis,sam
|
|
332
|
+
# → hikes[3]{id,name,distanceKm,elevationGain,companion,wasSunny}:
|
|
333
|
+
# → 1,Blue Lake Trail,7.5,320,ana,true
|
|
334
|
+
# → 2,Ridge Overlook,9.2,540,luis,false
|
|
335
|
+
# → 3,Wildflower Loop,5.1,180,sam,true
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Switch formats with `--format` or `--json`:
|
|
339
|
+
|
|
340
|
+
```sh
|
|
341
|
+
$ my-cli status --format json
|
|
342
|
+
# → {
|
|
343
|
+
# → "context": {
|
|
344
|
+
# → "task": "Our favorite hikes together",
|
|
345
|
+
# → "location": "Boulder",
|
|
346
|
+
# → "season": "spring_2025"
|
|
347
|
+
# → },
|
|
348
|
+
# → "friends": ["ana", "luis", "sam"],
|
|
349
|
+
# → "hikes": [
|
|
350
|
+
# → ... + 1000 more tokens
|
|
351
|
+
# → ]
|
|
352
|
+
# → }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Supported formats: `toon`, `json`, `yaml`, `md`, `jsonl`.
|
|
356
|
+
|
|
357
|
+
### Well-formed I/O
|
|
358
|
+
|
|
359
|
+
Agents fail when they guess at argument formats or misinterpret output structure. incur eliminates this by declaring schemas for arguments, options, environment variables, and output – every input is validated before `run` executes, and every output has a known shape that agents can rely on without parsing heuristics:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
cli.command('deploy', {
|
|
363
|
+
args: z.object({ env: z.enum(['staging', 'production']) }),
|
|
364
|
+
options: z.object({ force: z.boolean().optional() }),
|
|
365
|
+
env: z.object({ DEPLOY_TOKEN: z.string() }),
|
|
366
|
+
output: z.object({ url: z.string(), duration: z.number() }),
|
|
367
|
+
run({ args, options, env }) {
|
|
368
|
+
return { url: `https://${args.env}.example.com`, duration: 3.2 }
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Streaming
|
|
374
|
+
|
|
375
|
+
Use `async *run` to stream chunks incrementally. Yield objects for structured data or plain strings for text:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
cli.command('logs', {
|
|
379
|
+
description: 'Tail logs',
|
|
380
|
+
async *run() {
|
|
381
|
+
yield 'connecting...'
|
|
382
|
+
yield 'streaming logs'
|
|
383
|
+
yield 'done'
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
```sh
|
|
389
|
+
$ my-cli logs
|
|
390
|
+
# → connecting...
|
|
391
|
+
# → streaming logs
|
|
392
|
+
# → done
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Each yielded value is written as a line in human/TOON mode. With `--format jsonl`, each chunk becomes `{"type":"chunk","data":"..."}`. You can also yield objects:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
async *run() {
|
|
399
|
+
yield { progress: 50 }
|
|
400
|
+
yield { progress: 100 }
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Use `ok()` or `error()` as the return value to attach CTAs or signal failure:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
async *run({ ok }) {
|
|
408
|
+
yield { step: 1 }
|
|
409
|
+
yield { step: 2 }
|
|
410
|
+
return ok(undefined, { cta: { commands: ['status'] } })
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Inferred types
|
|
415
|
+
|
|
416
|
+
Type safety isn’t just for humans – agents building CLIs with incur get immediate feedback when they pass the wrong argument type or return the wrong shape. Schemas flow through generics so `run` callbacks, `output`, and `cta` commands are all fully inferred with zero manual annotations:
|
|
417
|
+
|
|
418
|
+
```ts twoslash
|
|
419
|
+
cli.command('greet', {
|
|
420
|
+
args: z.object({ name: z.string() }),
|
|
421
|
+
options: z.object({ loud: z.boolean().default(false) }),
|
|
422
|
+
output: z.object({ message: z.string() }),
|
|
423
|
+
run({ args, options, ok }) {
|
|
424
|
+
args.name
|
|
425
|
+
// ^? (property) name: string
|
|
426
|
+
options.loud
|
|
427
|
+
// ^? (property) loud: boolean
|
|
428
|
+
return ok({ message: `hello ${args.name}` }, {
|
|
429
|
+
// ^? (property) message: string
|
|
430
|
+
cta: { commands: ['greet world'] },
|
|
431
|
+
// ^? 'greet' | 'other-cmd'
|
|
432
|
+
})
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Global options
|
|
438
|
+
|
|
439
|
+
Every incur CLI includes these flags automatically:
|
|
440
|
+
|
|
441
|
+
| Flag | Description |
|
|
442
|
+
| ---------------- | -------------------------------------------- |
|
|
443
|
+
| `--help`, `-h` | Show help for the CLI or a specific command |
|
|
444
|
+
| `--version` | Print CLI version |
|
|
445
|
+
| `--llms` | Output agent-readable command manifest |
|
|
446
|
+
| `--mcp` | Start as an MCP stdio server |
|
|
447
|
+
| `--json` | Shorthand for `--format json` |
|
|
448
|
+
| `--format <fmt>` | Output format: `toon`, `json`, `yaml`, `md` |
|
|
449
|
+
| `--verbose` | Include full envelope (`ok`, `data`, `meta`) |
|
|
450
|
+
|
|
135
451
|
## API Reference
|
|
136
452
|
|
|
137
453
|
> TODO
|
package/SKILL.md
CHANGED
|
@@ -582,6 +582,40 @@ await cli.serve(['install', 'express', '--json'], {
|
|
|
582
582
|
| `exit` | `(code: number) => void` | Override exit handler |
|
|
583
583
|
| `env` | `Record<string, string \| undefined>` | Override environment variables |
|
|
584
584
|
|
|
585
|
+
## Streaming
|
|
586
|
+
|
|
587
|
+
Use `async *run` to stream chunks incrementally. Yield objects for structured data or plain strings for text:
|
|
588
|
+
|
|
589
|
+
```ts
|
|
590
|
+
cli.command('logs', {
|
|
591
|
+
description: 'Tail logs',
|
|
592
|
+
async *run() {
|
|
593
|
+
yield 'connecting...'
|
|
594
|
+
yield 'streaming logs'
|
|
595
|
+
yield 'done'
|
|
596
|
+
},
|
|
597
|
+
})
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Each yielded value is written as a line in human/TOON mode. With `--format jsonl`, each chunk becomes `{"type":"chunk","data":"..."}`. You can also yield objects:
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
async *run() {
|
|
604
|
+
yield { progress: 50 }
|
|
605
|
+
yield { progress: 100 }
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Use `ok()` or `error()` as the return value to attach CTAs or signal failure:
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
async *run({ ok }) {
|
|
613
|
+
yield { step: 1 }
|
|
614
|
+
yield { step: 2 }
|
|
615
|
+
return ok(undefined, { cta: { commands: ['status'] } })
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
585
619
|
## Type Generation
|
|
586
620
|
|
|
587
621
|
Generate type definitions for your CLI's command map to get typed CTAs:
|
package/package.json
CHANGED
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
"[!start-pkg]": "",
|
|
13
13
|
"name": "incur",
|
|
14
14
|
"type": "module",
|
|
15
|
-
"version": "0.0
|
|
15
|
+
"version": "0.1.0",
|
|
16
16
|
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/wevm/incur"
|
|
20
|
+
},
|
|
17
21
|
"files": [
|
|
18
22
|
"examples",
|
|
19
23
|
"dist",
|
package/src/e2e.test.ts
CHANGED
|
@@ -282,7 +282,7 @@ describe('output formats', () => {
|
|
|
282
282
|
},
|
|
283
283
|
"meta": {
|
|
284
284
|
"command": "ping",
|
|
285
|
-
"duration": "
|
|
285
|
+
"duration": "<stripped>",
|
|
286
286
|
},
|
|
287
287
|
"ok": true,
|
|
288
288
|
}
|
|
@@ -308,7 +308,7 @@ describe('output formats', () => {
|
|
|
308
308
|
},
|
|
309
309
|
"meta": {
|
|
310
310
|
"command": "project deploy status",
|
|
311
|
-
"duration": "
|
|
311
|
+
"duration": "<stripped>",
|
|
312
312
|
},
|
|
313
313
|
"ok": true,
|
|
314
314
|
}
|
|
@@ -364,7 +364,7 @@ describe('error handling', () => {
|
|
|
364
364
|
],
|
|
365
365
|
"description": "Suggested commands:",
|
|
366
366
|
},
|
|
367
|
-
"duration": "
|
|
367
|
+
"duration": "<stripped>",
|
|
368
368
|
},
|
|
369
369
|
"ok": false,
|
|
370
370
|
}
|
|
@@ -413,7 +413,7 @@ describe('error handling', () => {
|
|
|
413
413
|
},
|
|
414
414
|
"meta": {
|
|
415
415
|
"command": "nonexistent",
|
|
416
|
-
"duration": "
|
|
416
|
+
"duration": "<stripped>",
|
|
417
417
|
},
|
|
418
418
|
"ok": false,
|
|
419
419
|
}
|
|
@@ -575,13 +575,33 @@ describe('streaming', () => {
|
|
|
575
575
|
],
|
|
576
576
|
"meta": {
|
|
577
577
|
"command": "stream",
|
|
578
|
-
"duration": "
|
|
578
|
+
"duration": "<stripped>",
|
|
579
579
|
},
|
|
580
580
|
"ok": true,
|
|
581
581
|
}
|
|
582
582
|
`)
|
|
583
583
|
})
|
|
584
584
|
|
|
585
|
+
test('plain text streams as lines', async () => {
|
|
586
|
+
const { output } = await serve(createApp(), ['stream-text'])
|
|
587
|
+
expect(output).toMatchInlineSnapshot(`
|
|
588
|
+
"hello
|
|
589
|
+
world
|
|
590
|
+
"
|
|
591
|
+
`)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
test('plain text streams as jsonl chunks', async () => {
|
|
595
|
+
const { output } = await serve(createApp(), ['stream-text', '--format', 'jsonl'])
|
|
596
|
+
const lines = output
|
|
597
|
+
.trim()
|
|
598
|
+
.split('\n')
|
|
599
|
+
.map((l) => JSON.parse(l))
|
|
600
|
+
expect(lines[0]).toEqual({ type: 'chunk', data: 'hello' })
|
|
601
|
+
expect(lines[1]).toEqual({ type: 'chunk', data: 'world' })
|
|
602
|
+
expect(lines[2].type).toBe('done')
|
|
603
|
+
})
|
|
604
|
+
|
|
585
605
|
test('--format jsonl explicit', async () => {
|
|
586
606
|
const { output } = await serve(createApp(), ['stream', '--format', 'jsonl'])
|
|
587
607
|
const lines = output
|
|
@@ -697,6 +717,7 @@ describe('help', () => {
|
|
|
697
717
|
stream Stream chunks
|
|
698
718
|
stream-error Stream with mid-stream error
|
|
699
719
|
stream-ok Stream with ok() return
|
|
720
|
+
stream-text Stream plain text
|
|
700
721
|
stream-throw Stream that throws
|
|
701
722
|
validate-fail Fails validation
|
|
702
723
|
|
|
@@ -861,6 +882,7 @@ describe('--llms', () => {
|
|
|
861
882
|
"stream",
|
|
862
883
|
"stream-error",
|
|
863
884
|
"stream-ok",
|
|
885
|
+
"stream-text",
|
|
864
886
|
"stream-throw",
|
|
865
887
|
"validate-fail",
|
|
866
888
|
]
|
|
@@ -1059,6 +1081,7 @@ describe('typegen', () => {
|
|
|
1059
1081
|
'stream': { args: {}; options: {} }
|
|
1060
1082
|
'stream-error': { args: {}; options: {} }
|
|
1061
1083
|
'stream-ok': { args: {}; options: {} }
|
|
1084
|
+
'stream-text': { args: {}; options: {} }
|
|
1062
1085
|
'stream-throw': { args: {}; options: {} }
|
|
1063
1086
|
'validate-fail': { args: { email: string; age: number }; options: {} }
|
|
1064
1087
|
}
|
|
@@ -1238,7 +1261,7 @@ describe('edge cases', () => {
|
|
|
1238
1261
|
},
|
|
1239
1262
|
"meta": {
|
|
1240
1263
|
"command": "project deploy create",
|
|
1241
|
-
"duration": "
|
|
1264
|
+
"duration": "<stripped>",
|
|
1242
1265
|
},
|
|
1243
1266
|
"ok": true,
|
|
1244
1267
|
}
|
|
@@ -1391,7 +1414,9 @@ async function serve(
|
|
|
1391
1414
|
...options,
|
|
1392
1415
|
})
|
|
1393
1416
|
return {
|
|
1394
|
-
output: output
|
|
1417
|
+
output: output
|
|
1418
|
+
.replace(/duration: \d+ms/g, 'duration: <stripped>')
|
|
1419
|
+
.replace(/"duration": "\d+ms"/g, '"duration": "<stripped>"'),
|
|
1395
1420
|
exitCode,
|
|
1396
1421
|
}
|
|
1397
1422
|
}
|
|
@@ -1677,6 +1702,14 @@ function createApp() {
|
|
|
1677
1702
|
},
|
|
1678
1703
|
})
|
|
1679
1704
|
|
|
1705
|
+
cli.command('stream-text', {
|
|
1706
|
+
description: 'Stream plain text',
|
|
1707
|
+
async *run() {
|
|
1708
|
+
yield 'hello'
|
|
1709
|
+
yield 'world'
|
|
1710
|
+
},
|
|
1711
|
+
})
|
|
1712
|
+
|
|
1680
1713
|
cli.command('stream-ok', {
|
|
1681
1714
|
description: 'Stream with ok() return',
|
|
1682
1715
|
async *run({ ok }) {
|
package/examples/presto/cli.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { Cli, z } from 'incur'
|
|
2
|
-
|
|
3
|
-
const cli = Cli.create('presto', {
|
|
4
|
-
version: '0.4.1',
|
|
5
|
-
description: 'A command-line HTTP client with built-in MPP payment support.',
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
// query (root-level URL request) — modeled as an explicit subcommand
|
|
9
|
-
cli.command('query', {
|
|
10
|
-
description: 'Make an HTTP request with optional payment',
|
|
11
|
-
args: z.object({
|
|
12
|
-
url: z.string().describe('URL to request'),
|
|
13
|
-
}),
|
|
14
|
-
options: z.object({
|
|
15
|
-
dryRun: z.boolean().optional().describe('Show what would be paid without executing'),
|
|
16
|
-
method: z.string().optional().describe('Custom request method (GET, POST, PUT, DELETE, ...)'),
|
|
17
|
-
header: z
|
|
18
|
-
.array(z.string())
|
|
19
|
-
.optional()
|
|
20
|
-
.describe("Add custom header (e.g. 'Accept: text/plain')"),
|
|
21
|
-
data: z.array(z.string()).optional().describe('POST data (use @filename or @- for stdin)'),
|
|
22
|
-
json: z.string().optional().describe('Send JSON data with Content-Type header'),
|
|
23
|
-
include: z.boolean().optional().describe('Include HTTP response headers in output'),
|
|
24
|
-
output: z.string().optional().describe('Write output to file'),
|
|
25
|
-
timeout: z.number().optional().describe('Maximum time for the request in seconds'),
|
|
26
|
-
noRedirect: z.boolean().optional().describe('Disable following redirects'),
|
|
27
|
-
network: z.string().optional().describe('Restrict to a specific network'),
|
|
28
|
-
}),
|
|
29
|
-
alias: {
|
|
30
|
-
method: 'X',
|
|
31
|
-
header: 'H',
|
|
32
|
-
data: 'd',
|
|
33
|
-
include: 'i',
|
|
34
|
-
output: 'o',
|
|
35
|
-
timeout: 'm',
|
|
36
|
-
network: 'n',
|
|
37
|
-
},
|
|
38
|
-
examples: [
|
|
39
|
-
{ args: { url: 'https://api.example.com/data' }, description: 'Simple GET' },
|
|
40
|
-
{
|
|
41
|
-
args: { url: 'https://api.example.com/data' },
|
|
42
|
-
options: { method: 'POST', json: '{"key":"value"}' },
|
|
43
|
-
description: 'POST with JSON body',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
args: { url: 'https://api.example.com/data' },
|
|
47
|
-
options: { dryRun: true },
|
|
48
|
-
description: 'Preview payment without executing',
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
run({ error }) {
|
|
52
|
-
const loggedIn = true
|
|
53
|
-
if (!loggedIn)
|
|
54
|
-
return error({
|
|
55
|
-
code: 'NOT_AUTHENTICATED',
|
|
56
|
-
message: 'No wallet connected. Log in first.',
|
|
57
|
-
retryable: true,
|
|
58
|
-
cta: {
|
|
59
|
-
description: 'To authenticate:',
|
|
60
|
-
commands: [{ command: 'login', description: 'Sign up or log in to your Tempo wallet' }],
|
|
61
|
-
},
|
|
62
|
-
})
|
|
63
|
-
return {
|
|
64
|
-
id: 'chatcmpl-abc123',
|
|
65
|
-
object: 'chat.completion',
|
|
66
|
-
model: 'gpt-4o-mini',
|
|
67
|
-
choices: [
|
|
68
|
-
{
|
|
69
|
-
index: 0,
|
|
70
|
-
message: { role: 'assistant', content: 'Hello! How can I help you today?' },
|
|
71
|
-
finish_reason: 'stop',
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
usage: { prompt_tokens: 8, completion_tokens: 9, total_tokens: 17 },
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
cli.command('login', {
|
|
80
|
-
description: 'Sign up or log in to your Tempo wallet',
|
|
81
|
-
run({ ok }) {
|
|
82
|
-
return ok(
|
|
83
|
-
{ status: 'logged_in' },
|
|
84
|
-
{
|
|
85
|
-
cta: {
|
|
86
|
-
description: 'Next steps:',
|
|
87
|
-
commands: [
|
|
88
|
-
{ command: 'whoami', description: 'Check your wallet address and balances' },
|
|
89
|
-
{
|
|
90
|
-
command: 'query',
|
|
91
|
-
args: { url: 'https://api.example.com/data' },
|
|
92
|
-
description: 'Make your first paid request',
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
)
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
cli.command('logout', {
|
|
102
|
-
description: 'Log out and disconnect your wallet',
|
|
103
|
-
options: z.object({
|
|
104
|
-
yes: z.boolean().optional().describe('Skip confirmation prompt'),
|
|
105
|
-
}),
|
|
106
|
-
run() {
|
|
107
|
-
return { status: 'logged_out' }
|
|
108
|
-
},
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
cli.command('whoami', {
|
|
112
|
-
description: 'Show wallet address, balances, and access keys',
|
|
113
|
-
output: z.object({
|
|
114
|
-
address: z.string(),
|
|
115
|
-
balances: z.array(
|
|
116
|
-
z.object({
|
|
117
|
-
network: z.string(),
|
|
118
|
-
amount: z.string(),
|
|
119
|
-
}),
|
|
120
|
-
),
|
|
121
|
-
}),
|
|
122
|
-
run() {
|
|
123
|
-
return {
|
|
124
|
-
address: '0x1234...abcd',
|
|
125
|
-
balances: [{ network: 'tempo', amount: '100.00 TEMPO' }],
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
// session subcommand group
|
|
131
|
-
const session = Cli.create('session', { description: 'Manage payment sessions' })
|
|
132
|
-
|
|
133
|
-
session.command('list', {
|
|
134
|
-
description: 'List active payment sessions',
|
|
135
|
-
options: z.object({
|
|
136
|
-
all: z.boolean().optional().describe('Show all channels: active, orphaned, and closing'),
|
|
137
|
-
orphaned: z.boolean().optional().describe('Scan on-chain for orphaned channels'),
|
|
138
|
-
closed: z.boolean().optional().describe('Show channels pending finalization'),
|
|
139
|
-
network: z.string().optional().describe('Filter by network'),
|
|
140
|
-
}),
|
|
141
|
-
run() {
|
|
142
|
-
return { sessions: [] }
|
|
143
|
-
},
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
session.command('close', {
|
|
147
|
-
description: 'Close a payment session',
|
|
148
|
-
args: z.object({
|
|
149
|
-
url: z.string().optional().describe('URL, origin, or channel ID (0x...) to close'),
|
|
150
|
-
}),
|
|
151
|
-
options: z.object({
|
|
152
|
-
all: z.boolean().optional().describe('Close all active sessions'),
|
|
153
|
-
orphaned: z.boolean().optional().describe('Close only orphaned on-chain channels'),
|
|
154
|
-
closed: z.boolean().optional().describe('Finalize channels pending close'),
|
|
155
|
-
}),
|
|
156
|
-
run({ ok }) {
|
|
157
|
-
return ok(
|
|
158
|
-
{ closed: true },
|
|
159
|
-
{
|
|
160
|
-
cta: {
|
|
161
|
-
description: 'Suggested commands:',
|
|
162
|
-
commands: [
|
|
163
|
-
{ command: 'session list', description: 'View remaining sessions' },
|
|
164
|
-
{ command: 'whoami', description: 'Check updated balances' },
|
|
165
|
-
],
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
)
|
|
169
|
-
},
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
session.command('recover', {
|
|
173
|
-
description: 'Recover a session from on-chain state',
|
|
174
|
-
run() {
|
|
175
|
-
return { recovered: true }
|
|
176
|
-
},
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
cli.command(session)
|
|
180
|
-
|
|
181
|
-
// key subcommand group
|
|
182
|
-
const key = Cli.create('key', { description: 'Manage access keys' })
|
|
183
|
-
|
|
184
|
-
key.command('list', {
|
|
185
|
-
description: 'List all access keys and their spending limits',
|
|
186
|
-
run() {
|
|
187
|
-
return { keys: [] }
|
|
188
|
-
},
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
key.command('create', {
|
|
192
|
-
description: 'Create a new access key for a local wallet',
|
|
193
|
-
options: z.object({
|
|
194
|
-
name: z.string().optional().describe('Wallet name'),
|
|
195
|
-
}),
|
|
196
|
-
run() {
|
|
197
|
-
return { created: true }
|
|
198
|
-
},
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
cli.command(key)
|
|
202
|
-
|
|
203
|
-
// wallet subcommand group
|
|
204
|
-
const wallet = Cli.create('wallet', { description: 'Manage wallets' })
|
|
205
|
-
|
|
206
|
-
wallet.command('create', {
|
|
207
|
-
description: 'Create a new wallet',
|
|
208
|
-
options: z.object({
|
|
209
|
-
name: z.string().optional().describe('Name for the wallet'),
|
|
210
|
-
passkey: z.boolean().optional().describe('Create a passkey-based wallet via browser auth'),
|
|
211
|
-
}),
|
|
212
|
-
run() {
|
|
213
|
-
return { created: true }
|
|
214
|
-
},
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
wallet.command('import', {
|
|
218
|
-
description: 'Import an existing private key as a local wallet',
|
|
219
|
-
options: z.object({
|
|
220
|
-
name: z.string().optional().describe('Name for the wallet'),
|
|
221
|
-
stdinKey: z.boolean().optional().describe('Read the private key from stdin'),
|
|
222
|
-
}),
|
|
223
|
-
run() {
|
|
224
|
-
return { imported: true }
|
|
225
|
-
},
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
wallet.command('delete', {
|
|
229
|
-
description: 'Delete a wallet',
|
|
230
|
-
args: z.object({
|
|
231
|
-
name: z.string().optional().describe('Wallet name to delete'),
|
|
232
|
-
}),
|
|
233
|
-
options: z.object({
|
|
234
|
-
passkey: z.boolean().optional().describe('Delete the passkey wallet'),
|
|
235
|
-
yes: z.boolean().optional().describe('Skip confirmation prompt'),
|
|
236
|
-
}),
|
|
237
|
-
run() {
|
|
238
|
-
return { deleted: true }
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
cli.command(wallet)
|
|
243
|
-
|
|
244
|
-
cli.serve()
|
|
245
|
-
|
|
246
|
-
export default cli
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/incur/incur/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/incur/incur/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../incur/src/bin.ts" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../incur/src/bin.ts" "$@"
|
|
21
|
-
fi
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/incur/incur/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/node_modules:/home/runner/work/incur/incur/node_modules/.pnpm/tsx@4.21.0/node_modules:/home/runner/work/incur/incur/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/incur/incur/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/node_modules:/home/runner/work/incur/incur/node_modules/.pnpm/tsx@4.21.0/node_modules:/home/runner/work/incur/incur/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
21
|
-
fi
|