agentikit 0.0.7 → 0.0.8

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 (80) hide show
  1. package/README.md +113 -77
  2. package/dist/index.d.ts +13 -3
  3. package/dist/index.js +7 -2
  4. package/dist/src/asset-spec.d.ts +14 -0
  5. package/dist/src/asset-spec.js +46 -0
  6. package/dist/src/cli.js +154 -52
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +31 -0
  10. package/dist/src/config.js +74 -0
  11. package/dist/src/embedder.d.ts +10 -0
  12. package/dist/src/embedder.js +87 -0
  13. package/dist/src/frontmatter.d.ts +30 -0
  14. package/dist/src/frontmatter.js +86 -0
  15. package/dist/src/indexer.d.ts +20 -2
  16. package/dist/src/indexer.js +212 -80
  17. package/dist/src/init.d.ts +19 -0
  18. package/dist/src/init.js +87 -0
  19. package/dist/src/llm.d.ts +15 -0
  20. package/dist/src/llm.js +91 -0
  21. package/dist/src/markdown.d.ts +18 -0
  22. package/dist/src/markdown.js +77 -0
  23. package/dist/src/metadata.d.ts +10 -2
  24. package/dist/src/metadata.js +146 -30
  25. package/dist/src/ripgrep-install.d.ts +12 -0
  26. package/dist/src/ripgrep-install.js +169 -0
  27. package/dist/src/ripgrep-resolve.d.ts +13 -0
  28. package/dist/src/ripgrep-resolve.js +68 -0
  29. package/dist/src/ripgrep.d.ts +3 -36
  30. package/dist/src/ripgrep.js +2 -262
  31. package/dist/src/similarity.d.ts +1 -2
  32. package/dist/src/similarity.js +11 -0
  33. package/dist/src/stash-ref.d.ts +7 -0
  34. package/dist/src/stash-ref.js +33 -0
  35. package/dist/src/stash-resolve.d.ts +2 -0
  36. package/dist/src/stash-resolve.js +45 -0
  37. package/dist/src/stash-search.d.ts +6 -0
  38. package/dist/src/stash-search.js +269 -0
  39. package/dist/src/stash-show.d.ts +5 -0
  40. package/dist/src/stash-show.js +107 -0
  41. package/dist/src/stash-types.d.ts +53 -0
  42. package/dist/src/stash-types.js +1 -0
  43. package/dist/src/stash.d.ts +8 -63
  44. package/dist/src/stash.js +4 -633
  45. package/dist/src/tool-runner.d.ts +35 -0
  46. package/dist/src/tool-runner.js +100 -0
  47. package/dist/src/walker.d.ts +19 -0
  48. package/dist/src/walker.js +47 -0
  49. package/package.json +8 -14
  50. package/src/asset-spec.ts +69 -0
  51. package/src/cli.ts +164 -48
  52. package/src/common.ts +58 -0
  53. package/src/config.ts +124 -0
  54. package/src/embedder.ts +117 -0
  55. package/src/frontmatter.ts +95 -0
  56. package/src/indexer.ts +244 -84
  57. package/src/init.ts +106 -0
  58. package/src/llm.ts +124 -0
  59. package/src/markdown.ts +106 -0
  60. package/src/metadata.ts +157 -29
  61. package/src/ripgrep-install.ts +200 -0
  62. package/src/ripgrep-resolve.ts +72 -0
  63. package/src/ripgrep.ts +3 -315
  64. package/src/similarity.ts +13 -1
  65. package/src/stash-ref.ts +41 -0
  66. package/src/stash-resolve.ts +47 -0
  67. package/src/stash-search.ts +343 -0
  68. package/src/stash-show.ts +104 -0
  69. package/src/stash-types.ts +46 -0
  70. package/src/stash.ts +16 -760
  71. package/src/tool-runner.ts +129 -0
  72. package/src/walker.ts +53 -0
  73. package/.claude-plugin/plugin.json +0 -21
  74. package/commands/open.md +0 -11
  75. package/commands/run.md +0 -11
  76. package/commands/search.md +0 -11
  77. package/dist/src/plugin.d.ts +0 -2
  78. package/dist/src/plugin.js +0 -55
  79. package/skills/stash/SKILL.md +0 -73
  80. package/src/plugin.ts +0 -56
