incur 0.2.0 → 0.2.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.
package/dist/Cli.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { estimateTokenCount, sliceByTokens } from 'tokenx';
1
2
  import * as Completions from './Completions.js';
3
+ import * as Filter from './Filter.js';
2
4
  import { IncurError, ValidationError } from './Errors.js';
3
5
  import * as Fetch from './Fetch.js';
4
6
  import * as Openapi from './Openapi.js';
@@ -19,6 +21,7 @@ export function create(nameOrDefinition, definition) {
19
21
  const commands = new Map();
20
22
  const middlewares = [];
21
23
  const pending = [];
24
+ const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0');
22
25
  const cli = {
23
26
  name,
24
27
  description: def.description,
@@ -69,6 +72,16 @@ export function create(nameOrDefinition, definition) {
69
72
  });
70
73
  return cli;
71
74
  },
75
+ async fetch(req) {
76
+ if (pending.length > 0)
77
+ await Promise.all(pending);
78
+ return fetchImpl(name, commands, req, {
79
+ mcpHandler,
80
+ middlewares,
81
+ rootCommand: rootDef,
82
+ vars: def.vars,
83
+ });
84
+ },
72
85
  async serve(argv = process.argv.slice(2), serveOptions = {}) {
73
86
  if (pending.length > 0)
74
87
  await Promise.all(pending);
@@ -106,7 +119,7 @@ export function create(nameOrDefinition, definition) {
106
119
  async function serveImpl(name, commands, argv, options = {}) {
107
120
  const stdout = options.stdout ?? ((s) => process.stdout.write(s));
108
121
  const exit = options.exit ?? ((code) => process.exit(code));
109
- const { verbose, format: formatFlag, formatExplicit, llms, mcp: mcpFlag, help, version, rest: filtered, } = extractBuiltinFlags(argv);
122
+ const { verbose, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, mcp: mcpFlag, help, version, schema, rest: filtered, } = extractBuiltinFlags(argv);
110
123
  // --mcp: start as MCP stdio server
111
124
  if (mcpFlag) {
112
125
  await Mcp.serve(name, options.version ?? '0.0.0', commands);
@@ -138,7 +151,7 @@ async function serveImpl(name, commands, argv, options = {}) {
138
151
  stdout(s.endsWith('\n') ? s : `${s}\n`);
139
152
  }
140
153
  // Skills staleness check (skip for built-in commands)
141
- if (!llms && !help && !version) {
154
+ if (!llms && !schema && !help && !version) {
142
155
  const isSkillsAdd = filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills');
143
156
  const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp');
144
157
  if (!isSkillsAdd && !isMcpAdd) {
@@ -479,6 +492,40 @@ async function serveImpl(name, commands, argv, options = {}) {
479
492
  }
480
493
  return;
481
494
  }
495
+ // --schema: output JSON Schema for a command's args, env, options, output
496
+ if (schema) {
497
+ if ('help' in resolved) {
498
+ writeln(Help.formatRoot(`${name} ${resolved.path}`, {
499
+ description: resolved.description,
500
+ commands: collectHelpCommands(resolved.commands),
501
+ }));
502
+ return;
503
+ }
504
+ if ('error' in resolved) {
505
+ const parent = resolved.path ? `${name} ${resolved.path}` : name;
506
+ writeln(`Error: '${resolved.error}' is not a command for '${parent}'.`);
507
+ exit(1);
508
+ return;
509
+ }
510
+ if ('fetchGateway' in resolved) {
511
+ writeln('--schema is not supported for fetch commands.');
512
+ exit(1);
513
+ return;
514
+ }
515
+ const cmd = resolved.command;
516
+ const format = formatExplicit ? formatFlag : 'toon';
517
+ const result = {};
518
+ if (cmd.args)
519
+ result.args = Schema.toJsonSchema(cmd.args);
520
+ if (cmd.env)
521
+ result.env = Schema.toJsonSchema(cmd.env);
522
+ if (cmd.options)
523
+ result.options = Schema.toJsonSchema(cmd.options);
524
+ if (cmd.output)
525
+ result.output = Schema.toJsonSchema(cmd.output);
526
+ writeln(Formatter.format(result, format));
527
+ return;
528
+ }
482
529
  if ('help' in resolved) {
483
530
  writeln(Help.formatRoot(`${name} ${resolved.path}`, {
484
531
  description: resolved.description,
@@ -499,28 +546,75 @@ async function serveImpl(name, commands, argv, options = {}) {
499
546
  // Resolve outputPolicy: command/group → CLI-level → default ('all')
500
547
  const effectiveOutputPolicy = ('outputPolicy' in resolved && resolved.outputPolicy) || options.outputPolicy;
501
548
  const renderOutput = !(human && !formatExplicit && effectiveOutputPolicy === 'agent-only');
549
+ const filterPaths = filterOutput ? Filter.parse(filterOutput) : undefined;
550
+ function truncate(s) {
551
+ if (tokenLimit == null && tokenOffset == null)
552
+ return { text: s, truncated: false };
553
+ const total = estimateTokenCount(s);
554
+ const offset = tokenOffset ?? 0;
555
+ const end = tokenLimit != null ? offset + tokenLimit : total;
556
+ if (offset === 0 && end >= total)
557
+ return { text: s, truncated: false };
558
+ const sliced = sliceByTokens(s, offset, end);
559
+ const actualEnd = Math.min(end, total);
560
+ const nextOffset = actualEnd < total ? actualEnd : undefined;
561
+ return {
562
+ text: `${sliced}\n[truncated: showing tokens ${offset}–${actualEnd} of ${total}]`,
563
+ truncated: true,
564
+ nextOffset,
565
+ };
566
+ }
502
567
  function write(output) {
568
+ if (filterPaths && output.ok && output.data != null)
569
+ output = { ...output, data: Filter.apply(output.data, filterPaths) };
570
+ if (tokenCount) {
571
+ const base = output.ok ? output.data : output.error;
572
+ const formatted = base != null ? Formatter.format(base, format) : '';
573
+ return writeln(String(estimateTokenCount(formatted)));
574
+ }
503
575
  const cta = output.meta.cta;
504
576
  if (human && !verbose) {
505
- if (output.ok && output.data != null && renderOutput)
506
- writeln(Formatter.format(output.data, format));
577
+ if (output.ok && output.data != null && renderOutput) {
578
+ const t = truncate(Formatter.format(output.data, format));
579
+ writeln(t.text);
580
+ }
507
581
  else if (!output.ok)
508
582
  writeln(formatHumanError(output.error));
509
583
  if (cta)
510
584
  writeln(formatHumanCta(cta));
511
585
  return;
512
586
  }
513
- if (verbose)
587
+ if (verbose) {
588
+ if (tokenLimit != null || tokenOffset != null) {
589
+ // Truncate data separately so meta (including nextOffset) is always visible
590
+ const dataFormatted = output.ok && output.data != null
591
+ ? Formatter.format(output.data, format)
592
+ : !output.ok
593
+ ? Formatter.format(output.error, format)
594
+ : '';
595
+ const t = truncate(dataFormatted);
596
+ if (t.truncated) {
597
+ const envelope = output.ok
598
+ ? { ok: true, data: t.text }
599
+ : { ok: false, error: t.text };
600
+ const meta = { ...output.meta };
601
+ if (t.nextOffset != null)
602
+ meta.nextOffset = t.nextOffset;
603
+ envelope.meta = meta;
604
+ return writeln(Formatter.format(envelope, format));
605
+ }
606
+ }
514
607
  return writeln(Formatter.format(output, format));
608
+ }
515
609
  const base = output.ok ? output.data : output.error;
516
610
  const formatted = Formatter.format(base, format);
517
611
  if (!cta) {
518
612
  if (formatted)
519
- writeln(formatted);
613
+ writeln(truncate(formatted).text);
520
614
  return;
521
615
  }
522
616
  const payload = typeof base === 'object' && base !== null ? { ...base, cta } : { data: base, cta };
523
- writeln(Formatter.format(payload, format));
617
+ writeln(truncate(Formatter.format(payload, format)).text);
524
618
  }
525
619
  if ('error' in effective) {
526
620
  const helpCmd = effective.path ? `${name} ${effective.path} --help` : `${name} --help`;
@@ -573,6 +667,7 @@ async function serveImpl(name, commands, argv, options = {}) {
573
667
  human,
574
668
  renderOutput,
575
669
  verbose,
670
+ truncate,
576
671
  write,
577
672
  writeln,
578
673
  exit,
@@ -617,6 +712,8 @@ async function serveImpl(name, commands, argv, options = {}) {
617
712
  command: path,
618
713
  env: cliEnv,
619
714
  error: errorFn,
715
+ format,
716
+ formatExplicit,
620
717
  name,
621
718
  set(key, value) { varsMap[key] = value; },
622
719
  var: varsMap,
@@ -625,13 +722,14 @@ async function serveImpl(name, commands, argv, options = {}) {
625
722
  const handleMwSentinel = (result) => {
626
723
  if (!isSentinel(result) || result[sentinel] !== 'error')
627
724
  return;
628
- const cta = formatCtaBlock(name, result.cta);
725
+ const err = result;
726
+ const cta = formatCtaBlock(name, err.cta);
629
727
  write({
630
728
  ok: false,
631
- error: { code: result.code, message: result.message, ...(result.retryable !== undefined ? { retryable: result.retryable } : undefined) },
729
+ error: { code: err.code, message: err.message, ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined) },
632
730
  meta: { command: path, duration: `${Math.round(performance.now() - start)}ms`, ...(cta ? { cta } : undefined) },
633
731
  });
634
- exit(1);
732
+ exit(err.exitCode ?? 1);
635
733
  };
636
734
  const composed = fetchMiddleware.reduceRight((next, mw) => async () => { handleMwSentinel(await mw(mwCtx, next)); }, runFetch);
637
735
  await composed();
@@ -649,7 +747,7 @@ async function serveImpl(name, commands, argv, options = {}) {
649
747
  },
650
748
  meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
651
749
  });
652
- exit(1);
750
+ exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
653
751
  }
654
752
  return;
655
753
  }
@@ -684,10 +782,12 @@ async function serveImpl(name, commands, argv, options = {}) {
684
782
  agent: !human,
685
783
  args,
686
784
  env,
785
+ error: errorFn,
786
+ format,
787
+ formatExplicit,
687
788
  name,
688
- options: parsedOptions,
689
789
  ok: okFn,
690
- error: errorFn,
790
+ options: parsedOptions,
691
791
  var: varsMap,
692
792
  version: options.version,
693
793
  });
@@ -702,6 +802,7 @@ async function serveImpl(name, commands, argv, options = {}) {
702
802
  human,
703
803
  renderOutput,
704
804
  verbose,
805
+ truncate,
705
806
  write,
706
807
  writeln,
707
808
  exit,
@@ -723,12 +824,13 @@ async function serveImpl(name, commands, argv, options = {}) {
723
824
  });
724
825
  }
725
826
  else {
827
+ const err = awaited;
726
828
  write({
727
829
  ok: false,
728
830
  error: {
729
- code: awaited.code,
730
- message: awaited.message,
731
- ...(awaited.retryable !== undefined ? { retryable: awaited.retryable } : undefined),
831
+ code: err.code,
832
+ message: err.message,
833
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
732
834
  },
733
835
  meta: {
734
836
  command: path,
@@ -736,7 +838,7 @@ async function serveImpl(name, commands, argv, options = {}) {
736
838
  ...(cta ? { cta } : undefined),
737
839
  },
738
840
  });
739
- exit(1);
841
+ exit(err.exitCode ?? 1);
740
842
  }
741
843
  }
742
844
  else {
@@ -761,6 +863,8 @@ async function serveImpl(name, commands, argv, options = {}) {
761
863
  command: path,
762
864
  env: cliEnv,
763
865
  error: errorFn,
866
+ format,
867
+ formatExplicit,
764
868
  name,
765
869
  set(key, value) {
766
870
  varsMap[key] = value;
@@ -771,13 +875,14 @@ async function serveImpl(name, commands, argv, options = {}) {
771
875
  const handleMwSentinel = (result) => {
772
876
  if (!isSentinel(result) || result[sentinel] !== 'error')
773
877
  return;
774
- const cta = formatCtaBlock(name, result.cta);
878
+ const err = result;
879
+ const cta = formatCtaBlock(name, err.cta);
775
880
  write({
776
881
  ok: false,
777
882
  error: {
778
- code: result.code,
779
- message: result.message,
780
- ...(result.retryable !== undefined ? { retryable: result.retryable } : undefined),
883
+ code: err.code,
884
+ message: err.message,
885
+ ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
781
886
  },
782
887
  meta: {
783
888
  command: path,
@@ -785,7 +890,7 @@ async function serveImpl(name, commands, argv, options = {}) {
785
890
  ...(cta ? { cta } : undefined),
786
891
  },
787
892
  });
788
- exit(1);
893
+ exit(err.exitCode ?? 1);
789
894
  };
790
895
  const composed = allMiddleware.reduceRight((next, mw) => async () => {
791
896
  handleMwSentinel(await mw(mwCtx, next));
@@ -820,8 +925,235 @@ async function serveImpl(name, commands, argv, options = {}) {
820
925
  return;
821
926
  }
822
927
  write(errorOutput);
823
- exit(1);
928
+ exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
929
+ }
930
+ }
931
+ /** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
932
+ function createMcpHttpHandler(name, version) {
933
+ let transport;
934
+ return async (req, commands) => {
935
+ if (!transport) {
936
+ const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
937
+ const { WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
938
+ const server = new McpServer({ name, version });
939
+ for (const tool of Mcp.collectTools(commands, [])) {
940
+ const mergedShape = {
941
+ ...tool.command.args?.shape,
942
+ ...tool.command.options?.shape,
943
+ };
944
+ const hasInput = Object.keys(mergedShape).length > 0;
945
+ server.registerTool(tool.name, {
946
+ ...(tool.description ? { description: tool.description } : undefined),
947
+ ...(hasInput ? { inputSchema: mergedShape } : undefined),
948
+ }, async (...callArgs) => {
949
+ const params = hasInput ? callArgs[0] : {};
950
+ return Mcp.callTool(tool, params);
951
+ });
952
+ }
953
+ transport = new WebStandardStreamableHTTPServerTransport({
954
+ sessionIdGenerator: () => crypto.randomUUID(),
955
+ enableJsonResponse: true,
956
+ });
957
+ await server.connect(transport);
958
+ }
959
+ return transport.handleRequest(req);
960
+ };
961
+ }
962
+ /** @internal Handles an HTTP request by resolving a command and returning a JSON Response. */
963
+ async function fetchImpl(name, commands, req, options = {}) {
964
+ const start = performance.now();
965
+ const url = new URL(req.url);
966
+ const segments = url.pathname.split('/').filter(Boolean);
967
+ // MCP over HTTP: route /mcp to the MCP transport
968
+ if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
969
+ return options.mcpHandler(req, commands);
970
+ // .well-known/skills/ — Agent Skills Discovery (RFC)
971
+ if (segments[0] === '.well-known' &&
972
+ segments[1] === 'skills' &&
973
+ segments.length >= 3 &&
974
+ req.method === 'GET') {
975
+ const groups = new Map();
976
+ const cmds = collectSkillCommands(commands, [], groups);
977
+ // GET /.well-known/skills/index.json
978
+ if (segments[2] === 'index.json' && segments.length === 3) {
979
+ const files = Skill.split(name, cmds, 1, groups);
980
+ const skills = files.map((f) => {
981
+ const descMatch = f.content.match(/^description:\s*(.+)$/m);
982
+ return {
983
+ name: f.dir || name,
984
+ description: descMatch?.[1] ?? '',
985
+ files: ['SKILL.md'],
986
+ };
987
+ });
988
+ return new Response(JSON.stringify({ skills }), {
989
+ status: 200,
990
+ headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=300' },
991
+ });
992
+ }
993
+ // GET /.well-known/skills/{skill-name}/SKILL.md
994
+ if (segments.length === 4 && segments[3] === 'SKILL.md') {
995
+ const skillName = segments[2];
996
+ const files = Skill.split(name, cmds, 1, groups);
997
+ const file = files.find((f) => (f.dir || name) === skillName);
998
+ if (file)
999
+ return new Response(file.content, {
1000
+ status: 200,
1001
+ headers: { 'content-type': 'text/markdown', 'cache-control': 'public, max-age=300' },
1002
+ });
1003
+ return new Response('Not Found', { status: 404 });
1004
+ }
1005
+ return new Response('Not Found', { status: 404 });
1006
+ }
1007
+ // Parse options from search params (GET) or body (non-GET)
1008
+ let inputOptions = {};
1009
+ if (req.method === 'GET')
1010
+ for (const [key, value] of url.searchParams)
1011
+ inputOptions[key] = value;
1012
+ else {
1013
+ try {
1014
+ const contentType = req.headers.get('content-type') ?? '';
1015
+ if (contentType.includes('application/json'))
1016
+ inputOptions = (await req.json());
1017
+ }
1018
+ catch { }
824
1019
  }
1020
+ function jsonResponse(body, status) {
1021
+ return new Response(JSON.stringify(body), {
1022
+ status,
1023
+ headers: { 'content-type': 'application/json' },
1024
+ });
1025
+ }
1026
+ // Resolve command from path segments
1027
+ if (segments.length === 0) {
1028
+ // Root path
1029
+ if (options.rootCommand)
1030
+ return executeCommand(name, options.rootCommand, [], inputOptions, start, options);
1031
+ return jsonResponse({ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: 'No root command defined.' }, meta: { command: '/', duration: `${Math.round(performance.now() - start)}ms` } }, 404);
1032
+ }
1033
+ const resolved = resolveCommand(commands, segments);
1034
+ if ('error' in resolved)
1035
+ return jsonResponse({ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: `'${resolved.error}' is not a command for '${resolved.path ? `${name} ${resolved.path}` : name}'.` }, meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` } }, 404);
1036
+ if ('help' in resolved)
1037
+ return jsonResponse({ ok: false, error: { code: 'COMMAND_NOT_FOUND', message: `'${resolved.path}' is a command group. Specify a subcommand.` }, meta: { command: resolved.path, duration: `${Math.round(performance.now() - start)}ms` } }, 404);
1038
+ if ('fetchGateway' in resolved)
1039
+ return resolved.fetchGateway.fetch(req);
1040
+ const { command, path, rest } = resolved;
1041
+ return executeCommand(path, command, rest, inputOptions, start, options);
1042
+ }
1043
+ /** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
1044
+ async function executeCommand(path, command, rest, inputOptions, start, options) {
1045
+ function jsonResponse(body, status) {
1046
+ return new Response(JSON.stringify(body), {
1047
+ status,
1048
+ headers: { 'content-type': 'application/json' },
1049
+ });
1050
+ }
1051
+ const sentinel_ = Symbol.for('incur.sentinel');
1052
+ const varsMap = options.vars ? options.vars.parse({}) : {};
1053
+ let response;
1054
+ const runCommand = async () => {
1055
+ const { args } = Parser.parse(rest, { args: command.args });
1056
+ const parsedOptions = command.options ? command.options.parse(inputOptions) : {};
1057
+ const okFn = (data) => ({ [sentinel_]: 'ok', data });
1058
+ const errorFn = (opts) => ({ [sentinel_]: 'error', ...opts });
1059
+ const result = command.run({
1060
+ agent: true,
1061
+ args,
1062
+ env: {},
1063
+ error: errorFn,
1064
+ format: 'json',
1065
+ formatExplicit: true,
1066
+ name: path,
1067
+ ok: okFn,
1068
+ options: parsedOptions,
1069
+ var: varsMap,
1070
+ version: undefined,
1071
+ });
1072
+ // Streaming path — async generator → NDJSON response
1073
+ if (isAsyncGenerator(result)) {
1074
+ const stream = new ReadableStream({
1075
+ async start(controller) {
1076
+ const encoder = new TextEncoder();
1077
+ try {
1078
+ let returnValue;
1079
+ while (true) {
1080
+ const { value, done } = await result.next();
1081
+ if (done) {
1082
+ returnValue = value;
1083
+ break;
1084
+ }
1085
+ if (isSentinel(value) && value[sentinel] === 'error') {
1086
+ const tagged = value;
1087
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'));
1088
+ controller.close();
1089
+ return;
1090
+ }
1091
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
1092
+ }
1093
+ const meta = { command: path };
1094
+ if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
1095
+ const tagged = returnValue;
1096
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: tagged.code, message: tagged.message } }) + '\n'));
1097
+ }
1098
+ else {
1099
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'));
1100
+ }
1101
+ }
1102
+ catch (error) {
1103
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', ok: false, error: { code: 'UNKNOWN', message: error instanceof Error ? error.message : String(error) } }) + '\n'));
1104
+ }
1105
+ controller.close();
1106
+ },
1107
+ });
1108
+ response = new Response(stream, { status: 200, headers: { 'content-type': 'application/x-ndjson' } });
1109
+ return;
1110
+ }
1111
+ const awaited = await result;
1112
+ const duration = `${Math.round(performance.now() - start)}ms`;
1113
+ if (typeof awaited === 'object' && awaited !== null && sentinel_ in awaited) {
1114
+ const tagged = awaited;
1115
+ if (tagged[sentinel_] === 'error')
1116
+ response = jsonResponse({ ok: false, error: { code: tagged.code, message: tagged.message }, meta: { command: path, duration } }, 500);
1117
+ else
1118
+ response = jsonResponse({ ok: true, data: tagged.data, meta: { command: path, duration } }, 200);
1119
+ return;
1120
+ }
1121
+ response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200);
1122
+ };
1123
+ try {
1124
+ const allMiddleware = options.middlewares ?? [];
1125
+ if (allMiddleware.length > 0) {
1126
+ const errorFn = (opts) => {
1127
+ const duration = `${Math.round(performance.now() - start)}ms`;
1128
+ response = jsonResponse({ ok: false, error: { code: opts.code, message: opts.message }, meta: { command: path, duration } }, 500);
1129
+ return undefined;
1130
+ };
1131
+ const mwCtx = {
1132
+ agent: true,
1133
+ command: path,
1134
+ env: {},
1135
+ error: errorFn,
1136
+ format: 'json',
1137
+ formatExplicit: true,
1138
+ name: path,
1139
+ set(key, value) { varsMap[key] = value; },
1140
+ var: varsMap,
1141
+ version: undefined,
1142
+ };
1143
+ const composed = allMiddleware.reduceRight((next, mw) => async () => { await mw(mwCtx, next); }, runCommand);
1144
+ await composed();
1145
+ }
1146
+ else {
1147
+ await runCommand();
1148
+ }
1149
+ }
1150
+ catch (error) {
1151
+ const duration = `${Math.round(performance.now() - start)}ms`;
1152
+ if (error instanceof ValidationError)
1153
+ return jsonResponse({ ok: false, error: { code: 'VALIDATION_ERROR', message: error.message }, meta: { command: path, duration } }, 400);
1154
+ return jsonResponse({ ok: false, error: { code: error instanceof IncurError ? error.code : 'UNKNOWN', message: error instanceof Error ? error.message : String(error) }, meta: { command: path, duration } }, 500);
1155
+ }
1156
+ return response;
825
1157
  }
826
1158
  /** @internal Formats a validation error for TTY with usage hint. */
827
1159
  function formatHumanValidationError(cli, path, command, error, envSource) {
@@ -911,8 +1243,13 @@ function extractBuiltinFlags(argv) {
911
1243
  let mcp = false;
912
1244
  let help = false;
913
1245
  let version = false;
1246
+ let schema = false;
914
1247
  let format = 'toon';
915
1248
  let formatExplicit = false;
1249
+ let filterOutput;
1250
+ let tokenLimit;
1251
+ let tokenOffset;
1252
+ let tokenCount = false;
916
1253
  const rest = [];
917
1254
  for (let i = 0; i < argv.length; i++) {
918
1255
  const token = argv[i];
@@ -926,6 +1263,8 @@ function extractBuiltinFlags(argv) {
926
1263
  help = true;
927
1264
  else if (token === '--version')
928
1265
  version = true;
1266
+ else if (token === '--schema')
1267
+ schema = true;
929
1268
  else if (token === '--json') {
930
1269
  format = 'json';
931
1270
  formatExplicit = true;
@@ -935,10 +1274,24 @@ function extractBuiltinFlags(argv) {
935
1274
  formatExplicit = true;
936
1275
  i++;
937
1276
  }
1277
+ else if (token === '--filter-output' && argv[i + 1]) {
1278
+ filterOutput = argv[i + 1];
1279
+ i++;
1280
+ }
1281
+ else if (token === '--token-limit' && argv[i + 1]) {
1282
+ tokenLimit = Number(argv[i + 1]);
1283
+ i++;
1284
+ }
1285
+ else if (token === '--token-offset' && argv[i + 1]) {
1286
+ tokenOffset = Number(argv[i + 1]);
1287
+ i++;
1288
+ }
1289
+ else if (token === '--token-count')
1290
+ tokenCount = true;
938
1291
  else
939
1292
  rest.push(token);
940
1293
  }
941
- return { verbose, format, formatExplicit, llms, mcp, help, version, rest };
1294
+ return { verbose, format, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, mcp, help, version, schema, rest };
942
1295
  }
943
1296
  /** @internal Collects immediate child commands/groups for help output. */
944
1297
  function collectHelpCommands(commands) {
@@ -1053,14 +1406,14 @@ async function handleStreaming(generator, ctx) {
1053
1406
  }));
1054
1407
  else
1055
1408
  ctx.writeln(formatHumanError({ code: tagged.code, message: tagged.message }));
1056
- ctx.exit(1);
1409
+ ctx.exit(tagged.exitCode ?? 1);
1057
1410
  return;
1058
1411
  }
1059
1412
  }
1060
1413
  if (useJsonl)
1061
1414
  ctx.writeln(JSON.stringify({ type: 'chunk', data: value }));
1062
1415
  else if (ctx.renderOutput)
1063
- ctx.writeln(Formatter.format(value, ctx.format));
1416
+ ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text);
1064
1417
  }
1065
1418
  // Handle return value — error() or ok() sentinel
1066
1419
  if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
@@ -1077,7 +1430,7 @@ async function handleStreaming(generator, ctx) {
1077
1430
  }));
1078
1431
  else
1079
1432
  ctx.writeln(formatHumanError({ code: err.code, message: err.message }));
1080
- ctx.exit(1);
1433
+ ctx.exit(err.exitCode ?? 1);
1081
1434
  return;
1082
1435
  }
1083
1436
  const cta = isSentinel(returnValue) && returnValue[sentinel] === 'ok'
@@ -1111,7 +1464,7 @@ async function handleStreaming(generator, ctx) {
1111
1464
  code: 'UNKNOWN',
1112
1465
  message: error instanceof Error ? error.message : String(error),
1113
1466
  }));
1114
- ctx.exit(1);
1467
+ ctx.exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
1115
1468
  }
1116
1469
  }
1117
1470
  else {
@@ -1140,7 +1493,7 @@ async function handleStreaming(generator, ctx) {
1140
1493
  duration: `${Math.round(performance.now() - ctx.start)}ms`,
1141
1494
  },
1142
1495
  });
1143
- ctx.exit(1);
1496
+ ctx.exit(tagged.exitCode ?? 1);
1144
1497
  return;
1145
1498
  }
1146
1499
  }
@@ -1160,7 +1513,7 @@ async function handleStreaming(generator, ctx) {
1160
1513
  duration: `${Math.round(performance.now() - ctx.start)}ms`,
1161
1514
  },
1162
1515
  });
1163
- ctx.exit(1);
1516
+ ctx.exit(err.exitCode ?? 1);
1164
1517
  return;
1165
1518
  }
1166
1519
  const cta = isSentinel(returnValue) && returnValue[sentinel] === 'ok'
@@ -1188,7 +1541,7 @@ async function handleStreaming(generator, ctx) {
1188
1541
  duration: `${Math.round(performance.now() - ctx.start)}ms`,
1189
1542
  },
1190
1543
  });
1191
- ctx.exit(1);
1544
+ ctx.exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
1192
1545
  }
1193
1546
  }
1194
1547
  }