agentikit 0.0.9 → 0.0.12
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/README.md +129 -214
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/src/asset-spec.d.ts +2 -0
- package/dist/src/asset-spec.js +22 -3
- package/dist/src/asset-type-handler.d.ts +27 -0
- package/dist/src/asset-type-handler.js +33 -0
- package/dist/src/cli.js +201 -75
- package/dist/src/common.d.ts +6 -1
- package/dist/src/common.js +18 -4
- package/dist/src/config-cli.d.ts +9 -0
- package/dist/src/config-cli.js +473 -0
- package/dist/src/config.d.ts +19 -6
- package/dist/src/config.js +139 -29
- package/dist/src/db.d.ts +46 -0
- package/dist/src/db.js +299 -0
- package/dist/src/embedder.js +12 -7
- package/dist/src/github.d.ts +4 -0
- package/dist/src/github.js +19 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +26 -0
- package/dist/src/handlers/command-handler.d.ts +2 -0
- package/dist/src/handlers/command-handler.js +23 -0
- package/dist/src/handlers/index.d.ts +6 -0
- package/dist/src/handlers/index.js +23 -0
- package/dist/src/handlers/knowledge-handler.d.ts +2 -0
- package/dist/src/handlers/knowledge-handler.js +56 -0
- package/dist/src/handlers/markdown-helpers.d.ts +7 -0
- package/dist/src/handlers/markdown-helpers.js +15 -0
- package/dist/src/handlers/script-handler.d.ts +2 -0
- package/dist/src/handlers/script-handler.js +78 -0
- package/dist/src/handlers/skill-handler.d.ts +2 -0
- package/dist/src/handlers/skill-handler.js +30 -0
- package/dist/src/handlers/tool-handler.d.ts +2 -0
- package/dist/src/handlers/tool-handler.js +58 -0
- package/dist/src/indexer.d.ts +1 -23
- package/dist/src/indexer.js +162 -155
- package/dist/src/init.d.ts +2 -2
- package/dist/src/init.js +21 -9
- package/dist/src/llm.js +4 -3
- package/dist/src/metadata.d.ts +0 -1
- package/dist/src/metadata.js +6 -64
- package/dist/src/origin-resolve.d.ts +19 -0
- package/dist/src/origin-resolve.js +53 -0
- package/dist/src/registry-install.d.ts +2 -2
- package/dist/src/registry-install.js +142 -35
- package/dist/src/registry-resolve.js +90 -22
- package/dist/src/registry-search.d.ts +22 -0
- package/dist/src/registry-search.js +231 -97
- package/dist/src/registry-types.d.ts +9 -2
- package/dist/src/stash-add.js +4 -4
- package/dist/src/stash-clone.d.ts +22 -0
- package/dist/src/stash-clone.js +83 -0
- package/dist/src/stash-ref.d.ts +27 -3
- package/dist/src/stash-ref.js +63 -24
- package/dist/src/stash-registry.js +12 -12
- package/dist/src/stash-resolve.js +3 -0
- package/dist/src/stash-search.js +168 -164
- package/dist/src/stash-show.d.ts +1 -1
- package/dist/src/stash-show.js +28 -96
- package/dist/src/stash-source.d.ts +24 -0
- package/dist/src/stash-source.js +81 -0
- package/dist/src/stash-types.d.ts +14 -4
- package/dist/src/stash.d.ts +6 -0
- package/dist/src/stash.js +3 -0
- package/dist/src/tool-runner.d.ts +1 -1
- package/dist/src/tool-runner.js +18 -5
- package/package.json +7 -2
- package/src/asset-spec.ts +20 -4
- package/src/asset-type-handler.ts +77 -0
- package/src/cli.ts +213 -82
- package/src/common.ts +23 -5
- package/src/config-cli.ts +499 -0
- package/src/config.ts +160 -38
- package/src/db.ts +411 -0
- package/src/embedder.ts +22 -11
- package/src/github.ts +21 -0
- package/src/handlers/agent-handler.ts +32 -0
- package/src/handlers/command-handler.ts +29 -0
- package/src/handlers/index.ts +25 -0
- package/src/handlers/knowledge-handler.ts +62 -0
- package/src/handlers/markdown-helpers.ts +19 -0
- package/src/handlers/script-handler.ts +92 -0
- package/src/handlers/skill-handler.ts +37 -0
- package/src/handlers/tool-handler.ts +71 -0
- package/src/indexer.ts +208 -187
- package/src/init.ts +17 -9
- package/src/llm.ts +4 -3
- package/src/metadata.ts +5 -65
- package/src/origin-resolve.ts +67 -0
- package/src/registry-install.ts +158 -42
- package/src/registry-resolve.ts +92 -23
- package/src/registry-search.ts +288 -98
- package/src/registry-types.ts +10 -2
- package/src/stash-add.ts +14 -17
- package/src/stash-clone.ts +127 -0
- package/src/stash-ref.ts +84 -26
- package/src/stash-registry.ts +12 -12
- package/src/stash-resolve.ts +3 -0
- package/src/stash-search.ts +202 -184
- package/src/stash-show.ts +33 -90
- package/src/stash-source.ts +103 -0
- package/src/stash-types.ts +14 -4
- package/src/stash.ts +8 -0
- package/src/tool-runner.ts +18 -5
- package/dist/src/similarity.d.ts +0 -34
- package/src/similarity.ts +0 -271
package/src/stash-show.ts
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { resolveStashDir } from "./common"
|
|
5
|
-
import { parseOpenRef } from "./stash-ref"
|
|
2
|
+
import { parseAssetRef } from "./stash-ref"
|
|
3
|
+
import { resolveSourcesForOrigin } from "./origin-resolve"
|
|
6
4
|
import { resolveAssetPath } from "./stash-resolve"
|
|
7
5
|
import type { KnowledgeView, ShowResponse } from "./stash-types"
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { loadConfig } from "./config"
|
|
6
|
+
import { getHandler } from "./asset-type-handler"
|
|
7
|
+
import { resolveStashSources, findSourceForPath } from "./stash-source"
|
|
11
8
|
|
|
12
|
-
export function agentikitShow(input: { ref: string; view?: KnowledgeView }): ShowResponse {
|
|
13
|
-
const parsed =
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...config.additionalStashDirs.filter((d) => {
|
|
19
|
-
try { return fs.statSync(d).isDirectory() } catch { return false }
|
|
20
|
-
}),
|
|
21
|
-
]
|
|
9
|
+
export async function agentikitShow(input: { ref: string; view?: KnowledgeView }): Promise<ShowResponse> {
|
|
10
|
+
const parsed = parseAssetRef(input.ref)
|
|
11
|
+
const allSources = resolveStashSources()
|
|
12
|
+
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources)
|
|
13
|
+
|
|
14
|
+
const allStashDirs = searchSources.map((s) => s.path)
|
|
22
15
|
|
|
23
16
|
let assetPath: string | undefined
|
|
24
17
|
let lastError: Error | undefined
|
|
@@ -30,83 +23,33 @@ export function agentikitShow(input: { ref: string; view?: KnowledgeView }): Sho
|
|
|
30
23
|
lastError = err instanceof Error ? err : new Error(String(err))
|
|
31
24
|
}
|
|
32
25
|
}
|
|
26
|
+
|
|
27
|
+
if (!assetPath && parsed.origin && searchSources.length === 0) {
|
|
28
|
+
const installCmd = `akm add ${parsed.origin}`
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Stash asset not found for ref: ${parsed.type}:${parsed.name}. ` +
|
|
31
|
+
`Kit "${parsed.origin}" is not installed. Run: ${installCmd}`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
33
35
|
if (!assetPath) {
|
|
34
36
|
throw lastError ?? new Error(`Stash asset not found for ref: ${parsed.type}:${parsed.name}`)
|
|
35
37
|
}
|
|
36
38
|
const content = fs.readFileSync(assetPath, "utf8")
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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: "Dispatching prompt must include the agent's full prompt content verbatim; summaries are non-compliant. \n\n"
|
|
64
|
-
+ parsedMd.content,
|
|
65
|
-
toolPolicy: parsedMd.data.tools,
|
|
66
|
-
modelHint: parsedMd.data.model,
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
case "tool": {
|
|
70
|
-
const assetStashDir = allStashDirs.find((d) => path.resolve(assetPath!).startsWith(path.resolve(d) + path.sep)) ?? stashDir
|
|
71
|
-
const toolInfo = buildToolInfo(assetStashDir, assetPath)
|
|
72
|
-
return {
|
|
73
|
-
type: "tool",
|
|
74
|
-
name: parsed.name,
|
|
75
|
-
path: assetPath,
|
|
76
|
-
runCmd: toolInfo.runCmd,
|
|
77
|
-
kind: toolInfo.kind,
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
case "knowledge": {
|
|
81
|
-
const v = input.view ?? { mode: "full" }
|
|
82
|
-
switch (v.mode) {
|
|
83
|
-
case "toc": {
|
|
84
|
-
const toc = parseMarkdownToc(content)
|
|
85
|
-
return { type: "knowledge", name: parsed.name, path: assetPath, content: formatToc(toc) }
|
|
86
|
-
}
|
|
87
|
-
case "frontmatter": {
|
|
88
|
-
const fm = extractFrontmatterOnly(content)
|
|
89
|
-
return { type: "knowledge", name: parsed.name, path: assetPath, content: fm ?? "(no frontmatter)" }
|
|
90
|
-
}
|
|
91
|
-
case "section": {
|
|
92
|
-
const section = extractSection(content, v.heading)
|
|
93
|
-
if (!section) {
|
|
94
|
-
return {
|
|
95
|
-
type: "knowledge",
|
|
96
|
-
name: parsed.name,
|
|
97
|
-
path: assetPath,
|
|
98
|
-
content: `Section "${v.heading}" not found in ${parsed.name}. Try --view toc to discover available headings.`,
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return { type: "knowledge", name: parsed.name, path: assetPath, content: section.content }
|
|
102
|
-
}
|
|
103
|
-
case "lines": {
|
|
104
|
-
return { type: "knowledge", name: parsed.name, path: assetPath, content: extractLineRange(content, v.start, v.end) }
|
|
105
|
-
}
|
|
106
|
-
default: {
|
|
107
|
-
return { type: "knowledge", name: parsed.name, path: assetPath, content }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
40
|
+
const source = findSourceForPath(assetPath, allSources)
|
|
41
|
+
const handler = getHandler(parsed.type)
|
|
42
|
+
const response = handler.buildShowResponse({
|
|
43
|
+
name: parsed.name,
|
|
44
|
+
path: assetPath,
|
|
45
|
+
content,
|
|
46
|
+
view: input.view,
|
|
47
|
+
stashDirs: allStashDirs,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...response,
|
|
52
|
+
registryId: source?.registryId,
|
|
53
|
+
editable: source?.writable ?? false,
|
|
111
54
|
}
|
|
112
55
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { resolveStashDir } from "./common"
|
|
4
|
+
import { loadConfig } from "./config"
|
|
5
|
+
|
|
6
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export type StashSourceKind = "working" | "mounted" | "installed"
|
|
9
|
+
|
|
10
|
+
export interface StashSource {
|
|
11
|
+
kind: StashSourceKind
|
|
12
|
+
path: string
|
|
13
|
+
/** For installed sources, the registry entry id */
|
|
14
|
+
registryId?: string
|
|
15
|
+
/** Whether this source is writable (only working stash) */
|
|
16
|
+
writable: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Resolution ──────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the ordered list of stash sources:
|
|
23
|
+
* 1. Working stash (writable)
|
|
24
|
+
* 2. Mounted stash dirs (read-only, user-configured)
|
|
25
|
+
* 3. Installed stash dirs (read-only, derived from registry.installed)
|
|
26
|
+
*/
|
|
27
|
+
export function resolveStashSources(overrideStashDir?: string): StashSource[] {
|
|
28
|
+
const stashDir = overrideStashDir ?? resolveStashDir()
|
|
29
|
+
const config = loadConfig()
|
|
30
|
+
|
|
31
|
+
const sources: StashSource[] = [
|
|
32
|
+
{ kind: "working", path: stashDir, writable: true },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for (const dir of config.mountedStashDirs) {
|
|
36
|
+
if (isSuspiciousStashRoot(dir)) {
|
|
37
|
+
console.warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`)
|
|
38
|
+
}
|
|
39
|
+
if (isValidDirectory(dir)) {
|
|
40
|
+
sources.push({ kind: "mounted", path: dir, writable: false })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const entry of config.registry?.installed ?? []) {
|
|
45
|
+
if (isSuspiciousStashRoot(entry.stashRoot)) {
|
|
46
|
+
console.warn(`Warning: stash root "${entry.stashRoot}" appears to be a system directory. This may be unintentional.`)
|
|
47
|
+
}
|
|
48
|
+
if (isValidDirectory(entry.stashRoot)) {
|
|
49
|
+
sources.push({
|
|
50
|
+
kind: "installed",
|
|
51
|
+
path: entry.stashRoot,
|
|
52
|
+
registryId: entry.id,
|
|
53
|
+
writable: false,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return sources
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convenience: returns just the directory paths, preserving priority order.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveAllStashDirs(overrideStashDir?: string): string[] {
|
|
65
|
+
return resolveStashSources(overrideStashDir).map((s) => s.path)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find which source a file path belongs to.
|
|
70
|
+
*/
|
|
71
|
+
export function findSourceForPath(filePath: string, sources: StashSource[]): StashSource | undefined {
|
|
72
|
+
const resolved = path.resolve(filePath)
|
|
73
|
+
for (const source of sources) {
|
|
74
|
+
if (resolved.startsWith(path.resolve(source.path) + path.sep)) return source
|
|
75
|
+
}
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const SUSPICIOUS_ROOTS = new Set(['/', '/etc', '/bin', '/sbin', '/usr', '/var', '/tmp', '/dev', '/proc', '/sys'])
|
|
82
|
+
|
|
83
|
+
function isSuspiciousStashRoot(dir: string): boolean {
|
|
84
|
+
const resolved = path.resolve(dir)
|
|
85
|
+
const normalized = process.platform === 'win32' ? resolved.toLowerCase() : resolved
|
|
86
|
+
if (SUSPICIOUS_ROOTS.has(normalized)) return true
|
|
87
|
+
if (process.platform === 'win32') {
|
|
88
|
+
// Check for Windows system directories
|
|
89
|
+
const winDir = (process.env.SystemRoot || 'C:\\Windows').toLowerCase()
|
|
90
|
+
if (normalized === winDir || normalized.startsWith(winDir + path.sep)) return true
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function isValidDirectory(dir: string): boolean {
|
|
98
|
+
try {
|
|
99
|
+
return fs.statSync(dir).isDirectory()
|
|
100
|
+
} catch {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/stash-types.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface LocalSearchHit {
|
|
|
12
12
|
name: string
|
|
13
13
|
path: string
|
|
14
14
|
openRef: string
|
|
15
|
+
/** For installed sources, the registry id */
|
|
16
|
+
registryId?: string
|
|
17
|
+
/** Whether this asset is editable (only true for working stash) */
|
|
18
|
+
editable?: boolean
|
|
15
19
|
description?: string
|
|
16
20
|
tags?: string[]
|
|
17
21
|
score?: number
|
|
@@ -41,6 +45,8 @@ export interface RegistrySearchResultHit {
|
|
|
41
45
|
metadata?: Record<string, string>
|
|
42
46
|
installRef: string
|
|
43
47
|
installCmd: string
|
|
48
|
+
/** Whether this entry was manually reviewed and approved */
|
|
49
|
+
curated?: boolean
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export type SearchHit = LocalSearchHit | RegistrySearchResultHit
|
|
@@ -72,7 +78,7 @@ export interface AddResponse {
|
|
|
72
78
|
installedAt: string
|
|
73
79
|
}
|
|
74
80
|
config: {
|
|
75
|
-
|
|
81
|
+
mountedStashDirs: string[]
|
|
76
82
|
installedRegistryCount: number
|
|
77
83
|
}
|
|
78
84
|
index: {
|
|
@@ -129,7 +135,7 @@ export interface RemoveResponse {
|
|
|
129
135
|
stashRoot: string
|
|
130
136
|
}
|
|
131
137
|
config: {
|
|
132
|
-
|
|
138
|
+
mountedStashDirs: string[]
|
|
133
139
|
installedRegistryCount: number
|
|
134
140
|
}
|
|
135
141
|
index: {
|
|
@@ -154,7 +160,7 @@ export interface ReinstallResponse {
|
|
|
154
160
|
all: boolean
|
|
155
161
|
processed: ReinstallResultItem[]
|
|
156
162
|
config: {
|
|
157
|
-
|
|
163
|
+
mountedStashDirs: string[]
|
|
158
164
|
installedRegistryCount: number
|
|
159
165
|
}
|
|
160
166
|
index: {
|
|
@@ -188,7 +194,7 @@ export interface UpdateResponse {
|
|
|
188
194
|
all: boolean
|
|
189
195
|
processed: UpdateResultItem[]
|
|
190
196
|
config: {
|
|
191
|
-
|
|
197
|
+
mountedStashDirs: string[]
|
|
192
198
|
installedRegistryCount: number
|
|
193
199
|
}
|
|
194
200
|
index: {
|
|
@@ -211,6 +217,10 @@ export interface ShowResponse {
|
|
|
211
217
|
modelHint?: unknown
|
|
212
218
|
runCmd?: string
|
|
213
219
|
kind?: ToolKind
|
|
220
|
+
/** For installed sources, the registry id */
|
|
221
|
+
registryId?: string
|
|
222
|
+
/** Whether this asset is editable (only true for working stash) */
|
|
223
|
+
editable?: boolean
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
export type KnowledgeView =
|
package/src/stash.ts
CHANGED
|
@@ -3,11 +3,17 @@ export { resolveStashDir } from "./common"
|
|
|
3
3
|
export { agentikitInit } from "./init"
|
|
4
4
|
export type { InitResponse } from "./init"
|
|
5
5
|
export type { ToolKind } from "./tool-runner"
|
|
6
|
+
export type { AssetTypeHandler, ShowInput } from "./asset-type-handler"
|
|
7
|
+
export { registerAssetType, getHandler, getAllHandlers, getRegisteredTypeNames } from "./asset-type-handler"
|
|
6
8
|
|
|
7
9
|
export { agentikitSearch } from "./stash-search"
|
|
8
10
|
export { agentikitShow } from "./stash-show"
|
|
9
11
|
export { agentikitAdd } from "./stash-add"
|
|
12
|
+
export { agentikitClone } from "./stash-clone"
|
|
13
|
+
|
|
10
14
|
export { agentikitList, agentikitRemove, agentikitReinstall, agentikitUpdate } from "./stash-registry"
|
|
15
|
+
export { resolveStashSources, resolveAllStashDirs, findSourceForPath } from "./stash-source"
|
|
16
|
+
export type { StashSource, StashSourceKind } from "./stash-source"
|
|
11
17
|
|
|
12
18
|
export type {
|
|
13
19
|
AddResponse,
|
|
@@ -29,3 +35,5 @@ export type {
|
|
|
29
35
|
ReinstallResultItem,
|
|
30
36
|
UpdateResultItem,
|
|
31
37
|
} from "./stash-types"
|
|
38
|
+
|
|
39
|
+
export type { CloneOptions, CloneResponse } from "./stash-clone"
|
package/src/tool-runner.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface ToolInfo {
|
|
|
34
34
|
*
|
|
35
35
|
* For `.ts` / `.js` files, looks up the nearest `package.json` so that
|
|
36
36
|
* `bun install` can be run in the correct working directory when the
|
|
37
|
-
* `
|
|
37
|
+
* `AKM_BUN_INSTALL` env flag is set.
|
|
38
38
|
*/
|
|
39
39
|
export function buildToolInfo(stashDir: string, filePath: string): ToolInfo {
|
|
40
40
|
const ext = path.extname(filePath).toLowerCase()
|
|
@@ -67,8 +67,17 @@ export function buildToolInfo(stashDir: string, filePath: string): ToolInfo {
|
|
|
67
67
|
throw new Error(`Unsupported tool extension: ${ext}`)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
const
|
|
70
|
+
// Determine the type root by checking which subdirectory contains the file
|
|
71
|
+
const resolvedFile = path.resolve(filePath)
|
|
72
|
+
let typeRoot = path.resolve(path.join(stashDir, "tools"))
|
|
73
|
+
for (const subdir of ["tools", "scripts"]) {
|
|
74
|
+
const candidate = path.resolve(path.join(stashDir, subdir))
|
|
75
|
+
if (resolvedFile.startsWith(candidate + path.sep)) {
|
|
76
|
+
typeRoot = candidate
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const pkgDir = findNearestPackageDir(path.dirname(filePath), typeRoot)
|
|
72
81
|
if (!pkgDir) {
|
|
73
82
|
return {
|
|
74
83
|
runCmd: `bun ${shellQuote(filePath)}`,
|
|
@@ -76,7 +85,7 @@ export function buildToolInfo(stashDir: string, filePath: string): ToolInfo {
|
|
|
76
85
|
execute: { command: "bun", args: [filePath] },
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
|
-
const installFlag = process.env.
|
|
88
|
+
const installFlag = process.env.AKM_BUN_INSTALL
|
|
80
89
|
const shouldInstall = installFlag === "1" || installFlag === "true" || installFlag === "yes"
|
|
81
90
|
|
|
82
91
|
const quotedPkgDir = shellQuote(pkgDir)
|
|
@@ -101,7 +110,11 @@ export function shellQuote(input: string): string {
|
|
|
101
110
|
throw new Error("Unsupported control characters in stash path.")
|
|
102
111
|
}
|
|
103
112
|
if (IS_WINDOWS) {
|
|
104
|
-
|
|
113
|
+
const escaped = input
|
|
114
|
+
.replace(/%/g, "%%")
|
|
115
|
+
.replace(/([\\^|&<>])/g, "^$1")
|
|
116
|
+
.replace(/"/g, '""')
|
|
117
|
+
return `"${escaped}"`
|
|
105
118
|
}
|
|
106
119
|
const escaped = input
|
|
107
120
|
.replace(/\\/g, "\\\\")
|
package/dist/src/similarity.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { StashEntry } from "./metadata";
|
|
2
|
-
export interface ScoredEntry {
|
|
3
|
-
id: string;
|
|
4
|
-
text: string;
|
|
5
|
-
entry: StashEntry;
|
|
6
|
-
path: string;
|
|
7
|
-
}
|
|
8
|
-
export interface ScoredResult {
|
|
9
|
-
entry: StashEntry;
|
|
10
|
-
path: string;
|
|
11
|
-
score: number;
|
|
12
|
-
}
|
|
13
|
-
export interface SearchAdapter {
|
|
14
|
-
buildIndex(entries: ScoredEntry[]): void;
|
|
15
|
-
search(query: string, limit: number, typeFilter?: string): ScoredResult[];
|
|
16
|
-
}
|
|
17
|
-
export interface SerializedTfIdf {
|
|
18
|
-
idf: Record<string, number>;
|
|
19
|
-
docs: Array<{
|
|
20
|
-
id: string;
|
|
21
|
-
termFreqs: Record<string, number>;
|
|
22
|
-
magnitude: number;
|
|
23
|
-
}>;
|
|
24
|
-
}
|
|
25
|
-
export declare class TfIdfAdapter implements SearchAdapter {
|
|
26
|
-
private documents;
|
|
27
|
-
private idf;
|
|
28
|
-
private entries;
|
|
29
|
-
buildIndex(entries: ScoredEntry[]): void;
|
|
30
|
-
search(query: string, limit: number, typeFilter?: string): ScoredResult[];
|
|
31
|
-
serialize(): SerializedTfIdf;
|
|
32
|
-
static deserialize(data: SerializedTfIdf, entries: ScoredEntry[]): TfIdfAdapter;
|
|
33
|
-
private substringFallback;
|
|
34
|
-
}
|
package/src/similarity.ts
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import type { StashEntry } from "./metadata"
|
|
2
|
-
|
|
3
|
-
// ── Adapter Interface ───────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export interface ScoredEntry {
|
|
6
|
-
id: string
|
|
7
|
-
text: string
|
|
8
|
-
entry: StashEntry
|
|
9
|
-
path: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ScoredResult {
|
|
13
|
-
entry: StashEntry
|
|
14
|
-
path: string
|
|
15
|
-
score: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface SearchAdapter {
|
|
19
|
-
buildIndex(entries: ScoredEntry[]): void
|
|
20
|
-
search(query: string, limit: number, typeFilter?: string): ScoredResult[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ── TF-IDF Implementation ───────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
interface TfIdfDocument {
|
|
26
|
-
entry: ScoredEntry
|
|
27
|
-
termFreqs: Map<string, number>
|
|
28
|
-
magnitude: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface SerializedTfIdf {
|
|
32
|
-
idf: Record<string, number>
|
|
33
|
-
docs: Array<{
|
|
34
|
-
id: string
|
|
35
|
-
termFreqs: Record<string, number>
|
|
36
|
-
magnitude: number
|
|
37
|
-
}>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class TfIdfAdapter implements SearchAdapter {
|
|
41
|
-
private documents: TfIdfDocument[] = []
|
|
42
|
-
private idf: Map<string, number> = new Map()
|
|
43
|
-
private entries: ScoredEntry[] = []
|
|
44
|
-
|
|
45
|
-
buildIndex(entries: ScoredEntry[]): void {
|
|
46
|
-
this.entries = entries
|
|
47
|
-
const docCount = entries.length
|
|
48
|
-
if (docCount === 0) return
|
|
49
|
-
|
|
50
|
-
// Compute term frequencies per document
|
|
51
|
-
const docFreqs = new Map<string, number>()
|
|
52
|
-
this.documents = entries.map((entry) => {
|
|
53
|
-
const tokens = tokenize(entry.text)
|
|
54
|
-
const termFreqs = new Map<string, number>()
|
|
55
|
-
|
|
56
|
-
for (const token of tokens) {
|
|
57
|
-
termFreqs.set(token, (termFreqs.get(token) || 0) + 1)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Track document frequency for IDF
|
|
61
|
-
for (const term of termFreqs.keys()) {
|
|
62
|
-
docFreqs.set(term, (docFreqs.get(term) || 0) + 1)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return { entry, termFreqs, magnitude: 0 }
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// Compute IDF: log(N / df)
|
|
69
|
-
this.idf = new Map()
|
|
70
|
-
for (const [term, df] of docFreqs) {
|
|
71
|
-
this.idf.set(term, Math.log(docCount / df))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Compute document magnitudes for cosine similarity
|
|
75
|
-
for (const doc of this.documents) {
|
|
76
|
-
let sumSq = 0
|
|
77
|
-
for (const [term, tf] of doc.termFreqs) {
|
|
78
|
-
const idf = this.idf.get(term) || 0
|
|
79
|
-
const tfidf = tf * idf
|
|
80
|
-
sumSq += tfidf * tfidf
|
|
81
|
-
}
|
|
82
|
-
doc.magnitude = Math.sqrt(sumSq)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
search(query: string, limit: number, typeFilter?: string): ScoredResult[] {
|
|
87
|
-
if (this.documents.length === 0) return []
|
|
88
|
-
|
|
89
|
-
const queryTokens = tokenize(query.toLowerCase())
|
|
90
|
-
if (queryTokens.length === 0) {
|
|
91
|
-
// Empty query: return all, sorted by type
|
|
92
|
-
return this.documents
|
|
93
|
-
.filter((d) => !typeFilter || typeFilter === "any" || d.entry.entry.type === typeFilter)
|
|
94
|
-
.slice(0, limit)
|
|
95
|
-
.map((d) => ({
|
|
96
|
-
entry: d.entry.entry,
|
|
97
|
-
path: d.entry.path,
|
|
98
|
-
score: 1,
|
|
99
|
-
}))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Build query TF-IDF vector
|
|
103
|
-
const queryTermFreqs = new Map<string, number>()
|
|
104
|
-
for (const token of queryTokens) {
|
|
105
|
-
queryTermFreqs.set(token, (queryTermFreqs.get(token) || 0) + 1)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let queryMagnitude = 0
|
|
109
|
-
const queryVector = new Map<string, number>()
|
|
110
|
-
for (const [term, tf] of queryTermFreqs) {
|
|
111
|
-
const idf = this.idf.get(term) || 0
|
|
112
|
-
const tfidf = tf * idf
|
|
113
|
-
queryVector.set(term, tfidf)
|
|
114
|
-
queryMagnitude += tfidf * tfidf
|
|
115
|
-
}
|
|
116
|
-
queryMagnitude = Math.sqrt(queryMagnitude)
|
|
117
|
-
|
|
118
|
-
if (queryMagnitude === 0) {
|
|
119
|
-
// All query terms are unknown — fallback to substring match
|
|
120
|
-
return this.substringFallback(query, limit, typeFilter)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const results: ScoredResult[] = []
|
|
124
|
-
const querySet = new Set(queryTokens)
|
|
125
|
-
|
|
126
|
-
for (const doc of this.documents) {
|
|
127
|
-
if (typeFilter && typeFilter !== "any" && doc.entry.entry.type !== typeFilter) continue
|
|
128
|
-
|
|
129
|
-
// Cosine similarity
|
|
130
|
-
let dotProduct = 0
|
|
131
|
-
for (const [term, queryTfidf] of queryVector) {
|
|
132
|
-
const docTf = doc.termFreqs.get(term) || 0
|
|
133
|
-
if (docTf === 0) continue
|
|
134
|
-
const docIdf = this.idf.get(term) || 0
|
|
135
|
-
dotProduct += queryTfidf * (docTf * docIdf)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let score = doc.magnitude > 0 && queryMagnitude > 0
|
|
139
|
-
? dotProduct / (doc.magnitude * queryMagnitude)
|
|
140
|
-
: 0
|
|
141
|
-
|
|
142
|
-
// Boost: tag exact match
|
|
143
|
-
const tags = doc.entry.entry.tags || []
|
|
144
|
-
for (const tag of tags) {
|
|
145
|
-
if (querySet.has(tag.toLowerCase())) {
|
|
146
|
-
score += 0.15
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Boost: intent phrase contains query token
|
|
151
|
-
const intents = doc.entry.entry.intents || []
|
|
152
|
-
for (const intent of intents) {
|
|
153
|
-
const intentLower = intent.toLowerCase()
|
|
154
|
-
for (const token of queryTokens) {
|
|
155
|
-
if (intentLower.includes(token)) {
|
|
156
|
-
score += 0.12
|
|
157
|
-
break // one boost per intent phrase
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Boost: name contains query token
|
|
163
|
-
const nameLower = doc.entry.entry.name.toLowerCase().replace(/[-_]/g, " ")
|
|
164
|
-
for (const token of queryTokens) {
|
|
165
|
-
if (nameLower.includes(token)) {
|
|
166
|
-
score += 0.1
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (score > 0) {
|
|
172
|
-
results.push({
|
|
173
|
-
entry: doc.entry.entry,
|
|
174
|
-
path: doc.entry.path,
|
|
175
|
-
score: Math.round(score * 1000) / 1000,
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
results.sort((a, b) => b.score - a.score)
|
|
181
|
-
return results.slice(0, limit)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
serialize(): SerializedTfIdf {
|
|
185
|
-
const idf: Record<string, number> = {}
|
|
186
|
-
for (const [term, val] of this.idf) {
|
|
187
|
-
idf[term] = val
|
|
188
|
-
}
|
|
189
|
-
const docs = this.documents.map((d) => {
|
|
190
|
-
const termFreqs: Record<string, number> = {}
|
|
191
|
-
for (const [term, tf] of d.termFreqs) {
|
|
192
|
-
termFreqs[term] = tf
|
|
193
|
-
}
|
|
194
|
-
return { id: d.entry.id, termFreqs, magnitude: d.magnitude }
|
|
195
|
-
})
|
|
196
|
-
return { idf, docs }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
static deserialize(data: SerializedTfIdf, entries: ScoredEntry[]): TfIdfAdapter {
|
|
200
|
-
const adapter = new TfIdfAdapter()
|
|
201
|
-
adapter.entries = entries
|
|
202
|
-
|
|
203
|
-
adapter.idf = new Map(Object.entries(data.idf))
|
|
204
|
-
|
|
205
|
-
const entryMap = new Map(entries.map((e) => [e.id, e]))
|
|
206
|
-
adapter.documents = data.docs
|
|
207
|
-
.map((d) => {
|
|
208
|
-
const entry = entryMap.get(d.id)
|
|
209
|
-
if (!entry) return null
|
|
210
|
-
return {
|
|
211
|
-
entry,
|
|
212
|
-
termFreqs: new Map(Object.entries(d.termFreqs)),
|
|
213
|
-
magnitude: d.magnitude,
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
.filter((d): d is TfIdfDocument => d !== null)
|
|
217
|
-
|
|
218
|
-
return adapter
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private substringFallback(query: string, limit: number, typeFilter?: string): ScoredResult[] {
|
|
222
|
-
const q = query.toLowerCase()
|
|
223
|
-
const tokens = tokenize(q)
|
|
224
|
-
return this.documents
|
|
225
|
-
.map((d) => {
|
|
226
|
-
if (typeFilter && typeFilter !== "any" && d.entry.entry.type !== typeFilter) return null
|
|
227
|
-
// Check if any query token matches the document text or name
|
|
228
|
-
const text = d.entry.text
|
|
229
|
-
const name = d.entry.entry.name.toLowerCase()
|
|
230
|
-
let matchCount = 0
|
|
231
|
-
for (const token of tokens) {
|
|
232
|
-
if (text.includes(token) || name.includes(token)) matchCount++
|
|
233
|
-
}
|
|
234
|
-
// Also check full substring match
|
|
235
|
-
if (text.includes(q) || name.includes(q)) matchCount = Math.max(matchCount, tokens.length)
|
|
236
|
-
if (matchCount === 0) return null
|
|
237
|
-
return {
|
|
238
|
-
entry: d.entry.entry,
|
|
239
|
-
path: d.entry.path,
|
|
240
|
-
score: Math.round((matchCount / Math.max(tokens.length, 1)) * 500) / 1000,
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
.filter((d): d is ScoredResult => d !== null)
|
|
244
|
-
.sort((a, b) => b.score - a.score)
|
|
245
|
-
.slice(0, limit)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ── Tokenization ────────────────────────────────────────────────────────────
|
|
250
|
-
|
|
251
|
-
const STOP_WORDS = new Set([
|
|
252
|
-
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
253
|
-
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
254
|
-
"should", "may", "might", "shall", "can", "need", "dare", "ought",
|
|
255
|
-
"to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
|
|
256
|
-
"into", "through", "during", "before", "after", "above", "below",
|
|
257
|
-
"and", "but", "or", "nor", "not", "so", "yet", "both", "either",
|
|
258
|
-
"neither", "each", "every", "all", "any", "few", "more", "most",
|
|
259
|
-
"other", "some", "such", "no", "only", "own", "same", "than",
|
|
260
|
-
"too", "very", "just", "because", "if", "when", "where", "how",
|
|
261
|
-
"what", "which", "who", "whom", "this", "that", "these", "those",
|
|
262
|
-
"it", "its",
|
|
263
|
-
])
|
|
264
|
-
|
|
265
|
-
function tokenize(text: string): string[] {
|
|
266
|
-
return text
|
|
267
|
-
.toLowerCase()
|
|
268
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
269
|
-
.split(/\s+/)
|
|
270
|
-
.filter((t) => t.length > 1 && !STOP_WORDS.has(t))
|
|
271
|
-
}
|