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/README.md +158 -9
- package/SKILL.md +149 -0
- package/dist/Cli.d.ts +16 -4
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +384 -31
- package/dist/Cli.js.map +1 -1
- package/dist/Errors.d.ts +4 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +3 -0
- package/dist/Errors.js.map +1 -1
- package/dist/Filter.d.ts +14 -0
- package/dist/Filter.d.ts.map +1 -0
- package/dist/Filter.js +134 -0
- package/dist/Filter.js.map +1 -0
- package/dist/Help.js +5 -0
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +26 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -2
- package/dist/Mcp.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +8 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +2 -1
- package/src/Cli.test-d.ts +25 -0
- package/src/Cli.test.ts +829 -0
- package/src/Cli.ts +492 -37
- package/src/Errors.ts +5 -0
- package/src/Filter.test.ts +237 -0
- package/src/Filter.ts +139 -0
- package/src/Help.test.ts +35 -0
- package/src/Help.ts +5 -0
- package/src/Mcp.ts +3 -3
- package/src/e2e.test.ts +715 -0
- package/src/index.ts +1 -0
- package/src/middleware.ts +9 -2
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
|
-
|
|
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
|
|
725
|
+
const err = result;
|
|
726
|
+
const cta = formatCtaBlock(name, err.cta);
|
|
629
727
|
write({
|
|
630
728
|
ok: false,
|
|
631
|
-
error: { code:
|
|
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
|
-
|
|
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:
|
|
730
|
-
message:
|
|
731
|
-
...(
|
|
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
|
|
878
|
+
const err = result;
|
|
879
|
+
const cta = formatCtaBlock(name, err.cta);
|
|
775
880
|
write({
|
|
776
881
|
ok: false,
|
|
777
882
|
error: {
|
|
778
|
-
code:
|
|
779
|
-
message:
|
|
780
|
-
...(
|
|
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
|
}
|