agentikit 0.0.12 → 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.
Files changed (146) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +186 -100
  3. package/dist/cli.js +671 -0
  4. package/dist/common.js +192 -0
  5. package/dist/{src/config-cli.js → config-cli.js} +14 -6
  6. package/dist/{src/config.js → config.js} +92 -24
  7. package/dist/{src/db.js → db.js} +109 -35
  8. package/dist/{src/embedder.js → embedder.js} +57 -2
  9. package/dist/file-context.js +158 -0
  10. package/dist/{src/handlers → handlers}/command-handler.js +2 -0
  11. package/dist/{src/handlers → handlers}/index.js +0 -6
  12. package/dist/{src/indexer.js → indexer.js} +34 -10
  13. package/dist/init.js +43 -0
  14. package/dist/lockfile.js +55 -0
  15. package/dist/matchers.js +157 -0
  16. package/dist/{src/metadata.js → metadata.js} +12 -1
  17. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  18. package/dist/paths.js +82 -0
  19. package/dist/{src/registry-install.js → registry-install.js} +145 -17
  20. package/dist/{src/registry-resolve.js → registry-resolve.js} +178 -18
  21. package/dist/{src/registry-search.js → registry-search.js} +8 -16
  22. package/dist/renderers.js +276 -0
  23. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +5 -5
  24. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  25. package/dist/self-update.js +220 -0
  26. package/dist/{src/stash-add.js → stash-add.js} +11 -2
  27. package/dist/stash-clone.js +115 -0
  28. package/dist/{src/stash-registry.js → stash-registry.js} +15 -41
  29. package/dist/{src/stash-search.js → stash-search.js} +67 -55
  30. package/dist/{src/stash-show.js → stash-show.js} +30 -3
  31. package/dist/{src/stash-source.js → stash-source.js} +56 -9
  32. package/dist/submit.js +552 -0
  33. package/dist/{src/walker.js → walker.js} +38 -0
  34. package/package.json +7 -16
  35. package/dist/index.d.ts +0 -28
  36. package/dist/index.js +0 -15
  37. package/dist/src/asset-spec.d.ts +0 -16
  38. package/dist/src/asset-type-handler.d.ts +0 -27
  39. package/dist/src/cli.d.ts +0 -2
  40. package/dist/src/cli.js +0 -399
  41. package/dist/src/common.d.ts +0 -13
  42. package/dist/src/common.js +0 -60
  43. package/dist/src/config-cli.d.ts +0 -9
  44. package/dist/src/config.d.ts +0 -50
  45. package/dist/src/db.d.ts +0 -46
  46. package/dist/src/embedder.d.ts +0 -10
  47. package/dist/src/frontmatter.d.ts +0 -30
  48. package/dist/src/github.d.ts +0 -4
  49. package/dist/src/handlers/agent-handler.d.ts +0 -2
  50. package/dist/src/handlers/command-handler.d.ts +0 -2
  51. package/dist/src/handlers/index.d.ts +0 -6
  52. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  53. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  54. package/dist/src/handlers/script-handler.d.ts +0 -2
  55. package/dist/src/handlers/skill-handler.d.ts +0 -2
  56. package/dist/src/handlers/tool-handler.d.ts +0 -2
  57. package/dist/src/indexer.d.ts +0 -22
  58. package/dist/src/init.d.ts +0 -19
  59. package/dist/src/init.js +0 -99
  60. package/dist/src/llm.d.ts +0 -15
  61. package/dist/src/markdown.d.ts +0 -18
  62. package/dist/src/metadata.d.ts +0 -41
  63. package/dist/src/origin-resolve.d.ts +0 -19
  64. package/dist/src/registry-install.d.ts +0 -11
  65. package/dist/src/registry-resolve.d.ts +0 -3
  66. package/dist/src/registry-search.d.ts +0 -27
  67. package/dist/src/registry-types.d.ts +0 -62
  68. package/dist/src/ripgrep-install.d.ts +0 -12
  69. package/dist/src/ripgrep-resolve.d.ts +0 -13
  70. package/dist/src/ripgrep.d.ts +0 -3
  71. package/dist/src/similarity.js +0 -211
  72. package/dist/src/stash-add.d.ts +0 -4
  73. package/dist/src/stash-clone.d.ts +0 -22
  74. package/dist/src/stash-clone.js +0 -83
  75. package/dist/src/stash-ref.d.ts +0 -31
  76. package/dist/src/stash-registry.d.ts +0 -18
  77. package/dist/src/stash-resolve.d.ts +0 -2
  78. package/dist/src/stash-search.d.ts +0 -8
  79. package/dist/src/stash-show.d.ts +0 -5
  80. package/dist/src/stash-source.d.ts +0 -24
  81. package/dist/src/stash-types.d.ts +0 -227
  82. package/dist/src/stash.d.ts +0 -16
  83. package/dist/src/stash.js +0 -9
  84. package/dist/src/tool-runner.d.ts +0 -35
  85. package/dist/src/walker.d.ts +0 -19
  86. package/src/asset-spec.ts +0 -85
  87. package/src/asset-type-handler.ts +0 -77
  88. package/src/cli.ts +0 -427
  89. package/src/common.ts +0 -76
  90. package/src/config-cli.ts +0 -499
  91. package/src/config.ts +0 -305
  92. package/src/db.ts +0 -411
  93. package/src/embedder.ts +0 -128
  94. package/src/frontmatter.ts +0 -95
  95. package/src/github.ts +0 -21
  96. package/src/handlers/agent-handler.ts +0 -32
  97. package/src/handlers/command-handler.ts +0 -29
  98. package/src/handlers/index.ts +0 -25
  99. package/src/handlers/knowledge-handler.ts +0 -62
  100. package/src/handlers/markdown-helpers.ts +0 -19
  101. package/src/handlers/script-handler.ts +0 -92
  102. package/src/handlers/skill-handler.ts +0 -37
  103. package/src/handlers/tool-handler.ts +0 -71
  104. package/src/indexer.ts +0 -392
  105. package/src/init.ts +0 -114
  106. package/src/llm.ts +0 -125
  107. package/src/markdown.ts +0 -106
  108. package/src/metadata.ts +0 -333
  109. package/src/origin-resolve.ts +0 -67
  110. package/src/registry-install.ts +0 -361
  111. package/src/registry-resolve.ts +0 -341
  112. package/src/registry-search.ts +0 -335
  113. package/src/registry-types.ts +0 -72
  114. package/src/ripgrep-install.ts +0 -200
  115. package/src/ripgrep-resolve.ts +0 -72
  116. package/src/ripgrep.ts +0 -3
  117. package/src/stash-add.ts +0 -63
  118. package/src/stash-clone.ts +0 -127
  119. package/src/stash-ref.ts +0 -99
  120. package/src/stash-registry.ts +0 -259
  121. package/src/stash-resolve.ts +0 -50
  122. package/src/stash-search.ts +0 -613
  123. package/src/stash-show.ts +0 -55
  124. package/src/stash-source.ts +0 -103
  125. package/src/stash-types.ts +0 -231
  126. package/src/stash.ts +0 -39
  127. package/src/tool-runner.ts +0 -142
  128. package/src/walker.ts +0 -53
  129. /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
  130. /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
  131. /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
  132. /package/dist/{src/github.js → github.js} +0 -0
  133. /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
  134. /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
  135. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  136. /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
  137. /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
  138. /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
  139. /package/dist/{src/llm.js → llm.js} +0 -0
  140. /package/dist/{src/markdown.js → markdown.js} +0 -0
  141. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  142. /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
  143. /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
  144. /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
  145. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
  146. /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
