akm-cli 0.6.0-rc1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +9 -9
- package/dist/cli.js +199 -114
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/{curate.js → commands/curate.js} +8 -3
- package/dist/{info.js → commands/info.js} +15 -9
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +4 -7
- package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
- package/dist/{migration-help.js → commands/migration-help.js} +2 -2
- package/dist/{registry-search.js → commands/registry-search.js} +8 -6
- package/dist/{remember.js → commands/remember.js} +55 -49
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +69 -3
- package/dist/{stash-show.js → commands/show.js} +104 -84
- package/dist/{stash-add.js → commands/source-add.js} +42 -32
- package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +1 -1
- package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
- package/dist/{config.js → core/config.js} +133 -56
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
- package/dist/core/write-source.js +280 -0
- package/dist/{db-search.js → indexer/db-search.js} +25 -19
- package/dist/{db.js → indexer/db.js} +79 -47
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +132 -33
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +3 -6
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +52 -41
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +1 -1
- package/dist/{llm-client.js → llm/client.js} +1 -1
- package/dist/{embedders → llm/embedders}/local.js +2 -2
- package/dist/{embedders → llm/embedders}/remote.js +1 -1
- package/dist/{embedders → llm/embedders}/types.js +1 -1
- package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
- package/dist/{cli-hints.js → output/cli-hints.js} +3 -0
- package/dist/{output-context.js → output/context.js} +21 -3
- package/dist/{renderers.js → output/renderers.js} +9 -65
- package/dist/{output-shapes.js → output/shapes.js} +18 -4
- package/dist/{output-text.js → output/text.js} +2 -2
- package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/{providers → registry/providers}/index.js +1 -1
- package/dist/{providers → registry/providers}/skills-sh.js +59 -3
- package/dist/{providers → registry/providers}/static-index.js +80 -12
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +16 -56
- package/dist/{stash-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +53 -64
- package/dist/{stash-providers → sources/providers}/index.js +3 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/{stash-providers → sources/providers}/npm.js +42 -41
- package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
- package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
- package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
- package/dist/{stash-providers → sources/providers}/website.js +29 -65
- package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
- package/dist/{wiki.js → wiki/wiki.js} +34 -18
- package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +72 -28
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/migration/release-notes/0.6.0.md +91 -23
- package/package.json +1 -1
- package/dist/errors.js +0 -45
- package/dist/llm.js +0 -16
- package/dist/registry-factory.js +0 -19
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -3
- package/dist/stash-providers/filesystem.js +0 -71
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -260
- /package/dist/{common.js → core/common.js} +0 -0
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{paths.js → core/paths.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{embedder.js → llm/embedder.js} +0 -0
- /package/dist/{embedders → llm/embedders}/cache.js +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{setup-steps.js → setup/steps.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fetchWithRetry } from "../common";
|
|
5
|
-
import { ConfigError, NotFoundError } from "../errors";
|
|
6
|
-
import { getRegistryIndexCacheDir } from "../paths";
|
|
7
|
-
import { registerStashProvider } from "../stash-provider-factory";
|
|
8
|
-
import { isExpired, sanitizeString } from "./provider-utils";
|
|
9
|
-
/** Per-query cache TTL in milliseconds (5 minutes). */
|
|
10
|
-
const QUERY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
11
|
-
/** Maximum age before query cache is considered stale but still usable (1 hour). */
|
|
12
|
-
const QUERY_CACHE_STALE_MS = 60 * 60 * 1000;
|
|
13
|
-
/**
|
|
14
|
-
* Single source of truth for OpenViking type → akm asset type mapping.
|
|
15
|
-
* Used by both search hit mapping and show response mapping.
|
|
16
|
-
*/
|
|
17
|
-
const OV_TYPE_MAP = {
|
|
18
|
-
skill: "skill",
|
|
19
|
-
skills: "skill",
|
|
20
|
-
memory: "memory",
|
|
21
|
-
memories: "memory",
|
|
22
|
-
resource: "knowledge",
|
|
23
|
-
resources: "knowledge",
|
|
24
|
-
knowledge: "knowledge",
|
|
25
|
-
agent: "agent",
|
|
26
|
-
agents: "agent",
|
|
27
|
-
command: "command",
|
|
28
|
-
commands: "command",
|
|
29
|
-
script: "script",
|
|
30
|
-
scripts: "script",
|
|
31
|
-
};
|
|
32
|
-
class OpenVikingStashProvider {
|
|
33
|
-
type = "openviking";
|
|
34
|
-
name;
|
|
35
|
-
config;
|
|
36
|
-
constructor(config) {
|
|
37
|
-
this.config = config;
|
|
38
|
-
this.name = config.name ?? "openviking";
|
|
39
|
-
// Validate baseUrl scheme to prevent SSRF via file:// or other non-HTTP schemes
|
|
40
|
-
const rawUrl = config.url ?? "";
|
|
41
|
-
if (rawUrl) {
|
|
42
|
-
try {
|
|
43
|
-
const parsed = new URL(rawUrl);
|
|
44
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
45
|
-
throw new ConfigError(`OpenViking baseUrl must use http:// or https://, got "${parsed.protocol}" in "${rawUrl}"`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
if (err instanceof ConfigError)
|
|
50
|
-
throw err;
|
|
51
|
-
throw new ConfigError(`OpenViking baseUrl is not a valid URL: "${rawUrl}"`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
async search(options) {
|
|
56
|
-
try {
|
|
57
|
-
const entries = await this.fetchResults(options.query, options.limit);
|
|
58
|
-
const limited = entries.slice(0, options.limit);
|
|
59
|
-
const hits = this.mapToStashHits(limited);
|
|
60
|
-
return { hits };
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
-
return { hits: [], warnings: [`Stash ${this.name}: ${message}`] };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
async show(ref, _view) {
|
|
68
|
-
const trimmed = ref.trim();
|
|
69
|
-
// Accept both viking:// URIs (legacy/internal) and type:name refs
|
|
70
|
-
const uri = trimmed.startsWith("viking://") ? trimmed : refToVikingUri(trimmed);
|
|
71
|
-
const baseUrl = this.baseUrl;
|
|
72
|
-
const headers = this.authHeaders;
|
|
73
|
-
const [statResult, contentResult] = await Promise.all([
|
|
74
|
-
fetchOVJson(`${baseUrl}/api/v1/fs/stat?uri=${encodeURIComponent(uri)}`, headers),
|
|
75
|
-
fetchOVJson(`${baseUrl}/api/v1/content/read?uri=${encodeURIComponent(uri)}&offset=0&limit=-1`, headers),
|
|
76
|
-
]);
|
|
77
|
-
if (statResult == null && contentResult == null) {
|
|
78
|
-
throw new NotFoundError(`Could not fetch remote asset "${trimmed}". The OpenViking server at ${baseUrl} may be unreachable or the resource does not exist.`);
|
|
79
|
-
}
|
|
80
|
-
if (contentResult == null) {
|
|
81
|
-
throw new NotFoundError(`Content not found for remote asset "${trimmed}". The server returned metadata but no content.`);
|
|
82
|
-
}
|
|
83
|
-
const stat = (typeof statResult === "object" && statResult !== null ? statResult : {});
|
|
84
|
-
const uriPath = uri.replace(/^viking:\/\//, "");
|
|
85
|
-
// Sanitize untrusted fields to strip terminal control characters
|
|
86
|
-
const name = sanitizeString(stat.name) || uriPath.split("/").pop() || "unknown";
|
|
87
|
-
const ovType = sanitizeString(stat.type) || inferTypeFromUri(uri);
|
|
88
|
-
const assetType = OV_TYPE_MAP[ovType] ?? "knowledge";
|
|
89
|
-
const content = typeof contentResult === "string" ? contentResult : "";
|
|
90
|
-
const description = sanitizeString(stat.abstract, 1000) || undefined;
|
|
91
|
-
const assetRef = `${assetType}:${name}`;
|
|
92
|
-
return {
|
|
93
|
-
type: assetType,
|
|
94
|
-
name,
|
|
95
|
-
path: assetRef,
|
|
96
|
-
action: `Remote content from OpenViking — ${assetRef}`,
|
|
97
|
-
content,
|
|
98
|
-
description,
|
|
99
|
-
editable: false,
|
|
100
|
-
origin: "remote",
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
canShow(_ref) {
|
|
104
|
-
return !!(this.config.url ?? "").trim();
|
|
105
|
-
}
|
|
106
|
-
get baseUrl() {
|
|
107
|
-
return (this.config.url ?? "").replace(/\/+$/, "");
|
|
108
|
-
}
|
|
109
|
-
get authHeaders() {
|
|
110
|
-
const headers = {};
|
|
111
|
-
const apiKey = this.config.options?.apiKey ?? undefined;
|
|
112
|
-
if (apiKey)
|
|
113
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
114
|
-
return headers;
|
|
115
|
-
}
|
|
116
|
-
async fetchResults(query, limit) {
|
|
117
|
-
const cachePath = this.queryCachePath(query, limit);
|
|
118
|
-
const cached = this.readQueryCache(cachePath);
|
|
119
|
-
if (cached && !isExpired(cached.mtime, QUERY_CACHE_TTL_MS)) {
|
|
120
|
-
return cached.entries;
|
|
121
|
-
}
|
|
122
|
-
const baseUrl = this.baseUrl;
|
|
123
|
-
const searchType = this.config.options?.searchType ?? "semantic";
|
|
124
|
-
try {
|
|
125
|
-
let url;
|
|
126
|
-
let body;
|
|
127
|
-
if (searchType === "text") {
|
|
128
|
-
url = `${baseUrl}/api/v1/search/grep`;
|
|
129
|
-
body = JSON.stringify({ uri: "viking://", pattern: query, case_insensitive: true });
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
url = `${baseUrl}/api/v1/search/find`;
|
|
133
|
-
body = JSON.stringify({ query, limit });
|
|
134
|
-
}
|
|
135
|
-
const headers = { "Content-Type": "application/json", ...this.authHeaders };
|
|
136
|
-
const response = await fetchWithRetry(url, { method: "POST", headers, body }, { timeout: 10_000, retries: 1 });
|
|
137
|
-
if (!response.ok) {
|
|
138
|
-
throw new Error(`HTTP ${response.status}`);
|
|
139
|
-
}
|
|
140
|
-
const data = (await response.json());
|
|
141
|
-
if (data.status !== "ok") {
|
|
142
|
-
throw new Error(data.error ?? "OpenViking returned error status");
|
|
143
|
-
}
|
|
144
|
-
const entries = parseOVSearchResponse(data.result);
|
|
145
|
-
this.writeQueryCache(cachePath, entries);
|
|
146
|
-
return entries;
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
if (cached && !isExpired(cached.mtime, QUERY_CACHE_STALE_MS)) {
|
|
150
|
-
return cached.entries;
|
|
151
|
-
}
|
|
152
|
-
throw err;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
mapToStashHits(entries) {
|
|
156
|
-
if (entries.length === 0)
|
|
157
|
-
return [];
|
|
158
|
-
const maxScore = entries.reduce((max, e) => Math.max(max, e.score), 0.01);
|
|
159
|
-
return entries.map((entry) => {
|
|
160
|
-
const name = sanitizeString(entry.name);
|
|
161
|
-
const abstract = sanitizeString(entry.abstract, 1000);
|
|
162
|
-
const type = sanitizeString(entry.type);
|
|
163
|
-
const assetType = OV_TYPE_MAP[type] ?? "knowledge";
|
|
164
|
-
const ref = `${assetType}:${name}`;
|
|
165
|
-
return {
|
|
166
|
-
type: assetType,
|
|
167
|
-
name,
|
|
168
|
-
path: ref,
|
|
169
|
-
ref,
|
|
170
|
-
origin: this.type,
|
|
171
|
-
editable: false,
|
|
172
|
-
description: abstract || undefined,
|
|
173
|
-
action: `akm show ${ref}`,
|
|
174
|
-
score: Math.round((entry.score / maxScore) * 1000) / 1000,
|
|
175
|
-
};
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
queryCachePath(query, limit) {
|
|
179
|
-
const cacheDir = getRegistryIndexCacheDir();
|
|
180
|
-
const hasher = createHash("md5");
|
|
181
|
-
hasher.update(this.config.url ?? "");
|
|
182
|
-
hasher.update("\0");
|
|
183
|
-
hasher.update(query.trim().toLowerCase());
|
|
184
|
-
hasher.update("\0");
|
|
185
|
-
hasher.update(String(limit));
|
|
186
|
-
hasher.update("\0");
|
|
187
|
-
const searchType = this.config.options?.searchType ?? "semantic";
|
|
188
|
-
hasher.update(searchType);
|
|
189
|
-
hasher.update("\0");
|
|
190
|
-
const apiKey = this.config.options?.apiKey ?? "";
|
|
191
|
-
hasher.update(apiKey);
|
|
192
|
-
const hash = hasher.digest("hex");
|
|
193
|
-
return path.join(cacheDir, `openviking-search-${hash}.json`);
|
|
194
|
-
}
|
|
195
|
-
readQueryCache(cachePath) {
|
|
196
|
-
try {
|
|
197
|
-
const stat = fs.statSync(cachePath);
|
|
198
|
-
const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
199
|
-
if (!Array.isArray(raw))
|
|
200
|
-
return null;
|
|
201
|
-
const entries = raw.filter(isValidOVEntry);
|
|
202
|
-
return { entries, mtime: stat.mtimeMs };
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
writeQueryCache(cachePath, entries) {
|
|
209
|
-
try {
|
|
210
|
-
const dir = path.dirname(cachePath);
|
|
211
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
212
|
-
const tmpPath = `${cachePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
213
|
-
// 0o600: owner read/write only — cache may contain search terms tied to API keys
|
|
214
|
-
fs.writeFileSync(tmpPath, JSON.stringify(entries), { encoding: "utf8", mode: 0o600 });
|
|
215
|
-
fs.renameSync(tmpPath, cachePath);
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
// Best-effort caching
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
// ── Self-register ───────────────────────────────────────────────────────────
|
|
223
|
-
registerStashProvider("openviking", (config) => new OpenVikingStashProvider(config));
|
|
224
|
-
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
225
|
-
/**
|
|
226
|
-
* Convert a type:name ref to a viking:// URI for the OpenViking API.
|
|
227
|
-
* Maps the akm asset type back to the OV plural form (e.g. "skill" -> "skills").
|
|
228
|
-
*/
|
|
229
|
-
function refToVikingUri(ref) {
|
|
230
|
-
const colon = ref.indexOf(":");
|
|
231
|
-
if (colon <= 0)
|
|
232
|
-
return `viking://${ref}`;
|
|
233
|
-
const name = ref.slice(colon + 1);
|
|
234
|
-
const type = ref.slice(0, colon);
|
|
235
|
-
const ovDir = AKM_TO_OV_DIR[type] ?? type;
|
|
236
|
-
return `viking://${ovDir}/${name}`;
|
|
237
|
-
}
|
|
238
|
-
/** Reverse map: akm asset type → OpenViking directory name (plural). */
|
|
239
|
-
const AKM_TO_OV_DIR = {
|
|
240
|
-
skill: "skills",
|
|
241
|
-
memory: "memories",
|
|
242
|
-
knowledge: "resources",
|
|
243
|
-
agent: "agents",
|
|
244
|
-
command: "commands",
|
|
245
|
-
script: "scripts",
|
|
246
|
-
};
|
|
247
|
-
function parseOVSearchResponse(result) {
|
|
248
|
-
if (Array.isArray(result))
|
|
249
|
-
return result.filter(isValidOVEntry);
|
|
250
|
-
if (typeof result !== "object" || result === null)
|
|
251
|
-
return [];
|
|
252
|
-
const grouped = result;
|
|
253
|
-
if (Array.isArray(grouped.matches)) {
|
|
254
|
-
return deduplicateGrepMatches(grouped.matches);
|
|
255
|
-
}
|
|
256
|
-
const entries = [];
|
|
257
|
-
for (const [category, items] of Object.entries(grouped)) {
|
|
258
|
-
if (category === "total")
|
|
259
|
-
continue;
|
|
260
|
-
if (!Array.isArray(items))
|
|
261
|
-
continue;
|
|
262
|
-
for (const item of items) {
|
|
263
|
-
if (!isValidOVSearchItem(item))
|
|
264
|
-
continue;
|
|
265
|
-
entries.push({
|
|
266
|
-
uri: item.uri,
|
|
267
|
-
name: extractNameFromUri(item.uri),
|
|
268
|
-
score: item.score,
|
|
269
|
-
type: item.context_type ?? category,
|
|
270
|
-
abstract: item.abstract ?? undefined,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return entries;
|
|
275
|
-
}
|
|
276
|
-
function isValidGrepMatch(item) {
|
|
277
|
-
if (typeof item !== "object" || item === null)
|
|
278
|
-
return false;
|
|
279
|
-
const obj = item;
|
|
280
|
-
return typeof obj.uri === "string" && typeof obj.content === "string";
|
|
281
|
-
}
|
|
282
|
-
function deduplicateGrepMatches(matches) {
|
|
283
|
-
const byUri = new Map();
|
|
284
|
-
for (const m of matches) {
|
|
285
|
-
if (!isValidGrepMatch(m))
|
|
286
|
-
continue;
|
|
287
|
-
const existing = byUri.get(m.uri);
|
|
288
|
-
if (existing) {
|
|
289
|
-
existing.count++;
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
const pathSegment = m.uri.replace(/^viking:\/\//, "").split("/")[0] ?? "";
|
|
293
|
-
byUri.set(m.uri, { content: m.content, count: 1, type: pathSegment });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
const maxCount = Math.max(...[...byUri.values()].map((v) => v.count), 1);
|
|
297
|
-
const entries = [];
|
|
298
|
-
for (const [uri, { content, count, type }] of byUri) {
|
|
299
|
-
entries.push({
|
|
300
|
-
uri,
|
|
301
|
-
name: extractNameFromUri(uri),
|
|
302
|
-
score: count / maxCount,
|
|
303
|
-
type,
|
|
304
|
-
abstract: content.slice(0, 200),
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
entries.sort((a, b) => b.score - a.score);
|
|
308
|
-
return entries;
|
|
309
|
-
}
|
|
310
|
-
function isValidOVEntry(entry) {
|
|
311
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
312
|
-
return false;
|
|
313
|
-
const obj = entry;
|
|
314
|
-
return typeof obj.uri === "string" && typeof obj.name === "string" && typeof obj.score === "number";
|
|
315
|
-
}
|
|
316
|
-
function isValidOVSearchItem(item) {
|
|
317
|
-
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
318
|
-
return false;
|
|
319
|
-
const obj = item;
|
|
320
|
-
return typeof obj.uri === "string" && typeof obj.score === "number";
|
|
321
|
-
}
|
|
322
|
-
function extractNameFromUri(uri) {
|
|
323
|
-
const uriPath = uri.replace(/^viking:\/\//, "");
|
|
324
|
-
const segments = uriPath.split("/").filter(Boolean);
|
|
325
|
-
const last = segments[segments.length - 1] ?? "unknown";
|
|
326
|
-
return last.replace(/\.[^.]+$/, "");
|
|
327
|
-
}
|
|
328
|
-
async function fetchOVJson(url, headers) {
|
|
329
|
-
try {
|
|
330
|
-
const response = await fetchWithRetry(url, { headers }, { timeout: 10_000, retries: 1 });
|
|
331
|
-
if (!response.ok)
|
|
332
|
-
return null;
|
|
333
|
-
const data = (await response.json());
|
|
334
|
-
if (data.status !== "ok")
|
|
335
|
-
return null;
|
|
336
|
-
return data.result ?? null;
|
|
337
|
-
}
|
|
338
|
-
catch {
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function inferTypeFromUri(uri) {
|
|
343
|
-
const uriPath = uri.replace(/^viking:\/\//, "");
|
|
344
|
-
const firstSegment = uriPath.split("/")[0] ?? "";
|
|
345
|
-
return OV_TYPE_MAP[firstSegment] ?? "knowledge";
|
|
346
|
-
}
|
|
347
|
-
// ── Exports for testing ─────────────────────────────────────────────────────
|
|
348
|
-
export { OpenVikingStashProvider, parseOVSearchResponse, refToVikingUri };
|
package/dist/stash-types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
|
|
2
|
-
const ALLOWED_FRONTMATTER_KEYS = new Set(["description", "tags", "params"]);
|
|
3
|
-
const STEP_ID_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4
|
-
export class WorkflowValidationError extends Error {
|
|
5
|
-
constructor(message) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.name = "WorkflowValidationError";
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
export function parseWorkflowMarkdown(markdown) {
|
|
11
|
-
const parsed = parseFrontmatter(markdown);
|
|
12
|
-
validateFrontmatter(parsed.data);
|
|
13
|
-
const title = extractWorkflowTitle(parsed.content);
|
|
14
|
-
const parameters = extractWorkflowParameters(parsed.data);
|
|
15
|
-
const tags = extractWorkflowTags(parsed.data, parsed.frontmatter);
|
|
16
|
-
const steps = extractWorkflowSteps(parsed.content);
|
|
17
|
-
return {
|
|
18
|
-
title,
|
|
19
|
-
description: toStringOrUndefined(parsed.data.description),
|
|
20
|
-
...(tags ? { tags } : {}),
|
|
21
|
-
...(parameters ? { parameters } : {}),
|
|
22
|
-
steps,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function validateFrontmatter(data) {
|
|
26
|
-
const unsupported = Object.keys(data).filter((key) => !ALLOWED_FRONTMATTER_KEYS.has(key));
|
|
27
|
-
if (unsupported.length > 0) {
|
|
28
|
-
throw new WorkflowValidationError(`Workflow frontmatter only supports description, tags, and params. Unsupported key(s): ${unsupported.join(", ")}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function extractWorkflowTitle(body) {
|
|
32
|
-
const matches = Array.from(body.matchAll(/^#\s+Workflow:\s+(.+?)\s*$/gm));
|
|
33
|
-
if (matches.length === 0) {
|
|
34
|
-
throw new WorkflowValidationError('Workflow markdown must contain a "# Workflow: <title>" heading.');
|
|
35
|
-
}
|
|
36
|
-
if (matches.length > 1) {
|
|
37
|
-
throw new WorkflowValidationError('Workflow markdown must contain exactly one "# Workflow: <title>" heading.');
|
|
38
|
-
}
|
|
39
|
-
const title = matches[0]?.[1]?.trim() ?? "";
|
|
40
|
-
if (!title) {
|
|
41
|
-
throw new WorkflowValidationError('Workflow markdown must contain a non-empty "# Workflow: <title>" heading.');
|
|
42
|
-
}
|
|
43
|
-
return title;
|
|
44
|
-
}
|
|
45
|
-
function extractWorkflowTags(data, frontmatter) {
|
|
46
|
-
const tags = data.tags;
|
|
47
|
-
if (typeof tags === "undefined")
|
|
48
|
-
return undefined;
|
|
49
|
-
if (typeof tags === "string") {
|
|
50
|
-
const trimmed = tags.trim();
|
|
51
|
-
return trimmed ? [trimmed] : undefined;
|
|
52
|
-
}
|
|
53
|
-
if (frontmatter &&
|
|
54
|
-
typeof tags === "object" &&
|
|
55
|
-
tags !== null &&
|
|
56
|
-
!Array.isArray(tags) &&
|
|
57
|
-
Object.keys(tags).length === 0) {
|
|
58
|
-
const blockTags = extractTagListFromFrontmatter(frontmatter);
|
|
59
|
-
if (blockTags)
|
|
60
|
-
return blockTags;
|
|
61
|
-
}
|
|
62
|
-
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string" && tag.trim().length > 0)) {
|
|
63
|
-
throw new WorkflowValidationError("Workflow frontmatter `tags` must be a string or an array of non-empty strings.");
|
|
64
|
-
}
|
|
65
|
-
return tags.map((tag) => tag.trim());
|
|
66
|
-
}
|
|
67
|
-
function extractWorkflowParameters(data) {
|
|
68
|
-
const params = data.params;
|
|
69
|
-
if (typeof params === "undefined")
|
|
70
|
-
return undefined;
|
|
71
|
-
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
72
|
-
throw new WorkflowValidationError("Workflow frontmatter `params` must be a mapping of parameter names to descriptions.");
|
|
73
|
-
}
|
|
74
|
-
const entries = Object.entries(params);
|
|
75
|
-
if (entries.length === 0)
|
|
76
|
-
return undefined;
|
|
77
|
-
return entries.map(([name, description]) => {
|
|
78
|
-
if (!name.trim()) {
|
|
79
|
-
throw new WorkflowValidationError("Workflow parameter names must be non-empty.");
|
|
80
|
-
}
|
|
81
|
-
if (typeof description !== "string" || !description.trim()) {
|
|
82
|
-
throw new WorkflowValidationError(`Workflow parameter "${name}" must have a non-empty string description in frontmatter params.`);
|
|
83
|
-
}
|
|
84
|
-
return { name: name.trim(), description: description.trim() };
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
function extractWorkflowSteps(body) {
|
|
88
|
-
const lines = normalizeLines(body);
|
|
89
|
-
const titleLineIndex = lines.findIndex((line) => /^#\s+Workflow:\s+/.test(line));
|
|
90
|
-
if (titleLineIndex === -1) {
|
|
91
|
-
throw new WorkflowValidationError('Workflow markdown must contain a "# Workflow: <title>" heading.');
|
|
92
|
-
}
|
|
93
|
-
const steps = [];
|
|
94
|
-
let index = titleLineIndex + 1;
|
|
95
|
-
// Skip optional intro prose before the first ## Step: section.
|
|
96
|
-
while (index < lines.length && !/^##\s+Step:\s+/.test((lines[index] ?? "").trim())) {
|
|
97
|
-
const line = lines[index] ?? "";
|
|
98
|
-
const trimmed = line.trim();
|
|
99
|
-
if (trimmed.startsWith("# ") && !/^#\s+Workflow:\s+/.test(trimmed)) {
|
|
100
|
-
throw new WorkflowValidationError(`Unexpected top-level heading after workflow title: "${trimmed}".`);
|
|
101
|
-
}
|
|
102
|
-
index++;
|
|
103
|
-
}
|
|
104
|
-
while (index < lines.length) {
|
|
105
|
-
const line = lines[index] ?? "";
|
|
106
|
-
const trimmed = line.trim();
|
|
107
|
-
if (!trimmed) {
|
|
108
|
-
index++;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (trimmed.startsWith("# ") && !/^#\s+Workflow:\s+/.test(trimmed)) {
|
|
112
|
-
throw new WorkflowValidationError(`Unexpected top-level heading after workflow title: "${trimmed}".`);
|
|
113
|
-
}
|
|
114
|
-
const stepHeader = trimmed.match(/^##\s+Step:\s+(.+?)\s*$/);
|
|
115
|
-
if (!stepHeader) {
|
|
116
|
-
throw new WorkflowValidationError(`Expected a "## Step: <title>" section after the workflow title, but found: "${trimmed}".`);
|
|
117
|
-
}
|
|
118
|
-
const stepTitle = stepHeader[1].trim();
|
|
119
|
-
const sequenceIndex = steps.length;
|
|
120
|
-
index++;
|
|
121
|
-
let stepId;
|
|
122
|
-
let instructions;
|
|
123
|
-
let completionCriteria;
|
|
124
|
-
while (index < lines.length) {
|
|
125
|
-
const current = lines[index] ?? "";
|
|
126
|
-
const currentTrimmed = current.trim();
|
|
127
|
-
if (/^##\s+Step:\s+/.test(currentTrimmed))
|
|
128
|
-
break;
|
|
129
|
-
if (/^#\s+/.test(currentTrimmed)) {
|
|
130
|
-
throw new WorkflowValidationError(`Unexpected heading "${currentTrimmed}" inside step "${stepTitle}". Only step sections and step subsections are allowed.`);
|
|
131
|
-
}
|
|
132
|
-
if (!currentTrimmed) {
|
|
133
|
-
index++;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const stepIdMatch = currentTrimmed.match(/^Step ID:\s+(.+?)\s*$/);
|
|
137
|
-
if (stepIdMatch) {
|
|
138
|
-
if (stepId) {
|
|
139
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" must contain exactly one "Step ID: <id>" line.`);
|
|
140
|
-
}
|
|
141
|
-
stepId = stepIdMatch[1].trim();
|
|
142
|
-
if (!STEP_ID_REGEX.test(stepId)) {
|
|
143
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" has invalid Step ID "${stepId}". Use letters, numbers, ".", "_" or "-".`);
|
|
144
|
-
}
|
|
145
|
-
index++;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const subsection = currentTrimmed.match(/^###\s+(.+?)\s*$/);
|
|
149
|
-
if (!subsection) {
|
|
150
|
-
throw new WorkflowValidationError(`Unexpected content in step "${stepTitle}". Add "Step ID: <id>" before subsections or move text under "### Instructions".`);
|
|
151
|
-
}
|
|
152
|
-
const subsectionName = subsection[1].trim();
|
|
153
|
-
index++;
|
|
154
|
-
const block = collectSectionBlock(lines, index);
|
|
155
|
-
index = block.nextIndex;
|
|
156
|
-
if (subsectionName === "Instructions") {
|
|
157
|
-
if (instructions) {
|
|
158
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" must contain exactly one "### Instructions" section.`);
|
|
159
|
-
}
|
|
160
|
-
instructions = block.text;
|
|
161
|
-
if (!instructions) {
|
|
162
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" must include instructions text.`);
|
|
163
|
-
}
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (subsectionName === "Completion Criteria") {
|
|
167
|
-
if (completionCriteria) {
|
|
168
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" must contain at most one "### Completion Criteria" section.`);
|
|
169
|
-
}
|
|
170
|
-
completionCriteria = block.items;
|
|
171
|
-
if (!completionCriteria || completionCriteria.length === 0) {
|
|
172
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" has an empty "### Completion Criteria" section.`);
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
throw new WorkflowValidationError(`Unknown subsection "### ${subsectionName}" in step "${stepTitle}". Only "### Instructions" and optional "### Completion Criteria" are supported.`);
|
|
177
|
-
}
|
|
178
|
-
if (!stepId) {
|
|
179
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" must contain exactly one "Step ID: <id>" line.`);
|
|
180
|
-
}
|
|
181
|
-
if (!instructions) {
|
|
182
|
-
throw new WorkflowValidationError(`Step "${stepTitle}" must contain a "### Instructions" section.`);
|
|
183
|
-
}
|
|
184
|
-
steps.push({
|
|
185
|
-
id: stepId,
|
|
186
|
-
title: stepTitle,
|
|
187
|
-
instructions,
|
|
188
|
-
...(completionCriteria ? { completionCriteria } : {}),
|
|
189
|
-
sequenceIndex,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
if (steps.length === 0) {
|
|
193
|
-
throw new WorkflowValidationError('Workflow markdown must contain at least one "## Step: <title>" section.');
|
|
194
|
-
}
|
|
195
|
-
const seenStepIds = new Set();
|
|
196
|
-
for (const step of steps) {
|
|
197
|
-
if (seenStepIds.has(step.id)) {
|
|
198
|
-
throw new WorkflowValidationError(`Workflow step IDs must be unique. Duplicate Step ID: "${step.id}".`);
|
|
199
|
-
}
|
|
200
|
-
seenStepIds.add(step.id);
|
|
201
|
-
}
|
|
202
|
-
return steps;
|
|
203
|
-
}
|
|
204
|
-
function normalizeLines(body) {
|
|
205
|
-
return body.replace(/\r\n|\r/g, "\n").split("\n");
|
|
206
|
-
}
|
|
207
|
-
function collectSectionBlock(lines, startIndex) {
|
|
208
|
-
const collected = [];
|
|
209
|
-
let index = startIndex;
|
|
210
|
-
while (index < lines.length) {
|
|
211
|
-
const line = lines[index] ?? "";
|
|
212
|
-
const trimmed = line.trim();
|
|
213
|
-
if (/^##\s+Step:\s+/.test(trimmed) || /^###\s+/.test(trimmed) || /^#\s+/.test(trimmed))
|
|
214
|
-
break;
|
|
215
|
-
collected.push(line);
|
|
216
|
-
index++;
|
|
217
|
-
}
|
|
218
|
-
const text = collected.join("\n").trim();
|
|
219
|
-
const items = text
|
|
220
|
-
? text
|
|
221
|
-
.split("\n")
|
|
222
|
-
.map((line) => line.trim())
|
|
223
|
-
.filter(Boolean)
|
|
224
|
-
.map((line) => line.replace(/^[-*]\s*/, "").trim())
|
|
225
|
-
.filter(Boolean)
|
|
226
|
-
: undefined;
|
|
227
|
-
return { text, items, nextIndex: index };
|
|
228
|
-
}
|
|
229
|
-
function extractTagListFromFrontmatter(frontmatter) {
|
|
230
|
-
const lines = frontmatter.split("\n");
|
|
231
|
-
const tagIndex = lines.findIndex((line) => /^tags:\s*$/.test(line.trim()));
|
|
232
|
-
if (tagIndex === -1)
|
|
233
|
-
return undefined;
|
|
234
|
-
const tags = [];
|
|
235
|
-
for (let index = tagIndex + 1; index < lines.length; index++) {
|
|
236
|
-
const line = lines[index] ?? "";
|
|
237
|
-
const trimmed = line.trim();
|
|
238
|
-
if (!trimmed)
|
|
239
|
-
continue;
|
|
240
|
-
if (!line.startsWith(" "))
|
|
241
|
-
break;
|
|
242
|
-
const match = trimmed.match(/^-\s+(.+?)\s*$/);
|
|
243
|
-
if (!match) {
|
|
244
|
-
throw new WorkflowValidationError("Workflow frontmatter `tags` must contain only dash-prefixed list items when declared as a block list.");
|
|
245
|
-
}
|
|
246
|
-
const tag = stripMatchingQuotes(match[1]?.trim() ?? "");
|
|
247
|
-
if (tag)
|
|
248
|
-
tags.push(tag);
|
|
249
|
-
}
|
|
250
|
-
return tags.length > 0 ? tags : undefined;
|
|
251
|
-
}
|
|
252
|
-
function stripMatchingQuotes(value) {
|
|
253
|
-
if (value.length >= 2) {
|
|
254
|
-
const quote = value[0];
|
|
255
|
-
if ((quote === '"' || quote === "'") && value[value.length - 1] === quote) {
|
|
256
|
-
return value.slice(1, -1).trim();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return value;
|
|
260
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|