@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared context-word extraction for skill recommendations.
|
|
3
|
+
* @module @skillsmith/core/services/context-words
|
|
4
|
+
* @see SMI-5986: CLI `recommend --context` (`recommend.ts:76`) and MCP
|
|
5
|
+
* `skill_recommend`'s `project_context` (`recommend.ts:127`) each derived a
|
|
6
|
+
* "context words" slice for the recommendation stack via
|
|
7
|
+
* `.filter((w) => w.length > 3)` — a bare length threshold that silently
|
|
8
|
+
* dropped real 2-3 character technical terms ("git", "ci", "aws", "sql")
|
|
9
|
+
* that happen to be short. When a caller's context consisted only of such
|
|
10
|
+
* terms, the resulting derived stack was empty and the SMI-5896
|
|
11
|
+
* empty-stack guard (`buildEmptyStackGuidance`, this module's sibling)
|
|
12
|
+
* fired even though the caller *did* supply usable context.
|
|
13
|
+
*
|
|
14
|
+
* Shared here (not duplicated per-twin) so CLI and MCP can't independently
|
|
15
|
+
* drift on what counts as noise vs. a real short technical term — the same
|
|
16
|
+
* class of duplication risk `buildEmptyStackGuidance` above already closed
|
|
17
|
+
* for the empty-stack message itself (plan-review correction, SMI-5984
|
|
18
|
+
* Wave 1: a bare length threshold "admits noise words" and leaves the two
|
|
19
|
+
* twins free to drift apart again independently).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Extract up to `maxWords` usable technical terms from free-text project
|
|
23
|
+
* context for the recommendation stack.
|
|
24
|
+
*
|
|
25
|
+
* Replaces the old `.filter((w) => w.length > 3)` threshold: real technical
|
|
26
|
+
* terms of 2-3 characters ("git", "ci", "aws", "sql") are now kept, while
|
|
27
|
+
* single-character tokens, punctuation-only tokens (including one that
|
|
28
|
+
* survives edge-stripping because it's made entirely of `+`/`#`, e.g. "++"
|
|
29
|
+
* or "+#" — PR review round 3, SMI-5986), and short English function words
|
|
30
|
+
* ("a", "the", "is", ...) are still dropped as noise. Words of 4+
|
|
31
|
+
* characters are unaffected — this only changes the outcome for the short
|
|
32
|
+
* end of the spectrum the length threshold got wrong.
|
|
33
|
+
*
|
|
34
|
+
* @param projectContext - Free-text project/context description. `null`/
|
|
35
|
+
* `undefined`/empty returns `[]`, mirroring both callers' pre-existing
|
|
36
|
+
* `if (project_context)` guard.
|
|
37
|
+
* @param maxWords - Maximum number of words to return (default 5).
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractContextWords(projectContext: string | undefined | null, maxWords?: number): string[];
|
|
40
|
+
//# sourceMappingURL=context-words.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-words.d.ts","sourceRoot":"","sources":["../../../src/services/context-words.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAmIH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACzC,QAAQ,GAAE,MAA0B,GACnC,MAAM,EAAE,CAcV"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared context-word extraction for skill recommendations.
|
|
3
|
+
* @module @skillsmith/core/services/context-words
|
|
4
|
+
* @see SMI-5986: CLI `recommend --context` (`recommend.ts:76`) and MCP
|
|
5
|
+
* `skill_recommend`'s `project_context` (`recommend.ts:127`) each derived a
|
|
6
|
+
* "context words" slice for the recommendation stack via
|
|
7
|
+
* `.filter((w) => w.length > 3)` — a bare length threshold that silently
|
|
8
|
+
* dropped real 2-3 character technical terms ("git", "ci", "aws", "sql")
|
|
9
|
+
* that happen to be short. When a caller's context consisted only of such
|
|
10
|
+
* terms, the resulting derived stack was empty and the SMI-5896
|
|
11
|
+
* empty-stack guard (`buildEmptyStackGuidance`, this module's sibling)
|
|
12
|
+
* fired even though the caller *did* supply usable context.
|
|
13
|
+
*
|
|
14
|
+
* Shared here (not duplicated per-twin) so CLI and MCP can't independently
|
|
15
|
+
* drift on what counts as noise vs. a real short technical term — the same
|
|
16
|
+
* class of duplication risk `buildEmptyStackGuidance` above already closed
|
|
17
|
+
* for the empty-stack message itself (plan-review correction, SMI-5984
|
|
18
|
+
* Wave 1: a bare length threshold "admits noise words" and leaves the two
|
|
19
|
+
* twins free to drift apart again independently).
|
|
20
|
+
*/
|
|
21
|
+
/** Maximum context words carried into the recommendation stack (matches both callers' pre-existing `.slice(0, 5)`). */
|
|
22
|
+
const MAX_CONTEXT_WORDS = 5;
|
|
23
|
+
/**
|
|
24
|
+
* Common short English function words that would otherwise pass a
|
|
25
|
+
* length-based filter and pollute the recommendation stack as noise. Kept
|
|
26
|
+
* intentionally short and grammar-only — no 2-3 letter technical acronym
|
|
27
|
+
* ("ci", "ai", "ml", "db", "os", "ui", "js", "go", "git", "aws", "sql",
|
|
28
|
+
* "cli", "api", "sdk", "css") appears here, since those are exactly the real
|
|
29
|
+
* terms this fix exists to stop dropping.
|
|
30
|
+
*/
|
|
31
|
+
const CONTEXT_STOPWORDS = new Set([
|
|
32
|
+
// 2-letter
|
|
33
|
+
'an',
|
|
34
|
+
'is',
|
|
35
|
+
'it',
|
|
36
|
+
'be',
|
|
37
|
+
'to',
|
|
38
|
+
'of',
|
|
39
|
+
'in',
|
|
40
|
+
'on',
|
|
41
|
+
'at',
|
|
42
|
+
'as',
|
|
43
|
+
'or',
|
|
44
|
+
'if',
|
|
45
|
+
'so',
|
|
46
|
+
'no',
|
|
47
|
+
'do',
|
|
48
|
+
'am',
|
|
49
|
+
'we',
|
|
50
|
+
'us',
|
|
51
|
+
'he',
|
|
52
|
+
'my',
|
|
53
|
+
'up',
|
|
54
|
+
// 3-letter
|
|
55
|
+
'the',
|
|
56
|
+
'and',
|
|
57
|
+
'but',
|
|
58
|
+
'not',
|
|
59
|
+
'did',
|
|
60
|
+
'has',
|
|
61
|
+
'had',
|
|
62
|
+
'she',
|
|
63
|
+
'him',
|
|
64
|
+
'her',
|
|
65
|
+
'its',
|
|
66
|
+
'our',
|
|
67
|
+
'all',
|
|
68
|
+
'few',
|
|
69
|
+
'out',
|
|
70
|
+
'off',
|
|
71
|
+
'via',
|
|
72
|
+
'per',
|
|
73
|
+
'own',
|
|
74
|
+
'too',
|
|
75
|
+
'yet',
|
|
76
|
+
'nor',
|
|
77
|
+
'you',
|
|
78
|
+
'are',
|
|
79
|
+
'was',
|
|
80
|
+
'for',
|
|
81
|
+
'may',
|
|
82
|
+
'who',
|
|
83
|
+
'why',
|
|
84
|
+
'how',
|
|
85
|
+
// Code-review correction (SMI-5986): "any" (TS/SQL keyword), "let"
|
|
86
|
+
// (JS/Rust keyword), and "can" (CAN-bus acronym) were removed from this
|
|
87
|
+
// set — each is a real technical term this fix exists to stop dropping,
|
|
88
|
+
// same class as "git"/"ci"/"aws"/"sql". Leaving them in would have
|
|
89
|
+
// contradicted this list's own documented promise above.
|
|
90
|
+
]);
|
|
91
|
+
/**
|
|
92
|
+
* Whether a single character counts as "keep" (not strippable edge
|
|
93
|
+
* punctuation) — see `stripEdgePunctuation`. Plain character-code range
|
|
94
|
+
* checks, not a regex — this is the actual "no regex" implementation (PR
|
|
95
|
+
* review round 3, SMI-5986): round 2's fix still called `.test()` against a
|
|
96
|
+
* single-character-class regex, which isn't the polynomial-backtracking
|
|
97
|
+
* shape CodeQL flags, but didn't match its own doc comment's "manual
|
|
98
|
+
* index-scan rather than a regex" claim either.
|
|
99
|
+
*/
|
|
100
|
+
function isKeepChar(ch) {
|
|
101
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch === '+' || ch === '#';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a word contains at least one alphanumeric character — used to
|
|
105
|
+
* reject punctuation-only tokens ("++", "##", "+#") that `isKeepChar` alone
|
|
106
|
+
* would let through, since `+`/`#` count as "keep" but aren't themselves
|
|
107
|
+
* usable content (PR review round 3, SMI-5986: `extractContextWords('++ ##
|
|
108
|
+
* +#')` was returning `['++', '##', '+#']` — all three passed the length
|
|
109
|
+
* filter with no alphanumeric substance at all).
|
|
110
|
+
*/
|
|
111
|
+
function hasAlphaNumeric(word) {
|
|
112
|
+
for (let i = 0; i < word.length; i++) {
|
|
113
|
+
const ch = word[i];
|
|
114
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Strip leading/trailing punctuation from an already-lowercased token
|
|
121
|
+
* without touching interior characters — "k8s" is untouched, but "git," ->
|
|
122
|
+
* "git" and "(sql)" -> "sql". Real-world context strings aren't always
|
|
123
|
+
* clean whitespace-delimited tokens (trailing commas, wrapping parens),
|
|
124
|
+
* and a stray comma would otherwise make "git," fail to match the real
|
|
125
|
+
* "git" term downstream.
|
|
126
|
+
*
|
|
127
|
+
* `+` and `#` count as "keep" characters (code-review correction, SMI-5986)
|
|
128
|
+
* — they're meaningful trailing characters in real technical terms ("c++",
|
|
129
|
+
* "c#"), not punctuation noise; stripping them turned both into the single
|
|
130
|
+
* character "c", which the length filter then discarded entirely. A
|
|
131
|
+
* punctuation-only token surviving this step (e.g. "++") is rejected
|
|
132
|
+
* downstream by `hasAlphaNumeric`, not here — this function only trims
|
|
133
|
+
* edges, it doesn't judge the result's usability.
|
|
134
|
+
*
|
|
135
|
+
* O(n), two linear index scans, no regex anywhere in the call chain
|
|
136
|
+
* (CodeQL js/polynomial-redos, SMI-5986 PR review round 2 — verified
|
|
137
|
+
* empirically, a 200k-character adversarial input runs in under 1ms).
|
|
138
|
+
*/
|
|
139
|
+
function stripEdgePunctuation(word) {
|
|
140
|
+
let start = 0;
|
|
141
|
+
while (start < word.length && !isKeepChar(word[start]))
|
|
142
|
+
start++;
|
|
143
|
+
let end = word.length;
|
|
144
|
+
while (end > start && !isKeepChar(word[end - 1]))
|
|
145
|
+
end--;
|
|
146
|
+
return word.slice(start, end);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract up to `maxWords` usable technical terms from free-text project
|
|
150
|
+
* context for the recommendation stack.
|
|
151
|
+
*
|
|
152
|
+
* Replaces the old `.filter((w) => w.length > 3)` threshold: real technical
|
|
153
|
+
* terms of 2-3 characters ("git", "ci", "aws", "sql") are now kept, while
|
|
154
|
+
* single-character tokens, punctuation-only tokens (including one that
|
|
155
|
+
* survives edge-stripping because it's made entirely of `+`/`#`, e.g. "++"
|
|
156
|
+
* or "+#" — PR review round 3, SMI-5986), and short English function words
|
|
157
|
+
* ("a", "the", "is", ...) are still dropped as noise. Words of 4+
|
|
158
|
+
* characters are unaffected — this only changes the outcome for the short
|
|
159
|
+
* end of the spectrum the length threshold got wrong.
|
|
160
|
+
*
|
|
161
|
+
* @param projectContext - Free-text project/context description. `null`/
|
|
162
|
+
* `undefined`/empty returns `[]`, mirroring both callers' pre-existing
|
|
163
|
+
* `if (project_context)` guard.
|
|
164
|
+
* @param maxWords - Maximum number of words to return (default 5).
|
|
165
|
+
*/
|
|
166
|
+
export function extractContextWords(projectContext, maxWords = MAX_CONTEXT_WORDS) {
|
|
167
|
+
if (!projectContext)
|
|
168
|
+
return [];
|
|
169
|
+
return projectContext
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.split(/\s+/)
|
|
172
|
+
.map(stripEdgePunctuation)
|
|
173
|
+
.filter((word) => word.length >= 2 &&
|
|
174
|
+
hasAlphaNumeric(word) &&
|
|
175
|
+
!(word.length <= 3 && CONTEXT_STOPWORDS.has(word)))
|
|
176
|
+
.slice(0, maxWords);
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=context-words.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-words.js","sourceRoot":"","sources":["../../../src/services/context-words.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-words.test.d.ts","sourceRoot":"","sources":["../../../src/services/context-words.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unit tests for the shared context-word extraction helper
|
|
3
|
+
* (SMI-5986). Both CLI `recommend --context` and MCP `skill_recommend`'s
|
|
4
|
+
* `project_context` now call this instead of duplicating
|
|
5
|
+
* `.filter((w) => w.length > 3)`, so their behavior can't drift apart.
|
|
6
|
+
* @module @skillsmith/core/services/context-words.test
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { extractContextWords } from './context-words.js';
|
|
10
|
+
describe('extractContextWords (SMI-5986)', () => {
|
|
11
|
+
it('keeps real short technical terms that the old length>3 threshold dropped', () => {
|
|
12
|
+
const words = extractContextWords('git ci aws sql k8s');
|
|
13
|
+
expect(words).toEqual(['git', 'ci', 'aws', 'sql', 'k8s']);
|
|
14
|
+
});
|
|
15
|
+
it('does not derive an empty stack from a context of only short technical terms', () => {
|
|
16
|
+
// SMI-5986 regression: this exact shape used to filter to [], which then
|
|
17
|
+
// tripped the SMI-5896 empty-stack guard even though the caller supplied
|
|
18
|
+
// usable context.
|
|
19
|
+
expect(extractContextWords('git').length).toBeGreaterThan(0);
|
|
20
|
+
expect(extractContextWords('ci').length).toBeGreaterThan(0);
|
|
21
|
+
});
|
|
22
|
+
it('drops single-character tokens as noise', () => {
|
|
23
|
+
expect(extractContextWords('a i x')).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
it('drops short English stopwords but keeps a real term in the same input', () => {
|
|
26
|
+
const words = extractContextWords('a be api testing');
|
|
27
|
+
expect(words).not.toContain('a');
|
|
28
|
+
expect(words).not.toContain('be');
|
|
29
|
+
expect(words).toContain('api');
|
|
30
|
+
expect(words).toContain('testing');
|
|
31
|
+
});
|
|
32
|
+
it('drops punctuation-only tokens', () => {
|
|
33
|
+
expect(extractContextWords('... -- !!')).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it('drops tokens made entirely of + and #, not just other punctuation', () => {
|
|
36
|
+
// PR review round 3 (SMI-5986): + and # count as "keep" characters for
|
|
37
|
+
// stripEdgePunctuation (so "c++"/"c#" survive), but a token with no
|
|
38
|
+
// alphanumeric content at all ("++", "##", "+#") must still be rejected
|
|
39
|
+
// as noise — it isn't a real technical term just because it contains a
|
|
40
|
+
// "keep" character.
|
|
41
|
+
expect(extractContextWords('++ ## +#')).toEqual([]);
|
|
42
|
+
expect(extractContextWords('++')).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
it('drops a token that is leading-punctuation-only after stripping ("+++x" keeps content, "+++" alone does not)', () => {
|
|
45
|
+
expect(extractContextWords('+++')).toEqual([]);
|
|
46
|
+
expect(extractContextWords('+++git')).toEqual(['+++git']);
|
|
47
|
+
});
|
|
48
|
+
it('strips edge punctuation without mangling the term ("git," -> "git")', () => {
|
|
49
|
+
expect(extractContextWords('git,')).toEqual(['git']);
|
|
50
|
+
expect(extractContextWords('(sql)')).toEqual(['sql']);
|
|
51
|
+
});
|
|
52
|
+
it('is case-insensitive ("Git" -> "git")', () => {
|
|
53
|
+
expect(extractContextWords('Git')).toEqual(['git']);
|
|
54
|
+
expect(extractContextWords('AWS SQL')).toEqual(['aws', 'sql']);
|
|
55
|
+
});
|
|
56
|
+
it('preserves + and # as meaningful trailing characters, not strippable punctuation', () => {
|
|
57
|
+
// Code-review regression (SMI-5986): edge-punctuation stripping used to
|
|
58
|
+
// treat "+" and "#" as noise, turning "c++"/"c#" into the single
|
|
59
|
+
// character "c", which the length filter then discarded entirely.
|
|
60
|
+
expect(extractContextWords('c++')).toEqual(['c++']);
|
|
61
|
+
expect(extractContextWords('c#')).toEqual(['c#']);
|
|
62
|
+
expect(extractContextWords('c++,')).toEqual(['c++']);
|
|
63
|
+
expect(extractContextWords('(c#)')).toEqual(['c#']);
|
|
64
|
+
});
|
|
65
|
+
it('does not filter real technical terms that are also common English words', () => {
|
|
66
|
+
// Code-review regression (SMI-5986): "any" (TS/SQL keyword), "let"
|
|
67
|
+
// (JS/Rust keyword), and "can" (CAN-bus acronym) were previously in the
|
|
68
|
+
// stopword set, contradicting its own documented promise.
|
|
69
|
+
expect(extractContextWords('any')).toEqual(['any']);
|
|
70
|
+
expect(extractContextWords('let')).toEqual(['let']);
|
|
71
|
+
expect(extractContextWords('can')).toEqual(['can']);
|
|
72
|
+
});
|
|
73
|
+
it('leaves 4+ character words unaffected (pre-existing behavior preserved)', () => {
|
|
74
|
+
const words = extractContextWords('testing utilities framework');
|
|
75
|
+
expect(words).toEqual(['testing', 'utilities', 'framework']);
|
|
76
|
+
});
|
|
77
|
+
it('slices to the max word count (default 5)', () => {
|
|
78
|
+
const words = extractContextWords('one two three four five six seven');
|
|
79
|
+
expect(words).toHaveLength(5);
|
|
80
|
+
});
|
|
81
|
+
it('respects a custom maxWords argument', () => {
|
|
82
|
+
const words = extractContextWords('git ci aws sql k8s docker', 3);
|
|
83
|
+
expect(words).toEqual(['git', 'ci', 'aws']);
|
|
84
|
+
});
|
|
85
|
+
it('returns [] for undefined, null, or empty input', () => {
|
|
86
|
+
expect(extractContextWords(undefined)).toEqual([]);
|
|
87
|
+
expect(extractContextWords(null)).toEqual([]);
|
|
88
|
+
expect(extractContextWords('')).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
it('returns [] for whitespace-only input', () => {
|
|
91
|
+
expect(extractContextWords(' ')).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=context-words.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-words.test.js","sourceRoot":"","sources":["../../../src/services/context-words.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared empty-derived-stack guard for skill recommendations.
|
|
3
|
+
* @module @skillsmith/core/services/recommend-guard
|
|
4
|
+
* @see SMI-5896 (Wave 3, discovery-tool consistency): CLI `recommend` and MCP
|
|
5
|
+
* `skill_recommend` each derive a technology "stack" client-side — CLI from
|
|
6
|
+
* codebase analysis (`buildStackFromAnalysis`), MCP from installed skills +
|
|
7
|
+
* project context keywords — and either can legitimately derive `[]` (a
|
|
8
|
+
* non-Node stack, an all-devDeps project, or an unsupported language). The
|
|
9
|
+
* `skills-recommend` edge function hard-rejects an empty `stack` with a 400
|
|
10
|
+
* (defense in depth, intentionally unchanged by this fix — see the plan's
|
|
11
|
+
* Wave 3 Step 2 "decided contract").
|
|
12
|
+
*
|
|
13
|
+
* Both callers now detect the empty-stack case BEFORE calling the API
|
|
14
|
+
* instead of letting the guaranteed-400 round trip happen and reacting to
|
|
15
|
+
* the failure after the fact — this is the one guidance string they both
|
|
16
|
+
* surface, so it can't drift between CLI and MCP wording the way the two
|
|
17
|
+
* tools' pre-fix behavior already had (a hard `process.exit(1)` crash on
|
|
18
|
+
* CLI vs a silently-swallowed warning on MCP for the exact same input
|
|
19
|
+
* shape).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Guidance shown when no technology stack could be derived for
|
|
23
|
+
* recommendations. Written to read naturally from both a terminal (CLI) and
|
|
24
|
+
* an MCP tool response consumed by a calling agent — neither surface names
|
|
25
|
+
* its own flag here, since the same string serves both.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildEmptyStackGuidance(): string;
|
|
28
|
+
//# sourceMappingURL=recommend-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommend-guard.d.ts","sourceRoot":"","sources":["../../../src/services/recommend-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAQhD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared empty-derived-stack guard for skill recommendations.
|
|
3
|
+
* @module @skillsmith/core/services/recommend-guard
|
|
4
|
+
* @see SMI-5896 (Wave 3, discovery-tool consistency): CLI `recommend` and MCP
|
|
5
|
+
* `skill_recommend` each derive a technology "stack" client-side — CLI from
|
|
6
|
+
* codebase analysis (`buildStackFromAnalysis`), MCP from installed skills +
|
|
7
|
+
* project context keywords — and either can legitimately derive `[]` (a
|
|
8
|
+
* non-Node stack, an all-devDeps project, or an unsupported language). The
|
|
9
|
+
* `skills-recommend` edge function hard-rejects an empty `stack` with a 400
|
|
10
|
+
* (defense in depth, intentionally unchanged by this fix — see the plan's
|
|
11
|
+
* Wave 3 Step 2 "decided contract").
|
|
12
|
+
*
|
|
13
|
+
* Both callers now detect the empty-stack case BEFORE calling the API
|
|
14
|
+
* instead of letting the guaranteed-400 round trip happen and reacting to
|
|
15
|
+
* the failure after the fact — this is the one guidance string they both
|
|
16
|
+
* surface, so it can't drift between CLI and MCP wording the way the two
|
|
17
|
+
* tools' pre-fix behavior already had (a hard `process.exit(1)` crash on
|
|
18
|
+
* CLI vs a silently-swallowed warning on MCP for the exact same input
|
|
19
|
+
* shape).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Guidance shown when no technology stack could be derived for
|
|
23
|
+
* recommendations. Written to read naturally from both a terminal (CLI) and
|
|
24
|
+
* an MCP tool response consumed by a calling agent — neither surface names
|
|
25
|
+
* its own flag here, since the same string serves both.
|
|
26
|
+
*/
|
|
27
|
+
export function buildEmptyStackGuidance() {
|
|
28
|
+
return ('No technology stack could be derived for recommendations — this usually ' +
|
|
29
|
+
'means a non-Node project, a stack with no production dependencies, or an ' +
|
|
30
|
+
'unsupported language, not a backend or registry problem. Provide project ' +
|
|
31
|
+
'context (a short description of the project or its tooling) or an ' +
|
|
32
|
+
'explicit list of installed/currently-used skills, then try again.');
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=recommend-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommend-guard.js","sourceRoot":"","sources":["../../../src/services/recommend-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,CACL,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,oEAAoE;QACpE,mEAAmE,CACpE,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommend-guard.test.d.ts","sourceRoot":"","sources":["../../../src/services/recommend-guard.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unit tests for the shared empty-derived-stack guidance string
|
|
3
|
+
* (SMI-5896 Wave 3 Step 2). Both CLI `recommend` and MCP `skill_recommend`
|
|
4
|
+
* surface this exact string so their empty-stack messaging can't drift.
|
|
5
|
+
* @module @skillsmith/core/services/recommend-guard.test
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { buildEmptyStackGuidance } from './recommend-guard.js';
|
|
9
|
+
describe('buildEmptyStackGuidance (SMI-5896 Wave 3 Step 2)', () => {
|
|
10
|
+
it('returns a non-empty string', () => {
|
|
11
|
+
const guidance = buildEmptyStackGuidance();
|
|
12
|
+
expect(typeof guidance).toBe('string');
|
|
13
|
+
expect(guidance.length).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
it('explains the empty stack is a legitimate under-detection, not a backend fault', () => {
|
|
16
|
+
const guidance = buildEmptyStackGuidance();
|
|
17
|
+
expect(guidance).toContain('No technology stack could be derived');
|
|
18
|
+
});
|
|
19
|
+
it('guides the caller toward providing context or an installed-skills list', () => {
|
|
20
|
+
const guidance = buildEmptyStackGuidance();
|
|
21
|
+
expect(guidance.toLowerCase()).toContain('project context');
|
|
22
|
+
expect(guidance.toLowerCase()).toContain('installed');
|
|
23
|
+
});
|
|
24
|
+
it('is deterministic (identical wording every call) so CLI and MCP never drift apart', () => {
|
|
25
|
+
expect(buildEmptyStackGuidance()).toBe(buildEmptyStackGuidance());
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=recommend-guard.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommend-guard.test.js","sourceRoot":"","sources":["../../../src/services/recommend-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAE9D,QAAQ,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACtC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wEAAwE,EAAE,GAAG,EAAE;QAChF,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;QAC3D,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,uBAAuB,EAAE,CAAC,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Content-based skill install path (private registry).
|
|
3
|
+
* @module @skillsmith/core/services/skill-installation.content
|
|
4
|
+
* @see SMI-5905 Wave 1: installFromContent() + resolveFreshAccessToken() extraction
|
|
5
|
+
* @see docs/internal/implementation/private-registry-skill-install.md
|
|
6
|
+
*
|
|
7
|
+
* `install()` (skill-installation.service.ts) only knows how to fetch a
|
|
8
|
+
* skill from GitHub. This module adds a second entry point,
|
|
9
|
+
* `installFromContent()`, for skills whose content has already been resolved
|
|
10
|
+
* elsewhere (a private-registry `content` JSONB column, read via a
|
|
11
|
+
* per-team-member Supabase session or the `private-registry-get` Edge
|
|
12
|
+
* Function — Waves 2/3 wire those transports; this module only needs the
|
|
13
|
+
* already-fetched `{skillId, version, content}` triple).
|
|
14
|
+
*
|
|
15
|
+
* SCOPE TRIM (Sol review #5 — read before extending this function):
|
|
16
|
+
* `installFromContent()` deliberately reuses only two things from the
|
|
17
|
+
* `install()` policy chain:
|
|
18
|
+
* 1. `writeInstallFiles()`'s disk-write + rollback-on-partial-write handling
|
|
19
|
+
* (skill-installation.io.ts), plus this module's own manifest-driven
|
|
20
|
+
* already-installed/force gate (mirrors `install()`'s ALREADY_INSTALLED
|
|
21
|
+
* check).
|
|
22
|
+
* 2. A security scan at the `community` trust tier, via the same
|
|
23
|
+
* `classifyBundledFile()`/`isRejectableScan()` policy `install()` uses
|
|
24
|
+
* for optional bundled files (skill-installation.policy.ts) — applied
|
|
25
|
+
* here to every content-map entry (including "SKILL.md" itself, which
|
|
26
|
+
* falls through to the conservative 'structured' default class since it
|
|
27
|
+
* has no BUNDLED_SCAN_FILES entry).
|
|
28
|
+
*
|
|
29
|
+
* It deliberately does NOT route through the rest of `install()`'s chain:
|
|
30
|
+
* - `skipScan` is NOT an accepted option here. Registry-content installs
|
|
31
|
+
* always scan — there is no opt-out, unlike `install()`'s tier-gated
|
|
32
|
+
* skipScan.
|
|
33
|
+
* - Dependency-intelligence extraction/persistence (extractDepIntel,
|
|
34
|
+
* persistDependencies, checkDepsAgainstQuarantine), risk-history
|
|
35
|
+
* recording, AI-defence feedback, and co-install-session recording are
|
|
36
|
+
* all OUT of v1 scope — a known, intentional gap, not a silent drop.
|
|
37
|
+
* Wave 3's MCP `install` action and Wave 4's CLI command do not get
|
|
38
|
+
* these signals for private-registry installs yet.
|
|
39
|
+
* - Frontmatter/manifest validation on `SKILL.md` itself DOES still run
|
|
40
|
+
* (`validateSkillMd()`), same as any other install.
|
|
41
|
+
*
|
|
42
|
+
* PATH VALIDATION (Sol review #2, critical, confirmed exploitable): every key
|
|
43
|
+
* in `content` is attacker-controlled — any team member with publish access
|
|
44
|
+
* chooses these filenames — and flows into `writeInstallFiles()`'s
|
|
45
|
+
* `path.join(installPath, subSkill.filename)` (skill-installation.io.ts:167)
|
|
46
|
+
* with NO containment check of its own. `path.join('/a/b', '../../etc/passwd')`
|
|
47
|
+
* escapes `installPath` after normalization, so every key is validated here,
|
|
48
|
+
* BEFORE any disk write — see `validateContentKeys()`.
|
|
49
|
+
*/
|
|
50
|
+
import type { Database } from '../db/database-interface.js';
|
|
51
|
+
import type { ClientId } from '../install/paths.js';
|
|
52
|
+
import type { ProgressCallback, InstallFromContentOptions, InstallResult } from './skill-installation.types.js';
|
|
53
|
+
import type { ManifestManager } from './skill-manifest.js';
|
|
54
|
+
/**
|
|
55
|
+
* Validate every key in a content map before any disk write.
|
|
56
|
+
*
|
|
57
|
+
* Rejects (as a single combined error, first offense wins):
|
|
58
|
+
* - path-traversal (`..` segment), absolute paths (posix or win32/UNC),
|
|
59
|
+
* NUL bytes, backslashes, empty/bare-"."/".git"-adjacent keys;
|
|
60
|
+
* - any key that, after `path.resolve()` against `installPath`, resolves
|
|
61
|
+
* outside `installPath` (a lexical backstop behind the checks above,
|
|
62
|
+
* matching `writeInstallFiles()`'s own lexical escape check);
|
|
63
|
+
* - two keys that `path.normalize()` to the same on-disk path (a
|
|
64
|
+
* collision — e.g. "SKILL.md" and "./SKILL.md").
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateContentKeys(content: Record<string, string>, installPath: string): {
|
|
67
|
+
valid: true;
|
|
68
|
+
} | {
|
|
69
|
+
valid: false;
|
|
70
|
+
error: string;
|
|
71
|
+
};
|
|
72
|
+
/** Internal params bag: the caller's InstallFromContentOptions plus the service state
|
|
73
|
+
* installFromContent() needs (mirrors performUninstall()'s flattened-params convention
|
|
74
|
+
* in skill-installation.helpers.ts). */
|
|
75
|
+
export interface InstallFromContentParams extends InstallFromContentOptions {
|
|
76
|
+
db: Database;
|
|
77
|
+
skillsDir: string;
|
|
78
|
+
manifest: ManifestManager;
|
|
79
|
+
client: ClientId;
|
|
80
|
+
onProgress: ProgressCallback;
|
|
81
|
+
/**
|
|
82
|
+
* SMI-5982 code-review fix #1: base dir for resolving a relative companion-agent
|
|
83
|
+
* target (Antigravity only) — see writeInstallFiles()'s own doc comment.
|
|
84
|
+
* PR-review follow-up: optional (not required) — an omitted value must flow
|
|
85
|
+
* through to `resolveCompanionAgentPath()`'s own required-`baseDir` guard as
|
|
86
|
+
* `undefined`, not be silently defaulted anywhere in this chain.
|
|
87
|
+
*/
|
|
88
|
+
companionBaseDir?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Install an already-resolved private-registry skill's content to disk.
|
|
92
|
+
*
|
|
93
|
+
* See this file's header doc comment for the full scope trim vs. `install()`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function installFromContent(params: InstallFromContentParams): Promise<InstallResult>;
|
|
96
|
+
//# sourceMappingURL=skill-installation.content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installation.content.d.ts","sourceRoot":"","sources":["../../../src/services/skill-installation.content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,KAAK,EACV,gBAAgB,EAChB,yBAAyB,EACzB,aAAa,EACd,MAAM,+BAA+B,CAAA;AAEtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AA8C1D;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,EAAE,MAAM,GAClB;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAgCnD;AA8DD;;yCAEyC;AACzC,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB;IACzE,EAAE,EAAE,QAAQ,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,eAAe,CAAA;IACzB,MAAM,EAAE,QAAQ,CAAA;IAChB,UAAU,EAAE,gBAAgB,CAAA;IAC5B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqKjG"}
|