- return dot;
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
- db.exec("DELETE FROM entries");
30
- db.exec("DELETE FROM entries_fts");
31
- if (isVecAvailable()) {
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 || !isVecAvailable())
190
+ if (!config.semanticSearch)
170
191
  return false;
171
192
  try {
172
- const { embed } = await import("./embedder.js");
193
+ const { embedBatch } = await import("./embedder.js");
173
194
  const allEntries = getAllEntriesForEmbedding(db);
174
- for (const { id, searchText } of allEntries) {
175
- const embedding = await embed(searchText, config.embedding);
176
- upsertEmbedding(db, id, embedding);
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 entries_vec v WHERE v.id = e.id)
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
+ }
@@ -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
+ }
@@ -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
- fs.writeFileSync(filePath, JSON.stringify(stash, null, 2) + "\n", "utf8");
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 (working → mounted → installed)
9
- * 2. "local" → working stash only
10
- * 3. exact match → installed source whose registryId matches verbatim
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 → mounted source whose path matches
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.filter((s) => s.kind === "working");
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.kind === "installed" && s.registryId === origin);
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.kind === "installed" && s.registryId === parsed.id);
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
- // Mounted stash by resolved path
38
+ // Match by resolved path (any source, including installed)
38
39
  const resolvedOrigin = path.resolve(origin);
39
- const byPath = allSources.filter((s) => s.kind === "mounted" && path.resolve(s.path) === resolvedOrigin);
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