agentikit 0.0.7 → 0.0.9

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 (98) hide show
  1. package/README.md +215 -76
  2. package/dist/index.d.ts +17 -3
  3. package/dist/index.js +10 -2
  4. package/dist/src/asset-spec.d.ts +14 -0
  5. package/dist/src/asset-spec.js +46 -0
  6. package/dist/src/cli.js +268 -57
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +37 -0
  10. package/dist/src/config.js +124 -0
  11. package/dist/src/embedder.d.ts +10 -0
  12. package/dist/src/embedder.js +87 -0
  13. package/dist/src/frontmatter.d.ts +30 -0
  14. package/dist/src/frontmatter.js +86 -0
  15. package/dist/src/indexer.d.ts +20 -2
  16. package/dist/src/indexer.js +212 -80
  17. package/dist/src/init.d.ts +19 -0
  18. package/dist/src/init.js +87 -0
  19. package/dist/src/llm.d.ts +15 -0
  20. package/dist/src/llm.js +91 -0
  21. package/dist/src/markdown.d.ts +18 -0
  22. package/dist/src/markdown.js +77 -0
  23. package/dist/src/metadata.d.ts +11 -2
  24. package/dist/src/metadata.js +161 -29
  25. package/dist/src/registry-install.d.ts +11 -0
  26. package/dist/src/registry-install.js +208 -0
  27. package/dist/src/registry-resolve.d.ts +3 -0
  28. package/dist/src/registry-resolve.js +231 -0
  29. package/dist/src/registry-search.d.ts +5 -0
  30. package/dist/src/registry-search.js +129 -0
  31. package/dist/src/registry-types.d.ts +55 -0
  32. package/dist/src/registry-types.js +1 -0
  33. package/dist/src/ripgrep-install.d.ts +12 -0
  34. package/dist/src/ripgrep-install.js +169 -0
  35. package/dist/src/ripgrep-resolve.d.ts +13 -0
  36. package/dist/src/ripgrep-resolve.js +68 -0
  37. package/dist/src/ripgrep.d.ts +3 -36
  38. package/dist/src/ripgrep.js +2 -262
  39. package/dist/src/similarity.d.ts +1 -2
  40. package/dist/src/similarity.js +11 -0
  41. package/dist/src/stash-add.d.ts +4 -0
  42. package/dist/src/stash-add.js +59 -0
  43. package/dist/src/stash-ref.d.ts +7 -0
  44. package/dist/src/stash-ref.js +33 -0
  45. package/dist/src/stash-registry.d.ts +18 -0
  46. package/dist/src/stash-registry.js +221 -0
  47. package/dist/src/stash-resolve.d.ts +2 -0
  48. package/dist/src/stash-resolve.js +45 -0
  49. package/dist/src/stash-search.d.ts +8 -0
  50. package/dist/src/stash-search.js +484 -0
  51. package/dist/src/stash-show.d.ts +5 -0
  52. package/dist/src/stash-show.js +114 -0
  53. package/dist/src/stash-types.d.ts +217 -0
  54. package/dist/src/stash-types.js +1 -0
  55. package/dist/src/stash.d.ts +10 -63
  56. package/dist/src/stash.js +6 -633
  57. package/dist/src/tool-runner.d.ts +35 -0
  58. package/dist/src/tool-runner.js +100 -0
  59. package/dist/src/walker.d.ts +19 -0
  60. package/dist/src/walker.js +47 -0
  61. package/package.json +8 -14
  62. package/src/asset-spec.ts +69 -0
  63. package/src/cli.ts +282 -46
  64. package/src/common.ts +58 -0
  65. package/src/config.ts +183 -0
  66. package/src/embedder.ts +117 -0
  67. package/src/frontmatter.ts +95 -0
  68. package/src/indexer.ts +244 -84
  69. package/src/init.ts +106 -0
  70. package/src/llm.ts +124 -0
  71. package/src/markdown.ts +106 -0
  72. package/src/metadata.ts +171 -27
  73. package/src/registry-install.ts +245 -0
  74. package/src/registry-resolve.ts +272 -0
  75. package/src/registry-search.ts +145 -0
  76. package/src/registry-types.ts +64 -0
  77. package/src/ripgrep-install.ts +200 -0
  78. package/src/ripgrep-resolve.ts +72 -0
  79. package/src/ripgrep.ts +3 -315
  80. package/src/similarity.ts +13 -1
  81. package/src/stash-add.ts +66 -0
  82. package/src/stash-ref.ts +41 -0
  83. package/src/stash-registry.ts +259 -0
  84. package/src/stash-resolve.ts +47 -0
  85. package/src/stash-search.ts +595 -0
  86. package/src/stash-show.ts +112 -0
  87. package/src/stash-types.ts +221 -0
  88. package/src/stash.ts +31 -760
  89. package/src/tool-runner.ts +129 -0
  90. package/src/walker.ts +53 -0
  91. package/.claude-plugin/plugin.json +0 -21
  92. package/commands/open.md +0 -11
  93. package/commands/run.md +0 -11
  94. package/commands/search.md +0 -11
  95. package/dist/src/plugin.d.ts +0 -2
  96. package/dist/src/plugin.js +0 -55
  97. package/skills/stash/SKILL.md +0 -73
  98. package/src/plugin.ts +0 -56
