agentikit 0.0.8 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +135 -117
  2. package/dist/index.d.ts +13 -3
  3. package/dist/index.js +7 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +335 -100
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +25 -6
  14. package/dist/src/config.js +188 -28
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +1 -1
  42. package/dist/src/metadata.js +22 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +11 -0
  46. package/dist/src/registry-install.js +315 -0
  47. package/dist/src/registry-resolve.d.ts +3 -0
  48. package/dist/src/registry-resolve.js +299 -0
  49. package/dist/src/registry-search.d.ts +27 -0
  50. package/dist/src/registry-search.js +263 -0
  51. package/dist/src/registry-types.d.ts +62 -0
  52. package/dist/src/registry-types.js +1 -0
  53. package/dist/src/stash-add.d.ts +4 -0
  54. package/dist/src/stash-add.js +59 -0
  55. package/dist/src/stash-clone.d.ts +22 -0
  56. package/dist/src/stash-clone.js +83 -0
  57. package/dist/src/stash-ref.d.ts +27 -3
  58. package/dist/src/stash-ref.js +63 -24
  59. package/dist/src/stash-registry.d.ts +18 -0
  60. package/dist/src/stash-registry.js +221 -0
  61. package/dist/src/stash-resolve.js +3 -0
  62. package/dist/src/stash-search.d.ts +3 -1
  63. package/dist/src/stash-search.js +357 -138
  64. package/dist/src/stash-show.d.ts +1 -1
  65. package/dist/src/stash-show.js +28 -89
  66. package/dist/src/stash-source.d.ts +24 -0
  67. package/dist/src/stash-source.js +81 -0
  68. package/dist/src/stash-types.d.ts +175 -1
  69. package/dist/src/stash.d.ts +9 -1
  70. package/dist/src/stash.js +5 -0
  71. package/dist/src/tool-runner.d.ts +1 -1
  72. package/dist/src/tool-runner.js +18 -5
  73. package/package.json +7 -2
  74. package/src/asset-spec.ts +20 -4
  75. package/src/asset-type-handler.ts +77 -0
  76. package/src/cli.ts +354 -103
  77. package/src/common.ts +23 -5
  78. package/src/config-cli.ts +499 -0
  79. package/src/config.ts +218 -37
  80. package/src/db.ts +411 -0
  81. package/src/embedder.ts +22 -11
  82. package/src/github.ts +21 -0
  83. package/src/handlers/agent-handler.ts +32 -0
  84. package/src/handlers/command-handler.ts +29 -0
  85. package/src/handlers/index.ts +25 -0
  86. package/src/handlers/knowledge-handler.ts +62 -0
  87. package/src/handlers/markdown-helpers.ts +19 -0
  88. package/src/handlers/script-handler.ts +92 -0
  89. package/src/handlers/skill-handler.ts +37 -0
  90. package/src/handlers/tool-handler.ts +71 -0
  91. package/src/indexer.ts +208 -187
  92. package/src/init.ts +17 -9
  93. package/src/llm.ts +4 -3
  94. package/src/metadata.ts +21 -65
  95. package/src/origin-resolve.ts +67 -0
  96. package/src/registry-install.ts +361 -0
  97. package/src/registry-resolve.ts +341 -0
  98. package/src/registry-search.ts +335 -0
  99. package/src/registry-types.ts +72 -0
  100. package/src/stash-add.ts +63 -0
  101. package/src/stash-clone.ts +127 -0
  102. package/src/stash-ref.ts +84 -26
  103. package/src/stash-registry.ts +259 -0
  104. package/src/stash-resolve.ts +3 -0
  105. package/src/stash-search.ts +425 -155
  106. package/src/stash-show.ts +33 -82
  107. package/src/stash-source.ts +103 -0
  108. package/src/stash-types.ts +186 -1
  109. package/src/stash.ts +23 -0
  110. package/src/tool-runner.ts +18 -5
  111. package/dist/src/similarity.d.ts +0 -34
  112. package/src/similarity.ts +0 -271
