@alfe.ai/integrations 0.0.7 → 0.0.9

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,16 +604,21 @@ 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
+ /**
618
+ * Ensure the plugin is in plugins.allow in openclaw.json.
619
+ * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
620
+ */
621
+ private ensurePluginsAllow;
617
622
  /**
618
623
  * Check if a plugin is already installed in ~/.openclaw/extensions/.
619
624
  * OpenClaw names extension dirs as {pkg-name-with-dashes}-{hash}.
@@ -623,29 +628,22 @@ declare class OpenClawApplier implements RuntimeApplier {
623
628
  applySkill(name: string, srcPath: string): Promise<void>;
624
629
  removeSkill(name: string): Promise<void>;
625
630
  /**
626
- * Deep-merge integration config into the OpenClaw agent config file.
631
+ * Apply integration config to the OpenClaw runtime via `openclaw config set`.
627
632
  *
628
- * Each integration's config contribution is tracked in
629
- * `_integrations.{integrationId}` within the config file so it can be
630
- * 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.
631
635
  */
632
636
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
633
637
  /**
634
638
  * Remove config previously applied by an integration.
635
639
  *
636
- * Rebuilds the config by re-merging all remaining integrations' configs,
637
- * 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`.
638
642
  */
639
643
  removeConfig(integrationId: string): Promise<void>;
640
644
  isAvailable(): Promise<boolean>;
641
- private readConfig;
642
- private writeConfig;
643
- /**
644
- * Extract the base config by stripping all keys that were contributed
645
- * by integrations. This is done by removing each integration's keys
646
- * from the current config.
647
- */
648
- private getBaseConfig;
645
+ private readTracking;
646
+ private writeTracking;
649
647
  }
650
648
  //#endregion
651
649
  //#region src/lock.d.ts
package/dist/index.js CHANGED
@@ -662,6 +662,21 @@ async function runHookWithContext(integrationPath, hookScript, options) {
662
662
  * plugins/skills to runtimes. deactivate() removes them.
663
663
  */
664
664
  /**
665
+ * Deep-walk an object and replace `{{config.KEY}}` patterns with values
666
+ * from the per-agent config. Used to compose manifest runtime config
667
+ * with per-agent secrets (e.g., gateway token).
668
+ */
669
+ function interpolateSelfConfig(obj, config) {
670
+ const result = {};
671
+ for (const [key, value] of Object.entries(obj)) if (typeof value === "string") result[key] = value.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_match, configKey) => {
672
+ const val = config[configKey];
673
+ return typeof val === "string" || typeof val === "number" ? String(val) : _match;
674
+ });
675
+ else if (value && typeof value === "object" && !Array.isArray(value)) result[key] = interpolateSelfConfig(value, config);
676
+ else result[key] = value;
677
+ return result;
678
+ }
679
+ /**
665
680
  * Merge universal installs with runtime-specific installs from the manifest.
666
681
  */
667
682
  function resolveInstallsForRuntime(manifest, runtime) {
@@ -871,8 +886,10 @@ var IntegrationManager = class {
871
886
  await applier.applySkill(skillName, srcPath);
872
887
  }
873
888
  if (runtimeConfig && Object.keys(runtimeConfig).length > 0) {
889
+ const agentConfig = entry.config;
890
+ const interpolatedConfig = interpolateSelfConfig(runtimeConfig, agentConfig);
874
891
  this.log.info(`Applying config for ${integrationId} to ${runtimeName}`);
875
- await applier.applyConfig(integrationId, runtimeConfig);
892
+ await applier.applyConfig(integrationId, interpolatedConfig);
876
893
  }
877
894
  if (plugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, plugins, skills, installPath);
878
895
  }
@@ -1161,44 +1178,82 @@ var IntegrationManager = class {
1161
1178
  /**
1162
1179
  * OpenClawApplier — applies plugins, skills, and config to the OpenClaw runtime.
1163
1180
  *
1164
- * Plugins are installed via `npm install` in the OpenClaw workspace directory.
1181
+ * Plugins are installed via `openclaw plugins install`.
1165
1182
  * Skills are copied to ~/.alfe/skills/{name}.
1166
- * Config is deep-merged into the OpenClaw agent config, with per-integration
1167
- * 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.
1168
1186
  */
1169
1187
  const execFileAsync = promisify(execFile);
1188
+ const log = createLogger("OpenClawApplier");
1170
1189
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
1171
1190
  /**
1172
- * Deep-merge source into target, returning a new object.
1173
- * 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"]]
1174
1193
  */
1175
- function deepMerge(target, source) {
1176
- const result = { ...target };
1177
- for (const key of Object.keys(source)) {
1178
- const srcVal = source[key];
1179
- const tgtVal = result[key];
1180
- if (srcVal !== null && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal !== null && typeof tgtVal === "object" && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
1181
- else result[key] = srcVal;
1182
- }
1183
- 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;
1184
1202
  }
1185
1203
  var OpenClawApplier = class {
1186
1204
  runtime = "openclaw";
1187
1205
  workspace;
1188
1206
  skillsDir;
1189
- configPath;
1207
+ trackingPath;
1190
1208
  constructor(options) {
1191
1209
  this.workspace = options.workspace;
1192
1210
  this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
1193
- this.configPath = options.configPath ?? join(this.workspace, "config.json");
1211
+ this.trackingPath = options.configPath ?? join(this.workspace, "config.json");
1194
1212
  }
1195
1213
  async applyPlugin(pkg) {
1196
- if (this.isPluginInstalled(pkg)) return;
1197
- await execFileAsync("openclaw", [
1198
- "plugins",
1199
- "install",
1200
- pkg
1201
- ], { timeout: 6e4 });
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
+ });
1223
+ }
1224
+ await this.ensurePluginsAllow(pkg);
1225
+ }
1226
+ /**
1227
+ * Ensure the plugin is in plugins.allow in openclaw.json.
1228
+ * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
1229
+ */
1230
+ async ensurePluginsAllow(pkg) {
1231
+ let currentAllow = [];
1232
+ try {
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;
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
+ }
1202
1257
  }