package/dist/src/cli.js CHANGED
@@ -1,62 +1,273 @@
1
1
  #!/usr/bin/env node
2
- import { agentikitSearch, agentikitOpen, agentikitRun, agentikitInit } from "./stash";
2
+ import { defineCommand, runMain } from "citty";
3
+ import { agentikitAdd, agentikitList, agentikitReinstall, agentikitRemove, agentikitSearch, agentikitShow, agentikitUpdate, } from "./stash";
4
+ import { agentikitInit } from "./init";
3
5
  import { agentikitIndex } from "./indexer";
4
- const args = process.argv.slice(2);
5
- const command = args[0];
6
- function flag(name) {
7
- const idx = args.indexOf(name);
8
- return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
6
+ import { loadConfig, updateConfig } from "./config";
7
+ import { resolveStashDir } from "./common";
8
+ const initCommand = defineCommand({
9
+ meta: { name: "init", description: "Initialize agentikit stash directory and set AGENTIKIT_STASH_DIR" },
10
+ run() {
11
+ return runWithJsonErrors(() => {
12
+ const result = agentikitInit();
13
+ console.log(JSON.stringify(result, null, 2));
14
+ });
15
+ },
16
+ });
17
+ const indexCommand = defineCommand({
18
+ meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
19
+ args: {
20
+ full: { type: "boolean", description: "Force full reindex", default: false },
21
+ },
22
+ async run({ args }) {
23
+ await runWithJsonErrors(async () => {
24
+ const result = await agentikitIndex({ full: args.full });
25
+ console.log(JSON.stringify(result, null, 2));
26
+ });
27
+ },
28
+ });
29
+ const searchCommand = defineCommand({
30
+ meta: { name: "search", description: "Search the stash" },
31
+ args: {
32
+ query: { type: "positional", description: "Search query", required: false, default: "" },
33
+ type: { type: "string", description: "Asset type filter (tool|skill|command|agent|knowledge|any)" },
34
+ limit: { type: "string", description: "Maximum number of results" },
35
+ usage: { type: "string", description: "Usage metadata mode (none|both|item|guide)", default: "both" },
36
+ source: { type: "string", description: "Search source (local|registry|both)", default: "local" },
37
+ },
38
+ async run({ args }) {
39
+ await runWithJsonErrors(async () => {
40
+ const type = args.type;
41
+ const limit = args.limit ? parseInt(args.limit, 10) : undefined;
42
+ const usage = parseSearchUsageMode(args.usage);
43
+ const source = parseSearchSource(args.source);
44
+ console.log(JSON.stringify(await agentikitSearch({ query: args.query, type, limit, usage, source }), null, 2));
45
+ });
46
+ },
47
+ });
48
+ const addCommand = defineCommand({
49
+ meta: { name: "add", description: "Install a registry package into the stash" },
50
+ args: {
51
+ ref: { type: "positional", description: "Registry ref (npm package, owner/repo, or github URL)", required: true },
52
+ },
53
+ async run({ args }) {
54
+ await runWithJsonErrors(async () => {
55
+ console.log(JSON.stringify(await agentikitAdd({ ref: args.ref }), null, 2));
56
+ });
57
+ },
58
+ });
59
+ const listCommand = defineCommand({
60
+ meta: { name: "list", description: "List installed registry packages from config" },
61
+ async run() {
62
+ await runWithJsonErrors(async () => {
63
+ console.log(JSON.stringify(await agentikitList(), null, 2));
64
+ });
65
+ },
66
+ });
67
+ const removeCommand = defineCommand({
68
+ meta: { name: "remove", description: "Remove an installed registry package by id or ref" },
69
+ args: {
70
+ target: { type: "positional", description: "Installed target (id or ref)", required: true },
71
+ },
72
+ async run({ args }) {
73
+ await runWithJsonErrors(async () => {
74
+ console.log(JSON.stringify(await agentikitRemove({ target: args.target }), null, 2));
75
+ });
76
+ },
77
+ });
78
+ const updateCommand = defineCommand({
79
+ meta: { name: "update", description: "Update one or all installed registry packages" },
80
+ args: {
81
+ target: { type: "positional", description: "Installed target (id or ref)", required: false },
82
+ all: { type: "boolean", description: "Update all installed entries", default: false },
83
+ },
84
+ async run({ args }) {
85
+ await runWithJsonErrors(async () => {
86
+ console.log(JSON.stringify(await agentikitUpdate({ target: args.target, all: args.all }), null, 2));
87
+ });
88
+ },
89
+ });
90
+ const reinstallCommand = defineCommand({
91
+ meta: { name: "reinstall", description: "Reinstall one or all installed registry packages" },
92
+ args: {
93
+ target: { type: "positional", description: "Installed target (id or ref)", required: false },
94
+ all: { type: "boolean", description: "Reinstall all installed entries", default: false },
95
+ },
96
+ async run({ args }) {
97
+ await runWithJsonErrors(async () => {
98
+ console.log(JSON.stringify(await agentikitReinstall({ target: args.target, all: args.all }), null, 2));
99
+ });
100
+ },
101
+ });
102
+ const showCommand = defineCommand({
103
+ meta: { name: "show", description: "Show a stash asset by ref (e.g. agent:bunjs-typescript-coder.md)" },
104
+ args: {
105
+ ref: { type: "positional", description: "Asset ref (type:name)", required: true },
106
+ view: { type: "string", description: "Knowledge view mode (full|toc|frontmatter|section|lines)" },
107
+ heading: { type: "string", description: "Section heading (for --view section)" },
108
+ start: { type: "string", description: "Start line (for --view lines)" },
109
+ end: { type: "string", description: "End line (for --view lines)" },
110
+ },
111
+ run({ args }) {
112
+ return runWithJsonErrors(() => {
113
+ let view;
114
+ if (args.view) {
115
+ switch (args.view) {
116
+ case "section":
117
+ view = { mode: "section", heading: args.heading ?? "" };
118
+ break;
119
+ case "lines":
120
+ view = {
121
+ mode: "lines",
122
+ start: Number(args.start ?? "1"),
123
+ end: args.end ? parseInt(args.end, 10) : Number.MAX_SAFE_INTEGER,
124
+ };
125
+ break;
126
+ case "toc":
127
+ case "frontmatter":
128
+ case "full":
129
+ view = { mode: args.view };
130
+ break;
131
+ default:
132
+ throw new Error(`Unknown view mode: ${args.view}. Expected one of: full|toc|frontmatter|section|lines`);
133
+ }
134
+ }
135
+ console.log(JSON.stringify(agentikitShow({ ref: args.ref, view }), null, 2));
136
+ });
137
+ },
138
+ });
139
+ const configCommand = defineCommand({
140
+ meta: { name: "config", description: "Show or update configuration" },
141
+ args: {
142
+ set: { type: "string", description: "Update a config key (key=value format)" },
143
+ },
144
+ run({ args }) {
145
+ return runWithJsonErrors(() => {
146
+ const stashDir = resolveStashDir();
147
+ if (args.set) {
148
+ const eqIndex = args.set.indexOf("=");
149
+ if (eqIndex === -1) {
150
+ throw new Error("--set expects key=value format");
151
+ }
152
+ const key = args.set.slice(0, eqIndex);
153
+ const value = args.set.slice(eqIndex + 1);
154
+ const partial = parseConfigValue(key, value);
155
+ const config = updateConfig(partial, stashDir);
156
+ console.log(JSON.stringify(config, null, 2));
157
+ }
158
+ else {
159
+ const config = loadConfig(stashDir);
160
+ console.log(JSON.stringify(config, null, 2));
161
+ }
162
+ });
163
+ },
164
+ });
165
+ const main = defineCommand({
166
+ meta: {
167
+ name: "akm",
168
+ description: "CLI tool to search, open, and run extension assets from an agentikit stash directory.",
169
+ },
170
+ subCommands: {
171
+ init: initCommand,
172
+ index: indexCommand,
173
+ add: addCommand,
174
+ list: listCommand,
175
+ remove: removeCommand,
176
+ update: updateCommand,
177
+ reinstall: reinstallCommand,
178
+ search: searchCommand,
179
+ show: showCommand,
180
+ config: configCommand,
181
+ },
182
+ });
183
+ runMain(main);
184
+ const SEARCH_USAGE_MODES = ["none", "both", "item", "guide"];
185
+ const SEARCH_SOURCES = ["local", "registry", "both"];
186
+ function parseSearchUsageMode(value) {
187
+ if (SEARCH_USAGE_MODES.includes(value))
188
+ return value;
189
+ throw new Error(`Invalid value for --usage: ${value}. Expected one of: ${SEARCH_USAGE_MODES.join("|")}`);
9
190
  }
10
- function usage() {
11
- console.error("Usage: agentikit <init|search|open|run> [options]");
12
- console.error("");
13
- console.error("Commands:");
14
- console.error(" init Initialize agentikit stash directory and set AGENTIKIT_STASH_DIR");
15
- console.error(" index Build search index with metadata generation");
16
- console.error(" search [query] Search the stash (--type tool|skill|command|agent|any) (--limit N)");
17
- console.error(" open <type:name> Open a stash asset by ref");
18
- console.error(" run <type:name> Run a tool by ref");
19
- process.exit(1);
191
+ function parseSearchSource(value) {
192
+ if (SEARCH_SOURCES.includes(value))
193
+ return value;
194
+ throw new Error(`Invalid value for --source: ${value}. Expected one of: ${SEARCH_SOURCES.join("|")}`);
20
195
  }
21
- switch (command) {
22
- case "init": {
23
- const result = agentikitInit();
24
- console.log(JSON.stringify(result, null, 2));
25
- break;
26
- }
27
- case "index": {
28
- const result = agentikitIndex();
29
- console.log(JSON.stringify(result, null, 2));
30
- break;
31
- }
32
- case "search": {
33
- const query = args.find((a, i) => i > 0 && !a.startsWith("--") && args[i - 1] !== "--type" && args[i - 1] !== "--limit") ?? "";
34
- const type = flag("--type");
35
- const limitStr = flag("--limit");
36
- const limit = limitStr ? parseInt(limitStr, 10) : undefined;
37
- console.log(JSON.stringify(agentikitSearch({ query, type, limit }), null, 2));
38
- break;
39
- }
40
- case "open": {
41
- const ref = args[1];
42
- if (!ref) {
43
- console.error("Error: missing ref argument\n");
44
- usage();
45
- }
46
- console.log(JSON.stringify(agentikitOpen({ ref }), null, 2));
47
- break;
48
- }
49
- case "run": {
50
- const ref = args[1];
51
- if (!ref) {
52
- console.error("Error: missing ref argument\n");
53
- usage();
54
- }
55
- const result = agentikitRun({ ref });
56
- console.log(JSON.stringify(result, null, 2));
57
- process.exit(result.exitCode);
58
- break;
59
- }
60
- default:
61
- usage();
196
+ function parseConnectionValue(key, value, exampleEndpoint, exampleModel) {
197
+ if (value === "null" || value === "")
198
+ return undefined;
199
+ let parsed;
200
+ try {
201
+ parsed = JSON.parse(value);
202
+ }
203
+ catch {
204
+ throw new Error(`Invalid value for ${key}: expected JSON object with endpoint and model`
205
+ + ` (e.g. '{"endpoint":"${exampleEndpoint}","model":"${exampleModel}"}')`);
206
+ }
207
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
208
+ throw new Error(`Invalid value for ${key}: expected a JSON object`);
209
+ }
210
+ const obj = parsed;
211
+ if (typeof obj.endpoint !== "string" || !obj.endpoint || typeof obj.model !== "string" || !obj.model) {
212
+ throw new Error(`Invalid value for ${key}: "endpoint" and "model" are required string fields`);
213
+ }
214
+ const result = {
215
+ endpoint: obj.endpoint,
216
+ model: obj.model,
217
+ };
218
+ if (typeof obj.apiKey === "string" && obj.apiKey) {
219
+ result.apiKey = obj.apiKey;
220
+ }
221
+ return result;
222
+ }
223
+ function parseConfigValue(key, value) {
224
+ switch (key) {
225
+ case "semanticSearch":
226
+ if (value !== "true" && value !== "false") {
227
+ throw new Error(`Invalid value for semanticSearch: expected "true" or "false"`);
228
+ }
229
+ return { semanticSearch: value === "true" };
230
+ case "additionalStashDirs":
231
+ try {
232
+ const parsed = JSON.parse(value);
233
+ if (!Array.isArray(parsed))
234
+ throw new Error("expected JSON array");
235
+ return { additionalStashDirs: parsed.filter((d) => typeof d === "string") };
236
+ }
237
+ catch {
238
+ throw new Error(`Invalid value for additionalStashDirs: expected JSON array (e.g. '["/path/a","/path/b"]')`);
239
+ }
240
+ case "embedding":
241
+ return { embedding: parseConnectionValue("embedding", value, "http://localhost:11434/v1/embeddings", "nomic-embed-text") };
242
+ case "llm":
243
+ return { llm: parseConnectionValue("llm", value, "http://localhost:11434/v1/chat/completions", "llama3.2") };
244
+ default:
245
+ throw new Error(`Unknown config key: ${key}`);
246
+ }
247
+ }
248
+ async function runWithJsonErrors(fn) {
249
+ try {
250
+ await fn();
251
+ }
252
+ catch (error) {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ const hint = buildHint(message);
255
+ console.error(JSON.stringify({ ok: false, error: message, hint }, null, 2));
256
+ process.exit(1);
257
+ }
258
+ }
259
+ function buildHint(message) {
260
+ if (message.includes("AGENTIKIT_STASH_DIR"))
261
+ return "Run `akm init` or set AGENTIKIT_STASH_DIR to a valid directory.";
262
+ if (message.includes("Either <target> or --all is required"))
263
+ return "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg`.";
264
+ if (message.includes("Specify either <target> or --all"))
265
+ return "Use only one: a positional target or `--all`.";
266
+ if (message.includes("No installed registry entry matched target"))
267
+ return "Run `akm list` to view installed ids/refs, then retry with one of those values.";
268
+ if (message.includes("Invalid value for --source"))
269
+ return "Pick one of: local, registry, both.";
270
+ if (message.includes("Invalid value for --usage"))
271
+ return "Pick one of: none, both, item, guide.";
272
+ return undefined;
62
273
  }
@@ -0,0 +1,8 @@
1
+ export type AgentikitAssetType = "tool" | "skill" | "command" | "agent" | "knowledge";
2
+ export declare const IS_WINDOWS: boolean;
3
+ export { SCRIPT_EXTENSIONS, TYPE_DIRS } from "./asset-spec";
4
+ export declare function isAssetType(type: string): type is AgentikitAssetType;
5
+ export declare function resolveStashDir(): string;
6
+ export declare function toPosix(input: string): string;
7
+ export declare function hasErrnoCode(error: unknown, code: string): boolean;
8
+ export declare function isWithin(candidate: string, root: string): boolean;
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { TYPE_DIRS } from "./asset-spec";
4
+ // ── Constants ───────────────────────────────────────────────────────────────
5
+ export const IS_WINDOWS = process.platform === "win32";
6
+ export { SCRIPT_EXTENSIONS, TYPE_DIRS } from "./asset-spec";
7
+ // ── Validators ──────────────────────────────────────────────────────────────
8
+ export function isAssetType(type) {
9
+ return type in TYPE_DIRS;
10
+ }
11
+ // ── Utilities ───────────────────────────────────────────────────────────────
12
+ export function resolveStashDir() {
13
+ const raw = process.env.AGENTIKIT_STASH_DIR?.trim();
14
+ if (!raw) {
15
+ throw new Error("AGENTIKIT_STASH_DIR is not set. Set it to your Agentikit stash path.");
16
+ }
17
+ const stashDir = path.resolve(raw);
18
+ let stat;
19
+ try {
20
+ stat = fs.statSync(stashDir);
21
+ }
22
+ catch {
23
+ throw new Error(`Unable to read AGENTIKIT_STASH_DIR at "${stashDir}".`);
24
+ }
25
+ if (!stat.isDirectory()) {
26
+ throw new Error(`AGENTIKIT_STASH_DIR must point to a directory: "${stashDir}".`);
27
+ }
28
+ return stashDir;
29
+ }
30
+ export function toPosix(input) {
31
+ return input.replace(/\\/g, "/");
32
+ }
33
+ export function hasErrnoCode(error, code) {
34
+ if (typeof error !== "object" || error === null || !("code" in error))
35
+ return false;
36
+ return error.code === code;
37
+ }
38
+ export function isWithin(candidate, root) {
39
+ const normalizedRoot = normalizeFsPathForComparison(path.resolve(root));
40
+ const normalizedCandidate = normalizeFsPathForComparison(path.resolve(candidate));
41
+ const rel = path.relative(normalizedRoot, normalizedCandidate);
42
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
43
+ }
44
+ function normalizeFsPathForComparison(value) {
45
+ return process.platform === "win32" ? value.toLowerCase() : value;
46
+ }
@@ -0,0 +1,37 @@
1
+ import type { RegistryInstalledEntry } from "./registry-types";
2
+ export interface EmbeddingConnectionConfig {
3
+ /** OpenAI-compatible embeddings endpoint (e.g. "http://localhost:11434/v1/embeddings") */
4
+ endpoint: string;
5
+ /** Model name to use for embeddings (e.g. "nomic-embed-text") */
6
+ model: string;
7
+ /** Optional API key for authenticated endpoints */
8
+ apiKey?: string;
9
+ }
10
+ export interface LlmConnectionConfig {
11
+ /** OpenAI-compatible chat completions endpoint (e.g. "http://localhost:11434/v1/chat/completions") */
12
+ endpoint: string;
13
+ /** Model name to use (e.g. "llama3.2") */
14
+ model: string;
15
+ /** Optional API key for authenticated endpoints */
16
+ apiKey?: string;
17
+ }
18
+ export interface AgentikitConfig {
19
+ /** Whether semantic search is enabled. Default: true */
20
+ semanticSearch: boolean;
21
+ /** Additional stash directories to search alongside the primary one */
22
+ additionalStashDirs: string[];
23
+ /** OpenAI-compatible embedding endpoint config. If not set, uses local @xenova/transformers */
24
+ embedding?: EmbeddingConnectionConfig;
25
+ /** OpenAI-compatible LLM endpoint config for metadata generation. If not set, uses heuristic generation */
26
+ llm?: LlmConnectionConfig;
27
+ /** Installed registry sources and local cache metadata */
28
+ registry?: RegistryConfig;
29
+ }
30
+ export interface RegistryConfig {
31
+ installed: RegistryInstalledEntry[];
32
+ }
33
+ export declare const DEFAULT_CONFIG: AgentikitConfig;
34
+ export declare function getConfigPath(stashDir: string): string;
35
+ export declare function loadConfig(stashDir?: string): AgentikitConfig;
36
+ export declare function saveConfig(config: AgentikitConfig, stashDir?: string): void;
37
+ export declare function updateConfig(partial: Partial<AgentikitConfig>, stashDir?: string): AgentikitConfig;
@@ -0,0 +1,124 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveStashDir } from "./common";
4
+ // ── Defaults ────────────────────────────────────────────────────────────────
5
+ export const DEFAULT_CONFIG = {
6
+ semanticSearch: true,
7
+ additionalStashDirs: [],
8
+ };
9
+ // ── Paths ───────────────────────────────────────────────────────────────────
10
+ export function getConfigPath(stashDir) {
11
+ return path.join(stashDir, "config.json");
12
+ }
13
+ // ── Load / Save / Update ────────────────────────────────────────────────────
14
+ export function loadConfig(stashDir) {
15
+ const dir = stashDir ?? resolveStashDir();
16
+ const configPath = getConfigPath(dir);
17
+ let raw;
18
+ try {
19
+ raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
20
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
21
+ return { ...DEFAULT_CONFIG };
22
+ }
23
+ }
24
+ catch {
25
+ return { ...DEFAULT_CONFIG };
26
+ }
27
+ return pickKnownKeys(raw);
28
+ }
29
+ export function saveConfig(config, stashDir) {
30
+ const dir = stashDir ?? resolveStashDir();
31
+ const configPath = getConfigPath(dir);
32
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
33
+ }
34
+ export function updateConfig(partial, stashDir) {
35
+ const dir = stashDir ?? resolveStashDir();
36
+ const current = loadConfig(dir);
37
+ const merged = { ...current, ...partial };
38
+ saveConfig(merged, dir);
39
+ return merged;
40
+ }
41
+ // ── Helpers ─────────────────────────────────────────────────────────────────
42
+ function pickKnownKeys(raw) {
43
+ const config = { ...DEFAULT_CONFIG };
44
+ if (typeof raw.semanticSearch === "boolean") {
45
+ config.semanticSearch = raw.semanticSearch;
46
+ }
47
+ if (Array.isArray(raw.additionalStashDirs)) {
48
+ config.additionalStashDirs = raw.additionalStashDirs.filter((d) => typeof d === "string");
49
+ }
50
+ const embedding = parseConnectionConfig(raw.embedding);
51
+ if (embedding)
52
+ config.embedding = embedding;
53
+ const llm = parseConnectionConfig(raw.llm);
54
+ if (llm)
55
+ config.llm = llm;
56
+ const registry = parseRegistryConfig(raw.registry);
57
+ if (registry)
58
+ config.registry = registry;
59
+ return config;
60
+ }
61
+ function parseConnectionConfig(value) {
62
+ if (typeof value !== "object" || value === null || Array.isArray(value))
63
+ return undefined;
64
+ const obj = value;
65
+ if (typeof obj.endpoint !== "string" || !obj.endpoint)
66
+ return undefined;
67
+ if (typeof obj.model !== "string" || !obj.model)
68
+ return undefined;
69
+ const result = {
70
+ endpoint: obj.endpoint,
71
+ model: obj.model,
72
+ };
73
+ if (typeof obj.apiKey === "string" && obj.apiKey) {
74
+ result.apiKey = obj.apiKey;
75
+ }
76
+ return result;
77
+ }
78
+ function parseRegistryConfig(value) {
79
+ if (typeof value !== "object" || value === null || Array.isArray(value))
80
+ return undefined;
81
+ const obj = value;
82
+ if (!Array.isArray(obj.installed))
83
+ return undefined;
84
+ const installed = obj.installed
85
+ .map((entry) => parseRegistryInstalledEntry(entry))
86
+ .filter((entry) => entry !== undefined);
87
+ return { installed };
88
+ }
89
+ function parseRegistryInstalledEntry(value) {
90
+ if (typeof value !== "object" || value === null || Array.isArray(value))
91
+ return undefined;
92
+ const obj = value;
93
+ const id = asNonEmptyString(obj.id);
94
+ const source = asRegistrySource(obj.source);
95
+ const ref = asNonEmptyString(obj.ref);
96
+ const artifactUrl = asNonEmptyString(obj.artifactUrl);
97
+ const stashRoot = asNonEmptyString(obj.stashRoot);
98
+ const cacheDir = asNonEmptyString(obj.cacheDir);
99
+ const installedAt = asNonEmptyString(obj.installedAt);
100
+ if (!id || !source || !ref || !artifactUrl || !stashRoot || !cacheDir || !installedAt)
101
+ return undefined;
102
+ const entry = {
103
+ id,
104
+ source,
105
+ ref,
106
+ artifactUrl,
107
+ stashRoot,
108
+ cacheDir,
109
+ installedAt,
110
+ };
111
+ const resolvedVersion = asNonEmptyString(obj.resolvedVersion);
112
+ if (resolvedVersion)
113
+ entry.resolvedVersion = resolvedVersion;
114
+ const resolvedRevision = asNonEmptyString(obj.resolvedRevision);
115
+ if (resolvedRevision)
116
+ entry.resolvedRevision = resolvedRevision;
117
+ return entry;
118
+ }
119
+ function asNonEmptyString(value) {
120
+ return typeof value === "string" && value ? value : undefined;
121
+ }
122
+ function asRegistrySource(value) {
123
+ return value === "npm" || value === "github" ? value : undefined;
124
+ }
@@ -0,0 +1,10 @@
1
+ import type { EmbeddingConnectionConfig } from "./config";
2
+ export type EmbeddingVector = number[];
3
+ /**
4
+ * Generate an embedding for the given text.
5
+ * If embeddingConfig is provided, uses the configured OpenAI-compatible endpoint.
6
+ * Otherwise falls back to local @xenova/transformers.
7
+ */
8
+ export declare function embed(text: string, embeddingConfig?: EmbeddingConnectionConfig): Promise<EmbeddingVector>;
9
+ export declare function cosineSimilarity(a: EmbeddingVector, b: EmbeddingVector): number;
10
+ export declare function isEmbeddingAvailable(embeddingConfig?: EmbeddingConnectionConfig): Promise<boolean>;
@@ -0,0 +1,87 @@
1
+ // ── Singleton local embedder ────────────────────────────────────────────────
2
+ let localEmbedder;
3
+ async function getLocalEmbedder() {
4
+ if (!localEmbedder) {
5
+ let pipeline;
6
+ try {
7
+ const mod = await import("@xenova/transformers");
8
+ pipeline = mod.pipeline;
9
+ }
10
+ catch {
11
+ throw new Error("Semantic search requires @xenova/transformers. Install it with: npm install @xenova/transformers");
12
+ }
13
+ localEmbedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
14
+ }
15
+ return localEmbedder;
16
+ }
17
+ async function embedLocal(text) {
18
+ const model = await getLocalEmbedder();
19
+ const result = await model(text, { pooling: "mean", normalize: true });
20
+ return Array.from(result.data);
21
+ }
22
+ // ── OpenAI-compatible remote embedder ───────────────────────────────────────
23
+ async function embedRemote(text, config) {
24
+ const headers = { "Content-Type": "application/json" };
25
+ if (config.apiKey) {
26
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
27
+ }
28
+ const response = await fetch(config.endpoint, {
29
+ method: "POST",
30
+ headers,
31
+ body: JSON.stringify({
32
+ input: text,
33
+ model: config.model,
34
+ }),
35
+ });
36
+ if (!response.ok) {
37
+ const body = await response.text().catch(() => "");
38
+ throw new Error(`Embedding request failed (${response.status}): ${body}`);
39
+ }
40
+ const json = (await response.json());
41
+ if (!json.data?.[0]?.embedding) {
42
+ throw new Error("Unexpected embedding response format: missing data[0].embedding");
43
+ }
44
+ return json.data[0].embedding;
45
+ }
46
+ // ── Public API ──────────────────────────────────────────────────────────────
47
+ /**
48
+ * Generate an embedding for the given text.
49
+ * If embeddingConfig is provided, uses the configured OpenAI-compatible endpoint.
50
+ * Otherwise falls back to local @xenova/transformers.
51
+ */
52
+ export async function embed(text, embeddingConfig) {
53
+ if (embeddingConfig) {
54
+ return embedRemote(text, embeddingConfig);
55
+ }
56
+ return embedLocal(text);
57
+ }
58
+ // ── Similarity ──────────────────────────────────────────────────────────────
59
+ export function cosineSimilarity(a, b) {
60
+ const len = Math.min(a.length, b.length);
61
+ if (len === 0)
62
+ return 0;
63
+ let dot = 0;
64
+ for (let i = 0; i < len; i++) {
65
+ dot += a[i] * b[i];
66
+ }
67
+ return dot;
68
+ }
69
+ // ── Availability check ──────────────────────────────────────────────────────
70
+ export async function isEmbeddingAvailable(embeddingConfig) {
71
+ if (embeddingConfig) {
72
+ try {
73
+ await embedRemote("test", embeddingConfig);
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ try {
81
+ await getLocalEmbedder();
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }