@canonical/pragma-cli 0.30.0 → 0.31.0

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 (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -1,5 +1,6 @@
1
1
  import type { Store } from "@canonical/ke";
2
2
  import { PragmaError } from "#error";
3
+ import resolveUri from "../../../graph/helpers/resolveUri.js";
3
4
  import type {
4
5
  ColumnDef,
5
6
  LookupField,
@@ -9,45 +10,85 @@ import type {
9
10
  } from "../../contracts.js";
10
11
  import type { Formatters } from "../../formatters.js";
11
12
  import lookupMany from "../../lookupMany.js";
13
+ import parseSampleCount, {
14
+ DEFAULT_SAMPLE_COUNT,
15
+ MAX_SAMPLE_COUNT,
16
+ MIN_SAMPLE_COUNT,
17
+ } from "../../parseSampleCount.js";
18
+ import pickRandom from "../../pickRandom.js";
12
19
  import {
13
20
  renderListLlm,
14
21
  renderListPlain,
15
22
  renderLookupLlm,
16
23
  renderLookupPlain,
17
24
  } from "../../renderers.js";
18
- import { suggestNames } from "../../suggestions/index.js";
25
+ import createSampleFormatters from "../../sampleFormatters.js";
26
+ import {
27
+ expandGlob,
28
+ isGlobPattern,
29
+ suggestNames,
30
+ } from "../../suggestions/index.js";
31
+ import type { SampleOutput } from "../../types/index.js";
19
32
  import requirePragmaContext from "../requirePragmaContext.js";
20
33
  import type { LookupStory, ReadStory, StoryParam } from "../types.js";
21
34
  import applyPackFilters from "./applyPackFilters.js";
22
- import buildLookupQuery, { buildLookupNamesQuery } from "./buildLookupQuery.js";
35
+ import applyPackSearch from "./applyPackSearch.js";
36
+ import buildLookupQuery, {
37
+ buildExpandQuery,
38
+ buildLookupByIriQuery,
39
+ buildLookupNamesQuery,
40
+ } from "./buildLookupQuery.js";
23
41
  import runSelectQuery from "./runSelectQuery.js";
24
42
  import type {
25
43
  StoryPackDefinition,
44
+ StoryPackDisclosure,
26
45
  StoryPackFilter,
46
+ StoryPackList,
27
47
  StoryPackLookup,
48
+ StoryPackSearch,
28
49
  } from "./types.js";
50
+ import { isEmbeddableIri } from "./validateStoryPackDefinition.js";
29
51
 
30
52
  /** A pack entity/row: SELECT variable name → string value. */
31
53
  type PackRow = Record<string, string>;
32
54
 
55
+ /**
56
+ * A looked-up pack entity: flat SELECT fields plus any expanded child arrays
57
+ * (pack v1 nested projections). Scalar fields are strings; an expand's value
58
+ * is the array of its child rows.
59
+ */
60
+ type PackEntity = Record<string, string | readonly PackRow[]>;
61
+
62
+ /** Resolved sample data: the drawn entities plus the population size. */
63
+ interface PackSampleData {
64
+ readonly samples: PackEntity[];
65
+ readonly totalCount: number;
66
+ }
67
+
33
68
  /** The read stories a pack definition compiles to. */
34
69
  export interface CompiledPackStories {
35
70
  readonly list: ReadStory<PackRow[], PackRow[]>;
36
- readonly lookup?: LookupStory<PackRow, PackRow>;
71
+ /** Extra list-shaped verbs (e.g. `categories`), in declaration order. */
72
+ readonly verbs: readonly ReadStory<PackRow[], PackRow[]>[];
73
+ readonly lookup?: LookupStory<PackEntity, PackEntity>;
74
+ /** The `sample` story, when the lookup declares the capability. */
75
+ readonly sample?: ReadStory<PackSampleData, SampleOutput<PackEntity>>;
37
76
  }
38
77
 
39
78
  /**
40
79
  * Compile one validated story-pack definition into kernel read stories.
41
80
  *
42
- * The list story runs the pack's preferred SELECT verbatim; the lookup
43
- * story runs the generated, injection-safe per-name query. Both render
44
- * through the shared generic renderers with the runtime's merged prefix
45
- * map, so foreign namespaces display compacted.
81
+ * The list story (and any extra list-shaped verbs) run the pack's
82
+ * preferred SELECT verbatim; the lookup and sample stories run generated,
83
+ * injection-safe queries. All render through the shared generic renderers
84
+ * with the runtime's merged prefix map, so foreign namespaces display
85
+ * compacted.
46
86
  *
47
87
  * @param definition - A validated pack definition.
48
88
  * @param source - Where the definition came from, for diagnostics.
49
89
  * @param prefixes - The merged prefix map used for display.
50
- * @returns The compiled list story and, when declared, the lookup story.
90
+ * @returns The compiled list story, extra verbs, and — when declared —
91
+ * the lookup and sample stories.
51
92
  */
52
93
  export default function compilePackStories(
53
94
  definition: StoryPackDefinition,
@@ -55,17 +96,71 @@ export default function compilePackStories(
55
96
  prefixes: Readonly<Record<string, string>>,
56
97
  ): CompiledPackStories {
57
98
  const { noun } = definition;
58
- const heading = capitalize(noun);
59
99
  const description = definition.description ?? `List ${noun} entries`;
60
100
 
61
- const listColumns: ColumnDef<PackRow>[] = definition.list.columns.map(
62
- (column) => ({
63
- key: column.field,
64
- label: column.label ?? column.field,
65
- }),
66
- );
101
+ const list = compileListVerb(definition.list, {
102
+ noun,
103
+ verb: "list",
104
+ heading: capitalize(noun),
105
+ description,
106
+ toolDescription:
107
+ definition.toolDescription ?? `${description} (story pack: ${source}).`,
108
+ source,
109
+ prefixes,
110
+ });
111
+
112
+ const verbs = (definition.verbs ?? []).map((verb) => {
113
+ const verbDescription = verb.description ?? `List ${noun} ${verb.verb}`;
114
+ return compileListVerb(verb, {
115
+ noun,
116
+ verb: verb.verb,
117
+ heading: `${capitalize(noun)} ${verb.verb}`,
118
+ description: verbDescription,
119
+ toolDescription:
120
+ verb.toolDescription ?? `${verbDescription} (story pack: ${source}).`,
121
+ source,
122
+ prefixes,
123
+ });
124
+ });
125
+
126
+ const compiled = definition.lookup
127
+ ? compileLookup(definition.lookup, noun, source, prefixes)
128
+ : undefined;
129
+
130
+ return {
131
+ list,
132
+ verbs,
133
+ ...(compiled ? { lookup: compiled.lookup } : {}),
134
+ ...(compiled?.sample ? { sample: compiled.sample } : {}),
135
+ };
136
+ }
137
+
138
+ /** Presentation facts for one compiled list-shaped verb. */
139
+ interface ListVerbMeta {
140
+ readonly noun: string;
141
+ readonly verb: string;
142
+ readonly heading: string;
143
+ readonly description: string;
144
+ readonly toolDescription: string;
145
+ readonly source: string;
146
+ readonly prefixes: Readonly<Record<string, string>>;
147
+ }
148
+
149
+ /**
150
+ * Compile one list-shaped story half (the `list` verb or an extra verb)
151
+ * into a kernel read story.
152
+ */
153
+ function compileListVerb(
154
+ shape: StoryPackList,
155
+ meta: ListVerbMeta,
156
+ ): ReadStory<PackRow[], PackRow[]> {
157
+ const { noun, verb, source, prefixes } = meta;
158
+ const listColumns: ColumnDef<PackRow>[] = shape.columns.map((column) => ({
159
+ key: column.field,
160
+ label: column.label ?? column.field,
161
+ }));
67
162
  const listOptions: RenderListOptions<PackRow> = {
68
- heading,
163
+ heading: meta.heading,
69
164
  columns: listColumns,
70
165
  prefixes,
71
166
  };
@@ -75,40 +170,40 @@ export default function compilePackStories(
75
170
  json: (rows) => JSON.stringify(rows, null, 2),
76
171
  };
77
172
 
78
- const filters = definition.list.filters;
79
- const filterExample = filters?.at(0);
80
- const list: ReadStory<PackRow[], PackRow[]> = {
173
+ const filters = shape.filters;
174
+ const search = shape.search;
175
+ // The generated example uses the first filter with a declared value set —
176
+ // a value-free filter has no representative value to show.
177
+ const filterExample = filters?.find((filter) => filter.values !== undefined);
178
+ return {
81
179
  noun,
82
- verb: "list",
83
- description,
84
- toolDescription:
85
- definition.toolDescription ?? `${description} (story pack: ${source}).`,
86
- params: projectFilterParams(filters),
180
+ verb,
181
+ description: meta.description,
182
+ toolDescription: meta.toolDescription,
183
+ params: [...projectFilterParams(filters), ...projectSearchParam(search)],
87
184
  examples: [
88
- `pragma ${noun} list`,
185
+ `pragma ${noun} ${verb}`,
89
186
  ...(filterExample
90
187
  ? [
91
- `pragma ${noun} list --${filterExample.param} ${quoteExampleValue(filterExample.values.at(0) ?? "")}`,
188
+ `pragma ${noun} ${verb} --${filterExample.param} ${quoteExampleValue(filterExample.values?.at(0) ?? "")}`,
92
189
  ]
93
190
  : []),
94
- `pragma ${noun} list --llm`,
191
+ `pragma ${noun} ${verb} --llm`,
95
192
  ],
96
193
  resolve: async (rt, params) =>
97
- applyPackFilters(
98
- await runSelectQuery(rt.store, definition.list.query, source),
99
- filters,
194
+ applyPackSearch(
195
+ applyPackFilters(
196
+ await runSelectQuery(rt.store, shape.query, source),
197
+ filters,
198
+ params,
199
+ ),
200
+ search,
100
201
  params,
101
202
  ),
102
203
  toOutput: (rows) => rows,
103
204
  formatters: listFormatters,
104
205
  toEnvelope: (rows) => ({ data: rows, meta: { count: rows.length } }),
105
206
  };
106
-
107
- const lookup = definition.lookup
108
- ? compileLookup(definition.lookup, noun, source, prefixes)
109
- : undefined;
110
-
111
- return { list, ...(lookup ? { lookup } : {}) };
112
207
  }
113
208
 
114
209
  function compileLookup(
@@ -116,35 +211,79 @@ function compileLookup(
116
211
  noun: string,
117
212
  source: string,
118
213
  prefixes: Readonly<Record<string, string>>,
119
- ): LookupStory<PackRow, PackRow> {
120
- const fields: LookupField<PackRow>[] = (lookup.fields ?? []).map((field) => ({
121
- label: field.label ?? field.name,
122
- value: (entity) => entity[field.name],
123
- }));
124
- const sections: SectionDef<PackRow>[] = (lookup.sections ?? []).map(
214
+ ): {
215
+ lookup: LookupStory<PackEntity, PackEntity>;
216
+ sample?: ReadStory<PackSampleData, SampleOutput<PackEntity>>;
217
+ } {
218
+ const fields: LookupField<PackEntity>[] = (lookup.fields ?? []).map(
219
+ (field) => ({
220
+ label: field.label ?? field.name,
221
+ value: (entity) => entity[field.name],
222
+ }),
223
+ );
224
+ const flatSections: SectionDef<PackEntity>[] = (lookup.sections ?? []).map(
125
225
  (section) => ({
126
226
  key: section.name,
127
227
  heading: section.label ?? section.name,
128
228
  kind: section.kind ?? "field",
129
229
  }),
130
230
  );
131
- const lookupOptions: RenderLookupOptions<PackRow> = {
132
- title: (entity) => entity.name ?? entity.uri ?? "(unnamed)",
231
+ // Expands render after the flat sections, as list/table collections.
232
+ const expandSections: SectionDef<PackEntity>[] = (lookup.expand ?? []).map(
233
+ (expand) => ({
234
+ key: expand.name,
235
+ heading: expand.heading ?? expand.name,
236
+ kind: expand.kind ?? "list",
237
+ ...(expand.showWhenEmpty ? { showWhenEmpty: true } : {}),
238
+ }),
239
+ );
240
+ const sections = [...flatSections, ...expandSections];
241
+ const lookupOptions: RenderLookupOptions<PackEntity> = {
242
+ title: (entity) => scalar(entity.name) ?? scalar(entity.uri) ?? "(unnamed)",
133
243
  fields,
134
244
  sections,
135
245
  prefixes,
136
246
  };
137
- const lookupFormatters: Formatters<PackRow> = {
247
+ const lookupFormatters: Formatters<PackEntity> = {
138
248
  plain: (entity) => renderLookupPlain(entity, lookupOptions),
139
249
  llm: (entity) => renderLookupLlm(entity, lookupOptions),
140
250
  json: (entity) => JSON.stringify(entity, null, 2),
141
251
  };
142
252
 
143
- return {
253
+ // Disclosure capability: a declared level set derives a `--detail <level>`
254
+ // parameter (enum of the levels) on both surfaces, plus one legacy alias
255
+ // boolean per non-base level (`--digest`, `--detailed`) that implies its
256
+ // level — preserving the pre-pack flag ergonomics on both surfaces.
257
+ const disclosure = lookup.disclosure;
258
+ const detailParams: StoryParam[] = disclosure
259
+ ? [
260
+ {
261
+ name: "detail",
262
+ type: "string",
263
+ description: `Disclosure level (${disclosure.levels.join(", ")})`,
264
+ enum: [...disclosure.levels],
265
+ },
266
+ ...disclosure.levels.slice(1).map(
267
+ (level): StoryParam => ({
268
+ name: level,
269
+ type: "boolean",
270
+ description: `Shorthand for --detail ${level}`,
271
+ toolDescription: `Shorthand for detail="${level}"`,
272
+ default: false,
273
+ }),
274
+ ),
275
+ ]
276
+ : [];
277
+
278
+ const lookupStory: LookupStory<PackEntity, PackEntity> = {
144
279
  noun,
145
- description: `Look up ${noun} details by name`,
146
- toolDescription: `Get details for one or more ${noun} entries by name (story pack: ${source}).`,
147
- namesDescription: `${capitalize(noun)} names`,
280
+ description:
281
+ lookup.description ?? `Look up ${noun} details by name, IRI, or glob`,
282
+ toolDescription:
283
+ lookup.toolDescription ??
284
+ `Get details for one or more ${noun} entries by name, prefixed name (ex:thing), absolute IRI, or glob pattern (story pack: ${source}).`,
285
+ namesDescription: `${capitalize(noun)} names, IRIs/prefixed names, or glob patterns`,
286
+ params: detailParams,
148
287
  complete: async (partial, cmdCtx) => {
149
288
  const ctx = requirePragmaContext(cmdCtx);
150
289
  const names = await listEntityNames(ctx.store, lookup, source);
@@ -153,10 +292,40 @@ function compileLookup(
153
292
  );
154
293
  },
155
294
  examples: [`pragma ${noun} lookup <name>`],
156
- resolve: (rt, names) =>
157
- lookupMany(names, (query) =>
158
- lookupPackEntity(rt.store, lookup, noun, query, source),
159
- ),
295
+ resolve: async (rt, names, params, view) => {
296
+ const level = disclosure
297
+ ? resolveDetailLevel(
298
+ params,
299
+ rt.config ?? {},
300
+ disclosure,
301
+ view?.surface ?? "cli",
302
+ )
303
+ : undefined;
304
+ // Glob queries (`react/*`) expand against the entity name list before
305
+ // resolution; a glob matching nothing becomes its own error entry.
306
+ const expanded = await expandPackLookupQueries(
307
+ rt.store,
308
+ lookup,
309
+ noun,
310
+ source,
311
+ names,
312
+ );
313
+ const result = await lookupMany(expanded.names, (query) =>
314
+ lookupPackEntity(
315
+ rt.store,
316
+ lookup,
317
+ noun,
318
+ query,
319
+ source,
320
+ level,
321
+ prefixes,
322
+ ),
323
+ );
324
+ return {
325
+ ...result,
326
+ errors: [...result.errors, ...expanded.globErrors],
327
+ };
328
+ },
160
329
  toFmtInput: (entity) => entity,
161
330
  formatters: lookupFormatters,
162
331
  emptyNamesError: () =>
@@ -168,13 +337,159 @@ function compileLookup(
168
337
  },
169
338
  }),
170
339
  };
340
+
341
+ const sample = lookup.sample
342
+ ? compileSample(lookup, noun, source, prefixes, lookupFormatters)
343
+ : undefined;
344
+
345
+ return { lookup: lookupStory, ...(sample ? { sample } : {}) };
171
346
  }
172
347
 
173
348
  /**
174
- * Look up one pack entity by name via the generated query.
349
+ * Compile the `sample` story for a lookup that declares the capability.
350
+ *
351
+ * Draws N random entity names (clamped 1–5) and resolves each through the
352
+ * SAME lookup path at the HIGHEST disclosure level, so exemplars carry the
353
+ * full shape — fields, expands, and all. Rendering reuses the lookup
354
+ * formatters per entity under the shared sample header.
355
+ */
356
+ function compileSample(
357
+ lookup: StoryPackLookup,
358
+ noun: string,
359
+ source: string,
360
+ prefixes: Readonly<Record<string, string>>,
361
+ lookupFormatters: Formatters<PackEntity>,
362
+ ): ReadStory<PackSampleData, SampleOutput<PackEntity>> {
363
+ const config =
364
+ lookup.sample === true || lookup.sample === undefined ? {} : lookup.sample;
365
+ const description =
366
+ config.description ??
367
+ `Return randomly selected complete ${noun} entries as exemplars for shape discovery`;
368
+ const toolDescription =
369
+ config.toolDescription ??
370
+ `Return ${MIN_SAMPLE_COUNT}–${MAX_SAMPLE_COUNT} randomly selected complete ${noun} entries as exemplars. Use BEFORE writing queries to see actual data shapes, property names, and value formats. Each call returns different instances.`;
371
+ const sampleFormatters = createSampleFormatters<PackEntity>(
372
+ `${noun} entries`,
373
+ (entity, mode) => lookupFormatters[mode](entity),
374
+ );
375
+ const toOutput = (data: PackSampleData): SampleOutput<PackEntity> => ({
376
+ ...data,
377
+ nextSteps: sampleNextSteps(noun, data),
378
+ });
379
+
380
+ return {
381
+ noun,
382
+ verb: "sample",
383
+ description,
384
+ toolDescription,
385
+ params: [
386
+ {
387
+ name: "count",
388
+ type: "string",
389
+ description: `Number of samples to return (${MIN_SAMPLE_COUNT}–${MAX_SAMPLE_COUNT}, default ${config.count ?? DEFAULT_SAMPLE_COUNT})`,
390
+ positional: true,
391
+ },
392
+ ],
393
+ examples: [`pragma ${noun} sample`, `pragma ${noun} sample 3`],
394
+ resolve: async (rt, params) => {
395
+ const count = parseSampleCount(params.count ?? config.count);
396
+ const names = await listEntityNames(rt.store, lookup, source);
397
+ // Exemplars resolve at the HIGHEST disclosure level so every expand
398
+ // is fetched — samples exist to show the full data shape.
399
+ const level = lookup.disclosure?.levels.at(-1);
400
+ const selected = pickRandom(names, count);
401
+ const samples = await Promise.all(
402
+ selected.map((name) =>
403
+ lookupPackEntity(
404
+ rt.store,
405
+ lookup,
406
+ noun,
407
+ name,
408
+ source,
409
+ level,
410
+ prefixes,
411
+ ),
412
+ ),
413
+ );
414
+ return { samples, totalCount: names.length };
415
+ },
416
+ toOutput,
417
+ formatters: sampleFormatters,
418
+ toEnvelope: (data) => ({
419
+ data: toOutput(data),
420
+ meta: { count: data.samples.length },
421
+ }),
422
+ };
423
+ }
424
+
425
+ /** Agent-facing follow-ups appended to sample output. */
426
+ function sampleNextSteps(noun: string, data: PackSampleData): string[] {
427
+ return [
428
+ `These are ${data.samples.length} of ${data.totalCount} total ${noun} entries.`,
429
+ `Use ${noun}_lookup to inspect specific entries by name.`,
430
+ `Use ${noun}_list to browse all entries.`,
431
+ ];
432
+ }
433
+
434
+ /**
435
+ * Partition lookup queries into concrete names and expanded globs.
436
+ *
437
+ * Literal names pass through unchanged. Glob patterns expand against the
438
+ * pack's entity name list; a glob matching nothing produces an
439
+ * `EMPTY_RESULTS` error entry (mirroring the shared lookup-glob contract)
440
+ * instead of failing the whole batch.
441
+ *
442
+ * @note Impure — fetches the entity name list when a glob is present.
443
+ */
444
+ async function expandPackLookupQueries(
445
+ store: Store,
446
+ lookup: StoryPackLookup,
447
+ noun: string,
448
+ source: string,
449
+ queries: readonly string[],
450
+ ): Promise<{
451
+ names: string[];
452
+ globErrors: { query: string; code: string; message: string }[];
453
+ }> {
454
+ if (!queries.some(isGlobPattern)) {
455
+ return { names: [...queries], globErrors: [] };
456
+ }
457
+
458
+ const allNames = await listEntityNames(store, lookup, source);
459
+ const names: string[] = [];
460
+ const globErrors: { query: string; code: string; message: string }[] = [];
461
+ for (const query of queries) {
462
+ if (!isGlobPattern(query)) {
463
+ names.push(query);
464
+ continue;
465
+ }
466
+ const expanded = expandGlob(query, allNames);
467
+ if (expanded.length === 0) {
468
+ globErrors.push({
469
+ query,
470
+ code: "EMPTY_RESULTS",
471
+ message: `No ${noun} entries matching "${query}".`,
472
+ });
473
+ } else {
474
+ names.push(...expanded);
475
+ }
476
+ }
477
+ return { names, globErrors };
478
+ }
479
+
480
+ /**
481
+ * Look up one pack entity by name, prefixed name, or absolute IRI.
482
+ *
483
+ * A query shaped like an IRI (`https://…` or `prefix:local`) is resolved
484
+ * through the runtime prefix map and addressed exactly via a generated
485
+ * `BIND(<iri> AS ?uri)` query; anything else matches the `by` property's
486
+ * value case-insensitively. Both query forms are generated — the resolved
487
+ * IRI is validated against the embeddable-IRI shape and names are escaped
488
+ * as SPARQL string literals, so user input never reaches query text raw.
175
489
  *
176
490
  * @throws PragmaError with code `ENTITY_NOT_FOUND` and ranked suggestions
177
- * when no entity matches.
491
+ * when no entity matches, or `INVALID_INPUT` when an IRI-shaped query
492
+ * uses an unknown prefix or malformed IRI.
178
493
  * @note Impure — queries the ke store.
179
494
  */
180
495
  async function lookupPackEntity(
@@ -183,26 +498,156 @@ async function lookupPackEntity(
183
498
  noun: string,
184
499
  query: string,
185
500
  source: string,
186
- ): Promise<PackRow> {
501
+ level: string | undefined,
502
+ prefixes: Readonly<Record<string, string>>,
503
+ ): Promise<PackEntity> {
187
504
  const rows = await runSelectQuery(
188
505
  store,
189
- buildLookupQuery(lookup, query),
506
+ buildEntityQuery(lookup, query, prefixes),
190
507
  source,
191
508
  );
192
- const entity = rows.at(0);
193
- if (entity) {
194
- return entity;
509
+ const base = rows.at(0);
510
+ if (!base) {
511
+ const candidates = await listEntityNames(store, lookup, source);
512
+ throw PragmaError.notFound(noun, query, {
513
+ suggestions: suggestNames(query, candidates),
514
+ recovery: {
515
+ message: `List available ${noun} entries.`,
516
+ cli: `pragma ${noun} list`,
517
+ mcp: { tool: `${noun}_list` },
518
+ },
519
+ });
195
520
  }
196
521
 
197
- const candidates = await listEntityNames(store, lookup, source);
198
- throw PragmaError.notFound(noun, query, {
199
- suggestions: suggestNames(query, candidates),
200
- recovery: {
201
- message: `List available ${noun} entries.`,
202
- cli: `pragma ${noun} list`,
203
- mcp: { tool: `${noun}_list` },
204
- },
205
- });
522
+ // Enrich with any declared multi-valued expands (pack v1 nested projections).
523
+ // Each expand runs a sub-SELECT bound to the resolved entity IRI (`base.uri`),
524
+ // which is store-derived, never user input. An expand tagged with a
525
+ // disclosure level is fetched only at/above that level — so a lower level
526
+ // never runs the sub-SELECT and the renderer omits the (absent) section.
527
+ const entity: PackEntity = { ...base };
528
+ const entityUri = base.uri;
529
+ if (entityUri) {
530
+ const levels = lookup.disclosure?.levels ?? [];
531
+ const activeIdx = level ? levels.indexOf(level) : Number.POSITIVE_INFINITY;
532
+ for (const expand of lookup.expand ?? []) {
533
+ const requiredIdx = expand.level ? levels.indexOf(expand.level) : 0;
534
+ if (activeIdx < requiredIdx) continue;
535
+ entity[expand.name] = await runSelectQuery(
536
+ store,
537
+ buildExpandQuery(expand, entityUri),
538
+ source,
539
+ );
540
+ }
541
+ }
542
+ return entity;
543
+ }
544
+
545
+ /**
546
+ * Build the base entity SELECT for a lookup query.
547
+ *
548
+ * Detection mirrors the retired built-in standard lookup: a query starting
549
+ * with `http(s)://` or containing `:` is treated as an IRI or prefixed
550
+ * name and resolved through the merged prefix map. The resolved IRI must
551
+ * still match the embeddable shape the pack validator holds authored
552
+ * terms to; anything else is rejected before it can reach query text.
553
+ *
554
+ * @throws PragmaError with code `INVALID_INPUT` when the query is
555
+ * IRI-shaped but uses an unknown prefix or resolves to a non-embeddable
556
+ * IRI.
557
+ */
558
+ function buildEntityQuery(
559
+ lookup: StoryPackLookup,
560
+ query: string,
561
+ prefixes: Readonly<Record<string, string>>,
562
+ ): string {
563
+ if (!looksLikeIri(query)) {
564
+ return buildLookupQuery(lookup, query);
565
+ }
566
+ // resolveUri validates prefix membership and rejects IRI-breaking
567
+ // characters; the embeddable-shape check adds the validator's stricter
568
+ // `scheme://` contract before the IRI is embedded as `<iri>`.
569
+ const resolved = resolveUri(query, prefixes);
570
+ if (!isEmbeddableIri(resolved)) {
571
+ throw PragmaError.invalidInput("name", query, {
572
+ recovery: {
573
+ message:
574
+ "Use an absolute IRI (https://…), a prefixed name (ex:thing), or a plain entity name.",
575
+ },
576
+ });
577
+ }
578
+ return buildLookupByIriQuery(lookup, resolved);
579
+ }
580
+
581
+ /** Whether a lookup query addresses an entity by IRI or prefixed name. */
582
+ function looksLikeIri(query: string): boolean {
583
+ return (
584
+ query.startsWith("http://") ||
585
+ query.startsWith("https://") ||
586
+ query.includes(":")
587
+ );
588
+ }
589
+
590
+ /**
591
+ * Resolve the effective disclosure level for a lookup call.
592
+ *
593
+ * Precedence (highest → lowest): an explicit `--detail`/`detail` value, a
594
+ * legacy alias flag set to true (the HIGHEST requested level wins when
595
+ * several are set), the global `config.detail` default, the MCP full-data
596
+ * default, the pack's declared `disclosure.default`, then the base level
597
+ * (the first declared). A config/pack value that is not one of the pack's
598
+ * levels is ignored (a global default may target another pack).
599
+ *
600
+ * `config.detail` deliberately outranks the MCP default: the config value
601
+ * is something the user explicitly chose, while the MCP default is only a
602
+ * surface fallback — a user who configured `detail: summary` gets summary
603
+ * on both surfaces.
604
+ *
605
+ * The MCP default follows the ratified surface contract (see
606
+ * `resolveLookupDetailed`): the CLI opts in while MCP opts out, so an MCP
607
+ * call with no explicit or configured choice resolves to the highest
608
+ * level — agents want full data by default — and an alias explicitly set
609
+ * to false (e.g. `detailed: false`) opts down to the pack default or base
610
+ * level.
611
+ */
612
+ function resolveDetailLevel(
613
+ params: Record<string, unknown>,
614
+ config: { readonly detail?: string },
615
+ disclosure: StoryPackDisclosure,
616
+ surface: "cli" | "mcp",
617
+ ): string {
618
+ const { levels } = disclosure;
619
+ const explicit =
620
+ typeof params.detail === "string" ? params.detail : undefined;
621
+ if (explicit !== undefined && levels.includes(explicit)) {
622
+ return explicit;
623
+ }
624
+
625
+ const aliased = levels
626
+ .filter((level, index) => index > 0 && params[level] === true)
627
+ .at(-1);
628
+ if (aliased !== undefined) {
629
+ return aliased;
630
+ }
631
+
632
+ if (config.detail !== undefined && levels.includes(config.detail)) {
633
+ return config.detail;
634
+ }
635
+
636
+ if (surface === "mcp") {
637
+ const optedDown = levels.some(
638
+ (level, index) => index > 0 && params[level] === false,
639
+ );
640
+ const highest = levels.at(-1);
641
+ if (!optedDown && highest !== undefined) {
642
+ return highest;
643
+ }
644
+ }
645
+
646
+ const packDefault = disclosure.default;
647
+ if (packDefault !== undefined && levels.includes(packDefault)) {
648
+ return packDefault;
649
+ }
650
+ return levels[0] ?? "";
206
651
  }
207
652
 
208
653
  async function listEntityNames(
@@ -218,6 +663,13 @@ async function listEntityNames(
218
663
  return rows.map((row) => row.name ?? "").filter((name) => name !== "");
219
664
  }
220
665
 
666
+ /** Return a value only when it is a scalar string (expands hold arrays). */
667
+ function scalar(
668
+ value: string | readonly PackRow[] | undefined,
669
+ ): string | undefined {
670
+ return typeof value === "string" ? value : undefined;
671
+ }
672
+
221
673
  /**
222
674
  * Quote a filter value for a shell help example when it is not a bare word.
223
675
  *
@@ -232,8 +684,10 @@ function quoteExampleValue(value: string): string {
232
684
  /**
233
685
  * Project declared pack filters onto kernel story parameters.
234
686
  *
235
- * The declared value set becomes the parameter's enum, which the kernel
236
- * projects to CLI select choices and an MCP enum schema.
687
+ * A declared value set becomes the parameter's enum, which the kernel
688
+ * projects to CLI select choices and an MCP enum schema. A value-free
689
+ * filter projects as a plain string parameter — its valid values are
690
+ * data-driven and enforced only as a post-query row predicate.
237
691
  */
238
692
  function projectFilterParams(
239
693
  filters: readonly StoryPackFilter[] | undefined,
@@ -243,11 +697,28 @@ function projectFilterParams(
243
697
  type: "string",
244
698
  description:
245
699
  filter.description ??
246
- `Filter by ${filter.variable} (${filter.values.join(", ")})`,
247
- enum: filter.values,
700
+ (filter.values
701
+ ? `Filter by ${filter.variable} (${filter.values.join(", ")})`
702
+ : `Filter by ${filter.variable}`),
703
+ ...(filter.values ? { enum: filter.values } : {}),
248
704
  }));
249
705
  }
250
706
 
707
+ /**
708
+ * Project a declared free-text search onto the `search` story parameter.
709
+ */
710
+ function projectSearchParam(search: StoryPackSearch | undefined): StoryParam[] {
711
+ if (!search) return [];
712
+ return [
713
+ {
714
+ name: "search",
715
+ type: "string",
716
+ description:
717
+ search.description ?? `Search in ${search.variables.join(", ")}`,
718
+ },
719
+ ];
720
+ }
721
+
251
722
  function capitalize(value: string): string {
252
723
  return value.charAt(0).toUpperCase() + value.slice(1);
253
724
  }