@@ -0,0 +1,343 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { type AgentikitAssetType, hasErrnoCode, resolveStashDir } from "./common"
4
+ import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec"
5
+ import { loadSearchIndex, buildSearchText, type IndexedEntry } from "./indexer"
6
+ import { TfIdfAdapter, type ScoredEntry } from "./similarity"
7
+ import { buildToolInfo } from "./tool-runner"
8
+ import { walkStash } from "./walker"
9
+ import { makeOpenRef } from "./stash-ref"
10
+ import type { AgentikitSearchType, SearchHit, SearchResponse } from "./stash-types"
11
+ import { loadConfig } from "./config"
12
+
13
+ type IndexedAsset = {
14
+ type: AgentikitAssetType
15
+ name: string
16
+ path: string
17
+ }
18
+
19
+ const DEFAULT_LIMIT = 20
20
+
21
+ export async function agentikitSearch(input: {
22
+ query: string
23
+ type?: AgentikitSearchType
24
+ limit?: number
25
+ }): Promise<SearchResponse> {
26
+ const t0 = Date.now()
27
+ const query = input.query.trim().toLowerCase()
28
+ const searchType = input.type ?? "any"
29
+ const limit = normalizeLimit(input.limit)
30
+ const stashDir = resolveStashDir()
31
+ const config = loadConfig(stashDir)
32
+
33
+ const allStashDirs = [
34
+ stashDir,
35
+ ...config.additionalStashDirs.filter((d) => {
36
+ try { return fs.statSync(d).isDirectory() } catch { return false }
37
+ }),
38
+ ]
39
+
40
+ // Try indexed search (single unified pipeline: embedding + TF-IDF as weighted features)
41
+ const index = loadSearchIndex()
42
+ if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
43
+ const { hits, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config)
44
+ return {
45
+ stashDir,
46
+ hits,
47
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
48
+ timing: { totalMs: Date.now() - t0, rankMs, embedMs },
49
+ }
50
+ }
51
+
52
+ // No index: fall back to filesystem walk + substring match across all stash dirs
53
+ const hits = allStashDirs
54
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir))
55
+ .slice(0, limit)
56
+
57
+ return {
58
+ stashDir,
59
+ hits,
60
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
61
+ timing: { totalMs: Date.now() - t0 },
62
+ }
63
+ }
64
+
65
+ // ── Unified indexed search ──────────────────────────────────────────────────
66
+
67
+ async function searchIndex(
68
+ index: import("./indexer").SearchIndex,
69
+ query: string,
70
+ searchType: AgentikitSearchType,
71
+ limit: number,
72
+ stashDir: string,
73
+ allStashDirs: string[],
74
+ config: import("./config").AgentikitConfig,
75
+ ): Promise<{ hits: SearchHit[]; embedMs?: number; rankMs?: number }> {
76
+ // Filter candidates by type
77
+ let candidates = index.entries
78
+ if (searchType !== "any") {
79
+ candidates = candidates.filter((ie) => ie.entry.type === searchType)
80
+ }
81
+
82
+ if (candidates.length === 0) return { hits: [] }
83
+
84
+ // Empty query: return all entries (no scoring needed)
85
+ if (!query) {
86
+ return { hits: candidates.slice(0, limit).map((ie) =>
87
+ buildIndexedHit({ entry: ie.entry, path: ie.path, score: 1, query, rankingMode: "tfidf", defaultStashDir: stashDir, allStashDirs }),
88
+ ) }
89
+ }
90
+
91
+ // Score each candidate using available signals
92
+ const tEmbed0 = Date.now()
93
+ const embeddingScores = await tryEmbeddingScores(candidates, query, config)
94
+ const embedMs = Date.now() - tEmbed0
95
+
96
+ const tRank0 = Date.now()
97
+ const tfidfScores = computeTfidfScores(index, candidates, query, searchType)
98
+
99
+ const scored: Array<{ ie: IndexedEntry; score: number; rankingMode: "semantic" | "tfidf" }> = []
100
+
101
+ for (const ie of candidates) {
102
+ const key = ie.path
103
+ const embScore = embeddingScores?.get(key)
104
+ const tfidfScore = tfidfScores.get(key) ?? 0
105
+
106
+ if (embScore !== undefined) {
107
+ // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
108
+ const blended = embScore * 0.7 + tfidfScore * 0.3
109
+ if (blended > 0) scored.push({ ie, score: blended, rankingMode: "semantic" })
110
+ } else if (tfidfScore > 0) {
111
+ scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" })
112
+ }
113
+ }
114
+
115
+ scored.sort((a, b) => b.score - a.score)
116
+ const rankMs = Date.now() - tRank0
117
+
118
+ return { embedMs, rankMs, hits: scored.slice(0, limit).map(({ ie, score, rankingMode }) =>
119
+ buildIndexedHit({
120
+ entry: ie.entry,
121
+ path: ie.path,
122
+ score: Math.round(score * 1000) / 1000,
123
+ query,
124
+ rankingMode,
125
+ defaultStashDir: stashDir,
126
+ allStashDirs,
127
+ }),
128
+ ) }
129
+ }
130
+
131
+ // ── Embedding scorer ────────────────────────────────────────────────────────
132
+
133
+ async function tryEmbeddingScores(
134
+ candidates: IndexedEntry[],
135
+ query: string,
136
+ config: import("./config").AgentikitConfig,
137
+ ): Promise<Map<string, number> | null> {
138
+ if (!config.semanticSearch) return null
139
+
140
+ const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0)
141
+ if (withEmbeddings.length === 0) return null
142
+
143
+ try {
144
+ const { embed, cosineSimilarity } = await import("./embedder.js")
145
+ const queryEmbedding = await embed(query, config.embedding)
146
+ const scores = new Map<string, number>()
147
+ for (const ie of withEmbeddings) {
148
+ scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding!))
149
+ }
150
+ return scores
151
+ } catch {
152
+ return null
153
+ }
154
+ }
155
+
156
+ // ── TF-IDF scorer ───────────────────────────────────────────────────────────
157
+
158
+ function computeTfidfScores(
159
+ index: import("./indexer").SearchIndex,
160
+ candidates: IndexedEntry[],
161
+ query: string,
162
+ searchType: AgentikitSearchType,
163
+ ): Map<string, number> {
164
+ const candidateScoredEntries = toScoredEntries(candidates)
165
+
166
+ let adapter: TfIdfAdapter
167
+ if (index.tfidf) {
168
+ const allScored = toScoredEntries(index.entries)
169
+ adapter = TfIdfAdapter.deserialize(index.tfidf, allScored)
170
+ } else {
171
+ adapter = new TfIdfAdapter()
172
+ adapter.buildIndex(candidateScoredEntries)
173
+ }
174
+
175
+ const typeFilter = searchType === "any" ? undefined : searchType
176
+ const results = adapter.search(query, candidates.length, typeFilter)
177
+
178
+ const scores = new Map<string, number>()
179
+ for (const r of results) {
180
+ scores.set(r.path, r.score)
181
+ }
182
+ return scores
183
+ }
184
+
185
+ // ── Substring fallback (no index) ───────────────────────────────────────────
186
+
187
+ function substringSearch(
188
+ query: string,
189
+ searchType: AgentikitSearchType,
190
+ limit: number,
191
+ stashDir: string,
192
+ ): SearchHit[] {
193
+ const assets = indexAssets(stashDir, searchType)
194
+ return assets
195
+ .filter((asset) => asset.name.toLowerCase().includes(query))
196
+ .sort(compareAssets)
197
+ .slice(0, limit)
198
+ .map((asset): SearchHit => assetToSearchHit(asset, stashDir))
199
+ }
200
+
201
+ // ── Hit building ────────────────────────────────────────────────────────────
202
+
203
+ function findStashDirForPath(filePath: string, stashDirs: string[]): string | undefined {
204
+ const resolved = path.resolve(filePath)
205
+ for (const dir of stashDirs) {
206
+ if (resolved.startsWith(path.resolve(dir) + path.sep)) return dir
207
+ }
208
+ return undefined
209
+ }
210
+
211
+ function buildIndexedHit(input: {
212
+ entry: IndexedEntry["entry"]
213
+ path: string
214
+ score: number
215
+ query: string
216
+ rankingMode: "semantic" | "tfidf"
217
+ defaultStashDir: string
218
+ allStashDirs: string[]
219
+ }): SearchHit {
220
+ const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir
221
+ const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type])
222
+ const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
223
+ ?? input.entry.name
224
+
225
+ const qualityBoost = input.entry.generated === true ? 0 : 0.05
226
+ const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0
227
+ const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000
228
+
229
+ const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost)
230
+
231
+ const hit: SearchHit = {
232
+ type: input.entry.type,
233
+ name: input.entry.name,
234
+ path: input.path,
235
+ openRef: makeOpenRef(input.entry.type, openRefName),
236
+ description: input.entry.description,
237
+ tags: input.entry.tags,
238
+ score,
239
+ whyMatched,
240
+ }
241
+
242
+ if (input.entry.type === "tool") {
243
+ try {
244
+ const toolInfo = buildToolInfo(entryStashDir, input.path)
245
+ hit.runCmd = toolInfo.runCmd
246
+ hit.kind = toolInfo.kind
247
+ } catch (error: unknown) {
248
+ if (!hasErrnoCode(error, "ENOENT")) throw error
249
+ }
250
+ }
251
+
252
+ return hit
253
+ }
254
+
255
+ function buildWhyMatched(
256
+ entry: IndexedEntry["entry"],
257
+ query: string,
258
+ rankingMode: "semantic" | "tfidf",
259
+ qualityBoost: number,
260
+ confidenceBoost: number,
261
+ ): string[] {
262
+ const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"]
263
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean)
264
+
265
+ const name = entry.name.toLowerCase()
266
+ const tags = entry.tags?.join(" ").toLowerCase() ?? ""
267
+ const intents = entry.intents?.join(" ").toLowerCase() ?? ""
268
+ const aliases = entry.aliases?.join(" ").toLowerCase() ?? ""
269
+
270
+ if (tokens.some((t) => name.includes(t))) reasons.push("matched name tokens")
271
+ if (tokens.some((t) => tags.includes(t))) reasons.push("matched tags")
272
+ if (tokens.some((t) => intents.includes(t))) reasons.push("matched intents")
273
+ if (tokens.some((t) => aliases.includes(t))) reasons.push("matched aliases")
274
+ if (qualityBoost > 0) reasons.push("curated metadata boost")
275
+ if (confidenceBoost > 0) reasons.push("metadata confidence boost")
276
+
277
+ return reasons
278
+ }
279
+
280
+ // ── Helpers ─────────────────────────────────────────────────────────────────
281
+
282
+ function toScoredEntries(entries: IndexedEntry[]): ScoredEntry[] {
283
+ return entries.map((ie) => ({
284
+ id: `${ie.entry.type}:${ie.entry.name}`,
285
+ text: buildSearchText(ie.entry),
286
+ entry: ie.entry,
287
+ path: ie.path,
288
+ }))
289
+ }
290
+
291
+ function assetToSearchHit(asset: IndexedAsset, stashDir: string): SearchHit {
292
+ if (asset.type !== "tool") {
293
+ return {
294
+ type: asset.type,
295
+ name: asset.name,
296
+ path: asset.path,
297
+ openRef: makeOpenRef(asset.type, asset.name),
298
+ }
299
+ }
300
+ const toolInfo = buildToolInfo(stashDir, asset.path)
301
+ return {
302
+ type: "tool",
303
+ name: asset.name,
304
+ path: asset.path,
305
+ openRef: makeOpenRef("tool", asset.name),
306
+ runCmd: toolInfo.runCmd,
307
+ kind: toolInfo.kind,
308
+ }
309
+ }
310
+
311
+ function normalizeLimit(limit?: number): number {
312
+ if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
313
+ return DEFAULT_LIMIT
314
+ }
315
+ return Math.min(Math.floor(limit), 200)
316
+ }
317
+
318
+ function fileToAsset(assetType: AgentikitAssetType, root: string, file: string): IndexedAsset | undefined {
319
+ const name = deriveCanonicalAssetName(assetType, root, file)
320
+ if (!name) return undefined
321
+ return { type: assetType, name, path: file }
322
+ }
323
+
324
+ function indexAssets(stashDir: string, type: AgentikitSearchType): IndexedAsset[] {
325
+ const assets: IndexedAsset[] = []
326
+ const types = type === "any" ? ASSET_TYPES : [type]
327
+ for (const assetType of types) {
328
+ const root = path.join(stashDir, TYPE_DIRS[assetType])
329
+ const groups = walkStash(root, assetType)
330
+ for (const { files } of groups) {
331
+ for (const file of files) {
332
+ const asset = fileToAsset(assetType, root, file)
333
+ if (asset) assets.push(asset)
334
+ }
335
+ }
336
+ }
337
+ return assets
338
+ }
339
+
340
+ function compareAssets(a: IndexedAsset, b: IndexedAsset): number {
341
+ if (a.type !== b.type) return a.type.localeCompare(b.type)
342
+ return a.name.localeCompare(b.name)
343
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { parseFrontmatter, toStringOrUndefined } from "./frontmatter"
4
+ import { resolveStashDir } from "./common"
5
+ import { parseOpenRef } from "./stash-ref"
6
+ import { resolveAssetPath } from "./stash-resolve"
7
+ import type { KnowledgeView, ShowResponse } from "./stash-types"
8
+ import { parseMarkdownToc, extractSection, extractLineRange, extractFrontmatterOnly, formatToc } from "./markdown"
9
+ import { buildToolInfo } from "./tool-runner"
10
+ import { loadConfig } from "./config"
11
+
12
+ export function agentikitShow(input: { ref: string; view?: KnowledgeView }): ShowResponse {
13
+ const parsed = parseOpenRef(input.ref)
14
+ const stashDir = resolveStashDir()
15
+ const config = loadConfig(stashDir)
16
+ const allStashDirs = [
17
+ stashDir,
18
+ ...config.additionalStashDirs.filter((d) => {
19
+ try { return fs.statSync(d).isDirectory() } catch { return false }
20
+ }),
21
+ ]
22
+
23
+ let assetPath: string | undefined
24
+ let lastError: Error | undefined
25
+ for (const dir of allStashDirs) {
26
+ try {
27
+ assetPath = resolveAssetPath(dir, parsed.type, parsed.name)
28
+ break
29
+ } catch (err) {
30
+ lastError = err instanceof Error ? err : new Error(String(err))
31
+ }
32
+ }
33
+ if (!assetPath) {
34
+ throw lastError ?? new Error(`Stash asset not found for ref: ${parsed.type}:${parsed.name}`)
35
+ }
36
+ const content = fs.readFileSync(assetPath, "utf8")
37
+
38
+ switch (parsed.type) {
39
+ case "skill":
40
+ return {
41
+ type: "skill",
42
+ name: parsed.name,
43
+ path: assetPath,
44
+ content,
45
+ }
46
+ case "command": {
47
+ const parsedMd = parseFrontmatter(content)
48
+ return {
49
+ type: "command",
50
+ name: parsed.name,
51
+ path: assetPath,
52
+ description: toStringOrUndefined(parsedMd.data.description),
53
+ template: parsedMd.content,
54
+ }
55
+ }
56
+ case "agent": {
57
+ const parsedMd = parseFrontmatter(content)
58
+ return {
59
+ type: "agent",
60
+ name: parsed.name,
61
+ path: assetPath,
62
+ description: toStringOrUndefined(parsedMd.data.description),
63
+ prompt: parsedMd.content,
64
+ toolPolicy: parsedMd.data.tools,
65
+ modelHint: parsedMd.data.model,
66
+ }
67
+ }
68
+ case "tool": {
69
+ const assetStashDir = allStashDirs.find((d) => path.resolve(assetPath!).startsWith(path.resolve(d) + path.sep)) ?? stashDir
70
+ const toolInfo = buildToolInfo(assetStashDir, assetPath)
71
+ return {
72
+ type: "tool",
73
+ name: parsed.name,
74
+ path: assetPath,
75
+ runCmd: toolInfo.runCmd,
76
+ kind: toolInfo.kind,
77
+ }
78
+ }
79
+ case "knowledge": {
80
+ const v = input.view ?? { mode: "full" }
81
+ switch (v.mode) {
82
+ case "toc": {
83
+ const toc = parseMarkdownToc(content)
84
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: formatToc(toc) }
85
+ }
86
+ case "frontmatter": {
87
+ const fm = extractFrontmatterOnly(content)
88
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: fm ?? "(no frontmatter)" }
89
+ }
90
+ case "section": {
91
+ const section = extractSection(content, v.heading)
92
+ if (!section) throw new Error(`Section "${v.heading}" not found in ${parsed.name}`)
93
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: section.content }
94
+ }
95
+ case "lines": {
96
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: extractLineRange(content, v.start, v.end) }
97
+ }
98
+ default: {
99
+ return { type: "knowledge", name: parsed.name, path: assetPath, content }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,46 @@
1
+ import type { AgentikitAssetType } from "./common"
2
+ import type { ToolKind } from "./tool-runner"
3
+
4
+ export type AgentikitSearchType = AgentikitAssetType | "any"
5
+
6
+ export interface SearchHit {
7
+ type: AgentikitAssetType
8
+ name: string
9
+ path: string
10
+ openRef: string
11
+ description?: string
12
+ tags?: string[]
13
+ score?: number
14
+ whyMatched?: string[]
15
+ runCmd?: string
16
+ kind?: ToolKind
17
+ }
18
+
19
+ export interface SearchResponse {
20
+ stashDir: string
21
+ hits: SearchHit[]
22
+ tip?: string
23
+ /** Timing counters in milliseconds */
24
+ timing?: { totalMs: number; rankMs?: number; embedMs?: number }
25
+ }
26
+
27
+ export interface ShowResponse {
28
+ type: AgentikitAssetType
29
+ name: string
30
+ path: string
31
+ content?: string
32
+ template?: string
33
+ prompt?: string
34
+ description?: string
35
+ toolPolicy?: unknown
36
+ modelHint?: unknown
37
+ runCmd?: string
38
+ kind?: ToolKind
39
+ }
40
+
41
+ export type KnowledgeView =
42
+ | { mode: "full" }
43
+ | { mode: "toc" }
44
+ | { mode: "frontmatter" }
45
+ | { mode: "section"; heading: string }
46
+ | { mode: "lines"; start: number; end: number }