agentikit 0.0.13 → 0.0.14
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/LICENSE +385 -0
- package/README.md +180 -110
- package/dist/cli.js +671 -0
- package/dist/common.js +192 -0
- package/dist/{src/config-cli.js → config-cli.js} +14 -6
- package/dist/{src/config.js → config.js} +92 -24
- package/dist/{src/db.js → db.js} +109 -35
- package/dist/{src/embedder.js → embedder.js} +57 -2
- package/dist/file-context.js +158 -0
- package/dist/{src/handlers → handlers}/command-handler.js +2 -0
- package/dist/{src/handlers → handlers}/index.js +0 -6
- package/dist/{src/indexer.js → indexer.js} +34 -10
- package/dist/init.js +43 -0
- package/dist/lockfile.js +55 -0
- package/dist/matchers.js +157 -0
- package/dist/{src/metadata.js → metadata.js} +12 -1
- package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
- package/dist/paths.js +82 -0
- package/dist/{src/registry-install.js → registry-install.js} +145 -17
- package/dist/{src/registry-resolve.js → registry-resolve.js} +178 -18
- package/dist/{src/registry-search.js → registry-search.js} +8 -16
- package/dist/renderers.js +276 -0
- package/dist/{src/ripgrep-install.js → ripgrep-install.js} +5 -5
- package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
- package/dist/self-update.js +220 -0
- package/dist/{src/stash-add.js → stash-add.js} +11 -2
- package/dist/stash-clone.js +115 -0
- package/dist/{src/stash-registry.js → stash-registry.js} +15 -41
- package/dist/{src/stash-search.js → stash-search.js} +67 -55
- package/dist/{src/stash-show.js → stash-show.js} +30 -3
- package/dist/{src/stash-source.js → stash-source.js} +56 -9
- package/dist/submit.js +552 -0
- package/dist/{src/walker.js → walker.js} +38 -0
- package/package.json +7 -16
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -15
- package/dist/src/asset-spec.d.ts +0 -16
- package/dist/src/asset-type-handler.d.ts +0 -27
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -399
- package/dist/src/common.d.ts +0 -13
- package/dist/src/common.js +0 -60
- package/dist/src/config-cli.d.ts +0 -9
- package/dist/src/config.d.ts +0 -50
- package/dist/src/db.d.ts +0 -46
- package/dist/src/embedder.d.ts +0 -10
- package/dist/src/frontmatter.d.ts +0 -30
- package/dist/src/github.d.ts +0 -4
- package/dist/src/handlers/agent-handler.d.ts +0 -2
- package/dist/src/handlers/command-handler.d.ts +0 -2
- package/dist/src/handlers/index.d.ts +0 -6
- package/dist/src/handlers/knowledge-handler.d.ts +0 -2
- package/dist/src/handlers/markdown-helpers.d.ts +0 -7
- package/dist/src/handlers/script-handler.d.ts +0 -2
- package/dist/src/handlers/skill-handler.d.ts +0 -2
- package/dist/src/handlers/tool-handler.d.ts +0 -2
- package/dist/src/indexer.d.ts +0 -22
- package/dist/src/init.d.ts +0 -19
- package/dist/src/init.js +0 -99
- package/dist/src/llm.d.ts +0 -15
- package/dist/src/markdown.d.ts +0 -18
- package/dist/src/metadata.d.ts +0 -41
- package/dist/src/origin-resolve.d.ts +0 -19
- package/dist/src/registry-install.d.ts +0 -11
- package/dist/src/registry-resolve.d.ts +0 -3
- package/dist/src/registry-search.d.ts +0 -27
- package/dist/src/registry-types.d.ts +0 -62
- package/dist/src/ripgrep-install.d.ts +0 -12
- package/dist/src/ripgrep-resolve.d.ts +0 -13
- package/dist/src/ripgrep.d.ts +0 -3
- package/dist/src/stash-add.d.ts +0 -4
- package/dist/src/stash-clone.d.ts +0 -22
- package/dist/src/stash-clone.js +0 -83
- package/dist/src/stash-ref.d.ts +0 -31
- package/dist/src/stash-registry.d.ts +0 -18
- package/dist/src/stash-resolve.d.ts +0 -2
- package/dist/src/stash-search.d.ts +0 -8
- package/dist/src/stash-show.d.ts +0 -5
- package/dist/src/stash-source.d.ts +0 -24
- package/dist/src/stash-types.d.ts +0 -227
- package/dist/src/stash.d.ts +0 -16
- package/dist/src/stash.js +0 -9
- package/dist/src/tool-runner.d.ts +0 -35
- package/dist/src/walker.d.ts +0 -19
- package/src/asset-spec.ts +0 -85
- package/src/asset-type-handler.ts +0 -77
- package/src/cli.ts +0 -427
- package/src/common.ts +0 -76
- package/src/config-cli.ts +0 -499
- package/src/config.ts +0 -305
- package/src/db.ts +0 -411
- package/src/embedder.ts +0 -128
- package/src/frontmatter.ts +0 -95
- package/src/github.ts +0 -21
- package/src/handlers/agent-handler.ts +0 -32
- package/src/handlers/command-handler.ts +0 -29
- package/src/handlers/index.ts +0 -25
- package/src/handlers/knowledge-handler.ts +0 -62
- package/src/handlers/markdown-helpers.ts +0 -19
- package/src/handlers/script-handler.ts +0 -92
- package/src/handlers/skill-handler.ts +0 -37
- package/src/handlers/tool-handler.ts +0 -71
- package/src/indexer.ts +0 -392
- package/src/init.ts +0 -114
- package/src/llm.ts +0 -125
- package/src/markdown.ts +0 -106
- package/src/metadata.ts +0 -333
- package/src/origin-resolve.ts +0 -67
- package/src/registry-install.ts +0 -361
- package/src/registry-resolve.ts +0 -341
- package/src/registry-search.ts +0 -335
- package/src/registry-types.ts +0 -72
- package/src/ripgrep-install.ts +0 -200
- package/src/ripgrep-resolve.ts +0 -72
- package/src/ripgrep.ts +0 -3
- package/src/stash-add.ts +0 -63
- package/src/stash-clone.ts +0 -127
- package/src/stash-ref.ts +0 -99
- package/src/stash-registry.ts +0 -259
- package/src/stash-resolve.ts +0 -50
- package/src/stash-search.ts +0 -613
- package/src/stash-show.ts +0 -55
- package/src/stash-source.ts +0 -103
- package/src/stash-types.ts +0 -231
- package/src/stash.ts +0 -39
- package/src/tool-runner.ts +0 -142
- package/src/walker.ts +0 -53
- /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
- /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
- /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
- /package/dist/{src/github.js → github.js} +0 -0
- /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
- /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
- /package/dist/{src/llm.js → llm.js} +0 -0
- /package/dist/{src/markdown.js → markdown.js} +0 -0
- /package/dist/{src/registry-types.js → registry-types.js} +0 -0
- /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
- /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
- /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
- /package/dist/{src/stash-types.js → stash-types.js} +0 -0
- /package/dist/{src/tool-runner.js → tool-runner.js} +0 -0
|
@@ -60,16 +60,71 @@ export async function embed(text, embeddingConfig) {
|
|
|
60
60
|
}
|
|
61
61
|
return embedLocal(text);
|
|
62
62
|
}
|
|
63
|
+
// ── Batch embedding ─────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Generate embeddings for multiple texts in batch.
|
|
66
|
+
* Uses the OpenAI-compatible batch API for remote endpoints (batches of 100).
|
|
67
|
+
* Falls back to sequential embedding for local transformer pipeline.
|
|
68
|
+
*/
|
|
69
|
+
export async function embedBatch(texts, embeddingConfig) {
|
|
70
|
+
if (texts.length === 0)
|
|
71
|
+
return [];
|
|
72
|
+
if (embeddingConfig) {
|
|
73
|
+
return embedRemoteBatch(texts, embeddingConfig);
|
|
74
|
+
}
|
|
75
|
+
// Local transformer: process sequentially (pipeline handles one at a time)
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const text of texts) {
|
|
78
|
+
results.push(await embedLocal(text));
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
async function embedRemoteBatch(texts, config) {
|
|
83
|
+
const BATCH_SIZE = 100;
|
|
84
|
+
const results = [];
|
|
85
|
+
const headers = { "Content-Type": "application/json" };
|
|
86
|
+
if (config.apiKey) {
|
|
87
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
88
|
+
}
|
|
89
|
+
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
90
|
+
const batch = texts.slice(i, i + BATCH_SIZE);
|
|
91
|
+
const body = {
|
|
92
|
+
input: batch,
|
|
93
|
+
model: config.model,
|
|
94
|
+
};
|
|
95
|
+
if (config.dimension) {
|
|
96
|
+
body.dimensions = config.dimension;
|
|
97
|
+
}
|
|
98
|
+
const response = await fetchWithTimeout(config.endpoint, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers,
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const respBody = await response.text().catch(() => "");
|
|
105
|
+
throw new Error(`Embedding batch request failed (${response.status}): ${respBody}`);
|
|
106
|
+
}
|
|
107
|
+
const json = (await response.json());
|
|
108
|
+
if (!json.data || json.data.length !== batch.length) {
|
|
109
|
+
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}`);
|
|
110
|
+
}
|
|
111
|
+
results.push(...json.data.map((d) => d.embedding));
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
63
115
|
// ── Similarity ──────────────────────────────────────────────────────────────
|
|
64
116
|
export function cosineSimilarity(a, b) {
|
|
65
117
|
const len = Math.min(a.length, b.length);
|
|
66
118
|
if (len === 0)
|
|
67
119
|
return 0;
|
|
68
|
-
let dot = 0;
|
|
120
|
+
let dot = 0, magA = 0, magB = 0;
|
|
69
121
|
for (let i = 0; i < len; i++) {
|
|
70
122
|
dot += a[i] * b[i];
|
|
123
|
+
magA += a[i] * a[i];
|
|
124
|
+
magB += b[i] * b[i];
|
|
71
125
|
}
|
|
72
|
-
|
|
126
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
127
|
+
return denom === 0 ? 0 : dot / denom;
|
|
73
128
|
}
|
|
74
129
|
// ── Availability check ──────────────────────────────────────────────────────
|
|
75
130
|
export async function isEmbeddingAvailable(embeddingConfig) {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible asset resolution system.
|
|
3
|
+
*
|
|
4
|
+
* Provides a rich FileContext built once per file during walking, plus a
|
|
5
|
+
* matcher/renderer registry that decouples asset classification from rendering.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
10
|
+
import { toPosix } from "./common";
|
|
11
|
+
/**
|
|
12
|
+
* Build a FileContext from a stash root and an absolute file path.
|
|
13
|
+
*
|
|
14
|
+
* Path-derived fields are computed eagerly. The content, frontmatter, and
|
|
15
|
+
* stat getters use lazy caching so the file is only read from disk when
|
|
16
|
+
* (and if) a matcher or renderer actually needs it.
|
|
17
|
+
*/
|
|
18
|
+
export function buildFileContext(stashRoot, absPath) {
|
|
19
|
+
const relPath = toPosix(path.relative(stashRoot, absPath));
|
|
20
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
21
|
+
const fileName = path.basename(absPath);
|
|
22
|
+
const parentDirAbs = path.dirname(absPath);
|
|
23
|
+
const parentDir = path.basename(parentDirAbs);
|
|
24
|
+
// Compute ancestor directory segments from the POSIX relPath's directory portion.
|
|
25
|
+
// For "tools/azure/deploy/run.sh" the dir portion is "tools/azure/deploy"
|
|
26
|
+
// which splits into ["tools", "azure", "deploy"].
|
|
27
|
+
const relDir = toPosix(path.dirname(relPath));
|
|
28
|
+
const ancestorDirs = relDir === "." ? [] : relDir.split("/").filter((seg) => seg.length > 0);
|
|
29
|
+
// Lazy caches
|
|
30
|
+
let cachedContent;
|
|
31
|
+
let cachedFrontmatter;
|
|
32
|
+
let frontmatterComputed = false;
|
|
33
|
+
let cachedStat;
|
|
34
|
+
return {
|
|
35
|
+
absPath,
|
|
36
|
+
relPath,
|
|
37
|
+
ext,
|
|
38
|
+
fileName,
|
|
39
|
+
parentDir,
|
|
40
|
+
parentDirAbs,
|
|
41
|
+
ancestorDirs,
|
|
42
|
+
stashRoot,
|
|
43
|
+
content() {
|
|
44
|
+
if (cachedContent === undefined) {
|
|
45
|
+
cachedContent = fs.readFileSync(absPath, "utf8");
|
|
46
|
+
}
|
|
47
|
+
return cachedContent;
|
|
48
|
+
},
|
|
49
|
+
frontmatter() {
|
|
50
|
+
if (!frontmatterComputed) {
|
|
51
|
+
const raw = this.content();
|
|
52
|
+
const parsed = parseFrontmatter(raw);
|
|
53
|
+
cachedFrontmatter =
|
|
54
|
+
Object.keys(parsed.data).length > 0 ? parsed.data : null;
|
|
55
|
+
frontmatterComputed = true;
|
|
56
|
+
}
|
|
57
|
+
return cachedFrontmatter;
|
|
58
|
+
},
|
|
59
|
+
stat() {
|
|
60
|
+
if (cachedStat === undefined) {
|
|
61
|
+
cachedStat = fs.statSync(absPath);
|
|
62
|
+
}
|
|
63
|
+
return cachedStat;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ── Registry ─────────────────────────────────────────────────────────────────
|
|
68
|
+
/** Ordered list of registered matchers. Later registrations win ties. */
|
|
69
|
+
const matchers = [];
|
|
70
|
+
/** Renderer lookup by name. */
|
|
71
|
+
const renderers = new Map();
|
|
72
|
+
let builtinsInitialized = false;
|
|
73
|
+
/**
|
|
74
|
+
* Ensure that built-in matchers and renderers are registered.
|
|
75
|
+
* Called lazily on first use of runMatchers/getRenderer.
|
|
76
|
+
*/
|
|
77
|
+
function ensureBuiltinsRegistered() {
|
|
78
|
+
if (builtinsInitialized)
|
|
79
|
+
return;
|
|
80
|
+
builtinsInitialized = true;
|
|
81
|
+
// Side-effect imports that register matchers/renderers at module load
|
|
82
|
+
require("./matchers");
|
|
83
|
+
require("./renderers");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Register an AssetMatcher.
|
|
87
|
+
*
|
|
88
|
+
* Matchers are evaluated in registration order. When two matchers produce
|
|
89
|
+
* the same specificity score, the one registered later wins.
|
|
90
|
+
*/
|
|
91
|
+
export function registerMatcher(matcher) {
|
|
92
|
+
matchers.push(matcher);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Register an AssetRenderer.
|
|
96
|
+
*
|
|
97
|
+
* If a renderer with the same name already exists it is silently replaced.
|
|
98
|
+
*/
|
|
99
|
+
export function registerRenderer(renderer) {
|
|
100
|
+
renderers.set(renderer.name, renderer);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Look up a renderer by name.
|
|
104
|
+
*/
|
|
105
|
+
export function getRenderer(name) {
|
|
106
|
+
ensureBuiltinsRegistered();
|
|
107
|
+
return renderers.get(name);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Return all registered renderers (snapshot, safe to iterate).
|
|
111
|
+
*/
|
|
112
|
+
export function getAllRenderers() {
|
|
113
|
+
ensureBuiltinsRegistered();
|
|
114
|
+
return Array.from(renderers.values());
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Run every registered matcher against a FileContext and return the
|
|
118
|
+
* highest-specificity result.
|
|
119
|
+
*
|
|
120
|
+
* Resolution rules:
|
|
121
|
+
* 1. Every matcher is invoked; null returns are discarded.
|
|
122
|
+
* 2. Results are ranked by specificity (descending).
|
|
123
|
+
* 3. Ties are broken by registration order: the matcher registered later wins
|
|
124
|
+
* (this lets user-registered matchers override built-in ones).
|
|
125
|
+
* 4. Returns null when no matcher claims the file.
|
|
126
|
+
*/
|
|
127
|
+
export function runMatchers(ctx) {
|
|
128
|
+
ensureBuiltinsRegistered();
|
|
129
|
+
// Collect (result, registrationIndex) pairs from all matchers.
|
|
130
|
+
const hits = [];
|
|
131
|
+
for (let i = 0; i < matchers.length; i++) {
|
|
132
|
+
const result = matchers[i](ctx);
|
|
133
|
+
if (result !== null) {
|
|
134
|
+
hits.push({ result, index: i });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (hits.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
// Sort by specificity descending, then by registration index descending (later wins ties).
|
|
140
|
+
hits.sort((a, b) => {
|
|
141
|
+
const specDiff = b.result.specificity - a.result.specificity;
|
|
142
|
+
if (specDiff !== 0)
|
|
143
|
+
return specDiff;
|
|
144
|
+
return b.index - a.index;
|
|
145
|
+
});
|
|
146
|
+
return hits[0].result;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build a RenderContext by merging a FileContext with its winning MatchResult
|
|
150
|
+
* and the list of stash search paths.
|
|
151
|
+
*/
|
|
152
|
+
export function buildRenderContext(ctx, match, stashDirs) {
|
|
153
|
+
return {
|
|
154
|
+
...ctx,
|
|
155
|
+
matchResult: match,
|
|
156
|
+
stashDirs,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -14,6 +14,8 @@ export const commandHandler = {
|
|
|
14
14
|
path: input.path,
|
|
15
15
|
description: toStringOrUndefined(parsedMd.data.description),
|
|
16
16
|
template: parsedMd.content,
|
|
17
|
+
modelHint: parsedMd.data.model,
|
|
18
|
+
agent: toStringOrUndefined(parsedMd.data.agent),
|
|
17
19
|
};
|
|
18
20
|
},
|
|
19
21
|
defaultUsageGuide: [
|
|
@@ -15,9 +15,3 @@ registerAssetType(commandHandler);
|
|
|
15
15
|
registerAssetType(agentHandler);
|
|
16
16
|
registerAssetType(knowledgeHandler);
|
|
17
17
|
registerAssetType(scriptHandler);
|
|
18
|
-
export { toolHandler } from "./tool-handler";
|
|
19
|
-
export { skillHandler } from "./skill-handler";
|
|
20
|
-
export { commandHandler } from "./command-handler";
|
|
21
|
-
export { agentHandler } from "./agent-handler";
|
|
22
|
-
export { knowledgeHandler } from "./knowledge-handler";
|
|
23
|
-
export { scriptHandler } from "./script-handler";
|
|
@@ -4,7 +4,7 @@ import { resolveStashDir } from "./common";
|
|
|
4
4
|
import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec";
|
|
5
5
|
import { loadStashFile, writeStashFile, generateMetadata, } from "./metadata";
|
|
6
6
|
import { walkStash } from "./walker";
|
|
7
|
-
import { openDatabase, closeDatabase, getDbPath, getMeta, setMeta, upsertEntry, deleteEntriesByDir, rebuildFts, upsertEmbedding, getEntriesByDir, getEntryCount, isVecAvailable, DB_VERSION, } from "./db";
|
|
7
|
+
import { openDatabase, closeDatabase, getDbPath, getMeta, setMeta, upsertEntry, deleteEntriesByDir, rebuildFts, upsertEmbedding, getEntriesByDir, getEntryCount, isVecAvailable, warnIfVecMissing, DB_VERSION, } from "./db";
|
|
8
8
|
// ── Indexer ──────────────────────────────────────────────────────────────────
|
|
9
9
|
export async function agentikitIndex(options) {
|
|
10
10
|
const stashDir = options?.stashDir || resolveStashDir();
|
|
@@ -26,14 +26,19 @@ export async function agentikitIndex(options) {
|
|
|
26
26
|
const builtAtMs = isIncremental ? new Date(prevBuiltAt).getTime() : 0;
|
|
27
27
|
if (options?.full || !isIncremental) {
|
|
28
28
|
// Wipe all entries for full rebuild or stashDir change
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// Delete from child tables first to respect foreign key constraints
|
|
30
|
+
try {
|
|
31
|
+
db.exec("DELETE FROM embeddings");
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
34
|
+
if (isVecAvailable(db)) {
|
|
32
35
|
try {
|
|
33
36
|
db.exec("DELETE FROM entries_vec");
|
|
34
37
|
}
|
|
35
38
|
catch { /* ignore */ }
|
|
36
39
|
}
|
|
40
|
+
db.exec("DELETE FROM entries_fts");
|
|
41
|
+
db.exec("DELETE FROM entries");
|
|
37
42
|
}
|
|
38
43
|
const tWalkStart = Date.now();
|
|
39
44
|
// Walk stash dirs and index entries
|
|
@@ -54,6 +59,8 @@ export async function agentikitIndex(options) {
|
|
|
54
59
|
setMeta(db, "stashDirs", JSON.stringify(allStashDirs));
|
|
55
60
|
setMeta(db, "hasEmbeddings", hasEmbeddings ? "1" : "0");
|
|
56
61
|
const totalEntries = getEntryCount(db);
|
|
62
|
+
// Warn on every index run if using JS fallback with many entries
|
|
63
|
+
warnIfVecMissing(db);
|
|
57
64
|
const tEnd = Date.now();
|
|
58
65
|
return {
|
|
59
66
|
stashDir,
|
|
@@ -117,6 +124,20 @@ function indexEntries(db, allStashDirs, stashDir, isIncremental, builtAtMs) {
|
|
|
117
124
|
stash = migration.stash;
|
|
118
125
|
writeStashFile(dirPath, stash);
|
|
119
126
|
}
|
|
127
|
+
// Check for files on disk that aren't covered by existing .stash.json entries.
|
|
128
|
+
// This handles the case where new files are added after the initial index.
|
|
129
|
+
const coveredFiles = new Set(stash.entries
|
|
130
|
+
.map((e) => e.entry)
|
|
131
|
+
.filter((e) => !!e));
|
|
132
|
+
const uncoveredFiles = files.filter((f) => !coveredFiles.has(path.basename(f)));
|
|
133
|
+
if (uncoveredFiles.length > 0) {
|
|
134
|
+
const generated = generateMetadata(dirPath, assetType, uncoveredFiles, typeRoot);
|
|
135
|
+
if (generated.entries.length > 0) {
|
|
136
|
+
stash = { entries: [...stash.entries, ...generated.entries] };
|
|
137
|
+
writeStashFile(dirPath, stash);
|
|
138
|
+
generatedCount += generated.entries.length;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
120
141
|
}
|
|
121
142
|
if (!stash) {
|
|
122
143
|
// Generate metadata heuristically
|
|
@@ -166,14 +187,17 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm) {
|
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
async function generateEmbeddingsForDb(db, config) {
|
|
169
|
-
if (!config.semanticSearch
|
|
190
|
+
if (!config.semanticSearch)
|
|
170
191
|
return false;
|
|
171
192
|
try {
|
|
172
|
-
const {
|
|
193
|
+
const { embedBatch } = await import("./embedder.js");
|
|
173
194
|
const allEntries = getAllEntriesForEmbedding(db);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
if (allEntries.length === 0)
|
|
196
|
+
return true;
|
|
197
|
+
const texts = allEntries.map((e) => e.searchText);
|
|
198
|
+
const embeddings = await embedBatch(texts, config.embedding);
|
|
199
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
200
|
+
upsertEmbedding(db, allEntries[i].id, embeddings[i]);
|
|
177
201
|
}
|
|
178
202
|
return true;
|
|
179
203
|
}
|
|
@@ -187,7 +211,7 @@ function getAllEntriesForEmbedding(db) {
|
|
|
187
211
|
return db
|
|
188
212
|
.prepare(`
|
|
189
213
|
SELECT e.id, e.search_text AS searchText FROM entries e
|
|
190
|
-
WHERE NOT EXISTS (SELECT 1 FROM
|
|
214
|
+
WHERE NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)
|
|
191
215
|
`)
|
|
192
216
|
.all();
|
|
193
217
|
}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentikit initialization logic.
|
|
3
|
+
*
|
|
4
|
+
* Creates the working stash directory structure, persists the stashDir
|
|
5
|
+
* in config.json, and ensures ripgrep is available.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { TYPE_DIRS } from "./common";
|
|
10
|
+
import { ensureRg } from "./ripgrep-install";
|
|
11
|
+
import { loadConfig, saveConfig, getConfigPath } from "./config";
|
|
12
|
+
import { getDefaultStashDir, getBinDir } from "./paths";
|
|
13
|
+
export async function agentikitInit(options) {
|
|
14
|
+
const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
|
|
15
|
+
let created = false;
|
|
16
|
+
if (!fs.existsSync(stashDir)) {
|
|
17
|
+
fs.mkdirSync(stashDir, { recursive: true });
|
|
18
|
+
created = true;
|
|
19
|
+
}
|
|
20
|
+
for (const sub of Object.values(TYPE_DIRS)) {
|
|
21
|
+
const subDir = path.join(stashDir, sub);
|
|
22
|
+
if (!fs.existsSync(subDir)) {
|
|
23
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Persist stashDir in config.json
|
|
27
|
+
const configPath = getConfigPath();
|
|
28
|
+
const existing = loadConfig();
|
|
29
|
+
if (!existing.stashDir || existing.stashDir !== stashDir) {
|
|
30
|
+
saveConfig({ ...existing, stashDir });
|
|
31
|
+
}
|
|
32
|
+
// Ensure ripgrep is available (install to cache/bin if needed)
|
|
33
|
+
let ripgrep;
|
|
34
|
+
try {
|
|
35
|
+
const binDir = getBinDir();
|
|
36
|
+
const rgResult = ensureRg(binDir);
|
|
37
|
+
ripgrep = rgResult;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Non-fatal: ripgrep is optional, search works without it
|
|
41
|
+
}
|
|
42
|
+
return { stashDir, created, configPath, ripgrep };
|
|
43
|
+
}
|
package/dist/lockfile.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getConfigDir } from "./config";
|
|
4
|
+
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
5
|
+
function getLockfilePath() {
|
|
6
|
+
return path.join(getConfigDir(), "stash.lock");
|
|
7
|
+
}
|
|
8
|
+
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
9
|
+
export function readLockfile() {
|
|
10
|
+
const lockfilePath = getLockfilePath();
|
|
11
|
+
try {
|
|
12
|
+
const raw = JSON.parse(fs.readFileSync(lockfilePath, "utf8"));
|
|
13
|
+
if (!Array.isArray(raw))
|
|
14
|
+
return [];
|
|
15
|
+
return raw.filter(isValidLockfileEntry);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function writeLockfile(entries) {
|
|
22
|
+
const lockfilePath = getLockfilePath();
|
|
23
|
+
const dir = path.dirname(lockfilePath);
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
const tmpPath = lockfilePath + `.tmp.${process.pid}`;
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2) + "\n", "utf8");
|
|
28
|
+
fs.renameSync(tmpPath, lockfilePath);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
try {
|
|
32
|
+
fs.unlinkSync(tmpPath);
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore cleanup failure */ }
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function upsertLockEntry(entry) {
|
|
39
|
+
const entries = readLockfile();
|
|
40
|
+
const withoutExisting = entries.filter((e) => e.id !== entry.id);
|
|
41
|
+
writeLockfile([...withoutExisting, entry]);
|
|
42
|
+
}
|
|
43
|
+
export function removeLockEntry(id) {
|
|
44
|
+
const entries = readLockfile();
|
|
45
|
+
writeLockfile(entries.filter((e) => e.id !== id));
|
|
46
|
+
}
|
|
47
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
48
|
+
function isValidLockfileEntry(value) {
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
50
|
+
return false;
|
|
51
|
+
const obj = value;
|
|
52
|
+
return (typeof obj.id === "string" && obj.id !== "" &&
|
|
53
|
+
typeof obj.source === "string" && ["npm", "github", "git", "local"].includes(obj.source) &&
|
|
54
|
+
typeof obj.ref === "string" && obj.ref !== "");
|
|
55
|
+
}
|
package/dist/matchers.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in asset matchers for the agentikit file classification system.
|
|
3
|
+
*
|
|
4
|
+
* Four matchers are registered at module load time, each at a different
|
|
5
|
+
* specificity level. Extension and content determine type; directories are
|
|
6
|
+
* optional specificity boosts, not requirements.
|
|
7
|
+
*
|
|
8
|
+
* - `extensionMatcher` (3) -- classifies any file by extension alone.
|
|
9
|
+
* Ensures every known file type is discoverable regardless of directory.
|
|
10
|
+
* - `directoryMatcher` (10) -- boosts specificity when the first ancestor
|
|
11
|
+
* directory matches a known type name (e.g. `scripts/`, `agents/`).
|
|
12
|
+
* - `parentDirHintMatcher` (15) -- boosts specificity based on the
|
|
13
|
+
* immediate parent directory name.
|
|
14
|
+
* - `smartMdMatcher` (20 / 18 / 8 / 5) -- inspects markdown frontmatter
|
|
15
|
+
* and body content for agent/command signals; falls back to "knowledge"
|
|
16
|
+
* at specificity 5 when no signals are found. Command signals (`agent`
|
|
17
|
+
* frontmatter, `$ARGUMENTS`/`$1`-`$3` placeholders) return 18.
|
|
18
|
+
*/
|
|
19
|
+
import { SCRIPT_EXTENSIONS_BROAD } from "./asset-spec";
|
|
20
|
+
import { registerMatcher } from "./file-context";
|
|
21
|
+
// ── extensionMatcher (specificity: 3) ────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Base-level matcher that classifies files purely by extension.
|
|
24
|
+
*
|
|
25
|
+
* This is the foundation of the classification system: every file with a
|
|
26
|
+
* known extension gets a type, regardless of what directory it lives in.
|
|
27
|
+
* Higher-specificity matchers (directory, content) can override this.
|
|
28
|
+
*
|
|
29
|
+
* .md files are NOT handled here -- smartMdMatcher provides richer
|
|
30
|
+
* classification for markdown via frontmatter inspection.
|
|
31
|
+
*/
|
|
32
|
+
export function extensionMatcher(ctx) {
|
|
33
|
+
// SKILL.md is a skill regardless of location
|
|
34
|
+
if (ctx.fileName === "SKILL.md") {
|
|
35
|
+
return { type: "skill", specificity: 3, renderer: "skill-md" };
|
|
36
|
+
}
|
|
37
|
+
// Known script extensions (excluding .md, handled by smartMdMatcher)
|
|
38
|
+
if (SCRIPT_EXTENSIONS_BROAD.has(ctx.ext)) {
|
|
39
|
+
return { type: "script", specificity: 3, renderer: "script-source" };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// ── directoryMatcher (specificity: 10) ──────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Directory-based matcher that boosts specificity when the first ancestor
|
|
46
|
+
* directory segment from the stash root matches a known type name.
|
|
47
|
+
*
|
|
48
|
+
* Accepts ALL known script extensions in both `tools/` and `scripts/`
|
|
49
|
+
* directories -- the distinction is purely organizational.
|
|
50
|
+
*/
|
|
51
|
+
export function directoryMatcher(ctx) {
|
|
52
|
+
const topDir = ctx.ancestorDirs[0];
|
|
53
|
+
if (!topDir)
|
|
54
|
+
return null;
|
|
55
|
+
const ext = ctx.ext;
|
|
56
|
+
if ((topDir === "tools" || topDir === "scripts") && SCRIPT_EXTENSIONS_BROAD.has(ext)) {
|
|
57
|
+
return { type: "script", specificity: 10, renderer: "script-source" };
|
|
58
|
+
}
|
|
59
|
+
if (topDir === "skills" && ctx.fileName === "SKILL.md") {
|
|
60
|
+
return { type: "skill", specificity: 10, renderer: "skill-md" };
|
|
61
|
+
}
|
|
62
|
+
if (topDir === "commands" && ext === ".md") {
|
|
63
|
+
return { type: "command", specificity: 10, renderer: "command-md" };
|
|
64
|
+
}
|
|
65
|
+
if (topDir === "agents" && ext === ".md") {
|
|
66
|
+
return { type: "agent", specificity: 10, renderer: "agent-md" };
|
|
67
|
+
}
|
|
68
|
+
if (topDir === "knowledge" && ext === ".md") {
|
|
69
|
+
return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// ── parentDirHintMatcher (specificity: 15) ──────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Uses the immediate parent directory name as a hint. More specific than
|
|
76
|
+
* the ancestor-based directory matcher because the file might be nested
|
|
77
|
+
* several levels deep, yet its immediate parent can still carry strong
|
|
78
|
+
* naming conventions (e.g. `my-project/agents/planning.md`).
|
|
79
|
+
*
|
|
80
|
+
* Accepts ALL known script extensions in both `tools/` and `scripts/`.
|
|
81
|
+
*/
|
|
82
|
+
export function parentDirHintMatcher(ctx) {
|
|
83
|
+
const { parentDir, ext, fileName } = ctx;
|
|
84
|
+
if ((parentDir === "tools" || parentDir === "scripts") && SCRIPT_EXTENSIONS_BROAD.has(ext)) {
|
|
85
|
+
return { type: "script", specificity: 15, renderer: "script-source" };
|
|
86
|
+
}
|
|
87
|
+
if (parentDir === "skills" && fileName === "SKILL.md") {
|
|
88
|
+
return { type: "skill", specificity: 15, renderer: "skill-md" };
|
|
89
|
+
}
|
|
90
|
+
if (parentDir === "agents" && ext === ".md") {
|
|
91
|
+
return { type: "agent", specificity: 15, renderer: "agent-md" };
|
|
92
|
+
}
|
|
93
|
+
if (parentDir === "commands" && ext === ".md") {
|
|
94
|
+
return { type: "command", specificity: 15, renderer: "command-md" };
|
|
95
|
+
}
|
|
96
|
+
if (parentDir === "knowledge" && ext === ".md") {
|
|
97
|
+
return { type: "knowledge", specificity: 15, renderer: "knowledge-md" };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// ── smartMdMatcher (specificity: 20 / 18 / 8 / 5) ──────────────────────────
|
|
102
|
+
/** Pattern that matches OpenCode command placeholders in markdown body. */
|
|
103
|
+
const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
|
|
104
|
+
/**
|
|
105
|
+
* Content-based matcher for `.md` files. Inspects frontmatter keys and body
|
|
106
|
+
* content to classify markdown as agent, command, or knowledge.
|
|
107
|
+
*
|
|
108
|
+
* Specificity levels:
|
|
109
|
+
* 20 -- agent-exclusive signals (`tools`, `toolPolicy`)
|
|
110
|
+
* 18 -- command content signals (`agent` frontmatter, `$ARGUMENTS`/`$1`-`$3`)
|
|
111
|
+
* 8 -- weak agent signal (`model` alone)
|
|
112
|
+
* 5 -- knowledge fallback (any unclassified `.md`)
|
|
113
|
+
*
|
|
114
|
+
* Command signals at 18 override directory hints (10/15) because the content
|
|
115
|
+
* unambiguously identifies a command template. Agent-exclusive signals at 20
|
|
116
|
+
* still win over command signals when both are present.
|
|
117
|
+
*/
|
|
118
|
+
export function smartMdMatcher(ctx) {
|
|
119
|
+
if (ctx.ext !== ".md")
|
|
120
|
+
return null;
|
|
121
|
+
const fm = ctx.frontmatter();
|
|
122
|
+
if (fm) {
|
|
123
|
+
// Agent-exclusive indicators: toolPolicy or tools
|
|
124
|
+
// These return high specificity (20) to override everything else.
|
|
125
|
+
if ("toolPolicy" in fm || "tools" in fm) {
|
|
126
|
+
return { type: "agent", specificity: 20, renderer: "agent-md" };
|
|
127
|
+
}
|
|
128
|
+
// Command signal: `agent` frontmatter key names a dispatch target.
|
|
129
|
+
// This is an OpenCode convention specific to commands.
|
|
130
|
+
if ("agent" in fm) {
|
|
131
|
+
return { type: "command", specificity: 18, renderer: "command-md" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Command signal: body contains $ARGUMENTS or $1/$2/$3 placeholders.
|
|
135
|
+
// These are definitively command template patterns (OpenCode convention).
|
|
136
|
+
const body = ctx.content();
|
|
137
|
+
if (COMMAND_PLACEHOLDER_RE.test(body)) {
|
|
138
|
+
return { type: "command", specificity: 18, renderer: "command-md" };
|
|
139
|
+
}
|
|
140
|
+
if (fm) {
|
|
141
|
+
// model alone is a weaker agent signal (specificity 8) -- it can appear
|
|
142
|
+
// on commands too (OpenCode convention). Directory hints (10/15) win
|
|
143
|
+
// when the file lives in commands/, but model still classifies an .md
|
|
144
|
+
// as agent when no directory hint is present.
|
|
145
|
+
if ("model" in fm) {
|
|
146
|
+
return { type: "agent", specificity: 8, renderer: "agent-md" };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Weak fallback: any .md file is assumed to be knowledge
|
|
150
|
+
return { type: "knowledge", specificity: 5, renderer: "knowledge-md" };
|
|
151
|
+
}
|
|
152
|
+
// ── Registration ────────────────────────────────────────────────────────────
|
|
153
|
+
// Order matters: later registrations win ties at the same specificity.
|
|
154
|
+
registerMatcher(extensionMatcher);
|
|
155
|
+
registerMatcher(directoryMatcher);
|
|
156
|
+
registerMatcher(parentDirHintMatcher);
|
|
157
|
+
registerMatcher(smartMdMatcher);
|
|
@@ -31,7 +31,18 @@ export function loadStashFile(dirPath) {
|
|
|
31
31
|
}
|
|
32
32
|
export function writeStashFile(dirPath, stash) {
|
|
33
33
|
const filePath = stashFilePath(dirPath);
|
|
34
|
-
|
|
34
|
+
const tmpPath = filePath + `.tmp.${process.pid}`;
|
|
35
|
+
try {
|
|
36
|
+
fs.writeFileSync(tmpPath, JSON.stringify(stash, null, 2) + "\n", "utf8");
|
|
37
|
+
fs.renameSync(tmpPath, filePath);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(tmpPath);
|
|
42
|
+
}
|
|
43
|
+
catch { /* ignore cleanup failure */ }
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
export function validateStashEntry(entry) {
|
|
37
48
|
if (typeof entry !== "object" || entry === null)
|
|
@@ -5,21 +5,22 @@ import { parseRegistryRef } from "./registry-resolve";
|
|
|
5
5
|
* sources, return the subset of sources to search.
|
|
6
6
|
*
|
|
7
7
|
* Resolution order:
|
|
8
|
-
* 1. undefined → all sources
|
|
9
|
-
* 2. "local" →
|
|
10
|
-
* 3. exact match →
|
|
8
|
+
* 1. undefined → all sources
|
|
9
|
+
* 2. "local" → primary stash only (first entry)
|
|
10
|
+
* 3. exact match → source whose registryId matches verbatim
|
|
11
11
|
* 4. parsed match → parse origin as a registry ref, match by parsed ID
|
|
12
|
-
* 5. path match →
|
|
12
|
+
* 5. path match → source whose resolved path matches the origin
|
|
13
13
|
* 6. empty → indicates a remote/uninstalled origin (caller decides)
|
|
14
14
|
*/
|
|
15
15
|
export function resolveSourcesForOrigin(origin, allSources) {
|
|
16
16
|
if (!origin)
|
|
17
17
|
return allSources;
|
|
18
|
+
// "local" means the primary stash (first entry)
|
|
18
19
|
if (origin === "local") {
|
|
19
|
-
return allSources.
|
|
20
|
+
return allSources.length > 0 ? [allSources[0]] : [];
|
|
20
21
|
}
|
|
21
22
|
// Exact registryId match (e.g. origin is "npm:@scope/pkg")
|
|
22
|
-
const byExactId = allSources.filter((s) => s.
|
|
23
|
+
const byExactId = allSources.filter((s) => s.registryId !== undefined && s.registryId === origin);
|
|
23
24
|
if (byExactId.length > 0)
|
|
24
25
|
return byExactId;
|
|
25
26
|
// Parse origin as a registry ref and match by parsed ID.
|
|
@@ -27,16 +28,16 @@ export function resolveSourcesForOrigin(origin, allSources) {
|
|
|
27
28
|
// "@scope/pkg" matches "npm:@scope/pkg".
|
|
28
29
|
try {
|
|
29
30
|
const parsed = parseRegistryRef(origin);
|
|
30
|
-
const byParsedId = allSources.filter((s) => s.
|
|
31
|
+
const byParsedId = allSources.filter((s) => s.registryId !== undefined && s.registryId === parsed.id);
|
|
31
32
|
if (byParsedId.length > 0)
|
|
32
33
|
return byParsedId;
|
|
33
34
|
}
|
|
34
35
|
catch {
|
|
35
36
|
// Not a valid registry ref — continue to path matching
|
|
36
37
|
}
|
|
37
|
-
//
|
|
38
|
+
// Match by resolved path (any source, including installed)
|
|
38
39
|
const resolvedOrigin = path.resolve(origin);
|
|
39
|
-
const byPath = allSources.filter((s) =>
|
|
40
|
+
const byPath = allSources.filter((s) => path.resolve(s.path) === resolvedOrigin);
|
|
40
41
|
if (byPath.length > 0)
|
|
41
42
|
return byPath;
|
|
42
43
|
// No match — origin may be remote/uninstalled
|