loopengine 0.1.15 → 0.1.24

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.
Files changed (42) hide show
  1. package/README.md +124 -299
  2. package/dist/bin/{package-manager.d.ts → ability-manager.d.ts} +39 -33
  3. package/dist/bin/{package-manager.js → ability-manager.js} +85 -74
  4. package/dist/bin/cli.js +22 -22
  5. package/dist/core/agent-config.d.ts +1 -1
  6. package/dist/core/gateway-tools.js +4 -4
  7. package/dist/core/mcpplug.d.ts +23 -0
  8. package/dist/core/mcpplug.js +64 -0
  9. package/dist/core/run-agent.js +2 -2
  10. package/dist/core/session-store.d.ts +1 -1
  11. package/dist/core/session-store.js +1 -1
  12. package/dist/core/sessionknit.d.ts +87 -0
  13. package/dist/core/sessionknit.js +189 -0
  14. package/dist/core/skillgarden/activation.d.ts +4 -0
  15. package/dist/core/skillgarden/activation.js +16 -0
  16. package/dist/core/skillgarden/budget.d.ts +20 -0
  17. package/dist/core/skillgarden/budget.js +28 -0
  18. package/dist/core/skillgarden/discovery.d.ts +11 -0
  19. package/dist/core/skillgarden/discovery.js +40 -0
  20. package/dist/core/skillgarden/frontmatter.d.ts +10 -0
  21. package/dist/core/skillgarden/frontmatter.js +35 -0
  22. package/dist/core/skillgarden/index.d.ts +8 -0
  23. package/dist/core/skillgarden/index.js +12 -0
  24. package/dist/core/skillgarden/loader.d.ts +31 -0
  25. package/dist/core/skillgarden/loader.js +70 -0
  26. package/dist/core/skillgarden/substitute.d.ts +4 -0
  27. package/dist/core/skillgarden/substitute.js +13 -0
  28. package/dist/core/skillgarden/types.d.ts +23 -0
  29. package/dist/core/skillgarden/types.js +1 -0
  30. package/dist/core/toollane.d.ts +46 -0
  31. package/dist/core/toollane.js +69 -0
  32. package/dist/index.d.ts +8 -2
  33. package/dist/index.js +4 -1
  34. package/dist/web/agents-config-page.js +7 -203
  35. package/dist/web/env-admin.d.ts +6 -6
  36. package/dist/web/env-admin.js +11 -11
  37. package/dist/web/http-tool-admin.d.ts +2 -2
  38. package/dist/web/http-tool-admin.js +4 -4
  39. package/dist/web/skills-admin.js +1 -1
  40. package/package.json +10 -7
  41. package/dist/web/skillgarden-admin.d.ts +0 -24
  42. package/dist/web/skillgarden-admin.js +0 -76
@@ -1,8 +1,8 @@
1
- // Backs `loopengine add-package|upgrade-package|remove-package` (see
2
- // bin/cli.ts) — the install/upgrade/remove mechanics PACKAGES.md
3
- // specifies for a "loopengine package": a bundle of tool files, skill
1
+ // Backs `loopengine add-ability|upgrade-ability|remove-ability` (see
2
+ // bin/cli.ts) — the install/upgrade/remove mechanics ABILITIES.md
3
+ // specifies for a "loopengine ability": a bundle of tool files, skill
4
4
  // directories, and actauth rules, installed by *copying* files into an
5
- // agent's own tree (never an npm import) so a package's code is exactly
5
+ // agent's own tree (never an npm import) so an ability's code is exactly
6
6
  // as reviewable/hand-editable as anything the Admin UI's HTTP tool
7
7
  // builder already generates (see web/http-tool-admin.ts's own header
8
8
  // comment on `generateToolCode` for that same "real code, not an opaque
@@ -14,7 +14,7 @@
14
14
  // `threeWayMerge`'s `git merge-file --diff3` — since `loopengine` and
15
15
  // `create-loopengine` are separate published packages and a runtime
16
16
  // depending on a scaffolding tool (or vice versa) isn't a dependency
17
- // direction worth introducing to share ~30 lines (see PACKAGES.md's
17
+ // direction worth introducing to share ~30 lines (see ABILITIES.md's
18
18
  // "Upgrading" section).
19
19
  import { createHash } from 'node:crypto';
20
20
  import { execFileSync } from 'node:child_process';
@@ -26,29 +26,29 @@ import semver from 'semver';
26
26
  import { agentDir } from '../core/gateway-tools.js';
27
27
  import { addToolToIndex, removeToolFromIndex, toCamelCase } from '../web/http-tool-admin.js';
28
28
  import { addActauthRule, updateActauthRule, removeActauthRule, readActauthConfig } from '../web/actauth-admin.js';
