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
package/src/config.ts ADDED
@@ -0,0 +1,124 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { resolveStashDir } from "./common"
4
+
5
+ // ── Types ───────────────────────────────────────────────────────────────────
6
+
7
+ export interface EmbeddingConnectionConfig {
8
+ /** OpenAI-compatible embeddings endpoint (e.g. "http://localhost:11434/v1/embeddings") */
9
+ endpoint: string
10
+ /** Model name to use for embeddings (e.g. "nomic-embed-text") */
11
+ model: string
12
+ /** Optional API key for authenticated endpoints */
13
+ apiKey?: string
14
+ }
15
+
16
+ export interface LlmConnectionConfig {
17
+ /** OpenAI-compatible chat completions endpoint (e.g. "http://localhost:11434/v1/chat/completions") */
18
+ endpoint: string
19
+ /** Model name to use (e.g. "llama3.2") */
20
+ model: string
21
+ /** Optional API key for authenticated endpoints */
22
+ apiKey?: string
23
+ }
24
+
25
+ export interface AgentikitConfig {
26
+ /** Whether semantic search is enabled. Default: true */
27
+ semanticSearch: boolean
28
+ /** Additional stash directories to search alongside the primary one */
29
+ additionalStashDirs: string[]
30
+ /** OpenAI-compatible embedding endpoint config. If not set, uses local @xenova/transformers */
31
+ embedding?: EmbeddingConnectionConfig
32
+ /** OpenAI-compatible LLM endpoint config for metadata generation. If not set, uses heuristic generation */
33
+ llm?: LlmConnectionConfig
34
+ }
35
+
36
+ // ── Defaults ────────────────────────────────────────────────────────────────
37
+
38
+ export const DEFAULT_CONFIG: AgentikitConfig = {
39
+ semanticSearch: true,
40
+ additionalStashDirs: [],
41
+ }
42
+
43
+ // ── Paths ───────────────────────────────────────────────────────────────────
44
+
45
+ export function getConfigPath(stashDir: string): string {
46
+ return path.join(stashDir, "config.json")
47
+ }
48
+
49
+ // ── Load / Save / Update ────────────────────────────────────────────────────
50
+
51
+ export function loadConfig(stashDir?: string): AgentikitConfig {
52
+ const dir = stashDir ?? resolveStashDir()
53
+ const configPath = getConfigPath(dir)
54
+
55
+ let raw: Record<string, unknown>
56
+ try {
57
+ raw = JSON.parse(fs.readFileSync(configPath, "utf8"))
58
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
59
+ return { ...DEFAULT_CONFIG }
60
+ }
61
+ } catch {
62
+ return { ...DEFAULT_CONFIG }
63
+ }
64
+
65
+ return pickKnownKeys(raw)
66
+ }
67
+
68
+ export function saveConfig(config: AgentikitConfig, stashDir?: string): void {
69
+ const dir = stashDir ?? resolveStashDir()
70
+ const configPath = getConfigPath(dir)
71
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8")
72
+ }
73
+
74
+ export function updateConfig(
75
+ partial: Partial<AgentikitConfig>,
76
+ stashDir?: string,
77
+ ): AgentikitConfig {
78
+ const dir = stashDir ?? resolveStashDir()
79
+ const current = loadConfig(dir)
80
+ const merged: AgentikitConfig = { ...current, ...partial }
81
+ saveConfig(merged, dir)
82
+ return merged
83
+ }
84
+
85
+ // ── Helpers ─────────────────────────────────────────────────────────────────
86
+
87
+ function pickKnownKeys(raw: Record<string, unknown>): AgentikitConfig {
88
+ const config: AgentikitConfig = { ...DEFAULT_CONFIG }
89
+
90
+ if (typeof raw.semanticSearch === "boolean") {
91
+ config.semanticSearch = raw.semanticSearch
92
+ }
93
+
94
+ if (Array.isArray(raw.additionalStashDirs)) {
95
+ config.additionalStashDirs = raw.additionalStashDirs.filter(
96
+ (d): d is string => typeof d === "string",
97
+ )
98
+ }
99
+
100
+ const embedding = parseConnectionConfig(raw.embedding)
101
+ if (embedding) config.embedding = embedding
102
+
103
+ const llm = parseConnectionConfig(raw.llm)
104
+ if (llm) config.llm = llm
105
+
106
+ return config
107
+ }
108
+
109
+ function parseConnectionConfig(
110
+ value: unknown,
111
+ ): EmbeddingConnectionConfig | LlmConnectionConfig | undefined {
112
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined
113
+ const obj = value as Record<string, unknown>
114
+ if (typeof obj.endpoint !== "string" || !obj.endpoint) return undefined
115
+ if (typeof obj.model !== "string" || !obj.model) return undefined
116
+ const result: { endpoint: string; model: string; apiKey?: string } = {
117
+ endpoint: obj.endpoint,
118
+ model: obj.model,
119
+ }
120
+ if (typeof obj.apiKey === "string" && obj.apiKey) {
121
+ result.apiKey = obj.apiKey
122
+ }
123
+ return result
124
+ }
@@ -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
+ }