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 CHANGED
@@ -1,17 +1,41 @@
1
- # incur
2
-
3
- Simple CLI framework for agents and humans.
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
- ```sh
10
- # Skills (recommended — lighter on tokens)
11
- > "Run `npx incur skills add`, then show me what I can do with incur."
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
- # MCP
14
- > "Run `npx incur mcp add`, then show me what I can do with incur."
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
- Every incur CLI gets built-in agent discovery:
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 lighter on tokens)
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.2",
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": "0ms",
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": "0ms",
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": "0ms",
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": "0ms",
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": "0ms",
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": "0ms",
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.replace(/duration: \d+ms/g, 'duration: <stripped>'),
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 }) {
@@ -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
@@ -1,14 +0,0 @@
1
- {
2
- "name": "presto",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "presto": "node --import tsx cli.ts"
7
- },
8
- "devDependencies": {
9
- "tsx": "^4.21.0"
10
- },
11
- "dependencies": {
12
- "incur": "workspace:*"
13
- }
14
- }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "paths": {
5
- "incur": ["../../src"]
6
- }
7
- },
8
- "include": ["*.ts"]
9
- }