incur 0.4.0 → 0.4.2

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.
Files changed (115) hide show
  1. package/README.md +83 -22
  2. package/SKILL.md +6 -6
  3. package/dist/Cli.d.ts +46 -26
  4. package/dist/Cli.d.ts.map +1 -1
  5. package/dist/Cli.js +728 -441
  6. package/dist/Cli.js.map +1 -1
  7. package/dist/Completions.d.ts +4 -3
  8. package/dist/Completions.d.ts.map +1 -1
  9. package/dist/Completions.js +17 -10
  10. package/dist/Completions.js.map +1 -1
  11. package/dist/Fetch.d.ts.map +1 -1
  12. package/dist/Fetch.js +10 -9
  13. package/dist/Fetch.js.map +1 -1
  14. package/dist/Filter.js +0 -18
  15. package/dist/Filter.js.map +1 -1
  16. package/dist/Formatter.d.ts.map +1 -1
  17. package/dist/Formatter.js +6 -1
  18. package/dist/Formatter.js.map +1 -1
  19. package/dist/Help.d.ts +7 -1
  20. package/dist/Help.d.ts.map +1 -1
  21. package/dist/Help.js +44 -27
  22. package/dist/Help.js.map +1 -1
  23. package/dist/Mcp.d.ts +37 -5
  24. package/dist/Mcp.d.ts.map +1 -1
  25. package/dist/Mcp.js +71 -72
  26. package/dist/Mcp.js.map +1 -1
  27. package/dist/Openapi.d.ts.map +1 -1
  28. package/dist/Openapi.js +22 -14
  29. package/dist/Openapi.js.map +1 -1
  30. package/dist/Parser.d.ts +4 -0
  31. package/dist/Parser.d.ts.map +1 -1
  32. package/dist/Parser.js +70 -38
  33. package/dist/Parser.js.map +1 -1
  34. package/dist/Schema.d.ts +5 -1
  35. package/dist/Schema.d.ts.map +1 -1
  36. package/dist/Schema.js +13 -2
  37. package/dist/Schema.js.map +1 -1
  38. package/dist/Skill.d.ts +2 -1
  39. package/dist/Skill.d.ts.map +1 -1
  40. package/dist/Skill.js +33 -19
  41. package/dist/Skill.js.map +1 -1
  42. package/dist/Skillgen.js +1 -1
  43. package/dist/Skillgen.js.map +1 -1
  44. package/dist/SyncSkills.d.ts +48 -0
  45. package/dist/SyncSkills.d.ts.map +1 -1
  46. package/dist/SyncSkills.js +108 -10
  47. package/dist/SyncSkills.js.map +1 -1
  48. package/dist/Typegen.js +4 -2
  49. package/dist/Typegen.js.map +1 -1
  50. package/dist/bin.d.ts +2 -1
  51. package/dist/bin.d.ts.map +1 -1
  52. package/dist/bin.js +17 -2
  53. package/dist/bin.js.map +1 -1
  54. package/dist/internal/command.d.ts +170 -0
  55. package/dist/internal/command.d.ts.map +1 -0
  56. package/dist/internal/command.js +292 -0
  57. package/dist/internal/command.js.map +1 -0
  58. package/dist/internal/configSchema.d.ts +8 -0
  59. package/dist/internal/configSchema.d.ts.map +1 -0
  60. package/dist/internal/configSchema.js +57 -0
  61. package/dist/internal/configSchema.js.map +1 -0
  62. package/dist/internal/dereference.d.ts +12 -0
  63. package/dist/internal/dereference.d.ts.map +1 -0
  64. package/dist/internal/dereference.js +71 -0
  65. package/dist/internal/dereference.js.map +1 -0
  66. package/dist/internal/helpers.d.ts +9 -0
  67. package/dist/internal/helpers.d.ts.map +1 -0
  68. package/dist/internal/helpers.js +54 -0
  69. package/dist/internal/helpers.js.map +1 -0
  70. package/dist/middleware.d.ts +6 -8
  71. package/dist/middleware.d.ts.map +1 -1
  72. package/dist/middleware.js +1 -1
  73. package/dist/middleware.js.map +1 -1
  74. package/examples/npm/.npmrc.json +21 -0
  75. package/examples/npm/config.schema.json +134 -0
  76. package/package.json +6 -29
  77. package/src/Cli.test-d.ts +44 -33
  78. package/src/Cli.test.ts +1231 -101
  79. package/src/Cli.ts +877 -569
  80. package/src/Completions.test.ts +136 -12
  81. package/src/Completions.ts +18 -13
  82. package/src/Fetch.test.ts +21 -0
  83. package/src/Fetch.ts +8 -10
  84. package/src/Filter.ts +0 -17
  85. package/src/Formatter.test.ts +15 -2
  86. package/src/Formatter.ts +5 -1
  87. package/src/Help.test.ts +184 -20
  88. package/src/Help.ts +52 -28
  89. package/src/Mcp.test.ts +159 -0
  90. package/src/Mcp.ts +108 -86
  91. package/src/Openapi.test.ts +17 -5
  92. package/src/Openapi.ts +21 -15
  93. package/src/Parser.test-d.ts +22 -0
  94. package/src/Parser.test.ts +89 -0
  95. package/src/Parser.ts +87 -36
  96. package/src/Schema.test.ts +29 -0
  97. package/src/Schema.ts +12 -2
  98. package/src/Skill.test.ts +87 -6
  99. package/src/Skill.ts +38 -21
  100. package/src/Skillgen.ts +1 -1
  101. package/src/SyncMcp.test.ts +6 -8
  102. package/src/SyncSkills.test.ts +146 -3
  103. package/src/SyncSkills.ts +191 -10
  104. package/src/Typegen.test.ts +15 -0
  105. package/src/Typegen.ts +4 -2
  106. package/src/bin.ts +21 -2
  107. package/src/e2e.test.ts +188 -98
  108. package/src/internal/command.ts +449 -0
  109. package/src/internal/configSchema.test.ts +193 -0
  110. package/src/internal/configSchema.ts +66 -0
  111. package/src/internal/dereference.test.ts +695 -0
  112. package/src/internal/dereference.ts +75 -0
  113. package/src/internal/helpers.test.ts +75 -0
  114. package/src/internal/helpers.ts +59 -0
  115. package/src/middleware.ts +5 -12
