@omnidev-ai/core 0.11.0 → 0.12.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/dist/index.d.ts +190 -7
- package/dist/index.js +773 -91
- package/package.json +1 -1
- package/src/capability/loader.ts +32 -4
- package/src/capability/sources.ts +303 -56
- package/src/index.ts +3 -0
- package/src/mcp-json/manager.ts +0 -7
- package/src/security/index.ts +11 -0
- package/src/security/scanner.ts +563 -0
- package/src/security/types.ts +108 -0
- package/src/state/index.ts +1 -0
- package/src/state/security-allows.ts +178 -0
- package/src/sync.ts +65 -45
- package/src/types/index.ts +52 -1
package/package.json
CHANGED
package/src/capability/loader.ts
CHANGED
|
@@ -71,17 +71,35 @@ export async function loadCapabilityConfig(capabilityPath: string): Promise<Capa
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Dynamically imports capability exports
|
|
75
|
-
*
|
|
74
|
+
* Dynamically imports capability exports.
|
|
75
|
+
* Checks for built output (dist/index.js) first, then falls back to index.js or index.ts.
|
|
76
|
+
* Returns an empty object if no entry point exists.
|
|
76
77
|
*
|
|
77
78
|
* @param capabilityPath - Path to the capability directory
|
|
78
79
|
* @returns Exported module or empty object
|
|
79
80
|
* @throws Error if import fails
|
|
80
81
|
*/
|
|
81
82
|
async function importCapabilityExports(capabilityPath: string): Promise<Record<string, unknown>> {
|
|
82
|
-
|
|
83
|
+
// Check for entry points in order of preference:
|
|
84
|
+
// 1. Built output (dist/index.js) - compiled TypeScript
|
|
85
|
+
// 2. Plain JavaScript (index.js)
|
|
86
|
+
// 3. TypeScript source (index.ts) - only works with TypeScript-aware runtimes
|
|
87
|
+
const builtIndexPath = join(capabilityPath, "dist", "index.js");
|
|
88
|
+
const jsIndexPath = join(capabilityPath, "index.js");
|
|
89
|
+
const tsIndexPath = join(capabilityPath, "index.ts");
|
|
90
|
+
|
|
91
|
+
let indexPath: string | null = null;
|
|
92
|
+
|
|
93
|
+
if (existsSync(builtIndexPath)) {
|
|
94
|
+
indexPath = builtIndexPath;
|
|
95
|
+
} else if (existsSync(jsIndexPath)) {
|
|
96
|
+
indexPath = jsIndexPath;
|
|
97
|
+
} else if (existsSync(tsIndexPath)) {
|
|
98
|
+
// TypeScript file exists but no built output - this will likely fail with Node.js
|
|
99
|
+
indexPath = tsIndexPath;
|
|
100
|
+
}
|
|
83
101
|
|
|
84
|
-
if (!
|
|
102
|
+
if (!indexPath) {
|
|
85
103
|
return {};
|
|
86
104
|
}
|
|
87
105
|
|
|
@@ -95,6 +113,16 @@ async function importCapabilityExports(capabilityPath: string): Promise<Record<s
|
|
|
95
113
|
if (errorMessage.includes("Cannot find module")) {
|
|
96
114
|
const match = errorMessage.match(/Cannot find module '([^']+)'/);
|
|
97
115
|
const missingModule = match ? match[1] : "unknown";
|
|
116
|
+
|
|
117
|
+
// Check if this is a TypeScript resolution issue
|
|
118
|
+
if (indexPath === tsIndexPath && missingModule?.endsWith(".js")) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Capability at ${capabilityPath} has TypeScript files but no built output.\n` +
|
|
121
|
+
`Add a "build" script to package.json (e.g., "build": "tsc") and run it to compile TypeScript.\n` +
|
|
122
|
+
`The build output should be in dist/index.js.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
throw new Error(
|
|
99
127
|
`Missing dependency '${missingModule}' for capability at ${capabilityPath}.\n` +
|
|
100
128
|
`If this is a project-specific capability, install dependencies or remove it from .omni/capabilities/`,
|
|
@@ -20,8 +20,10 @@ import type {
|
|
|
20
20
|
FileCapabilitySourceConfig,
|
|
21
21
|
GitCapabilitySourceConfig,
|
|
22
22
|
OmniConfig,
|
|
23
|
+
VersionSource,
|
|
23
24
|
} from "../types/index.js";
|
|
24
25
|
import { isFileSourceConfig } from "../types/index.js";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
25
27
|
|
|
26
28
|
// Local path for .omni directory
|
|
27
29
|
const OMNI_LOCAL = ".omni";
|
|
@@ -42,8 +44,12 @@ export interface FetchResult {
|
|
|
42
44
|
id: string;
|
|
43
45
|
path: string;
|
|
44
46
|
version: string;
|
|
47
|
+
/** Source where version was detected from */
|
|
48
|
+
versionSource: VersionSource;
|
|
45
49
|
/** Git commit hash */
|
|
46
50
|
commit?: string;
|
|
51
|
+
/** Content hash for file sources (SHA-256) */
|
|
52
|
+
contentHash?: string;
|
|
47
53
|
updated: boolean;
|
|
48
54
|
wrapped: boolean;
|
|
49
55
|
}
|
|
@@ -243,12 +249,18 @@ function stringifyLockFile(lockFile: CapabilitiesLockFile): string {
|
|
|
243
249
|
lines.push(`[capabilities.${id}]`);
|
|
244
250
|
lines.push(`source = "${entry.source}"`);
|
|
245
251
|
lines.push(`version = "${entry.version}"`);
|
|
252
|
+
if (entry.version_source) {
|
|
253
|
+
lines.push(`version_source = "${entry.version_source}"`);
|
|
254
|
+
}
|
|
246
255
|
if (entry.commit) {
|
|
247
256
|
lines.push(`commit = "${entry.commit}"`);
|
|
248
257
|
}
|
|
249
258
|
if (entry.ref) {
|
|
250
259
|
lines.push(`ref = "${entry.ref}"`);
|
|
251
260
|
}
|
|
261
|
+
if (entry.content_hash) {
|
|
262
|
+
lines.push(`content_hash = "${entry.content_hash}"`);
|
|
263
|
+
}
|
|
252
264
|
lines.push(`updated_at = "${entry.updated_at}"`);
|
|
253
265
|
lines.push("");
|
|
254
266
|
}
|
|
@@ -294,6 +306,150 @@ function shortCommit(commit: string): string {
|
|
|
294
306
|
return commit.substring(0, 7);
|
|
295
307
|
}
|
|
296
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Get short content hash (12 chars)
|
|
311
|
+
*/
|
|
312
|
+
function shortContentHash(hash: string): string {
|
|
313
|
+
return hash.substring(0, 12);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Default patterns to exclude from content hashing
|
|
318
|
+
*/
|
|
319
|
+
const CONTENT_HASH_EXCLUDES = [
|
|
320
|
+
".git",
|
|
321
|
+
"node_modules",
|
|
322
|
+
".omni",
|
|
323
|
+
"__pycache__",
|
|
324
|
+
".pytest_cache",
|
|
325
|
+
".mypy_cache",
|
|
326
|
+
"dist",
|
|
327
|
+
"build",
|
|
328
|
+
".DS_Store",
|
|
329
|
+
"Thumbs.db",
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Compute a stable SHA-256 content hash for a directory.
|
|
334
|
+
* Files are processed in sorted order to ensure deterministic output.
|
|
335
|
+
* Excludes common non-semantic artifacts (.git, node_modules, etc.)
|
|
336
|
+
*/
|
|
337
|
+
export async function computeContentHash(
|
|
338
|
+
dirPath: string,
|
|
339
|
+
excludePatterns: string[] = CONTENT_HASH_EXCLUDES,
|
|
340
|
+
): Promise<string> {
|
|
341
|
+
const hash = createHash("sha256");
|
|
342
|
+
const files: Array<{ relativePath: string; content: Buffer }> = [];
|
|
343
|
+
|
|
344
|
+
async function collectFiles(currentPath: string, relativeTo: string): Promise<void> {
|
|
345
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
346
|
+
// Sort entries for deterministic ordering
|
|
347
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
348
|
+
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const fullPath = join(currentPath, entry.name);
|
|
351
|
+
const relativePath = fullPath.slice(relativeTo.length + 1);
|
|
352
|
+
|
|
353
|
+
// Skip excluded patterns
|
|
354
|
+
if (
|
|
355
|
+
excludePatterns.some(
|
|
356
|
+
(pattern) => entry.name === pattern || relativePath.startsWith(`${pattern}/`),
|
|
357
|
+
)
|
|
358
|
+
) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (entry.isDirectory()) {
|
|
363
|
+
await collectFiles(fullPath, relativeTo);
|
|
364
|
+
} else if (entry.isFile()) {
|
|
365
|
+
const content = await readFile(fullPath);
|
|
366
|
+
files.push({ relativePath, content });
|
|
367
|
+
}
|
|
368
|
+
// Skip symlinks for security
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
await collectFiles(dirPath, dirPath);
|
|
373
|
+
|
|
374
|
+
// Sort files by path and hash them
|
|
375
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
// Include both path and content in hash for integrity
|
|
378
|
+
hash.update(file.relativePath);
|
|
379
|
+
hash.update(file.content);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return hash.digest("hex");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Result of version detection
|
|
387
|
+
*/
|
|
388
|
+
export interface VersionDetectionResult {
|
|
389
|
+
version: string;
|
|
390
|
+
source: VersionSource;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Detect the display version for a capability directory.
|
|
395
|
+
* Checks in order: capability.toml > plugin.json > package.json > fallback
|
|
396
|
+
*
|
|
397
|
+
* @param dirPath - Path to the capability directory
|
|
398
|
+
* @param fallback - Fallback version if no source is found (e.g., commit hash or content hash)
|
|
399
|
+
* @param fallbackSource - Source type for the fallback
|
|
400
|
+
*/
|
|
401
|
+
export async function detectDisplayVersion(
|
|
402
|
+
dirPath: string,
|
|
403
|
+
fallback: string,
|
|
404
|
+
fallbackSource: VersionSource,
|
|
405
|
+
): Promise<VersionDetectionResult> {
|
|
406
|
+
// 1. Check capability.toml
|
|
407
|
+
const capTomlPath = join(dirPath, "capability.toml");
|
|
408
|
+
if (existsSync(capTomlPath)) {
|
|
409
|
+
try {
|
|
410
|
+
const content = await readFile(capTomlPath, "utf-8");
|
|
411
|
+
const parsed = parseToml(content) as Record<string, unknown>;
|
|
412
|
+
const capability = parsed["capability"] as Record<string, unknown> | undefined;
|
|
413
|
+
if (capability?.["version"] && typeof capability["version"] === "string") {
|
|
414
|
+
return { version: capability["version"], source: "capability.toml" };
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
// Continue to next source
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 2. Check .claude-plugin/plugin.json
|
|
422
|
+
const pluginJsonPath = join(dirPath, ".claude-plugin", "plugin.json");
|
|
423
|
+
if (existsSync(pluginJsonPath)) {
|
|
424
|
+
try {
|
|
425
|
+
const content = await readFile(pluginJsonPath, "utf-8");
|
|
426
|
+
const parsed = JSON.parse(content);
|
|
427
|
+
if (parsed.version && typeof parsed.version === "string") {
|
|
428
|
+
return { version: parsed.version, source: "plugin.json" };
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
// Continue to next source
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 3. Check package.json
|
|
436
|
+
const pkgJsonPath = join(dirPath, "package.json");
|
|
437
|
+
if (existsSync(pkgJsonPath)) {
|
|
438
|
+
try {
|
|
439
|
+
const content = await readFile(pkgJsonPath, "utf-8");
|
|
440
|
+
const parsed = JSON.parse(content);
|
|
441
|
+
if (parsed.version && typeof parsed.version === "string") {
|
|
442
|
+
return { version: parsed.version, source: "package.json" };
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
// Continue to fallback
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 4. Fallback to commit hash (git) or content hash (file)
|
|
450
|
+
return { version: fallback, source: fallbackSource };
|
|
451
|
+
}
|
|
452
|
+
|
|
297
453
|
/**
|
|
298
454
|
* Clone a git repository
|
|
299
455
|
*/
|
|
@@ -728,15 +884,9 @@ async function fetchGitCapabilitySource(
|
|
|
728
884
|
|
|
729
885
|
// Check if already cloned to temp
|
|
730
886
|
if (existsSync(join(tempPath, ".git"))) {
|
|
731
|
-
if (!options?.silent) {
|
|
732
|
-
console.log(` Checking ${id}...`);
|
|
733
|
-
}
|
|
734
887
|
updated = await fetchRepo(tempPath, config.ref);
|
|
735
888
|
commit = await getRepoCommit(tempPath);
|
|
736
889
|
} else {
|
|
737
|
-
if (!options?.silent) {
|
|
738
|
-
console.log(` Cloning ${id} from ${config.source}...`);
|
|
739
|
-
}
|
|
740
890
|
await mkdir(join(tempPath, ".."), { recursive: true });
|
|
741
891
|
await cloneRepo(gitUrl, tempPath, config.ref);
|
|
742
892
|
commit = await getRepoCommit(tempPath);
|
|
@@ -756,6 +906,9 @@ async function fetchGitCapabilitySource(
|
|
|
756
906
|
await mkdir(join(targetPath, ".."), { recursive: true });
|
|
757
907
|
await cp(sourcePath, targetPath, { recursive: true });
|
|
758
908
|
|
|
909
|
+
// Clean up temp directory after successful copy
|
|
910
|
+
await rm(tempPath, { recursive: true });
|
|
911
|
+
|
|
759
912
|
repoPath = targetPath;
|
|
760
913
|
} else {
|
|
761
914
|
// Clone directly to target (no subdirectory)
|
|
@@ -802,24 +955,15 @@ async function fetchGitCapabilitySource(
|
|
|
802
955
|
}
|
|
803
956
|
}
|
|
804
957
|
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
if (existsSync(pkgJsonPath)) {
|
|
809
|
-
try {
|
|
810
|
-
const pkgJson = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
811
|
-
if (pkgJson.version) {
|
|
812
|
-
version = pkgJson.version;
|
|
813
|
-
}
|
|
814
|
-
} catch {
|
|
815
|
-
// Ignore parse errors
|
|
816
|
-
}
|
|
817
|
-
}
|
|
958
|
+
// Detect version using unified version detection
|
|
959
|
+
// Fallback to short commit hash for git sources
|
|
960
|
+
const versionResult = await detectDisplayVersion(repoPath, shortCommit(commit), "commit");
|
|
818
961
|
|
|
819
962
|
return {
|
|
820
963
|
id,
|
|
821
964
|
path: targetPath,
|
|
822
|
-
version,
|
|
965
|
+
version: versionResult.version,
|
|
966
|
+
versionSource: versionResult.source,
|
|
823
967
|
commit,
|
|
824
968
|
updated,
|
|
825
969
|
wrapped: needsWrap,
|
|
@@ -828,6 +972,7 @@ async function fetchGitCapabilitySource(
|
|
|
828
972
|
|
|
829
973
|
/**
|
|
830
974
|
* Fetch a file-sourced capability (copy from local path)
|
|
975
|
+
* Supports wrapping directories without capability.toml if they have skills/agents/etc.
|
|
831
976
|
*/
|
|
832
977
|
async function fetchFileCapabilitySource(
|
|
833
978
|
id: string,
|
|
@@ -848,9 +993,20 @@ async function fetchFileCapabilitySource(
|
|
|
848
993
|
throw new Error(`File source must be a directory: ${sourcePath}`);
|
|
849
994
|
}
|
|
850
995
|
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
996
|
+
// Compute content hash for the source (before copy, for reproducibility)
|
|
997
|
+
const contentHash = await computeContentHash(sourcePath);
|
|
998
|
+
|
|
999
|
+
// Check if we need to wrap (no capability.toml but has content)
|
|
1000
|
+
const hasCapToml = existsSync(join(sourcePath, "capability.toml"));
|
|
1001
|
+
let needsWrap = false;
|
|
1002
|
+
|
|
1003
|
+
if (!hasCapToml) {
|
|
1004
|
+
needsWrap = await shouldWrapDirectory(sourcePath);
|
|
1005
|
+
if (!needsWrap) {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`No capability.toml found in: ${sourcePath} (and no wrappable content detected)`,
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
854
1010
|
}
|
|
855
1011
|
|
|
856
1012
|
if (!options?.silent) {
|
|
@@ -868,31 +1024,124 @@ async function fetchFileCapabilitySource(
|
|
|
868
1024
|
// Copy directory contents
|
|
869
1025
|
await cp(sourcePath, targetPath, { recursive: true });
|
|
870
1026
|
|
|
871
|
-
//
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1027
|
+
// If needs wrapping, generate capability.toml in target (not source)
|
|
1028
|
+
if (needsWrap) {
|
|
1029
|
+
// Normalize folder names (singular -> plural)
|
|
1030
|
+
await normalizeFolderNames(targetPath);
|
|
1031
|
+
|
|
1032
|
+
// Discover content and generate capability.toml
|
|
1033
|
+
const content = await discoverContent(targetPath);
|
|
1034
|
+
await generateFileSourceCapabilityToml(
|
|
1035
|
+
id,
|
|
1036
|
+
config.source,
|
|
1037
|
+
shortContentHash(contentHash),
|
|
1038
|
+
content,
|
|
1039
|
+
targetPath,
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
if (!options?.silent) {
|
|
1043
|
+
const parts: string[] = [];
|
|
1044
|
+
if (content.skills.length > 0) parts.push(`${content.skills.length} skills`);
|
|
1045
|
+
if (content.agents.length > 0) parts.push(`${content.agents.length} agents`);
|
|
1046
|
+
if (content.commands.length > 0) parts.push(`${content.commands.length} commands`);
|
|
1047
|
+
if (parts.length > 0) {
|
|
1048
|
+
console.log(` Wrapped: ${parts.join(", ")}`);
|
|
881
1049
|
}
|
|
882
|
-
} catch {
|
|
883
|
-
// Ignore parse errors
|
|
884
1050
|
}
|
|
885
1051
|
}
|
|
886
1052
|
|
|
1053
|
+
// Detect version using unified version detection
|
|
1054
|
+
// Fallback to short content hash for file sources
|
|
1055
|
+
const versionResult = await detectDisplayVersion(
|
|
1056
|
+
targetPath,
|
|
1057
|
+
shortContentHash(contentHash),
|
|
1058
|
+
"content_hash",
|
|
1059
|
+
);
|
|
1060
|
+
|
|
887
1061
|
return {
|
|
888
1062
|
id,
|
|
889
1063
|
path: targetPath,
|
|
890
|
-
version,
|
|
1064
|
+
version: versionResult.version,
|
|
1065
|
+
versionSource: versionResult.source,
|
|
1066
|
+
contentHash,
|
|
891
1067
|
updated: true,
|
|
892
|
-
wrapped:
|
|
1068
|
+
wrapped: needsWrap,
|
|
893
1069
|
};
|
|
894
1070
|
}
|
|
895
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* Generate a capability.toml for a wrapped file source
|
|
1074
|
+
*/
|
|
1075
|
+
async function generateFileSourceCapabilityToml(
|
|
1076
|
+
id: string,
|
|
1077
|
+
source: string,
|
|
1078
|
+
hashVersion: string,
|
|
1079
|
+
content: DiscoveredContent,
|
|
1080
|
+
targetPath: string,
|
|
1081
|
+
): Promise<void> {
|
|
1082
|
+
// Try to get metadata from plugin.json
|
|
1083
|
+
const pluginMeta = await parsePluginJson(targetPath);
|
|
1084
|
+
|
|
1085
|
+
// Try to get description from README
|
|
1086
|
+
const readmeDesc = await readReadmeDescription(targetPath);
|
|
1087
|
+
|
|
1088
|
+
// Build description based on available sources
|
|
1089
|
+
let description: string;
|
|
1090
|
+
if (pluginMeta?.description) {
|
|
1091
|
+
description = pluginMeta.description;
|
|
1092
|
+
} else if (readmeDesc) {
|
|
1093
|
+
description = readmeDesc;
|
|
1094
|
+
} else {
|
|
1095
|
+
// Fallback: build from discovered content
|
|
1096
|
+
const parts: string[] = [];
|
|
1097
|
+
if (content.skills.length > 0) {
|
|
1098
|
+
parts.push(`${content.skills.length} skill${content.skills.length > 1 ? "s" : ""}`);
|
|
1099
|
+
}
|
|
1100
|
+
if (content.agents.length > 0) {
|
|
1101
|
+
parts.push(`${content.agents.length} agent${content.agents.length > 1 ? "s" : ""}`);
|
|
1102
|
+
}
|
|
1103
|
+
if (content.commands.length > 0) {
|
|
1104
|
+
parts.push(`${content.commands.length} command${content.commands.length > 1 ? "s" : ""}`);
|
|
1105
|
+
}
|
|
1106
|
+
description = parts.length > 0 ? `${parts.join(", ")}` : `Wrapped from ${source}`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Use plugin metadata for name and version if available
|
|
1110
|
+
const name = pluginMeta?.name || `${id} (wrapped)`;
|
|
1111
|
+
const version = pluginMeta?.version || hashVersion;
|
|
1112
|
+
|
|
1113
|
+
// Build TOML content
|
|
1114
|
+
let tomlContent = `# Auto-generated by OmniDev - DO NOT EDIT
|
|
1115
|
+
# This capability was wrapped from a local directory
|
|
1116
|
+
|
|
1117
|
+
[capability]
|
|
1118
|
+
id = "${id}"
|
|
1119
|
+
name = "${name}"
|
|
1120
|
+
version = "${version}"
|
|
1121
|
+
description = "${description}"
|
|
1122
|
+
`;
|
|
1123
|
+
|
|
1124
|
+
// Add author if available from plugin.json
|
|
1125
|
+
if (pluginMeta?.author?.name || pluginMeta?.author?.email) {
|
|
1126
|
+
tomlContent += "\n[capability.author]\n";
|
|
1127
|
+
if (pluginMeta.author.name) {
|
|
1128
|
+
tomlContent += `name = "${pluginMeta.author.name}"\n`;
|
|
1129
|
+
}
|
|
1130
|
+
if (pluginMeta.author.email) {
|
|
1131
|
+
tomlContent += `email = "${pluginMeta.author.email}"\n`;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Add metadata section
|
|
1136
|
+
tomlContent += `
|
|
1137
|
+
[capability.metadata]
|
|
1138
|
+
wrapped = true
|
|
1139
|
+
source = "${source}"
|
|
1140
|
+
`;
|
|
1141
|
+
|
|
1142
|
+
await writeFile(join(targetPath, "capability.toml"), tomlContent, "utf-8");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
896
1145
|
/**
|
|
897
1146
|
* Fetch a single capability source (git or file)
|
|
898
1147
|
*/
|
|
@@ -1074,15 +1323,17 @@ export async function fetchAllCapabilitySources(
|
|
|
1074
1323
|
// Generate MCP capabilities FIRST
|
|
1075
1324
|
await generateMcpCapabilities(config);
|
|
1076
1325
|
|
|
1326
|
+
// Clean up any stale temp directories from previous syncs
|
|
1327
|
+
const tempDir = join(OMNI_LOCAL, "_temp");
|
|
1328
|
+
if (existsSync(tempDir)) {
|
|
1329
|
+
await rm(tempDir, { recursive: true });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1077
1332
|
const sources = config.capabilities?.sources;
|
|
1078
1333
|
if (!sources || Object.keys(sources).length === 0) {
|
|
1079
1334
|
return [];
|
|
1080
1335
|
}
|
|
1081
1336
|
|
|
1082
|
-
if (!options?.silent) {
|
|
1083
|
-
console.log("Fetching capability sources...");
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
1337
|
const results: FetchResult[] = [];
|
|
1087
1338
|
const lockFile = await loadLockFile();
|
|
1088
1339
|
let lockUpdated = false;
|
|
@@ -1096,6 +1347,7 @@ export async function fetchAllCapabilitySources(
|
|
|
1096
1347
|
const lockEntry: CapabilityLockEntry = {
|
|
1097
1348
|
source: typeof source === "string" ? source : source.source,
|
|
1098
1349
|
version: result.version,
|
|
1350
|
+
version_source: result.versionSource,
|
|
1099
1351
|
updated_at: new Date().toISOString(),
|
|
1100
1352
|
};
|
|
1101
1353
|
|
|
@@ -1103,6 +1355,10 @@ export async function fetchAllCapabilitySources(
|
|
|
1103
1355
|
if (result.commit) {
|
|
1104
1356
|
lockEntry.commit = result.commit;
|
|
1105
1357
|
}
|
|
1358
|
+
// File source: use content hash
|
|
1359
|
+
if (result.contentHash) {
|
|
1360
|
+
lockEntry.content_hash = result.contentHash;
|
|
1361
|
+
}
|
|
1106
1362
|
// Only access ref if it's a git source
|
|
1107
1363
|
if (!isFileSourceConfig(source)) {
|
|
1108
1364
|
const gitConfig = parseSourceConfig(source) as GitCapabilitySourceConfig;
|
|
@@ -1112,17 +1368,17 @@ export async function fetchAllCapabilitySources(
|
|
|
1112
1368
|
}
|
|
1113
1369
|
|
|
1114
1370
|
// Check if lock entry changed
|
|
1371
|
+
// For git sources: compare commit hash
|
|
1372
|
+
// For file sources: compare content hash
|
|
1115
1373
|
const existing = lockFile.capabilities[id];
|
|
1116
|
-
const hasChanged =
|
|
1374
|
+
const hasChanged =
|
|
1375
|
+
!existing ||
|
|
1376
|
+
(result.commit && existing.commit !== result.commit) ||
|
|
1377
|
+
(result.contentHash && existing.content_hash !== result.contentHash);
|
|
1117
1378
|
|
|
1118
1379
|
if (hasChanged) {
|
|
1119
1380
|
lockFile.capabilities[id] = lockEntry;
|
|
1120
1381
|
lockUpdated = true;
|
|
1121
|
-
|
|
1122
|
-
if (!options?.silent && result.updated) {
|
|
1123
|
-
const oldVersion = existing?.version || "new";
|
|
1124
|
-
console.log(` ${result.wrapped ? "+" : "~"} ${id}: ${oldVersion} -> ${result.version}`);
|
|
1125
|
-
}
|
|
1126
1382
|
}
|
|
1127
1383
|
} catch (error) {
|
|
1128
1384
|
console.error(` Failed to fetch ${id}: ${error}`);
|
|
@@ -1134,15 +1390,6 @@ export async function fetchAllCapabilitySources(
|
|
|
1134
1390
|
await saveLockFile(lockFile);
|
|
1135
1391
|
}
|
|
1136
1392
|
|
|
1137
|
-
if (!options?.silent && results.length > 0) {
|
|
1138
|
-
const updated = results.filter((r) => r.updated).length;
|
|
1139
|
-
if (updated > 0) {
|
|
1140
|
-
console.log(` Updated ${updated} capability source(s)`);
|
|
1141
|
-
} else {
|
|
1142
|
-
console.log(` All ${results.length} source(s) up to date`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
1393
|
return results;
|
|
1147
1394
|
}
|
|
1148
1395
|
|
package/src/index.ts
CHANGED
package/src/mcp-json/manager.ts
CHANGED
|
@@ -132,7 +132,6 @@ function buildMcpServerConfig(mcp: McpConfig): McpServerConfig {
|
|
|
132
132
|
export async function syncMcpJson(
|
|
133
133
|
capabilities: LoadedCapability[],
|
|
134
134
|
previousManifest: ResourceManifest,
|
|
135
|
-
options: { silent?: boolean } = {},
|
|
136
135
|
): Promise<void> {
|
|
137
136
|
const mcpJson = await readMcpJson();
|
|
138
137
|
|
|
@@ -150,17 +149,11 @@ export async function syncMcpJson(
|
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
// Add MCPs from all enabled capabilities
|
|
153
|
-
let addedCount = 0;
|
|
154
152
|
for (const cap of capabilities) {
|
|
155
153
|
if (cap.config.mcp) {
|
|
156
154
|
mcpJson.mcpServers[cap.id] = buildMcpServerConfig(cap.config.mcp);
|
|
157
|
-
addedCount++;
|
|
158
155
|
}
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
await writeMcpJson(mcpJson);
|
|
162
|
-
|
|
163
|
-
if (!options.silent) {
|
|
164
|
-
console.log(` - .mcp.json (${addedCount} MCP server(s))`);
|
|
165
|
-
}
|
|
166
159
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security scanning module for capability supply-chain safety
|
|
3
|
+
*
|
|
4
|
+
* Provides opt-in scanning for:
|
|
5
|
+
* - Suspicious Unicode characters (bidi overrides, zero-width, control chars)
|
|
6
|
+
* - Symlinks that escape capability directories
|
|
7
|
+
* - Suspicious script patterns in hooks
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from "./scanner.js";
|
|
11
|
+
export * from "./types.js";
|