@alfe.ai/integrations 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -604,20 +604,19 @@ interface OpenClawApplierOptions {
604
604
  workspace: string;
605
605
  /** Override skills directory (defaults to ~/.alfe/skills/) */
606
606
  skillsDir?: string;
607
- /** Path to the OpenClaw agent config file (defaults to {workspace}/config.json) */
607
+ /** Path to the integration tracking file (defaults to {workspace}/config.json) */
608
608
  configPath?: string;
609
609
  }
610
610
  declare class OpenClawApplier implements RuntimeApplier {
611
611
  readonly runtime = "openclaw";
612
612
  private workspace;
613
613
  private skillsDir;
614
- private configPath;
614
+ private trackingPath;
615
615
  constructor(options: OpenClawApplierOptions);
616
616
  applyPlugin(pkg: string): Promise<void>;
617
617
  /**
618
618
  * Ensure the plugin is in plugins.allow in openclaw.json.
619
- * When we skip `openclaw plugins install` (because the plugin dir exists),
620
- * the allowlist may not include the plugin — add it if missing.
619
+ * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
621
620
  */
622
621
  private ensurePluginsAllow;
623
622
  /**
@@ -629,29 +628,22 @@ declare class OpenClawApplier implements RuntimeApplier {
629
628
  applySkill(name: string, srcPath: string): Promise<void>;
630
629
  removeSkill(name: string): Promise<void>;
631
630
  /**
632
- * Deep-merge integration config into the OpenClaw agent config file.
631
+ * Apply integration config to the OpenClaw runtime via `openclaw config set`.
633
632
  *
634
- * Each integration's config contribution is tracked in
635
- * `_integrations.{integrationId}` within the config file so it can be
636
- * cleanly removed later.
633
+ * Each integration's config contribution is tracked in the tracking file
634
+ * (config.json) so it can be cleanly removed later.
637
635
  */
638
636
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
639
637
  /**
640
638
  * Remove config previously applied by an integration.
641
639
  *
642
- * Rebuilds the config by re-merging all remaining integrations' configs,
643
- * ensuring clean removal without orphaned keys.
640
+ * Reads the tracking file to find which config keys this integration set,
641
+ * then removes them via `openclaw config unset`.
644
642
  */
645
643
  removeConfig(integrationId: string): Promise<void>;
646
644
  isAvailable(): Promise<boolean>;
647
- private readConfig;
648
- private writeConfig;
649
- /**
650
- * Extract the base config by stripping all keys that were contributed
651
- * by integrations. This is done by removing each integration's keys
652
- * from the current config.
653
- */
654
- private getBaseConfig;
645
+ private readTracking;
646
+ private writeTracking;
655
647
  }
656
648
  //#endregion
657
649
  //#region src/lock.d.ts
package/dist/index.js CHANGED
@@ -893,6 +893,7 @@ var IntegrationManager = class {
893
893
  }
894
894
  if (plugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, plugins, skills, installPath);
895
895
  }