package/src/e2e.test.ts CHANGED
@@ -3,6 +3,7 @@ import { Cli, Errors, Skill, Typegen, z } from 'incur'
3
3
  import { app as honoApp } from '../test/fixtures/hono-api.js'
4
4
 
5
5
  let __mockSkillsHash: string | undefined
6
+ let __mockSkillsInstalled = true
6
7
 
7
8
  const originalIsTTY = process.stdout.isTTY
8
9
  beforeAll(() => {
@@ -14,7 +15,11 @@ afterAll(() => {
14
15
 
15
16
  vi.mock('./SyncSkills.js', async (importOriginal) => {
16
17
  const actual = await importOriginal<typeof import('./SyncSkills.js')>()
17
- return { ...actual, readHash: () => __mockSkillsHash }
18
+ return {
19
+ ...actual,
20
+ hasInstalledSkills: () => __mockSkillsInstalled,
21
+ readHash: () => __mockSkillsHash,
22
+ }
18
23
  })
19
24
 
20
25
  describe('routing', () => {
@@ -70,9 +75,9 @@ describe('routing', () => {
70
75
  "code: COMMAND_NOT_FOUND
71
76
  message: 'nonexistent' is not a command for 'app'.
72
77
  cta:
73
- description: "See available commands:"
74
- commands[1]{command}:
75
- app --help
78
+ description: "Suggested command:"
79
+ commands[1]{command,description}:
80
+ app --help,see all available commands
76
81
  "
77
82
  `)
78
83
  })
@@ -85,8 +90,8 @@ describe('routing', () => {
85
90
  expect(output).toMatchInlineSnapshot(`
86
91
  "Error: 'nonexistent' is not a command for 'app'.
87
92
 
88
- See available commands:
89
- app --help
93
+ Suggested command:
94
+ app --help # see all available commands
90
95
  "
91
96
  `)
92
97
  })
@@ -98,9 +103,9 @@ describe('routing', () => {
98
103
  "code: COMMAND_NOT_FOUND
99
104
  message: 'whoami' is not a command for 'app auth'.
100
105
  cta:
101
- description: "See available commands:"
102
- commands[1]{command}:
103
- app auth --help
106
+ description: "Suggested command:"
107
+ commands[1]{command,description}:
108
+ app auth --help,see all available commands
104
109
  "
105
110
  `)
106
111
  })
@@ -112,9 +117,9 @@ describe('routing', () => {
112
117
  "code: COMMAND_NOT_FOUND
113
118
  message: 'nope' is not a command for 'app project deploy'.
114
119
  cta:
115
- description: "See available commands:"
116
- commands[1]{command}:
117
- app project deploy --help
120
+ description: "Suggested command:"
121
+ commands[1]{command,description}:
122
+ app project deploy --help,see all available commands
118
123
  "
119
124
  `)
120
125
  })
@@ -173,7 +178,7 @@ describe('args and options', () => {
173
178
  'read',
174
179
  '--scopes',
175
180
  'write',
176
- '--verbose',
181
+ '--full-output',
177
182
  '--format',
178
183
  'json',
179
184
  ])
@@ -192,7 +197,7 @@ describe('args and options', () => {
192
197
  'list',
193
198
  '--limit',
194
199
  '5',
195
- '--verbose',
200
+ '--full-output',
196
201
  '--format',
197
202
  'json',
198
203
  ])
@@ -327,8 +332,8 @@ describe('output formats', () => {
327
332
  `)
328
333
  })
329
334
 
330
- test('--verbose full envelope', async () => {
331
- const { output } = await serve(createApp(), ['ping', '--verbose'])
335
+ test('--full-output full envelope', async () => {
336
+ const { output } = await serve(createApp(), ['ping', '--full-output'])
332
337
  expect(output).toMatchInlineSnapshot(`
333
338
  "ok: true
334
339
  data:
@@ -340,8 +345,8 @@ describe('output formats', () => {
340
345
  `)
341
346
  })
342
347
 
343
- test('--verbose --format json full envelope', async () => {
344
- const { output } = await serve(createApp(), ['ping', '--verbose', '--format', 'json'])
348
+ test('--full-output --format json full envelope', async () => {
349
+ const { output } = await serve(createApp(), ['ping', '--full-output', '--format', 'json'])
345
350
  expect(json(output)).toMatchInlineSnapshot(`
346
351
  {
347
352
  "data": {
@@ -356,13 +361,13 @@ describe('output formats', () => {
356
361
  `)
357
362
  })
358
363
 
359
- test('nested command path in verbose meta', async () => {
364
+ test('nested command path in full-output meta', async () => {
360
365
  const { output } = await serve(createApp(), [
361
366
  'project',
362
367
  'deploy',
363
368
  'status',
364
369
  'd-1',
365
- '--verbose',
370
+ '--full-output',
366
371
  '--format',
367
372
  'json',
368
373
  ])
@@ -400,8 +405,8 @@ describe('undefined output', () => {
400
405
  expect(output).toBe('')
401
406
  })
402
407
 
403
- test('void command shows envelope with --verbose', async () => {
404
- const { output } = await serve(createApp(), ['noop', '--verbose', '--format', 'json'])
408
+ test('void command shows envelope with --full-output', async () => {
409
+ const { output } = await serve(createApp(), ['noop', '--full-output', '--format', 'json'])
405
410
  expect(json(output)).toMatchInlineSnapshot(`
406
411
  {
407
412
  "meta": {
@@ -455,10 +460,10 @@ describe('--token-limit and --token-offset', () => {
455
460
  `)
456
461
  })
457
462
 
458
- test('works with --verbose', async () => {
463
+ test('works with --full-output', async () => {
459
464
  const { output } = await serve(createApp(), [
460
465
  'ping',
461
- '--verbose',
466
+ '--full-output',
462
467
  '--format',
463
468
  'json',
464
469
  '--token-limit',
@@ -479,10 +484,10 @@ describe('--token-limit and --token-offset', () => {
479
484
  `)
480
485
  })
481
486
 
482
- test('--verbose includes meta.nextOffset when truncated', async () => {
487
+ test('--full-output includes meta.nextOffset when truncated', async () => {
483
488
  const { output } = await serve(createApp(), [
484
489
  'ping',
485
- '--verbose',
490
+ '--full-output',
486
491
  '--format',
487
492
  'json',
488
493
  '--token-limit',
@@ -492,10 +497,10 @@ describe('--token-limit and --token-offset', () => {
492
497
  expect(output).toContain('[truncated:')
493
498
  })
494
499
 
495
- test('--verbose omits meta.nextOffset when not truncated', async () => {
500
+ test('--full-output omits meta.nextOffset when not truncated', async () => {
496
501
  const { output } = await serve(createApp(), [
497
502
  'ping',
498
- '--verbose',
503
+ '--full-output',
499
504
  '--format',
500
505
  'json',
501
506
  '--token-limit',
@@ -575,7 +580,7 @@ describe('error handling', () => {
575
580
  const { output, exitCode } = await serve(createApp(), [
576
581
  'auth',
577
582
  'status',
578
- '--verbose',
583
+ '--full-output',
579
584
  '--format',
580
585
  'json',
581
586
  ])
@@ -595,7 +600,7 @@ describe('error handling', () => {
595
600
  "command": "app auth login",
596
601
  },
597
602
  ],
598
- "description": "Suggested commands:",
603
+ "description": "Suggested command:",
599
604
  },
600
605
  "duration": "<stripped>",
601
606
  },
@@ -633,7 +638,7 @@ describe('error handling', () => {
633
638
  test('command not found returns error envelope', async () => {
634
639
  const { output, exitCode } = await serve(createApp(), [
635
640
  'nonexistent',
636
- '--verbose',
641
+ '--full-output',
637
642
  '--format',
638
643
  'json',
639
644
  ])
@@ -650,9 +655,10 @@ describe('error handling', () => {
650
655
  "commands": [
651
656
  {
652
657
  "command": "app --help",
658
+ "description": "see all available commands",
653
659
  },
654
660
  ],
655
- "description": "See available commands:",
661
+ "description": "Suggested command:",
656
662
  },
657
663
  "duration": "<stripped>",
658
664
  },
@@ -675,7 +681,13 @@ describe('error handling', () => {
675
681
 
676
682
  describe('cta', () => {
677
683
  test('ok() with string CTAs', async () => {
678
- const { output } = await serve(createApp(), ['auth', 'login', '--verbose', '--format', 'json'])
684
+ const { output } = await serve(createApp(), [
685
+ 'auth',
686
+ 'login',
687
+ '--full-output',
688
+ '--format',
689
+ 'json',
690
+ ])
679
691
  expect(json(output).meta.cta).toMatchInlineSnapshot(`
680
692
  {
681
693
  "commands": [
@@ -693,7 +705,7 @@ describe('cta', () => {
693
705
  'project',
694
706
  'create',
695
707
  'MyProject',
696
- '--verbose',
708
+ '--full-output',
697
709
  '--format',
698
710
  'json',
699
711
  ])
@@ -714,7 +726,13 @@ describe('cta', () => {
714
726
  })
715
727
 
716
728
  test('error() with CTA', async () => {
717
- const { output } = await serve(createApp(), ['auth', 'status', '--verbose', '--format', 'json'])
729
+ const { output } = await serve(createApp(), [
730
+ 'auth',
731
+ 'status',
732
+ '--full-output',
733
+ '--format',
734
+ 'json',
735
+ ])
718
736
  expect(json(output).meta.cta).toMatchInlineSnapshot(`
719
737
  {
720
738
  "commands": [
@@ -722,13 +740,13 @@ describe('cta', () => {
722
740
  "command": "app auth login",
723
741
  },
724
742
  ],
725
- "description": "Suggested commands:",
743
+ "description": "Suggested command:",
726
744
  }
727
745
  `)
728
746
  })
729
747
 
730
748
  test('plain return omits CTA', async () => {
731
- const { output } = await serve(createApp(), ['ping', '--verbose', '--format', 'json'])
749
+ const { output } = await serve(createApp(), ['ping', '--full-output', '--format', 'json'])
732
750
  expect(json(output).meta.cta).toBeUndefined()
733
751
  })
734
752
 
@@ -737,7 +755,7 @@ describe('cta', () => {
737
755
  'project',
738
756
  'list',
739
757
  '--archived',
740
- '--verbose',
758
+ '--full-output',
741
759
  '--format',
742
760
  'json',
743
761
  ])
@@ -779,8 +797,8 @@ describe('streaming', () => {
779
797
  `)
780
798
  })
781
799
 
782
- test('default streams toon per chunk (--verbose)', async () => {
783
- const { output } = await serve(createApp(), ['stream', '--verbose'])
800
+ test('default streams toon per chunk (--full-output)', async () => {
801
+ const { output } = await serve(createApp(), ['stream', '--full-output'])
784
802
  expect(output).toMatchInlineSnapshot(`
785
803
  "content: hello
786
804
  content: world
@@ -802,8 +820,8 @@ describe('streaming', () => {
802
820
  `)
803
821
  })
804
822
 
805
- test('--format json --verbose buffers with envelope', async () => {
806
- const { output } = await serve(createApp(), ['stream', '--verbose', '--format', 'json'])
823
+ test('--format json --full-output buffers with envelope', async () => {
824
+ const { output } = await serve(createApp(), ['stream', '--full-output', '--format', 'json'])
807
825
  expect(json(output)).toMatchInlineSnapshot(`
808
826
  {
809
827
  "data": [
@@ -868,7 +886,7 @@ describe('streaming', () => {
868
886
  "command": "app ping",
869
887
  },
870
888
  ],
871
- "description": "Suggested commands:",
889
+ "description": "Suggested command:",
872
890
  }
873
891
  `)
874
892
  })
@@ -877,7 +895,7 @@ describe('streaming', () => {
877
895
  const { output } = await serve(createApp(), ['stream-ok'])
878
896
  expect(output).toContain('n: 1')
879
897
  expect(output).toContain('n: 2')
880
- expect(output).toContain('Suggested commands:')
898
+ expect(output).toContain('Suggested command:')
881
899
  expect(output).toContain('app ping')
882
900
  })
883
901
 
@@ -963,22 +981,22 @@ describe('help', () => {
963
981
  stream-throw Stream that throws
964
982
  validate-fail Fails validation
965
983
 
966
- Built-in Commands:
984
+ Integrations:
967
985
  completions Generate shell completion script
968
- mcp add Register as an MCP server
969
- skills add Sync skill files to your agent
986
+ mcp add Register as MCP server
987
+ skills Sync skill files to agents (add, list)
970
988
 
971
989
  Global Options:
972
990
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
973
991
  --format <toon|json|yaml|md|jsonl> Output format
992
+ --full-output Show full output envelope
974
993
  --help Show help
975
994
  --llms, --llms-full Print LLM-readable manifest
976
995
  --mcp Start as MCP stdio server
977
- --schema Show JSON Schema for a command
996
+ --schema Show JSON Schema for command
978
997
  --token-count Print token count of output (instead of output)
979
998
  --token-limit <n> Limit output to n tokens
980
999
  --token-offset <n> Skip first n tokens of output
981
- --verbose Show full output envelope
982
1000
  --version Show version
983
1001
  "
984
1002
  `)
@@ -1005,13 +1023,13 @@ describe('help', () => {
1005
1023
  Global Options:
1006
1024
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
1007
1025
  --format <toon|json|yaml|md|jsonl> Output format
1026
+ --full-output Show full output envelope
1008
1027
  --help Show help
1009
1028
  --llms, --llms-full Print LLM-readable manifest
1010
- --schema Show JSON Schema for a command
1029
+ --schema Show JSON Schema for command
1011
1030
  --token-count Print token count of output (instead of output)
1012
1031
  --token-limit <n> Limit output to n tokens
1013
1032
  --token-offset <n> Skip first n tokens of output
1014
- --verbose Show full output envelope
1015
1033
  "
1016
1034
  `)
1017
1035
  })
@@ -1032,13 +1050,13 @@ describe('help', () => {
1032
1050
  Global Options:
1033
1051
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
1034
1052
  --format <toon|json|yaml|md|jsonl> Output format
1053
+ --full-output Show full output envelope
1035
1054
  --help Show help
1036
1055
  --llms, --llms-full Print LLM-readable manifest
1037
- --schema Show JSON Schema for a command
1056
+ --schema Show JSON Schema for command
1038
1057
  --token-count Print token count of output (instead of output)
1039
1058
  --token-limit <n> Limit output to n tokens
1040
1059
  --token-offset <n> Skip first n tokens of output
1041
- --verbose Show full output envelope
1042
1060
  "
1043
1061
  `)
1044
1062
  })
@@ -1053,18 +1071,18 @@ describe('help', () => {
1053
1071
  Options:
1054
1072
  --limit, -l <number> Max results (default: 20)
1055
1073
  --sort, -s <name|created|updated> Sort field (default: name)
1056
- --archived <boolean> Include archived (default: false)
1074
+ --archived Include archived
1057
1075
 
1058
1076
  Global Options:
1059
1077
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
1060
1078
  --format <toon|json|yaml|md|jsonl> Output format
1079
+ --full-output Show full output envelope
1061
1080
  --help Show help
1062
1081
  --llms, --llms-full Print LLM-readable manifest
1063
- --schema Show JSON Schema for a command
1082
+ --schema Show JSON Schema for command
1064
1083
  --token-count Print token count of output (instead of output)
1065
1084
  --token-limit <n> Limit output to n tokens
1066
1085
  --token-offset <n> Skip first n tokens of output
1067
- --verbose Show full output envelope
1068
1086
  "
1069
1087
  `)
1070
1088
  })
@@ -1081,7 +1099,7 @@ describe('help', () => {
1081
1099
 
1082
1100
  Options:
1083
1101
  --branch, -b <string> Branch to deploy (default: main)
1084
- --dry-run <boolean> Dry run mode (default: false)
1102
+ --dry-run Dry run mode
1085
1103
 
1086
1104
  Examples:
1087
1105
  app project deploy create staging # Deploy staging from main
@@ -1090,13 +1108,13 @@ describe('help', () => {
1090
1108
  Global Options:
1091
1109
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
1092
1110
  --format <toon|json|yaml|md|jsonl> Output format
1111
+ --full-output Show full output envelope
1093
1112
  --help Show help
1094
1113
  --llms, --llms-full Print LLM-readable manifest
1095
- --schema Show JSON Schema for a command
1114
+ --schema Show JSON Schema for command
1096
1115
  --token-count Print token count of output (instead of output)
1097
1116
  --token-limit <n> Limit output to n tokens
1098
1117
  --token-offset <n> Skip first n tokens of output
1099
- --verbose Show full output envelope
1100
1118
  "
1101
1119
  `)
1102
1120
  })
@@ -1589,8 +1607,8 @@ describe('typegen', () => {
1589
1607
  'auth login': { args: {}; options: { hostname: string; web: boolean; scopes: string[] } }
1590
1608
  'auth logout': { args: {}; options: {} }
1591
1609
  'auth status': { args: {}; options: {} }
1592
- 'config': { args: { key: string }; options: {} }
1593
- 'echo': { args: { message: string; repeat: number }; options: { upper: boolean; prefix: string } }
1610
+ 'config': { args: { key?: string }; options: {} }
1611
+ 'echo': { args: { message: string; repeat?: number }; options: { upper: boolean; prefix: string } }
1594
1612
  'explode': { args: {}; options: {} }
1595
1613
  'explode-clac': { args: {}; options: {} }
1596
1614
  'noop': { args: {}; options: {} }
@@ -1738,22 +1756,22 @@ describe('root command with subcommands', () => {
1738
1756
  info Show info
1739
1757
  version Show version
1740
1758
 
1741
- Built-in Commands:
1759
+ Integrations:
1742
1760
  completions Generate shell completion script
1743
- mcp add Register as an MCP server
1744
- skills add Sync skill files to your agent
1761
+ mcp add Register as MCP server
1762
+ skills Sync skill files to agents (add, list)
1745
1763
 
1746
1764
  Global Options:
1747
1765
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
1748
1766
  --format <toon|json|yaml|md|jsonl> Output format
1767
+ --full-output Show full output envelope
1749
1768
  --help Show help
1750
1769
  --llms, --llms-full Print LLM-readable manifest
1751
1770
  --mcp Start as MCP stdio server
1752
- --schema Show JSON Schema for a command
1771
+ --schema Show JSON Schema for command
1753
1772
  --token-count Print token count of output (instead of output)
1754
1773
  --token-limit <n> Limit output to n tokens
1755
1774
  --token-offset <n> Skip first n tokens of output
1756
- --verbose Show full output envelope
1757
1775
  --version Show version
1758
1776
  "
1759
1777
  `)
@@ -1786,7 +1804,7 @@ describe('edge cases', () => {
1786
1804
  "description": "View "Alpha"",
1787
1805
  },
1788
1806
  ],
1789
- "description": "Suggested commands:",
1807
+ "description": "Suggested command:",
1790
1808
  },
1791
1809
  "items": [
1792
1810
  {
@@ -1864,7 +1882,7 @@ describe('edge cases', () => {
1864
1882
  'prod',
1865
1883
  '--branch',
1866
1884
  'release',
1867
- '--verbose',
1885
+ '--full-output',
1868
1886
  ])
1869
1887
  expect(json(output)).toMatchInlineSnapshot(`
1870
1888
  {
@@ -1893,7 +1911,7 @@ describe('env', () => {
1893
1911
  test('env vars passed to handler', async () => {
1894
1912
  const { output } = await serve(
1895
1913
  createApp(),
1896
- ['auth', 'login', '--verbose', '--format', 'json'],
1914
+ ['auth', 'login', '--full-output', '--format', 'json'],
1897
1915
  { env: { AUTH_HOST: 'custom.example.com' } },
1898
1916
  )
1899
1917
  expect(json(output).data.hostname).toBe('custom.example.com')
@@ -1902,7 +1920,7 @@ describe('env', () => {
1902
1920
  test('env defaults applied when var is unset', async () => {
1903
1921
  const { output } = await serve(
1904
1922
  createApp(),
1905
- ['auth', 'login', '--verbose', '--format', 'json'],
1923
+ ['auth', 'login', '--full-output', '--format', 'json'],
1906
1924
  { env: {} },
1907
1925
  )
1908
1926
  expect(json(output).data.hostname).toBe('api.example.com')
@@ -1917,19 +1935,19 @@ describe('env', () => {
1917
1935
 
1918
1936
  Options:
1919
1937
  --hostname, -h <string> API hostname (default: api.example.com)
1920
- --web, -w <boolean> Open browser (default: false)
1938
+ --web, -w Open browser
1921
1939
  --scopes <array> OAuth scopes
1922
1940
 
1923
1941
  Global Options:
1924
1942
  --filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
1925
1943
  --format <toon|json|yaml|md|jsonl> Output format
1944
+ --full-output Show full output envelope
1926
1945
  --help Show help
1927
1946
  --llms, --llms-full Print LLM-readable manifest
1928
- --schema Show JSON Schema for a command
1947
+ --schema Show JSON Schema for command
1929
1948
  --token-count Print token count of output (instead of output)
1930
1949
  --token-limit <n> Limit output to n tokens
1931
1950
  --token-offset <n> Skip first n tokens of output
1932
- --verbose Show full output envelope
1933
1951
 
1934
1952
  Environment Variables:
1935
1953
  AUTH_TOKEN Pre-existing auth token
@@ -1970,17 +1988,21 @@ describe('skills staleness', () => {
1970
1988
  beforeEach(() => {
1971
1989
  stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
1972
1990
  __mockSkillsHash = undefined
1991
+ __mockSkillsInstalled = true
1973
1992
  })
1974
1993
 
1975
1994
  afterEach(() => {
1976
1995
  stderrSpy.mockRestore()
1996
+ __mockSkillsHash = undefined
1997
+ __mockSkillsInstalled = true
1977
1998
  })
1978
1999
 
1979
- test('warns when running a command with stale skills', async () => {
2000
+ test('includes skills CTA when stale', async () => {
1980
2001
  __mockSkillsHash = '0000000000000000'
1981
2002
  const { output } = await serve(createApp(), ['ping'])
1982
2003
  expect(output).toContain('pong: true')
1983
- expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Skills are out of date.'))
2004
+ expect(output).toContain('Skills are out of date:')
2005
+ expect(output).toContain('skills add')
1984
2006
  })
1985
2007
 
1986
2008
  test('no warning when skills hash matches', async () => {
@@ -1991,20 +2013,28 @@ describe('skills staleness', () => {
1991
2013
 
1992
2014
  const { output } = await serve(cli, ['ping'])
1993
2015
  expect(output).toContain('pong: true')
1994
- expect(stderrSpy).not.toHaveBeenCalled()
2016
+ expect(output).not.toContain('Skills are out of date')
1995
2017
  })
1996
2018
 
1997
2019
  test('no warning on first use (no hash stored)', async () => {
1998
2020
  __mockSkillsHash = undefined
1999
2021
  const { output } = await serve(createApp(), ['ping'])
2000
2022
  expect(output).toContain('pong: true')
2001
- expect(stderrSpy).not.toHaveBeenCalled()
2023
+ expect(output).not.toContain('Skills are out of date')
2024
+ })
2025
+
2026
+ test('no warning when skills are not installed', async () => {
2027
+ __mockSkillsHash = '0000000000000000'
2028
+ __mockSkillsInstalled = false
2029
+ const { output } = await serve(createApp(), ['ping'])
2030
+ expect(output).toContain('pong: true')
2031
+ expect(output).not.toContain('Skills are out of date')
2002
2032
  })
2003
2033
 
2004
2034
  test('no warning for --llms', async () => {
2005
2035
  __mockSkillsHash = '0000000000000000'
2006
- await serve(createApp(), ['--llms'])
2007
- expect(stderrSpy).not.toHaveBeenCalled()
2036
+ const { output } = await serve(createApp(), ['--llms'])
2037
+ expect(output).not.toContain('Skills are out of date')
2008
2038
  })
2009
2039
 
2010
2040
  test('no warning for --mcp', async () => {
@@ -2034,9 +2064,9 @@ describe('middleware', () => {
2034
2064
  `)
2035
2065
  })
2036
2066
 
2037
- test('vars: verbose envelope includes var data', async () => {
2067
+ test('vars: full-output envelope includes var data', async () => {
2038
2068
  const { cli } = createMiddlewareApp()
2039
- const { output } = await serve(cli, ['whoami', '--verbose', '--format', 'json'])
2069
+ const { output } = await serve(cli, ['whoami', '--full-output', '--format', 'json'])
2040
2070
  const parsed = json(output)
2041
2071
  expect(parsed.data.user).toBe('alice')
2042
2072
  expect(parsed.data.requestId).toBe('req-default')
@@ -2204,7 +2234,7 @@ describe('fetch gateway', () => {
2204
2234
  })
2205
2235
 
2206
2236
  test('query params from --key value', async () => {
2207
- const { output } = await serve(createApp(), ['api', 'users', '--limit', '5'])
2237
+ await serve(createApp(), ['api', 'users', '--limit', '5'])
2208
2238
  const { output: jsonOut } = await serve(createApp(), [
2209
2239
  'api',
2210
2240
  'users',
@@ -2273,8 +2303,14 @@ describe('fetch gateway', () => {
2273
2303
  expect(json(output)).toEqual({ ok: true })
2274
2304
  })
2275
2305
 
2276
- test('--verbose wraps in envelope', async () => {
2277
- const { output } = await serve(createApp(), ['api', 'health', '--verbose', '--format', 'json'])
2306
+ test('--full-output wraps in envelope', async () => {
2307
+ const { output } = await serve(createApp(), [
2308
+ 'api',
2309
+ 'health',
2310
+ '--full-output',
2311
+ '--format',
2312
+ 'json',
2313
+ ])
2278
2314
  const parsed = json(output)
2279
2315
  expect(parsed.ok).toBe(true)
2280
2316
  expect(parsed.data).toEqual({ ok: true })
@@ -2441,6 +2477,18 @@ describe('fetch api', () => {
2441
2477
  },
2442
2478
  "meta": {
2443
2479
  "command": "project create",
2480
+ "cta": {
2481
+ "commands": [
2482
+ {
2483
+ "command": "app project get p-new",
2484
+ "description": "View "MyProject"",
2485
+ },
2486
+ {
2487
+ "command": "app project list",
2488
+ },
2489
+ ],
2490
+ "description": "Suggested commands:",
2491
+ },
2444
2492
  "duration": "<stripped>",
2445
2493
  },
2446
2494
  "ok": true,
@@ -2524,21 +2572,22 @@ describe('fetch api', () => {
2524
2572
  const cli = createApp()
2525
2573
  expect(await fetchJson(cli, new Request('http://localhost/explode-clac')))
2526
2574
  .toMatchInlineSnapshot(`
2527
- {
2528
- "body": {
2529
- "error": {
2530
- "code": "QUOTA_EXCEEDED",
2531
- "message": "Rate limit exceeded",
2532
- },
2533
- "meta": {
2534
- "command": "explode-clac",
2535
- "duration": "<stripped>",
2575
+ {
2576
+ "body": {
2577
+ "error": {
2578
+ "code": "QUOTA_EXCEEDED",
2579
+ "message": "Rate limit exceeded",
2580
+ "retryable": true,
2581
+ },
2582
+ "meta": {
2583
+ "command": "explode-clac",
2584
+ "duration": "<stripped>",
2585
+ },
2586
+ "ok": false,
2536
2587
  },
2537
- "ok": false,
2538
- },
2539
- "status": 500,
2540
- }
2541
- `)
2588
+ "status": 500,
2589
+ }
2590
+ `)
2542
2591
  })
2543
2592
 
2544
2593
  test('validation error → 400', async () => {
@@ -2832,6 +2881,47 @@ describe('fetch api', () => {
2832
2881
  `)
2833
2882
  })
2834
2883
 
2884
+ test('tools/call with no-args command', async () => {
2885
+ const cli = createApp()
2886
+ const sessionId = await initSession(cli)
2887
+ const res = await mcpRequest(
2888
+ cli,
2889
+ {
2890
+ jsonrpc: '2.0',
2891
+ id: 6,
2892
+ method: 'tools/call',
2893
+ params: { name: 'ping', arguments: {} },
2894
+ },
2895
+ sessionId,
2896
+ )
2897
+ expect(res.status).toBe(200)
2898
+ const body = await res.json()
2899
+ expect(JSON.parse(body.result.content[0].text)).toMatchInlineSnapshot(`
2900
+ {
2901
+ "pong": true,
2902
+ }
2903
+ `)
2904
+ })
2905
+
2906
+ test('tools/call with streaming command', async () => {
2907
+ const cli = createApp()
2908
+ const sessionId = await initSession(cli)
2909
+ const res = await mcpRequest(
2910
+ cli,
2911
+ {
2912
+ jsonrpc: '2.0',
2913
+ id: 7,
2914
+ method: 'tools/call',
2915
+ params: { name: 'stream', arguments: {} },
2916
+ },
2917
+ sessionId,
2918
+ )
2919
+ expect(res.status).toBe(200)
2920
+ const body = await res.json()
2921
+ const chunks = JSON.parse(body.result.content[0].text)
2922
+ expect(chunks).toEqual([{ content: 'hello' }, { content: 'world' }])
2923
+ })
2924
+
2835
2925
  test('non-/mcp paths still work alongside MCP', async () => {
2836
2926
  const cli = createApp()
2837
2927
  // Initialize MCP first