incur 0.1.4 → 0.1.6
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 +81 -31
- package/SKILL.md +45 -11
- package/dist/Cli.d.ts +27 -0
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +80 -61
- package/dist/Cli.js.map +1 -1
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +22 -9
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.js +4 -4
- package/dist/bin.js.map +1 -1
- package/dist/internal/agents.d.ts +7 -0
- package/dist/internal/agents.d.ts.map +1 -1
- package/dist/internal/agents.js +30 -2
- package/dist/internal/agents.js.map +1 -1
- package/examples/npm/cli.ts +8 -8
- package/package.json +1 -1
- package/src/Cli.test-d.ts +12 -12
- package/src/Cli.test.ts +446 -29
- package/src/Cli.ts +146 -82
- package/src/Mcp.test.ts +6 -6
- package/src/SyncSkills.ts +26 -9
- package/src/bin.ts +4 -4
- package/src/e2e.test.ts +93 -39
- package/src/internal/agents.ts +32 -2
package/src/Cli.ts
CHANGED
|
@@ -128,7 +128,9 @@ export function create<
|
|
|
128
128
|
const output extends z.ZodType | undefined = undefined,
|
|
129
129
|
>(
|
|
130
130
|
definition: create.Options<args, env, opts, output> & { name: string; run: Function },
|
|
131
|
-
): Cli<{
|
|
131
|
+
): Cli<{
|
|
132
|
+
[key in (typeof definition)['name']]: { args: InferOutput<args>; options: InferOutput<opts> }
|
|
133
|
+
}>
|
|
132
134
|
/** Creates a router CLI from a single options object (e.g. package.json). */
|
|
133
135
|
export function create<
|
|
134
136
|
const args extends z.ZodObject<any> | undefined = undefined,
|
|
@@ -162,7 +164,13 @@ export function create(
|
|
|
162
164
|
}
|
|
163
165
|
const sub = nameOrCli as Cli
|
|
164
166
|
const subCommands = toCommands.get(sub)!
|
|
165
|
-
|
|
167
|
+
const subOutputPolicy = toOutputPolicy.get(sub)
|
|
168
|
+
commands.set(sub.name, {
|
|
169
|
+
_group: true,
|
|
170
|
+
description: sub.description,
|
|
171
|
+
commands: subCommands,
|
|
172
|
+
...(subOutputPolicy ? { outputPolicy: subOutputPolicy } : undefined),
|
|
173
|
+
})
|
|
166
174
|
return cli
|
|
167
175
|
},
|
|
168
176
|
|
|
@@ -172,6 +180,7 @@ export function create(
|
|
|
172
180
|
description: def.description,
|
|
173
181
|
format: def.format,
|
|
174
182
|
mcp: def.mcp,
|
|
183
|
+
outputPolicy: def.outputPolicy,
|
|
175
184
|
rootCommand: rootDef,
|
|
176
185
|
sync: def.sync,
|
|
177
186
|
version: def.version,
|
|
@@ -180,6 +189,7 @@ export function create(
|
|
|
180
189
|
}
|
|
181
190
|
|
|
182
191
|
if (rootDef) toRootDefinition.set(cli as unknown as Root, rootDef)
|
|
192
|
+
if (def.outputPolicy) toOutputPolicy.set(cli, def.outputPolicy)
|
|
183
193
|
toCommands.set(cli, commands)
|
|
184
194
|
return cli
|
|
185
195
|
}
|
|
@@ -210,25 +220,37 @@ export declare namespace create {
|
|
|
210
220
|
options?: options | undefined
|
|
211
221
|
/** Zod schema for the return value. */
|
|
212
222
|
output?: output | undefined
|
|
223
|
+
/**
|
|
224
|
+
* Controls when output data is displayed. Inherited by child commands when set on a group or root CLI.
|
|
225
|
+
*
|
|
226
|
+
* - `'all'` — displays to both humans and agents.
|
|
227
|
+
* - `'agent-only'` — suppresses data output in human/TTY mode while still returning it to agents.
|
|
228
|
+
*
|
|
229
|
+
* @default 'all'
|
|
230
|
+
*/
|
|
231
|
+
outputPolicy?: OutputPolicy | undefined
|
|
213
232
|
/** Alternative usage patterns shown in help output. */
|
|
214
233
|
usage?: Usage<args, options>[] | undefined
|
|
215
234
|
/** The root command handler. When provided, creates a leaf CLI with no subcommands. */
|
|
216
235
|
run?:
|
|
217
236
|
| ((context: {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
/** Whether the consumer is an agent (stdout is not a TTY). */
|
|
238
|
+
agent: boolean
|
|
239
|
+
/** Positional arguments. */
|
|
240
|
+
args: InferOutput<args>
|
|
241
|
+
/** Parsed environment variables. */
|
|
242
|
+
env: InferOutput<env>
|
|
243
|
+
/** Return an error result with optional CTAs. */
|
|
244
|
+
error: (options: {
|
|
245
|
+
code: string
|
|
246
|
+
cta?: CtaBlock | undefined
|
|
247
|
+
message: string
|
|
248
|
+
retryable?: boolean | undefined
|
|
249
|
+
}) => never
|
|
250
|
+
/** Return a success result with optional metadata (e.g. CTAs). */
|
|
251
|
+
ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
|
|
252
|
+
options: InferOutput<options>
|
|
253
|
+
}) =>
|
|
232
254
|
| InferReturn<output>
|
|
233
255
|
| Promise<InferReturn<output>>
|
|
234
256
|
| AsyncGenerator<InferReturn<output>, unknown, unknown>)
|
|
@@ -300,8 +322,8 @@ async function serveImpl(
|
|
|
300
322
|
return
|
|
301
323
|
}
|
|
302
324
|
|
|
303
|
-
// Human mode:
|
|
304
|
-
const human =
|
|
325
|
+
// Human mode: stdout is a TTY.
|
|
326
|
+
const human = process.stdout.isTTY === true
|
|
305
327
|
|
|
306
328
|
function writeln(s: string) {
|
|
307
329
|
stdout(s.endsWith('\n') ? s : `${s}\n`)
|
|
@@ -377,14 +399,15 @@ async function serveImpl(
|
|
|
377
399
|
const rest = filtered.slice(skillsIdx + 2)
|
|
378
400
|
const depthArg = rest.indexOf('--depth')
|
|
379
401
|
const depthEq = rest.find((t) => t.startsWith('--depth='))
|
|
380
|
-
const depth =
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
402
|
+
const depth =
|
|
403
|
+
depthArg !== -1
|
|
404
|
+
? Number(rest[depthArg + 1])
|
|
405
|
+
: depthEq
|
|
406
|
+
? Number(depthEq.split('=')[1])
|
|
407
|
+
: (options.sync?.depth ?? 1)
|
|
385
408
|
const global = rest.includes('--no-global') ? false : undefined
|
|
386
409
|
try {
|
|
387
|
-
|
|
410
|
+
stdout('Syncing...')
|
|
388
411
|
const result = await SyncSkills.sync(name, commands, {
|
|
389
412
|
cwd: options.sync?.cwd,
|
|
390
413
|
depth,
|
|
@@ -392,31 +415,30 @@ async function serveImpl(
|
|
|
392
415
|
global,
|
|
393
416
|
include: options.sync?.include,
|
|
394
417
|
})
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (suggestions && suggestions.length > 0) {
|
|
412
|
-
lines.push('')
|
|
413
|
-
lines.push(`Your agent can now use ${name}. Try asking:`)
|
|
414
|
-
for (const s of suggestions) lines.push(` "${s}"`)
|
|
415
|
-
}
|
|
418
|
+
stdout('\r\x1b[K')
|
|
419
|
+
const lines: string[] = []
|
|
420
|
+
const skillLabel = (s: (typeof result.skills)[number]) =>
|
|
421
|
+
s.external || s.name === name ? s.name : `${name}-${s.name}`
|
|
422
|
+
const maxLen = Math.max(...result.skills.map((s) => skillLabel(s).length))
|
|
423
|
+
for (const s of result.skills) {
|
|
424
|
+
const label = skillLabel(s)
|
|
425
|
+
const padding = s.description
|
|
426
|
+
? `${' '.repeat(maxLen - label.length)} ${s.description}`
|
|
427
|
+
: ''
|
|
428
|
+
lines.push(` ✓ ${label}${padding}`)
|
|
429
|
+
}
|
|
430
|
+
lines.push('')
|
|
431
|
+
lines.push(`${result.skills.length} skill${result.skills.length === 1 ? '' : 's'} synced`)
|
|
432
|
+
const suggestions = options.sync?.suggestions
|
|
433
|
+
if (suggestions && suggestions.length > 0) {
|
|
416
434
|
lines.push('')
|
|
417
|
-
lines.push(`
|
|
418
|
-
|
|
419
|
-
}
|
|
435
|
+
lines.push(`Your agent can now use ${name}. Try asking:`)
|
|
436
|
+
for (const s of suggestions) lines.push(` "${s}"`)
|
|
437
|
+
}
|
|
438
|
+
lines.push('')
|
|
439
|
+
lines.push(`Run \`${name} --help\` to see the full command reference.`)
|
|
440
|
+
writeln(lines.join('\n'))
|
|
441
|
+
if (verbose || formatExplicit) {
|
|
420
442
|
const output: Record<string, unknown> = { skills: result.paths }
|
|
421
443
|
if (verbose && result.agents.length > 0) output.agents = result.agents
|
|
422
444
|
writeln(Formatter.format(output, formatExplicit ? formatFlag : 'toon'))
|
|
@@ -463,27 +485,26 @@ async function serveImpl(
|
|
|
463
485
|
}
|
|
464
486
|
|
|
465
487
|
try {
|
|
466
|
-
|
|
488
|
+
stdout('Registering MCP server...')
|
|
467
489
|
const result = await SyncMcp.register(name, {
|
|
468
490
|
command,
|
|
469
491
|
global,
|
|
470
492
|
agents,
|
|
471
493
|
})
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
494
|
+
stdout('\r\x1b[K')
|
|
495
|
+
const lines: string[] = []
|
|
496
|
+
lines.push(`✓ Registered ${name} as MCP server`)
|
|
497
|
+
if (result.agents.length > 0) lines.push(` Agents: ${result.agents.join(', ')}`)
|
|
498
|
+
lines.push('')
|
|
499
|
+
lines.push(`Agents can now use ${name} tools.`)
|
|
500
|
+
const suggestions = options.sync?.suggestions
|
|
501
|
+
if (suggestions && suggestions.length > 0) {
|
|
477
502
|
lines.push('')
|
|
478
|
-
lines.push(
|
|
479
|
-
const suggestions
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
for (const s of suggestions) lines.push(` "${s}"`)
|
|
484
|
-
}
|
|
485
|
-
writeln(lines.join('\n'))
|
|
486
|
-
} else
|
|
503
|
+
lines.push('Try asking:')
|
|
504
|
+
for (const s of suggestions) lines.push(` "${s}"`)
|
|
505
|
+
}
|
|
506
|
+
writeln(lines.join('\n'))
|
|
507
|
+
if (verbose || formatExplicit)
|
|
487
508
|
writeln(
|
|
488
509
|
Formatter.format(
|
|
489
510
|
{ name, command: result.command, agents: result.agents },
|
|
@@ -526,7 +547,7 @@ async function serveImpl(
|
|
|
526
547
|
|
|
527
548
|
const resolved =
|
|
528
549
|
filtered.length === 0 && options.rootCommand
|
|
529
|
-
?
|
|
550
|
+
? { command: options.rootCommand, path: name, rest: [] as string[] }
|
|
530
551
|
: resolveCommand(commands, filtered)
|
|
531
552
|
|
|
532
553
|
// --help after a command → show help for that command
|
|
@@ -607,10 +628,22 @@ async function serveImpl(
|
|
|
607
628
|
const resolvedFormat = 'command' in resolved && resolved.command.format
|
|
608
629
|
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
|
|
609
630
|
|
|
631
|
+
// Fall back to root command when no subcommand matches
|
|
632
|
+
const effective =
|
|
633
|
+
'error' in resolved && options.rootCommand
|
|
634
|
+
? { command: options.rootCommand, path: name, rest: filtered }
|
|
635
|
+
: resolved
|
|
636
|
+
|
|
637
|
+
// Resolve outputPolicy: command/group → CLI-level → default ('all')
|
|
638
|
+
const effectiveOutputPolicy =
|
|
639
|
+
('command' in resolved && resolved.outputPolicy) || options.outputPolicy
|
|
640
|
+
const renderOutput = !(human && !formatExplicit && effectiveOutputPolicy === 'agent-only')
|
|
641
|
+
|
|
610
642
|
function write(output: Output) {
|
|
611
643
|
const cta = output.meta.cta
|
|
612
|
-
if (human) {
|
|
613
|
-
if (output.ok && output.data != null
|
|
644
|
+
if (human && !verbose) {
|
|
645
|
+
if (output.ok && output.data != null && renderOutput)
|
|
646
|
+
writeln(Formatter.format(output.data, format))
|
|
614
647
|
else if (!output.ok) writeln(formatHumanError(output.error))
|
|
615
648
|
if (cta) writeln(formatHumanCta(cta))
|
|
616
649
|
return
|
|
@@ -627,16 +660,10 @@ async function serveImpl(
|
|
|
627
660
|
writeln(Formatter.format(payload, format))
|
|
628
661
|
}
|
|
629
662
|
|
|
630
|
-
// Fall back to root command when no subcommand matches
|
|
631
|
-
const effective =
|
|
632
|
-
'error' in resolved && options.rootCommand
|
|
633
|
-
? { command: options.rootCommand, path: name, rest: filtered }
|
|
634
|
-
: resolved
|
|
635
|
-
|
|
636
663
|
if ('error' in effective) {
|
|
637
664
|
const helpCmd = effective.path ? `${name} ${effective.path} --help` : `${name} --help`
|
|
638
665
|
const message = `'${effective.error}' is not a command. See '${helpCmd}' for a list of available commands.`
|
|
639
|
-
if (human) {
|
|
666
|
+
if (human && !verbose) {
|
|
640
667
|
writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }))
|
|
641
668
|
exit(1)
|
|
642
669
|
return
|
|
@@ -678,6 +705,7 @@ async function serveImpl(
|
|
|
678
705
|
}
|
|
679
706
|
|
|
680
707
|
const result = command.run({
|
|
708
|
+
agent: !human,
|
|
681
709
|
args,
|
|
682
710
|
env,
|
|
683
711
|
options: parsedOptions,
|
|
@@ -694,6 +722,7 @@ async function serveImpl(
|
|
|
694
722
|
format,
|
|
695
723
|
formatExplicit,
|
|
696
724
|
human,
|
|
725
|
+
renderOutput,
|
|
697
726
|
verbose,
|
|
698
727
|
write,
|
|
699
728
|
writeln,
|
|
@@ -762,7 +791,7 @@ async function serveImpl(
|
|
|
762
791
|
},
|
|
763
792
|
}
|
|
764
793
|
|
|
765
|
-
if (human && error instanceof ValidationError) {
|
|
794
|
+
if (human && !formatExplicit && error instanceof ValidationError) {
|
|
766
795
|
writeln(formatHumanValidationError(name, path, command, error))
|
|
767
796
|
exit(1)
|
|
768
797
|
return
|
|
@@ -804,7 +833,12 @@ function resolveCommand(
|
|
|
804
833
|
commands: Map<string, CommandEntry>,
|
|
805
834
|
tokens: string[],
|
|
806
835
|
):
|
|
807
|
-
| {
|
|
836
|
+
| {
|
|
837
|
+
command: CommandDefinition<any, any, any>
|
|
838
|
+
outputPolicy?: OutputPolicy | undefined
|
|
839
|
+
path: string
|
|
840
|
+
rest: string[]
|
|
841
|
+
}
|
|
808
842
|
| {
|
|
809
843
|
help: true
|
|
810
844
|
path: string
|
|
@@ -819,8 +853,10 @@ function resolveCommand(
|
|
|
819
853
|
let entry = commands.get(first)!
|
|
820
854
|
const path = [first]
|
|
821
855
|
let remaining = rest
|
|
856
|
+
let inheritedOutputPolicy: OutputPolicy | undefined
|
|
822
857
|
|
|
823
858
|
while (isGroup(entry)) {
|
|
859
|
+
if (entry.outputPolicy) inheritedOutputPolicy = entry.outputPolicy
|
|
824
860
|
const next = remaining[0]
|
|
825
861
|
if (!next)
|
|
826
862
|
return {
|
|
@@ -840,7 +876,13 @@ function resolveCommand(
|
|
|
840
876
|
entry = child
|
|
841
877
|
}
|
|
842
878
|
|
|
843
|
-
|
|
879
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
880
|
+
return {
|
|
881
|
+
command: entry,
|
|
882
|
+
path: path.join(' '),
|
|
883
|
+
rest: remaining,
|
|
884
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
885
|
+
}
|
|
844
886
|
}
|
|
845
887
|
|
|
846
888
|
/** @internal Options for serveImpl, extending public serve.Options with internal metadata. */
|
|
@@ -849,6 +891,8 @@ declare namespace serveImpl {
|
|
|
849
891
|
description?: string | undefined
|
|
850
892
|
/** CLI-level default output format. */
|
|
851
893
|
format?: Formatter.Format | undefined
|
|
894
|
+
/** CLI-level default output policy. */
|
|
895
|
+
outputPolicy?: OutputPolicy | undefined
|
|
852
896
|
mcp?:
|
|
853
897
|
| {
|
|
854
898
|
agents?: string[] | undefined
|
|
@@ -921,10 +965,14 @@ export type CommandsMap = Record<
|
|
|
921
965
|
/** @internal Entry stored in a command map — either a leaf definition or a group. */
|
|
922
966
|
type CommandEntry = CommandDefinition<any, any, any> | InternalGroup
|
|
923
967
|
|
|
968
|
+
/** Controls when output data is displayed. `'all'` displays to both humans and agents. `'agent-only'` suppresses data output in human/TTY mode. */
|
|
969
|
+
export type OutputPolicy = 'agent-only' | 'all'
|
|
970
|
+
|
|
924
971
|
/** @internal A command group's internal storage. */
|
|
925
972
|
type InternalGroup = {
|
|
926
973
|
_group: true
|
|
927
974
|
description?: string | undefined
|
|
975
|
+
outputPolicy?: OutputPolicy | undefined
|
|
928
976
|
commands: Map<string, CommandEntry>
|
|
929
977
|
}
|
|
930
978
|
|
|
@@ -939,6 +987,9 @@ export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
|
|
|
939
987
|
/** @internal Maps root CLI instances to their command definitions. */
|
|
940
988
|
const toRootDefinition = new WeakMap<Root, CommandDefinition<any, any, any>>()
|
|
941
989
|
|
|
990
|
+
/** @internal Maps CLI instances to their output policy. */
|
|
991
|
+
const toOutputPolicy = new WeakMap<Cli, OutputPolicy>()
|
|
992
|
+
|
|
942
993
|
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
943
994
|
const sentinel = Symbol.for('incur.sentinel')
|
|
944
995
|
|
|
@@ -1016,16 +1067,17 @@ async function handleStreaming(
|
|
|
1016
1067
|
format: Formatter.Format
|
|
1017
1068
|
formatExplicit: boolean
|
|
1018
1069
|
human: boolean
|
|
1070
|
+
renderOutput: boolean
|
|
1019
1071
|
verbose: boolean
|
|
1020
1072
|
write: (output: Output) => void
|
|
1021
1073
|
writeln: (s: string) => void
|
|
1022
1074
|
exit: (code: number) => void
|
|
1023
1075
|
},
|
|
1024
1076
|
) {
|
|
1025
|
-
// Incremental:
|
|
1077
|
+
// Incremental: no explicit format (default toon), or explicit jsonl
|
|
1026
1078
|
// Buffered: explicit json/yaml/toon/md
|
|
1027
|
-
const useJsonl =
|
|
1028
|
-
const incremental =
|
|
1079
|
+
const useJsonl = ctx.formatExplicit && ctx.format === 'jsonl'
|
|
1080
|
+
const incremental = useJsonl || !ctx.formatExplicit
|
|
1029
1081
|
|
|
1030
1082
|
if (incremental) {
|
|
1031
1083
|
// Incremental output: write each chunk as it arrives
|
|
@@ -1060,7 +1112,7 @@ async function handleStreaming(
|
|
|
1060
1112
|
}
|
|
1061
1113
|
}
|
|
1062
1114
|
if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
|
|
1063
|
-
else ctx.writeln(Formatter.format(value, 'toon'))
|
|
1115
|
+
else if (ctx.renderOutput) ctx.writeln(Formatter.format(value, 'toon'))
|
|
1064
1116
|
}
|
|
1065
1117
|
|
|
1066
1118
|
// Handle return value — error() or ok() sentinel
|
|
@@ -1444,8 +1496,8 @@ type CommandDefinition<
|
|
|
1444
1496
|
> = {
|
|
1445
1497
|
/** Map of option names to single-char aliases. */
|
|
1446
1498
|
alias?: options extends z.ZodObject<any>
|
|
1447
|
-
|
|
1448
|
-
|
|
1499
|
+
? Partial<Record<keyof z.output<options>, string>>
|
|
1500
|
+
: Record<string, string> | undefined
|
|
1449
1501
|
/** Zod schema for positional arguments. */
|
|
1450
1502
|
args?: args | undefined
|
|
1451
1503
|
/** A short description of what the command does. */
|
|
@@ -1462,10 +1514,22 @@ type CommandDefinition<
|
|
|
1462
1514
|
options?: options | undefined
|
|
1463
1515
|
/** Zod schema for the command's return value. */
|
|
1464
1516
|
output?: output | undefined
|
|
1517
|
+
/**
|
|
1518
|
+
* Controls when output data is displayed. Inherited by child commands when set on a group.
|
|
1519
|
+
*
|
|
1520
|
+
* - `'all'` — displays to both humans and agents.
|
|
1521
|
+
* - `'agent-only'` — suppresses data output in human/TTY mode while still returning it to agents.
|
|
1522
|
+
*
|
|
1523
|
+
* @default 'all'
|
|
1524
|
+
*/
|
|
1525
|
+
outputPolicy?: OutputPolicy | undefined
|
|
1465
1526
|
/** Alternative usage patterns shown in help output. */
|
|
1466
1527
|
usage?: Usage<args, options>[] | undefined
|
|
1467
1528
|
/** The command handler. Return a value for single-return, or use `async *run` to stream chunks. */
|
|
1468
1529
|
run(context: {
|
|
1530
|
+
/** Whether the consumer is an agent (stdout is not a TTY). */
|
|
1531
|
+
agent: boolean
|
|
1532
|
+
/** Positional arguments. */
|
|
1469
1533
|
args: InferOutput<args>
|
|
1470
1534
|
/** Parsed environment variables. */
|
|
1471
1535
|
env: InferOutput<env>
|
package/src/Mcp.test.ts
CHANGED
|
@@ -19,8 +19,8 @@ function createTestCommands() {
|
|
|
19
19
|
options: z.object({
|
|
20
20
|
upper: z.boolean().default(false).describe('Uppercase output'),
|
|
21
21
|
}),
|
|
22
|
-
run(
|
|
23
|
-
const msg = options.upper ? args.message.toUpperCase() : args.message
|
|
22
|
+
run(c: any) {
|
|
23
|
+
const msg = c.options.upper ? c.args.message.toUpperCase() : c.args.message
|
|
24
24
|
return { result: msg }
|
|
25
25
|
},
|
|
26
26
|
})
|
|
@@ -34,8 +34,8 @@ function createTestCommands() {
|
|
|
34
34
|
{
|
|
35
35
|
description: 'Say hello',
|
|
36
36
|
args: z.object({ name: z.string().describe('Name to greet') }),
|
|
37
|
-
run(
|
|
38
|
-
return { greeting: `hello ${args.name}` }
|
|
37
|
+
run(c: any) {
|
|
38
|
+
return { greeting: `hello ${c.args.name}` }
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
41
|
],
|
|
@@ -44,8 +44,8 @@ function createTestCommands() {
|
|
|
44
44
|
|
|
45
45
|
commands.set('fail', {
|
|
46
46
|
description: 'Always fails',
|
|
47
|
-
run(
|
|
48
|
-
return error({ code: 'BOOM', message: 'it broke' })
|
|
47
|
+
run(c: any) {
|
|
48
|
+
return c.error({ code: 'BOOM', message: 'it broke' })
|
|
49
49
|
},
|
|
50
50
|
})
|
|
51
51
|
|
package/src/SyncSkills.ts
CHANGED
|
@@ -58,9 +58,19 @@ export async function sync(
|
|
|
58
58
|
|
|
59
59
|
const { paths, agents } = Agents.install(tmpDir, { global, cwd })
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Remove stale skills from previous installs
|
|
62
|
+
const currentNames = new Set(paths.map((p) => path.basename(p)))
|
|
63
|
+
const prev = readMeta(name)
|
|
64
|
+
if (prev?.skills) {
|
|
65
|
+
for (const old of prev.skills) {
|
|
66
|
+
if (currentNames.has(old)) continue
|
|
67
|
+
Agents.remove(old, { global, cwd })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Write skills hash + names for staleness detection
|
|
62
72
|
const hashEntries = collectEntries(commands, [])
|
|
63
|
-
|
|
73
|
+
writeMeta(name, Skill.hash(hashEntries), [...currentNames])
|
|
64
74
|
|
|
65
75
|
return { skills, paths, agents }
|
|
66
76
|
} finally {
|
|
@@ -158,20 +168,27 @@ function hashPath(name: string): string {
|
|
|
158
168
|
return path.join(dir, 'incur', `${name}.json`)
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
/** @internal Writes the skills
|
|
162
|
-
function
|
|
171
|
+
/** @internal Writes the skills metadata for staleness detection and cleanup. */
|
|
172
|
+
function writeMeta(name: string, hash: string, skills: string[]) {
|
|
163
173
|
const file = hashPath(name)
|
|
164
174
|
const dir = path.dirname(file)
|
|
165
175
|
if (!fsSync.existsSync(dir)) fsSync.mkdirSync(dir, { recursive: true })
|
|
166
|
-
fsSync.writeFileSync(
|
|
176
|
+
fsSync.writeFileSync(
|
|
177
|
+
file,
|
|
178
|
+
JSON.stringify({ hash, skills, at: new Date().toISOString() }) + '\n',
|
|
179
|
+
)
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
/** Reads the stored
|
|
170
|
-
|
|
182
|
+
/** @internal Reads the stored metadata for a CLI. */
|
|
183
|
+
function readMeta(name: string): { hash: string; skills?: string[] } | undefined {
|
|
171
184
|
try {
|
|
172
|
-
|
|
173
|
-
return data.hash
|
|
185
|
+
return JSON.parse(fsSync.readFileSync(hashPath(name), 'utf-8'))
|
|
174
186
|
} catch {
|
|
175
187
|
return undefined
|
|
176
188
|
}
|
|
177
189
|
}
|
|
190
|
+
|
|
191
|
+
/** Reads the stored skills hash for a CLI. Returns `undefined` if no hash exists. */
|
|
192
|
+
export function readHash(name: string): string | undefined {
|
|
193
|
+
return readMeta(name)?.hash
|
|
194
|
+
}
|
package/src/bin.ts
CHANGED
|
@@ -19,10 +19,10 @@ const cli = Cli.create('incur', {
|
|
|
19
19
|
entry: z.string().optional().describe('Entrypoint path (absolute)'),
|
|
20
20
|
output: z.string().optional().describe('Output path (absolute)'),
|
|
21
21
|
}),
|
|
22
|
-
async run(
|
|
23
|
-
const dir = options.dir ?? '.'
|
|
24
|
-
const entry = options.entry ?? dir
|
|
25
|
-
const output = options.output ?? path.join(dir, 'incur.generated.ts')
|
|
22
|
+
async run(c) {
|
|
23
|
+
const dir = c.options.dir ?? '.'
|
|
24
|
+
const entry = c.options.entry ?? dir
|
|
25
|
+
const output = c.options.output ?? path.join(dir, 'incur.generated.ts')
|
|
26
26
|
await Typegen.generate(entry, output)
|
|
27
27
|
return { dir, entry, output }
|
|
28
28
|
},
|