@@ -0,0 +1,27 @@
1
+ import type { RegistrySearchResponse } from "./registry-types";
2
+ export interface RegistryIndex {
3
+ version: number;
4
+ updatedAt: string;
5
+ kits: RegistryKitEntry[];
6
+ }
7
+ export interface RegistryKitEntry {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ ref: string;
12
+ source: "npm" | "github" | "git";
13
+ homepage?: string;
14
+ tags?: string[];
15
+ assetTypes?: string[];
16
+ author?: string;
17
+ license?: string;
18
+ latestVersion?: string;
19
+ /** Whether this entry was manually reviewed and approved */
20
+ curated?: boolean;
21
+ }
22
+ export interface RegistrySearchOptions {
23
+ limit?: number;
24
+ /** Override registry URL(s). Accepts a single URL or an array. */
25
+ registryUrls?: string | string[];
26
+ }
27
+ export declare function searchRegistry(query: string, options?: RegistrySearchOptions): Promise<RegistrySearchResponse>;
@@ -0,0 +1,263 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fetchWithTimeout } from "./common";
4
+ // ── Constants ───────────────────────────────────────────────────────────────
5
+ /** Default registry index URL. Override via config or AKM_REGISTRY_URL env var. */
6
+ const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/itlackey/agentikit-registry/main/index.json";
7
+ /** Cache TTL in milliseconds (1 hour). */
8
+ const CACHE_TTL_MS = 60 * 60 * 1000;
9
+ /** Maximum age before cache is considered stale but still usable as fallback (7 days). */
10
+ const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
11
+ // ── Public API ──────────────────────────────────────────────────────────────
12
+ export async function searchRegistry(query, options) {
13
+ const trimmed = query.trim();
14
+ if (!trimmed) {
15
+ return { query: "", hits: [], warnings: [] };
16
+ }
17
+ const limit = clampLimit(options?.limit);
18
+ const urls = resolveRegistryUrls(options?.registryUrls);
19
+ const warnings = [];
20
+ // Load index from all configured registries, merge kits
21
+ const allKits = [];
22
+ for (const url of urls) {
23
+ try {
24
+ const index = await loadIndex(url);
25
+ if (index) {
26
+ allKits.push(...index.kits);
27
+ }
28
+ }
29
+ catch (err) {
30
+ warnings.push(`Registry ${url}: ${toErrorMessage(err)}`);
31
+ }
32
+ }
33
+ // Score and rank
34
+ const hits = scoreKits(allKits, trimmed, limit);
35
+ return { query: trimmed, hits, warnings };
36
+ }
37
+ // ── Index loading with cache ────────────────────────────────────────────────
38
+ async function loadIndex(url) {
39
+ const cachePath = indexCachePath(url);
40
+ const cached = readCachedIndex(cachePath);
41
+ // Fresh cache: return immediately
42
+ if (cached && !isCacheExpired(cached.mtime)) {
43
+ return cached.index;
44
+ }
45
+ // Try to fetch fresh index
46
+ try {
47
+ const response = await fetchWithTimeout(url, undefined, 10_000);
48
+ if (!response.ok) {
49
+ throw new Error(`HTTP ${response.status}`);
50
+ }
51
+ const data = (await response.json());
52
+ const index = parseRegistryIndex(data);
53
+ if (index) {
54
+ writeCachedIndex(cachePath, index);
55
+ return index;
56
+ }
57
+ throw new Error("Invalid registry index format");
58
+ }
59
+ catch (err) {
60
+ // Fetch failed — use stale cache if available
61
+ if (cached && !isCacheStale(cached.mtime)) {
62
+ return cached.index;
63
+ }
64
+ throw err;
65
+ }
66
+ }
67
+ function readCachedIndex(cachePath) {
68
+ try {
69
+ const stat = fs.statSync(cachePath);
70
+ const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
71
+ const index = parseRegistryIndex(raw);
72
+ if (!index)
73
+ return null;
74
+ return { index, mtime: stat.mtimeMs };
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ function writeCachedIndex(cachePath, index) {
81
+ try {
82
+ const dir = path.dirname(cachePath);
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ const tmpPath = cachePath + `.tmp.${process.pid}`;
85
+ fs.writeFileSync(tmpPath, JSON.stringify(index), "utf8");
86
+ fs.renameSync(tmpPath, cachePath);
87
+ }
88
+ catch {
89
+ // Best-effort caching — don't fail the search if we can't write
90
+ }
91
+ }
92
+ function indexCachePath(url) {
93
+ const cacheRoot = resolveCacheDir();
94
+ // Deterministic filename from URL
95
+ const slug = url
96
+ .replace(/[^a-zA-Z0-9]+/g, "-")
97
+ .replace(/^-+|-+$/g, "")
98
+ .slice(0, 120);
99
+ return path.join(cacheRoot, "registry-index", `${slug}.json`);
100
+ }
101
+ function resolveCacheDir() {
102
+ const xdgCache = process.env.XDG_CACHE_HOME?.trim();
103
+ if (xdgCache)
104
+ return path.join(path.resolve(xdgCache), "agentikit");
105
+ const home = process.env.HOME?.trim();
106
+ if (!home)
107
+ return path.join("/tmp", "agentikit-cache");
108
+ return path.join(path.resolve(home), ".cache", "agentikit");
109
+ }
110
+ function isCacheExpired(mtimeMs) {
111
+ return Date.now() - mtimeMs > CACHE_TTL_MS;
112
+ }
113
+ function isCacheStale(mtimeMs) {
114
+ return Date.now() - mtimeMs > CACHE_STALE_MS;
115
+ }
116
+ // ── Index parsing ───────────────────────────────────────────────────────────
117
+ function parseRegistryIndex(data) {
118
+ if (typeof data !== "object" || data === null || Array.isArray(data))
119
+ return null;
120
+ const obj = data;
121
+ if (typeof obj.version !== "number" || obj.version !== 1)
122
+ return null;
123
+ if (typeof obj.updatedAt !== "string")
124
+ return null;
125
+ if (!Array.isArray(obj.kits))
126
+ return null;
127
+ const kits = obj.kits.flatMap((raw) => {
128
+ const kit = parseKitEntry(raw);
129
+ return kit ? [kit] : [];
130
+ });
131
+ return { version: 1, updatedAt: obj.updatedAt, kits };
132
+ }
133
+ function parseKitEntry(raw) {
134
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
135
+ return null;
136
+ const obj = raw;
137
+ const id = asString(obj.id);
138
+ const name = asString(obj.name);
139
+ const ref = asString(obj.ref);
140
+ const source = asSource(obj.source);
141
+ if (!id || !name || !ref || !source)
142
+ return null;
143
+ return {
144
+ id,
145
+ name,
146
+ ref,
147
+ source,
148
+ description: asString(obj.description),
149
+ homepage: asString(obj.homepage),
150
+ tags: asStringArray(obj.tags),
151
+ assetTypes: asStringArray(obj.assetTypes),
152
+ author: asString(obj.author),
153
+ license: asString(obj.license),
154
+ latestVersion: asString(obj.latestVersion),
155
+ curated: obj.curated === true ? true : undefined,
156
+ };
157
+ }
158
+ // ── Scoring ─────────────────────────────────────────────────────────────────
159
+ function scoreKits(kits, query, limit) {
160
+ const tokens = query
161
+ .toLowerCase()
162
+ .split(/\s+/)
163
+ .filter(Boolean);
164
+ const scored = [];
165
+ for (const kit of kits) {
166
+ const score = scoreKit(kit, tokens);
167
+ if (score > 0) {
168
+ scored.push({ kit, score });
169
+ }
170
+ }
171
+ scored.sort((a, b) => b.score - a.score);
172
+ return scored.slice(0, limit).map(({ kit, score }) => toSearchHit(kit, score));
173
+ }
174
+ function scoreKit(kit, tokens) {
175
+ let score = 0;
176
+ const nameLower = kit.name.toLowerCase();
177
+ const descLower = (kit.description ?? "").toLowerCase();
178
+ const tagsLower = (kit.tags ?? []).map((t) => t.toLowerCase());
179
+ for (const token of tokens) {
180
+ // Exact name match is strongest signal
181
+ if (nameLower === token) {
182
+ score += 1.0;
183
+ }
184
+ else if (nameLower.includes(token)) {
185
+ score += 0.6;
186
+ }
187
+ // Tag matches are high-signal (curated keywords)
188
+ if (tagsLower.some((tag) => tag === token)) {
189
+ score += 0.5;
190
+ }
191
+ else if (tagsLower.some((tag) => tag.includes(token))) {
192
+ score += 0.25;
193
+ }
194
+ // Description substring
195
+ if (descLower.includes(token)) {
196
+ score += 0.2;
197
+ }
198
+ // Author match
199
+ if (kit.author?.toLowerCase().includes(token)) {
200
+ score += 0.15;
201
+ }
202
+ }
203
+ // Normalize by token count so multi-word queries don't inflate scores
204
+ return tokens.length > 0 ? score / tokens.length : 0;
205
+ }
206
+ function toSearchHit(kit, score) {
207
+ const metadata = {};
208
+ if (kit.latestVersion)
209
+ metadata.version = kit.latestVersion;
210
+ if (kit.author)
211
+ metadata.author = kit.author;
212
+ if (kit.license)
213
+ metadata.license = kit.license;
214
+ if (kit.assetTypes?.length)
215
+ metadata.assetTypes = kit.assetTypes.join(", ");
216
+ return {
217
+ source: kit.source,
218
+ id: kit.id,
219
+ title: kit.name,
220
+ description: kit.description,
221
+ ref: kit.ref,
222
+ homepage: kit.homepage,
223
+ score: Math.round(score * 1000) / 1000,
224
+ metadata,
225
+ curated: kit.curated,
226
+ };
227
+ }
228
+ // ── Registry URL resolution ─────────────────────────────────────────────────
229
+ function resolveRegistryUrls(override) {
230
+ if (override) {
231
+ const urls = Array.isArray(override) ? override : [override];
232
+ return urls.filter(Boolean);
233
+ }
234
+ // Allow env var override (comma-separated)
235
+ const envUrls = process.env.AKM_REGISTRY_URL?.trim();
236
+ if (envUrls) {
237
+ return envUrls.split(",").map((u) => u.trim()).filter(Boolean);
238
+ }
239
+ return [DEFAULT_REGISTRY_URL];
240
+ }
241
+ // ── Utilities ───────────────────────────────────────────────────────────────
242
+ function clampLimit(limit) {
243
+ if (!limit || !Number.isFinite(limit))
244
+ return 20;
245
+ return Math.min(100, Math.max(1, Math.trunc(limit)));
246
+ }
247
+ function asString(value) {
248
+ return typeof value === "string" && value ? value : undefined;
249
+ }
250
+ function asSource(value) {
251
+ return value === "npm" || value === "github" || value === "git"
252
+ ? value
253
+ : undefined;
254
+ }
255
+ function asStringArray(value) {
256
+ if (!Array.isArray(value))
257
+ return undefined;
258
+ const filtered = value.filter((v) => typeof v === "string");
259
+ return filtered.length > 0 ? filtered : undefined;
260
+ }
261
+ function toErrorMessage(error) {
262
+ return error instanceof Error ? error.message : String(error);
263
+ }
@@ -0,0 +1,62 @@
1
+ export type RegistrySource = "npm" | "github" | "git";
2
+ export interface RegistryRefBase {
3
+ source: RegistrySource;
4
+ ref: string;
5
+ id: string;
6
+ }
7
+ export interface ParsedNpmRef extends RegistryRefBase {
8
+ source: "npm";
9
+ packageName: string;
10
+ requestedVersionOrTag?: string;
11
+ }
12
+ export interface ParsedGithubRef extends RegistryRefBase {
13
+ source: "github";
14
+ owner: string;
15
+ repo: string;
16
+ requestedRef?: string;
17
+ }
18
+ export interface ParsedGitRef extends RegistryRefBase {
19
+ source: "git";
20
+ repoRoot: string;
21
+ sourcePath: string;
22
+ }
23
+ export type ParsedRegistryRef = ParsedNpmRef | ParsedGithubRef | ParsedGitRef;
24
+ export interface ResolvedRegistryArtifact {
25
+ id: string;
26
+ source: RegistrySource;
27
+ ref: string;
28
+ artifactUrl: string;
29
+ resolvedVersion?: string;
30
+ resolvedRevision?: string;
31
+ }
32
+ export interface RegistryInstalledEntry {
33
+ id: string;
34
+ source: RegistrySource;
35
+ ref: string;
36
+ resolvedVersion?: string;
37
+ resolvedRevision?: string;
38
+ artifactUrl: string;
39
+ stashRoot: string;
40
+ cacheDir: string;
41
+ installedAt: string;
42
+ }
43
+ export interface RegistryInstallResult extends RegistryInstalledEntry {
44
+ extractedDir: string;
45
+ }
46
+ export interface RegistrySearchHit {
47
+ source: RegistrySource;
48
+ id: string;
49
+ title: string;
50
+ description?: string;
51
+ ref: string;
52
+ homepage?: string;
53
+ score?: number;
54
+ metadata?: Record<string, string>;
55
+ /** Whether this entry was manually reviewed and approved */
56
+ curated?: boolean;
57
+ }
58
+ export interface RegistrySearchResponse {
59
+ query: string;
60
+ hits: RegistrySearchHit[];
61
+ warnings: string[];
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { AddResponse } from "./stash-types";
2
+ export declare function agentikitAdd(input: {
3
+ ref: string;
4
+ }): Promise<AddResponse>;
@@ -0,0 +1,59 @@
1
+ import { agentikitIndex } from "./indexer";
2
+ import fs from "node:fs";
3
+ import { resolveStashDir } from "./common";
4
+ import { loadConfig } from "./config";
5
+ import { upsertInstalledRegistryEntry, installRegistryRef } from "./registry-install";
6
+ export async function agentikitAdd(input) {
7
+ const ref = input.ref.trim();
8
+ if (!ref)
9
+ throw new Error("Install ref or local git directory is required.");
10
+ const stashDir = resolveStashDir();
11
+ const installed = await installRegistryRef(ref);
12
+ const replaced = loadConfig().registry?.installed.find((entry) => entry.id === installed.id);
13
+ const config = upsertInstalledRegistryEntry({
14
+ id: installed.id,
15
+ source: installed.source,
16
+ ref: installed.ref,
17
+ artifactUrl: installed.artifactUrl,
18
+ resolvedVersion: installed.resolvedVersion,
19
+ resolvedRevision: installed.resolvedRevision,
20
+ stashRoot: installed.stashRoot,
21
+ cacheDir: installed.cacheDir,
22
+ installedAt: installed.installedAt,
23
+ });
24
+ if (replaced && replaced.cacheDir !== installed.cacheDir) {
25
+ try {
26
+ fs.rmSync(replaced.cacheDir, { recursive: true, force: true });
27
+ }
28
+ catch {
29
+ // Best-effort cleanup only.
30
+ }
31
+ }
32
+ const index = await agentikitIndex({ stashDir });
33
+ return {
34
+ stashDir,
35
+ ref,
36
+ installed: {
37
+ id: installed.id,
38
+ source: installed.source,
39
+ ref: installed.ref,
40
+ artifactUrl: installed.artifactUrl,
41
+ resolvedVersion: installed.resolvedVersion,
42
+ resolvedRevision: installed.resolvedRevision,
43
+ stashRoot: installed.stashRoot,
44
+ cacheDir: installed.cacheDir,
45
+ extractedDir: installed.extractedDir,
46
+ installedAt: installed.installedAt,
47
+ },
48
+ config: {
49
+ mountedStashDirs: config.mountedStashDirs,
50
+ installedRegistryCount: config.registry?.installed.length ?? 0,
51
+ },
52
+ index: {
53
+ mode: index.mode,
54
+ totalEntries: index.totalEntries,
55
+ directoriesScanned: index.directoriesScanned,
56
+ directoriesSkipped: index.directoriesSkipped,
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,22 @@
1
+ import { type StashSourceKind } from "./stash-source";
2
+ export interface CloneOptions {
3
+ /** Source ref (e.g., npm:@scope/pkg//tool:deploy.sh) */
4
+ sourceRef: string;
5
+ /** Optional new name for the cloned asset */
6
+ newName?: string;
7
+ /** If true, overwrite existing asset in working stash */
8
+ force?: boolean;
9
+ }
10
+ export interface CloneResponse {
11
+ source: {
12
+ path: string;
13
+ sourceKind: StashSourceKind;
14
+ registryId?: string;
15
+ };
16
+ destination: {
17
+ path: string;
18
+ ref: string;
19
+ };
20
+ overwritten: boolean;
21
+ }
22
+ export declare function agentikitClone(options: CloneOptions): Promise<CloneResponse>;
@@ -0,0 +1,83 @@
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 } from "./origin-resolve";
6
+ import { resolveAssetPath } from "./stash-resolve";
7
+ import { resolveStashSources, findSourceForPath } from "./stash-source";
8
+ export async function agentikitClone(options) {
9
+ const parsed = parseAssetRef(options.sourceRef);
10
+ const allSources = resolveStashSources();
11
+ const workingSource = allSources.find((s) => s.kind === "working");
12
+ if (!workingSource) {
13
+ throw new Error("No working stash configured. Run `akm init` first.");
14
+ }
15
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
16
+ let sourcePath;
17
+ let lastError;
18
+ for (const source of searchSources) {
19
+ try {
20
+ sourcePath = resolveAssetPath(source.path, parsed.type, parsed.name);
21
+ break;
22
+ }
23
+ catch (err) {
24
+ lastError = err instanceof Error ? err : new Error(String(err));
25
+ }
26
+ }
27
+ if (!sourcePath) {
28
+ throw lastError ?? new Error(`Source asset not found for ref: ${options.sourceRef}`);
29
+ }
30
+ const sourceSource = findSourceForPath(sourcePath, allSources);
31
+ const sourceKind = sourceSource?.kind ?? "working";
32
+ const destName = options.newName ?? parsed.name;
33
+ const typeDir = TYPE_DIRS[parsed.type];
34
+ const workingDir = workingSource.path;
35
+ // Guard against self-clone
36
+ if (parsed.type === "skill") {
37
+ const sourceSkillDir = path.resolve(path.dirname(sourcePath));
38
+ const destSkillDir = path.resolve(path.join(workingDir, typeDir, destName));
39
+ if (sourceSkillDir === destSkillDir) {
40
+ throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
41
+ }
42
+ }
43
+ else {
44
+ const resolvedSource = path.resolve(sourcePath);
45
+ const resolvedDest = path.resolve(path.join(workingDir, typeDir, destName));
46
+ if (resolvedSource === resolvedDest) {
47
+ throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
48
+ }
49
+ }
50
+ let destPath;
51
+ if (parsed.type === "skill") {
52
+ const sourceSkillDir = path.dirname(sourcePath);
53
+ const destSkillDir = path.join(workingDir, typeDir, destName);
54
+ const overwritten = fs.existsSync(destSkillDir);
55
+ if (overwritten && !options.force) {
56
+ throw new Error(`Asset already exists in working stash: ${destSkillDir}. Use --force to overwrite.`);
57
+ }
58
+ if (overwritten) {
59
+ fs.rmSync(destSkillDir, { recursive: true, force: true });
60
+ }
61
+ fs.cpSync(sourceSkillDir, destSkillDir, { recursive: true });
62
+ destPath = path.join(destSkillDir, "SKILL.md");
63
+ const ref = makeAssetRef(parsed.type, destName, "local");
64
+ return {
65
+ source: { path: sourcePath, sourceKind, registryId: sourceSource?.registryId },
66
+ destination: { path: destPath, ref },
67
+ overwritten,
68
+ };
69
+ }
70
+ destPath = path.join(workingDir, typeDir, destName);
71
+ const overwritten = fs.existsSync(destPath);
72
+ if (overwritten && !options.force) {
73
+ throw new Error(`Asset already exists in working stash: ${destPath}. Use --force to overwrite.`);
74
+ }
75
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
76
+ fs.copyFileSync(sourcePath, destPath);
77
+ const ref = makeAssetRef(parsed.type, destName, "local");
78
+ return {
79
+ source: { path: sourcePath, sourceKind, registryId: sourceSource?.registryId },
80
+ destination: { path: destPath, ref },
81
+ overwritten,
82
+ };
83
+ }
@@ -1,7 +1,31 @@
1
1
  import { type AgentikitAssetType } from "./common";
2
- export interface OpenRef {
2
+ export interface AssetRef {
3
3
  type: AgentikitAssetType;
4
4
  name: string;
5
+ /**
6
+ * Where to find this asset.
7
+ * - undefined: search all sources (working → mounted → installed)
8
+ * - "local": working stash only
9
+ * - registry ref: e.g. "npm:@scope/pkg", "owner/repo", "github:owner/repo#v1"
10
+ * - filesystem path: e.g. "/mnt/shared-stash"
11
+ */
12
+ origin?: string;
5
13
  }
6
- export declare function parseOpenRef(ref: string): OpenRef;
7
- export declare function makeOpenRef(type: AgentikitAssetType, name: string): string;
14
+ /**
15
+ * Build a ref string from components.
16
+ *
17
+ * Examples:
18
+ * makeAssetRef("tool", "deploy.sh")
19
+ * → "tool:deploy.sh"
20
+ * makeAssetRef("tool", "deploy.sh", "npm:@scope/pkg")
21
+ * → "npm:@scope/pkg//tool:deploy.sh"
22
+ * makeAssetRef("skill", "code-review", "local")
23
+ * → "local//skill:code-review"
24
+ * makeAssetRef("tool", "db/migrate/run.sh", "owner/repo")
25
+ * → "owner/repo//tool:db/migrate/run.sh"
26
+ */
27
+ export declare function makeAssetRef(type: AgentikitAssetType, name: string, origin?: string): string;
28
+ /**
29
+ * Parse a ref string in the format `[origin//]type:name`.
30
+ */
31
+ export declare function parseAssetRef(ref: string): AssetRef;
@@ -1,33 +1,72 @@
1
1
  import path from "node:path";
2
2
  import { isAssetType } from "./common";
3
- export function parseOpenRef(ref) {
4
- const separator = ref.indexOf(":");
5
- if (separator <= 0) {
6
- throw new Error("Invalid open ref. Expected format '<type>:<name>'.");
7
- }
8
- const rawType = ref.slice(0, separator);
9
- const rawName = ref.slice(separator + 1);
10
- if (!isAssetType(rawType)) {
11
- throw new Error(`Invalid open ref type: "${rawType}".`);
3
+ // ── Construction ────────────────────────────────────────────────────────────
4
+ /**
5
+ * Build a ref string from components.
6
+ *
7
+ * Examples:
8
+ * makeAssetRef("tool", "deploy.sh")
9
+ * → "tool:deploy.sh"
10
+ * makeAssetRef("tool", "deploy.sh", "npm:@scope/pkg")
11
+ * → "npm:@scope/pkg//tool:deploy.sh"
12
+ * makeAssetRef("skill", "code-review", "local")
13
+ * → "local//skill:code-review"
14
+ * makeAssetRef("tool", "db/migrate/run.sh", "owner/repo")
15
+ * → "owner/repo//tool:db/migrate/run.sh"
16
+ */
17
+ export function makeAssetRef(type, name, origin) {
18
+ validateName(name);
19
+ const normalized = normalizeName(name);
20
+ const asset = `${type}:${normalized}`;
21
+ if (!origin)
22
+ return asset;
23
+ return `${origin}//${asset}`;
24
+ }
25
+ // ── Parsing ─────────────────────────────────────────────────────────────────
26
+ /**
27
+ * Parse a ref string in the format `[origin//]type:name`.
28
+ */
29
+ export function parseAssetRef(ref) {
30
+ const trimmed = ref.trim();
31
+ if (!trimmed)
32
+ throw new Error("Empty ref.");
33
+ let origin;
34
+ let body = trimmed;
35
+ const boundary = trimmed.indexOf("//");
36
+ if (boundary >= 0) {
37
+ origin = trimmed.slice(0, boundary);
38
+ body = trimmed.slice(boundary + 2);
39
+ if (!origin)
40
+ throw new Error("Empty origin in ref.");
12
41
  }
13
- let name;
14
- try {
15
- name = decodeURIComponent(rawName);
42
+ const colon = body.indexOf(":");
43
+ if (colon <= 0) {
44
+ throw new Error(`Invalid ref "${trimmed}". Expected [origin//]type:name`);
16
45
  }
17
- catch {
18
- throw new Error("Invalid open ref encoding.");
46
+ const rawType = body.slice(0, colon);
47
+ const rawName = body.slice(colon + 1);
48
+ if (!isAssetType(rawType)) {
49
+ throw new Error(`Invalid asset type: "${rawType}".`);
19
50
  }
51
+ validateName(rawName);
52
+ const name = normalizeName(rawName);
53
+ return { type: rawType, name, origin: origin || undefined };
54
+ }
55
+ // ── Validation ──────────────────────────────────────────────────────────────
56
+ function validateName(name) {
57
+ if (!name)
58
+ throw new Error("Empty asset name.");
59
+ if (name.includes("\0"))
60
+ throw new Error("Null byte in asset name.");
61
+ if (/^[A-Za-z]:/.test(name))
62
+ throw new Error("Windows drive path in asset name.");
20
63
  const normalized = path.posix.normalize(name.replace(/\\/g, "/"));
21
- if (!name
22
- || name.includes("\0")
23
- || /^[A-Za-z]:/.test(name)
24
- || path.posix.isAbsolute(normalized)
25
- || normalized === ".."
26
- || normalized.startsWith("../")) {
27
- throw new Error("Invalid open ref name.");
64
+ if (path.posix.isAbsolute(normalized))
65
+ throw new Error("Absolute path in asset name.");
66
+ if (normalized === ".." || normalized.startsWith("../")) {
67
+ throw new Error("Path traversal in asset name.");
28
68
  }
29
- return { type: rawType, name: normalized };
30
69
  }
31
- export function makeOpenRef(type, name) {
32
- return `${type}:${encodeURIComponent(name)}`;
70
+ function normalizeName(name) {
71
+ return path.posix.normalize(name.replace(/\\/g, "/"));
33
72
  }
@@ -0,0 +1,18 @@
1
+ import type { ListResponse, RemoveResponse, ReinstallResponse, UpdateResponse } from "./stash-types";
2
+ export declare function agentikitList(input?: {
3
+ stashDir?: string;
4
+ }): Promise<ListResponse>;
5
+ export declare function agentikitRemove(input: {
6
+ target: string;
7
+ stashDir?: string;
8
+ }): Promise<RemoveResponse>;
9
+ export declare function agentikitReinstall(input?: {
10
+ target?: string;
11
+ all?: boolean;
12
+ stashDir?: string;
13
+ }): Promise<ReinstallResponse>;
14
+ export declare function agentikitUpdate(input?: {
15
+ target?: string;
16
+ all?: boolean;
17
+ stashDir?: string;
18
+ }): Promise<UpdateResponse>;