oh-my-knowledge 0.34.0 → 0.36.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/README.md +3 -0
- package/README.zh.md +3 -0
- package/dist/assets/agent-skills/omk/SKILL.md +2 -2
- package/dist/assets/agent-skills/omk/references/commands.md +45 -0
- package/dist/authoring/evolver.js +13 -4
- package/dist/cli/commands/doctor.js +2 -1
- package/dist/cli/commands/eval/index.d.ts +1 -0
- package/dist/cli/commands/eval/index.js +40 -5
- package/dist/cli/commands/install.d.ts +2 -0
- package/dist/cli/commands/install.js +45 -20
- package/dist/cli/commands/list.d.ts +46 -0
- package/dist/cli/commands/list.js +243 -0
- package/dist/cli/commands/sample.d.ts +1 -1
- package/dist/cli/commands/sample.js +25 -12
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/i18n-dict/install.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/install.js +20 -0
- package/dist/cli/lib/i18n-dict/list.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/list.js +32 -0
- package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/run.js +8 -0
- package/dist/cli/lib/i18n-dict.d.ts +2 -1
- package/dist/cli/lib/i18n-dict.js +2 -0
- package/dist/cli/lib/parse-run-config/variant-resolution.js +7 -0
- package/dist/cli/lib/run-tally.js +1 -1
- package/dist/cli/lib/shared.js +1 -1
- package/dist/doctor/index.js +6 -6
- package/dist/eval-core/cache.d.ts +11 -3
- package/dist/eval-core/cache.js +13 -5
- package/dist/eval-core/dependency-checker.js +5 -3
- package/dist/eval-core/evaluation-execution.js +4 -1
- package/dist/eval-core/evaluation-reporting.d.ts +1 -0
- package/dist/eval-core/evaluation-reporting.js +20 -9
- package/dist/eval-core/execution-strategy.js +6 -4
- package/dist/eval-core/task-planner.js +2 -1
- package/dist/eval-workflows/batch-evaluation-workflow.js +3 -2
- package/dist/eval-workflows/evaluation-preparation.js +3 -1
- package/dist/eval-workflows/run-evaluation.js +2 -2
- package/dist/inputs/content-hash.d.ts +28 -0
- package/dist/inputs/content-hash.js +106 -0
- package/dist/inputs/eval-config.js +44 -11
- package/dist/inputs/materialize-copy.d.ts +37 -0
- package/dist/inputs/materialize-copy.js +193 -0
- package/dist/inputs/skill-loader.d.ts +65 -2
- package/dist/inputs/skill-loader.js +314 -16
- package/dist/inputs/source-resolver.d.ts +16 -9
- package/dist/inputs/source-resolver.js +57 -77
- package/dist/managed/evidence.d.ts +22 -0
- package/dist/managed/evidence.js +143 -0
- package/dist/managed/index.d.ts +2 -0
- package/dist/managed/index.js +2 -0
- package/dist/managed/list-view.d.ts +55 -0
- package/dist/managed/list-view.js +66 -0
- package/dist/managed/store.d.ts +16 -20
- package/dist/managed/store.js +85 -84
- package/dist/observability/experience.d.ts +2 -0
- package/dist/observability/experience.js +31 -2
- package/dist/observability/inbox.js +31 -5
- package/dist/observability/review-state.js +22 -11
- package/dist/observability/soft-standards/llm-extractor.js +3 -3
- package/dist/observability/soft-standards/skill-standards-store.d.ts +1 -0
- package/dist/observability/soft-standards/skill-standards-store.js +77 -14
- package/dist/observability/soft-standards/types.d.ts +2 -2
- package/dist/renderer/html-renderer.js +3 -3
- package/dist/renderer/layout.js +2 -2
- package/dist/server/report-server.js +1 -1
- package/dist/server/report-store.d.ts +1 -1
- package/dist/server/report-store.js +16 -15
- package/dist/server/skill-index.js +5 -4
- package/dist/types/doctor.d.ts +2 -2
- package/dist/types/doctor.js +1 -1
- package/dist/types/eval.d.ts +12 -1
- package/dist/types/managed.d.ts +29 -4
- package/dist/types/observability.d.ts +6 -6
- package/dist/types/report.d.ts +24 -5
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Artifact } from '../types/index.js';
|
|
1
|
+
import type { Artifact, RemoteGitRef } from '../types/index.js';
|
|
2
2
|
export declare function gitShowFile(ref: string, filePath: string, cwd?: string): string | null;
|
|
3
3
|
/**
|
|
4
4
|
* 取 `<ref>:<filePath>` 的原始字节(二进制资产用,不做 utf-8 解码);不存在/非 blob/出错返回 null。
|
|
@@ -66,6 +66,61 @@ export interface GitSkillRef {
|
|
|
66
66
|
* 参数传入,基准差异留给调用方、归类逻辑单一来源。任一探测命中即返回;都不中返回 null。
|
|
67
67
|
*/
|
|
68
68
|
export declare function classifyGitSkillRef(ref: string, gitRelDir: string, spec: string, cwd?: string): GitSkillRef | null;
|
|
69
|
+
/**
|
|
70
|
+
* 源解析的结构化错误 —— 不依赖 CLI:以 `messageKey` 抛出,由调用方(install 走 tCli、eval 走自身
|
|
71
|
+
* 中文错误)映射成本地化文案。住在 skill-loader(而非 source-resolver)是为了让共享的
|
|
72
|
+
* `materializeGitSkillTree` 能抛它而不致 skill-loader → source-resolver 反向成环;source-resolver
|
|
73
|
+
* re-export 以保持 install 既有 import 不破。
|
|
74
|
+
*/
|
|
75
|
+
export declare class SourceResolveError extends Error {
|
|
76
|
+
readonly messageKey: string;
|
|
77
|
+
readonly params: Record<string, string | number>;
|
|
78
|
+
constructor(messageKey: string, params?: Record<string, string | number>);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 校验 git tree 条目路径在物化目标内,越界即 fail closed(抛 SourceResolveError)。
|
|
82
|
+
* 双保险:既显式拒 `..` / 空段(git tree 可被手工构造出名为 `..` 的子树),也用 resolve 兜底
|
|
83
|
+
* 确认落点仍在 temp 之下(绝对路径 / 符号化逃逸)。绝不静默跳过——跳过会让物化树与真实树发散。
|
|
84
|
+
*/
|
|
85
|
+
export declare function assertContainedRelPath(temp: string, relPath: string): void;
|
|
86
|
+
export interface MaterializedGitTree {
|
|
87
|
+
/** 物化后的本地根:目录-skill 为临时目录、文件-skill 为临时 .md;喂 hashArtifactSource + 分发。 */
|
|
88
|
+
localRoot: string;
|
|
89
|
+
isDirectorySkill: boolean;
|
|
90
|
+
name: string;
|
|
91
|
+
/** 释放临时目录;调用方务必 try/finally 调用(物化只为算哈/分发,用完即删)。 */
|
|
92
|
+
cleanup: () => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 把 git 某个 ref 上的 skill(已由 `classifyGitSkillRef` 归类)逐文件物化到临时目录 ——
|
|
96
|
+
* **install 与 eval 共用此一处物化**,保证两侧拿到完全一致的本地树(此前 install/eval 各写一套
|
|
97
|
+
* git 解析正是四轮 bug 的根源,已靠共享 helper 收敛)。eval 仅为算整树指纹(hashArtifactSource)而
|
|
98
|
+
* 物化,算完即 cleanup;install 物化后分发并登记。失败(越界路径 / 空树 / 取不到 blob)抛
|
|
99
|
+
* SourceResolveError,绝不静默落空壳。
|
|
100
|
+
*/
|
|
101
|
+
export declare function materializeGitSkillTree(ref: string, resolved: GitSkillRef, repoRoot: string): MaterializedGitTree;
|
|
102
|
+
export interface RemoteGitCheckout {
|
|
103
|
+
/** 临时 bare 仓库路径,作为 git helper 的 cwd(repoRoot)。 */
|
|
104
|
+
repoRoot: string;
|
|
105
|
+
/** fetch 落地后 rev-parse 出的**实际 SHA**(branch/tag 会漂,记录与重取都钉 SHA)。 */
|
|
106
|
+
ref: string;
|
|
107
|
+
/** 删临时 bare 仓库;调用方务必 try/finally 调用。 */
|
|
108
|
+
cleanup: () => void;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 远端 git URL 形态校验:接受 https(s):// / ssh:// / git:// / file:// 协议 URL、scp 形式
|
|
112
|
+
* `user@host:path`、绝对本地路径(file 远端 / 测试)。明显非法(空、相对裸串)拒。
|
|
113
|
+
*/
|
|
114
|
+
export declare function isPlausibleGitUrl(url: string): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* 远端 git:clone 某 ref 到临时 bare 仓库、pin 实际 SHA —— **install 与 eval 共用此一处**,远端只是
|
|
117
|
+
* 把下游 git helper(classifyGitSkillRef / materializeGitSkillTree)的 repoRoot 从「当前仓库」换成
|
|
118
|
+
* 「fetch 下来的临时 bare」,其余一字不改。`git init --bare` + `git fetch --depth 1`(省带宽,只取该
|
|
119
|
+
* ref 的 tip 树);ref 为 branch/tag/HEAD 通用,裸 SHA 取决于服务端是否允许 reachable-SHA fetch。
|
|
120
|
+
* 认证依赖本机 git 凭证(SSH agent / credential helper),omk 不自管 token。失败 fail-closed
|
|
121
|
+
* (SourceResolveError),由调用方本地化;eval 侧捕获后改抛中文 Error。
|
|
122
|
+
*/
|
|
123
|
+
export declare function fetchRemoteGitRef(url: string, ref: string): RemoteGitCheckout;
|
|
69
124
|
export declare function discoverVariants(skillDir: string): string[];
|
|
70
125
|
export declare function discoverBatchSkills(skillDir: string): Array<{
|
|
71
126
|
name: string;
|
|
@@ -79,6 +134,9 @@ export interface ResolveArtifactsOptions {
|
|
|
79
134
|
* 显式 per-variant 隔离声明走 spec.allowedSkills(prepareEvaluationRun 按 spec 身份绑定),
|
|
80
135
|
* 不经此处——resolveArtifacts 只认 strictBaseline 默认,隔离绑定收成单一来源。 */
|
|
81
136
|
strictBaseline?: boolean;
|
|
137
|
+
/** Default true. dir-skill 是否落地隔离副本(写 `~/.oh-my-knowledge/trees` 并设 execRoot)。
|
|
138
|
+
* 只有 eval(执行隔离)需要;doctor / loadSkills 等纯读路径传 false,只算指纹与正文、不写副本。 */
|
|
139
|
+
materialize?: boolean;
|
|
82
140
|
}
|
|
83
141
|
/**
|
|
84
142
|
* Parse variant expression, extracting optional cwd suffix.
|
|
@@ -106,10 +164,15 @@ export declare function variantIdentity(expr: string, skillDir?: string, cwd?: s
|
|
|
106
164
|
export declare function skillNameFromPath(filePath: string): string;
|
|
107
165
|
/** 从 variant 表达式(纯 artifact 身份,可能是路径)取短名。 */
|
|
108
166
|
export declare function variantExprToSkillName(expr: string): string;
|
|
109
|
-
/** variant 输入:纯 artifact
|
|
167
|
+
/** variant 输入:纯 artifact 表达式字符串、结构化 `{expr, cwd?}`、或远端 git 结构化 `{git, cwd?, name}`
|
|
168
|
+
* (url/ref/spec 分字段,绝不拼成单串再经 parseGitInput/parseVariantCwd 切分)。 */
|
|
110
169
|
export type VariantInput = string | {
|
|
111
170
|
expr: string;
|
|
112
171
|
cwd?: string;
|
|
172
|
+
} | {
|
|
173
|
+
git: RemoteGitRef;
|
|
174
|
+
cwd?: string;
|
|
175
|
+
name: string;
|
|
113
176
|
};
|
|
114
177
|
export declare function resolveArtifacts(skillDir: string, variants: VariantInput[], opts?: ResolveArtifactsOptions): Artifact[];
|
|
115
178
|
/** 消歧:多个 variant resolve 出同名时(如 `v1/greeter.md` vs `v2/greeter.md` 都叫 `greeter`),
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
2
|
-
import { resolve, join, relative, dirname, basename } from 'node:path';
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync, realpathSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { resolve, join, relative, dirname, basename, sep } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { extractSkillHardRules, extractSkillWorkflows } from '../shared/hard-rules.js';
|
|
6
|
+
import { hashArtifactSource, hashBytes, isDistributablePath } from './content-hash.js';
|
|
7
|
+
import { materializeIsolatedCopy } from './materialize-copy.js';
|
|
5
8
|
function parseFrontmatterPreflight(content) {
|
|
6
9
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7
10
|
if (!match)
|
|
@@ -49,7 +52,10 @@ const GIT_PROBE_STDIO = ['ignore', 'pipe', 'ignore'];
|
|
|
49
52
|
// 由 resolveGitRepoContext 解出 repoRoot 后逐处显式传入。
|
|
50
53
|
export function gitShowFile(ref, filePath, cwd = process.cwd()) {
|
|
51
54
|
try {
|
|
52
|
-
|
|
55
|
+
// `--` 隔断 tree-ish:ref 可能来自盘上受管记录的 locator(用户可手改 / 随仓库分发,被 omk list 等只读
|
|
56
|
+
// 命令喂进来),前缀 `-` 的 ref 不得被当成 git 选项解析(与 #219 fetch 路径同口径,见
|
|
57
|
+
// feedback_git_subprocess_dashdash)。加 `--` 后 dash-ref 退化为「非法 object name」fail-closed,普通 ref 输出不变。
|
|
58
|
+
return execFileSync('git', ['cat-file', 'blob', '--', `${ref}:${filePath}`], { cwd, encoding: 'utf-8', stdio: GIT_PROBE_STDIO }).trim();
|
|
53
59
|
}
|
|
54
60
|
catch {
|
|
55
61
|
return null;
|
|
@@ -61,7 +67,7 @@ export function gitShowFile(ref, filePath, cwd = process.cwd()) {
|
|
|
61
67
|
*/
|
|
62
68
|
export function gitShowBytes(ref, filePath, cwd = process.cwd()) {
|
|
63
69
|
try {
|
|
64
|
-
return execFileSync('git', ['cat-file', 'blob', `${ref}:${filePath}`], { cwd, stdio: GIT_PROBE_STDIO }); // 无 encoding → Buffer
|
|
70
|
+
return execFileSync('git', ['cat-file', 'blob', '--', `${ref}:${filePath}`], { cwd, stdio: GIT_PROBE_STDIO }); // 无 encoding → Buffer;`--` 隔断同 gitShowFile
|
|
65
71
|
}
|
|
66
72
|
catch {
|
|
67
73
|
return null;
|
|
@@ -77,7 +83,7 @@ export function gitShowBytes(ref, filePath, cwd = process.cwd()) {
|
|
|
77
83
|
export function gitLsTreeBlobs(ref, treePath, cwd = process.cwd()) {
|
|
78
84
|
let out;
|
|
79
85
|
try {
|
|
80
|
-
out = execFileSync('git', ['ls-tree', '-r', '-z', '--full-tree', `${ref}:${treePath}`], { cwd, encoding: 'utf-8', stdio: GIT_PROBE_STDIO });
|
|
86
|
+
out = execFileSync('git', ['ls-tree', '-r', '-z', '--full-tree', '--', `${ref}:${treePath}`], { cwd, encoding: 'utf-8', stdio: GIT_PROBE_STDIO });
|
|
81
87
|
}
|
|
82
88
|
catch {
|
|
83
89
|
return [];
|
|
@@ -179,6 +185,158 @@ export function classifyGitSkillRef(ref, gitRelDir, spec, cwd = process.cwd()) {
|
|
|
179
185
|
}
|
|
180
186
|
return null;
|
|
181
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* 源解析的结构化错误 —— 不依赖 CLI:以 `messageKey` 抛出,由调用方(install 走 tCli、eval 走自身
|
|
190
|
+
* 中文错误)映射成本地化文案。住在 skill-loader(而非 source-resolver)是为了让共享的
|
|
191
|
+
* `materializeGitSkillTree` 能抛它而不致 skill-loader → source-resolver 反向成环;source-resolver
|
|
192
|
+
* re-export 以保持 install 既有 import 不破。
|
|
193
|
+
*/
|
|
194
|
+
export class SourceResolveError extends Error {
|
|
195
|
+
messageKey;
|
|
196
|
+
params;
|
|
197
|
+
constructor(messageKey, params = {}) {
|
|
198
|
+
super(messageKey);
|
|
199
|
+
this.name = 'SourceResolveError';
|
|
200
|
+
this.messageKey = messageKey;
|
|
201
|
+
this.params = params;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 校验 git tree 条目路径在物化目标内,越界即 fail closed(抛 SourceResolveError)。
|
|
206
|
+
* 双保险:既显式拒 `..` / 空段(git tree 可被手工构造出名为 `..` 的子树),也用 resolve 兜底
|
|
207
|
+
* 确认落点仍在 temp 之下(绝对路径 / 符号化逃逸)。绝不静默跳过——跳过会让物化树与真实树发散。
|
|
208
|
+
*/
|
|
209
|
+
export function assertContainedRelPath(temp, relPath) {
|
|
210
|
+
const segments = relPath.split('/');
|
|
211
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..')) {
|
|
212
|
+
throw new SourceResolveError('cli.install.git_unsafe_path', { path: relPath });
|
|
213
|
+
}
|
|
214
|
+
const root = resolve(temp);
|
|
215
|
+
const dest = resolve(temp, relPath);
|
|
216
|
+
if (dest !== root && !dest.startsWith(root + sep)) {
|
|
217
|
+
throw new SourceResolveError('cli.install.git_unsafe_path', { path: relPath });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 把 git 某个 ref 上的 skill(已由 `classifyGitSkillRef` 归类)逐文件物化到临时目录 ——
|
|
222
|
+
* **install 与 eval 共用此一处物化**,保证两侧拿到完全一致的本地树(此前 install/eval 各写一套
|
|
223
|
+
* git 解析正是四轮 bug 的根源,已靠共享 helper 收敛)。eval 仅为算整树指纹(hashArtifactSource)而
|
|
224
|
+
* 物化,算完即 cleanup;install 物化后分发并登记。失败(越界路径 / 空树 / 取不到 blob)抛
|
|
225
|
+
* SourceResolveError,绝不静默落空壳。
|
|
226
|
+
*/
|
|
227
|
+
export function materializeGitSkillTree(ref, resolved, repoRoot) {
|
|
228
|
+
const temp = mkdtempSync(join(tmpdir(), 'omk-git-skill-'));
|
|
229
|
+
const cleanup = () => {
|
|
230
|
+
try {
|
|
231
|
+
rmSync(temp, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// 临时目录清理失败不致命
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
if (resolved.isDir) {
|
|
239
|
+
for (const entry of gitLsTreeBlobs(ref, resolved.treePath, repoRoot)) {
|
|
240
|
+
// 安全边界 fail closed:git tree 可被 git mktree 手工构造出名为 `..` 的子树,`ls-tree -r` 会
|
|
241
|
+
// 吐 `../evil.txt`,`join(temp, ...)` 会逃出临时目录写盘、cleanup 也删不掉。任一越界路径直接抛错,
|
|
242
|
+
// 而非静默跳过——静默跳过会让物化树与真实 git tree / hash / 分发树发散。正常 checkout 永不触发。
|
|
243
|
+
assertContainedRelPath(temp, entry.path);
|
|
244
|
+
if (entry.mode === '120000' || entry.mode === '160000')
|
|
245
|
+
continue; // 跳过软链 / submodule(与本地分发一致)
|
|
246
|
+
if (!isDistributablePath(entry.path.split('/')))
|
|
247
|
+
continue; // 排除 .omk/.git/evolve 等
|
|
248
|
+
const bytes = gitShowBytes(ref, gitJoin(resolved.treePath, entry.path), repoRoot);
|
|
249
|
+
if (!bytes)
|
|
250
|
+
continue;
|
|
251
|
+
const dest = join(temp, entry.path);
|
|
252
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
253
|
+
// 保留可执行位(与本地 cpSync 一致);其余 0644。
|
|
254
|
+
writeFileSync(dest, bytes, { mode: entry.mode === '100755' ? 0o755 : 0o644 });
|
|
255
|
+
}
|
|
256
|
+
// 纵深防御:classify 与物化用的是两条 git 路径(gitShowFile vs ls-tree),万一发散(如 treePath 退化)
|
|
257
|
+
// 导致空树,这里失败而非静默物化一个空 skill。
|
|
258
|
+
if (!existsSync(join(temp, 'SKILL.md'))) {
|
|
259
|
+
throw new SourceResolveError('cli.install.git_skill_not_found', { ref, name: resolved.name });
|
|
260
|
+
}
|
|
261
|
+
return { localRoot: temp, isDirectorySkill: true, name: resolved.name, cleanup };
|
|
262
|
+
}
|
|
263
|
+
const bytes = gitShowBytes(ref, resolved.fileSkillPath, repoRoot);
|
|
264
|
+
if (!bytes)
|
|
265
|
+
throw new SourceResolveError('cli.install.git_skill_not_found', { ref, name: resolved.name });
|
|
266
|
+
const dest = join(temp, `${resolved.name}.md`);
|
|
267
|
+
writeFileSync(dest, bytes);
|
|
268
|
+
return { localRoot: dest, isDirectorySkill: false, name: resolved.name, cleanup };
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
cleanup();
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 远端 git URL 形态校验:接受 https(s):// / ssh:// / git:// / file:// 协议 URL、scp 形式
|
|
277
|
+
* `user@host:path`、绝对本地路径(file 远端 / 测试)。明显非法(空、相对裸串)拒。
|
|
278
|
+
*/
|
|
279
|
+
export function isPlausibleGitUrl(url) {
|
|
280
|
+
if (!url)
|
|
281
|
+
return false;
|
|
282
|
+
if (/^(https?|ssh|git|file):\/\//i.test(url))
|
|
283
|
+
return true;
|
|
284
|
+
if (/^[\w.-]+@[\w.-]+:/.test(url))
|
|
285
|
+
return true;
|
|
286
|
+
if (/^\//.test(url))
|
|
287
|
+
return true;
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* 远端 git:clone 某 ref 到临时 bare 仓库、pin 实际 SHA —— **install 与 eval 共用此一处**,远端只是
|
|
292
|
+
* 把下游 git helper(classifyGitSkillRef / materializeGitSkillTree)的 repoRoot 从「当前仓库」换成
|
|
293
|
+
* 「fetch 下来的临时 bare」,其余一字不改。`git init --bare` + `git fetch --depth 1`(省带宽,只取该
|
|
294
|
+
* ref 的 tip 树);ref 为 branch/tag/HEAD 通用,裸 SHA 取决于服务端是否允许 reachable-SHA fetch。
|
|
295
|
+
* 认证依赖本机 git 凭证(SSH agent / credential helper),omk 不自管 token。失败 fail-closed
|
|
296
|
+
* (SourceResolveError),由调用方本地化;eval 侧捕获后改抛中文 Error。
|
|
297
|
+
*/
|
|
298
|
+
export function fetchRemoteGitRef(url, ref) {
|
|
299
|
+
if (!isPlausibleGitUrl(url))
|
|
300
|
+
throw new SourceResolveError('cli.install.invalid_remote_url', { url });
|
|
301
|
+
const bare = mkdtempSync(join(tmpdir(), 'omk-git-remote-'));
|
|
302
|
+
const cleanup = () => {
|
|
303
|
+
try {
|
|
304
|
+
rmSync(bare, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// 临时目录清理失败不致命
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
try {
|
|
311
|
+
execFileSync('git', ['init', '--bare', '--quiet', bare], { stdio: GIT_PROBE_STDIO });
|
|
312
|
+
try {
|
|
313
|
+
// `--` 隔断:git fetch 会扫描**全部** argv 找带短横的选项,不止第一个。少了 `--`,一个形如
|
|
314
|
+
// `--upload-pack=<cmd>` 的 ref 会被当成选项执行任意命令(RCE)。`--` 后一切按位置参数
|
|
315
|
+
// (repository + refspec)解析,恶意 ref 退化成非法 refspec、fail-closed。url 也一并护在 `--` 后。
|
|
316
|
+
execFileSync('git', ['--git-dir', bare, 'fetch', '--depth', '1', '--quiet', '--', url, `${ref}:refs/omk/fetched`], { stdio: GIT_PROBE_STDIO });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
throw new SourceResolveError('cli.install.remote_fetch_failed', { url, ref });
|
|
320
|
+
}
|
|
321
|
+
let sha;
|
|
322
|
+
try {
|
|
323
|
+
sha = execFileSync('git', ['--git-dir', bare, 'rev-parse', 'refs/omk/fetched'], {
|
|
324
|
+
encoding: 'utf-8',
|
|
325
|
+
stdio: GIT_PROBE_STDIO,
|
|
326
|
+
}).trim();
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
throw new SourceResolveError('cli.install.remote_ref_not_found', { url, ref });
|
|
330
|
+
}
|
|
331
|
+
if (!sha)
|
|
332
|
+
throw new SourceResolveError('cli.install.remote_ref_not_found', { url, ref });
|
|
333
|
+
return { repoRoot: bare, ref: sha, cleanup };
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
cleanup();
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
182
340
|
export function discoverVariants(skillDir) {
|
|
183
341
|
if (!existsSync(skillDir))
|
|
184
342
|
return [];
|
|
@@ -254,7 +412,8 @@ export function discoverBatchSkills(skillDir) {
|
|
|
254
412
|
return skills;
|
|
255
413
|
}
|
|
256
414
|
export function loadSkills(skillDir, variants) {
|
|
257
|
-
|
|
415
|
+
// 只取 content 映射,不执行 → 不落副本(materialize:false)。
|
|
416
|
+
return Object.fromEntries(resolveArtifacts(skillDir, variants, { materialize: false }).map((artifact) => [artifact.name, artifact.content]));
|
|
258
417
|
}
|
|
259
418
|
/**
|
|
260
419
|
* Parse variant expression, extracting optional cwd suffix.
|
|
@@ -308,7 +467,8 @@ export function variantIdentity(expr, skillDir, cwd) {
|
|
|
308
467
|
// expr 已是纯 artifact 身份(@cwd 在 CLI/config 边界已剥离);cwd 结构化显式传入。
|
|
309
468
|
const name = expr;
|
|
310
469
|
let id;
|
|
311
|
-
if (name.startsWith('git:')) {
|
|
470
|
+
if (name.startsWith('git:') || name.startsWith('git+')) {
|
|
471
|
+
// 本地 git:<ref>:<spec> 与远端 git+<url>@<sha>:<spec> 都是稳定身份串,原样作 key,绝不再 split。
|
|
312
472
|
id = name;
|
|
313
473
|
}
|
|
314
474
|
else if (name === 'baseline') {
|
|
@@ -346,11 +506,97 @@ export function variantExprToSkillName(expr) {
|
|
|
346
506
|
// 以 .md 结尾的目录,两处会派生出不同短名)。
|
|
347
507
|
return skillNameFromPath(canonicalSkillAnchor(resolve(expr))) || expr;
|
|
348
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* 解析一个目录-skill 的整树指纹 + SKILL.md 正文;`materialize` 为真时另落地内容寻址隔离副本、返回执行根。
|
|
511
|
+
* 本地源 localDir 是真源目录;git 源 localDir 是 materializeGitSkillTree 物化出的临时目录。
|
|
512
|
+
* execRoot(副本)供 executor cwd / skillDir 锚定;skillRoot(真源)由各分支自行保留。
|
|
513
|
+
* 只有 eval 路径需要副本(执行隔离);doctor / loadSkills 只需指纹与正文,materialize=false 不落副本、
|
|
514
|
+
* 不写 `~/.oh-my-knowledge/trees`(避免纯读路径的副作用 I/O 与 trees 污染)。
|
|
515
|
+
*/
|
|
516
|
+
function isolateDirSkill(localDir, name, materialize) {
|
|
517
|
+
const content = readFileSync(join(localDir, 'SKILL.md'), 'utf-8').trim();
|
|
518
|
+
if (materialize) {
|
|
519
|
+
const copy = materializeIsolatedCopy(localDir, true, name);
|
|
520
|
+
return { execRoot: copy.copyRoot, contentHash: copy.contentHash, content };
|
|
521
|
+
}
|
|
522
|
+
return { contentHash: hashArtifactSource(localDir, true), content };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* 远端 git variant:fetch 到临时 bare → classify → materialize 临时树 → 隔离副本(dir,materialize 时)
|
|
526
|
+
* 或单文件字节(file)。url/ref/spec 全程结构化,locator 落 `git+<url>@<sha>:<spec>` 身份串。
|
|
527
|
+
* SourceResolveError(install 口径的 messageKey)在此转成 eval 的中文 Error。临时树与 bare 用完即清。
|
|
528
|
+
*/
|
|
529
|
+
function resolveRemoteGitVariant(input, materialize, artifacts) {
|
|
530
|
+
const { url, spec } = input.git;
|
|
531
|
+
const ref = input.git.ref ?? 'HEAD';
|
|
532
|
+
let checkout;
|
|
533
|
+
try {
|
|
534
|
+
checkout = fetchRemoteGitRef(url, ref);
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
if (err instanceof SourceResolveError) {
|
|
538
|
+
const zh = err.messageKey === 'cli.install.invalid_remote_url'
|
|
539
|
+
? `不是合法的 git 远端 URL:${url}`
|
|
540
|
+
: `从远端 ${url} 拉取 ref ${ref} 失败,请检查 URL / ref 是否存在与本机 git 凭证。`;
|
|
541
|
+
throw new Error(zh);
|
|
542
|
+
}
|
|
543
|
+
throw err;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const resolved = classifyGitSkillRef(checkout.ref, '', spec, checkout.repoRoot);
|
|
547
|
+
if (!resolved)
|
|
548
|
+
throw new Error(`在远端 ${url} 的 ref ${ref} 下找不到 skill ${spec}(既无 ${spec}/SKILL.md 也无 ${spec}.md)。`);
|
|
549
|
+
const mat = materializeGitSkillTree(checkout.ref, resolved, checkout.repoRoot);
|
|
550
|
+
try {
|
|
551
|
+
const locator = `git+${url}@${checkout.ref}:${spec}`;
|
|
552
|
+
if (resolved.isDir) {
|
|
553
|
+
const iso = isolateDirSkill(mat.localRoot, resolved.name, materialize);
|
|
554
|
+
artifacts.push({
|
|
555
|
+
name: input.name,
|
|
556
|
+
kind: 'skill',
|
|
557
|
+
source: 'git',
|
|
558
|
+
content: iso.content,
|
|
559
|
+
contentHash: iso.contentHash,
|
|
560
|
+
locator,
|
|
561
|
+
ref: checkout.ref,
|
|
562
|
+
cwd: input.cwd,
|
|
563
|
+
...(iso.execRoot ? { execRoot: iso.execRoot } : {}),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
const bytes = readFileSync(mat.localRoot);
|
|
568
|
+
artifacts.push({
|
|
569
|
+
name: input.name,
|
|
570
|
+
kind: 'skill',
|
|
571
|
+
source: 'git',
|
|
572
|
+
content: bytes.toString('utf-8').trim(),
|
|
573
|
+
contentHash: hashArtifactSource(mat.localRoot, false),
|
|
574
|
+
locator,
|
|
575
|
+
ref: checkout.ref,
|
|
576
|
+
cwd: input.cwd,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
mat.cleanup();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
checkout.cleanup();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
349
588
|
export function resolveArtifacts(skillDir, variants, opts = {}) {
|
|
350
589
|
const strictBaseline = opts.strictBaseline ?? true;
|
|
590
|
+
const materialize = opts.materialize ?? true;
|
|
351
591
|
const artifacts = [];
|
|
352
592
|
let gitCtx = null;
|
|
353
593
|
for (const rawVariant of variants) {
|
|
594
|
+
// 远端 git 结构化输入:url/ref/spec 分字段,fetch 到临时 bare 后复用 classify/materialize/隔离副本,
|
|
595
|
+
// URL 全程不经任何字符串切分(避开 parseGitInput 的 `:` 与 parseVariantCwd 的 `@`)。
|
|
596
|
+
if (typeof rawVariant === 'object' && 'git' in rawVariant) {
|
|
597
|
+
resolveRemoteGitVariant(rawVariant, materialize, artifacts);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
354
600
|
// 字符串即纯 expr(无 cwd);结构化对象直接取 expr/cwd。不再 split @cwd。
|
|
355
601
|
const variantName = typeof rawVariant === 'string' ? rawVariant : rawVariant.expr;
|
|
356
602
|
const variantCwd = typeof rawVariant === 'string' ? undefined : rawVariant.cwd;
|
|
@@ -383,10 +629,39 @@ export function resolveArtifacts(skillDir, variants, opts = {}) {
|
|
|
383
629
|
gitCtx = resolveGitRepoContext(skillDir);
|
|
384
630
|
// file-vs-dir 归类与 install 共用 classifyGitSkillRef(裸 spec 文件优先),两条路径绝不发散。
|
|
385
631
|
const resolved = classifyGitSkillRef(ref, gitCtx.relDir, name, gitCtx.repoRoot);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (
|
|
632
|
+
if (!resolved) {
|
|
633
|
+
throw new Error(`skill not found in git ${ref}: ${name}.md or ${name}/SKILL.md`);
|
|
634
|
+
}
|
|
635
|
+
if (resolved.isDir) {
|
|
636
|
+
// git 目录-skill 忠实执行:物化整树到临时目录 → 落地内容寻址隔离副本 → executor cwd 锚副本,
|
|
637
|
+
// agent 读得到 references/ 资产、资产成为真实运行时输入。整树指纹与 install 受管记录的
|
|
638
|
+
// contentHash 落同一空间 → evidence 可绑(见 docs/specs/evidence-gated-management.md §9)。
|
|
639
|
+
const mat = materializeGitSkillTree(ref, resolved, gitCtx.repoRoot);
|
|
640
|
+
let isolated;
|
|
641
|
+
try {
|
|
642
|
+
isolated = isolateDirSkill(mat.localRoot, resolved.name, materialize);
|
|
643
|
+
}
|
|
644
|
+
finally {
|
|
645
|
+
mat.cleanup(); // 临时物化树只为喂副本,落地完即删
|
|
646
|
+
}
|
|
647
|
+
artifacts.push({
|
|
648
|
+
name: variantName,
|
|
649
|
+
kind: 'skill',
|
|
650
|
+
source: 'git',
|
|
651
|
+
content: isolated.content,
|
|
652
|
+
contentHash: isolated.contentHash,
|
|
653
|
+
locator: name,
|
|
654
|
+
ref,
|
|
655
|
+
cwd: variantCwd,
|
|
656
|
+
...(isolated.execRoot ? { execRoot: isolated.execRoot } : {}),
|
|
657
|
+
});
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
// git 文件-skill:单个 .md 字节(与 install 单文件分支「不 trim」一致),无资产、本就与本地 /
|
|
661
|
+
// file 源同空间可绑,不走副本。
|
|
662
|
+
const skillMdBytes = gitShowBytes(ref, resolved.fileSkillPath, gitCtx.repoRoot);
|
|
663
|
+
const content = skillMdBytes !== null ? skillMdBytes.toString('utf-8').trim() : null;
|
|
664
|
+
if (!skillMdBytes || !content) {
|
|
390
665
|
throw new Error(`skill not found in git ${ref}: ${name}.md or ${name}/SKILL.md`);
|
|
391
666
|
}
|
|
392
667
|
artifacts.push({
|
|
@@ -394,6 +669,7 @@ export function resolveArtifacts(skillDir, variants, opts = {}) {
|
|
|
394
669
|
kind: 'skill',
|
|
395
670
|
source: 'git',
|
|
396
671
|
content,
|
|
672
|
+
contentHash: hashBytes(skillMdBytes),
|
|
397
673
|
locator: name,
|
|
398
674
|
ref,
|
|
399
675
|
cwd: variantCwd,
|
|
@@ -415,14 +691,32 @@ export function resolveArtifacts(skillDir, variants, opts = {}) {
|
|
|
415
691
|
const content = readFileSync(filePath, 'utf-8').trim();
|
|
416
692
|
const isSkillMd = basename(filePath) === 'SKILL.md';
|
|
417
693
|
const name = skillNameFromPath(filePath);
|
|
694
|
+
if (isSkillMd) {
|
|
695
|
+
// dir-skill:落地隔离副本,执行根锚副本(agent 读 references)、整树指纹;skillRoot 仍记真源(doctor 等用)。
|
|
696
|
+
const isolated = isolateDirSkill(dirname(filePath), name, materialize);
|
|
697
|
+
artifacts.push({
|
|
698
|
+
name,
|
|
699
|
+
kind: 'skill',
|
|
700
|
+
source: 'file-path',
|
|
701
|
+
content: isolated.content,
|
|
702
|
+
contentHash: isolated.contentHash,
|
|
703
|
+
locator: filePath,
|
|
704
|
+
cwd: variantCwd,
|
|
705
|
+
skillRoot: dirname(filePath),
|
|
706
|
+
...(isolated.execRoot ? { execRoot: isolated.execRoot } : {}),
|
|
707
|
+
metadata: buildMetadata(isolated.content),
|
|
708
|
+
});
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
// 裸 .md 文件-skill:单文件字节,无资产不 copy。
|
|
418
712
|
artifacts.push({
|
|
419
713
|
name,
|
|
420
714
|
kind: 'skill',
|
|
421
715
|
source: 'file-path',
|
|
422
716
|
content,
|
|
717
|
+
contentHash: hashArtifactSource(filePath, false),
|
|
423
718
|
locator: filePath,
|
|
424
719
|
cwd: variantCwd,
|
|
425
|
-
...(isSkillMd && { skillRoot: dirname(filePath) }),
|
|
426
720
|
metadata: buildMetadata(content),
|
|
427
721
|
});
|
|
428
722
|
continue;
|
|
@@ -437,23 +731,27 @@ export function resolveArtifacts(skillDir, variants, opts = {}) {
|
|
|
437
731
|
kind: 'skill',
|
|
438
732
|
source: 'variant-name',
|
|
439
733
|
content,
|
|
734
|
+
contentHash: hashArtifactSource(mdPath, false),
|
|
440
735
|
locator: mdPath,
|
|
441
736
|
cwd: variantCwd,
|
|
442
737
|
metadata: buildMetadata(content),
|
|
443
738
|
});
|
|
444
739
|
}
|
|
445
740
|
else if (existsSync(dirSkillPath)) {
|
|
446
|
-
// directory-skill:SKILL.md 引相对路径 assets
|
|
447
|
-
|
|
741
|
+
// directory-skill:SKILL.md 引相对路径 assets;落地隔离副本,executor cwd 锚副本读 references,
|
|
742
|
+
// skillRoot 仍记真源(doctor / dependency-checker 用)。
|
|
743
|
+
const isolated = isolateDirSkill(dirname(dirSkillPath), variantName, materialize);
|
|
448
744
|
artifacts.push({
|
|
449
745
|
name: variantName,
|
|
450
746
|
kind: 'skill',
|
|
451
747
|
source: 'variant-name',
|
|
452
|
-
content,
|
|
748
|
+
content: isolated.content,
|
|
749
|
+
contentHash: isolated.contentHash,
|
|
453
750
|
locator: dirSkillPath,
|
|
454
751
|
cwd: variantCwd,
|
|
455
752
|
skillRoot: dirname(dirSkillPath),
|
|
456
|
-
|
|
753
|
+
...(isolated.execRoot ? { execRoot: isolated.execRoot } : {}),
|
|
754
|
+
metadata: buildMetadata(isolated.content),
|
|
457
755
|
});
|
|
458
756
|
}
|
|
459
757
|
else if (variantCwd) {
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 新增一种源 = 加一个分支,install.ts 不动。本模块不依赖 CLI(不打印、不 tCli):错误以
|
|
4
4
|
* `SourceResolveError(messageKey)` 抛出,由调用方(install)映射成本地化文案。
|
|
5
5
|
*
|
|
6
|
-
* 当前支持:`file`(本地路径)、`git`(当前仓库的某个 ref)
|
|
7
|
-
*
|
|
6
|
+
* 当前支持:`file`(本地路径)、`git`(当前仓库的某个 ref)、远端 git(URL + ref,fetch 到临时 bare)。
|
|
7
|
+
* 三者沿同一 `ResolvedSource` 契约,install / managed 主干源无关。
|
|
8
8
|
*/
|
|
9
|
+
export { SourceResolveError } from './skill-loader.js';
|
|
9
10
|
export type SourceKind = 'file' | 'git';
|
|
10
11
|
export interface ResolvedSource {
|
|
11
12
|
sourceKind: SourceKind;
|
|
@@ -14,15 +15,21 @@ export interface ResolvedSource {
|
|
|
14
15
|
/** skill 短名(分发落点 / 记录 name)。 */
|
|
15
16
|
name: string;
|
|
16
17
|
isDirectorySkill: boolean;
|
|
17
|
-
/** 记录身份:file
|
|
18
|
+
/** 记录身份:file=绝对路径;本地 git=`git:<ref>:<spec>`;远端 git=`git+<url>@<sha>:<spec>`
|
|
19
|
+
* (均非临时物化路径;远端身份串仅作记录/相等比较,绝不回喂 parseGitInput 的 `:` 切分)。 */
|
|
18
20
|
locator: string;
|
|
21
|
+
/** git 来源的 ref;远端为 fetch 后 pin 的实际 SHA。 */
|
|
19
22
|
ref?: string;
|
|
20
|
-
/**
|
|
23
|
+
/** 远端 git 的 URL(本地源 / 本地 git 无)。供受管记录结构化存、drift 重取,不挤进 locator 再 split。 */
|
|
24
|
+
url?: string;
|
|
25
|
+
/** 释放临时资源(git 删临时目录 / 远端另删 bare);file 源为 noop。 */
|
|
21
26
|
cleanup: () => void;
|
|
22
27
|
}
|
|
23
|
-
export declare class SourceResolveError extends Error {
|
|
24
|
-
readonly messageKey: string;
|
|
25
|
-
readonly params: Record<string, string | number>;
|
|
26
|
-
constructor(messageKey: string, params?: Record<string, string | number>);
|
|
27
|
-
}
|
|
28
28
|
export declare function resolveInstallSource(input: string): ResolvedSource;
|
|
29
|
+
/**
|
|
30
|
+
* 远端 git 源:`<url>` + `<ref>` + repo 相对 `<spec>`(三者结构化传入,URL 不经任何字符串切分,
|
|
31
|
+
* 避开 `parseGitInput` 的 `:` 与 `parseVariantCwd` 的 `@`)。fetch 到临时 bare → 复用既有
|
|
32
|
+
* classifyGitSkillRef / materializeGitSkillTree(repoRoot 换成 bare)。locator 落 `git+<url>@<sha>:<spec>`
|
|
33
|
+
* 仅作记录身份;url 另以结构化字段携带,供 drift 重取。失败 fail-closed 并清临时资源。
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveRemoteGitSource(url: string, ref: string, spec: string): ResolvedSource;
|