chainlesschain 0.37.9 → 0.37.11
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 +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +259 -124
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager for CLI
|
|
3
|
+
*
|
|
4
|
+
* Persists chat/agent conversations to DB for resume and export.
|
|
5
|
+
* Lightweight port of desktop-app-vue/src/main/llm/session-manager.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
|
|
10
|
+
function ensureSessionsTable(db) {
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS llm_sessions (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
title TEXT DEFAULT 'Untitled',
|
|
15
|
+
provider TEXT DEFAULT '',
|
|
16
|
+
model TEXT DEFAULT '',
|
|
17
|
+
message_count INTEGER DEFAULT 0,
|
|
18
|
+
messages TEXT DEFAULT '[]',
|
|
19
|
+
summary TEXT DEFAULT '',
|
|
20
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
21
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a new session
|
|
28
|
+
*/
|
|
29
|
+
export function createSession(db, options = {}) {
|
|
30
|
+
ensureSessionsTable(db);
|
|
31
|
+
|
|
32
|
+
const id =
|
|
33
|
+
options.id ||
|
|
34
|
+
`session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
|
|
35
|
+
|
|
36
|
+
db.prepare(
|
|
37
|
+
`INSERT INTO llm_sessions (id, title, provider, model, messages) VALUES (?, ?, ?, ?, ?)`,
|
|
38
|
+
).run(
|
|
39
|
+
id,
|
|
40
|
+
options.title || "Untitled",
|
|
41
|
+
options.provider || "",
|
|
42
|
+
options.model || "",
|
|
43
|
+
JSON.stringify(options.messages || []),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return { id, title: options.title || "Untitled" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add a message to a session
|
|
51
|
+
*/
|
|
52
|
+
export function addMessage(db, sessionId, role, content) {
|
|
53
|
+
ensureSessionsTable(db);
|
|
54
|
+
|
|
55
|
+
const session = db
|
|
56
|
+
.prepare("SELECT messages, message_count FROM llm_sessions WHERE id = ?")
|
|
57
|
+
.get(sessionId);
|
|
58
|
+
|
|
59
|
+
if (!session) return null;
|
|
60
|
+
|
|
61
|
+
const messages = JSON.parse(session.messages || "[]");
|
|
62
|
+
messages.push({ role, content, timestamp: new Date().toISOString() });
|
|
63
|
+
|
|
64
|
+
db.prepare(
|
|
65
|
+
`UPDATE llm_sessions SET messages = ?, message_count = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
66
|
+
).run(JSON.stringify(messages), messages.length, sessionId);
|
|
67
|
+
|
|
68
|
+
return { messageCount: messages.length };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Save all messages at once (batch update)
|
|
73
|
+
*/
|
|
74
|
+
export function saveMessages(db, sessionId, messages) {
|
|
75
|
+
ensureSessionsTable(db);
|
|
76
|
+
|
|
77
|
+
const result = db
|
|
78
|
+
.prepare(
|
|
79
|
+
`UPDATE llm_sessions SET messages = ?, message_count = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
80
|
+
)
|
|
81
|
+
.run(JSON.stringify(messages), messages.length, sessionId);
|
|
82
|
+
|
|
83
|
+
return { messageCount: messages.length, updated: result.changes > 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a session by ID
|
|
88
|
+
*/
|
|
89
|
+
export function getSession(db, sessionId) {
|
|
90
|
+
ensureSessionsTable(db);
|
|
91
|
+
|
|
92
|
+
// Try exact match first, then prefix match
|
|
93
|
+
let session = db
|
|
94
|
+
.prepare("SELECT * FROM llm_sessions WHERE id = ?")
|
|
95
|
+
.get(sessionId);
|
|
96
|
+
|
|
97
|
+
if (!session) {
|
|
98
|
+
session = db
|
|
99
|
+
.prepare("SELECT * FROM llm_sessions WHERE id LIKE ? LIMIT 1")
|
|
100
|
+
.get(`${sessionId}%`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!session) return null;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
...session,
|
|
107
|
+
messages: JSON.parse(session.messages || "[]"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List all sessions
|
|
113
|
+
*/
|
|
114
|
+
export function listSessions(db, options = {}) {
|
|
115
|
+
ensureSessionsTable(db);
|
|
116
|
+
|
|
117
|
+
const limit = options.limit || 20;
|
|
118
|
+
|
|
119
|
+
return db
|
|
120
|
+
.prepare(
|
|
121
|
+
`SELECT id, title, provider, model, message_count, summary, created_at, updated_at
|
|
122
|
+
FROM llm_sessions
|
|
123
|
+
ORDER BY updated_at DESC
|
|
124
|
+
LIMIT ?`,
|
|
125
|
+
)
|
|
126
|
+
.all(limit);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Update session title or summary
|
|
131
|
+
*/
|
|
132
|
+
export function updateSession(db, sessionId, updates) {
|
|
133
|
+
ensureSessionsTable(db);
|
|
134
|
+
|
|
135
|
+
if (updates.title) {
|
|
136
|
+
db.prepare(
|
|
137
|
+
"UPDATE llm_sessions SET title = ?, updated_at = datetime('now') WHERE id = ?",
|
|
138
|
+
).run(updates.title, sessionId);
|
|
139
|
+
}
|
|
140
|
+
if (updates.summary) {
|
|
141
|
+
db.prepare(
|
|
142
|
+
"UPDATE llm_sessions SET summary = ?, updated_at = datetime('now') WHERE id = ?",
|
|
143
|
+
).run(updates.summary, sessionId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Delete a session
|
|
149
|
+
*/
|
|
150
|
+
export function deleteSession(db, sessionId) {
|
|
151
|
+
ensureSessionsTable(db);
|
|
152
|
+
const result = db
|
|
153
|
+
.prepare("DELETE FROM llm_sessions WHERE id = ?")
|
|
154
|
+
.run(sessionId);
|
|
155
|
+
return result.changes > 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Export session as markdown
|
|
160
|
+
*/
|
|
161
|
+
export function exportSessionMarkdown(session) {
|
|
162
|
+
const lines = [
|
|
163
|
+
`# ${session.title}`,
|
|
164
|
+
"",
|
|
165
|
+
`- **Created**: ${session.created_at}`,
|
|
166
|
+
`- **Provider**: ${session.provider || "unknown"}`,
|
|
167
|
+
`- **Model**: ${session.model || "unknown"}`,
|
|
168
|
+
`- **Messages**: ${session.message_count}`,
|
|
169
|
+
"",
|
|
170
|
+
"---",
|
|
171
|
+
"",
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const messages =
|
|
175
|
+
typeof session.messages === "string"
|
|
176
|
+
? JSON.parse(session.messages)
|
|
177
|
+
: session.messages || [];
|
|
178
|
+
|
|
179
|
+
for (const msg of messages) {
|
|
180
|
+
if (msg.role === "system") continue;
|
|
181
|
+
const label = msg.role === "user" ? "**You**" : "**AI**";
|
|
182
|
+
lines.push(`### ${label}`);
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(msg.content || "");
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-layer skill loader for CLI
|
|
3
|
+
*
|
|
4
|
+
* 4-layer priority system (highest wins on name collision):
|
|
5
|
+
* 0 (lowest) bundled — desktop-app-vue/.../skills/builtin/
|
|
6
|
+
* 1 marketplace — <userData>/marketplace/skills/
|
|
7
|
+
* 2 managed — <userData>/skills/
|
|
8
|
+
* 3 (highest) workspace — <projectRoot>/.chainlesschain/skills/
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { getElectronUserDataDir } from "./paths.js";
|
|
15
|
+
import { findProjectRoot } from "./project-detector.js";
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** Layer names in priority order (lowest → highest) */
|
|
20
|
+
export const LAYER_NAMES = ["bundled", "marketplace", "managed", "workspace"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Simple YAML frontmatter parser (no dependencies)
|
|
24
|
+
* Shared utility extracted from skill.js
|
|
25
|
+
*/
|
|
26
|
+
export function parseSkillMd(content) {
|
|
27
|
+
const lines = content.split("\n");
|
|
28
|
+
if (lines[0].trim() !== "---") return { data: {}, body: content };
|
|
29
|
+
|
|
30
|
+
let endIndex = -1;
|
|
31
|
+
for (let i = 1; i < lines.length; i++) {
|
|
32
|
+
if (lines[i].trim() === "---") {
|
|
33
|
+
endIndex = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (endIndex === -1) return { data: {}, body: content };
|
|
39
|
+
|
|
40
|
+
const yamlLines = lines.slice(1, endIndex);
|
|
41
|
+
const body = lines
|
|
42
|
+
.slice(endIndex + 1)
|
|
43
|
+
.join("\n")
|
|
44
|
+
.trim();
|
|
45
|
+
const data = {};
|
|
46
|
+
|
|
47
|
+
let currentKey = null;
|
|
48
|
+
let currentArray = null;
|
|
49
|
+
|
|
50
|
+
for (const line of yamlLines) {
|
|
51
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
52
|
+
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
|
|
55
|
+
if (trimmed.startsWith("- ")) {
|
|
56
|
+
const value = trimmed
|
|
57
|
+
.slice(2)
|
|
58
|
+
.trim()
|
|
59
|
+
.replace(/^['"]|['"]$/g, "");
|
|
60
|
+
if (currentArray) currentArray.push(value);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const colonIndex = trimmed.indexOf(":");
|
|
65
|
+
if (colonIndex > 0) {
|
|
66
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
67
|
+
let value = trimmed.slice(colonIndex + 1).trim();
|
|
68
|
+
|
|
69
|
+
// Convert kebab-case to camelCase
|
|
70
|
+
const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
71
|
+
|
|
72
|
+
if (value === "") {
|
|
73
|
+
currentKey = camelKey;
|
|
74
|
+
currentArray = null;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Handle inline arrays [a, b, c]
|
|
79
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
80
|
+
data[camelKey] = value
|
|
81
|
+
.slice(1, -1)
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((v) => v.trim().replace(/^['"]|['"]$/g, ""))
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
currentArray = null;
|
|
86
|
+
currentKey = null;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle booleans and numbers
|
|
91
|
+
if (value === "true") value = true;
|
|
92
|
+
else if (value === "false") value = false;
|
|
93
|
+
else if (value === "null") value = null;
|
|
94
|
+
else if (/^\d+(\.\d+)?$/.test(value)) value = parseFloat(value);
|
|
95
|
+
else value = value.replace(/^['"]|['"]$/g, "");
|
|
96
|
+
|
|
97
|
+
data[camelKey] = value;
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(data[camelKey])) {
|
|
100
|
+
currentArray = data[camelKey];
|
|
101
|
+
} else {
|
|
102
|
+
currentArray = null;
|
|
103
|
+
}
|
|
104
|
+
currentKey = camelKey;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { data, body };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Multi-layer CLI skill loader
|
|
113
|
+
*/
|
|
114
|
+
export class CLISkillLoader {
|
|
115
|
+
constructor() {
|
|
116
|
+
this._cache = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get paths for each layer
|
|
121
|
+
* @returns {{ layer: string, path: string, exists: boolean }[]}
|
|
122
|
+
*/
|
|
123
|
+
getLayerPaths() {
|
|
124
|
+
const layers = [];
|
|
125
|
+
|
|
126
|
+
// Layer 0: bundled — desktop-app-vue builtin skills
|
|
127
|
+
const bundledCandidates = [
|
|
128
|
+
path.resolve(
|
|
129
|
+
__dirname,
|
|
130
|
+
"../../../../desktop-app-vue/src/main/ai-engine/cowork/skills/builtin",
|
|
131
|
+
),
|
|
132
|
+
path.resolve(
|
|
133
|
+
process.cwd(),
|
|
134
|
+
"desktop-app-vue/src/main/ai-engine/cowork/skills/builtin",
|
|
135
|
+
),
|
|
136
|
+
];
|
|
137
|
+
let bundledPath = null;
|
|
138
|
+
for (const c of bundledCandidates) {
|
|
139
|
+
if (fs.existsSync(c)) {
|
|
140
|
+
bundledPath = c;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
layers.push({
|
|
145
|
+
layer: "bundled",
|
|
146
|
+
path: bundledPath || bundledCandidates[0],
|
|
147
|
+
exists: bundledPath !== null,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Layer 1: marketplace — <userData>/marketplace/skills/
|
|
151
|
+
const userData = getElectronUserDataDir();
|
|
152
|
+
const marketplacePath = path.join(userData, "marketplace", "skills");
|
|
153
|
+
layers.push({
|
|
154
|
+
layer: "marketplace",
|
|
155
|
+
path: marketplacePath,
|
|
156
|
+
exists: fs.existsSync(marketplacePath),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Layer 2: managed — <userData>/skills/
|
|
160
|
+
const managedPath = path.join(userData, "skills");
|
|
161
|
+
layers.push({
|
|
162
|
+
layer: "managed",
|
|
163
|
+
path: managedPath,
|
|
164
|
+
exists: fs.existsSync(managedPath),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Layer 3: workspace — <projectRoot>/.chainlesschain/skills/
|
|
168
|
+
const projectRoot = findProjectRoot();
|
|
169
|
+
if (projectRoot) {
|
|
170
|
+
const workspacePath = path.join(projectRoot, ".chainlesschain", "skills");
|
|
171
|
+
layers.push({
|
|
172
|
+
layer: "workspace",
|
|
173
|
+
path: workspacePath,
|
|
174
|
+
exists: fs.existsSync(workspacePath),
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
layers.push({
|
|
178
|
+
layer: "workspace",
|
|
179
|
+
path: null,
|
|
180
|
+
exists: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return layers;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Load skills from a single directory
|
|
189
|
+
* @param {string} dir - Directory to scan
|
|
190
|
+
* @param {string} layer - Layer name for source tracking
|
|
191
|
+
* @returns {object[]} Array of skill metadata
|
|
192
|
+
*/
|
|
193
|
+
_loadFromDir(dir, layer) {
|
|
194
|
+
const skills = [];
|
|
195
|
+
if (!dir || !fs.existsSync(dir)) return skills;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const dirs = fs.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
for (const entry of dirs) {
|
|
200
|
+
if (!entry.isDirectory()) continue;
|
|
201
|
+
|
|
202
|
+
const skillMd = path.join(dir, entry.name, "SKILL.md");
|
|
203
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const content = fs.readFileSync(skillMd, "utf-8");
|
|
207
|
+
const { data, body } = parseSkillMd(content);
|
|
208
|
+
|
|
209
|
+
skills.push({
|
|
210
|
+
id: data.name || entry.name,
|
|
211
|
+
displayName: data.displayName || entry.name,
|
|
212
|
+
description: data.description || "",
|
|
213
|
+
version: data.version || "1.0.0",
|
|
214
|
+
category: data.category || "uncategorized",
|
|
215
|
+
tags: data.tags || [],
|
|
216
|
+
userInvocable: data.userInvocable !== false,
|
|
217
|
+
handler: data.handler || null,
|
|
218
|
+
capabilities: data.capabilities || [],
|
|
219
|
+
os: data.os || [],
|
|
220
|
+
dirName: entry.name,
|
|
221
|
+
hasHandler: fs.existsSync(path.join(dir, entry.name, "handler.js")),
|
|
222
|
+
body,
|
|
223
|
+
source: layer,
|
|
224
|
+
skillDir: path.join(dir, entry.name),
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
// Skip malformed skill files
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Directory unreadable
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return skills;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Load all skills from all layers, applying priority override
|
|
239
|
+
* Higher-priority layers override same-name skills from lower layers.
|
|
240
|
+
* @returns {object[]} Resolved skill list
|
|
241
|
+
*/
|
|
242
|
+
loadAll() {
|
|
243
|
+
const layers = this.getLayerPaths();
|
|
244
|
+
const skillMap = new Map();
|
|
245
|
+
|
|
246
|
+
// Process in priority order (lowest first, so higher layers overwrite)
|
|
247
|
+
for (const { layer, path: layerPath, exists } of layers) {
|
|
248
|
+
if (!exists) continue;
|
|
249
|
+
const skills = this._loadFromDir(layerPath, layer);
|
|
250
|
+
for (const skill of skills) {
|
|
251
|
+
skillMap.set(skill.id, skill);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this._cache = Array.from(skillMap.values());
|
|
256
|
+
return this._cache;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get resolved skills (uses cache if available)
|
|
261
|
+
* @returns {object[]}
|
|
262
|
+
*/
|
|
263
|
+
getResolvedSkills() {
|
|
264
|
+
if (this._cache) return this._cache;
|
|
265
|
+
return this.loadAll();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Clear the cache
|
|
270
|
+
*/
|
|
271
|
+
clearCache() {
|
|
272
|
+
this._cache = null;
|
|
273
|
+
}
|
|
274
|
+
}
|