agentikit 0.0.7 → 0.0.9

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 (98) hide show
  1. package/README.md +215 -76
  2. package/dist/index.d.ts +17 -3
  3. package/dist/index.js +10 -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 +268 -57
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +37 -0
  10. package/dist/src/config.js +124 -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 +11 -2
  24. package/dist/src/metadata.js +161 -29
  25. package/dist/src/registry-install.d.ts +11 -0
  26. package/dist/src/registry-install.js +208 -0
  27. package/dist/src/registry-resolve.d.ts +3 -0
  28. package/dist/src/registry-resolve.js +231 -0
  29. package/dist/src/registry-search.d.ts +5 -0
  30. package/dist/src/registry-search.js +129 -0
  31. package/dist/src/registry-types.d.ts +55 -0
  32. package/dist/src/registry-types.js +1 -0
  33. package/dist/src/ripgrep-install.d.ts +12 -0
  34. package/dist/src/ripgrep-install.js +169 -0
  35. package/dist/src/ripgrep-resolve.d.ts +13 -0
  36. package/dist/src/ripgrep-resolve.js +68 -0
  37. package/dist/src/ripgrep.d.ts +3 -36
  38. package/dist/src/ripgrep.js +2 -262
  39. package/dist/src/similarity.d.ts +1 -2
  40. package/dist/src/similarity.js +11 -0
  41. package/dist/src/stash-add.d.ts +4 -0
  42. package/dist/src/stash-add.js +59 -0
  43. package/dist/src/stash-ref.d.ts +7 -0
  44. package/dist/src/stash-ref.js +33 -0
  45. package/dist/src/stash-registry.d.ts +18 -0
  46. package/dist/src/stash-registry.js +221 -0
  47. package/dist/src/stash-resolve.d.ts +2 -0
  48. package/dist/src/stash-resolve.js +45 -0
  49. package/dist/src/stash-search.d.ts +8 -0
  50. package/dist/src/stash-search.js +484 -0
  51. package/dist/src/stash-show.d.ts +5 -0
  52. package/dist/src/stash-show.js +114 -0
  53. package/dist/src/stash-types.d.ts +217 -0
  54. package/dist/src/stash-types.js +1 -0
  55. package/dist/src/stash.d.ts +10 -63
  56. package/dist/src/stash.js +6 -633
  57. package/dist/src/tool-runner.d.ts +35 -0
  58. package/dist/src/tool-runner.js +100 -0
  59. package/dist/src/walker.d.ts +19 -0
  60. package/dist/src/walker.js +47 -0
  61. package/package.json +8 -14
  62. package/src/asset-spec.ts +69 -0
  63. package/src/cli.ts +282 -46
  64. package/src/common.ts +58 -0
  65. package/src/config.ts +183 -0
  66. package/src/embedder.ts +117 -0
  67. package/src/frontmatter.ts +95 -0
  68. package/src/indexer.ts +244 -84
  69. package/src/init.ts +106 -0
  70. package/src/llm.ts +124 -0
  71. package/src/markdown.ts +106 -0
  72. package/src/metadata.ts +171 -27
  73. package/src/registry-install.ts +245 -0
  74. package/src/registry-resolve.ts +272 -0
  75. package/src/registry-search.ts +145 -0
  76. package/src/registry-types.ts +64 -0
  77. package/src/ripgrep-install.ts +200 -0
  78. package/src/ripgrep-resolve.ts +72 -0
  79. package/src/ripgrep.ts +3 -315
  80. package/src/similarity.ts +13 -1
  81. package/src/stash-add.ts +66 -0
  82. package/src/stash-ref.ts +41 -0
  83. package/src/stash-registry.ts +259 -0
  84. package/src/stash-resolve.ts +47 -0
  85. package/src/stash-search.ts +595 -0
  86. package/src/stash-show.ts +112 -0
  87. package/src/stash-types.ts +221 -0
  88. package/src/stash.ts +31 -760
  89. package/src/tool-runner.ts +129 -0
  90. package/src/walker.ts +53 -0
  91. package/.claude-plugin/plugin.json +0 -21
  92. package/commands/open.md +0 -11
  93. package/commands/run.md +0 -11
  94. package/commands/search.md +0 -11
  95. package/dist/src/plugin.d.ts +0 -2
  96. package/dist/src/plugin.js +0 -55
  97. package/skills/stash/SKILL.md +0 -73
  98. package/src/plugin.ts +0 -56
