@amalgm/agents 0.1.0 → 0.1.1
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/PURPOSE.md +2 -0
- package/README.md +12 -0
- package/dist/http/server.js +12 -8
- package/dist/http/skill-routes.d.ts +3 -0
- package/dist/http/skill-routes.js +25 -0
- package/dist/http-types.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/mcp/server.js +1 -1
- package/dist/skills/roots.d.ts +16 -0
- package/dist/skills/roots.js +105 -0
- package/dist/skills/scanner.d.ts +32 -0
- package/dist/skills/scanner.js +117 -0
- package/package.json +1 -1
package/PURPOSE.md
CHANGED
|
@@ -11,6 +11,8 @@ remain usable without Chat or Amalgm Engine.
|
|
|
11
11
|
Engine composes Agents with harnesses, credentials, Toolbox, files, Realtime,
|
|
12
12
|
and UI. This package stores references to those products and delegates
|
|
13
13
|
execution to an injected driver; it does not duplicate their state.
|
|
14
|
+
It also discovers installed agent skills across the canonical project and
|
|
15
|
+
harness roots so every transport presents the same available skill catalog.
|
|
14
16
|
|
|
15
17
|
## Primitives
|
|
16
18
|
|
package/README.md
CHANGED
|
@@ -71,6 +71,18 @@ session stays pinned to that revision even after the agent is edited.
|
|
|
71
71
|
| MCP | `@amalgm/agents/mcp`, `amalgm-agents-mcp` | Agent CRUD and agent-to-agent calls |
|
|
72
72
|
| Skill | `skills/amalgm-agents` | Minimal workflow over the Agents tools |
|
|
73
73
|
|
|
74
|
+
Installed skills are available from the REST surface without introducing a
|
|
75
|
+
second registry:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl http://127.0.0.1:PORT/skills
|
|
79
|
+
curl 'http://127.0.0.1:PORT/skills/list?include_content=true'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The scanner reads the same canonical project, shared, Codex, Claude Code, and
|
|
83
|
+
OpenCode roots as Engine. Hosts inject `skillRoots` when they need a selected
|
|
84
|
+
project or an isolated home.
|
|
85
|
+
|
|
74
86
|
## CLI
|
|
75
87
|
|
|
76
88
|
```bash
|
package/dist/http/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { asAgentError } from '../errors.js';
|
|
|
4
4
|
import { routeAgents } from './agent-routes.js';
|
|
5
5
|
import { guardRequest, readJson, sendJson } from './request.js';
|
|
6
6
|
import { routeSessions } from './session-routes.js';
|
|
7
|
+
import { routeSkills } from './skill-routes.js';
|
|
7
8
|
import { streamEvents } from './stream.js';
|
|
8
9
|
export function createRestServer(options = {}) {
|
|
9
10
|
const agents = options.agents || new Agents(options);
|
|
@@ -37,22 +38,25 @@ export function createRestServer(options = {}) {
|
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
const path = url.pathname.split('/').filter(Boolean);
|
|
40
|
-
if (path.shift() !== 'v1') {
|
|
41
|
-
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (path[0] === 'sessions' && path[2] === 'events' && path[3] === 'stream' && request.method === 'GET') {
|
|
45
|
-
streamEvents(agents, path[1] || '', request, response, Number(url.searchParams.get('after') || 0));
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
41
|
const context = {
|
|
49
42
|
agents,
|
|
50
43
|
method: request.method || 'GET',
|
|
51
44
|
path,
|
|
52
45
|
url,
|
|
46
|
+
skillRoots: options.skillRoots || {},
|
|
53
47
|
body: () => readJson(request, bodyLimit),
|
|
54
48
|
json: (status, value) => sendJson(response, status, value),
|
|
55
49
|
};
|
|
50
|
+
if (await routeSkills(context))
|
|
51
|
+
return;
|
|
52
|
+
if (path.shift() !== 'v1') {
|
|
53
|
+
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (path[0] === 'sessions' && path[2] === 'events' && path[3] === 'stream' && request.method === 'GET') {
|
|
57
|
+
streamEvents(agents, path[1] || '', request, response, Number(url.searchParams.get('after') || 0));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
56
60
|
if (await routeAgents(context) || await routeSessions(context))
|
|
57
61
|
return;
|
|
58
62
|
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** HTTP laws from Engine `server/routes/agents.js` and `skills/rest.js`. */
|
|
2
|
+
import { scanInstalledSkills } from '../skills/scanner.js';
|
|
3
|
+
function truthy(value) {
|
|
4
|
+
return value === '1' || value?.toLowerCase() === 'true';
|
|
5
|
+
}
|
|
6
|
+
export async function routeSkills(context) {
|
|
7
|
+
const [resource, action] = context.path;
|
|
8
|
+
if (resource !== 'skills')
|
|
9
|
+
return false;
|
|
10
|
+
const includeContent = truthy(context.url.searchParams.get('include_content'));
|
|
11
|
+
if ((!action || action === 'list') && context.method === 'GET') {
|
|
12
|
+
context.json(200, {
|
|
13
|
+
skills: scanInstalledSkills({ ...context.skillRoots, includeContent }).skills,
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (action === 'refresh' && context.method === 'POST') {
|
|
18
|
+
context.json(200, {
|
|
19
|
+
ok: true,
|
|
20
|
+
skills: scanInstalledSkills(context.skillRoots).skills,
|
|
21
|
+
});
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
package/dist/http-types.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import type { Server } from 'node:http';
|
|
|
2
2
|
import type { AddressInfo } from 'node:net';
|
|
3
3
|
import type { Agents } from './agents.js';
|
|
4
4
|
import type { AgentsOptions } from './types.js';
|
|
5
|
+
import type { SkillRootOptions } from './skills/roots.js';
|
|
5
6
|
export interface RestServerOptions extends AgentsOptions {
|
|
6
7
|
agents?: Agents;
|
|
7
8
|
token?: string;
|
|
8
9
|
bodyLimitBytes?: number;
|
|
10
|
+
skillRoots?: SkillRootOptions;
|
|
9
11
|
}
|
|
10
12
|
export interface RestServer {
|
|
11
13
|
agents: Agents;
|
|
@@ -18,6 +20,7 @@ export interface RouteContext {
|
|
|
18
20
|
method: string;
|
|
19
21
|
path: string[];
|
|
20
22
|
url: URL;
|
|
23
|
+
skillRoots: SkillRootOptions;
|
|
21
24
|
body(): Promise<Record<string, unknown>>;
|
|
22
25
|
json(status: number, value: unknown): void;
|
|
23
26
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,7 @@ export { defineDriver, driverSpecifiers, loadDriverModules } from './drivers.js'
|
|
|
4
4
|
export { definitionHash, normalizeDefinition, patchDefinition } from './definition.js';
|
|
5
5
|
export { messageText, normalizeMessage } from './messages.js';
|
|
6
6
|
export type * from './types.js';
|
|
7
|
+
export { buildCanonicalSkillRoots, classifySkillRoot } from './skills/roots.js';
|
|
8
|
+
export type { SkillRootClassification, SkillRootOptions } from './skills/roots.js';
|
|
9
|
+
export { parseSkillFrontmatter, scanInstalledSkills } from './skills/scanner.js';
|
|
10
|
+
export type { InstalledSkill, SkillScanOptions } from './skills/scanner.js';
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,5 @@ export { AgentError, asAgentError } from './errors.js';
|
|
|
3
3
|
export { defineDriver, driverSpecifiers, loadDriverModules } from './drivers.js';
|
|
4
4
|
export { definitionHash, normalizeDefinition, patchDefinition } from './definition.js';
|
|
5
5
|
export { messageText, normalizeMessage } from './messages.js';
|
|
6
|
+
export { buildCanonicalSkillRoots, classifySkillRoot } from './skills/roots.js';
|
|
7
|
+
export { parseSkillFrontmatter, scanInstalledSkills } from './skills/scanner.js';
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createMcpServer(options = {}) {
|
|
|
16
16
|
return {
|
|
17
17
|
protocolVersion: message.params?.protocolVersion || '2024-11-05',
|
|
18
18
|
capabilities: { tools: { listChanged: false } },
|
|
19
|
-
serverInfo: { name: 'amalgm-agents', version: '0.1.
|
|
19
|
+
serverInfo: { name: 'amalgm-agents', version: '0.1.1' },
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
if (message.method === 'ping')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Canonical installed-skill roots from Engine `skills/roots.js`. */
|
|
2
|
+
export interface SkillRootOptions {
|
|
3
|
+
selectedCwd?: string;
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
codexHome?: string;
|
|
6
|
+
claudeConfigDir?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SkillRootClassification {
|
|
9
|
+
provider: 'shared' | 'codex' | 'claude_code' | 'opencode';
|
|
10
|
+
scope: 'project' | 'global';
|
|
11
|
+
rootLabel: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function normalizePath(value: unknown): string;
|
|
14
|
+
export declare function buildCanonicalSkillRoots(options?: SkillRootOptions): string[];
|
|
15
|
+
export declare function isCodexWorktreeRoot(rootPath: string, options?: SkillRootOptions): boolean;
|
|
16
|
+
export declare function classifySkillRoot(rootPath: string, options?: SkillRootOptions): SkillRootClassification;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Canonical installed-skill roots from Engine `skills/roots.js`. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export function normalizePath(value) {
|
|
6
|
+
return typeof value === 'string' && value.trim() ? path.resolve(value.trim()) : '';
|
|
7
|
+
}
|
|
8
|
+
function existingDirectory(value) {
|
|
9
|
+
const directory = normalizePath(value);
|
|
10
|
+
if (!directory)
|
|
11
|
+
return '';
|
|
12
|
+
try {
|
|
13
|
+
return fs.statSync(directory).isDirectory() ? directory : '';
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function uniquePaths(values) {
|
|
20
|
+
return [...new Set(values.map(normalizePath).filter(Boolean))];
|
|
21
|
+
}
|
|
22
|
+
function nativeHome(options) {
|
|
23
|
+
return normalizePath(options.homeDir || process.env.AMALGM_NATIVE_HOME || os.homedir());
|
|
24
|
+
}
|
|
25
|
+
function projectRoots(selectedCwd) {
|
|
26
|
+
const roots = [];
|
|
27
|
+
let current = existingDirectory(selectedCwd);
|
|
28
|
+
while (current) {
|
|
29
|
+
const root = existingDirectory(path.join(current, '.agents', 'skills'));
|
|
30
|
+
if (root)
|
|
31
|
+
roots.push(root);
|
|
32
|
+
const parent = path.dirname(current);
|
|
33
|
+
if (!parent || parent === current)
|
|
34
|
+
break;
|
|
35
|
+
current = parent;
|
|
36
|
+
}
|
|
37
|
+
return roots;
|
|
38
|
+
}
|
|
39
|
+
function worktreeRoots(home) {
|
|
40
|
+
const worktrees = existingDirectory(path.join(home, '.codex', 'worktrees'));
|
|
41
|
+
if (!worktrees)
|
|
42
|
+
return [];
|
|
43
|
+
try {
|
|
44
|
+
return fs.readdirSync(worktrees, { withFileTypes: true })
|
|
45
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
46
|
+
.map((entry) => existingDirectory(path.join(worktrees, entry.name, '.agents', 'skills')))
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function buildCanonicalSkillRoots(options = {}) {
|
|
54
|
+
const selectedCwd = normalizePath(options.selectedCwd);
|
|
55
|
+
const home = nativeHome(options);
|
|
56
|
+
const codexHome = normalizePath(options.codexHome || process.env.CODEX_HOME);
|
|
57
|
+
const claudeConfig = normalizePath(options.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
|
|
58
|
+
return uniquePaths([
|
|
59
|
+
...projectRoots(selectedCwd),
|
|
60
|
+
home && path.join(home, '.agents', 'skills'),
|
|
61
|
+
codexHome && path.join(codexHome, 'skills'),
|
|
62
|
+
home && path.join(home, '.codex', 'skills'),
|
|
63
|
+
...worktreeRoots(home),
|
|
64
|
+
claudeConfig && path.join(claudeConfig, 'skills'),
|
|
65
|
+
home && path.join(home, '.claude', 'skills'),
|
|
66
|
+
home && path.join(home, '.config', 'claude', 'skills'),
|
|
67
|
+
home && path.join(home, '.config', 'opencode', 'skills'),
|
|
68
|
+
home && path.join(home, '.opencode', 'skills'),
|
|
69
|
+
]).filter(existingDirectory);
|
|
70
|
+
}
|
|
71
|
+
export function isCodexWorktreeRoot(rootPath, options = {}) {
|
|
72
|
+
const root = normalizePath(rootPath);
|
|
73
|
+
const home = nativeHome(options);
|
|
74
|
+
const worktrees = `${normalizePath(path.join(home, '.codex', 'worktrees'))}${path.sep}`;
|
|
75
|
+
return Boolean(root && home && root.startsWith(worktrees)
|
|
76
|
+
&& root.endsWith(path.normalize(path.join('.agents', 'skills'))));
|
|
77
|
+
}
|
|
78
|
+
export function classifySkillRoot(rootPath, options = {}) {
|
|
79
|
+
const root = normalizePath(rootPath);
|
|
80
|
+
const selectedCwd = normalizePath(options.selectedCwd);
|
|
81
|
+
const home = nativeHome(options);
|
|
82
|
+
const codexHome = normalizePath(options.codexHome || process.env.CODEX_HOME);
|
|
83
|
+
const claudeConfig = normalizePath(options.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
|
|
84
|
+
if ((selectedCwd && root.startsWith(path.join(selectedCwd, '.agents', 'skills')))
|
|
85
|
+
|| root.endsWith(path.normalize(path.join('.agents', 'skills')))) {
|
|
86
|
+
return { provider: 'shared', scope: 'project', rootLabel: 'Project skills' };
|
|
87
|
+
}
|
|
88
|
+
if (root === normalizePath(path.join(home, '.agents', 'skills'))) {
|
|
89
|
+
return { provider: 'shared', scope: 'global', rootLabel: 'Shared global skills' };
|
|
90
|
+
}
|
|
91
|
+
if ((codexHome && root === normalizePath(path.join(codexHome, 'skills')))
|
|
92
|
+
|| root === normalizePath(path.join(home, '.codex', 'skills'))) {
|
|
93
|
+
return { provider: 'codex', scope: 'global', rootLabel: 'Codex skills' };
|
|
94
|
+
}
|
|
95
|
+
if ((claudeConfig && root === normalizePath(path.join(claudeConfig, 'skills')))
|
|
96
|
+
|| root === normalizePath(path.join(home, '.claude', 'skills'))
|
|
97
|
+
|| root === normalizePath(path.join(home, '.config', 'claude', 'skills'))) {
|
|
98
|
+
return { provider: 'claude_code', scope: 'global', rootLabel: 'Claude Code skills' };
|
|
99
|
+
}
|
|
100
|
+
if (root === normalizePath(path.join(home, '.config', 'opencode', 'skills'))
|
|
101
|
+
|| root === normalizePath(path.join(home, '.opencode', 'skills'))) {
|
|
102
|
+
return { provider: 'opencode', scope: 'global', rootLabel: 'OpenCode skills' };
|
|
103
|
+
}
|
|
104
|
+
return { provider: 'shared', scope: 'global', rootLabel: 'Installed skills' };
|
|
105
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Installed-skill scanner from Engine `skills/scanner.js`. */
|
|
2
|
+
import type { SkillRootOptions } from './roots.js';
|
|
3
|
+
export interface InstalledSkill {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
contentHash?: string;
|
|
9
|
+
source: {
|
|
10
|
+
kind: 'installed';
|
|
11
|
+
provider: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
rootLabel: string;
|
|
14
|
+
path: string;
|
|
15
|
+
directoryPath: string;
|
|
16
|
+
resolvedPath: string;
|
|
17
|
+
resolvedDirectoryPath: string;
|
|
18
|
+
rootPath: string;
|
|
19
|
+
viaSymlink: boolean;
|
|
20
|
+
};
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
discoveredAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SkillScanOptions extends SkillRootOptions {
|
|
25
|
+
includeContent?: boolean;
|
|
26
|
+
now?: () => string;
|
|
27
|
+
}
|
|
28
|
+
export declare function parseSkillFrontmatter(markdown: unknown): Record<string, string>;
|
|
29
|
+
export declare function scanInstalledSkills(options?: SkillScanOptions): {
|
|
30
|
+
roots: string[];
|
|
31
|
+
skills: InstalledSkill[];
|
|
32
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Installed-skill scanner from Engine `skills/scanner.js`. */
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { buildCanonicalSkillRoots, classifySkillRoot, isCodexWorktreeRoot, normalizePath, } from './roots.js';
|
|
6
|
+
const FRONTMATTER = /^---\s*\n([\s\S]*?)\n---/;
|
|
7
|
+
function clean(value) {
|
|
8
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
9
|
+
}
|
|
10
|
+
function trimSlash(value) {
|
|
11
|
+
return value === path.sep ? value : value.replace(/[\\/]+$/, '');
|
|
12
|
+
}
|
|
13
|
+
export function parseSkillFrontmatter(markdown) {
|
|
14
|
+
const match = String(markdown || '').match(FRONTMATTER);
|
|
15
|
+
if (!match?.[1])
|
|
16
|
+
return {};
|
|
17
|
+
const fields = {};
|
|
18
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
19
|
+
const parts = line.match(/^\s*([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
|
|
20
|
+
if (!parts?.[1])
|
|
21
|
+
continue;
|
|
22
|
+
let value = parts[2]?.trim() || '';
|
|
23
|
+
if ((value.startsWith('"') && value.endsWith('"'))
|
|
24
|
+
|| (value.startsWith("'") && value.endsWith("'")))
|
|
25
|
+
value = value.slice(1, -1);
|
|
26
|
+
fields[parts[1]] = value;
|
|
27
|
+
}
|
|
28
|
+
return fields;
|
|
29
|
+
}
|
|
30
|
+
function scanDirectory(root, name, options) {
|
|
31
|
+
const directory = path.join(root, name);
|
|
32
|
+
const file = path.join(directory, 'SKILL.md');
|
|
33
|
+
let stat;
|
|
34
|
+
try {
|
|
35
|
+
stat = fs.statSync(file);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!stat.isFile())
|
|
41
|
+
return null;
|
|
42
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
43
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
44
|
+
const canonicalPath = normalizePath(file);
|
|
45
|
+
const classification = classifySkillRoot(root, options);
|
|
46
|
+
const skill = {
|
|
47
|
+
id: `skill-${crypto.createHash('sha256').update(canonicalPath).digest('hex').slice(0, 16)}`,
|
|
48
|
+
name: clean(frontmatter.name) || name,
|
|
49
|
+
description: clean(frontmatter.description),
|
|
50
|
+
source: {
|
|
51
|
+
kind: 'installed',
|
|
52
|
+
...classification,
|
|
53
|
+
path: canonicalPath,
|
|
54
|
+
directoryPath: trimSlash(normalizePath(directory)),
|
|
55
|
+
resolvedPath: normalizePath(fs.realpathSync(file)),
|
|
56
|
+
resolvedDirectoryPath: trimSlash(fs.realpathSync(directory)),
|
|
57
|
+
rootPath: normalizePath(root),
|
|
58
|
+
viaSymlink: fs.lstatSync(directory).isSymbolicLink() || fs.lstatSync(file).isSymbolicLink(),
|
|
59
|
+
},
|
|
60
|
+
updatedAt: stat.mtime.toISOString(),
|
|
61
|
+
discoveredAt: options.now?.() || new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
if (options.includeContent) {
|
|
64
|
+
skill.content = content;
|
|
65
|
+
skill.contentHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
return skill;
|
|
68
|
+
}
|
|
69
|
+
function entries(root) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.readdirSync(root, { withFileTypes: true }).filter((entry) => {
|
|
72
|
+
if (entry.name.startsWith('.'))
|
|
73
|
+
return false;
|
|
74
|
+
if (entry.isDirectory())
|
|
75
|
+
return true;
|
|
76
|
+
if (!entry.isSymbolicLink())
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
return fs.statSync(path.join(root, entry.name)).isDirectory();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function scanInstalledSkills(options = {}) {
|
|
91
|
+
const roots = buildCanonicalSkillRoots(options);
|
|
92
|
+
const paths = new Set();
|
|
93
|
+
const resolved = new Set();
|
|
94
|
+
const signatures = new Set();
|
|
95
|
+
const skills = [];
|
|
96
|
+
for (const root of roots) {
|
|
97
|
+
for (const entry of entries(root)) {
|
|
98
|
+
const skill = scanDirectory(root, entry.name, { ...options, includeContent: true });
|
|
99
|
+
if (!skill || paths.has(skill.source.path) || resolved.has(skill.source.resolvedPath))
|
|
100
|
+
continue;
|
|
101
|
+
const signature = `${skill.name.toLowerCase()}::${skill.contentHash || ''}`;
|
|
102
|
+
if (isCodexWorktreeRoot(root, options) && signatures.has(signature))
|
|
103
|
+
continue;
|
|
104
|
+
paths.add(skill.source.path);
|
|
105
|
+
resolved.add(skill.source.resolvedPath);
|
|
106
|
+
signatures.add(signature);
|
|
107
|
+
if (!options.includeContent) {
|
|
108
|
+
delete skill.content;
|
|
109
|
+
delete skill.contentHash;
|
|
110
|
+
}
|
|
111
|
+
skills.push(skill);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
skills.sort((left, right) => left.name.localeCompare(right.name)
|
|
115
|
+
|| left.source.path.localeCompare(right.source.path));
|
|
116
|
+
return { roots, skills };
|
|
117
|
+
}
|