896
+ this.state.setStatus(integrationId, "active");
896
897
  if (manifest.hooks.health_check) {
897
898
  this.log.info(`Running health check: ${manifest.hooks.health_check}`);
898
899
  const hookResult = await runHookWithContext(installPath, manifest.hooks.health_check, {
@@ -905,7 +906,6 @@ var IntegrationManager = class {
905
906
  return this.err("HEALTH_CHECK_FAILED", `Health check failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
906
907
  }
907
908
  }
908
- this.state.setStatus(integrationId, "active");
909
909
  this.log.info(`Integration "${integrationId}" activated`);
910
910
  return {
911
911
  ok: true,
@@ -1178,67 +1178,82 @@ var IntegrationManager = class {
1178
1178
  /**
1179
1179
  * OpenClawApplier — applies plugins, skills, and config to the OpenClaw runtime.
1180
1180
  *
1181
- * Plugins are installed via `npm install` in the OpenClaw workspace directory.
1181
+ * Plugins are installed via `openclaw plugins install`.
1182
1182
  * Skills are copied to ~/.alfe/skills/{name}.
1183
- * Config is deep-merged into the OpenClaw agent config, with per-integration
1184
- * tracking so changes can be cleanly removed on deactivation.
1183
+ * Config is applied via `openclaw config set` so OpenClaw manages its own
1184
+ * config file (openclaw.json) without clobbering. Per-integration tracking
1185
+ * is stored in a separate tracking file (config.json) for clean removal.
1185
1186
  */
1186
1187
  const execFileAsync = promisify(execFile);
1188
+ const log = createLogger("OpenClawApplier");
1187
1189
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
1188
1190
  /**
1189
- * Deep-merge source into target, returning a new object.
1190
- * Arrays are replaced, not concatenated.
1191
+ * Flatten a nested config object into dot-path key-value pairs.
1192
+ * e.g. { gateway: { bind: "lan" } } → [["gateway.bind", "lan"]]
1191
1193
  */
1192
- function deepMerge(target, source) {
1193
- const result = { ...target };
1194
- for (const key of Object.keys(source)) {
1195
- const srcVal = source[key];
1196
- const tgtVal = result[key];
1197
- if (srcVal !== null && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal !== null && typeof tgtVal === "object" && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
1198
- else result[key] = srcVal;
1199
- }
1200
- return result;
1194
+ function flattenConfig(obj, prefix = "") {
1195
+ const pairs = [];
1196
+ for (const [key, val] of Object.entries(obj)) {
1197
+ const path = prefix ? `${prefix}.${key}` : key;
1198
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) pairs.push(...flattenConfig(val, path));
1199
+ else pairs.push([path, val]);
1200
+ }
1201
+ return pairs;
1201
1202
  }
1202
1203
  var OpenClawApplier = class {
1203
1204
  runtime = "openclaw";
1204
1205
  workspace;
1205
1206
  skillsDir;
1206
- configPath;
1207
+ trackingPath;
1207
1208
  constructor(options) {
1208
1209
  this.workspace = options.workspace;
1209
1210
  this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
1210
- this.configPath = options.configPath ?? join(this.workspace, "config.json");
1211
+ this.trackingPath = options.configPath ?? join(this.workspace, "config.json");
1211
1212
  }
1212
1213
  async applyPlugin(pkg) {
1213
- if (this.isPluginInstalled(pkg)) {
1214
- this.ensurePluginsAllow(pkg);
1215
- return;
1214
+ if (!this.isPluginInstalled(pkg)) {
1215
+ await execFileAsync("openclaw", [
1216
+ "plugins",
1217
+ "install",
1218
+ pkg
1219
+ ], { timeout: 6e4 });
1220
+ await new Promise((r) => {
1221
+ setTimeout(r, 500);
1222
+ });
1216
1223
  }
1217
- await execFileAsync("openclaw", [
1218
- "plugins",
1219
- "install",
1220
- pkg
1221
- ], { timeout: 6e4 });
1224
+ await this.ensurePluginsAllow(pkg);
1222
1225
  }
1223
1226
  /**
1224
1227
  * Ensure the plugin is in plugins.allow in openclaw.json.
1225
- * When we skip `openclaw plugins install` (because the plugin dir exists),
1226
- * the allowlist may not include the plugin — add it if missing.
1228
+ * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
1227
1229
  */
1228
- ensurePluginsAllow(pkg) {
1229
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
1230
- if (!existsSync(configPath)) return;
1230
+ async ensurePluginsAllow(pkg) {
1231
+ let currentAllow = [];
1231
1232
  try {
1232
- const raw = readFileSync(configPath, "utf-8");
1233
- const config = JSON.parse(raw);
1234
- const plugins = config.plugins ?? {};
1235
- const allow = plugins.allow ?? [];
1236
- if (!allow.includes(pkg)) {
1237
- plugins.allow = [...allow, pkg];
1238
- config.plugins = plugins;
1239
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1240
- }
1233
+ const { stdout } = await execFileAsync("openclaw", [
1234
+ "config",
1235
+ "get",
1236
+ "plugins.allow"
1237
+ ], { timeout: 1e4 });
1238
+ const parsed = JSON.parse(stdout.trim());
1239
+ if (Array.isArray(parsed)) currentAllow = parsed;
1241
1240
  } catch {}
1241
+ if (currentAllow.includes(pkg)) return;
1242
+ const updated = [...currentAllow, pkg];
1243
+ try {
1244
+ await execFileAsync("openclaw", [
1245
+ "config",
1246
+ "set",
1247
+ "plugins.allow",
1248
+ JSON.stringify(updated),
1249
+ "--strict-json"
1250
+ ], { timeout: 1e4 });
1251
+ } catch (err) {
1252
+ log.warn({
1253
+ err: err instanceof Error ? err.message : String(err),
1254
+ pkg
1255
+ }, "Failed to set plugins.allow via openclaw config set");
1256
+ }
1242
1257
  }
1243
1258
  /**
1244
1259
  * Check if a plugin is already installed in ~/.openclaw/extensions/.
@@ -1273,69 +1288,76 @@ var OpenClawApplier = class {
1273
1288
  return Promise.resolve();
1274
1289
  }
1275
1290
  /**
1276
- * Deep-merge integration config into the OpenClaw agent config file.
1291
+ * Apply integration config to the OpenClaw runtime via `openclaw config set`.
1277
1292
  *
1278
- * Each integration's config contribution is tracked in
1279
- * `_integrations.{integrationId}` within the config file so it can be
1280
- * cleanly removed later.
1293
+ * Each integration's config contribution is tracked in the tracking file
1294
+ * (config.json) so it can be cleanly removed later.
1281
1295
  */
1282
- applyConfig(integrationId, config) {
1283
- const current = this.readConfig();
1284
- const integrations = current._integrations ?? {};
1296
+ async applyConfig(integrationId, config) {
1297
+ const tracking = this.readTracking();
1298
+ const integrations = tracking._integrations ?? {};
1285
1299
  integrations[integrationId] = config;
1286
- current._integrations = integrations;
1287
- const merged = deepMerge(current, config);
1288
- merged._integrations = current._integrations;
1289
- this.writeConfig(merged);
1290
- return Promise.resolve();
1300
+ tracking._integrations = integrations;
1301
+ this.writeTracking(tracking);
1302
+ const pairs = flattenConfig(config);
1303
+ for (const [path, value] of pairs) try {
1304
+ await execFileAsync("openclaw", [
1305
+ "config",
1306
+ "set",
1307
+ path,
1308
+ JSON.stringify(value),
1309
+ "--strict-json"
1310
+ ], { timeout: 1e4 });
1311
+ } catch (err) {
1312
+ log.error({
1313
+ err: err instanceof Error ? err.message : String(err),
1314
+ path,
1315
+ value
1316
+ }, "Failed to set config via openclaw config set");
1317
+ throw err;
1318
+ }
1291
1319
  }
1292
1320
  /**
1293
1321
  * Remove config previously applied by an integration.
1294
1322
  *
1295
- * Rebuilds the config by re-merging all remaining integrations' configs,
1296
- * ensuring clean removal without orphaned keys.
1323
+ * Reads the tracking file to find which config keys this integration set,
1324
+ * then removes them via `openclaw config unset`.
1297
1325
  */
1298
- removeConfig(integrationId) {
1299
- const current = this.readConfig();
1300
- const integrations = current._integrations ?? {};
1301
- if (!(integrationId in integrations)) return Promise.resolve();
1302
- const remainingIntegrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1303
- let rebuilt = this.getBaseConfig(current);
1304
- for (const cfg of Object.values(remainingIntegrations)) rebuilt = deepMerge(rebuilt, cfg);
1305
- rebuilt._integrations = remainingIntegrations;
1306
- this.writeConfig(rebuilt);
1307
- return Promise.resolve();
1326
+ async removeConfig(integrationId) {
1327
+ const tracking = this.readTracking();
1328
+ const integrations = tracking._integrations ?? {};
1329
+ if (!(integrationId in integrations)) return;
1330
+ const integrationConfig = integrations[integrationId];
1331
+ const pairs = flattenConfig(integrationConfig);
1332
+ for (const [path] of pairs) try {
1333
+ await execFileAsync("openclaw", [
1334
+ "config",
1335
+ "unset",
1336
+ path
1337
+ ], { timeout: 1e4 });
1338
+ } catch (err) {
1339
+ log.warn({
1340
+ err: err instanceof Error ? err.message : String(err),
1341
+ path
1342
+ }, "Failed to unset config via openclaw config unset");
1343
+ }
1344
+ tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1345
+ this.writeTracking(tracking);
1308
1346
  }
1309
1347
  isAvailable() {
1310
1348
  return Promise.resolve(existsSync(this.workspace));
1311
1349
  }
1312
- readConfig() {
1313
- if (!existsSync(this.configPath)) return {};
1350
+ readTracking() {
1351
+ if (!existsSync(this.trackingPath)) return {};
1314
1352
  try {
1315
- return JSON.parse(readFileSync(this.configPath, "utf-8"));
1353
+ return JSON.parse(readFileSync(this.trackingPath, "utf-8"));
1316
1354
  } catch {
1317
1355
  return {};
1318
1356
  }
1319
1357
  }
1320
- writeConfig(config) {
1321
- mkdirSync(join(this.configPath, ".."), { recursive: true });
1322
- writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1323
- }
1324
- /**
1325
- * Extract the base config by stripping all keys that were contributed
1326
- * by integrations. This is done by removing each integration's keys
1327
- * from the current config.
1328
- */
1329
- getBaseConfig(current) {
1330
- const integrations = current._integrations ?? {};
1331
- const allIntegrationKeys = /* @__PURE__ */ new Set();
1332
- for (const cfg of Object.values(integrations)) for (const key of Object.keys(cfg)) allIntegrationKeys.add(key);
1333
- const base = {};
1334
- for (const [key, val] of Object.entries(current)) {
1335
- if (key === "_integrations") continue;
1336
- if (!allIntegrationKeys.has(key)) base[key] = val;
1337
- }
1338
- return base;
1358
+ writeTracking(config) {
1359
+ mkdirSync(join(this.trackingPath, ".."), { recursive: true });
1360
+ writeFileSync(this.trackingPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1339
1361
  }
1340
1362
  };
1341
1363
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",