llm-cli-gateway 2.14.1 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +35 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +32 -0
- package/dist/index.js +10 -23
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +201 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to the llm-cli-gateway project.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.15.0] - 2026-07-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Updateable local skill packs.** Gateway startup now loads bundled skills,
|
|
12
|
+
optional `[skills].paths`, `LLM_GATEWAY_SKILLS_PATH`, and
|
|
13
|
+
`~/.llm-cli-gateway/skills` with deterministic override precedence. Skill
|
|
14
|
+
pack roots can include a `skill-pack.json` integrity manifest that pins each
|
|
15
|
+
loaded `SKILL.md` by SHA-256, allowing operators to update workflow skills
|
|
16
|
+
outside core gateway npm releases without silent network fetches.
|
|
17
|
+
|
|
7
18
|
## [2.14.1] - 2026-07-03
|
|
8
19
|
|
|
9
20
|
### Added
|
package/README.md
CHANGED
|
@@ -53,6 +53,40 @@ Or use directly with `npx` from an MCP client:
|
|
|
53
53
|
|
|
54
54
|
The repo ships agent-ready workflow skills under [`.agents/skills`](.agents/skills) for async orchestration, session continuity, multi-LLM review, implement-review-fix loops, retrospective evidence walks, and secure approval-gated dispatch. Seven caller-facing skills are bundled in the published npm package: `async-job-orchestration`, `multi-llm-review`, `session-workflow`, `secure-orchestration`, `implement-review-fix`, `retrospective-walk`, and `public-demo-session`. Machine-readable DAG-TOML plans live under [`docs/plans`](docs/plans) and [`setup/install-plan.dag.toml`](setup/install-plan.dag.toml) for workflows that need deterministic sequencing and verification gates.
|
|
55
55
|
|
|
56
|
+
Skill packs can be updated outside the core npm release by placing skill
|
|
57
|
+
directories in local, operator-controlled paths. The gateway loads bundled
|
|
58
|
+
skills first, then `[skills].paths`, then `LLM_GATEWAY_SKILLS_PATH`, then
|
|
59
|
+
`~/.llm-cli-gateway/skills` when it exists; later roots override earlier skills
|
|
60
|
+
by name. Each skill is a directory containing `SKILL.md`. A root may also carry
|
|
61
|
+
`skill-pack.json` to pin expected `SKILL.md` hashes:
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[skills]
|
|
65
|
+
paths = ["/opt/llm-cli-gateway/skills"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export LLM_GATEWAY_SKILLS_PATH="/opt/team-skill-pack:/opt/incident-skill-pack"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"name": "team-pack",
|
|
75
|
+
"version": "1.0.0",
|
|
76
|
+
"skills": [
|
|
77
|
+
{
|
|
78
|
+
"name": "incident-retrospective",
|
|
79
|
+
"sha256": "<sha256 of incident-retrospective/SKILL.md>"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The loader is intentionally local-only: it never fetches remote Markdown at
|
|
86
|
+
startup. To update a pack, install or replace files through your package manager
|
|
87
|
+
or deployment system, then restart the gateway so the advertised `skills://...`
|
|
88
|
+
resources refresh.
|
|
89
|
+
|
|
56
90
|
The next documentation focus is provider-specific skill and DAG-TOML pairs for each outbound CLI and API-provider family: Claude, Codex, Gemini/Antigravity, Grok, Mistral Vibe, Devin, Cursor Agent, OpenAI-compatible endpoints, Anthropic Messages, and xAI Responses. The implementation plan is tracked in [`docs/plans/provider-workflow-assets.dag.toml`](docs/plans/provider-workflow-assets.dag.toml), with each provider asset expected to cover install/login checks or token-env checks, session behavior, approval modes, cache/telemetry surfaces, failure modes, and a smoke-test gate.
|
|
57
91
|
|
|
58
92
|
## Trust & Supply Chain
|
|
@@ -1435,6 +1469,7 @@ await callTool("session_delete", {
|
|
|
1435
1469
|
LLM_GATEWAY_OAUTH_REQUIRE_CONSENT=1 LLM_GATEWAY_OAUTH_CONSENT_SECRET='choose-a-strong-code' node dist/index.js
|
|
1436
1470
|
```
|
|
1437
1471
|
- `LLM_GATEWAY_CONFIG`: Path to the gateway TOML config (default: `~/.llm-cli-gateway/config.toml`). See **Persistence configuration** above for the `[persistence]` schema.
|
|
1472
|
+
- `LLM_GATEWAY_SKILLS_PATH`: Extra local skill-pack roots to load at startup, separated by the host path delimiter (`:` on Linux/macOS, `;` on Windows). These paths are appended after `[skills].paths`; `~/.llm-cli-gateway/skills` still loads last when present.
|
|
1438
1473
|
- `LLM_GATEWAY_LOGS_DB`: **Deprecated** — overrides `[persistence].path` and selects `backend = "sqlite"` (or `backend = "none"` when set to `none`). Emits a deprecation warning at startup; migrate to `config.toml`.
|
|
1439
1474
|
```bash
|
|
1440
1475
|
# Custom path
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface Config {
|
|
|
15
15
|
database?: DatabaseConfig;
|
|
16
16
|
sessionTtl: number;
|
|
17
17
|
}
|
|
18
|
+
export interface SkillsConfig {
|
|
19
|
+
paths: string[];
|
|
20
|
+
sources: {
|
|
21
|
+
configFile: string | null;
|
|
22
|
+
envOverrides: string[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
18
25
|
export declare function loadConfig(): Config;
|
|
19
26
|
export declare const PERSISTENCE_BACKENDS: readonly ["sqlite", "postgres", "memory", "none"];
|
|
20
27
|
export type PersistenceBackend = (typeof PERSISTENCE_BACKENDS)[number];
|
|
@@ -46,6 +53,7 @@ export interface PersistenceConfigSources {
|
|
|
46
53
|
envOverrides: string[];
|
|
47
54
|
}
|
|
48
55
|
export declare function defaultGatewayConfigPath(): string;
|
|
56
|
+
export declare function loadSkillsConfig(logger?: Logger): SkillsConfig;
|
|
49
57
|
export declare function loadPersistenceConfig(logger?: Logger): PersistenceConfig;
|
|
50
58
|
export declare const DEFAULT_HTTP_MAX_SESSIONS = 100;
|
|
51
59
|
export declare const DEFAULT_HTTP_SESSION_IDLE_TTL_MS: number;
|
package/dist/config.js
CHANGED
|
@@ -14,6 +14,12 @@ const DatabaseUrlSchema = z
|
|
|
14
14
|
message: "Database URL must start with postgresql:// or postgres://",
|
|
15
15
|
});
|
|
16
16
|
export const DEFAULT_SESSION_TTL_SECONDS = 2592000;
|
|
17
|
+
const SkillsSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
paths: z.array(z.string().min(1)).default([]),
|
|
20
|
+
})
|
|
21
|
+
.strict()
|
|
22
|
+
.default({ paths: [] });
|
|
17
23
|
export function loadConfig() {
|
|
18
24
|
const databaseUrl = process.env.DATABASE_URL;
|
|
19
25
|
const rawSessionTtl = parseInt(process.env.SESSION_TTL || String(DEFAULT_SESSION_TTL_SECONDS), 10);
|
|
@@ -89,6 +95,32 @@ function defaultPersistenceConfigPath() {
|
|
|
89
95
|
export function defaultGatewayConfigPath() {
|
|
90
96
|
return defaultPersistenceConfigPath();
|
|
91
97
|
}
|
|
98
|
+
export function loadSkillsConfig(logger = noopLogger) {
|
|
99
|
+
const configPath = defaultGatewayConfigPath();
|
|
100
|
+
const { parsed, sourcePath } = readGatewayTomlFile(configPath, logger, "skills");
|
|
101
|
+
const rawSkills = parsed?.skills ?? {};
|
|
102
|
+
const sources = {
|
|
103
|
+
configFile: sourcePath,
|
|
104
|
+
envOverrides: [],
|
|
105
|
+
};
|
|
106
|
+
let skillsParsed;
|
|
107
|
+
try {
|
|
108
|
+
skillsParsed = SkillsSchema.parse(rawSkills);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`Invalid [skills] config: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
const paths = [...skillsParsed.paths];
|
|
114
|
+
const envPaths = process.env.LLM_GATEWAY_SKILLS_PATH;
|
|
115
|
+
if (envPaths !== undefined && envPaths.trim().length > 0) {
|
|
116
|
+
paths.push(...envPaths
|
|
117
|
+
.split(path.delimiter)
|
|
118
|
+
.map(part => part.trim())
|
|
119
|
+
.filter(Boolean));
|
|
120
|
+
sources.envOverrides.push("LLM_GATEWAY_SKILLS_PATH");
|
|
121
|
+
}
|
|
122
|
+
return { paths, sources };
|
|
123
|
+
}
|
|
92
124
|
function readPersistenceFile(configPath, logger) {
|
|
93
125
|
if (!existsSync(configPath)) {
|
|
94
126
|
return { raw: undefined, sourcePath: null };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mc
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, chmodSync, } from "fs";
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import { z } from "zod/v3";
|
|
@@ -21,7 +21,8 @@ import { ResourceProvider } from "./resources.js";
|
|
|
21
21
|
import { generateResourceDescriptors, generateProviderAcpDescriptors, } from "./provider-surface-generator.js";
|
|
22
22
|
import { PerformanceMetrics } from "./metrics.js";
|
|
23
23
|
import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
|
|
24
|
-
import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, loadAdminConfig, loadLimitsConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
|
|
24
|
+
import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, loadAdminConfig, loadLimitsConfig, loadSkillsConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
|
|
25
|
+
import { loadGatewaySkills } from "./skill-loader.js";
|
|
25
26
|
import { runAcpRequest } from "./acp/runtime.js";
|
|
26
27
|
import { isAcpError } from "./acp/errors.js";
|
|
27
28
|
import { redactSecrets } from "./secret-redaction.js";
|
|
@@ -184,27 +185,13 @@ function packageVersion() {
|
|
|
184
185
|
}
|
|
185
186
|
return "unknown";
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const content = readFileSync(skillPath, "utf-8");
|
|
195
|
-
const descMatch = content.match(/^---[\s\S]*?description:\s*(.+?)$/m);
|
|
196
|
-
const description = descMatch?.[1]?.trim() || dir.name;
|
|
197
|
-
skills.push({ name: dir.name, content, description });
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
}
|
|
205
|
-
return skills;
|
|
206
|
-
}
|
|
207
|
-
const loadedSkills = loadSkills();
|
|
188
|
+
const skillsConfig = loadSkillsConfig(logger);
|
|
189
|
+
const loadedSkills = loadGatewaySkills({
|
|
190
|
+
bundledSkillsDir: SKILLS_DIR,
|
|
191
|
+
configuredPaths: skillsConfig.paths,
|
|
192
|
+
envSkillsPath: undefined,
|
|
193
|
+
logger,
|
|
194
|
+
});
|
|
208
195
|
export function buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled = false) {
|
|
209
196
|
const asyncToolsNote = asyncJobsEnabled ? " | *_request_async (async)" : "";
|
|
210
197
|
const apiToolsNote = grokApiToolsEnabled ? ", grok_api_request" : "";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
export interface SkillEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
content: string;
|
|
5
|
+
description: string;
|
|
6
|
+
source: "bundled" | "external";
|
|
7
|
+
path: string;
|
|
8
|
+
pack: {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
manifestPath: string;
|
|
12
|
+
verified: boolean;
|
|
13
|
+
} | null;
|
|
14
|
+
}
|
|
15
|
+
export interface LoadGatewaySkillsOptions {
|
|
16
|
+
bundledSkillsDir: string;
|
|
17
|
+
configuredPaths?: string[];
|
|
18
|
+
envSkillsPath?: string | undefined;
|
|
19
|
+
userSkillsDir?: string;
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
}
|
|
22
|
+
export declare function defaultUserSkillsDir(): string;
|
|
23
|
+
export declare function parseSkillPathList(raw: string | undefined): string[];
|
|
24
|
+
export declare function loadGatewaySkills(options: LoadGatewaySkillsOptions): SkillEntry[];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { z } from "zod/v3";
|
|
6
|
+
import { logWarn, noopLogger } from "./logger.js";
|
|
7
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
8
|
+
const SkillPackManifestSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
name: z.string().regex(SKILL_NAME_PATTERN),
|
|
11
|
+
version: z.string().min(1),
|
|
12
|
+
skills: z
|
|
13
|
+
.array(z
|
|
14
|
+
.object({
|
|
15
|
+
name: z.string().regex(SKILL_NAME_PATTERN),
|
|
16
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/i),
|
|
17
|
+
})
|
|
18
|
+
.strict())
|
|
19
|
+
.default([]),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
export function defaultUserSkillsDir() {
|
|
23
|
+
return path.join(homedir(), ".llm-cli-gateway", "skills");
|
|
24
|
+
}
|
|
25
|
+
export function parseSkillPathList(raw) {
|
|
26
|
+
if (!raw)
|
|
27
|
+
return [];
|
|
28
|
+
return raw
|
|
29
|
+
.split(path.delimiter)
|
|
30
|
+
.map(part => part.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
export function loadGatewaySkills(options) {
|
|
34
|
+
const logger = options.logger ?? noopLogger;
|
|
35
|
+
const roots = [
|
|
36
|
+
{ path: options.bundledSkillsDir, source: "bundled", label: "bundled" },
|
|
37
|
+
...normalizeExternalRoots(options.configuredPaths ?? [], "configured"),
|
|
38
|
+
...normalizeExternalRoots(parseSkillPathList(options.envSkillsPath), "LLM_GATEWAY_SKILLS_PATH"),
|
|
39
|
+
];
|
|
40
|
+
const userSkillsDir = options.userSkillsDir ?? defaultUserSkillsDir();
|
|
41
|
+
if (existsSync(userSkillsDir)) {
|
|
42
|
+
roots.push({ path: userSkillsDir, source: "external", label: "user" });
|
|
43
|
+
}
|
|
44
|
+
const byName = new Map();
|
|
45
|
+
for (const root of roots) {
|
|
46
|
+
for (const skill of readSkillRoot(root, logger)) {
|
|
47
|
+
const prior = byName.get(skill.name);
|
|
48
|
+
if (prior) {
|
|
49
|
+
logWarn(logger, `Skill '${skill.name}' from ${skill.path} overrides ${prior.source} skill at ${prior.path}`);
|
|
50
|
+
}
|
|
51
|
+
byName.set(skill.name, skill);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
55
|
+
}
|
|
56
|
+
function normalizeExternalRoots(paths, label) {
|
|
57
|
+
return paths.map(p => ({
|
|
58
|
+
path: expandHome(p),
|
|
59
|
+
source: "external",
|
|
60
|
+
label,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function expandHome(p) {
|
|
64
|
+
return p === "~" ? homedir() : p.startsWith("~/") ? path.join(homedir(), p.slice(2)) : p;
|
|
65
|
+
}
|
|
66
|
+
function readSkillRoot(root, logger) {
|
|
67
|
+
if (!existsSync(root.path)) {
|
|
68
|
+
if (root.source === "external") {
|
|
69
|
+
logWarn(logger, `Configured skill path does not exist; skipping: ${root.path}`);
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
let stat;
|
|
74
|
+
try {
|
|
75
|
+
stat = statSync(root.path);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logWarn(logger, `Cannot stat skill path; skipping: ${root.path}`, err);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
if (!stat.isDirectory()) {
|
|
82
|
+
logWarn(logger, `Skill path is not a directory; skipping: ${root.path}`);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const manifest = readManifest(root, logger);
|
|
86
|
+
if (manifest === "invalid")
|
|
87
|
+
return [];
|
|
88
|
+
if (existsSync(path.join(root.path, "SKILL.md"))) {
|
|
89
|
+
const skill = readSkillDir(root.path, root, manifest, logger);
|
|
90
|
+
return skill ? [skill] : [];
|
|
91
|
+
}
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(root.path, { withFileTypes: true });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logWarn(logger, `Cannot read skill path; skipping: ${root.path}`, err);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const skills = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isDirectory())
|
|
103
|
+
continue;
|
|
104
|
+
const skill = readSkillDir(path.join(root.path, entry.name), root, manifest, logger);
|
|
105
|
+
if (skill)
|
|
106
|
+
skills.push(skill);
|
|
107
|
+
}
|
|
108
|
+
if (manifest && manifest.skills.length > 0) {
|
|
109
|
+
const loaded = new Set(skills.map(skill => skill.name));
|
|
110
|
+
for (const expected of manifest.skills) {
|
|
111
|
+
if (!loaded.has(expected.name)) {
|
|
112
|
+
logWarn(logger, `Skill pack '${manifest.name}' declares '${expected.name}', but no verified SKILL.md was loaded`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
117
|
+
}
|
|
118
|
+
function readManifest(root, logger) {
|
|
119
|
+
const manifestPath = path.join(root.path, "skill-pack.json");
|
|
120
|
+
if (!existsSync(manifestPath))
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
const raw = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
124
|
+
return SkillPackManifestSchema.parse(raw);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logWarn(logger, `Invalid skill-pack.json at ${manifestPath}; skipping skill pack`, err);
|
|
128
|
+
return "invalid";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function readSkillDir(skillDir, root, manifest, logger) {
|
|
132
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
133
|
+
if (!existsSync(skillPath))
|
|
134
|
+
return null;
|
|
135
|
+
let content;
|
|
136
|
+
try {
|
|
137
|
+
content = readFileSync(skillPath, "utf8");
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logWarn(logger, `Cannot read skill file; skipping: ${skillPath}`, err);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
144
|
+
const dirName = path.basename(skillDir);
|
|
145
|
+
const name = frontmatter.name || dirName;
|
|
146
|
+
if (!SKILL_NAME_PATTERN.test(name)) {
|
|
147
|
+
logWarn(logger, `Invalid skill name '${name}' in ${skillPath}; skipping`);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (frontmatter.name && frontmatter.name !== dirName) {
|
|
151
|
+
logWarn(logger, `Skill frontmatter name '${frontmatter.name}' does not match directory '${dirName}'; skipping ${skillPath}`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const packInfo = verifyManifestEntry(name, content, skillPath, root, manifest, logger);
|
|
155
|
+
if (packInfo === "skip")
|
|
156
|
+
return null;
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
content,
|
|
160
|
+
description: frontmatter.description || name,
|
|
161
|
+
source: root.source,
|
|
162
|
+
path: skillPath,
|
|
163
|
+
pack: packInfo,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function verifyManifestEntry(name, content, skillPath, root, manifest, logger) {
|
|
167
|
+
if (!manifest)
|
|
168
|
+
return null;
|
|
169
|
+
const expected = manifest.skills.find(skill => skill.name === name);
|
|
170
|
+
if (!expected) {
|
|
171
|
+
logWarn(logger, `Skill pack '${manifest.name}' does not list '${name}' in skill-pack.json; skipping ${skillPath}`);
|
|
172
|
+
return "skip";
|
|
173
|
+
}
|
|
174
|
+
const actual = createHash("sha256").update(content, "utf8").digest("hex");
|
|
175
|
+
if (actual.toLowerCase() !== expected.sha256.toLowerCase()) {
|
|
176
|
+
logWarn(logger, `Skill pack '${manifest.name}' hash mismatch for '${name}'; skipping ${skillPath}`);
|
|
177
|
+
return "skip";
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
name: manifest.name,
|
|
181
|
+
version: manifest.version,
|
|
182
|
+
manifestPath: path.join(root.path, "skill-pack.json"),
|
|
183
|
+
verified: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function parseSkillFrontmatter(content) {
|
|
187
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
188
|
+
if (!match)
|
|
189
|
+
return { name: null, description: null };
|
|
190
|
+
return {
|
|
191
|
+
name: scalarFrontmatterValue(match[1], "name"),
|
|
192
|
+
description: scalarFrontmatterValue(match[1], "description"),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function scalarFrontmatterValue(frontmatter, key) {
|
|
196
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
197
|
+
const match = frontmatter.match(new RegExp(`^${escapedKey}:\\s*(.+?)\\s*$`, "m"));
|
|
198
|
+
if (!match)
|
|
199
|
+
return null;
|
|
200
|
+
return match[1].replace(/^["']|["']$/g, "").trim();
|
|
201
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "llm-cli-gateway",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.15.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"mcpName": "io.github.verivus-oss/llm-cli-gateway",
|
|
5
5
|
"description": "Local-first MCP gateway for cross-model review across native coding-agent CLIs and API-token LLMs with sessions, durable jobs, and validation receipts.",
|
|
6
6
|
"license": "MIT",
|