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
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Dereferences all local `$ref` pointers in a JSON object (e.g. `{"$ref": "#/components/schemas/User"}`),
3
+ * replacing them inline with the resolved values. Only handles local (`#/...`) references.
4
+ *
5
+ * Handles circular references by caching a mutable placeholder before recursing.
6
+ *
7
+ * Minimal reimplementation of the dereferencing behavior from `@apidevtools/json-schema-ref-parser`
8
+ * (https://github.com/APIDevTools/json-schema-ref-parser). Only supports in-memory, local-pointer
9
+ * resolution — no file/URL resolution, no `$id` scoping.
10
+ */
11
+ export function dereference<value>(root: value): value {
12
+ const cache = new Map<string, unknown>()
13
+ return walk(root, root, cache) as value
14
+ }
15
+
16
+ function walk(node: unknown, root: unknown, cache: Map<string, unknown>): unknown {
17
+ if (Array.isArray(node)) return node.map((item) => walk(item, root, cache))
18
+
19
+ if (typeof node !== 'object' || node === null) return node
20
+
21
+ const obj = node as Record<string, unknown>
22
+
23
+ // Resolve $ref pointer
24
+ if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
25
+ const ref = obj.$ref
26
+ if (cache.has(ref)) return cache.get(ref)
27
+
28
+ const resolved = resolvePointer(root, ref)
29
+
30
+ // Non-object targets (primitives, arrays) can't be circular — resolve directly
31
+ if (typeof resolved !== 'object' || resolved === null || Array.isArray(resolved)) {
32
+ const dereferenced = walk(resolved, root, cache)
33
+ cache.set(ref, dereferenced)
34
+ return dereferenced
35
+ }
36
+
37
+ // Use a mutable placeholder so circular refs resolve to the same object.
38
+ // If the walked result is not a plain object (e.g. chained ref to primitive/array),
39
+ // skip the placeholder and cache directly.
40
+ const placeholder: Record<string, unknown> = {}
41
+ cache.set(ref, placeholder)
42
+ const dereferenced = walk(resolved, root, cache)
43
+ if (typeof dereferenced !== 'object' || dereferenced === null || Array.isArray(dereferenced)) {
44
+ cache.set(ref, dereferenced)
45
+ return dereferenced
46
+ }
47
+ Object.assign(placeholder, dereferenced)
48
+ return placeholder
49
+ }
50
+
51
+ const result: Record<string, unknown> = {}
52
+ for (const key of Object.keys(obj)) result[key] = walk(obj[key], root, cache)
53
+ return result
54
+ }
55
+
56
+ /** Resolves a JSON Pointer (e.g. `#/components/schemas/User`) against a root object. */
57
+ function resolvePointer(root: unknown, pointer: string): unknown {
58
+ // "#" or "#/" → root
59
+ const fragment = pointer.slice(1)
60
+ if (fragment === '' || fragment === '/') return root
61
+
62
+ const parts = fragment
63
+ .slice(1)
64
+ .split('/')
65
+ .map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'))
66
+
67
+ let current: unknown = root
68
+ for (const part of parts) {
69
+ if (typeof current !== 'object' || current === null)
70
+ throw new Error(`Cannot resolve $ref "${pointer}": path segment "${part}" not found`)
71
+ current = (current as Record<string, unknown>)[part]
72
+ if (current === undefined) throw new Error(`Cannot resolve $ref "${pointer}": "${part}" not found`)
73
+ }
74
+ return current
75
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { levenshtein, suggest } from './helpers.js'
4
+
5
+ describe('levenshtein', () => {
6
+ test('identical strings', () => {
7
+ expect(levenshtein('abc', 'abc')).toBe(0)
8
+ })
9
+
10
+ test('single insertion', () => {
11
+ expect(levenshtein('abc', 'abcd')).toBe(1)
12
+ })
13
+
14
+ test('single deletion', () => {
15
+ expect(levenshtein('abcd', 'abc')).toBe(1)
16
+ })
17
+
18
+ test('single substitution', () => {
19
+ expect(levenshtein('abc', 'axc')).toBe(1)
20
+ })
21
+
22
+ test('transposition counts as 2', () => {
23
+ expect(levenshtein('mpc', 'mcp')).toBe(2)
24
+ })
25
+
26
+ test('empty strings', () => {
27
+ expect(levenshtein('', '')).toBe(0)
28
+ expect(levenshtein('abc', '')).toBe(3)
29
+ expect(levenshtein('', 'abc')).toBe(3)
30
+ })
31
+ })
32
+
33
+ describe('suggest', () => {
34
+ const commands = ['deploy', 'status', 'list', 'create']
35
+
36
+ test('returns closest match within threshold', () => {
37
+ expect(suggest('deplyo', commands)).toBe('deploy')
38
+ })
39
+
40
+ test('returns match for transposition', () => {
41
+ expect(suggest('mpc', ['mcp', 'skills', 'completions'])).toBe('mcp')
42
+ })
43
+
44
+ test('returns undefined when no match is close enough', () => {
45
+ expect(suggest('xyz', commands)).toBeUndefined()
46
+ })
47
+
48
+ test('returns undefined for empty candidates', () => {
49
+ expect(suggest('deploy', [])).toBeUndefined()
50
+ })
51
+
52
+ test('picks the closest among multiple candidates', () => {
53
+ expect(suggest('craete', ['list', 'create', 'delete'])).toBe('create')
54
+ })
55
+
56
+ test('matches unambiguous prefix', () => {
57
+ expect(suggest('ski', ['mcp', 'skills', 'completions'])).toBe('skills')
58
+ })
59
+
60
+ test('prefers shorter prefix match among ambiguous prefixes', () => {
61
+ expect(suggest('ski', ['skills', 'skip'])).toBe('skip')
62
+ })
63
+
64
+ test('prefix match beats fuzzy match', () => {
65
+ expect(suggest('iss', ['issue', 'is', 'miss'])).toBe('issue')
66
+ })
67
+
68
+ test('contains match beats fuzzy match', () => {
69
+ expect(suggest('sue', ['issue', 'use', 'sum'])).toBe('issue')
70
+ })
71
+
72
+ test('case-insensitive matching', () => {
73
+ expect(suggest('Deploy', ['deploy', 'list'])).toBe('deploy')
74
+ })
75
+ })
@@ -0,0 +1,59 @@
1
+ /** Checks whether a value is a plain object record. */
2
+ export function isRecord(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
4
+ }
5
+
6
+ /** Converts a camelCase string to kebab-case. */
7
+ export function toKebab(value: string): string {
8
+ return value.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
9
+ }
10
+
11
+ /** Computes the Levenshtein edit distance between two strings. */
12
+ export function levenshtein(a: string, b: string): number {
13
+ const m = a.length
14
+ const n = b.length
15
+ const dp: number[] = Array.from({ length: n + 1 }, (_, i) => i)
16
+ for (let i = 1; i <= m; i++) {
17
+ let prev = dp[0]!
18
+ dp[0] = i
19
+ for (let j = 1; j <= n; j++) {
20
+ const tmp = dp[j]!
21
+ dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j]!, dp[j - 1]!)
22
+ prev = tmp
23
+ }
24
+ }
25
+ return dp[n]!
26
+ }
27
+
28
+ /** Suggests the closest command name from a set, returning it if within a reasonable edit distance. */
29
+ export function suggest(input: string, candidates: Iterable<string>): string | undefined {
30
+ const threshold = input.length <= 4 ? 2 : Math.floor(input.length / 2)
31
+ const lower = input.toLowerCase()
32
+ const all = Array.isArray(candidates) ? candidates : [...candidates]
33
+
34
+ let best: string | undefined
35
+ let bestScore = Infinity
36
+
37
+ for (const c of all) {
38
+ const lc = c.toLowerCase()
39
+ const dist = levenshtein(lower, lc)
40
+
41
+ let score: number
42
+ if (lc.startsWith(lower) && lc !== lower)
43
+ // prefix match — best tier
44
+ score = dist
45
+ else if (lc.includes(lower))
46
+ // contains match — middle tier
47
+ score = 100 + dist
48
+ else if (dist <= threshold)
49
+ // fuzzy match — last tier
50
+ score = 200 + dist
51
+ else continue
52
+
53
+ if (score < bestScore) {
54
+ bestScore = score
55
+ best = c
56
+ }
57
+ }
58
+ return best
59
+ }
package/src/middleware.ts CHANGED
@@ -10,16 +10,11 @@ type InferVars<vars extends z.ZodObject<any> | undefined> =
10
10
  type InferEnv<env extends z.ZodObject<any> | undefined> =