package/src/common.ts ADDED
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { TYPE_DIRS } from "./asset-spec"
4
+
5
+ // ── Types ───────────────────────────────────────────────────────────────────
6
+
7
+ export type AgentikitAssetType = "tool" | "skill" | "command" | "agent" | "knowledge"
8
+
9
+ // ── Constants ───────────────────────────────────────────────────────────────
10
+
11
+ export const IS_WINDOWS = process.platform === "win32"
12
+ export { SCRIPT_EXTENSIONS, TYPE_DIRS } from "./asset-spec"
13
+
14
+ // ── Validators ──────────────────────────────────────────────────────────────
15
+
16
+ export function isAssetType(type: string): type is AgentikitAssetType {
17
+ return type in TYPE_DIRS
18
+ }
19
+
20
+ // ── Utilities ───────────────────────────────────────────────────────────────
21
+
22
+ export function resolveStashDir(): string {
23
+ const raw = process.env.AGENTIKIT_STASH_DIR?.trim()
24
+ if (!raw) {
25
+ throw new Error("AGENTIKIT_STASH_DIR is not set. Set it to your Agentikit stash path.")
26
+ }
27
+ const stashDir = path.resolve(raw)
28
+ let stat: fs.Stats
29
+ try {
30
+ stat = fs.statSync(stashDir)
31
+ } catch {
32
+ throw new Error(`Unable to read AGENTIKIT_STASH_DIR at "${stashDir}".`)
33
+ }
34
+ if (!stat.isDirectory()) {
35
+ throw new Error(`AGENTIKIT_STASH_DIR must point to a directory: "${stashDir}".`)
36
+ }
37
+ return stashDir
38
+ }
39
+
40
+ export function toPosix(input: string): string {
41
+ return input.replace(/\\/g, "/")
42
+ }
43
+
44
+ export function hasErrnoCode(error: unknown, code: string): boolean {
45
+ if (typeof error !== "object" || error === null || !("code" in error)) return false
46
+ return (error as Record<string, unknown>).code === code
47
+ }
48
+
49
+ export function isWithin(candidate: string, root: string): boolean {
50
+ const normalizedRoot = normalizeFsPathForComparison(path.resolve(root))
51
+ const normalizedCandidate = normalizeFsPathForComparison(path.resolve(candidate))
52
+ const rel = path.relative(normalizedRoot, normalizedCandidate)
53
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
54
+ }
55
+
56
+ function normalizeFsPathForComparison(value: string): string {
57
+ return process.platform === "win32" ? value.toLowerCase() : value
58
+ }
package/src/config.ts ADDED
@@ -0,0 +1,183 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { resolveStashDir } from "./common"
4
+ import type { RegistryInstalledEntry, RegistrySource } from "./registry-types"
5
+
6
+ // ── Types ───────────────────────────────────────────────────────────────────
7
+
8
+ export interface EmbeddingConnectionConfig {
9
+ /** OpenAI-compatible embeddings endpoint (e.g. "http://localhost:11434/v1/embeddings") */
10
+ endpoint: string
11
+ /** Model name to use for embeddings (e.g. "nomic-embed-text") */
12
+ model: string
13
+ /** Optional API key for authenticated endpoints */
14
+ apiKey?: string
15
+ }
16
+
17
+ export interface LlmConnectionConfig {
18
+ /** OpenAI-compatible chat completions endpoint (e.g. "http://localhost:11434/v1/chat/completions") */
19
+ endpoint: string
20
+ /** Model name to use (e.g. "llama3.2") */
21
+ model: string
22
+ /** Optional API key for authenticated endpoints */
23
+ apiKey?: string
24
+ }
25
+
26
+ export interface AgentikitConfig {
27
+ /** Whether semantic search is enabled. Default: true */
28
+ semanticSearch: boolean
29
+ /** Additional stash directories to search alongside the primary one */
30
+ additionalStashDirs: string[]
31
+ /** OpenAI-compatible embedding endpoint config. If not set, uses local @xenova/transformers */
32
+ embedding?: EmbeddingConnectionConfig
33
+ /** OpenAI-compatible LLM endpoint config for metadata generation. If not set, uses heuristic generation */
34
+ llm?: LlmConnectionConfig
35
+ /** Installed registry sources and local cache metadata */
36
+ registry?: RegistryConfig
37
+ }
38
+
39
+ export interface RegistryConfig {
40
+ installed: RegistryInstalledEntry[]
41
+ }
42
+
43
+ // ── Defaults ────────────────────────────────────────────────────────────────
44
+
45
+ export const DEFAULT_CONFIG: AgentikitConfig = {
46
+ semanticSearch: true,
47
+ additionalStashDirs: [],
48
+ }
49
+
50
+ // ── Paths ───────────────────────────────────────────────────────────────────
51
+
52
+ export function getConfigPath(stashDir: string): string {
53
+ return path.join(stashDir, "config.json")
54
+ }
55
+
56
+ // ── Load / Save / Update ────────────────────────────────────────────────────
57
+
58
+ export function loadConfig(stashDir?: string): AgentikitConfig {
59
+ const dir = stashDir ?? resolveStashDir()
60
+ const configPath = getConfigPath(dir)
61
+
62
+ let raw: Record<string, unknown>
63
+ try {
64
+ raw = JSON.parse(fs.readFileSync(configPath, "utf8"))
65
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
66
+ return { ...DEFAULT_CONFIG }
67
+ }
68
+ } catch {
69
+ return { ...DEFAULT_CONFIG }
70
+ }
71
+
72
+ return pickKnownKeys(raw)
73
+ }
74
+
75
+ export function saveConfig(config: AgentikitConfig, stashDir?: string): void {
76
+ const dir = stashDir ?? resolveStashDir()
77
+ const configPath = getConfigPath(dir)
78
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8")
79
+ }
80
+
81
+ export function updateConfig(
82
+ partial: Partial<AgentikitConfig>,
83
+ stashDir?: string,
84
+ ): AgentikitConfig {
85
+ const dir = stashDir ?? resolveStashDir()
86
+ const current = loadConfig(dir)
87
+ const merged: AgentikitConfig = { ...current, ...partial }
88
+ saveConfig(merged, dir)
89
+ return merged
90
+ }
91
+
92
+ // ── Helpers ─────────────────────────────────────────────────────────────────
93
+
94
+ function pickKnownKeys(raw: Record<string, unknown>): AgentikitConfig {
95
+ const config: AgentikitConfig = { ...DEFAULT_CONFIG }
96
+
97
+ if (typeof raw.semanticSearch === "boolean") {
98
+ config.semanticSearch = raw.semanticSearch
99
+ }
100
+
101
+ if (Array.isArray(raw.additionalStashDirs)) {
102
+ config.additionalStashDirs = raw.additionalStashDirs.filter(
103
+ (d): d is string => typeof d === "string",
104
+ )
105
+ }
106
+
107
+ const embedding = parseConnectionConfig(raw.embedding)
108
+ if (embedding) config.embedding = embedding
109
+
110
+ const llm = parseConnectionConfig(raw.llm)
111
+ if (llm) config.llm = llm
112
+
113
+ const registry = parseRegistryConfig(raw.registry)
114
+ if (registry) config.registry = registry
115
+
116
+ return config
117
+ }
118
+
119
+ function parseConnectionConfig(
120
+ value: unknown,
121
+ ): EmbeddingConnectionConfig | LlmConnectionConfig | undefined {
122
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined
123
+ const obj = value as Record<string, unknown>
124
+ if (typeof obj.endpoint !== "string" || !obj.endpoint) return undefined
125
+ if (typeof obj.model !== "string" || !obj.model) return undefined
126
+ const result: { endpoint: string; model: string; apiKey?: string } = {
127
+ endpoint: obj.endpoint,
128
+ model: obj.model,
129
+ }
130
+ if (typeof obj.apiKey === "string" && obj.apiKey) {
131
+ result.apiKey = obj.apiKey
132
+ }
133
+ return result
134
+ }
135
+
136
+ function parseRegistryConfig(value: unknown): RegistryConfig | undefined {
137
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined
138
+ const obj = value as Record<string, unknown>
139
+ if (!Array.isArray(obj.installed)) return undefined
140
+
141
+ const installed = obj.installed
142
+ .map((entry) => parseRegistryInstalledEntry(entry))
143
+ .filter((entry): entry is RegistryInstalledEntry => entry !== undefined)
144
+
145
+ return { installed }
146
+ }
147
+
148
+ function parseRegistryInstalledEntry(value: unknown): RegistryInstalledEntry | undefined {
149
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined
150
+ const obj = value as Record<string, unknown>
151
+
152
+ const id = asNonEmptyString(obj.id)
153
+ const source = asRegistrySource(obj.source)
154
+ const ref = asNonEmptyString(obj.ref)
155
+ const artifactUrl = asNonEmptyString(obj.artifactUrl)
156
+ const stashRoot = asNonEmptyString(obj.stashRoot)
157
+ const cacheDir = asNonEmptyString(obj.cacheDir)
158
+ const installedAt = asNonEmptyString(obj.installedAt)
159
+ if (!id || !source || !ref || !artifactUrl || !stashRoot || !cacheDir || !installedAt) return undefined
160
+
161
+ const entry: RegistryInstalledEntry = {
162
+ id,
163
+ source,
164
+ ref,
165
+ artifactUrl,
166
+ stashRoot,
167
+ cacheDir,
168
+ installedAt,
169
+ }
170
+ const resolvedVersion = asNonEmptyString(obj.resolvedVersion)
171
+ if (resolvedVersion) entry.resolvedVersion = resolvedVersion
172
+ const resolvedRevision = asNonEmptyString(obj.resolvedRevision)
173
+ if (resolvedRevision) entry.resolvedRevision = resolvedRevision
174
+ return entry
175
+ }
176
+
177
+ function asNonEmptyString(value: unknown): string | undefined {
178
+ return typeof value === "string" && value ? value : undefined
179
+ }
180
+
181
+ function asRegistrySource(value: unknown): RegistrySource | undefined {
182
+ return value === "npm" || value === "github" ? value : undefined
183
+ }
@@ -0,0 +1,117 @@
1
+ import type { EmbeddingConnectionConfig } from "./config"
2
+
3
+ // ── Types ───────────────────────────────────────────────────────────────────
4
+
5
+ export type EmbeddingVector = number[]
6
+
7
+ // ── Singleton local embedder ────────────────────────────────────────────────
8
+
9
+ let localEmbedder: any
10
+
11
+ async function getLocalEmbedder(): Promise<any> {
12
+ if (!localEmbedder) {
13
+ let pipeline: any
14
+ try {
15
+ const mod = await import("@xenova/transformers")
16
+ pipeline = mod.pipeline
17
+ } catch {
18
+ throw new Error(
19
+ "Semantic search requires @xenova/transformers. Install it with: npm install @xenova/transformers",
20
+ )
21
+ }
22
+ localEmbedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2")
23
+ }
24
+ return localEmbedder
25
+ }
26
+
27
+ async function embedLocal(text: string): Promise<EmbeddingVector> {
28
+ const model = await getLocalEmbedder()
29
+ const result = await model(text, { pooling: "mean", normalize: true })
30
+ return Array.from(result.data) as number[]
31
+ }
32
+
33
+ // ── OpenAI-compatible remote embedder ───────────────────────────────────────
34
+
35
+ async function embedRemote(
36
+ text: string,
37
+ config: EmbeddingConnectionConfig,
38
+ ): Promise<EmbeddingVector> {
39
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
40
+ if (config.apiKey) {
41
+ headers["Authorization"] = `Bearer ${config.apiKey}`
42
+ }
43
+
44
+ const response = await fetch(config.endpoint, {
45
+ method: "POST",
46
+ headers,
47
+ body: JSON.stringify({
48
+ input: text,
49
+ model: config.model,
50
+ }),
51
+ })
52
+
53
+ if (!response.ok) {
54
+ const body = await response.text().catch(() => "")
55
+ throw new Error(`Embedding request failed (${response.status}): ${body}`)
56
+ }
57
+
58
+ const json = (await response.json()) as {
59
+ data: Array<{ embedding: number[] }>
60
+ }
61
+
62
+ if (!json.data?.[0]?.embedding) {
63
+ throw new Error("Unexpected embedding response format: missing data[0].embedding")
64
+ }
65
+
66
+ return json.data[0].embedding
67
+ }
68
+
69
+ // ── Public API ──────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Generate an embedding for the given text.
73
+ * If embeddingConfig is provided, uses the configured OpenAI-compatible endpoint.
74
+ * Otherwise falls back to local @xenova/transformers.
75
+ */
76
+ export async function embed(
77
+ text: string,
78
+ embeddingConfig?: EmbeddingConnectionConfig,
79
+ ): Promise<EmbeddingVector> {
80
+ if (embeddingConfig) {
81
+ return embedRemote(text, embeddingConfig)
82
+ }
83
+ return embedLocal(text)
84
+ }
85
+
86
+ // ── Similarity ──────────────────────────────────────────────────────────────
87
+
88
+ export function cosineSimilarity(a: EmbeddingVector, b: EmbeddingVector): number {
89
+ const len = Math.min(a.length, b.length)
90
+ if (len === 0) return 0
91
+ let dot = 0
92
+ for (let i = 0; i < len; i++) {
93
+ dot += a[i] * b[i]
94
+ }
95
+ return dot
96
+ }
97
+
98
+ // ── Availability check ──────────────────────────────────────────────────────
99
+
100
+ export async function isEmbeddingAvailable(
101
+ embeddingConfig?: EmbeddingConnectionConfig,
102
+ ): Promise<boolean> {
103
+ if (embeddingConfig) {
104
+ try {
105
+ await embedRemote("test", embeddingConfig)
106
+ return true
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+ try {
112
+ await getLocalEmbedder()
113
+ return true
114
+ } catch {
115
+ return false
116
+ }
117
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared frontmatter parsing utilities.
3
+ *
4
+ * Provides a single, canonical YAML-subset frontmatter parser used by both
5
+ * the stash open logic and the metadata generator.
6
+ */
7
+
8
+ /**
9
+ * Parse YAML-subset frontmatter from a Markdown (or similar) string.
10
+ *
11
+ * Returns the parsed key-value data and the remaining body content.
12
+ */
13
+ export function parseFrontmatter(raw: string): {
14
+ data: Record<string, unknown>
15
+ content: string
16
+ frontmatter: string | null
17
+ bodyStartLine: number
18
+ } {
19
+ const parsedBlock = parseFrontmatterBlock(raw)
20
+ if (!parsedBlock) {
21
+ return { data: {}, content: raw, frontmatter: null, bodyStartLine: 1 }
22
+ }
23
+
24
+ const data: Record<string, unknown> = {}
25
+ let currentKey: string | null = null
26
+ let nested: Record<string, unknown> | null = null
27
+
28
+ for (const line of parsedBlock.frontmatter.split(/\r?\n/)) {
29
+ const indented = line.match(/^ (\w[\w-]*):\s*(.+)$/)
30
+ if (indented && currentKey && nested) {
31
+ nested[indented[1]] = parseYamlScalar(indented[2].trim())
32
+ continue
33
+ }
34
+
35
+ const top = line.match(/^(\w[\w-]*):\s*(.*)$/)
36
+ if (!top) {
37
+ continue
38
+ }
39
+
40
+ currentKey = top[1]
41
+ const value = top[2].trim()
42
+ if (value === "") {
43
+ nested = {}
44
+ data[currentKey] = nested
45
+ } else {
46
+ nested = null
47
+ data[currentKey] = parseYamlScalar(value)
48
+ }
49
+ }
50
+ return {
51
+ data,
52
+ content: parsedBlock.content,
53
+ frontmatter: parsedBlock.frontmatter,
54
+ bodyStartLine: parsedBlock.bodyStartLine,
55
+ }
56
+ }
57
+
58
+ export function parseFrontmatterBlock(
59
+ raw: string,
60
+ ): { frontmatter: string; content: string; bodyStartLine: number } | null {
61
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
62
+ if (!match) return null
63
+ return {
64
+ frontmatter: match[1],
65
+ content: match[2],
66
+ bodyStartLine: countLines(raw.slice(0, match[0].length - match[2].length)) + 1,
67
+ }
68
+ }
69
+
70
+ function countLines(text: string): number {
71
+ if (text.length === 0) return 0
72
+ return text.split(/\r?\n/).length - 1
73
+ }
74
+
75
+ /**
76
+ * Parse a simple YAML scalar value (string, boolean, or number).
77
+ */
78
+ export function parseYamlScalar(value: string): unknown {
79
+ if (value === "") return ""
80
+ if (value === "true") return true
81
+ if (value === "false") return false
82
+ const asNumber = Number(value)
83
+ if (!Number.isNaN(asNumber)) return asNumber
84
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
85
+ return value.slice(1, -1)
86
+ }
87
+ return value
88
+ }
89
+
90
+ /**
91
+ * Coerce an unknown value to a trimmed string, or return undefined if empty/non-string.
92
+ */
93
+ export function toStringOrUndefined(value: unknown): string | undefined {
94
+ return typeof value === "string" && value.trim() ? value : undefined
95
+ }