@skillsmith/core 0.11.4 → 0.11.6
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 +60 -0
- package/README.md +10 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +20 -5
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/client.private-registry.d.ts +80 -0
- package/dist/src/api/client.private-registry.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.js +97 -0
- package/dist/src/api/client.private-registry.js.map +1 -0
- package/dist/src/api/client.private-registry.test.d.ts +9 -0
- package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.test.js +127 -0
- package/dist/src/api/client.private-registry.test.js.map +1 -0
- package/dist/src/api/client.test.js +39 -0
- package/dist/src/api/client.test.js.map +1 -1
- package/dist/src/api/client.toSkill.test.d.ts +25 -0
- package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
- package/dist/src/api/client.toSkill.test.js +97 -0
- package/dist/src/api/client.toSkill.test.js.map +1 -0
- package/dist/src/api/client.token-refresh.d.ts +8 -0
- package/dist/src/api/client.token-refresh.d.ts.map +1 -1
- package/dist/src/api/client.token-refresh.js +26 -0
- package/dist/src/api/client.token-refresh.js.map +1 -1
- package/dist/src/api/client.token-refresh.test.js +82 -0
- package/dist/src/api/client.token-refresh.test.js.map +1 -1
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +9 -0
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/security-summary.d.ts +73 -0
- package/dist/src/api/security-summary.d.ts.map +1 -0
- package/dist/src/api/security-summary.js +88 -0
- package/dist/src/api/security-summary.js.map +1 -0
- package/dist/src/api/security-summary.test.d.ts +9 -0
- package/dist/src/api/security-summary.test.d.ts.map +1 -0
- package/dist/src/api/security-summary.test.js +94 -0
- package/dist/src/api/security-summary.test.js.map +1 -0
- package/dist/src/compatibility/slugs.d.ts +7 -3
- package/dist/src/compatibility/slugs.d.ts.map +1 -1
- package/dist/src/compatibility/slugs.js +11 -4
- package/dist/src/compatibility/slugs.js.map +1 -1
- package/dist/src/config/config-atomic-write.d.ts +30 -10
- package/dist/src/config/config-atomic-write.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.js +33 -80
- package/dist/src/config/config-atomic-write.js.map +1 -1
- package/dist/src/config/config-atomic-write.test.d.ts +8 -0
- package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.test.js +64 -12
- package/dist/src/config/config-atomic-write.test.js.map +1 -1
- package/dist/src/config/owned-lock.acquire.d.ts +53 -0
- package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
- package/dist/src/config/owned-lock.acquire.js +140 -0
- package/dist/src/config/owned-lock.acquire.js.map +1 -0
- package/dist/src/config/owned-lock.claim.d.ts +84 -0
- package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
- package/dist/src/config/owned-lock.claim.js +310 -0
- package/dist/src/config/owned-lock.claim.js.map +1 -0
- package/dist/src/config/owned-lock.d.ts +113 -0
- package/dist/src/config/owned-lock.d.ts.map +1 -0
- package/dist/src/config/owned-lock.js +121 -0
- package/dist/src/config/owned-lock.js.map +1 -0
- package/dist/src/config/owned-lock.test.d.ts +12 -0
- package/dist/src/config/owned-lock.test.d.ts.map +1 -0
- package/dist/src/config/owned-lock.test.js +412 -0
- package/dist/src/config/owned-lock.test.js.map +1 -0
- package/dist/src/config/owned-lock.types.d.ts +78 -0
- package/dist/src/config/owned-lock.types.d.ts.map +1 -0
- package/dist/src/config/owned-lock.types.js +30 -0
- package/dist/src/config/owned-lock.types.js.map +1 -0
- package/dist/src/db/createDatabase.d.ts.map +1 -1
- package/dist/src/db/createDatabase.js +19 -7
- package/dist/src/db/createDatabase.js.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.d.ts +8 -0
- package/dist/src/db/drivers/sqljsDriver.d.ts.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.js +24 -2
- package/dist/src/db/drivers/sqljsDriver.js.map +1 -1
- package/dist/src/db/migration-runner.d.ts.map +1 -1
- package/dist/src/db/migration-runner.js +23 -2
- package/dist/src/db/migration-runner.js.map +1 -1
- package/dist/src/db/schema.d.ts.map +1 -1
- package/dist/src/db/schema.js +29 -3
- package/dist/src/db/schema.js.map +1 -1
- package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
- package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
- package/dist/src/embeddings/hnsw-store.exports.js +10 -0
- package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
- package/dist/src/embeddings/index.d.ts +1 -2
- package/dist/src/embeddings/index.d.ts.map +1 -1
- package/dist/src/embeddings/index.js +17 -3
- package/dist/src/embeddings/index.js.map +1 -1
- package/dist/src/embeddings/probe.d.ts.map +1 -1
- package/dist/src/embeddings/probe.js +4 -8
- package/dist/src/embeddings/probe.js.map +1 -1
- package/dist/src/exports/services.d.ts +5 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +27 -0
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/index.d.ts +2 -2
- package/dist/src/install/index.d.ts.map +1 -1
- package/dist/src/install/index.js +1 -1
- package/dist/src/install/index.js.map +1 -1
- package/dist/src/install/paths.d.ts +165 -1
- package/dist/src/install/paths.d.ts.map +1 -1
- package/dist/src/install/paths.js +254 -1
- package/dist/src/install/paths.js.map +1 -1
- package/dist/src/install/paths.test.js +174 -2
- package/dist/src/install/paths.test.js.map +1 -1
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
- package/dist/src/services/context-words.d.ts +40 -0
- package/dist/src/services/context-words.d.ts.map +1 -0
- package/dist/src/services/context-words.js +178 -0
- package/dist/src/services/context-words.js.map +1 -0
- package/dist/src/services/context-words.test.d.ts +2 -0
- package/dist/src/services/context-words.test.d.ts.map +1 -0
- package/dist/src/services/context-words.test.js +94 -0
- package/dist/src/services/context-words.test.js.map +1 -0
- package/dist/src/services/recommend-guard.d.ts +28 -0
- package/dist/src/services/recommend-guard.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.js +34 -0
- package/dist/src/services/recommend-guard.js.map +1 -0
- package/dist/src/services/recommend-guard.test.d.ts +2 -0
- package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.test.js +28 -0
- package/dist/src/services/recommend-guard.test.js.map +1 -0
- package/dist/src/services/skill-installation.content.d.ts +96 -0
- package/dist/src/services/skill-installation.content.d.ts.map +1 -0
- package/dist/src/services/skill-installation.content.js +319 -0
- package/dist/src/services/skill-installation.content.js.map +1 -0
- package/dist/src/services/skill-installation.helpers.d.ts +32 -1
- package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
- package/dist/src/services/skill-installation.helpers.js +62 -7
- package/dist/src/services/skill-installation.helpers.js.map +1 -1
- package/dist/src/services/skill-installation.helpers.test.js +108 -2
- package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
- package/dist/src/services/skill-installation.io.d.ts +22 -1
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +110 -6
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.service.d.ts +22 -1
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.js +30 -5
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/services/skill-installation.types.d.ts +42 -1
- package/dist/src/services/skill-installation.types.d.ts.map +1 -1
- package/dist/src/services/skill-installation.types.js.map +1 -1
- package/dist/src/services/skill-manifest.d.ts +21 -0
- package/dist/src/services/skill-manifest.d.ts.map +1 -1
- package/dist/src/services/skill-manifest.js +51 -6
- package/dist/src/services/skill-manifest.js.map +1 -1
- package/dist/src/services/skill-manifest.test.d.ts +2 -0
- package/dist/src/services/skill-manifest.test.d.ts.map +1 -0
- package/dist/src/services/skill-manifest.test.js +202 -0
- package/dist/src/services/skill-manifest.test.js.map +1 -0
- package/dist/src/services/skill-resolution.d.ts +67 -0
- package/dist/src/services/skill-resolution.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.js +67 -0
- package/dist/src/services/skill-resolution.js.map +1 -0
- package/dist/src/services/skill-resolution.test.d.ts +2 -0
- package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.test.js +143 -0
- package/dist/src/services/skill-resolution.test.js.map +1 -0
- package/dist/src/types/skill.d.ts +10 -0
- package/dist/src/types/skill.d.ts.map +1 -1
- package/dist/src/types.d.ts +9 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +4 -0
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/quiet-mode.d.ts +21 -0
- package/dist/src/utils/quiet-mode.d.ts.map +1 -0
- package/dist/src/utils/quiet-mode.js +26 -0
- package/dist/src/utils/quiet-mode.js.map +1 -0
- package/dist/tests/EmbeddingService.test.js +58 -1
- package/dist/tests/EmbeddingService.test.js.map +1 -1
- package/dist/tests/SecurityScanner.performance.test.js +5 -2
- package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
- package/dist/tests/db/database-abstraction.test.js +45 -1
- package/dist/tests/db/database-abstraction.test.js.map +1 -1
- package/dist/tests/db/migration-runner-race.test.d.ts +2 -0
- package/dist/tests/db/migration-runner-race.test.d.ts.map +1 -0
- package/dist/tests/db/migration-runner-race.test.js +75 -0
- package/dist/tests/db/migration-runner-race.test.js.map +1 -0
- package/dist/tests/db/schema-async.test.js +37 -1
- package/dist/tests/db/schema-async.test.js.map +1 -1
- package/dist/tests/db/sqljsDriver.test.js +63 -1
- package/dist/tests/db/sqljsDriver.test.js.map +1 -1
- package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
- package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
- package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
- package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
- package/dist/tests/schema.test.js +43 -1
- package/dist/tests/schema.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
- package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
- package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts +34 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +246 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js +112 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
- package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
- package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
- package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
- package/dist/tests/utils/quiet-mode.test.js +42 -0
- package/dist/tests/utils/quiet-mode.test.js.map +1 -0
- package/package.json +17 -5
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ScanReport, ScannerOptions } from '../security/index.js';
|
|
9
9
|
import type { TrustTier } from '../types/skill.js';
|
|
10
10
|
import type { DependencyDeclaration } from '../types/dependencies.js';
|
|
11
|
+
import type { ClientId } from '../install/paths.js';
|
|
11
12
|
/**
|
|
12
13
|
* Callback invoked during install/uninstall to report progress.
|
|
13
14
|
* CLI wires this to an `ora` spinner; mcp-server wires to MCP notifications.
|
|
@@ -15,6 +16,39 @@ import type { DependencyDeclaration } from '../types/dependencies.js';
|
|
|
15
16
|
export type ProgressCallback = (stage: string, detail: string) => void;
|
|
16
17
|
/** Action to take when a conflict is detected during skill update */
|
|
17
18
|
export type ConflictAction = 'overwrite' | 'merge' | 'cancel';
|
|
19
|
+
/**
|
|
20
|
+
* SMI-5905: a packaged skill's files as a flat { relativePath: fileText } map,
|
|
21
|
+
* e.g. `{ "SKILL.md": "...", "scripts/foo.sh": "..." }`.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately duplicated (not imported) from the mcp-server's own
|
|
24
|
+
* `SkillContent` (zod-inferred in `registry-tools.ts`) — core cannot depend
|
|
25
|
+
* on mcp-server. Keep the shape in sync if it ever changes.
|
|
26
|
+
*/
|
|
27
|
+
export type SkillContent = Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Options for `installFromContent()` — the content-based install path used by
|
|
30
|
+
* private-registry installs (SMI-5905). Distinct from `InstallOptions` (used
|
|
31
|
+
* by the GitHub-fetch `install()` path): the caller has already resolved
|
|
32
|
+
* `skillId`/`version`/`content` (e.g. via a registry `getContent()` call one
|
|
33
|
+
* layer up) rather than supplying a skillId for `install()` to resolve itself.
|
|
34
|
+
*/
|
|
35
|
+
export interface InstallFromContentOptions {
|
|
36
|
+
/** Registry skill ID (author/name format) this content was published under. */
|
|
37
|
+
skillId: string;
|
|
38
|
+
/**
|
|
39
|
+
* Already-resolved semver version string. `installFromContent()` does not
|
|
40
|
+
* choose a version itself — the caller resolves "no version specified"
|
|
41
|
+
* (defaulting to the most-recently-published version) before calling this,
|
|
42
|
+
* mirroring `registry-tools.live.ts`'s `get(teamId, skillId, version)`
|
|
43
|
+
* convention, so `install` and `get` never disagree about what "no
|
|
44
|
+
* version specified" means.
|
|
45
|
+
*/
|
|
46
|
+
version: string;
|
|
47
|
+
/** Packaged skill files; must include a non-empty `"SKILL.md"` entry. */
|
|
48
|
+
content: SkillContent;
|
|
49
|
+
/** Force reinstall if the skill is already installed (mirrors `InstallOptions.force`). */
|
|
50
|
+
force?: boolean;
|
|
51
|
+
}
|
|
18
52
|
/** Options for the install operation */
|
|
19
53
|
export interface InstallOptions {
|
|
20
54
|
/** Force reinstall if the skill already exists */
|
|
@@ -63,7 +97,7 @@ export interface OptimizationInfo {
|
|
|
63
97
|
* stable across releases. Adding a new code requires (a) a real new failure
|
|
64
98
|
* return path and (b) updating downstream funnel queries.
|
|
65
99
|
*/
|
|
66
|
-
export type InstallErrorCode = 'REGISTRY_LOOKUP_UNAVAILABLE' | 'REGISTRY_SKILL_NOT_FOUND' | 'QUARANTINED' | 'ALREADY_INSTALLED' | 'FETCH_FAILED' | 'VALIDATION_FAILED' | 'SKIP_SCAN_FORBIDDEN' | 'SCAN_REJECTED' | 'CONFIRMATION_REQUIRED' | 'UNKNOWN';
|
|
100
|
+
export type InstallErrorCode = 'REGISTRY_LOOKUP_UNAVAILABLE' | 'REGISTRY_SKILL_NOT_FOUND' | 'QUARANTINED' | 'ALREADY_INSTALLED' | 'FETCH_FAILED' | 'VALIDATION_FAILED' | 'SKIP_SCAN_FORBIDDEN' | 'SCAN_REJECTED' | 'CONFIRMATION_REQUIRED' | 'INVALID_CONTENT' | 'UNKNOWN';
|
|
67
101
|
/** Result of an install operation */
|
|
68
102
|
export interface InstallResult {
|
|
69
103
|
success: boolean;
|
|
@@ -143,6 +177,13 @@ export interface SkillManifestEntry {
|
|
|
143
177
|
pinnedVersion?: string;
|
|
144
178
|
/** How updates are handled */
|
|
145
179
|
updatePolicy?: 'auto' | 'manual' | 'never';
|
|
180
|
+
/**
|
|
181
|
+
* SMI-5894 (Wave 1 Step 3): which client this installation targets.
|
|
182
|
+
* Absent on manifest entries written before multi-client re-keying —
|
|
183
|
+
* treat a missing value as the canonical client (`claude-code`), matching
|
|
184
|
+
* `manifestKeyFor()`'s own default.
|
|
185
|
+
*/
|
|
186
|
+
client?: ClientId;
|
|
146
187
|
}
|
|
147
188
|
/** Manifest tracking all installed skills */
|
|
148
189
|
export interface SkillManifest {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-installation.types.d.ts","sourceRoot":"","sources":["../../../src/services/skill-installation.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"skill-installation.types.d.ts","sourceRoot":"","sources":["../../../src/services/skill-installation.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-installation.types.js","sourceRoot":"","sources":["../../../src/services/skill-installation.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"skill-installation.types.js","sourceRoot":"","sources":["../../../src/services/skill-installation.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyQH,+EAA+E;AAC/E,iBAAiB;AACjB,+EAA+E;AAE/E,iDAAiD;AACjD,MAAM,CAAC,MAAM,0BAA0B,GAAsC;IAC3E,QAAQ,EAAE;QACR,aAAa,EAAE,EAAE;QACjB,gBAAgB,EAAE,SAAS;KAC5B;IACD,OAAO,EAAE;QACP,aAAa,EAAE,EAAE;QACjB,gBAAgB,EAAE,SAAS;KAC5B;IACD,SAAS,EAAE;QACT,aAAa,EAAE,EAAE;QACjB,gBAAgB,EAAE,SAAS;KAC5B;IACD,KAAK,EAAE;QACL,aAAa,EAAE,GAAG;QAClB,gBAAgB,EAAE,UAAU;KAC7B;IACD,YAAY,EAAE;QACZ,aAAa,EAAE,EAAE;QACjB,gBAAgB,EAAE,OAAO;KAC1B;IACD,OAAO,EAAE;QACP,aAAa,EAAE,EAAE;QACjB,gBAAgB,EAAE,OAAO;KAC1B;IACD,6BAA6B;IAC7B,QAAQ,EAAE;QACR,aAAa,EAAE,EAAE;QACjB,gBAAgB,EAAE,SAAS;KAC5B;IACD,UAAU,EAAE;QACV,aAAa,EAAE,EAAE,EAAE,+DAA+D;QAClF,gBAAgB,EAAE,OAAO;KAC1B;CACF,CAAA"}
|
|
@@ -11,7 +11,28 @@ import type { SkillManifest } from './skill-installation.types.js';
|
|
|
11
11
|
export declare class ManifestManager {
|
|
12
12
|
private readonly manifestPath;
|
|
13
13
|
constructor(manifestPath: string);
|
|
14
|
+
/**
|
|
15
|
+
* SMI-6007: distinguishes "no manifest yet" (ENOENT — legitimate first-run
|
|
16
|
+
* case, safe to synthesize an empty manifest) from "a manifest file exists
|
|
17
|
+
* but couldn't be read/parsed" (corrupt JSON, permission error, I/O
|
|
18
|
+
* failure). The latter used to be silently swallowed into the same empty
|
|
19
|
+
* manifest, which is a real data-loss risk: a caller that then `save()`s
|
|
20
|
+
* that empty snapshot back out would erase every previously-recorded
|
|
21
|
+
* install. Now it throws loudly instead, so a corrupt manifest surfaces as
|
|
22
|
+
* an error rather than silently wiping state on the next write.
|
|
23
|
+
*/
|
|
14
24
|
load(): Promise<SkillManifest>;
|
|
25
|
+
/**
|
|
26
|
+
* SMI-6007: the temp filename now includes a `randomUUID()` suffix (not
|
|
27
|
+
* just `process.pid`) — two concurrent `save()` calls in the same process
|
|
28
|
+
* previously collided on an identical `.tmp.<pid>` path, letting one
|
|
29
|
+
* call's temp file win the write while the other's `rename()` either
|
|
30
|
+
* clobbered it mid-flight or failed outright. Each call now owns a
|
|
31
|
+
* uniquely-named temp file for its own lifetime. The write+rename is
|
|
32
|
+
* wrapped in try/catch so a failure best-effort removes only *this
|
|
33
|
+
* invocation's* temp file before rethrowing the original error (mirrors
|
|
34
|
+
* `sqljsDriver.ts`'s `persist()`, SMI-5997) — the error is never swallowed.
|
|
35
|
+
*/
|
|
15
36
|
save(manifest: SkillManifest): Promise<void>;
|
|
16
37
|
acquireLock(): Promise<void>;
|
|
17
38
|
releaseLock(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-manifest.d.ts","sourceRoot":"","sources":["../../../src/services/skill-manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"skill-manifest.d.ts","sourceRoot":"","sources":["../../../src/services/skill-manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAKlE;;;GAGG;AACH,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAAZ,YAAY,EAAE,MAAM;IAEjD;;;;;;;;;OASG;IACG,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;IAuBpC;;;;;;;;;;OAUG;IACG,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB5C,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B5B,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B,YAAY,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAUxF"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
8
9
|
const MANIFEST_LOCK_TIMEOUT_MS = 30000;
|
|
9
10
|
const MANIFEST_LOCK_RETRY_MS = 100;
|
|
10
11
|
/**
|
|
@@ -16,20 +17,64 @@ export class ManifestManager {
|
|
|
16
17
|
constructor(manifestPath) {
|
|
17
18
|
this.manifestPath = manifestPath;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* SMI-6007: distinguishes "no manifest yet" (ENOENT — legitimate first-run
|
|
22
|
+
* case, safe to synthesize an empty manifest) from "a manifest file exists
|
|
23
|
+
* but couldn't be read/parsed" (corrupt JSON, permission error, I/O
|
|
24
|
+
* failure). The latter used to be silently swallowed into the same empty
|
|
25
|
+
* manifest, which is a real data-loss risk: a caller that then `save()`s
|
|
26
|
+
* that empty snapshot back out would erase every previously-recorded
|
|
27
|
+
* install. Now it throws loudly instead, so a corrupt manifest surfaces as
|
|
28
|
+
* an error rather than silently wiping state on the next write.
|
|
29
|
+
*/
|
|
19
30
|
async load() {
|
|
31
|
+
let content;
|
|
32
|
+
try {
|
|
33
|
+
content = await fs.readFile(this.manifestPath, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT') {
|
|
37
|
+
return { version: '1.0.0', installedSkills: {} };
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
20
41
|
try {
|
|
21
|
-
const content = await fs.readFile(this.manifestPath, 'utf-8');
|
|
22
42
|
return JSON.parse(content);
|
|
23
43
|
}
|
|
24
|
-
catch {
|
|
25
|
-
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Error('Manifest file at ' +
|
|
46
|
+
this.manifestPath +
|
|
47
|
+
' exists but is corrupt/unparseable: ' +
|
|
48
|
+
(error instanceof Error ? error.message : String(error)));
|
|
26
49
|
}
|
|
27
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* SMI-6007: the temp filename now includes a `randomUUID()` suffix (not
|
|
53
|
+
* just `process.pid`) — two concurrent `save()` calls in the same process
|
|
54
|
+
* previously collided on an identical `.tmp.<pid>` path, letting one
|
|
55
|
+
* call's temp file win the write while the other's `rename()` either
|
|
56
|
+
* clobbered it mid-flight or failed outright. Each call now owns a
|
|
57
|
+
* uniquely-named temp file for its own lifetime. The write+rename is
|
|
58
|
+
* wrapped in try/catch so a failure best-effort removes only *this
|
|
59
|
+
* invocation's* temp file before rethrowing the original error (mirrors
|
|
60
|
+
* `sqljsDriver.ts`'s `persist()`, SMI-5997) — the error is never swallowed.
|
|
61
|
+
*/
|
|
28
62
|
async save(manifest) {
|
|
29
63
|
await fs.mkdir(path.dirname(this.manifestPath), { recursive: true });
|
|
30
|
-
const tempPath = this.manifestPath + '.tmp.' + process.pid;
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
const tempPath = this.manifestPath + '.tmp.' + process.pid + '.' + randomUUID();
|
|
65
|
+
try {
|
|
66
|
+
await fs.writeFile(tempPath, JSON.stringify(manifest, null, 2));
|
|
67
|
+
await fs.rename(tempPath, this.manifestPath);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
try {
|
|
71
|
+
await fs.unlink(tempPath);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// best-effort cleanup — surface the original error either way
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
33
78
|
}
|
|
34
79
|
async acquireLock() {
|
|
35
80
|
const lockPath = this.manifestPath + '.lock';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-manifest.js","sourceRoot":"","sources":["../../../src/services/skill-manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"skill-manifest.js","sourceRoot":"","sources":["../../../src/services/skill-manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-manifest.test.d.ts","sourceRoot":"","sources":["../../../src/services/skill-manifest.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for ManifestManager concurrency hardening (SMI-6007)
|
|
3
|
+
* @module @skillsmith/core/services/skill-manifest.test
|
|
4
|
+
* @see SMI-6007: manifest write-path concurrency hardening — split out of
|
|
5
|
+
* the flaky-test investigation SMI-6004. Two confirmed hazards:
|
|
6
|
+
* 1. performUninstall() bypassed the lock (fixed in
|
|
7
|
+
* skill-installation.helpers.ts — see its own test coverage).
|
|
8
|
+
* 2. save() computed a temp filename from `process.pid` alone, so two
|
|
9
|
+
* concurrent `save()` calls in the same process collided on the
|
|
10
|
+
* identical `.tmp.<pid>` path.
|
|
11
|
+
* Plus a load()-hardening item: distinguish "no manifest yet" (ENOENT,
|
|
12
|
+
* legitimate first run) from "manifest exists but is corrupt/unreadable"
|
|
13
|
+
* (should throw, not silently return an empty manifest that a later
|
|
14
|
+
* save() could use to erase real state).
|
|
15
|
+
*
|
|
16
|
+
* These tests use a real temp directory + real fs/promises calls (not an
|
|
17
|
+
* in-memory mock) so the concurrent cases exercise actual OS-level file
|
|
18
|
+
* operations — `acquireLock()`'s `wx`-flag file creation is only
|
|
19
|
+
* meaningfully racy against real fs semantics.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
// The Node ESM module namespace object is not configurable, so a plain
|
|
25
|
+
// `vi.spyOn(fs, 'readFile')` throws "Cannot redefine property" under this
|
|
26
|
+
// runtime. Instead, wrap the specific functions the write/rename-failure
|
|
27
|
+
// tests need to override in `vi.fn(actual.fn)` at mock-registration time —
|
|
28
|
+
// each defaults to forwarding to the real implementation (so every other
|
|
29
|
+
// test that doesn't override them still hits the real filesystem), and
|
|
30
|
+
// individual tests can layer a `mockImplementationOnce`/`mockRejectedValueOnce`
|
|
31
|
+
// on top without needing `vi.spyOn`.
|
|
32
|
+
vi.mock('fs/promises', async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal();
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
readFile: vi.fn(actual.readFile),
|
|
37
|
+
writeFile: vi.fn(actual.writeFile),
|
|
38
|
+
rename: vi.fn(actual.rename),
|
|
39
|
+
unlink: vi.fn(actual.unlink),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
import * as fs from 'fs/promises';
|
|
43
|
+
import { ManifestManager } from './skill-manifest.js';
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Helpers
|
|
46
|
+
// ============================================================================
|
|
47
|
+
function makeEntry(overrides = {}) {
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
return {
|
|
50
|
+
id: overrides.id ?? 'author/skill',
|
|
51
|
+
name: overrides.name ?? 'skill',
|
|
52
|
+
version: overrides.version ?? '1.0.0',
|
|
53
|
+
source: overrides.source ?? 'github:author/skill',
|
|
54
|
+
installPath: overrides.installPath ?? '/tmp/skill',
|
|
55
|
+
installedAt: overrides.installedAt ?? now,
|
|
56
|
+
lastUpdated: overrides.lastUpdated ?? now,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Tests
|
|
62
|
+
// ============================================================================
|
|
63
|
+
describe('ManifestManager concurrency hardening (SMI-6007)', () => {
|
|
64
|
+
let tmpDir;
|
|
65
|
+
let manifestPath;
|
|
66
|
+
let manager;
|
|
67
|
+
beforeEach(async () => {
|
|
68
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skillsmith-manifest-test-'));
|
|
69
|
+
manifestPath = path.join(tmpDir, 'manifest.json');
|
|
70
|
+
manager = new ManifestManager(manifestPath);
|
|
71
|
+
});
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
// --------------------------------------------------------------------------
|
|
77
|
+
// Item 1: two direct save() calls overlap; neither throws (temp-file fix)
|
|
78
|
+
// --------------------------------------------------------------------------
|
|
79
|
+
describe('save() same-pid temp-file collision', () => {
|
|
80
|
+
it('two overlapping save() calls both resolve without throwing', async () => {
|
|
81
|
+
const first = {
|
|
82
|
+
version: '1.0.0',
|
|
83
|
+
installedSkills: { a: makeEntry({ id: 'a' }) },
|
|
84
|
+
};
|
|
85
|
+
const second = {
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
installedSkills: { b: makeEntry({ id: 'b' }) },
|
|
88
|
+
};
|
|
89
|
+
await expect(Promise.all([manager.save(first), manager.save(second)])).resolves.toBeDefined();
|
|
90
|
+
// Whichever write landed last, the file must be a full, valid write —
|
|
91
|
+
// never a partial/corrupt mix of the two.
|
|
92
|
+
const loaded = await manager.load();
|
|
93
|
+
expect(Object.keys(loaded.installedSkills)).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
it('does not leave stray .tmp.* files behind after concurrent saves', async () => {
|
|
96
|
+
await Promise.all([
|
|
97
|
+
manager.save({ version: '1.0.0', installedSkills: {} }),
|
|
98
|
+
manager.save({ version: '1.0.0', installedSkills: {} }),
|
|
99
|
+
]);
|
|
100
|
+
const files = await fs.readdir(tmpDir);
|
|
101
|
+
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
105
|
+
// Item 2: two concurrent updateSafely() calls on DISTINCT entries; both
|
|
106
|
+
// survive (validates the lock actually serializes correctly)
|
|
107
|
+
// --------------------------------------------------------------------------
|
|
108
|
+
describe('updateSafely() serializes concurrent updates to distinct entries', () => {
|
|
109
|
+
it('both entries survive when two updateSafely() calls run concurrently', async () => {
|
|
110
|
+
await manager.save({ version: '1.0.0', installedSkills: {} });
|
|
111
|
+
await Promise.all([
|
|
112
|
+
manager.updateSafely((m) => ({
|
|
113
|
+
...m,
|
|
114
|
+
installedSkills: { ...m.installedSkills, 'skill-a': makeEntry({ id: 'skill-a' }) },
|
|
115
|
+
})),
|
|
116
|
+
manager.updateSafely((m) => ({
|
|
117
|
+
...m,
|
|
118
|
+
installedSkills: { ...m.installedSkills, 'skill-b': makeEntry({ id: 'skill-b' }) },
|
|
119
|
+
})),
|
|
120
|
+
]);
|
|
121
|
+
const loaded = await manager.load();
|
|
122
|
+
expect(Object.keys(loaded.installedSkills).sort()).toEqual(['skill-a', 'skill-b']);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// --------------------------------------------------------------------------
|
|
126
|
+
// Item 4: load() distinguishes ENOENT (expected first run) from a
|
|
127
|
+
// corrupt/unreadable manifest (should throw, not silently go empty)
|
|
128
|
+
// --------------------------------------------------------------------------
|
|
129
|
+
describe('load() distinguishes missing vs corrupt manifest', () => {
|
|
130
|
+
it('returns an empty manifest when the file does not exist (ENOENT — expected first run)', async () => {
|
|
131
|
+
const loaded = await manager.load();
|
|
132
|
+
expect(loaded).toEqual({ version: '1.0.0', installedSkills: {} });
|
|
133
|
+
});
|
|
134
|
+
it('throws (does not silently return an empty manifest) when the file contains unparseable JSON', async () => {
|
|
135
|
+
await fs.writeFile(manifestPath, '{ not valid json');
|
|
136
|
+
await expect(manager.load()).rejects.toThrow(/corrupt|unparseable/i);
|
|
137
|
+
});
|
|
138
|
+
it('rethrows a non-ENOENT read error (e.g. EACCES) instead of returning an empty manifest', async () => {
|
|
139
|
+
const permissionError = Object.assign(new Error('EACCES: permission denied'), {
|
|
140
|
+
code: 'EACCES',
|
|
141
|
+
});
|
|
142
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce(permissionError);
|
|
143
|
+
await expect(manager.load()).rejects.toThrow('EACCES');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// --------------------------------------------------------------------------
|
|
147
|
+
// Item 5: inject a write/rename failure — verify the unique-temp-file
|
|
148
|
+
// cleanup removes only that invocation's own temp file and the original
|
|
149
|
+
// error still propagates (not swallowed)
|
|
150
|
+
// --------------------------------------------------------------------------
|
|
151
|
+
describe('save() write/rename failure cleanup', () => {
|
|
152
|
+
it('on a writeFile failure, the temp file is cleaned up and the original error propagates', async () => {
|
|
153
|
+
const originalError = new Error('ENOSPC: no space left on device');
|
|
154
|
+
vi.mocked(fs.writeFile).mockRejectedValueOnce(originalError);
|
|
155
|
+
await expect(manager.save({ version: '1.0.0', installedSkills: {} })).rejects.toThrow(originalError);
|
|
156
|
+
const files = await fs.readdir(tmpDir).catch(() => []);
|
|
157
|
+
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
it('on a rename failure, the temp file is cleaned up, the original error propagates, and the manifest is untouched', async () => {
|
|
160
|
+
await manager.save({
|
|
161
|
+
version: '1.0.0',
|
|
162
|
+
installedSkills: { existing: makeEntry({ id: 'existing' }) },
|
|
163
|
+
});
|
|
164
|
+
const originalError = new Error('EPERM: operation not permitted');
|
|
165
|
+
vi.mocked(fs.rename).mockRejectedValueOnce(originalError);
|
|
166
|
+
await expect(manager.save({ version: '1.0.0', installedSkills: { other: makeEntry({ id: 'other' }) } })).rejects.toThrow(originalError);
|
|
167
|
+
const loaded = await manager.load();
|
|
168
|
+
expect(Object.keys(loaded.installedSkills)).toEqual(['existing']);
|
|
169
|
+
const files = await fs.readdir(tmpDir);
|
|
170
|
+
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
it('a failed save() only removes its own temp file, leaving a concurrently in-flight one untouched', async () => {
|
|
173
|
+
// call A's rename fails and must be cleaned up; call B's rename must
|
|
174
|
+
// succeed normally. Because each call's temp filename is now
|
|
175
|
+
// per-invocation-unique (SMI-6007), A's cleanup cannot touch B's file.
|
|
176
|
+
// mockRejectedValueOnce queues exactly one rejection ahead of the
|
|
177
|
+
// default (real) implementation, so whichever of the two concurrent
|
|
178
|
+
// renames happens to run first gets it — order isn't asserted below.
|
|
179
|
+
vi.mocked(fs.rename).mockRejectedValueOnce(new Error('simulated rename failure for call A'));
|
|
180
|
+
const callA = manager.save({
|
|
181
|
+
version: '1.0.0',
|
|
182
|
+
installedSkills: { a: makeEntry({ id: 'a' }) },
|
|
183
|
+
});
|
|
184
|
+
const callB = manager.save({
|
|
185
|
+
version: '1.0.0',
|
|
186
|
+
installedSkills: { b: makeEntry({ id: 'b' }) },
|
|
187
|
+
});
|
|
188
|
+
const results = await Promise.allSettled([callA, callB]);
|
|
189
|
+
// Order between the two concurrent calls isn't guaranteed by real fs
|
|
190
|
+
// I/O scheduling — assert the invariant that matters: exactly one
|
|
191
|
+
// call's rename failed (and was cleaned up), the other succeeded, and
|
|
192
|
+
// no cross-contamination between their distinct temp files occurred.
|
|
193
|
+
const rejected = results.filter((r) => r.status === 'rejected');
|
|
194
|
+
const fulfilled = results.filter((r) => r.status === 'fulfilled');
|
|
195
|
+
expect(rejected).toHaveLength(1);
|
|
196
|
+
expect(fulfilled).toHaveLength(1);
|
|
197
|
+
const files = await fs.readdir(tmpDir);
|
|
198
|
+
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
//# sourceMappingURL=skill-manifest.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-manifest.test.js","sourceRoot":"","sources":["../../../src/services/skill-manifest.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared API-first / local-fallback skill resolution.
|
|
3
|
+
* @module @skillsmith/core/services/skill-resolution
|
|
4
|
+
* @see SMI-1183: original API-first / local-fallback pattern, first written
|
|
5
|
+
* inline in the MCP `get_skill` tool.
|
|
6
|
+
* @see SMI-5896 (Wave 3, discovery-tool consistency): extracted here so
|
|
7
|
+
* `skill_compare` can share the exact same resolution flow instead of only
|
|
8
|
+
* ever querying the local cache — which, per SMI-5427, is no longer kept in
|
|
9
|
+
* sync with the remote-first registry, so a real, searchable registry skill
|
|
10
|
+
* was often simply absent locally and compare would report "not found" for
|
|
11
|
+
* a skill `search`/`get_skill` could both resolve fine.
|
|
12
|
+
*
|
|
13
|
+
* This module owns exactly the "try API, fall back to local DB, normalize
|
|
14
|
+
* not-found" flow — NOT the per-tool result-shape mapping (e.g. MCPSkill vs
|
|
15
|
+
* the compare tool's ExtendedSkill), which stays with each calling tool since
|
|
16
|
+
* those shapes genuinely differ and forcing one shared shape would just move
|
|
17
|
+
* the duplication rather than remove it.
|
|
18
|
+
*
|
|
19
|
+
* Known non-consumer (intentional): `lookupSkillFromRegistry()` in
|
|
20
|
+
* `@skillsmith/mcp-server/tools/install.helpers.ts` implements a similar
|
|
21
|
+
* API-first/local-fallback shape but a materially different contract —
|
|
22
|
+
* null-not-throw, no local fallback when the registry returns a skill with no
|
|
23
|
+
* `repo_url`, and a per-branch `quarantined` derivation this resolver has no
|
|
24
|
+
* concept of. See that function's docblock before attempting to unify them:
|
|
25
|
+
* doing so naively would relax an install-time security gate.
|
|
26
|
+
*/
|
|
27
|
+
import type { SkillsmithApiClient, ApiSearchResult } from '../api/client.js';
|
|
28
|
+
import type { SkillRepository } from '../repositories/SkillRepository.js';
|
|
29
|
+
import type { Skill } from '../types/skill.js';
|
|
30
|
+
/**
|
|
31
|
+
* Result of {@link resolveSkillApiFirst} — a discriminated union over the two
|
|
32
|
+
* possible sources so callers can branch on `source` and access the
|
|
33
|
+
* source-specific (snake_case API vs camelCase local DB) shape directly
|
|
34
|
+
* rather than a lossy, pre-merged shape.
|
|
35
|
+
*/
|
|
36
|
+
export type ResolvedSkill = {
|
|
37
|
+
source: 'api';
|
|
38
|
+
apiSkill: ApiSearchResult;
|
|
39
|
+
} | {
|
|
40
|
+
source: 'local';
|
|
41
|
+
dbSkill: Skill;
|
|
42
|
+
};
|
|
43
|
+
export interface ResolveSkillOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Request SKILL.md content alongside metadata on the API path. Only
|
|
46
|
+
* `get_skill` needs this; `skill_compare` never sets it. Default `false`.
|
|
47
|
+
*/
|
|
48
|
+
includeContent?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a skill by ID, API first with local-DB fallback (SMI-1183).
|
|
52
|
+
*
|
|
53
|
+
* - Tries the live API first (unless `apiClient.isOffline()`).
|
|
54
|
+
* - Falls back to the local `SkillRepository` on API-offline or API failure
|
|
55
|
+
* of any kind (network, 404, etc — all treated the same: fall through).
|
|
56
|
+
* - Throws a normalized `SkillsmithError(SKILL_NOT_FOUND)` when neither
|
|
57
|
+
* source has the skill. Every caller shares this exact message/details
|
|
58
|
+
* shape, so `get_skill` and `skill_compare` report not-found identically
|
|
59
|
+
* instead of two independently-worded messages that can drift.
|
|
60
|
+
*
|
|
61
|
+
* @param skillId - Already-validated/trimmed skill ID (author/name or UUID).
|
|
62
|
+
* @param apiClient - API client used for the primary lookup.
|
|
63
|
+
* @param skillRepository - Local repository used for the fallback lookup.
|
|
64
|
+
* @param options - Optional resolution behavior (e.g. `includeContent`).
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveSkillApiFirst(skillId: string, apiClient: SkillsmithApiClient, skillRepository: SkillRepository, options?: ResolveSkillOptions): Promise<ResolvedSkill>;
|
|
67
|
+
//# sourceMappingURL=skill-resolution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-resolution.d.ts","sourceRoot":"","sources":["../../../src/services/skill-resolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAUH,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE9C;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,KAAK,CAAA;CAAE,CAAA;AAEvC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,mBAAmB,EAC9B,eAAe,EAAE,eAAe,EAChC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,aAAa,CAAC,CA0BxB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared API-first / local-fallback skill resolution.
|
|
3
|
+
* @module @skillsmith/core/services/skill-resolution
|
|
4
|
+
* @see SMI-1183: original API-first / local-fallback pattern, first written
|
|
5
|
+
* inline in the MCP `get_skill` tool.
|
|
6
|
+
* @see SMI-5896 (Wave 3, discovery-tool consistency): extracted here so
|
|
7
|
+
* `skill_compare` can share the exact same resolution flow instead of only
|
|
8
|
+
* ever querying the local cache — which, per SMI-5427, is no longer kept in
|
|
9
|
+
* sync with the remote-first registry, so a real, searchable registry skill
|
|
10
|
+
* was often simply absent locally and compare would report "not found" for
|
|
11
|
+
* a skill `search`/`get_skill` could both resolve fine.
|
|
12
|
+
*
|
|
13
|
+
* This module owns exactly the "try API, fall back to local DB, normalize
|
|
14
|
+
* not-found" flow — NOT the per-tool result-shape mapping (e.g. MCPSkill vs
|
|
15
|
+
* the compare tool's ExtendedSkill), which stays with each calling tool since
|
|
16
|
+
* those shapes genuinely differ and forcing one shared shape would just move
|
|
17
|
+
* the duplication rather than remove it.
|
|
18
|
+
*
|
|
19
|
+
* Known non-consumer (intentional): `lookupSkillFromRegistry()` in
|
|
20
|
+
* `@skillsmith/mcp-server/tools/install.helpers.ts` implements a similar
|
|
21
|
+
* API-first/local-fallback shape but a materially different contract —
|
|
22
|
+
* null-not-throw, no local fallback when the registry returns a skill with no
|
|
23
|
+
* `repo_url`, and a per-branch `quarantined` derivation this resolver has no
|
|
24
|
+
* concept of. See that function's docblock before attempting to unify them:
|
|
25
|
+
* doing so naively would relax an install-time security gate.
|
|
26
|
+
*/
|
|
27
|
+
import { SkillsmithError, ErrorCodes } from '../errors.js';
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a skill by ID, API first with local-DB fallback (SMI-1183).
|
|
30
|
+
*
|
|
31
|
+
* - Tries the live API first (unless `apiClient.isOffline()`).
|
|
32
|
+
* - Falls back to the local `SkillRepository` on API-offline or API failure
|
|
33
|
+
* of any kind (network, 404, etc — all treated the same: fall through).
|
|
34
|
+
* - Throws a normalized `SkillsmithError(SKILL_NOT_FOUND)` when neither
|
|
35
|
+
* source has the skill. Every caller shares this exact message/details
|
|
36
|
+
* shape, so `get_skill` and `skill_compare` report not-found identically
|
|
37
|
+
* instead of two independently-worded messages that can drift.
|
|
38
|
+
*
|
|
39
|
+
* @param skillId - Already-validated/trimmed skill ID (author/name or UUID).
|
|
40
|
+
* @param apiClient - API client used for the primary lookup.
|
|
41
|
+
* @param skillRepository - Local repository used for the fallback lookup.
|
|
42
|
+
* @param options - Optional resolution behavior (e.g. `includeContent`).
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveSkillApiFirst(skillId, apiClient, skillRepository, options = {}) {
|
|
45
|
+
if (!apiClient.isOffline()) {
|
|
46
|
+
try {
|
|
47
|
+
const apiResponse = await apiClient.getSkill(skillId, {
|
|
48
|
+
includeContent: options.includeContent ?? false,
|
|
49
|
+
});
|
|
50
|
+
return { source: 'api', apiSkill: apiResponse.data };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
// SMI-1183: log and fall through to the local database for all errors —
|
|
54
|
+
// this allows local-only skills to resolve even when the API 404s.
|
|
55
|
+
console.warn('[skillsmith] API getSkill failed, using local database:', error.message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const dbSkill = skillRepository.findById(skillId);
|
|
59
|
+
if (!dbSkill) {
|
|
60
|
+
throw new SkillsmithError(ErrorCodes.SKILL_NOT_FOUND, `Skill "${skillId}" not found`, {
|
|
61
|
+
details: { id: skillId },
|
|
62
|
+
suggestion: 'Try searching for similar skills with the search tool',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return { source: 'local', dbSkill };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=skill-resolution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-resolution.js","sourceRoot":"","sources":["../../../src/services/skill-resolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AA8B1D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,SAA8B,EAC9B,eAAgC,EAChC,UAA+B,EAAE;IAEjC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE;gBACpD,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK;aAChD,CAAC,CAAA;YACF,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,CAAA;QACtD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,wEAAwE;YACxE,mEAAmE;YACnE,OAAO,CAAC,IAAI,CACV,yDAAyD,EACxD,KAAe,CAAC,OAAO,CACzB,CAAA;QACH,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACjD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,eAAe,CAAC,UAAU,CAAC,eAAe,EAAE,UAAU,OAAO,aAAa,EAAE;YACpF,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;YACxB,UAAU,EAAE,uDAAuD;SACpE,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;AACrC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-resolution.test.d.ts","sourceRoot":"","sources":["../../../src/services/skill-resolution.test.ts"],"names":[],"mappings":""}
|