29
- export class PackageManifestError extends Error {
29
+ export class AbilityManifestError extends Error {
30
30
  }
31
- export class PackageVersionError extends Error {
31
+ export class AbilityVersionError extends Error {
32
32
  }
33
- export class PackageCollisionError extends Error {
33
+ export class AbilityCollisionError extends Error {
34
34
  }
35
- export class PackageNotInstalledError extends Error {
35
+ export class AbilityNotInstalledError extends Error {
36
36
  }
37
- export class PackageAlreadyInstalledError extends Error {
37
+ export class AbilityAlreadyInstalledError extends Error {
38
38
  }
39
39
  // Same character sets tool names / skill ids are already validated
40
40
  // against elsewhere (web/http-tool-admin.ts's TOOL_NAME_PATTERN,
41
41
  // web/skills-admin.ts's SKILL_ID_PATTERN) — re-checked here because a
42
- // name derived from an untrusted package's own manifest becomes a path
42
+ // name derived from an untrusted ability's own manifest becomes a path
43
43
  // segment (agents/<agent>/tools/<name>.ts, .../skills/<id>/): without
44
- // this, a package declaring a skill dir literally named ".." would have
44
+ // this, an ability declaring a skill dir literally named ".." would have
45
45
  // `basename('..')` hand back `'..'` unchanged (path.basename does not
46
46
  // strip it), and the later `cpSync` would land one directory up, inside
47
47
  // the agent's own root instead of its skills/ folder.
48
48
  const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
49
49
  const SKILL_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
50
50
  function provenancePath(agentName) {
51
- return join(agentDir(agentName), '.loopengine-packages.json');
51
+ return join(agentDir(agentName), '.loopengine-abilities.json');
52
52
  }
53
53
  function readProvenance(agentName) {
54
54
  const path = provenancePath(agentName);
@@ -69,10 +69,10 @@ function sha256(content) {
69
69
  * accepts anything `npm pack` does: a public/private registry spec, a
70
70
  * git URL (`github:org/repo`, `git+ssh://...`), or a local `file:../path`
71
71
  * — the last of which is what this module's own tests point at, so no
72
- * real network call is needed there; see PACKAGES.md's "Publishing a
73
- * package" section). */
74
- export function fetchPackageDir(spec) {
75
- const workDir = mkdtempSync(join(tmpdir(), 'loopengine-package-'));
72
+ * real network call is needed there; see ABILITIES.md's "Publishing an
73
+ * ability" section). */
74
+ export function fetchAbilityDir(spec) {
75
+ const workDir = mkdtempSync(join(tmpdir(), 'loopengine-ability-'));
76
76
  execFileSync('npm', ['pack', spec, '--pack-destination', workDir], { stdio: 'pipe' });
77
77
  const tarball = readdirSync(workDir).find((f) => f.endsWith('.tgz'));
78
78
  if (!tarball) {
@@ -85,22 +85,33 @@ export function fetchPackageDir(spec) {
85
85
  execFileSync('tar', ['-xzf', join(workDir, tarball), '-C', workDir], { stdio: 'pipe' });
86
86
  return join(workDir, 'package');
87
87
  }
88
- function readManifest(packageDir) {
89
- const manifestPath = join(packageDir, 'loopengine.package.json');
88
+ function readManifest(abilityDir) {
89
+ const manifestPath = join(abilityDir, 'loopengine.ability.json');
90
90
  if (!existsSync(manifestPath)) {
91
- throw new PackageManifestError(`${packageDir} has no loopengine.package.json — not a valid loopengine package.`);
91
+ throw new AbilityManifestError(`${abilityDir} has no loopengine.ability.json — not a valid loopengine ability.`);
92
92
  }
93
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
94
- if (!manifest.name || !manifest.version || !manifest.loopengineVersion) {
95
- throw new PackageManifestError('loopengine.package.json must have "name", "version", and "loopengineVersion".');
93
+ const declared = JSON.parse(readFileSync(manifestPath, 'utf8'));
94
+ if (!declared.loopengineVersion) {
95
+ throw new AbilityManifestError('loopengine.ability.json must have "loopengineVersion".');
96
96
  }
97
- return manifest;
97
+ // name/version come from package.json, not loopengine.ability.json —
98
+ // see AbilityManifest's own doc comment for why duplicating them here
99
+ // would just be two numbers to keep in sync instead of one.
100
+ const pkgJsonPath = join(abilityDir, 'package.json');
101
+ if (!existsSync(pkgJsonPath)) {
102
+ throw new AbilityManifestError(`${abilityDir} has no package.json — every loopengine ability needs one (name/version), even though its own metadata lives in loopengine.ability.json.`);
103
+ }
104
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
105
+ if (!pkgJson.name || !pkgJson.version) {
106
+ throw new AbilityManifestError(`${pkgJsonPath} must have "name" and "version".`);
107
+ }
108
+ return { ...declared, name: pkgJson.name, version: pkgJson.version };
98
109
  }
99
110
  // The installing project's own dependencies.loopengine is itself a
100
111
  // range (e.g. "^0.1.10"), not a concrete installed version — checking
101
112
  // the *floor* of that range against the manifest's required range is
102
113
  // the conservative choice: if even the lowest version the range could
103
- // resolve to wouldn't satisfy the package, refuse now rather than risk
114
+ // resolve to wouldn't satisfy the ability, refuse now rather than risk
104
115
  // the exact silent-failure gap the Parallel-safe checkbox already hit
105
116
  // (a feature that imports an export an *actually installed* older
106
117
  // version doesn't have yet, with no error, just quietly not working).
@@ -131,7 +142,7 @@ function checkLoopengineVersion(manifest, installedRangeOverride) {
131
142
  return;
132
143
  installedRange = ownPkg.dependencies?.loopengine;
133
144
  if (!installedRange) {
134
- throw new PackageVersionError(`This project's package.json has no "loopengine" dependency — can't check compatibility.`);
145
+ throw new AbilityVersionError(`This project's package.json has no "loopengine" dependency — can't check compatibility.`);
135
146
  }
136
147
  }
137
148
  // A non-registry dependency specifier (file:, git:, workspace:, ...) —
@@ -149,19 +160,19 @@ function checkLoopengineVersion(manifest, installedRangeOverride) {
149
160
  return;
150
161
  }
151
162
  if (!floor || !semver.satisfies(floor, manifest.loopengineVersion)) {
152
- throw new PackageVersionError(`Package '${manifest.name}' needs loopengine ${manifest.loopengineVersion}, but this project depends on loopengine ${installedRange} — bump the dependency first.`);
163
+ throw new AbilityVersionError(`Ability '${manifest.name}' needs loopengine ${manifest.loopengineVersion}, but this project depends on loopengine ${installedRange} — bump the dependency first.`);
153
164
  }
154
165
  }
155
- function parseActauthRules(packageDir, manifest) {
166
+ function parseActauthRules(abilityDir, manifest) {
156
167
  if (!manifest.actauth)
157
168
  return [];
158
- const rulesPath = join(packageDir, manifest.actauth);
169
+ const rulesPath = join(abilityDir, manifest.actauth);
159
170
  if (!existsSync(rulesPath)) {
160
- throw new PackageManifestError(`Manifest references actauth file '${manifest.actauth}', which doesn't exist in the package.`);
171
+ throw new AbilityManifestError(`Manifest references actauth file '${manifest.actauth}', which doesn't exist in the ability.`);
161
172
  }
162
173
  const parsed = parseYaml(readFileSync(rulesPath, 'utf8'));
163
174
  if (!Array.isArray(parsed)) {
164
- throw new PackageManifestError(`'${manifest.actauth}' must be a YAML array of {name, scope, tool, decision} rules.`);
175
+ throw new AbilityManifestError(`'${manifest.actauth}' must be a YAML array of {name, scope, tool, decision} rules.`);
165
176
  }
166
177
  return parsed;
167
178
  }
@@ -171,7 +182,7 @@ function parseActauthRules(packageDir, manifest) {
171
182
  * (reusing addToolToIndex exactly as web/http-tool-admin.ts's own
172
183
  * createHttpTool does), copies each skill directory verbatim (a raw
173
184
  * cpSync, not skills-admin.ts's writeSkill — that regenerates
174
- * frontmatter from scratch and can't carry a package's own
185
+ * frontmatter from scratch and can't carry an ability's own
175
186
  * frontmatter/assets), and appends each actauth rule (addActauthRule).
176
187
  *
177
188
  * Deliberately does *not* attempt a live in-memory registry splice the
@@ -179,7 +190,7 @@ function parseActauthRules(packageDir, manifest) {
179
190
  * admin-created tool — that works there because the HTTP request
180
191
  * creating the tool runs *inside the same process* as the already-running
181
192
  * server, so updateAgent's mutation is visible to every later request in
182
- * that same process immediately. `add-package` is a separate, one-off
193
+ * that same process immediately. `add-ability` is a separate, one-off
183
194
  * CLI process with no connection to whatever server might be running
184
195
  * elsewhere — even a successful getEntry/updateAgent call here would
185
196
  * only mutate *this* CLI invocation's own throwaway registry, then
@@ -196,50 +207,50 @@ function parseActauthRules(packageDir, manifest) {
196
207
  *
197
208
  * All-or-nothing: every collision check below runs, and the whole
198
209
  * install is refused, before a single file is written. */
199
- export async function installPackage(agentName, spec, options = {}) {
200
- const fetch = options.fetchPackageDir ?? fetchPackageDir;
201
- const packageDir = fetch(spec);
202
- const manifest = readManifest(packageDir);
210
+ export async function installAbility(agentName, spec, options = {}) {
211
+ const fetch = options.fetchAbilityDir ?? fetchAbilityDir;
212
+ const abilityDir = fetch(spec);
213
+ const manifest = readManifest(abilityDir);
203
214
  checkLoopengineVersion(manifest, options.installedLoopengineRange);
204
215
  const provenance = readProvenance(agentName);
205
216
  if (provenance[manifest.name]) {
206
- throw new PackageAlreadyInstalledError(`Package '${manifest.name}' is already installed for '${agentName}' — use upgrade-package instead.`);
217
+ throw new AbilityAlreadyInstalledError(`Ability '${manifest.name}' is already installed for '${agentName}' — use upgrade-ability instead.`);
207
218
  }
208
219
  const toolFiles = manifest.tools ?? [];
209
220
  const skillDirs = manifest.skills ?? [];
210
- const rules = parseActauthRules(packageDir, manifest);
221
+ const rules = parseActauthRules(abilityDir, manifest);
211
222
  const toolsDir = join(agentDir(agentName), 'tools');
212
223
  const skillsDirPath = join(agentDir(agentName), 'skills');
213
224
  // Collision checks — refuse the whole install before writing anything,
214
225
  // same "refuse rather than guess" rule HttpToolExistsError/
215
226
  // HttpToolIndexShapeError already enforce for a single admin-created
216
- // tool, just applied package-wide.
227
+ // tool, just applied ability-wide.
217
228
  const toolNames = [];
218
229
  for (const toolFile of toolFiles) {
219
230
  const toolName = basename(toolFile, '.ts');
220
231
  if (!TOOL_NAME_PATTERN.test(toolName)) {
221
- throw new PackageManifestError(`Tool file '${toolFile}' doesn't name a valid tool (must be lowercase snake_case).`);
232
+ throw new AbilityManifestError(`Tool file '${toolFile}' doesn't name a valid tool (must be lowercase snake_case).`);
222
233
  }
223
234
  toolNames.push(toolName);
224
235
  if (existsSync(join(toolsDir, `${toolName}.ts`))) {
225
- throw new PackageCollisionError(`agents/${agentName}/tools/${toolName}.ts already exists.`);
236
+ throw new AbilityCollisionError(`agents/${agentName}/tools/${toolName}.ts already exists.`);
226
237
  }
227
238
  }
228
239
  const skillIds = [];
229
240
  for (const skillDir of skillDirs) {
230
241
  const skillId = basename(skillDir);
231
242
  if (!SKILL_ID_PATTERN.test(skillId)) {
232
- throw new PackageManifestError(`Skill directory '${skillDir}' doesn't name a valid skill id (must be lowercase, hyphen-separated).`);
243
+ throw new AbilityManifestError(`Skill directory '${skillDir}' doesn't name a valid skill id (must be lowercase, hyphen-separated).`);
233
244
  }
234
245
  skillIds.push(skillId);
235
246
  if (existsSync(join(skillsDirPath, skillId))) {
236
- throw new PackageCollisionError(`agents/${agentName}/skills/${skillId}/ already exists.`);
247
+ throw new AbilityCollisionError(`agents/${agentName}/skills/${skillId}/ already exists.`);
237
248
  }
238
249
  }
239
250
  const existingRuleNames = new Set(readActauthConfig(agentName).rules.map((r) => r.name));
240
251
  for (const rule of rules) {
241
252
  if (existingRuleNames.has(rule.name)) {
242
- throw new PackageCollisionError(`An actauth rule named '${rule.name}' already exists for '${agentName}'.`);
253
+ throw new AbilityCollisionError(`An actauth rule named '${rule.name}' already exists for '${agentName}'.`);
243
254
  }
244
255
  }
245
256
  const contentHashes = {};
@@ -252,7 +263,7 @@ export async function installPackage(agentName, spec, options = {}) {
252
263
  const indexPath = join(toolsDir, 'index.ts');
253
264
  for (const toolFile of toolFiles) {
254
265
  const toolName = basename(toolFile, '.ts');
255
- const code = readFileSync(join(packageDir, toolFile), 'utf8');
266
+ const code = readFileSync(join(abilityDir, toolFile), 'utf8');
256
267
  const destPath = join(toolsDir, `${toolName}.ts`);
257
268
  writeFileSync(destPath, code);
258
269
  contentHashes[`tools/${toolName}.ts`] = sha256(code);
@@ -268,7 +279,7 @@ export async function installPackage(agentName, spec, options = {}) {
268
279
  // Copy skill directories verbatim.
269
280
  for (const skillDir of skillDirs) {
270
281
  const skillId = basename(skillDir);
271
- const srcPath = join(packageDir, skillDir);
282
+ const srcPath = join(abilityDir, skillDir);
272
283
  const destPath = join(skillsDirPath, skillId);
273
284
  mkdirSync(skillsDirPath, { recursive: true });
274
285
  cpSync(srcPath, destPath, { recursive: true });
@@ -299,7 +310,7 @@ export async function installPackage(agentName, spec, options = {}) {
299
310
  // threeWayMerge already documents. Operates on a disposable scratch
300
311
  // copy, never the real project file directly.
301
312
  function mergeFile(mine, base, theirs) {
302
- const scratchDir = mkdtempSync(join(tmpdir(), 'loopengine-package-merge-'));
313
+ const scratchDir = mkdtempSync(join(tmpdir(), 'loopengine-ability-merge-'));
303
314
  const minePath = join(scratchDir, 'mine');
304
315
  const basePath = join(scratchDir, 'base');
305
316
  const theirsPath = join(scratchDir, 'theirs');
@@ -341,13 +352,13 @@ const PINNED_SPEC = /^(git\+|git:|https?:|file:|\.\.?\/|\/)/;
341
352
  * would still resolve there today. A git/file/URL spec is returned
342
353
  * as-is — see PINNED_SPEC's own doc comment for why appending "@version"
343
354
  * to one of those breaks instead of pinning. `recordedSpec` is only
344
- * absent for a package installed before InstalledPackageRecord.spec
345
- * existed; falling back to `packageName` there reproduces this
355
+ * absent for an ability installed before InstalledAbilityRecord.spec
356
+ * existed; falling back to `abilityName` there reproduces this
346
357
  * function's own old (buggy for a git/file install) behavior exactly —
347
- * no worse than before, and only for a package that hasn't upgraded
358
+ * no worse than before, and only for an ability that hasn't upgraded
348
359
  * since. */
349
- function oldContentSpec(recordedSpec, packageName, version) {
350
- const base = recordedSpec ?? packageName;
360
+ function oldContentSpec(recordedSpec, abilityName, version) {
361
+ const base = recordedSpec ?? abilityName;
351
362
  if (PINNED_SPEC.test(base))
352
363
  return base;
353
364
  // Strip any version/tag the spec already carries (a scoped name's own
@@ -357,27 +368,27 @@ function oldContentSpec(recordedSpec, packageName, version) {
357
368
  const bareName = base.startsWith('@') ? `@${base.slice(1).split('@')[0]}` : base.split('@')[0];
358
369
  return `${bareName}@${version}`;
359
370
  }
360
- /** Upgrades an already-installed package to whatever `spec` (defaulting
361
- * to `packageName`, i.e. "latest") currently resolves to. Tool and skill
371
+ /** Upgrades an already-installed ability to whatever `spec` (defaulting
372
+ * to `abilityName`, i.e. "latest") currently resolves to. Tool and skill
362
373
  * files get a real three-way merge (mine = current file, possibly
363
374
  * hand-edited since install; base = the version recorded at
364
375
  * install/last-upgrade, refetched via oldContentSpec; theirs = newly
365
376
  * fetched) — identical technique to `create-loopengine upgrade`, just
366
- * applied to package-managed files instead of template files. actauth
377
+ * applied to ability-managed files instead of template files. actauth
367
378
  * rules upgrade per-rule instead: the target actauth.yml holds rules
368
- * from other packages and hand-written ones too, so there's no coherent
379
+ * from other abilities and hand-written ones too, so there's no coherent
369
380
  * "whole file" base/theirs to merge — a rule unchanged since install
370
381
  * updates cleanly; a hand-edited one is left alone and reported as a
371
382
  * conflict rather than overwritten. */
372
- export async function upgradePackage(agentName, packageName, options = {}) {
373
- const fetch = options.fetchPackageDir ?? fetchPackageDir;
383
+ export async function upgradeAbility(agentName, abilityName, options = {}) {
384
+ const fetch = options.fetchAbilityDir ?? fetchAbilityDir;
374
385
  const provenance = readProvenance(agentName);
375
- const record = provenance[packageName];
386
+ const record = provenance[abilityName];
376
387
  if (!record) {
377
- throw new PackageNotInstalledError(`Package '${packageName}' isn't installed for '${agentName}'.`);
388
+ throw new AbilityNotInstalledError(`Ability '${abilityName}' isn't installed for '${agentName}'.`);
378
389
  }
379
- const spec = options.spec ?? packageName;
380
- const oldDir = fetch(oldContentSpec(record.spec, packageName, record.version));
390
+ const spec = options.spec ?? abilityName;
391
+ const oldDir = fetch(oldContentSpec(record.spec, abilityName, record.version));
381
392
  const newDir = fetch(spec);
382
393
  const oldManifest = readManifest(oldDir);
383
394
  const newManifest = readManifest(newDir);
@@ -452,7 +463,7 @@ export async function upgradePackage(agentName, packageName, options = {}) {
452
463
  updateActauthRule(agentName, ruleName, { scope: newRule.scope, tool: newRule.tool, decision: newRule.decision });
453
464
  results.push({ path, status: 'updated' });
454
465
  }
455
- provenance[packageName] = { ...record, version: newManifest.version, spec, contentHashes: newContentHashes, env: newManifest.env ?? record.env };
466
+ provenance[abilityName] = { ...record, version: newManifest.version, spec, contentHashes: newContentHashes, env: newManifest.env ?? record.env };
456
467
  writeProvenance(agentName, provenance);
457
468
  return { files: results };
458
469
  }
@@ -464,21 +475,21 @@ function isDirty(path, recordedHash) {
464
475
  return true;
465
476
  return sha256(readFileSync(path, 'utf8')) !== recordedHash;
466
477
  }
467
- /** Removes an installed package's tool and skill files, and its actauth
478
+ /** Removes an installed ability's tool and skill files, and its actauth
468
479
  * rules. A file whose content no longer matches the hash recorded at
469
480
  * install/last-upgrade is refused (reported in `refused`, left on disk)
470
- * unless `force` is passed — a concrete implementation of PACKAGES.md's
481
+ * unless `force` is passed — a concrete implementation of ABILITIES.md's
471
482
  * "refuses if any of them look hand-modified," cheaper than storing full
472
- * content or refetching the package to diff. Does not patch
483
+ * content or refetching the ability to diff. Does not patch
473
484
  * tools/index.ts to remove the now-dangling import — same "refuse
474
485
  * rather than guess a second time" doctrine addToolToIndex's own doc
475
486
  * comment already applies; a stale import surfaces as a clear build
476
487
  * error, not a silent break. */
477
- export function removePackage(agentName, packageName, force = false) {
488
+ export function removeAbility(agentName, abilityName, force = false) {
478
489
  const provenance = readProvenance(agentName);
479
- const record = provenance[packageName];
490
+ const record = provenance[abilityName];
480
491
  if (!record) {
481
- throw new PackageNotInstalledError(`Package '${packageName}' isn't installed for '${agentName}'.`);
492
+ throw new AbilityNotInstalledError(`Ability '${abilityName}' isn't installed for '${agentName}'.`);
482
493
  }
483
494
  const removed = [];
484
495
  const refused = [];
@@ -494,10 +505,10 @@ export function removePackage(agentName, packageName, force = false) {
494
505
  continue;
495
506
  }
496
507
  rmSync(fullPath, { force: true });
497
- // Undoes installPackage's own addToolToIndex call — without this, a
508
+ // Undoes installAbility's own addToolToIndex call — without this, a
498
509
  // deleted tool's import survives in tools/index.ts, and either fails
499
510
  // the next build (a dangling import to a file that no longer exists)
500
- // or, worse, silently duplicates on a later add-package of the same
511
+ // or, worse, silently duplicates on a later add-ability of the same
501
512
  // tool (addToolToIndex used to have no idempotence check at all —
502
513
  // now it does, but this is the actual fix: the stale entry shouldn't
503
514
  // be there to begin with).
@@ -525,10 +536,10 @@ export function removePackage(agentName, packageName, force = false) {
525
536
  }
526
537
  }
527
538
  if (refused.length === 0) {
528
- delete provenance[packageName];
539
+ delete provenance[abilityName];
529
540
  }
530
541
  else {
531
- provenance[packageName] = { ...record, tools: remainingTools, skills: remainingSkills, actauthRules: [] };
542
+ provenance[abilityName] = { ...record, tools: remainingTools, skills: remainingSkills, actauthRules: [] };
532
543
  }
533
544
  writeProvenance(agentName, provenance);
534
545
  return { removed, refused };
package/dist/bin/cli.js CHANGED
@@ -13,7 +13,7 @@ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
13
13
  import { spawn } from 'node:child_process';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { installPackage, upgradePackage, removePackage } from './package-manager.js';
16
+ import { installAbility, upgradeAbility, removeAbility } from './ability-manager.js';
17
17
  const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
18
18
  export class AgentNameError extends Error {
19
19
  }
@@ -231,7 +231,7 @@ async function requireAdapterFile(relPath) {
231
231
  process.exitCode = 1;
232
232
  return false;
233
233
  }
234
- // Shared by add-package/upgrade-package/remove-package below — pulls a
234
+ // Shared by add-ability/upgrade-ability/remove-ability below — pulls a
235
235
  // `--flag value` pair out of argv, same inline style `run`'s own
236
236
  // `--input` handling already uses, factored out only because three
237
237
  // commands need the identical `--agent <name>` extraction (a single use
@@ -301,22 +301,22 @@ async function main() {
301
301
  console.log(`Run the top-level agent: npx tsx adapters/cli.ts --agent ${parent.split('/')[0]} "hello"`);
302
302
  return;
303
303
  }
304
- // Installs a loopengine package (see PACKAGES.md) — a bundle of tool
304
+ // Installs a loopengine ability (see ABILITIES.md) — a bundle of tool
305
305
  // files, skill directories, and actauth rules published as an ordinary
306
306
  // npm package (public, private-registry, git, or a local file: path),
307
307
  // copied into the target agent's own tree rather than added as a
308
- // node_modules import (see package-manager.ts's own header comment for
308
+ // node_modules import (see ability-manager.ts's own header comment for
309
309
  // why). <spec> is whatever `npm pack` itself accepts.
310
- if (command === 'add-package') {
310
+ if (command === 'add-ability') {
311
311
  const { value: agent, rest: withoutAgent } = extractFlagValue(rest, '--agent');
312
312
  const [spec] = withoutAgent;
313
313
  if (!spec || !agent) {
314
- console.error('Usage: loopengine add-package <spec> --agent <name>');
314
+ console.error('Usage: loopengine add-ability <spec> --agent <name>');
315
315
  process.exitCode = 1;
316
316
  return;
317
317
  }
318
318
  try {
319
- const { installed } = await installPackage(agent, spec);
319
+ const { installed } = await installAbility(agent, spec);
320
320
  console.log(`Installed into agents/${agent}/:`);
321
321
  for (const p of installed)
322
322
  console.log(` ${p}`);
@@ -330,23 +330,23 @@ async function main() {
330
330
  }
331
331
  return;
332
332
  }
333
- // Pulls in whatever <packageName> (or --spec, if the package moved)
333
+ // Pulls in whatever <abilityName> (or --spec, if the ability moved)
334
334
  // currently resolves to, three-way-merging each tool/skill file it
335
335
  // manages against what changed upstream since install — a hand-edit
336
336
  // survives a clean merge; a real conflict leaves <<<<<<< markers to
337
337
  // resolve by hand, same as create-loopengine upgrade already does for
338
338
  // template files.
339
- if (command === 'upgrade-package') {
339
+ if (command === 'upgrade-ability') {
340
340
  const { value: agent, rest: withoutAgent } = extractFlagValue(rest, '--agent');
341
341
  const { value: spec, rest: withoutSpec } = extractFlagValue(withoutAgent, '--spec');
342
- const [packageName] = withoutSpec;
343
- if (!packageName || !agent) {
344
- console.error('Usage: loopengine upgrade-package <packageName> --agent <name> [--spec <spec>]');
342
+ const [abilityName] = withoutSpec;
343
+ if (!abilityName || !agent) {
344
+ console.error('Usage: loopengine upgrade-ability <abilityName> --agent <name> [--spec <spec>]');
345
345
  process.exitCode = 1;
346
346
  return;
347
347
  }
348
348
  try {
349
- const { files } = await upgradePackage(agent, packageName, spec ? { spec } : {});
349
+ const { files } = await upgradeAbility(agent, abilityName, spec ? { spec } : {});
350
350
  for (const f of files)
351
351
  console.log(` ${f.status.padEnd(9)}${f.path}`);
352
352
  const conflicts = files.filter((f) => f.status === 'conflict');
@@ -358,20 +358,20 @@ async function main() {
358
358
  }
359
359
  return;
360
360
  }
361
- // Removes a package's tool/skill files and actauth rules — refuses a
361
+ // Removes an ability's tool/skill files and actauth rules — refuses a
362
362
  // file whose content no longer matches what was recorded at
363
363
  // install/last-upgrade (a hand-edit since) unless --force is given.
364
- if (command === 'remove-package') {
364
+ if (command === 'remove-ability') {
365
365
  const { value: agent, rest: withoutAgent } = extractFlagValue(rest, '--agent');
366
366
  const { present: force, rest: withoutForce } = extractFlagPresence(withoutAgent, '--force');
367
- const [packageName] = withoutForce;
368
- if (!packageName || !agent) {
369
- console.error('Usage: loopengine remove-package <packageName> --agent <name> [--force]');
367
+ const [abilityName] = withoutForce;
368
+ if (!abilityName || !agent) {
369
+ console.error('Usage: loopengine remove-ability <abilityName> --agent <name> [--force]');
370
370
  process.exitCode = 1;
371
371
  return;
372
372
  }
373
373
  try {
374
- const { removed, refused } = removePackage(agent, packageName, force);
374
+ const { removed, refused } = removeAbility(agent, abilityName, force);
375
375
  for (const p of removed)
376
376
  console.log(` removed ${p}`);
377
377
  for (const p of refused)
@@ -463,9 +463,9 @@ async function main() {
463
463
  }
464
464
  console.error('Usage: loopengine add-agent <name>');
465
465
  console.error(' loopengine add-subagent <parent> <name>');
466
- console.error(' loopengine add-package <spec> --agent <name>');
467
- console.error(' loopengine upgrade-package <packageName> --agent <name> [--spec <spec>]');
468
- console.error(' loopengine remove-package <packageName> --agent <name> [--force]');
466
+ console.error(' loopengine add-ability <spec> --agent <name>');
467
+ console.error(' loopengine upgrade-ability <abilityName> --agent <name> [--spec <spec>]');
468
+ console.error(' loopengine remove-ability <abilityName> --agent <name> [--force]');
469
469
  console.error(' loopengine run <agent> [--session <id>] "<message>"');
470
470
  console.error(' loopengine serve');
471
471
  console.error(' loopengine dev');
@@ -1,6 +1,6 @@
1
1
  import type { Rule, Decision, Scope } from 'actauth';
2
2
  import type { Redis } from 'ioredis';
3
- import type { SafetyClassifier } from 'toollane';
3
+ import type { SafetyClassifier } from './toollane.js';
4
4
  import type { Summarizer } from './compaction.js';
5
5
  /** Which channel a runAgent() call is on — the value RunAgentOptions.channel
6
6
  * carries, checked against directly by AgentConfig.httpNotifier's own
@@ -1,7 +1,7 @@
1
1
  // Lets an operator register external tool gateways (Composio today —
2
- // Nango, Arcade, Scalekit are meant to slot in later as thin mcpplug
3
- // ToolSource adapters, same shape, once this mechanism is proven) against
4
- // an agent, without hand-writing a tools/index.ts. The registry is
2
+ // Nango, Arcade, Scalekit are meant to slot in later as thin
3
+ // mcpplug.ts ToolSource adapters, same shape, once this mechanism is
4
+ // proven) against an agent, without hand-writing a tools/index.ts. The registry is
5
5
  // agents/<name>/gateway-tools.yml — plain data, read fresh off disk every
6
6
  // call, same "no restart to see an edit" behavior actauth.yml already
7
7
  // has (see run-agent.ts's loadRules). adapters/http.ts's admin routes are
@@ -12,7 +12,7 @@ import { join } from 'node:path';
12
12
  import { execFile } from 'node:child_process';
13
13
  import { promisify } from 'node:util';
14
14
  import { parse as parseYaml, parseDocument, stringify as stringifyYaml } from 'yaml';
15
- import { connectComposioSource } from 'mcpplug';
15
+ import { connectComposioSource } from './mcpplug.js';
16
16
  const execFileAsync = promisify(execFile);
17
17
  // gateway-tools.yml is plain data (see this file's own header comment),
18
18
  // not compiled code — same "resolved against process.cwd(), like
@@ -0,0 +1,23 @@
1
+ import type { ToolDefinition } from './agent-config.js';
2
+ /** One connection to one external tool gateway (Composio today; a raw MCP
3
+ * client is a later, separate source). `name` is used to namespace every
4
+ * tool it produces, so two sources never collide. */
5
+ export interface ToolSource {
6
+ name: string;
7
+ loadTools(): Promise<ToolDefinition[]>;
8
+ close(): Promise<void>;
9
+ }
10
+ export interface ComposioSourceOptions {
11
+ /** Composio tool slugs to expose, e.g.
12
+ * "GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER". Explicit, not
13
+ * auto-discovered — same reasoning actauth rules are opt-in per tool
14
+ * rather than a blanket allow; an agent should only see the Composio
15
+ * actions its config actually lists. */
16
+ slugs: string[];
17
+ /** Defaults to 'composio' — override to point at a stand-in binary in tests. */
18
+ cliCommand?: string;
19
+ }
20
+ /** Sources ToolDefinitions from the Composio CLI — already-authenticated
21
+ * (`composio link <toolkit>`) tool execution across 1000+ apps, so this
22
+ * source needs no OAuth code of its own; Composio owns that entirely. */
23
+ export declare function connectComposioSource(name: string, options: ComposioSourceOptions): Promise<ToolSource>;
@@ -0,0 +1,64 @@
1
+ // Formerly the standalone `mcpplug` package — folded in here since it
2
+ // had exactly one real consumer (gateway-tools.ts, below) and the
3
+ // "usable by any agent runtime" framing it shipped with was aspirational,
4
+ // never actually exercised by a second host. Reuses this repo's own
5
+ // ToolDefinition (agent-config.ts) directly instead of mcpplug's own
6
+ // parallel-but-identical copy — that duplication only existed to keep
7
+ // mcpplug import-free of loopengine, which stopped mattering once it
8
+ // moved in.
9
+ import { execFile } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ const run = promisify(execFile);
12
+ function humanize(slug) {
13
+ return slug.toLowerCase().split('_').join(' ');
14
+ }
15
+ async function runCli(cliCommand, args) {
16
+ let stdout;
17
+ try {
18
+ ;
19
+ ({ stdout } = await run(cliCommand, args));
20
+ }
21
+ catch (err) {
22
+ const message = err instanceof Error ? err.message : String(err);
23
+ throw new Error(`composio CLI call failed (${args.join(' ')}): ${message}`);
24
+ }
25
+ try {
26
+ return JSON.parse(stdout);
27
+ }
28
+ catch {
29
+ throw new Error(`composio CLI returned non-JSON output for ${args.join(' ')}: ${stdout}`);
30
+ }
31
+ }
32
+ /** Sources ToolDefinitions from the Composio CLI — already-authenticated
33
+ * (`composio link <toolkit>`) tool execution across 1000+ apps, so this
34
+ * source needs no OAuth code of its own; Composio owns that entirely. */
35
+ export async function connectComposioSource(name, options) {
36
+ const cliCommand = options.cliCommand ?? 'composio';
37
+ return {
38
+ name,
39
+ async loadTools() {
40
+ const tools = [];
41
+ for (const slug of options.slugs) {
42
+ const schema = await runCli(cliCommand, ['execute', slug, '--get-schema']);
43
+ const inputSchema = schema.inputSchema ?? {};
44
+ tools.push({
45
+ name: `${name}_${slug}`,
46
+ description: humanize(slug),
47
+ input_schema: inputSchema,
48
+ execute: async (input) => {
49
+ const result = await runCli(cliCommand, ['execute', slug, '-d', JSON.stringify(input)]);
50
+ if (result.successful === false || result.error) {
51
+ throw new Error(`composio tool ${slug} failed: ${JSON.stringify(result.error ?? result)}`);
52
+ }
53
+ return result.data ?? result;
54
+ },
55
+ });
56
+ }
57
+ return tools;
58
+ },
59
+ // Each call is its own subprocess — nothing held open between calls,
60
+ // so there's nothing to tear down. Kept for symmetry with sources
61
+ // that do hold a live connection (e.g. a future raw MCP client).
62
+ close: async () => { },
63
+ };
64
+ }
@@ -8,10 +8,10 @@ import { dirname, join } from 'node:path';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { parse as parseYaml } from 'yaml';
10
10
  import { Gate, RuleSet, ConsoleApprover } from 'actauth';
11
- import { SkillGarden } from 'skillgarden';
11
+ import { SkillGarden } from './skillgarden/index.js';
12
12
  import { BudgetTracker } from './budget.js';
13
13
  import { Compactor } from './compaction.js';
14
- import { ToolLane } from 'toollane';
14
+ import { ToolLane } from './toollane.js';
15
15
  import { Recovery } from './recovery.js';
16
16
  import { loadAgentModule } from './discover-agents.js';
17
17
  import { agentAsTool } from './agent-as-tool.js';
@@ -1,5 +1,5 @@
1
1
  import { Redis } from 'ioredis';
2
- import { type Storage } from 'sessionknit';
2
+ import { type Storage } from './sessionknit.js';
3
3
  import type { Message } from '#core/run-agent.js';
4
4
  export interface SessionResult<T> {
5
5
  /** Exactly what this turn added — durably appended as-is, in order.