agentikit 0.0.3
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.
- package/.claude-plugin/plugin.json +21 -0
- package/README.md +147 -0
- package/commands/open.md +11 -0
- package/commands/run.md +11 -0
- package/commands/search.md +11 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +62 -0
- package/dist/src/indexer.d.ts +26 -0
- package/dist/src/indexer.js +167 -0
- package/dist/src/metadata.d.ts +33 -0
- package/dist/src/metadata.js +223 -0
- package/dist/src/plugin.d.ts +2 -0
- package/dist/src/plugin.js +55 -0
- package/dist/src/similarity.d.ts +35 -0
- package/dist/src/similarity.js +185 -0
- package/dist/src/stash.d.ts +58 -0
- package/dist/src/stash.js +580 -0
- package/package.json +66 -0
- package/skills/stash/SKILL.md +68 -0
- package/src/cli.ts +60 -0
- package/src/indexer.ts +211 -0
- package/src/metadata.ts +249 -0
- package/src/plugin.ts +56 -0
- package/src/similarity.ts +247 -0
- package/src/stash.ts +695 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stash
|
|
3
|
+
description: Search, open, and run extension assets from an Agentikit stash directory. Use when the user wants to find tools, skills, commands, or agents in their stash, view asset contents, or execute stash tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agentikit Stash
|
|
7
|
+
|
|
8
|
+
You have access to the `agentikit` CLI to manage extension assets from a stash directory.
|
|
9
|
+
|
|
10
|
+
The stash directory is configured via the `AGENTIKIT_STASH_DIR` environment variable and contains:
|
|
11
|
+
|
|
12
|
+
- **tools/** — executable scripts (.sh, .ts, .js, .ps1, .cmd, .bat)
|
|
13
|
+
- **skills/** — skill directories containing SKILL.md
|
|
14
|
+
- **commands/** — markdown template files
|
|
15
|
+
- **agents/** — markdown agent definition files
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### Build the search index
|
|
20
|
+
|
|
21
|
+
Scan stash directories, auto-generate missing `.stash.json` metadata, and build a semantic search index.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agentikit index
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run this after adding new extensions to enable semantic search ranking.
|
|
28
|
+
|
|
29
|
+
### Search the stash
|
|
30
|
+
|
|
31
|
+
Find assets by semantic similarity (if indexed) or name substring. Returns JSON with matching hits including `openRef` identifiers.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
agentikit search [query] [--type tool|skill|command|agent|any] [--limit N]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Open an asset
|
|
38
|
+
|
|
39
|
+
Retrieve the full content/payload of an asset using its `openRef` from search results.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
agentikit open <openRef>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Returns type-specific payloads:
|
|
46
|
+
- **skill** → full SKILL.md content
|
|
47
|
+
- **command** → markdown template + description
|
|
48
|
+
- **agent** → prompt + description, toolPolicy, modelHint
|
|
49
|
+
- **tool** → execution command and kind
|
|
50
|
+
|
|
51
|
+
### Run a tool
|
|
52
|
+
|
|
53
|
+
Execute a tool asset by its `openRef`. Only tool refs are supported.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
agentikit run <openRef>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Returns the tool's stdout/stderr output and exit code.
|
|
60
|
+
|
|
61
|
+
## Workflow
|
|
62
|
+
|
|
63
|
+
1. Build the index: `agentikit index`
|
|
64
|
+
2. Search for assets: `agentikit search "deploy" --type tool`
|
|
65
|
+
3. Inspect a result: `agentikit open <openRef>`
|
|
66
|
+
4. Run a tool: `agentikit run <openRef>`
|
|
67
|
+
|
|
68
|
+
All output is JSON for easy parsing.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { agentikitSearch, agentikitOpen, agentikitRun, agentikitInit } from "./stash"
|
|
3
|
+
import { agentikitIndex } from "./indexer"
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
const command = args[0]
|
|
7
|
+
|
|
8
|
+
function flag(name: string): string | undefined {
|
|
9
|
+
const idx = args.indexOf(name)
|
|
10
|
+
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function usage(): never {
|
|
14
|
+
console.error("Usage: agentikit <init|search|open|run> [options]")
|
|
15
|
+
console.error("")
|
|
16
|
+
console.error("Commands:")
|
|
17
|
+
console.error(" init Initialize agentikit stash directory and set AGENTIKIT_STASH_DIR")
|
|
18
|
+
console.error(" index Build search index with metadata generation")
|
|
19
|
+
console.error(" search [query] Search the stash (--type tool|skill|command|agent|any) (--limit N)")
|
|
20
|
+
console.error(" open <type:name> Open a stash asset by ref")
|
|
21
|
+
console.error(" run <type:name> Run a tool by ref")
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (command) {
|
|
26
|
+
case "init": {
|
|
27
|
+
const result = agentikitInit()
|
|
28
|
+
console.log(JSON.stringify(result, null, 2))
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
case "index": {
|
|
32
|
+
const result = agentikitIndex()
|
|
33
|
+
console.log(JSON.stringify(result, null, 2))
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
case "search": {
|
|
37
|
+
const query = args.find((a, i) => i > 0 && !a.startsWith("--") && args[i - 1] !== "--type" && args[i - 1] !== "--limit") ?? ""
|
|
38
|
+
const type = flag("--type") as "tool" | "skill" | "command" | "agent" | "any" | undefined
|
|
39
|
+
const limitStr = flag("--limit")
|
|
40
|
+
const limit = limitStr ? parseInt(limitStr, 10) : undefined
|
|
41
|
+
console.log(JSON.stringify(agentikitSearch({ query, type, limit }), null, 2))
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
case "open": {
|
|
45
|
+
const ref = args[1]
|
|
46
|
+
if (!ref) { console.error("Error: missing ref argument\n"); usage() }
|
|
47
|
+
console.log(JSON.stringify(agentikitOpen({ ref }), null, 2))
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
case "run": {
|
|
51
|
+
const ref = args[1]
|
|
52
|
+
if (!ref) { console.error("Error: missing ref argument\n"); usage() }
|
|
53
|
+
const result = agentikitRun({ ref })
|
|
54
|
+
console.log(JSON.stringify(result, null, 2))
|
|
55
|
+
process.exit(result.exitCode)
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
usage()
|
|
60
|
+
}
|
package/src/indexer.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { AgentikitAssetType } from "./stash"
|
|
4
|
+
import {
|
|
5
|
+
type StashFile,
|
|
6
|
+
type StashEntry,
|
|
7
|
+
loadStashFile,
|
|
8
|
+
writeStashFile,
|
|
9
|
+
generateMetadata,
|
|
10
|
+
} from "./metadata"
|
|
11
|
+
import { TfIdfAdapter, type ScoredEntry } from "./similarity"
|
|
12
|
+
|
|
13
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface IndexedEntry {
|
|
16
|
+
entry: StashEntry
|
|
17
|
+
path: string
|
|
18
|
+
dirPath: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SearchIndex {
|
|
22
|
+
version: number
|
|
23
|
+
builtAt: string
|
|
24
|
+
stashDir: string
|
|
25
|
+
entries: IndexedEntry[]
|
|
26
|
+
/** Serialized TF-IDF state (term frequencies, idf values) */
|
|
27
|
+
tfidf?: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IndexResponse {
|
|
31
|
+
stashDir: string
|
|
32
|
+
totalEntries: number
|
|
33
|
+
generatedMetadata: number
|
|
34
|
+
indexPath: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const INDEX_VERSION = 1
|
|
40
|
+
const SCRIPT_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"])
|
|
41
|
+
|
|
42
|
+
const TYPE_DIRS: Record<AgentikitAssetType, string> = {
|
|
43
|
+
tool: "tools",
|
|
44
|
+
skill: "skills",
|
|
45
|
+
command: "commands",
|
|
46
|
+
agent: "agents",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Index Path ──────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function getIndexPath(): string {
|
|
52
|
+
const cacheDir = process.env.XDG_CACHE_HOME
|
|
53
|
+
|| path.join(process.env.HOME || process.env.USERPROFILE || "", ".cache")
|
|
54
|
+
return path.join(cacheDir, "agentikit", "index.json")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadSearchIndex(): SearchIndex | null {
|
|
58
|
+
const indexPath = getIndexPath()
|
|
59
|
+
if (!fs.existsSync(indexPath)) return null
|
|
60
|
+
try {
|
|
61
|
+
const raw = JSON.parse(fs.readFileSync(indexPath, "utf8"))
|
|
62
|
+
if (raw?.version !== INDEX_VERSION) return null
|
|
63
|
+
return raw as SearchIndex
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Indexer ──────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function agentikitIndex(options?: { stashDir?: string }): IndexResponse {
|
|
72
|
+
const stashDir = options?.stashDir || resolveStashDirForIndex()
|
|
73
|
+
const allEntries: IndexedEntry[] = []
|
|
74
|
+
let generatedCount = 0
|
|
75
|
+
|
|
76
|
+
for (const assetType of Object.keys(TYPE_DIRS) as AgentikitAssetType[]) {
|
|
77
|
+
const typeRoot = path.join(stashDir, TYPE_DIRS[assetType])
|
|
78
|
+
if (!fs.existsSync(typeRoot) || !fs.statSync(typeRoot).isDirectory()) continue
|
|
79
|
+
|
|
80
|
+
// Group files by their immediate parent directory
|
|
81
|
+
const dirGroups = collectDirectoryGroups(typeRoot, assetType)
|
|
82
|
+
|
|
83
|
+
for (const [dirPath, files] of dirGroups) {
|
|
84
|
+
// Try loading existing .stash.json
|
|
85
|
+
let stash = loadStashFile(dirPath)
|
|
86
|
+
|
|
87
|
+
if (!stash) {
|
|
88
|
+
// Generate metadata
|
|
89
|
+
stash = generateMetadata(dirPath, assetType, files)
|
|
90
|
+
if (stash.entries.length > 0) {
|
|
91
|
+
writeStashFile(dirPath, stash)
|
|
92
|
+
generatedCount += stash.entries.length
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (stash) {
|
|
97
|
+
for (const entry of stash.entries) {
|
|
98
|
+
const entryPath = entry.entry
|
|
99
|
+
? path.join(dirPath, entry.entry)
|
|
100
|
+
: files[0] || dirPath
|
|
101
|
+
allEntries.push({ entry, path: entryPath, dirPath })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build TF-IDF index
|
|
108
|
+
const adapter = new TfIdfAdapter()
|
|
109
|
+
const scoredEntries: ScoredEntry[] = allEntries.map((ie) => ({
|
|
110
|
+
id: `${ie.entry.type}:${ie.entry.name}`,
|
|
111
|
+
text: buildSearchText(ie.entry),
|
|
112
|
+
entry: ie.entry,
|
|
113
|
+
path: ie.path,
|
|
114
|
+
}))
|
|
115
|
+
adapter.buildIndex(scoredEntries)
|
|
116
|
+
|
|
117
|
+
// Persist index
|
|
118
|
+
const indexPath = getIndexPath()
|
|
119
|
+
const indexDir = path.dirname(indexPath)
|
|
120
|
+
if (!fs.existsSync(indexDir)) {
|
|
121
|
+
fs.mkdirSync(indexDir, { recursive: true })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const index: SearchIndex = {
|
|
125
|
+
version: INDEX_VERSION,
|
|
126
|
+
builtAt: new Date().toISOString(),
|
|
127
|
+
stashDir,
|
|
128
|
+
entries: allEntries,
|
|
129
|
+
tfidf: adapter.serialize(),
|
|
130
|
+
}
|
|
131
|
+
fs.writeFileSync(indexPath, JSON.stringify(index) + "\n", "utf8")
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
stashDir,
|
|
135
|
+
totalEntries: allEntries.length,
|
|
136
|
+
generatedMetadata: generatedCount,
|
|
137
|
+
indexPath,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function collectDirectoryGroups(
|
|
144
|
+
typeRoot: string,
|
|
145
|
+
assetType: AgentikitAssetType,
|
|
146
|
+
): Map<string, string[]> {
|
|
147
|
+
const groups = new Map<string, string[]>()
|
|
148
|
+
|
|
149
|
+
const walk = (dir: string): void => {
|
|
150
|
+
if (!fs.existsSync(dir)) return
|
|
151
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (entry.name === ".stash.json") continue
|
|
154
|
+
const fullPath = path.join(dir, entry.name)
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
walk(fullPath)
|
|
157
|
+
} else if (entry.isFile() && isRelevantFile(entry.name, assetType)) {
|
|
158
|
+
const parentDir = path.dirname(fullPath)
|
|
159
|
+
const existing = groups.get(parentDir)
|
|
160
|
+
if (existing) {
|
|
161
|
+
existing.push(fullPath)
|
|
162
|
+
} else {
|
|
163
|
+
groups.set(parentDir, [fullPath])
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
walk(typeRoot)
|
|
170
|
+
return groups
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isRelevantFile(fileName: string, assetType: AgentikitAssetType): boolean {
|
|
174
|
+
const ext = path.extname(fileName).toLowerCase()
|
|
175
|
+
switch (assetType) {
|
|
176
|
+
case "tool":
|
|
177
|
+
return SCRIPT_EXTENSIONS.has(ext)
|
|
178
|
+
case "skill":
|
|
179
|
+
return fileName === "SKILL.md"
|
|
180
|
+
case "command":
|
|
181
|
+
case "agent":
|
|
182
|
+
return ext === ".md"
|
|
183
|
+
default:
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function buildSearchText(entry: StashEntry): string {
|
|
189
|
+
const parts: string[] = [entry.name.replace(/[-_]/g, " ")]
|
|
190
|
+
if (entry.description) parts.push(entry.description)
|
|
191
|
+
if (entry.tags) parts.push(entry.tags.join(" "))
|
|
192
|
+
if (entry.examples) parts.push(entry.examples.join(" "))
|
|
193
|
+
if (entry.intent) {
|
|
194
|
+
if (entry.intent.when) parts.push(entry.intent.when)
|
|
195
|
+
if (entry.intent.input) parts.push(entry.intent.input)
|
|
196
|
+
if (entry.intent.output) parts.push(entry.intent.output)
|
|
197
|
+
}
|
|
198
|
+
return parts.join(" ").toLowerCase()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveStashDirForIndex(): string {
|
|
202
|
+
const raw = process.env.AGENTIKIT_STASH_DIR?.trim()
|
|
203
|
+
if (!raw) {
|
|
204
|
+
throw new Error("AGENTIKIT_STASH_DIR is not set. Run 'agentikit init' first.")
|
|
205
|
+
}
|
|
206
|
+
const stashDir = path.resolve(raw)
|
|
207
|
+
if (!fs.existsSync(stashDir) || !fs.statSync(stashDir).isDirectory()) {
|
|
208
|
+
throw new Error(`AGENTIKIT_STASH_DIR does not exist or is not a directory: "${stashDir}"`)
|
|
209
|
+
}
|
|
210
|
+
return stashDir
|
|
211
|
+
}
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { AgentikitAssetType } from "./stash"
|
|
4
|
+
|
|
5
|
+
// ── Schema ──────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface StashIntent {
|
|
8
|
+
when?: string
|
|
9
|
+
input?: string
|
|
10
|
+
output?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StashEntry {
|
|
14
|
+
name: string
|
|
15
|
+
type: AgentikitAssetType
|
|
16
|
+
description?: string
|
|
17
|
+
tags?: string[]
|
|
18
|
+
examples?: string[]
|
|
19
|
+
intent?: StashIntent
|
|
20
|
+
entry?: string
|
|
21
|
+
generated?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StashFile {
|
|
25
|
+
entries: StashEntry[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Load / Write ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const STASH_FILENAME = ".stash.json"
|
|
31
|
+
|
|
32
|
+
export function stashFilePath(dirPath: string): string {
|
|
33
|
+
return path.join(dirPath, STASH_FILENAME)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadStashFile(dirPath: string): StashFile | null {
|
|
37
|
+
const filePath = stashFilePath(dirPath)
|
|
38
|
+
if (!fs.existsSync(filePath)) return null
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"))
|
|
41
|
+
if (!raw || !Array.isArray(raw.entries)) return null
|
|
42
|
+
const entries: StashEntry[] = []
|
|
43
|
+
for (const e of raw.entries) {
|
|
44
|
+
const validated = validateStashEntry(e)
|
|
45
|
+
if (validated) entries.push(validated)
|
|
46
|
+
}
|
|
47
|
+
return entries.length > 0 ? { entries } : null
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function writeStashFile(dirPath: string, stash: StashFile): void {
|
|
54
|
+
const filePath = stashFilePath(dirPath)
|
|
55
|
+
fs.writeFileSync(filePath, JSON.stringify(stash, null, 2) + "\n", "utf8")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateStashEntry(entry: unknown): StashEntry | null {
|
|
59
|
+
if (typeof entry !== "object" || entry === null) return null
|
|
60
|
+
const e = entry as Record<string, unknown>
|
|
61
|
+
if (typeof e.name !== "string" || !e.name) return null
|
|
62
|
+
if (typeof e.type !== "string" || !isValidType(e.type)) return null
|
|
63
|
+
|
|
64
|
+
const result: StashEntry = {
|
|
65
|
+
name: e.name,
|
|
66
|
+
type: e.type as AgentikitAssetType,
|
|
67
|
+
}
|
|
68
|
+
if (typeof e.description === "string" && e.description) result.description = e.description
|
|
69
|
+
if (Array.isArray(e.tags)) result.tags = e.tags.filter((t): t is string => typeof t === "string")
|
|
70
|
+
if (Array.isArray(e.examples)) result.examples = e.examples.filter((x): x is string => typeof x === "string")
|
|
71
|
+
if (typeof e.intent === "object" && e.intent !== null) {
|
|
72
|
+
const intent = e.intent as Record<string, unknown>
|
|
73
|
+
result.intent = {}
|
|
74
|
+
if (typeof intent.when === "string") result.intent.when = intent.when
|
|
75
|
+
if (typeof intent.input === "string") result.intent.input = intent.input
|
|
76
|
+
if (typeof intent.output === "string") result.intent.output = intent.output
|
|
77
|
+
}
|
|
78
|
+
if (typeof e.entry === "string" && e.entry) result.entry = e.entry
|
|
79
|
+
if (e.generated === true) result.generated = true
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isValidType(type: string): boolean {
|
|
85
|
+
return type === "tool" || type === "skill" || type === "command" || type === "agent"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const SCRIPT_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"])
|
|
91
|
+
|
|
92
|
+
export function generateMetadata(
|
|
93
|
+
dirPath: string,
|
|
94
|
+
assetType: AgentikitAssetType,
|
|
95
|
+
files: string[],
|
|
96
|
+
): StashFile {
|
|
97
|
+
const entries: StashEntry[] = []
|
|
98
|
+
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const ext = path.extname(file).toLowerCase()
|
|
101
|
+
const baseName = path.basename(file, ext)
|
|
102
|
+
|
|
103
|
+
// Skip non-relevant files
|
|
104
|
+
if (assetType === "tool" && !SCRIPT_EXTENSIONS.has(ext)) continue
|
|
105
|
+
if ((assetType === "command" || assetType === "agent") && ext !== ".md") continue
|
|
106
|
+
if (assetType === "skill" && path.basename(file) !== "SKILL.md") continue
|
|
107
|
+
|
|
108
|
+
const entry: StashEntry = {
|
|
109
|
+
name: baseName,
|
|
110
|
+
type: assetType,
|
|
111
|
+
generated: true,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Priority 3: package.json metadata
|
|
115
|
+
const pkgMeta = extractPackageMetadata(dirPath)
|
|
116
|
+
if (pkgMeta) {
|
|
117
|
+
if (pkgMeta.description && !entry.description) entry.description = pkgMeta.description
|
|
118
|
+
if (pkgMeta.keywords && pkgMeta.keywords.length > 0) entry.tags = pkgMeta.keywords
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Priority 2: Frontmatter (for .md files)
|
|
122
|
+
if (ext === ".md") {
|
|
123
|
+
const fm = extractFrontmatterDescription(file)
|
|
124
|
+
if (fm) entry.description = fm
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Priority 4: Code comments (for script files)
|
|
128
|
+
if (SCRIPT_EXTENSIONS.has(ext) && ext !== ".md") {
|
|
129
|
+
const commentDesc = extractDescriptionFromComments(file)
|
|
130
|
+
if (commentDesc && !entry.description) entry.description = commentDesc
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Priority 5: Filename heuristics (fallback)
|
|
134
|
+
if (!entry.description) {
|
|
135
|
+
entry.description = fileNameToDescription(baseName)
|
|
136
|
+
}
|
|
137
|
+
if (!entry.tags || entry.tags.length === 0) {
|
|
138
|
+
entry.tags = extractTagsFromPath(file, dirPath)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
entry.entry = path.basename(file)
|
|
142
|
+
entries.push(entry)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { entries }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function extractDescriptionFromComments(filePath: string): string | null {
|
|
149
|
+
let content: string
|
|
150
|
+
try {
|
|
151
|
+
content = fs.readFileSync(filePath, "utf8")
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const lines = content.split(/\r?\n/).slice(0, 50)
|
|
157
|
+
|
|
158
|
+
// Try JSDoc-style block comment: /** ... */
|
|
159
|
+
const blockStart = lines.findIndex((l) => /^\s*\/\*\*/.test(l))
|
|
160
|
+
if (blockStart >= 0) {
|
|
161
|
+
const desc: string[] = []
|
|
162
|
+
for (let i = blockStart; i < lines.length; i++) {
|
|
163
|
+
const line = lines[i]
|
|
164
|
+
if (i > blockStart && /\*\//.test(line)) break
|
|
165
|
+
const cleaned = line.replace(/^\s*\/?\*\*?\s?/, "").replace(/\*\/\s*$/, "").trim()
|
|
166
|
+
if (cleaned) desc.push(cleaned)
|
|
167
|
+
}
|
|
168
|
+
if (desc.length > 0) return desc.join(" ")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Try hash comments at start of file (skip shebang)
|
|
172
|
+
let start = 0
|
|
173
|
+
if (lines[0]?.startsWith("#!")) start = 1
|
|
174
|
+
const hashLines: string[] = []
|
|
175
|
+
for (let i = start; i < lines.length; i++) {
|
|
176
|
+
const line = lines[i].trim()
|
|
177
|
+
if (line.startsWith("#") && !line.startsWith("#!")) {
|
|
178
|
+
hashLines.push(line.replace(/^#+\s*/, "").trim())
|
|
179
|
+
} else if (line === "") {
|
|
180
|
+
continue
|
|
181
|
+
} else {
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (hashLines.length > 0) return hashLines.join(" ")
|
|
186
|
+
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function extractFrontmatterDescription(filePath: string): string | null {
|
|
191
|
+
let content: string
|
|
192
|
+
try {
|
|
193
|
+
content = fs.readFileSync(filePath, "utf8")
|
|
194
|
+
} catch {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
199
|
+
if (!match) return null
|
|
200
|
+
|
|
201
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
202
|
+
const m = line.match(/^description:\s*"?(.+?)"?\s*$/)
|
|
203
|
+
if (m) return m[1]
|
|
204
|
+
}
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function extractPackageMetadata(
|
|
209
|
+
dirPath: string,
|
|
210
|
+
): { name?: string; description?: string; keywords?: string[] } | null {
|
|
211
|
+
const pkgPath = path.join(dirPath, "package.json")
|
|
212
|
+
if (!fs.existsSync(pkgPath)) return null
|
|
213
|
+
try {
|
|
214
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
215
|
+
const result: { name?: string; description?: string; keywords?: string[] } = {}
|
|
216
|
+
if (typeof pkg.name === "string") result.name = pkg.name
|
|
217
|
+
if (typeof pkg.description === "string") result.description = pkg.description
|
|
218
|
+
if (Array.isArray(pkg.keywords)) {
|
|
219
|
+
result.keywords = pkg.keywords.filter((k: unknown): k is string => typeof k === "string")
|
|
220
|
+
}
|
|
221
|
+
return Object.keys(result).length > 0 ? result : null
|
|
222
|
+
} catch {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function fileNameToDescription(fileName: string): string {
|
|
228
|
+
return fileName
|
|
229
|
+
.replace(/[-_]+/g, " ")
|
|
230
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
231
|
+
.toLowerCase()
|
|
232
|
+
.trim()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function extractTagsFromPath(filePath: string, rootDir: string): string[] {
|
|
236
|
+
const rel = path.relative(rootDir, filePath)
|
|
237
|
+
const parts = rel.split(path.sep)
|
|
238
|
+
const tags = new Set<string>()
|
|
239
|
+
|
|
240
|
+
for (const part of parts) {
|
|
241
|
+
const name = part.replace(path.extname(part), "")
|
|
242
|
+
for (const token of name.split(/[-_./\\]+/)) {
|
|
243
|
+
const clean = token.toLowerCase().trim()
|
|
244
|
+
if (clean && clean.length > 1) tags.add(clean)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return Array.from(tags)
|
|
249
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { agentikitOpen, agentikitRun, agentikitSearch } from "./stash"
|
|
3
|
+
import { agentikitIndex } from "./indexer"
|
|
4
|
+
|
|
5
|
+
function tryJson(fn: () => unknown, action: string): string {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(fn())
|
|
8
|
+
} catch (error: unknown) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
10
|
+
return JSON.stringify({ ok: false, error: `Failed to ${action}: ${message}` })
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const plugin: Plugin = async () => ({
|
|
15
|
+
tool: {
|
|
16
|
+
agentikit_search: tool({
|
|
17
|
+
description: "Search the Agentikit stash for tools, skills, commands, and agents.",
|
|
18
|
+
args: {
|
|
19
|
+
query: tool.schema.string().describe("Case-insensitive substring search."),
|
|
20
|
+
type: tool.schema
|
|
21
|
+
.enum(["tool", "skill", "command", "agent", "any"])
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Optional type filter. Defaults to 'any'."),
|
|
24
|
+
limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
|
|
25
|
+
},
|
|
26
|
+
async execute({ query, type, limit }) {
|
|
27
|
+
return tryJson(() => agentikitSearch({ query, type, limit }), "search Agentikit stash")
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
agentikit_open: tool({
|
|
31
|
+
description: "Open a stash asset from an openRef returned by agentikit_search.",
|
|
32
|
+
args: {
|
|
33
|
+
ref: tool.schema.string().describe("Open reference returned by agentikit_search."),
|
|
34
|
+
},
|
|
35
|
+
async execute({ ref }) {
|
|
36
|
+
return tryJson(() => agentikitOpen({ ref }), "open stash asset")
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
agentikit_run: tool({
|
|
40
|
+
description: "Run a tool from the Agentikit stash by its openRef. Only tool refs are supported.",
|
|
41
|
+
args: {
|
|
42
|
+
ref: tool.schema.string().describe("Open reference of a tool returned by agentikit_search."),
|
|
43
|
+
},
|
|
44
|
+
async execute({ ref }) {
|
|
45
|
+
return tryJson(() => agentikitRun({ ref }), "run stash tool")
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
agentikit_index: tool({
|
|
49
|
+
description: "Build or rebuild the Agentikit search index. Scans stash directories, generates missing .stash.json metadata, and builds a semantic search index.",
|
|
50
|
+
args: {},
|
|
51
|
+
async execute() {
|
|
52
|
+
return tryJson(() => agentikitIndex(), "build Agentikit index")
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
})
|