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.
Files changed (145) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +180 -110
  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/stash-add.d.ts +0 -4
  72. package/dist/src/stash-clone.d.ts +0 -22
  73. package/dist/src/stash-clone.js +0 -83
  74. package/dist/src/stash-ref.d.ts +0 -31
  75. package/dist/src/stash-registry.d.ts +0 -18
  76. package/dist/src/stash-resolve.d.ts +0 -2
  77. package/dist/src/stash-search.d.ts +0 -8
  78. package/dist/src/stash-show.d.ts +0 -5
  79. package/dist/src/stash-source.d.ts +0 -24
  80. package/dist/src/stash-types.d.ts +0 -227
  81. package/dist/src/stash.d.ts +0 -16
  82. package/dist/src/stash.js +0 -9
  83. package/dist/src/tool-runner.d.ts +0 -35
  84. package/dist/src/walker.d.ts +0 -19
  85. package/src/asset-spec.ts +0 -85
  86. package/src/asset-type-handler.ts +0 -77
  87. package/src/cli.ts +0 -427
  88. package/src/common.ts +0 -76
  89. package/src/config-cli.ts +0 -499
  90. package/src/config.ts +0 -305
  91. package/src/db.ts +0 -411
  92. package/src/embedder.ts +0 -128
  93. package/src/frontmatter.ts +0 -95
  94. package/src/github.ts +0 -21
  95. package/src/handlers/agent-handler.ts +0 -32
  96. package/src/handlers/command-handler.ts +0 -29
  97. package/src/handlers/index.ts +0 -25
  98. package/src/handlers/knowledge-handler.ts +0 -62
  99. package/src/handlers/markdown-helpers.ts +0 -19
  100. package/src/handlers/script-handler.ts +0 -92
  101. package/src/handlers/skill-handler.ts +0 -37
  102. package/src/handlers/tool-handler.ts +0 -71
  103. package/src/indexer.ts +0 -392
  104. package/src/init.ts +0 -114
  105. package/src/llm.ts +0 -125
  106. package/src/markdown.ts +0 -106
  107. package/src/metadata.ts +0 -333
  108. package/src/origin-resolve.ts +0 -67
  109. package/src/registry-install.ts +0 -361
  110. package/src/registry-resolve.ts +0 -341
  111. package/src/registry-search.ts +0 -335
  112. package/src/registry-types.ts +0 -72
  113. package/src/ripgrep-install.ts +0 -200
  114. package/src/ripgrep-resolve.ts +0 -72
  115. package/src/ripgrep.ts +0 -3
  116. package/src/stash-add.ts +0 -63
  117. package/src/stash-clone.ts +0 -127
  118. package/src/stash-ref.ts +0 -99
  119. package/src/stash-registry.ts +0 -259
  120. package/src/stash-resolve.ts +0 -50
  121. package/src/stash-search.ts +0 -613
  122. package/src/stash-show.ts +0 -55
  123. package/src/stash-source.ts +0 -103
  124. package/src/stash-types.ts +0 -231
  125. package/src/stash.ts +0 -39
  126. package/src/tool-runner.ts +0 -142
  127. package/src/walker.ts +0 -53
  128. /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
  129. /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
  130. /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
  131. /package/dist/{src/github.js → github.js} +0 -0
  132. /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
  133. /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
  134. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  135. /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
  136. /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
  137. /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
  138. /package/dist/{src/llm.js → llm.js} +0 -0
  139. /package/dist/{src/markdown.js → markdown.js} +0 -0
  140. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  141. /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
  142. /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
  143. /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
  144. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
  145. /package/dist/{src/tool-runner.js → tool-runner.js} +0 -0
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { TYPE_DIRS } from "./asset-spec";
4
+ import { parseAssetRef, makeAssetRef } from "./stash-ref";
5
+ import { resolveSourcesForOrigin, isRemoteOrigin } from "./origin-resolve";
6
+ import { resolveAssetPath } from "./stash-resolve";
7
+ import { resolveStashSources, findSourceForPath, getPrimarySource } from "./stash-source";
8
+ import { installRegistryRef } from "./registry-install";
9
+ export async function agentikitClone(options) {
10
+ const parsed = parseAssetRef(options.sourceRef);
11
+ // When --dest is provided, the working stash is optional
12
+ let allSources;
13
+ try {
14
+ allSources = resolveStashSources();
15
+ }
16
+ catch (err) {
17
+ if (options.dest) {
18
+ allSources = [];
19
+ }
20
+ else {
21
+ throw err;
22
+ }
23
+ }
24
+ const primarySource = getPrimarySource(allSources);
25
+ const destRoot = options.dest ? path.resolve(options.dest) : primarySource?.path;
26
+ if (!destRoot) {
27
+ throw new Error("No working stash configured and no --dest provided. Run `akm init` or pass --dest.");
28
+ }
29
+ let searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
30
+ // Remote fetch fallback: if no local source matched and origin looks remote, fetch it
31
+ let remoteFetched;
32
+ if (searchSources.length === 0 && parsed.origin && isRemoteOrigin(parsed.origin, allSources)) {
33
+ const installResult = await installRegistryRef(parsed.origin);
34
+ const syntheticSource = {
35
+ path: installResult.stashRoot,
36
+ registryId: installResult.id,
37
+ };
38
+ searchSources = [syntheticSource];
39
+ allSources = [...allSources, syntheticSource];
40
+ remoteFetched = {
41
+ origin: parsed.origin,
42
+ stashRoot: installResult.stashRoot,
43
+ cacheDir: installResult.cacheDir,
44
+ };
45
+ }
46
+ let sourcePath;
47
+ let lastError;
48
+ for (const source of searchSources) {
49
+ try {
50
+ sourcePath = resolveAssetPath(source.path, parsed.type, parsed.name);
51
+ break;
52
+ }
53
+ catch (err) {
54
+ lastError = err instanceof Error ? err : new Error(String(err));
55
+ }
56
+ }
57
+ if (!sourcePath) {
58
+ const context = remoteFetched ? ` (remote package fetched but asset not found inside it)` : "";
59
+ throw lastError ?? new Error(`Source asset not found for ref: ${options.sourceRef}${context}`);
60
+ }
61
+ const sourceSource = findSourceForPath(sourcePath, allSources);
62
+ const destName = options.newName ?? parsed.name;
63
+ const typeDir = TYPE_DIRS[parsed.type];
64
+ const destLabel = options.dest ? "at destination" : "in working stash";
65
+ // Guard against self-clone
66
+ if (parsed.type === "skill") {
67
+ const sourceSkillDir = path.resolve(path.dirname(sourcePath));
68
+ const destSkillDir = path.resolve(path.join(destRoot, typeDir, destName));
69
+ if (sourceSkillDir === destSkillDir) {
70
+ throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
71
+ }
72
+ }
73
+ else {
74
+ const resolvedSource = path.resolve(sourcePath);
75
+ const resolvedDest = path.resolve(path.join(destRoot, typeDir, destName));
76
+ if (resolvedSource === resolvedDest) {
77
+ throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
78
+ }
79
+ }
80
+ let destPath;
81
+ if (parsed.type === "skill") {
82
+ const sourceSkillDir = path.dirname(sourcePath);
83
+ const destSkillDir = path.join(destRoot, typeDir, destName);
84
+ const overwritten = fs.existsSync(destSkillDir);
85
+ if (overwritten && !options.force) {
86
+ throw new Error(`Asset already exists ${destLabel}: ${destSkillDir}. Use --force to overwrite.`);
87
+ }
88
+ if (overwritten) {
89
+ fs.rmSync(destSkillDir, { recursive: true, force: true });
90
+ }
91
+ fs.cpSync(sourceSkillDir, destSkillDir, { recursive: true });
92
+ destPath = path.join(destSkillDir, "SKILL.md");
93
+ const ref = makeAssetRef(parsed.type, destName, "local");
94
+ return {
95
+ source: { path: sourcePath, registryId: sourceSource?.registryId },
96
+ destination: { path: destPath, ref },
97
+ overwritten,
98
+ ...(remoteFetched ? { remoteFetched } : {}),
99
+ };
100
+ }
101
+ destPath = path.join(destRoot, typeDir, destName);
102
+ const overwritten = fs.existsSync(destPath);
103
+ if (overwritten && !options.force) {
104
+ throw new Error(`Asset already exists ${destLabel}: ${destPath}. Use --force to overwrite.`);
105
+ }
106
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
107
+ fs.copyFileSync(sourcePath, destPath);
108
+ const ref = makeAssetRef(parsed.type, destName, "local");
109
+ return {
110
+ source: { path: sourcePath, registryId: sourceSource?.registryId },
111
+ destination: { path: destPath, ref },
112
+ overwritten,
113
+ ...(remoteFetched ? { remoteFetched } : {}),
114
+ };
115
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import { resolveStashDir } from "./common";
3
3
  import { loadConfig } from "./config";
4
4
  import { agentikitIndex } from "./indexer";
5
+ import { removeLockEntry, upsertLockEntry } from "./lockfile";
5
6
  import { installRegistryRef, removeInstalledRegistryEntry, upsertInstalledRegistryEntry, } from "./registry-install";
6
7
  import { parseRegistryRef } from "./registry-resolve";
7
8
  export async function agentikitList(input) {
@@ -29,6 +30,7 @@ export async function agentikitRemove(input) {
29
30
  const installed = config.registry?.installed ?? [];
30
31
  const entry = resolveInstalledTarget(installed, target);
31
32
  const updatedConfig = removeInstalledRegistryEntry(entry.id);
33
+ removeLockEntry(entry.id);
32
34
  cleanupDirectoryBestEffort(entry.cacheDir);
33
35
  const index = await agentikitIndex({ stashDir });
34
36
  return {
@@ -42,7 +44,7 @@ export async function agentikitRemove(input) {
42
44
  stashRoot: entry.stashRoot,
43
45
  },
44
46
  config: {
45
- mountedStashDirs: updatedConfig.mountedStashDirs,
47
+ searchPaths: updatedConfig.searchPaths,
46
48
  installedRegistryCount: updatedConfig.registry?.installed.length ?? 0,
47
49
  },
48
50
  index: {
@@ -53,56 +55,28 @@ export async function agentikitRemove(input) {
53
55
  },
54
56
  };
55
57
  }
56
- export async function agentikitReinstall(input) {
58
+ export async function agentikitUpdate(input) {
57
59
  const stashDir = input?.stashDir ?? resolveStashDir();
58
60
  const target = input?.target?.trim();
59
61
  const all = input?.all === true;
62
+ const force = input?.force === true;
60
63
  const installedEntries = loadConfig().registry?.installed ?? [];
61
64
  const selectedEntries = selectTargets(installedEntries, target, all);
62
65
  const processed = [];
63
66
  for (const entry of selectedEntries) {
64
- const installed = await installRegistryRef(entry.ref);
65
- upsertInstalledRegistryEntry(toInstalledEntry(installed));
66
- if (entry.cacheDir !== installed.cacheDir) {
67
+ if (force) {
67
68
  cleanupDirectoryBestEffort(entry.cacheDir);
68
69
  }
69
- processed.push({
70
- id: entry.id,
71
- source: entry.source,
72
- ref: entry.ref,
73
- previousCacheDir: entry.cacheDir,
74
- installed: toInstallStatus(installed),
75
- });
76
- }
77
- const index = await agentikitIndex({ stashDir });
78
- const config = loadConfig();
79
- return {
80
- stashDir,
81
- target,
82
- all,
83
- processed,
84
- config: {
85
- mountedStashDirs: config.mountedStashDirs,
86
- installedRegistryCount: config.registry?.installed.length ?? 0,
87
- },
88
- index: {
89
- mode: index.mode,
90
- totalEntries: index.totalEntries,
91
- directoriesScanned: index.directoriesScanned,
92
- directoriesSkipped: index.directoriesSkipped,
93
- },
94
- };
95
- }
96
- export async function agentikitUpdate(input) {
97
- const stashDir = input?.stashDir ?? resolveStashDir();
98
- const target = input?.target?.trim();
99
- const all = input?.all === true;
100
- const installedEntries = loadConfig().registry?.installed ?? [];
101
- const selectedEntries = selectTargets(installedEntries, target, all);
102
- const processed = [];
103
- for (const entry of selectedEntries) {
104
70
  const installed = await installRegistryRef(entry.ref);
105
71
  upsertInstalledRegistryEntry(toInstalledEntry(installed));
72
+ upsertLockEntry({
73
+ id: installed.id,
74
+ source: installed.source,
75
+ ref: installed.ref,
76
+ resolvedVersion: installed.resolvedVersion,
77
+ resolvedRevision: installed.resolvedRevision,
78
+ integrity: installed.integrity ?? (installed.source === "local" ? "local" : undefined),
79
+ });
106
80
  if (entry.cacheDir !== installed.cacheDir) {
107
81
  cleanupDirectoryBestEffort(entry.cacheDir);
108
82
  }
@@ -133,7 +107,7 @@ export async function agentikitUpdate(input) {
133
107
  all,
134
108
  processed,
135
109
  config: {
136
- mountedStashDirs: config.mountedStashDirs,
110
+ searchPaths: config.searchPaths,
137
111
  installedRegistryCount: config.registry?.installed.length ?? 0,
138
112
  },
139
113
  index: {
@@ -5,9 +5,9 @@ import { walkStash } from "./walker";
5
5
  import { makeAssetRef } from "./stash-ref";
6
6
  import { loadConfig } from "./config";
7
7
  import { searchRegistry } from "./registry-search";
8
- import { openDatabase, closeDatabase, getDbPath, getMeta, searchFts, searchVec, getAllEntries, getEntryCount, getEntryById, isVecAvailable, } from "./db";
8
+ import { openDatabase, closeDatabase, getDbPath, getMeta, searchFts, searchVec, getAllEntries, getEntryCount, getEntryById, } from "./db";
9
9
  import { tryGetHandler } from "./asset-type-handler";
10
- import { resolveStashSources, findSourceForPath } from "./stash-source";
10
+ import { resolveStashSources, findSourceForPath, isEditable, buildEditHint } from "./stash-source";
11
11
  const DEFAULT_LIMIT = 20;
12
12
  export async function agentikitSearch(input) {
13
13
  const t0 = Date.now();
@@ -18,6 +18,15 @@ export async function agentikitSearch(input) {
18
18
  const usageMode = parseSearchUsageMode(input.usage);
19
19
  const source = parseSearchSource(input.source);
20
20
  const sources = resolveStashSources();
21
+ if (sources.length === 0) {
22
+ return {
23
+ stashDir: "",
24
+ source: source ?? "all",
25
+ hits: [],
26
+ warnings: ["No stash sources configured. Run `akm init` first."],
27
+ timing: { totalMs: Date.now() - t0 },
28
+ };
29
+ }
21
30
  const stashDir = sources[0].path;
22
31
  const localResult = source === "registry"
23
32
  ? undefined
@@ -40,11 +49,12 @@ export async function agentikitSearch(input) {
40
49
  hits: localResult?.hits ?? [],
41
50
  usageGuide: localResult?.usageGuide,
42
51
  tip: localResult?.tip,
52
+ warnings: localResult?.warnings,
43
53
  timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
44
54
  };
45
55
  }
46
56
  const registryHits = (registryResult?.hits ?? []).map((hit) => {
47
- const installRef = hit.source === "npm" ? `npm:${hit.ref}` : `github:${hit.ref}`;
57
+ const installRef = hit.source === "npm" ? `npm:${hit.ref}` : hit.source === "git" ? `git+${hit.ref}` : `github:${hit.ref}`;
48
58
  return {
49
59
  hitSource: "registry",
50
60
  type: "registry",
@@ -79,7 +89,9 @@ export async function agentikitSearch(input) {
79
89
  hits: mergedHits,
80
90
  usageGuide: localResult?.usageGuide,
81
91
  tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
82
- warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
92
+ warnings: [...(localResult?.warnings ?? []), ...(registryResult?.warnings ?? [])].length
93
+ ? [...(localResult?.warnings ?? []), ...(registryResult?.warnings ?? [])]
94
+ : undefined,
83
95
  timing: { totalMs: Date.now() - t0 },
84
96
  };
85
97
  }
@@ -116,7 +128,7 @@ async function searchLocal(input) {
116
128
  console.warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
117
129
  }
118
130
  const hits = allStashDirs
119
- .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources))
131
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources, config))
120
132
  .slice(0, limit);
121
133
  const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined;
122
134
  return {
@@ -142,6 +154,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
142
154
  allStashDirs,
143
155
  sources,
144
156
  includeItemUsage: shouldIncludeItemUsage(usageMode),
157
+ config,
145
158
  }));
146
159
  return {
147
160
  hits,
@@ -157,58 +170,67 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
157
170
  const tRank0 = Date.now();
158
171
  const typeFilter = searchType === "any" ? undefined : searchType;
159
172
  const ftsResults = searchFts(db, query, limit * 3, typeFilter);
160
- // Build score map from FTS results (normalize BM25 scores)
161
- const ftsScoreMap = new Map();
162
- for (const r of ftsResults) {
163
- // BM25 returns negative scores (more negative = better match), normalize to 0-1
164
- const absScore = Math.abs(r.bm25Score);
165
- const normalized = absScore / (1 + absScore);
166
- ftsScoreMap.set(r.id, { score: normalized, result: r });
173
+ // Reciprocal Rank Fusion (RRF) constant
174
+ const RRF_K = 60;
175
+ // Build FTS rank map: rank 1 = best BM25, rank 2 = second best, etc.
176
+ // FTS results are already sorted by bm25Score (ascending, more negative = better)
177
+ const ftsRankMap = new Map();
178
+ for (let i = 0; i < ftsResults.length; i++) {
179
+ const r = ftsResults[i];
180
+ ftsRankMap.set(r.id, { rank: i + 1, result: r });
181
+ }
182
+ // Build embedding rank map: sort by cosine similarity descending
183
+ const embedRankMap = new Map();
184
+ if (embeddingScores) {
185
+ const sortedEmbeddings = [...embeddingScores.entries()].sort((a, b) => b[1] - a[1]);
186
+ for (let i = 0; i < sortedEmbeddings.length; i++) {
187
+ embedRankMap.set(sortedEmbeddings[i][0], i + 1);
188
+ }
167
189
  }
168
- // Blend scores
190
+ // Merge results using RRF
169
191
  const scored = [];
170
192
  const seenIds = new Set();
171
193
  // Process FTS results
172
- for (const [id, { score: ftsScore, result }] of ftsScoreMap) {
194
+ for (const [id, { rank, result }] of ftsRankMap) {
173
195
  seenIds.add(id);
174
- const embScore = embeddingScores?.get(id);
175
- if (embScore !== undefined) {
176
- const blended = embScore * 0.7 + ftsScore * 0.3;
177
- if (blended > 0)
178
- scored.push({ id, entry: result.entry, filePath: result.filePath, score: blended, rankingMode: "semantic" });
179
- }
180
- else if (ftsScore > 0) {
181
- scored.push({ id, entry: result.entry, filePath: result.filePath, score: ftsScore, rankingMode: "fts" });
182
- }
196
+ const ftsRrf = 1 / (RRF_K + rank);
197
+ const embedRank = embedRankMap.get(id);
198
+ const embedRrf = embedRank !== undefined ? 1 / (RRF_K + embedRank) : 0;
199
+ const rrfScore = ftsRrf + embedRrf;
200
+ const rankingMode = embedRrf > 0 ? "semantic" : "fts";
201
+ scored.push({ id, entry: result.entry, filePath: result.filePath, score: rrfScore, rankingMode });
183
202
  }
184
203
  // Add vec-only results not already in FTS results
185
204
  if (embeddingScores) {
186
- for (const [id, embScore] of embeddingScores) {
205
+ for (const [id] of embeddingScores) {
187
206
  if (seenIds.has(id))
188
207
  continue;
208
+ const embedRank = embedRankMap.get(id);
189
209
  const found = getEntryById(db, id);
190
210
  if (found) {
191
211
  if (typeFilter && found.entry.type !== typeFilter)
192
212
  continue;
213
+ const rrfScore = 1 / (RRF_K + embedRank);
193
214
  scored.push({
194
215
  id,
195
216
  entry: found.entry,
196
217
  filePath: found.filePath,
197
- score: embScore,
218
+ score: rrfScore,
198
219
  rankingMode: "semantic",
199
220
  });
200
221
  }
201
222
  }
202
223
  }
203
- // Apply boosts (tag, intent, name matches)
224
+ // Apply boosts as multiplicative factors
204
225
  const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean);
205
226
  for (const item of scored) {
206
227
  const entry = item.entry;
228
+ let boostSum = 0;
207
229
  // Tag boost
208
230
  if (entry.tags) {
209
231
  for (const tag of entry.tags) {
210
232
  if (queryTokens.some((t) => tag.toLowerCase() === t)) {
211
- item.score += 0.15;
233
+ boostSum += 0.15;
212
234
  }
213
235
  }
214
236
  }
@@ -218,7 +240,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
218
240
  const intentLower = intent.toLowerCase();
219
241
  for (const token of queryTokens) {
220
242
  if (intentLower.includes(token)) {
221
- item.score += 0.12;
243
+ boostSum += 0.12;
222
244
  break;
223
245
  }
224
246
  }
@@ -227,11 +249,9 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
227
249
  // Name boost
228
250
  const nameLower = entry.name.toLowerCase().replace(/[-_]/g, " ");
229
251
  if (queryTokens.some((t) => nameLower.includes(t))) {
230
- item.score += 0.1;
252
+ boostSum += 0.1;
231
253
  }
232
- }
233
- for (const item of scored) {
234
- item.score = Math.min(item.score, 1.0);
254
+ item.score = item.score * (1 + boostSum);
235
255
  }
236
256
  scored.sort((a, b) => b.score - a.score);
237
257
  const rankMs = Date.now() - tRank0;
@@ -246,6 +266,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
246
266
  allStashDirs,
247
267
  sources,
248
268
  includeItemUsage: shouldIncludeItemUsage(usageMode),
269
+ config,
249
270
  }));
250
271
  return {
251
272
  embedMs,
@@ -258,7 +279,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
258
279
  }
259
280
  // ── Vector scorer ───────────────────────────────────────────────────────────
260
281
  async function tryVecScores(db, query, k, config) {
261
- if (!config.semanticSearch || !isVecAvailable())
282
+ if (!config.semanticSearch)
262
283
  return null;
263
284
  const hasEmbeddings = getMeta(db, "hasEmbeddings");
264
285
  if (hasEmbeddings !== "1")
@@ -281,13 +302,13 @@ async function tryVecScores(db, query, k, config) {
281
302
  }
282
303
  }
283
304
  // ── Substring fallback (no index) ───────────────────────────────────────────
284
- function substringSearch(query, searchType, limit, stashDir, sources) {
305
+ function substringSearch(query, searchType, limit, stashDir, sources, config) {
285
306
  const assets = indexAssets(stashDir, searchType);
286
307
  return assets
287
308
  .filter((asset) => asset.name.toLowerCase().includes(query))
288
309
  .sort(compareAssets)
289
310
  .slice(0, limit)
290
- .map((asset) => assetToSearchHit(asset, stashDir, sources));
311
+ .map((asset) => assetToSearchHit(asset, stashDir, sources, config));
291
312
  }
292
313
  // ── Hit building ────────────────────────────────────────────────────────────
293
314
  function buildDbHit(input) {
@@ -297,9 +318,10 @@ function buildDbHit(input) {
297
318
  ?? input.entry.name;
298
319
  const qualityBoost = input.entry.generated === true ? 0 : 0.05;
299
320
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
300
- const score = Math.min(Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000, 1.0);
321
+ const score = Math.round(input.score * (1 + qualityBoost + confidenceBoost) * 1000) / 1000;
301
322
  const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost);
302
323
  const source = findSourceForPath(input.path, input.sources);
324
+ const editable = isEditable(input.path, input.config);
303
325
  const hit = {
304
326
  hitSource: "local",
305
327
  type: input.entry.type,
@@ -307,7 +329,8 @@ function buildDbHit(input) {
307
329
  path: input.path,
308
330
  openRef: makeAssetRef(input.entry.type, openRefName, source?.registryId),
309
331
  registryId: source?.registryId,
310
- editable: source?.writable ?? false,
332
+ editable,
333
+ ...(!editable ? { editHint: buildEditHint(input.path, input.entry.type, openRefName, source?.registryId) } : {}),
311
334
  description: input.entry.description,
312
335
  tags: input.entry.tags,
313
336
  score,
@@ -344,8 +367,9 @@ function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoos
344
367
  return reasons;
345
368
  }
346
369
  // ── Helpers ─────────────────────────────────────────────────────────────────
347
- function assetToSearchHit(asset, stashDir, sources) {
370
+ function assetToSearchHit(asset, stashDir, sources, config) {
348
371
  const source = findSourceForPath(asset.path, sources);
372
+ const editable = isEditable(asset.path, config);
349
373
  const hit = {
350
374
  hitSource: "local",
351
375
  type: asset.type,
@@ -353,7 +377,8 @@ function assetToSearchHit(asset, stashDir, sources) {
353
377
  path: asset.path,
354
378
  openRef: makeAssetRef(asset.type, asset.name, source?.registryId),
355
379
  registryId: source?.registryId,
356
- editable: source?.writable ?? false,
380
+ editable,
381
+ ...(!editable ? { editHint: buildEditHint(asset.path, asset.type, asset.name, source?.registryId) } : {}),
357
382
  };
358
383
  const handler = tryGetHandler(asset.type);
359
384
  if (handler?.enrichSearchHit) {
@@ -383,22 +408,9 @@ function parseSearchSource(source) {
383
408
  throw new Error(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`);
384
409
  }
385
410
  function mergeSearchHits(localHits, registryHits, limit) {
386
- const merged = [];
387
- let localIndex = 0;
388
- let registryIndex = 0;
389
- while (merged.length < limit && (localIndex < localHits.length || registryIndex < registryHits.length)) {
390
- if (localIndex < localHits.length) {
391
- merged.push(localHits[localIndex]);
392
- localIndex += 1;
393
- if (merged.length >= limit)
394
- break;
395
- }
396
- if (registryIndex < registryHits.length) {
397
- merged.push(registryHits[registryIndex]);
398
- registryIndex += 1;
399
- }
400
- }
401
- return merged;
411
+ const all = [...localHits, ...registryHits];
412
+ all.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
413
+ return all.slice(0, limit);
402
414
  }
403
415
  function shouldIncludeUsageGuide(mode) {
404
416
  return mode === "both" || mode === "guide";
@@ -3,9 +3,12 @@ import { parseAssetRef } from "./stash-ref";
3
3
  import { resolveSourcesForOrigin } from "./origin-resolve";
4
4
  import { resolveAssetPath } from "./stash-resolve";
5
5
  import { getHandler } from "./asset-type-handler";
6
- import { resolveStashSources, findSourceForPath } from "./stash-source";
6
+ import { resolveStashSources, findSourceForPath, isEditable, buildEditHint } from "./stash-source";
7
+ import { buildFileContext, runMatchers, getRenderer, buildRenderContext } from "./file-context";
8
+ import { loadConfig } from "./config";
7
9
  export async function agentikitShow(input) {
8
10
  const parsed = parseAssetRef(input.ref);
11
+ const config = loadConfig();
9
12
  const allSources = resolveStashSources();
10
13
  const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
11
14
  const allStashDirs = searchSources.map((s) => s.path);
@@ -28,8 +31,30 @@ export async function agentikitShow(input) {
28
31
  if (!assetPath) {
29
32
  throw lastError ?? new Error(`Stash asset not found for ref: ${parsed.type}:${parsed.name}`);
30
33
  }
31
- const content = fs.readFileSync(assetPath, "utf8");
32
34
  const source = findSourceForPath(assetPath, allSources);
35
+ const sourceStashDir = source?.path ?? allStashDirs[0];
36
+ // Try new renderer pipeline first
37
+ if (sourceStashDir) {
38
+ const fileCtx = buildFileContext(sourceStashDir, assetPath);
39
+ const match = runMatchers(fileCtx);
40
+ if (match) {
41
+ match.meta = { ...match.meta, name: parsed.name, view: input.view };
42
+ const renderer = getRenderer(match.renderer);
43
+ if (renderer) {
44
+ const renderCtx = buildRenderContext(fileCtx, match, allStashDirs);
45
+ const response = renderer.buildShowResponse(renderCtx);
46
+ const editable = isEditable(assetPath, config);
47
+ return {
48
+ ...response,
49
+ registryId: source?.registryId,
50
+ editable,
51
+ ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
52
+ };
53
+ }
54
+ }
55
+ }
56
+ // Fallback to legacy handler
57
+ const content = fs.readFileSync(assetPath, "utf8");
33
58
  const handler = getHandler(parsed.type);
34
59
  const response = handler.buildShowResponse({
35
60
  name: parsed.name,
@@ -38,9 +63,11 @@ export async function agentikitShow(input) {
38
63
  view: input.view,
39
64
  stashDirs: allStashDirs,
40
65
  });
66
+ const editable = isEditable(assetPath, config);
41
67
  return {
42
68
  ...response,
43
69
  registryId: source?.registryId,
44
- editable: source?.writable ?? false,
70
+ editable,
71
+ ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
45
72
  };
46
73
  }
@@ -4,23 +4,26 @@ import { resolveStashDir } from "./common";
4
4
  import { loadConfig } from "./config";
5
5
  // ── Resolution ──────────────────────────────────────────────────────────────
6
6
  /**
7
- * Build the ordered list of stash sources:
8
- * 1. Working stash (writable)
9
- * 2. Mounted stash dirs (read-only, user-configured)
10
- * 3. Installed stash dirs (read-only, derived from registry.installed)
7
+ * Build the ordered list of stash sources (search paths):
8
+ * 1. Primary stash dir (user's own, destination for clone)
9
+ * 2. Additional search paths (user-configured)
10
+ * 3. Installed kit paths (cache-managed, from registry)
11
+ *
12
+ * The first entry is always the primary stash. Additional entries come
13
+ * from `searchPaths` config and `registry.installed` entries.
11
14
  */
12
15
  export function resolveStashSources(overrideStashDir) {
13
16
  const stashDir = overrideStashDir ?? resolveStashDir();
14
17
  const config = loadConfig();
15
18
  const sources = [
16
- { kind: "working", path: stashDir, writable: true },
19
+ { path: stashDir },
17
20
  ];
18
- for (const dir of config.mountedStashDirs) {
21
+ for (const dir of config.searchPaths) {
19
22
  if (isSuspiciousStashRoot(dir)) {
20
23
  console.warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
21
24
  }
22
25
  if (isValidDirectory(dir)) {
23
- sources.push({ kind: "mounted", path: dir, writable: false });
26
+ sources.push({ path: dir });
24
27
  }
25
28
  }
26
29
  for (const entry of config.registry?.installed ?? []) {
@@ -29,10 +32,8 @@ export function resolveStashSources(overrideStashDir) {
29
32
  }
30
33
  if (isValidDirectory(entry.stashRoot)) {
31
34
  sources.push({
32
- kind: "installed",
33
35
  path: entry.stashRoot,
34
36
  registryId: entry.id,
35
- writable: false,
36
37
  });
37
38
  }
38
39
  }
@@ -55,6 +56,52 @@ export function findSourceForPath(filePath, sources) {
55
56
  }
56
57
  return undefined;
57
58
  }
59
+ /**
60
+ * Return the primary stash source (first entry in the list).
61
+ * This is the user's working stash and the default destination for clone.
62
+ */
63
+ export function getPrimarySource(sources) {
64
+ return sources[0];
65
+ }
66
+ // ── Editability ─────────────────────────────────────────────────────────────
67
+ /**
68
+ * Determine whether a file is safe to edit in place.
69
+ *
70
+ * The only files that are NOT editable are those inside a cache directory
71
+ * managed by the package manager (`registry.installed[].cacheDir`). These
72
+ * will be overwritten by `akm update` without warning.
73
+ *
74
+ * Everything else — working stash, search paths, local project dirs — is
75
+ * the user's domain to manage.
76
+ */
77
+ export function isEditable(filePath, config) {
78
+ const cfg = config ?? loadConfig();
79
+ const resolved = path.resolve(filePath);
80
+ const cacheManaged = cfg.registry?.installed ?? [];
81
+ const isWin = process.platform === "win32";
82
+ for (const entry of cacheManaged) {
83
+ const cacheRoot = path.resolve(entry.cacheDir);
84
+ if (isWin) {
85
+ // Windows paths are case-insensitive — normalize both sides
86
+ if (resolved.toLowerCase().startsWith(cacheRoot.toLowerCase() + path.sep))
87
+ return false;
88
+ }
89
+ else {
90
+ if (resolved.startsWith(cacheRoot + path.sep))
91
+ return false;
92
+ }
93
+ }
94
+ return true;
95
+ }
96
+ /**
97
+ * Build an actionable hint for the agent when a file is not editable.
98
+ * Callers must check `isEditable()` before calling — this function
99
+ * unconditionally returns the hint string.
100
+ */
101
+ export function buildEditHint(filePath, assetType, assetName, origin) {
102
+ const ref = origin ? `${origin}//${assetType}:${assetName}` : `${assetType}:${assetName}`;
103
+ return `This asset is managed by akm and may be overwritten on update. To edit, run: akm clone ${ref}`;
104
+ }
58
105
  // ── Validation ──────────────────────────────────────────────────────────────
59
106
  const SUSPICIOUS_ROOTS = new Set(['/', '/etc', '/bin', '/sbin', '/usr', '/var', '/tmp', '/dev', '/proc', '/sys']);
60
107
  function isSuspiciousStashRoot(dir) {