11
11
  env extends z.ZodObject<any> ? z.output<env> : {}
12
12
 
13
- /** @internal Infers the output type of an options schema, or `{}` if undefined. */
14
- type InferOptions<options extends z.ZodObject<any> | undefined> =
15
- options extends z.ZodObject<any> ? z.output<options> : {}
16
-
17
13
  /** Middleware handler that runs before/after command execution. */
18
14
  export type Handler<
19
15
  vars extends z.ZodObject<any> | undefined = undefined,
20
16
  env extends z.ZodObject<any> | undefined = undefined,
21
- options extends z.ZodObject<any> | undefined = undefined,
22
- > = (context: Context<vars, env, options>, next: () => Promise<void>) => Promise<void> | void
17
+ > = (context: Context<vars, env>, next: () => Promise<void>) => Promise<void> | void
23
18
 
24
19
  /** CTA block for middleware error/ok responses. */
25
20
  type CtaBlock = {
@@ -33,12 +28,13 @@ type CtaBlock = {
33
28
  export type Context<
34
29
  vars extends z.ZodObject<any> | undefined = undefined,
35
30
  env extends z.ZodObject<any> | undefined = undefined,
36
- options extends z.ZodObject<any> | undefined = undefined,
37
31
  > = {
38
32
  /** Whether the consumer is an agent (stdout is not a TTY). */
39
33
  agent: boolean
40
34
  /** The resolved command path. */
41
35
  command: string
36
+ /** The binary name the user invoked (e.g. an alias). Falls back to `name` when not resolvable. */
37
+ displayName: string
42
38
  /** Parsed environment variables from the CLI-level env schema. */
43
39
  env: InferEnv<env>
44
40
  /** Return an error result, short-circuiting the middleware chain. */
@@ -55,8 +51,6 @@ export type Context<
55
51
  formatExplicit: boolean
56
52
  /** The CLI name. */
57
53
  name: string
58
- /** Parsed options from the root CLI-level options schema. */
59
- options: InferOptions<options>
60
54
  /** Set a typed variable for downstream middleware and handlers. */
61
55
  set<key extends string & keyof InferVars<vars>>(key: key, value: InferVars<vars>[key]): void
62
56
  /** Variables set by upstream middleware. */
@@ -65,11 +59,10 @@ export type Context<
65
59
  version: string | undefined
66
60
  }
67
61
 
68
- /** Creates a strictly typed middleware handler. Pass the vars schema as a generic for typed `c.set()` and `c.var`, the env schema for typed `c.env`, and the options schema for typed `c.options`. */
62
+ /** Creates a strictly typed middleware handler. Pass the vars schema as a generic for typed `c.set()` and `c.var`, and the env schema for typed `c.env`. */
69
63
  export default function middleware<
70
64
  const vars extends z.ZodObject<any> | undefined = undefined,
71
65
  const env extends z.ZodObject<any> | undefined = undefined,
72
- const options extends z.ZodObject<any> | undefined = undefined,
73
- >(handler: Handler<vars, env, options>): Handler<vars, env, options> {
66
+ >(handler: Handler<vars, env>): Handler<vars, env> {
74
67
  return handler
75
68
  }