@alfe.ai/integrations 0.0.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/README.md +103 -0
- package/dist/index.d.ts +695 -0
- package/dist/index.js +1351 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { buildConfigValidationSchema, parseManifestFile } from "@alfe.ai/integration-manifest";
|
|
7
|
+
import { createLogger } from "@auriclabs/logger";
|
|
8
|
+
//#region src/registry.ts
|
|
9
|
+
const DEFAULT_API_URL = "https://api.alfe.ai";
|
|
10
|
+
const REGISTRY_PATH = "/integrations/registry";
|
|
11
|
+
var Registry = class {
|
|
12
|
+
index = null;
|
|
13
|
+
apiUrl;
|
|
14
|
+
/**
|
|
15
|
+
* @param apiUrl - Base URL for the Alfe API (e.g. "https://api.alfe.ai").
|
|
16
|
+
* Falls back to ALFE_API_URL env var, then the production URL.
|
|
17
|
+
*/
|
|
18
|
+
constructor(apiUrl) {
|
|
19
|
+
this.apiUrl = (apiUrl ?? process.env.ALFE_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load the registry index. Uses cache if already loaded.
|
|
23
|
+
*/
|
|
24
|
+
async load() {
|
|
25
|
+
if (this.index) return this.index;
|
|
26
|
+
const url = `${this.apiUrl}${REGISTRY_PATH}`;
|
|
27
|
+
const res = await fetch(url);
|
|
28
|
+
if (!res.ok) throw new Error(`Failed to fetch registry index from ${url}: ${String(res.status)} ${res.statusText}`);
|
|
29
|
+
const body = await res.json();
|
|
30
|
+
if (!body.success || !body.data?.integrations) throw new Error(`Invalid registry response from ${url}`);
|
|
31
|
+
const raw = body.data.integrations;
|
|
32
|
+
let integrations;
|
|
33
|
+
if (Array.isArray(raw)) {
|
|
34
|
+
integrations = {};
|
|
35
|
+
for (const entry of raw) {
|
|
36
|
+
const { id, ...rest } = entry;
|
|
37
|
+
integrations[id] = rest;
|
|
38
|
+
}
|
|
39
|
+
} else integrations = raw;
|
|
40
|
+
this.index = {
|
|
41
|
+
version: 1,
|
|
42
|
+
integrations
|
|
43
|
+
};
|
|
44
|
+
return this.index;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Force reload the index (bypass cache).
|
|
48
|
+
*/
|
|
49
|
+
async reload() {
|
|
50
|
+
this.index = null;
|
|
51
|
+
return this.load();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a specific integration entry by name.
|
|
55
|
+
*/
|
|
56
|
+
async get(id) {
|
|
57
|
+
return (await this.load()).integrations[id];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* List all integrations in the registry.
|
|
61
|
+
*/
|
|
62
|
+
async list() {
|
|
63
|
+
const index = await this.load();
|
|
64
|
+
return Object.entries(index.integrations).map(([id, entry]) => ({
|
|
65
|
+
id,
|
|
66
|
+
...entry
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Search integrations by name or description substring.
|
|
71
|
+
*/
|
|
72
|
+
async search(query) {
|
|
73
|
+
const all = await this.list();
|
|
74
|
+
const q = query.toLowerCase();
|
|
75
|
+
return all.filter((entry) => entry.id.toLowerCase().includes(q) || (entry.name?.toLowerCase().includes(q) ?? false) || entry.description.toLowerCase().includes(q));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/resolver.ts
|
|
80
|
+
var RegistryResolveError = class extends Error {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "RegistryResolveError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var Resolver = class {
|
|
87
|
+
registry;
|
|
88
|
+
constructor(registry) {
|
|
89
|
+
this.registry = registry;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve an integration name (+ optional version) to a git clone target.
|
|
93
|
+
*
|
|
94
|
+
* @param name - Integration name (e.g. "discord")
|
|
95
|
+
* @param version - Specific version (e.g. "1.0.0") or undefined for latest
|
|
96
|
+
* @returns Resolved integration with repo URL and git tag
|
|
97
|
+
*/
|
|
98
|
+
async resolve(id, version) {
|
|
99
|
+
const entry = await this.registry.get(id);
|
|
100
|
+
if (!entry) throw new RegistryResolveError(`Integration "${id}" not found in registry`);
|
|
101
|
+
const resolvedVersion = version ?? entry.latest;
|
|
102
|
+
if (!entry.versions.includes(resolvedVersion)) throw new RegistryResolveError(`Version "${resolvedVersion}" not found for integration "${id}". Available versions: ${entry.versions.join(", ")}`);
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
name: entry.name,
|
|
106
|
+
repo: entry.repository ? `${entry.repository}.git` : entry.repo,
|
|
107
|
+
version: resolvedVersion,
|
|
108
|
+
tag: `v${resolvedVersion}`,
|
|
109
|
+
commit: entry.commit,
|
|
110
|
+
subdir: entry.subdir,
|
|
111
|
+
description: entry.description
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if an integration exists in the registry.
|
|
116
|
+
*/
|
|
117
|
+
async exists(id) {
|
|
118
|
+
return await this.registry.get(id) !== void 0;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get all available versions for an integration.
|
|
122
|
+
*/
|
|
123
|
+
async versions(id) {
|
|
124
|
+
const entry = await this.registry.get(id);
|
|
125
|
+
if (!entry) throw new RegistryResolveError(`Integration "${id}" not found in registry`);
|
|
126
|
+
return entry.versions;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/installer.ts
|
|
131
|
+
/**
|
|
132
|
+
* Installer — git clone/fetch integrations to ~/.alfe/integrations/{name}/.
|
|
133
|
+
*
|
|
134
|
+
* Manages the local installation directory for integration repos.
|
|
135
|
+
* Each integration is a shallow git clone checked out to a version tag.
|
|
136
|
+
*/
|
|
137
|
+
const execFileAsync$1 = promisify(execFile);
|
|
138
|
+
const INTEGRATIONS_DIR = join(homedir(), ".alfe", "integrations");
|
|
139
|
+
const GIT_TIMEOUT_MS = 6e4;
|
|
140
|
+
var InstallerError = class extends Error {
|
|
141
|
+
constructor(message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "InstallerError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var Installer = class {
|
|
147
|
+
basePath;
|
|
148
|
+
/** Tracks subdir overrides per integration id */
|
|
149
|
+
subdirs = /* @__PURE__ */ new Map();
|
|
150
|
+
/**
|
|
151
|
+
* @param basePath - Override the integrations directory (for testing).
|
|
152
|
+
* Defaults to ~/.alfe/integrations/
|
|
153
|
+
*/
|
|
154
|
+
constructor(basePath) {
|
|
155
|
+
this.basePath = basePath ?? INTEGRATIONS_DIR;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the path where an integration's content lives.
|
|
159
|
+
* When a subdir is set, returns the subdir within the clone.
|
|
160
|
+
*/
|
|
161
|
+
getInstallPath(name) {
|
|
162
|
+
const base = join(this.basePath, name);
|
|
163
|
+
const subdir = this.subdirs.get(name);
|
|
164
|
+
return subdir ? join(base, subdir) : base;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get the root clone path (without subdir).
|
|
168
|
+
*/
|
|
169
|
+
getClonePath(name) {
|
|
170
|
+
return join(this.basePath, name);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Install an integration by cloning its git repo and checking out the version tag.
|
|
174
|
+
* Returns the effective install path (with subdir if present).
|
|
175
|
+
*/
|
|
176
|
+
async install(resolved) {
|
|
177
|
+
const clonePath = this.getClonePath(resolved.id);
|
|
178
|
+
if (existsSync(clonePath)) throw new InstallerError(`Integration "${resolved.id}" is already installed at ${clonePath}`);
|
|
179
|
+
if (resolved.subdir) this.subdirs.set(resolved.id, resolved.subdir);
|
|
180
|
+
mkdirSync(this.basePath, { recursive: true });
|
|
181
|
+
try {
|
|
182
|
+
if (resolved.commit) {
|
|
183
|
+
await execFileAsync$1("git", [
|
|
184
|
+
"clone",
|
|
185
|
+
resolved.repo,
|
|
186
|
+
clonePath
|
|
187
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
188
|
+
await execFileAsync$1("git", ["checkout", resolved.commit], {
|
|
189
|
+
cwd: clonePath,
|
|
190
|
+
timeout: GIT_TIMEOUT_MS
|
|
191
|
+
});
|
|
192
|
+
} else await execFileAsync$1("git", [
|
|
193
|
+
"clone",
|
|
194
|
+
"--depth",
|
|
195
|
+
"1",
|
|
196
|
+
"--branch",
|
|
197
|
+
resolved.tag,
|
|
198
|
+
resolved.repo,
|
|
199
|
+
clonePath
|
|
200
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (existsSync(clonePath)) rmSync(clonePath, {
|
|
203
|
+
recursive: true,
|
|
204
|
+
force: true
|
|
205
|
+
});
|
|
206
|
+
this.subdirs.delete(resolved.id);
|
|
207
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
208
|
+
throw new InstallerError(`Failed to clone integration "${resolved.id}": ${message}`);
|
|
209
|
+
}
|
|
210
|
+
return this.getInstallPath(resolved.id);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Update an installed integration to a new version.
|
|
214
|
+
*/
|
|
215
|
+
async update(name, resolved) {
|
|
216
|
+
const clonePath = this.getClonePath(name);
|
|
217
|
+
if (!existsSync(clonePath)) throw new InstallerError(`Integration "${name}" is not installed — cannot update`);
|
|
218
|
+
if (resolved.subdir) this.subdirs.set(name, resolved.subdir);
|
|
219
|
+
else this.subdirs.delete(name);
|
|
220
|
+
try {
|
|
221
|
+
await execFileAsync$1("git", [
|
|
222
|
+
"fetch",
|
|
223
|
+
"--depth",
|
|
224
|
+
"1",
|
|
225
|
+
"origin",
|
|
226
|
+
`tag`,
|
|
227
|
+
resolved.tag
|
|
228
|
+
], {
|
|
229
|
+
cwd: clonePath,
|
|
230
|
+
timeout: GIT_TIMEOUT_MS
|
|
231
|
+
});
|
|
232
|
+
await execFileAsync$1("git", ["checkout", resolved.tag], {
|
|
233
|
+
cwd: clonePath,
|
|
234
|
+
timeout: GIT_TIMEOUT_MS
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
throw new InstallerError(`Failed to update integration "${name}" to ${resolved.version}: ${message}`);
|
|
239
|
+
}
|
|
240
|
+
return this.getInstallPath(name);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Remove an installed integration.
|
|
244
|
+
*/
|
|
245
|
+
remove(name) {
|
|
246
|
+
const clonePath = this.getClonePath(name);
|
|
247
|
+
if (!existsSync(clonePath)) throw new InstallerError(`Integration "${name}" is not installed at ${clonePath}`);
|
|
248
|
+
rmSync(clonePath, {
|
|
249
|
+
recursive: true,
|
|
250
|
+
force: true
|
|
251
|
+
});
|
|
252
|
+
this.subdirs.delete(name);
|
|
253
|
+
return Promise.resolve();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* List all locally installed integrations.
|
|
257
|
+
*/
|
|
258
|
+
list() {
|
|
259
|
+
if (!existsSync(this.basePath)) return [];
|
|
260
|
+
const entries = readdirSync(this.basePath, { withFileTypes: true });
|
|
261
|
+
const installed = [];
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (!entry.isDirectory()) continue;
|
|
264
|
+
const integrationPath = join(this.basePath, entry.name);
|
|
265
|
+
const manifestPath = join(integrationPath, "alfe-integration.yaml");
|
|
266
|
+
if (!existsSync(manifestPath)) continue;
|
|
267
|
+
try {
|
|
268
|
+
const manifest = parseManifestFile(manifestPath);
|
|
269
|
+
installed.push({
|
|
270
|
+
id: manifest.id,
|
|
271
|
+
name: manifest.name,
|
|
272
|
+
version: manifest.version,
|
|
273
|
+
path: integrationPath
|
|
274
|
+
});
|
|
275
|
+
} catch {
|
|
276
|
+
installed.push({
|
|
277
|
+
id: entry.name,
|
|
278
|
+
version: "unknown",
|
|
279
|
+
path: integrationPath
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return installed;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check if an integration is installed.
|
|
287
|
+
*/
|
|
288
|
+
isInstalled(name) {
|
|
289
|
+
return existsSync(this.getClonePath(name));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/state.ts
|
|
294
|
+
/**
|
|
295
|
+
* State Manager — manages ~/.alfe/integrations.json.
|
|
296
|
+
*
|
|
297
|
+
* Read-modify-write with advisory file lock to prevent corruption
|
|
298
|
+
* from concurrent access. Secrets are NEVER written to this file.
|
|
299
|
+
*/
|
|
300
|
+
const DEFAULT_STATE_PATH = join(homedir(), ".alfe", "integrations.json");
|
|
301
|
+
const EMPTY_STATE = {
|
|
302
|
+
version: 1,
|
|
303
|
+
integrations: {}
|
|
304
|
+
};
|
|
305
|
+
var StateManager = class {
|
|
306
|
+
filePath;
|
|
307
|
+
lockHeld = false;
|
|
308
|
+
constructor(filePath) {
|
|
309
|
+
this.filePath = filePath ?? DEFAULT_STATE_PATH;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Read the current state file. Returns empty state if file doesn't exist.
|
|
313
|
+
*/
|
|
314
|
+
read() {
|
|
315
|
+
if (!existsSync(this.filePath)) return {
|
|
316
|
+
...EMPTY_STATE,
|
|
317
|
+
integrations: {}
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
321
|
+
const parsed = JSON.parse(raw);
|
|
322
|
+
if (typeof parsed !== "object" || parsed.version !== 1) return {
|
|
323
|
+
...EMPTY_STATE,
|
|
324
|
+
integrations: {}
|
|
325
|
+
};
|
|
326
|
+
return parsed;
|
|
327
|
+
} catch {
|
|
328
|
+
return {
|
|
329
|
+
...EMPTY_STATE,
|
|
330
|
+
integrations: {}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Write the state file atomically.
|
|
336
|
+
*/
|
|
337
|
+
write(state) {
|
|
338
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
339
|
+
writeFileSync(this.filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get a specific integration's state.
|
|
343
|
+
*/
|
|
344
|
+
get(name) {
|
|
345
|
+
return this.read().integrations[name];
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Set an integration's state (read-modify-write).
|
|
349
|
+
*/
|
|
350
|
+
set(name, entry) {
|
|
351
|
+
const state = this.read();
|
|
352
|
+
state.integrations[name] = entry;
|
|
353
|
+
this.write(state);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Update only specific fields of an integration's state.
|
|
357
|
+
*/
|
|
358
|
+
update(name, partial) {
|
|
359
|
+
const state = this.read();
|
|
360
|
+
if (!(name in state.integrations)) throw new Error(`Integration "${name}" not found in state`);
|
|
361
|
+
state.integrations[name] = {
|
|
362
|
+
...state.integrations[name],
|
|
363
|
+
...partial
|
|
364
|
+
};
|
|
365
|
+
this.write(state);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Update integration status.
|
|
369
|
+
*/
|
|
370
|
+
setStatus(name, status, error) {
|
|
371
|
+
const state = this.read();
|
|
372
|
+
if (!(name in state.integrations)) return;
|
|
373
|
+
const existing = state.integrations[name];
|
|
374
|
+
existing.status = status;
|
|
375
|
+
if (error !== void 0) existing.error = error;
|
|
376
|
+
else existing.error = void 0;
|
|
377
|
+
this.write(state);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Remove an integration from the state file.
|
|
381
|
+
*/
|
|
382
|
+
remove(name) {
|
|
383
|
+
const state = this.read();
|
|
384
|
+
state.integrations = Object.fromEntries(Object.entries(state.integrations).filter(([key]) => key !== name));
|
|
385
|
+
this.write(state);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* List all integrations in the state file.
|
|
389
|
+
*/
|
|
390
|
+
list() {
|
|
391
|
+
const state = this.read();
|
|
392
|
+
return Object.entries(state.integrations).map(([id, entry]) => ({
|
|
393
|
+
id,
|
|
394
|
+
...entry
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Check if an integration is in a given status.
|
|
399
|
+
*/
|
|
400
|
+
hasStatus(name, ...statuses) {
|
|
401
|
+
const entry = this.get(name);
|
|
402
|
+
if (!entry) return false;
|
|
403
|
+
return statuses.includes(entry.status);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/lock.ts
|
|
408
|
+
/**
|
|
409
|
+
* Lock Manager — tracks aggregate desired state across all active integrations.
|
|
410
|
+
*
|
|
411
|
+
* Maintains ~/.alfe/runtime-lock.json with what each runtime should have
|
|
412
|
+
* installed (plugins + skills) and which integration contributed each entry.
|
|
413
|
+
*/
|
|
414
|
+
const DEFAULT_LOCK_PATH = join(homedir(), ".alfe", "runtime-lock.json");
|
|
415
|
+
const EMPTY_LOCK = {
|
|
416
|
+
version: 1,
|
|
417
|
+
runtimes: {},
|
|
418
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
419
|
+
};
|
|
420
|
+
var LockManager = class {
|
|
421
|
+
filePath;
|
|
422
|
+
constructor(filePath) {
|
|
423
|
+
this.filePath = filePath ?? DEFAULT_LOCK_PATH;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Read the current lock file. Returns empty lock if file doesn't exist.
|
|
427
|
+
*/
|
|
428
|
+
read() {
|
|
429
|
+
if (!existsSync(this.filePath)) return {
|
|
430
|
+
...EMPTY_LOCK,
|
|
431
|
+
runtimes: {},
|
|
432
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
436
|
+
const parsed = JSON.parse(raw);
|
|
437
|
+
if (typeof parsed !== "object" || parsed === null || !("version" in parsed) || parsed.version !== 1) return {
|
|
438
|
+
...EMPTY_LOCK,
|
|
439
|
+
runtimes: {},
|
|
440
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
441
|
+
};
|
|
442
|
+
return parsed;
|
|
443
|
+
} catch {
|
|
444
|
+
return {
|
|
445
|
+
...EMPTY_LOCK,
|
|
446
|
+
runtimes: {},
|
|
447
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Write the lock file atomically.
|
|
453
|
+
*/
|
|
454
|
+
write(lock) {
|
|
455
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
456
|
+
lock.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
457
|
+
writeFileSync(this.filePath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Add entries for an integration activation in a specific runtime.
|
|
461
|
+
*/
|
|
462
|
+
addEntries(runtime, integrationId, version, plugins, skills, installPath) {
|
|
463
|
+
const lock = this.read();
|
|
464
|
+
if (!(runtime in lock.runtimes)) lock.runtimes[runtime] = {
|
|
465
|
+
plugins: [],
|
|
466
|
+
skills: []
|
|
467
|
+
};
|
|
468
|
+
const state = lock.runtimes[runtime];
|
|
469
|
+
for (const plugin of plugins) if (!state.plugins.some((p) => p.package === plugin.package && p.sourceIntegration === integrationId)) state.plugins.push({
|
|
470
|
+
package: plugin.package,
|
|
471
|
+
sourceIntegration: integrationId,
|
|
472
|
+
integrationVersion: version
|
|
473
|
+
});
|
|
474
|
+
for (const skill of skills) {
|
|
475
|
+
const name = skill.path.split("/").pop() ?? skill.path;
|
|
476
|
+
if (!state.skills.some((s) => s.name === name && s.sourceIntegration === integrationId)) state.skills.push({
|
|
477
|
+
name,
|
|
478
|
+
sourcePath: join(installPath, skill.path),
|
|
479
|
+
sourceIntegration: integrationId,
|
|
480
|
+
integrationVersion: version
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
this.write(lock);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Remove all entries for a given integration across all runtimes.
|
|
487
|
+
* Returns what was removed, keyed by runtime.
|
|
488
|
+
*/
|
|
489
|
+
removeEntries(integrationId) {
|
|
490
|
+
const lock = this.read();
|
|
491
|
+
const removed = {};
|
|
492
|
+
for (const [runtime, state] of Object.entries(lock.runtimes)) {
|
|
493
|
+
const removedPlugins = state.plugins.filter((p) => p.sourceIntegration === integrationId);
|
|
494
|
+
const removedSkills = state.skills.filter((s) => s.sourceIntegration === integrationId);
|
|
495
|
+
if (removedPlugins.length > 0 || removedSkills.length > 0) removed[runtime] = {
|
|
496
|
+
plugins: removedPlugins,
|
|
497
|
+
skills: removedSkills
|
|
498
|
+
};
|
|
499
|
+
state.plugins = state.plugins.filter((p) => p.sourceIntegration !== integrationId);
|
|
500
|
+
state.skills = state.skills.filter((s) => s.sourceIntegration !== integrationId);
|
|
501
|
+
}
|
|
502
|
+
this.write(lock);
|
|
503
|
+
return removed;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get the full desired state for a specific runtime.
|
|
507
|
+
*/
|
|
508
|
+
getDesiredState(runtime) {
|
|
509
|
+
return this.read().runtimes[runtime] ?? {
|
|
510
|
+
plugins: [],
|
|
511
|
+
skills: []
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/hooks.ts
|
|
517
|
+
/**
|
|
518
|
+
* Hook Runner — executes integration lifecycle hook scripts.
|
|
519
|
+
*
|
|
520
|
+
* Hooks are shell scripts defined in the integration manifest.
|
|
521
|
+
* They run as child processes with a 30-second timeout.
|
|
522
|
+
* stdout/stderr are captured and returned.
|
|
523
|
+
*
|
|
524
|
+
* The runner injects standard environment variables:
|
|
525
|
+
* - ALFE_INTEGRATION_DIR — path to the integration install directory
|
|
526
|
+
* - ALFE_STATE_DIR — path to the integration state directory (created if needed)
|
|
527
|
+
* - ALFE_<NAME>_<KEY> — config values (non-secret AND secret, both injected as env vars)
|
|
528
|
+
*
|
|
529
|
+
* Secrets are acceptable as env vars because:
|
|
530
|
+
* - Child process env is ephemeral (dies with the process)
|
|
531
|
+
* - Same security model as Docker secrets / systemd credentials
|
|
532
|
+
* - Never written to disk
|
|
533
|
+
*/
|
|
534
|
+
const HOOK_TIMEOUT_MS = 3e4;
|
|
535
|
+
const INTEGRATIONS_BASE_DIR = join(homedir(), ".alfe", "integrations");
|
|
536
|
+
const STATE_BASE_DIR = join(homedir(), ".alfe", "state");
|
|
537
|
+
/**
|
|
538
|
+
* Build the environment variables for a hook script execution.
|
|
539
|
+
*
|
|
540
|
+
* Injects:
|
|
541
|
+
* - ALFE_INTEGRATION_DIR=~/.alfe/integrations/{name}/
|
|
542
|
+
* - ALFE_STATE_DIR=~/.alfe/state/{name}/ (creates dir if needed)
|
|
543
|
+
* - ALFE_<NAME_UPPER>_<KEY_UPPER>=value for each config entry
|
|
544
|
+
* - ALFE_<NAME_UPPER>_<KEY_UPPER>=value for each secret entry
|
|
545
|
+
*/
|
|
546
|
+
function buildHookEnv(options, additionalEnv) {
|
|
547
|
+
const { integrationName, config, secrets } = options;
|
|
548
|
+
const nameUpper = integrationName.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
549
|
+
const integrationDir = join(INTEGRATIONS_BASE_DIR, integrationName);
|
|
550
|
+
const stateDir = join(STATE_BASE_DIR, integrationName);
|
|
551
|
+
mkdirSync(stateDir, {
|
|
552
|
+
recursive: true,
|
|
553
|
+
mode: 448
|
|
554
|
+
});
|
|
555
|
+
const env = {
|
|
556
|
+
...process.env,
|
|
557
|
+
ALFE_INTEGRATION_DIR: integrationDir,
|
|
558
|
+
ALFE_STATE_DIR: stateDir
|
|
559
|
+
};
|
|
560
|
+
if (config) {
|
|
561
|
+
for (const [key, value] of Object.entries(config)) if (value !== void 0 && value !== null) {
|
|
562
|
+
const envKey = `ALFE_${nameUpper}_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
563
|
+
env[envKey] = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (secrets) {
|
|
567
|
+
for (const [key, value] of secrets) if (value !== void 0 && value !== null) {
|
|
568
|
+
const envKey = `ALFE_${nameUpper}_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
569
|
+
env[envKey] = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (additionalEnv) Object.assign(env, additionalEnv);
|
|
573
|
+
return env;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Run a hook script from an integration directory.
|
|
577
|
+
*
|
|
578
|
+
* @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
|
|
579
|
+
* @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
|
|
580
|
+
* @param env - Additional environment variables to pass to the script
|
|
581
|
+
* @returns Hook execution result
|
|
582
|
+
*/
|
|
583
|
+
async function runHook(integrationPath, hookScript, env) {
|
|
584
|
+
const scriptPath = join(integrationPath, hookScript);
|
|
585
|
+
if (!existsSync(scriptPath)) return {
|
|
586
|
+
exitCode: 0,
|
|
587
|
+
stdout: "",
|
|
588
|
+
stderr: `Hook script not found: ${scriptPath} (skipped)`,
|
|
589
|
+
timedOut: false
|
|
590
|
+
};
|
|
591
|
+
return new Promise((resolve) => {
|
|
592
|
+
let stdout = "";
|
|
593
|
+
let stderr = "";
|
|
594
|
+
let timedOut = false;
|
|
595
|
+
const proc = spawn("bash", [scriptPath], {
|
|
596
|
+
cwd: integrationPath,
|
|
597
|
+
env: {
|
|
598
|
+
...process.env,
|
|
599
|
+
...env
|
|
600
|
+
},
|
|
601
|
+
stdio: [
|
|
602
|
+
"ignore",
|
|
603
|
+
"pipe",
|
|
604
|
+
"pipe"
|
|
605
|
+
]
|
|
606
|
+
});
|
|
607
|
+
const timer = setTimeout(() => {
|
|
608
|
+
timedOut = true;
|
|
609
|
+
proc.kill("SIGKILL");
|
|
610
|
+
}, HOOK_TIMEOUT_MS);
|
|
611
|
+
proc.stdout.on("data", (data) => {
|
|
612
|
+
stdout += data.toString();
|
|
613
|
+
if (stdout.length > 1e5) stdout = stdout.slice(0, 1e5) + "\n[truncated]";
|
|
614
|
+
});
|
|
615
|
+
proc.stderr.on("data", (data) => {
|
|
616
|
+
stderr += data.toString();
|
|
617
|
+
if (stderr.length > 1e5) stderr = stderr.slice(0, 1e5) + "\n[truncated]";
|
|
618
|
+
});
|
|
619
|
+
proc.on("close", (code) => {
|
|
620
|
+
clearTimeout(timer);
|
|
621
|
+
resolve({
|
|
622
|
+
exitCode: code ?? 1,
|
|
623
|
+
stdout: stdout.trim(),
|
|
624
|
+
stderr: stderr.trim(),
|
|
625
|
+
timedOut
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
proc.on("error", (err) => {
|
|
629
|
+
clearTimeout(timer);
|
|
630
|
+
resolve({
|
|
631
|
+
exitCode: 1,
|
|
632
|
+
stdout: "",
|
|
633
|
+
stderr: `Failed to execute hook: ${err.message}`,
|
|
634
|
+
timedOut: false
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Run a hook script with full integration context (config + secrets as env vars).
|
|
641
|
+
*
|
|
642
|
+
* This is the preferred method for running lifecycle hooks (activate, deactivate, health).
|
|
643
|
+
* It builds the complete environment including ALFE_INTEGRATION_DIR, ALFE_STATE_DIR,
|
|
644
|
+
* and all config/secret values as ALFE_<NAME>_<KEY> env vars.
|
|
645
|
+
*
|
|
646
|
+
* @param integrationPath - Base path of the integration
|
|
647
|
+
* @param hookScript - Relative path to the hook script
|
|
648
|
+
* @param options - Integration name, config, and secrets
|
|
649
|
+
* @returns Hook execution result
|
|
650
|
+
*/
|
|
651
|
+
async function runHookWithContext(integrationPath, hookScript, options) {
|
|
652
|
+
return runHook(integrationPath, hookScript, buildHookEnv(options));
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/appliers/openclaw-applier.ts
|
|
656
|
+
/**
|
|
657
|
+
* OpenClawApplier — applies plugins, skills, and config to the OpenClaw runtime.
|
|
658
|
+
*
|
|
659
|
+
* Plugins are installed via `pnpm add` in the OpenClaw workspace directory.
|
|
660
|
+
* Skills are copied to ~/.alfe/skills/{name}.
|
|
661
|
+
* Config is deep-merged into the OpenClaw agent config, with per-integration
|
|
662
|
+
* tracking so changes can be cleanly removed on deactivation.
|
|
663
|
+
*/
|
|
664
|
+
const execFileAsync = promisify(execFile);
|
|
665
|
+
const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
|
|
666
|
+
/**
|
|
667
|
+
* Deep-merge source into target, returning a new object.
|
|
668
|
+
* Arrays are replaced, not concatenated.
|
|
669
|
+
*/
|
|
670
|
+
function deepMerge(target, source) {
|
|
671
|
+
const result = { ...target };
|
|
672
|
+
for (const key of Object.keys(source)) {
|
|
673
|
+
const srcVal = source[key];
|
|
674
|
+
const tgtVal = result[key];
|
|
675
|
+
if (srcVal !== null && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal !== null && typeof tgtVal === "object" && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
|
|
676
|
+
else result[key] = srcVal;
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
const LLM_INTEGRATION_IDS = new Set(["anthropic", "openai"]);
|
|
681
|
+
/**
|
|
682
|
+
* Resolve LLM provider runtime config based on mode.
|
|
683
|
+
*
|
|
684
|
+
* - "alfe_credits": keep manifest config as-is (baseUrl points to local proxy,
|
|
685
|
+
* apiKey is a dummy — the proxy injects the real key)
|
|
686
|
+
* - "byok": remove baseUrl (SDK uses provider default), set apiKey from
|
|
687
|
+
* decrypted secret
|
|
688
|
+
*
|
|
689
|
+
* Non-LLM integrations pass through unchanged.
|
|
690
|
+
*/
|
|
691
|
+
function resolveLlmRuntimeConfig(integrationId, runtimeConfig, integrationConfig, secrets) {
|
|
692
|
+
if (!LLM_INTEGRATION_IDS.has(integrationId)) return runtimeConfig;
|
|
693
|
+
if (integrationConfig?.mode === "byok") {
|
|
694
|
+
const apiKey = secrets?.get("api_key");
|
|
695
|
+
const config = structuredClone(runtimeConfig);
|
|
696
|
+
const providers = config.models?.providers;
|
|
697
|
+
if (providers?.[integrationId]) {
|
|
698
|
+
delete providers[integrationId].baseUrl;
|
|
699
|
+
if (apiKey) providers[integrationId].apiKey = apiKey;
|
|
700
|
+
}
|
|
701
|
+
return config;
|
|
702
|
+
}
|
|
703
|
+
return runtimeConfig;
|
|
704
|
+
}
|
|
705
|
+
var OpenClawApplier = class {
|
|
706
|
+
runtime = "openclaw";
|
|
707
|
+
workspace;
|
|
708
|
+
skillsDir;
|
|
709
|
+
configPath;
|
|
710
|
+
constructor(options) {
|
|
711
|
+
this.workspace = options.workspace;
|
|
712
|
+
this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
|
|
713
|
+
this.configPath = options.configPath ?? join(this.workspace, "config.json");
|
|
714
|
+
}
|
|
715
|
+
async applyPlugin(pkg) {
|
|
716
|
+
await execFileAsync("pnpm", ["add", pkg], {
|
|
717
|
+
cwd: this.workspace,
|
|
718
|
+
timeout: 6e4
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async removePlugin(pkg) {
|
|
722
|
+
await execFileAsync("pnpm", ["remove", pkg], {
|
|
723
|
+
cwd: this.workspace,
|
|
724
|
+
timeout: 3e4
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
applySkill(name, srcPath) {
|
|
728
|
+
if (!existsSync(srcPath)) throw new Error(`Skill source path not found: ${srcPath}`);
|
|
729
|
+
mkdirSync(this.skillsDir, { recursive: true });
|
|
730
|
+
cpSync(srcPath, join(this.skillsDir, name), { recursive: true });
|
|
731
|
+
return Promise.resolve();
|
|
732
|
+
}
|
|
733
|
+
removeSkill(name) {
|
|
734
|
+
const skillPath = join(this.skillsDir, name);
|
|
735
|
+
if (existsSync(skillPath)) rmSync(skillPath, {
|
|
736
|
+
recursive: true,
|
|
737
|
+
force: true
|
|
738
|
+
});
|
|
739
|
+
return Promise.resolve();
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Deep-merge integration config into the OpenClaw agent config file.
|
|
743
|
+
*
|
|
744
|
+
* Each integration's config contribution is tracked in
|
|
745
|
+
* `_integrations.{integrationId}` within the config file so it can be
|
|
746
|
+
* cleanly removed later.
|
|
747
|
+
*/
|
|
748
|
+
applyConfig(integrationId, config) {
|
|
749
|
+
const current = this.readConfig();
|
|
750
|
+
const integrations = current._integrations ?? {};
|
|
751
|
+
integrations[integrationId] = config;
|
|
752
|
+
current._integrations = integrations;
|
|
753
|
+
const merged = deepMerge(current, config);
|
|
754
|
+
merged._integrations = current._integrations;
|
|
755
|
+
this.writeConfig(merged);
|
|
756
|
+
return Promise.resolve();
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Remove config previously applied by an integration.
|
|
760
|
+
*
|
|
761
|
+
* Rebuilds the config by re-merging all remaining integrations' configs,
|
|
762
|
+
* ensuring clean removal without orphaned keys.
|
|
763
|
+
*/
|
|
764
|
+
removeConfig(integrationId) {
|
|
765
|
+
const current = this.readConfig();
|
|
766
|
+
const integrations = current._integrations ?? {};
|
|
767
|
+
if (!(integrationId in integrations)) return Promise.resolve();
|
|
768
|
+
const remainingIntegrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
|
|
769
|
+
let rebuilt = this.getBaseConfig(current);
|
|
770
|
+
for (const cfg of Object.values(remainingIntegrations)) rebuilt = deepMerge(rebuilt, cfg);
|
|
771
|
+
rebuilt._integrations = remainingIntegrations;
|
|
772
|
+
this.writeConfig(rebuilt);
|
|
773
|
+
return Promise.resolve();
|
|
774
|
+
}
|
|
775
|
+
isAvailable() {
|
|
776
|
+
return Promise.resolve(existsSync(this.workspace));
|
|
777
|
+
}
|
|
778
|
+
readConfig() {
|
|
779
|
+
if (!existsSync(this.configPath)) return {};
|
|
780
|
+
try {
|
|
781
|
+
return JSON.parse(readFileSync(this.configPath, "utf-8"));
|
|
782
|
+
} catch {
|
|
783
|
+
return {};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
writeConfig(config) {
|
|
787
|
+
mkdirSync(join(this.configPath, ".."), { recursive: true });
|
|
788
|
+
writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Extract the base config by stripping all keys that were contributed
|
|
792
|
+
* by integrations. This is done by removing each integration's keys
|
|
793
|
+
* from the current config.
|
|
794
|
+
*/
|
|
795
|
+
getBaseConfig(current) {
|
|
796
|
+
const integrations = current._integrations ?? {};
|
|
797
|
+
const allIntegrationKeys = /* @__PURE__ */ new Set();
|
|
798
|
+
for (const cfg of Object.values(integrations)) for (const key of Object.keys(cfg)) allIntegrationKeys.add(key);
|
|
799
|
+
const base = {};
|
|
800
|
+
for (const [key, val] of Object.entries(current)) {
|
|
801
|
+
if (key === "_integrations") continue;
|
|
802
|
+
if (!allIntegrationKeys.has(key)) base[key] = val;
|
|
803
|
+
}
|
|
804
|
+
return base;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/integration-manager.ts
|
|
809
|
+
/**
|
|
810
|
+
* Integration Manager — full lifecycle management for Alfe integrations.
|
|
811
|
+
*
|
|
812
|
+
* Handles the complete install → configure → activate → deactivate → uninstall
|
|
813
|
+
* lifecycle. Uses runtime appliers to apply/remove plugins and skills to
|
|
814
|
+
* specific agent runtimes during activate/deactivate.
|
|
815
|
+
*
|
|
816
|
+
* Key change from v1: install() only clones + validates. activate() applies
|
|
817
|
+
* plugins/skills to runtimes. deactivate() removes them.
|
|
818
|
+
*/
|
|
819
|
+
/**
|
|
820
|
+
* Merge universal installs with runtime-specific installs from the manifest.
|
|
821
|
+
*/
|
|
822
|
+
function resolveInstallsForRuntime(manifest, runtime) {
|
|
823
|
+
const universal = manifest.installs;
|
|
824
|
+
const runtimeSpecific = universal.runtimes?.[runtime];
|
|
825
|
+
return {
|
|
826
|
+
plugins: [...universal.plugins ?? [], ...runtimeSpecific?.plugins ?? []],
|
|
827
|
+
skills: [...universal.skills ?? [], ...runtimeSpecific?.skills ?? []],
|
|
828
|
+
config: runtimeSpecific?.config
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
var IntegrationManager = class {
|
|
832
|
+
log = createLogger("IntegrationManager");
|
|
833
|
+
state;
|
|
834
|
+
registry;
|
|
835
|
+
resolver;
|
|
836
|
+
installer;
|
|
837
|
+
runtimeAppliers;
|
|
838
|
+
lockManager;
|
|
839
|
+
/** In-memory secret store — NEVER persisted to disk */
|
|
840
|
+
secrets = /* @__PURE__ */ new Map();
|
|
841
|
+
constructor(options) {
|
|
842
|
+
this.state = new StateManager(options?.statePath);
|
|
843
|
+
this.registry = new Registry();
|
|
844
|
+
this.resolver = new Resolver(this.registry);
|
|
845
|
+
this.installer = new Installer(options?.integrationsDir);
|
|
846
|
+
this.runtimeAppliers = options?.runtimeAppliers ?? /* @__PURE__ */ new Map();
|
|
847
|
+
this.lockManager = new LockManager(options?.lockPath);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Install an integration from the registry.
|
|
851
|
+
*
|
|
852
|
+
* 1. Resolve from registry
|
|
853
|
+
* 2. Git clone via registry installer
|
|
854
|
+
* 3. Parse + validate alfe-integration.yaml
|
|
855
|
+
* 4. Check depends_on are already installed
|
|
856
|
+
* 5. Run pre_install hook
|
|
857
|
+
* 6. Run post_install hook
|
|
858
|
+
* 7. Persist state to integrations.json
|
|
859
|
+
*
|
|
860
|
+
* Note: Skills and plugins are NOT applied during install.
|
|
861
|
+
* They are applied during activate() via runtime appliers.
|
|
862
|
+
*/
|
|
863
|
+
async install(params) {
|
|
864
|
+
const { name, version, config } = params;
|
|
865
|
+
if (!name) return this.err("INVALID_PARAMS", "Integration name is required");
|
|
866
|
+
const existing = this.state.get(name);
|
|
867
|
+
if (existing && existing.status !== "error") return this.err("ALREADY_INSTALLED", `Integration "${name}" is already installed (status: ${existing.status})`);
|
|
868
|
+
this.log.info(`Installing integration: ${name}${version ? `@${version}` : ""}`);
|
|
869
|
+
this.state.set(name, {
|
|
870
|
+
status: "installing",
|
|
871
|
+
version: version ?? "unknown",
|
|
872
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
873
|
+
config: {}
|
|
874
|
+
});
|
|
875
|
+
try {
|
|
876
|
+
const resolved = await this.resolver.resolve(name, version);
|
|
877
|
+
this.log.info(`Resolved ${name}@${resolved.version} from ${resolved.repo}`);
|
|
878
|
+
const installPath = await this.installer.install(resolved);
|
|
879
|
+
this.log.info(`Cloned to ${installPath}`);
|
|
880
|
+
const manifestPath = join(installPath, "alfe-integration.yaml");
|
|
881
|
+
if (!existsSync(manifestPath)) throw new Error(`No alfe-integration.yaml found in ${installPath}`);
|
|
882
|
+
const manifest = parseManifestFile(manifestPath);
|
|
883
|
+
this.log.info(`Manifest validated: ${manifest.id}@${manifest.version}`);
|
|
884
|
+
for (const dep of manifest.depends_on) {
|
|
885
|
+
const depState = this.state.get(dep);
|
|
886
|
+
if (!depState || depState.status === "error") throw new Error(`Dependency "${dep}" is not installed. Install it first: alfe integration install ${dep}`);
|
|
887
|
+
}
|
|
888
|
+
if (manifest.hooks.pre_install) {
|
|
889
|
+
this.log.info(`Running pre_install hook: ${manifest.hooks.pre_install}`);
|
|
890
|
+
const hookResult = await runHook(installPath, manifest.hooks.pre_install);
|
|
891
|
+
if (hookResult.exitCode !== 0) throw new Error(`pre_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
|
|
892
|
+
}
|
|
893
|
+
if (manifest.hooks.post_install) {
|
|
894
|
+
this.log.info(`Running post_install hook: ${manifest.hooks.post_install}`);
|
|
895
|
+
const hookResult = await runHookWithContext(installPath, manifest.hooks.post_install, {
|
|
896
|
+
integrationName: name,
|
|
897
|
+
config: config ?? {},
|
|
898
|
+
secrets: this.secrets.get(name)
|
|
899
|
+
});
|
|
900
|
+
if (hookResult.exitCode !== 0) this.log.warn(`post_install hook failed (non-fatal): ${hookResult.stderr || hookResult.stdout}`);
|
|
901
|
+
}
|
|
902
|
+
this.state.set(name, {
|
|
903
|
+
status: "installed",
|
|
904
|
+
version: manifest.version,
|
|
905
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
906
|
+
config: config ?? {}
|
|
907
|
+
});
|
|
908
|
+
this.log.info(`Integration "${name}" installed successfully`);
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
payload: {
|
|
912
|
+
id: manifest.id,
|
|
913
|
+
name: manifest.name,
|
|
914
|
+
version: manifest.version,
|
|
915
|
+
status: "installed",
|
|
916
|
+
capabilities: manifest.capabilities
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
} catch (err) {
|
|
920
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
921
|
+
this.log.error(`Failed to install "${name}": ${message}`);
|
|
922
|
+
try {
|
|
923
|
+
await this.installer.remove(name);
|
|
924
|
+
} catch {}
|
|
925
|
+
this.state.setStatus(name, "error", message);
|
|
926
|
+
return this.err("INSTALL_FAILED", message);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Configure an installed integration.
|
|
931
|
+
*
|
|
932
|
+
* 1. Load manifest for this integration
|
|
933
|
+
* 2. Validate config against config_schema
|
|
934
|
+
* 3. Store non-secret values in state file
|
|
935
|
+
* 4. Store secret values in memory only (NEVER disk)
|
|
936
|
+
*/
|
|
937
|
+
configure(params) {
|
|
938
|
+
const { name, config } = params;
|
|
939
|
+
if (!name) return this.err("INVALID_PARAMS", "Integration name is required");
|
|
940
|
+
const entry = this.state.get(name);
|
|
941
|
+
if (!entry) return this.err("NOT_FOUND", `Integration "${name}" is not installed`);
|
|
942
|
+
if (entry.status !== "installed" && entry.status !== "configured" && entry.status !== "active") return this.err("INVALID_STATE", `Cannot configure integration in "${entry.status}" state`);
|
|
943
|
+
this.log.info(`Configuring integration: ${name}`);
|
|
944
|
+
try {
|
|
945
|
+
const manifest = parseManifestFile(join(this.installer.getInstallPath(name), "alfe-integration.yaml"));
|
|
946
|
+
if (manifest.config_schema.length > 0) {
|
|
947
|
+
const result = buildConfigValidationSchema(manifest.config_schema).safeParse(config);
|
|
948
|
+
if (!result.success) {
|
|
949
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
950
|
+
return this.err("INVALID_CONFIG", `Config validation failed: ${issues}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const missingRequired = manifest.config_schema.filter((f) => f.required && config[f.key] === void 0).map((f) => f.key);
|
|
954
|
+
if (missingRequired.length > 0) return this.err("MISSING_CONFIG", `Missing required config fields: ${missingRequired.join(", ")}`);
|
|
955
|
+
const nonSecretConfig = {};
|
|
956
|
+
const secretConfig = /* @__PURE__ */ new Map();
|
|
957
|
+
for (const [key, value] of Object.entries(config)) if (manifest.config_schema.find((f) => f.key === key)?.type === "secret") secretConfig.set(key, value);
|
|
958
|
+
else nonSecretConfig[key] = value;
|
|
959
|
+
this.state.update(name, {
|
|
960
|
+
status: "configured",
|
|
961
|
+
config: nonSecretConfig
|
|
962
|
+
});
|
|
963
|
+
this.secrets.set(name, secretConfig);
|
|
964
|
+
this.log.info(`Integration "${name}" configured (${String(secretConfig.size)} secrets in memory)`);
|
|
965
|
+
return {
|
|
966
|
+
ok: true,
|
|
967
|
+
payload: {
|
|
968
|
+
name,
|
|
969
|
+
status: "configured",
|
|
970
|
+
configUpdated: true,
|
|
971
|
+
secretCount: secretConfig.size
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
} catch (err) {
|
|
975
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
976
|
+
this.log.error(`Failed to configure "${name}": ${message}`);
|
|
977
|
+
return this.err("CONFIGURE_FAILED", message);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Activate an installed + configured integration.
|
|
982
|
+
*
|
|
983
|
+
* 1. Check integration is installed + configured
|
|
984
|
+
* 2. For each registered runtime applier, apply plugins + skills
|
|
985
|
+
* 3. Update lock file
|
|
986
|
+
* 4. Run health_check hook if present
|
|
987
|
+
* 5. Mark as active in state
|
|
988
|
+
*/
|
|
989
|
+
async activate(integrationId) {
|
|
990
|
+
const entry = this.state.get(integrationId);
|
|
991
|
+
if (!entry) return this.err("NOT_FOUND", `Integration "${integrationId}" is not installed`);
|
|
992
|
+
if (entry.status === "active") return {
|
|
993
|
+
ok: true,
|
|
994
|
+
payload: {
|
|
995
|
+
name: integrationId,
|
|
996
|
+
status: "active",
|
|
997
|
+
message: "Already active"
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
if (entry.status !== "configured" && entry.status !== "installed") return this.err("INVALID_STATE", `Cannot activate integration in "${entry.status}" state`);
|
|
1001
|
+
this.log.info(`Activating integration: ${integrationId}`);
|
|
1002
|
+
try {
|
|
1003
|
+
const installPath = this.installer.getInstallPath(integrationId);
|
|
1004
|
+
const manifestPath = join(installPath, "alfe-integration.yaml");
|
|
1005
|
+
if (!existsSync(manifestPath)) return this.err("MANIFEST_MISSING", `Manifest not found at ${manifestPath}`);
|
|
1006
|
+
const manifest = parseManifestFile(manifestPath);
|
|
1007
|
+
const supportedAgents = manifest.supported_agents;
|
|
1008
|
+
for (const [runtimeName, applier] of this.runtimeAppliers) {
|
|
1009
|
+
if (!await applier.isAvailable()) {
|
|
1010
|
+
this.log.warn(`Runtime "${runtimeName}" is not available — skipping`);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (supportedAgents && supportedAgents.length > 0 && !supportedAgents.includes(runtimeName)) {
|
|
1014
|
+
this.log.warn(`Integration "${integrationId}" does not support runtime "${runtimeName}" (supported: ${supportedAgents.join(", ")}) — skipping`);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const { plugins, skills, config: runtimeConfig } = resolveInstallsForRuntime(manifest, runtimeName);
|
|
1018
|
+
for (const plugin of plugins) {
|
|
1019
|
+
this.log.info(`Applying plugin ${plugin.package} to ${runtimeName}`);
|
|
1020
|
+
await applier.applyPlugin(plugin.package, installPath);
|
|
1021
|
+
}
|
|
1022
|
+
for (const skill of skills) {
|
|
1023
|
+
const skillName = skill.path.split("/").pop() ?? skill.path;
|
|
1024
|
+
const srcPath = join(installPath, skill.path);
|
|
1025
|
+
this.log.info(`Applying skill ${skillName} to ${runtimeName}`);
|
|
1026
|
+
await applier.applySkill(skillName, srcPath);
|
|
1027
|
+
}
|
|
1028
|
+
if (runtimeConfig && Object.keys(runtimeConfig).length > 0) {
|
|
1029
|
+
const resolvedConfig = resolveLlmRuntimeConfig(integrationId, runtimeConfig, entry.config, this.secrets.get(integrationId));
|
|
1030
|
+
this.log.info(`Applying config for ${integrationId} to ${runtimeName}`);
|
|
1031
|
+
await applier.applyConfig(integrationId, resolvedConfig);
|
|
1032
|
+
}
|
|
1033
|
+
if (plugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, plugins, skills, installPath);
|
|
1034
|
+
}
|
|
1035
|
+
if (manifest.hooks.health_check) {
|
|
1036
|
+
this.log.info(`Running health check: ${manifest.hooks.health_check}`);
|
|
1037
|
+
const hookResult = await runHookWithContext(installPath, manifest.hooks.health_check, {
|
|
1038
|
+
integrationName: integrationId,
|
|
1039
|
+
config: entry.config,
|
|
1040
|
+
secrets: this.secrets.get(integrationId)
|
|
1041
|
+
});
|
|
1042
|
+
if (hookResult.exitCode !== 0) {
|
|
1043
|
+
this.state.setStatus(integrationId, "error", `Health check failed: ${hookResult.stderr || hookResult.stdout}`);
|
|
1044
|
+
return this.err("HEALTH_CHECK_FAILED", `Health check failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
this.state.setStatus(integrationId, "active");
|
|
1048
|
+
this.log.info(`Integration "${integrationId}" activated`);
|
|
1049
|
+
return {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
payload: {
|
|
1052
|
+
name: integrationId,
|
|
1053
|
+
status: "active"
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1058
|
+
this.log.error(`Failed to activate "${integrationId}": ${message}`);
|
|
1059
|
+
return this.err("ACTIVATE_FAILED", message);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Deactivate a running integration.
|
|
1064
|
+
*
|
|
1065
|
+
* 1. Read lock file to find all entries for this integration
|
|
1066
|
+
* 2. For each runtime's applier, remove plugins + skills
|
|
1067
|
+
* 3. Update lock file
|
|
1068
|
+
* 4. Set state to configured
|
|
1069
|
+
*/
|
|
1070
|
+
async deactivate(integrationId) {
|
|
1071
|
+
const entry = this.state.get(integrationId);
|
|
1072
|
+
if (!entry) return this.err("NOT_FOUND", `Integration "${integrationId}" is not installed`);
|
|
1073
|
+
if (entry.status !== "active") return {
|
|
1074
|
+
ok: true,
|
|
1075
|
+
payload: {
|
|
1076
|
+
name: integrationId,
|
|
1077
|
+
status: entry.status,
|
|
1078
|
+
message: "Not active"
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
this.log.info(`Deactivating integration: ${integrationId}`);
|
|
1082
|
+
try {
|
|
1083
|
+
const removed = this.lockManager.removeEntries(integrationId);
|
|
1084
|
+
for (const [runtimeName, entries] of Object.entries(removed)) {
|
|
1085
|
+
const applier = this.runtimeAppliers.get(runtimeName);
|
|
1086
|
+
if (!applier) {
|
|
1087
|
+
this.log.warn(`No applier for runtime "${runtimeName}" — cannot remove entries`);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (!await applier.isAvailable()) {
|
|
1091
|
+
this.log.warn(`Runtime "${runtimeName}" is not available — skipping removal`);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
for (const plugin of entries.plugins) {
|
|
1095
|
+
this.log.info(`Removing plugin ${plugin.package} from ${runtimeName}`);
|
|
1096
|
+
try {
|
|
1097
|
+
await applier.removePlugin(plugin.package);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
this.log.warn(`Failed to remove plugin ${plugin.package} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const skill of entries.skills) {
|
|
1103
|
+
this.log.info(`Removing skill ${skill.name} from ${runtimeName}`);
|
|
1104
|
+
try {
|
|
1105
|
+
await applier.removeSkill(skill.name);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
this.log.warn(`Failed to remove skill ${skill.name} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
this.log.info(`Removing config for ${integrationId} from ${runtimeName}`);
|
|
1111
|
+
try {
|
|
1112
|
+
await applier.removeConfig(integrationId);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
this.log.warn(`Failed to remove config for ${integrationId} from ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
this.state.setStatus(integrationId, "configured");
|
|
1118
|
+
return {
|
|
1119
|
+
ok: true,
|
|
1120
|
+
payload: {
|
|
1121
|
+
name: integrationId,
|
|
1122
|
+
status: "configured"
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1127
|
+
this.log.error(`Failed to deactivate "${integrationId}": ${message}`);
|
|
1128
|
+
return this.err("DEACTIVATE_FAILED", message);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Uninstall an integration completely.
|
|
1133
|
+
*
|
|
1134
|
+
* 1. Deactivate first (removes plugins/skills via appliers)
|
|
1135
|
+
* 2. Run pre_uninstall hook
|
|
1136
|
+
* 3. Run post_uninstall hook
|
|
1137
|
+
* 4. Remove git clone from ~/.alfe/integrations/
|
|
1138
|
+
* 5. Remove from state file
|
|
1139
|
+
*/
|
|
1140
|
+
async uninstall(params) {
|
|
1141
|
+
const { name } = params;
|
|
1142
|
+
if (!name) return this.err("INVALID_PARAMS", "Integration name is required");
|
|
1143
|
+
const entry = this.state.get(name);
|
|
1144
|
+
if (!entry) return this.err("NOT_FOUND", `Integration "${name}" is not installed`);
|
|
1145
|
+
this.log.info(`Uninstalling integration: ${name}`);
|
|
1146
|
+
try {
|
|
1147
|
+
if (entry.status === "active") await this.deactivate(name);
|
|
1148
|
+
const installPath = this.installer.getInstallPath(name);
|
|
1149
|
+
let manifest = null;
|
|
1150
|
+
const manifestPath = join(installPath, "alfe-integration.yaml");
|
|
1151
|
+
if (existsSync(manifestPath)) try {
|
|
1152
|
+
manifest = parseManifestFile(manifestPath);
|
|
1153
|
+
} catch {
|
|
1154
|
+
this.log.warn(`Could not parse manifest for "${name}" — proceeding with basic cleanup`);
|
|
1155
|
+
}
|
|
1156
|
+
if (manifest?.hooks.pre_uninstall) {
|
|
1157
|
+
this.log.info(`Running pre_uninstall hook: ${manifest.hooks.pre_uninstall}`);
|
|
1158
|
+
const hookResult = await runHookWithContext(installPath, manifest.hooks.pre_uninstall, {
|
|
1159
|
+
integrationName: name,
|
|
1160
|
+
config: entry.config,
|
|
1161
|
+
secrets: this.secrets.get(name)
|
|
1162
|
+
});
|
|
1163
|
+
if (hookResult.exitCode !== 0) this.log.warn(`pre_uninstall hook failed (continuing): ${hookResult.stderr}`);
|
|
1164
|
+
}
|
|
1165
|
+
if (manifest?.hooks.post_uninstall) {
|
|
1166
|
+
this.log.info(`Running post_uninstall hook: ${manifest.hooks.post_uninstall}`);
|
|
1167
|
+
const hookResult = await runHookWithContext(installPath, manifest.hooks.post_uninstall, {
|
|
1168
|
+
integrationName: name,
|
|
1169
|
+
config: entry.config,
|
|
1170
|
+
secrets: this.secrets.get(name)
|
|
1171
|
+
});
|
|
1172
|
+
if (hookResult.exitCode !== 0) this.log.warn(`post_uninstall hook failed (non-fatal): ${hookResult.stderr}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (this.installer.isInstalled(name)) {
|
|
1175
|
+
await this.installer.remove(name);
|
|
1176
|
+
this.log.info(`Removed integration files from ${installPath}`);
|
|
1177
|
+
}
|
|
1178
|
+
this.state.remove(name);
|
|
1179
|
+
this.secrets.delete(name);
|
|
1180
|
+
this.log.info(`Integration "${name}" uninstalled`);
|
|
1181
|
+
return {
|
|
1182
|
+
ok: true,
|
|
1183
|
+
payload: {
|
|
1184
|
+
name,
|
|
1185
|
+
status: "removed"
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1190
|
+
this.log.error(`Failed to uninstall "${name}": ${message}`);
|
|
1191
|
+
return this.err("UNINSTALL_FAILED", message);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Run health checks for one or all integrations.
|
|
1196
|
+
*/
|
|
1197
|
+
async health(params) {
|
|
1198
|
+
const { name } = params;
|
|
1199
|
+
if (name) return {
|
|
1200
|
+
ok: true,
|
|
1201
|
+
payload: await this.checkHealth(name)
|
|
1202
|
+
};
|
|
1203
|
+
const entries = this.state.list();
|
|
1204
|
+
const results = [];
|
|
1205
|
+
for (const entry of entries) {
|
|
1206
|
+
const result = await this.checkHealth(entry.id);
|
|
1207
|
+
results.push(result);
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
ok: true,
|
|
1211
|
+
payload: {
|
|
1212
|
+
overall: results.every((r) => r.healthy),
|
|
1213
|
+
results,
|
|
1214
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* List all installed integrations.
|
|
1220
|
+
*/
|
|
1221
|
+
list() {
|
|
1222
|
+
return this.state.list().map((entry) => ({
|
|
1223
|
+
id: entry.id,
|
|
1224
|
+
name: void 0,
|
|
1225
|
+
version: entry.version,
|
|
1226
|
+
status: entry.status,
|
|
1227
|
+
installedAt: entry.installedAt,
|
|
1228
|
+
config: entry.config,
|
|
1229
|
+
error: entry.error
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Get a specific integration by name.
|
|
1234
|
+
*/
|
|
1235
|
+
get(name) {
|
|
1236
|
+
const entry = this.state.get(name);
|
|
1237
|
+
if (!entry) return void 0;
|
|
1238
|
+
return {
|
|
1239
|
+
id: name,
|
|
1240
|
+
version: entry.version,
|
|
1241
|
+
status: entry.status,
|
|
1242
|
+
installedAt: entry.installedAt,
|
|
1243
|
+
config: entry.config,
|
|
1244
|
+
error: entry.error
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Clear in-memory secrets (called on daemon restart).
|
|
1249
|
+
*/
|
|
1250
|
+
clear() {
|
|
1251
|
+
this.secrets.clear();
|
|
1252
|
+
}
|
|
1253
|
+
async checkHealth(id) {
|
|
1254
|
+
const entry = this.state.get(id);
|
|
1255
|
+
if (!entry) return {
|
|
1256
|
+
id,
|
|
1257
|
+
healthy: false,
|
|
1258
|
+
status: "error",
|
|
1259
|
+
message: "Not installed"
|
|
1260
|
+
};
|
|
1261
|
+
const installPath = this.installer.getInstallPath(id);
|
|
1262
|
+
const manifestPath = join(installPath, "alfe-integration.yaml");
|
|
1263
|
+
if (!existsSync(manifestPath)) return {
|
|
1264
|
+
id,
|
|
1265
|
+
healthy: false,
|
|
1266
|
+
status: entry.status,
|
|
1267
|
+
version: entry.version,
|
|
1268
|
+
message: "Manifest file missing — integration may be corrupted"
|
|
1269
|
+
};
|
|
1270
|
+
try {
|
|
1271
|
+
const manifest = parseManifestFile(manifestPath);
|
|
1272
|
+
if (!manifest.hooks.health_check) return {
|
|
1273
|
+
id,
|
|
1274
|
+
name: manifest.name,
|
|
1275
|
+
healthy: entry.status === "active" || entry.status === "configured" || entry.status === "installed",
|
|
1276
|
+
status: entry.status,
|
|
1277
|
+
version: manifest.version,
|
|
1278
|
+
message: "No health check hook defined"
|
|
1279
|
+
};
|
|
1280
|
+
const hookResult = await runHookWithContext(installPath, manifest.hooks.health_check, {
|
|
1281
|
+
integrationName: id,
|
|
1282
|
+
config: entry.config,
|
|
1283
|
+
secrets: this.secrets.get(id)
|
|
1284
|
+
});
|
|
1285
|
+
return {
|
|
1286
|
+
id,
|
|
1287
|
+
name: manifest.name,
|
|
1288
|
+
healthy: hookResult.exitCode === 0,
|
|
1289
|
+
status: entry.status,
|
|
1290
|
+
version: manifest.version,
|
|
1291
|
+
message: hookResult.exitCode === 0 ? "Healthy" : `Health check failed (exit ${String(hookResult.exitCode)})`,
|
|
1292
|
+
stdout: hookResult.stdout || void 0,
|
|
1293
|
+
stderr: hookResult.stderr || void 0
|
|
1294
|
+
};
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
return {
|
|
1297
|
+
id,
|
|
1298
|
+
healthy: false,
|
|
1299
|
+
status: entry.status,
|
|
1300
|
+
version: entry.version,
|
|
1301
|
+
message: `Health check error: ${err instanceof Error ? err.message : String(err)}`
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
err(code, message) {
|
|
1306
|
+
return {
|
|
1307
|
+
ok: false,
|
|
1308
|
+
error: {
|
|
1309
|
+
code,
|
|
1310
|
+
message
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
//#endregion
|
|
1316
|
+
//#region src/adapter.ts
|
|
1317
|
+
var IntegrationManagerAdapter = class {
|
|
1318
|
+
constructor(manager) {
|
|
1319
|
+
this.manager = manager;
|
|
1320
|
+
}
|
|
1321
|
+
getInstalledIntegrations() {
|
|
1322
|
+
const integrations = this.manager.list();
|
|
1323
|
+
return Promise.resolve(integrations.map((i) => ({
|
|
1324
|
+
id: i.id,
|
|
1325
|
+
version: i.version ?? "unknown",
|
|
1326
|
+
status: i.status
|
|
1327
|
+
})));
|
|
1328
|
+
}
|
|
1329
|
+
async install(integrationId, version, config) {
|
|
1330
|
+
const result = await this.manager.install({
|
|
1331
|
+
name: integrationId,
|
|
1332
|
+
version,
|
|
1333
|
+
config
|
|
1334
|
+
});
|
|
1335
|
+
if (!result.ok) throw new Error(result.error?.message ?? `Failed to install ${integrationId}`);
|
|
1336
|
+
}
|
|
1337
|
+
async activate(integrationId) {
|
|
1338
|
+
const result = await this.manager.activate(integrationId);
|
|
1339
|
+
if (!result.ok) throw new Error(result.error?.message ?? `Failed to activate ${integrationId}`);
|
|
1340
|
+
}
|
|
1341
|
+
async deactivate(integrationId) {
|
|
1342
|
+
const result = await this.manager.deactivate(integrationId);
|
|
1343
|
+
if (!result.ok) throw new Error(result.error?.message ?? `Failed to deactivate ${integrationId}`);
|
|
1344
|
+
}
|
|
1345
|
+
async uninstall(integrationId) {
|
|
1346
|
+
const result = await this.manager.uninstall({ name: integrationId });
|
|
1347
|
+
if (!result.ok) throw new Error(result.error?.message ?? `Failed to uninstall ${integrationId}`);
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
//#endregion
|
|
1351
|
+
export { Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
|