1203
1258
  /**
1204
1259
  * Check if a plugin is already installed in ~/.openclaw/extensions/.
@@ -1207,7 +1262,7 @@ var OpenClawApplier = class {
1207
1262
  isPluginInstalled(pkg) {
1208
1263
  const extensionsDir = join(homedir(), ".openclaw", "extensions");
1209
1264
  if (!existsSync(extensionsDir)) return false;
1210
- const prefix = pkg.replaceAll("/", "-").replaceAll("@", "");
1265
+ const prefix = pkg.replaceAll("/", "-");
1211
1266
  return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix));
1212
1267
  }
1213
1268
  async removePlugin(pkg) {
@@ -1233,69 +1288,76 @@ var OpenClawApplier = class {
1233
1288
  return Promise.resolve();
1234
1289
  }
1235
1290
  /**
1236
- * Deep-merge integration config into the OpenClaw agent config file.
1291
+ * Apply integration config to the OpenClaw runtime via `openclaw config set`.
1237
1292
  *
1238
- * Each integration's config contribution is tracked in
1239
- * `_integrations.{integrationId}` within the config file so it can be
1240
- * 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.
1241
1295
  */
1242
- applyConfig(integrationId, config) {
1243
- const current = this.readConfig();
1244
- const integrations = current._integrations ?? {};
1296
+ async applyConfig(integrationId, config) {
1297
+ const tracking = this.readTracking();
1298
+ const integrations = tracking._integrations ?? {};
1245
1299
  integrations[integrationId] = config;
1246
- current._integrations = integrations;
1247
- const merged = deepMerge(current, config);
1248
- merged._integrations = current._integrations;
1249
- this.writeConfig(merged);
1250
- 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
+ }
1251
1319
  }
1252
1320
  /**
1253
1321
  * Remove config previously applied by an integration.
1254
1322
  *
1255
- * Rebuilds the config by re-merging all remaining integrations' configs,
1256
- * 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`.
1257
1325
  */
1258
- removeConfig(integrationId) {
1259
- const current = this.readConfig();
1260
- const integrations = current._integrations ?? {};
1261
- if (!(integrationId in integrations)) return Promise.resolve();
1262
- const remainingIntegrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1263
- let rebuilt = this.getBaseConfig(current);
1264
- for (const cfg of Object.values(remainingIntegrations)) rebuilt = deepMerge(rebuilt, cfg);
1265
- rebuilt._integrations = remainingIntegrations;
1266
- this.writeConfig(rebuilt);
1267
- 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);
1268
1346
  }
1269
1347
  isAvailable() {
1270
1348
  return Promise.resolve(existsSync(this.workspace));
1271
1349
  }
1272
- readConfig() {
1273
- if (!existsSync(this.configPath)) return {};
1350
+ readTracking() {
1351
+ if (!existsSync(this.trackingPath)) return {};
1274
1352
  try {
1275
- return JSON.parse(readFileSync(this.configPath, "utf-8"));
1353
+ return JSON.parse(readFileSync(this.trackingPath, "utf-8"));
1276
1354
  } catch {
1277
1355
  return {};
1278
1356
  }
1279
1357
  }
1280
- writeConfig(config) {
1281
- mkdirSync(join(this.configPath, ".."), { recursive: true });
1282
- writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1283
- }
1284
- /**
1285
- * Extract the base config by stripping all keys that were contributed
1286
- * by integrations. This is done by removing each integration's keys
1287
- * from the current config.
1288
- */
1289
- getBaseConfig(current) {
1290
- const integrations = current._integrations ?? {};
1291
- const allIntegrationKeys = /* @__PURE__ */ new Set();
1292
- for (const cfg of Object.values(integrations)) for (const key of Object.keys(cfg)) allIntegrationKeys.add(key);
1293
- const base = {};
1294
- for (const [key, val] of Object.entries(current)) {
1295
- if (key === "_integrations") continue;
1296
- if (!allIntegrationKeys.has(key)) base[key] = val;
1297
- }
1298
- return base;
1358
+ writeTracking(config) {
1359
+ mkdirSync(join(this.trackingPath, ".."), { recursive: true });
1360
+ writeFileSync(this.trackingPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1299
1361
  }
1300
1362
  };
1301
1363
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",