loopengine 0.1.22 → 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.
package/README.md CHANGED
@@ -25,10 +25,10 @@ top to bottom. Run agents over a CLI, an HTTP API, or both.
25
25
  - **Sessions persist automatically.** Send a message, get a reply, come
26
26
  back later with the same session id and continue — no database to wire
27
27
  up yourself, and it survives a crash mid-turn cleanly.
28
- - **A package system for reusable capabilities.** Install a bundle of
28
+ - **An ability system for reusable capabilities.** Install a bundle of
29
29
  tools, a skill, and the permission rules that gate them with one command
30
30
  — real, reviewable files in your own repo, not an opaque dependency. See
31
- [Package system](#package-system) below.
31
+ [Ability system](#ability-system) below.
32
32
  - **Does real work, safely.** Tools can hit a database, send emails, call
33
33
  GitHub/Slack, anything with an `execute` function. Permission rules gate
34
34
  what happens without a human: safe reads auto-run, risky actions get
@@ -118,7 +118,7 @@ redeploy needed for most of it:
118
118
  | **Skills** | Create, edit, and delete `SKILL.md` files for this agent directly in the browser — write the body, preview the rendered markdown, save. |
119
119
  | **Tools** | Local hand-written tools, gateway-sourced tools (e.g. [Composio](https://composio.dev)), and subagents-as-tools, in one place. Connect a new external gateway source or add/remove a tool without touching a file. |
120
120
  | **ActAuth** | Add, edit, and delete permission rules — scope, tool, condition, decision — and change `default_decision`, live. |
121
- | **Environment** | Every env var a package (see [Package system](#package-system)) declared it needs, across everything installed for this agent — which ones are set, which are missing, and a form to set one. A value is never echoed back once set. |
121
+ | **Environment** | Every env var an ability (see [Ability system](#ability-system)) declared it needs, across everything installed for this agent — which ones are set, which are missing, and a form to set one. A value is never echoed back once set. |
122
122
 
123
123
  Every tab is backed by a real API (`GET /agents/:name/config`, `.../actauth`,
124
124
  `.../env`, ...) that reuses the exact same resolution `runAgent()` itself
@@ -170,15 +170,15 @@ click, a webhook HMAC signature, a magic-link token) ship under
170
170
  live and durable, worked examples, and how resumption actually works
171
171
  under the hood.
172
172
 
173
- ## Package system
173
+ ## Ability system
174
174
 
175
175
  Giving an agent a new capability is usually three separate, hand-authored
176
176
  things: a tool file, a `SKILL.md` teaching the model when to use it, and
177
- an `actauth` rule allowing it to actually run. A **loopengine package**
177
+ an `actauth` rule allowing it to actually run. A **loopengine ability**
178
178
  bundles all three into one installable unit:
179
179
 
180
180
  ```bash
181
- npx loopengine add-package <spec> --agent customer-service
181
+ npx loopengine add-ability <spec> --agent customer-service
182
182
  ```
183
183
 
184
184
  `<spec>` is anything `npm pack` understands — a public or private npm
@@ -189,17 +189,17 @@ dependency — reviewable, diffable, and editable the same as anything you
189
189
  would have hand-written, not an opaque black box.
190
190
 
191
191
  ```bash
192
- npx loopengine upgrade-package <packageName> --agent customer-service
193
- npx loopengine remove-package <packageName> --agent customer-service
192
+ npx loopengine upgrade-ability <abilityName> --agent customer-service
193
+ npx loopengine remove-ability <abilityName> --agent customer-service
194
194
  ```
195
195
 
196
196
  Upgrading does a real three-way merge per file — a hand-edit since install
197
197
  survives, a genuine conflict leaves `<<<<<<<` markers to resolve by hand
198
198
  instead of silently overwriting either side. Removing refuses a file
199
- that's been modified since install unless you pass `--force`. A package
199
+ that's been modified since install unless you pass `--force`. An ability
200
200
  can also declare the env vars its tools need, which then show up in the
201
201
  Admin UI's [Environment tab](#admin-ui) automatically once installed. See
202
- [`PACKAGES.md`](PACKAGES.md) for the full design — format, publishing,
202
+ [`ABILITIES.md`](ABILITIES.md) for the full design — format, publishing,
203
203
  and how the merge/upgrade mechanics work in detail.
204
204
 
205
205
  ## Running an agent
@@ -1,48 +1,54 @@
1
- export declare class PackageManifestError extends Error {
1
+ export declare class AbilityManifestError extends Error {
2
2
  }
3
- export declare class PackageVersionError extends Error {
3
+ export declare class AbilityVersionError extends Error {
4
4
  }
5
- export declare class PackageCollisionError extends Error {
5
+ export declare class AbilityCollisionError extends Error {
6
6
  }
7
- export declare class PackageNotInstalledError extends Error {
7
+ export declare class AbilityNotInstalledError extends Error {
8
8
  }
9
- export declare class PackageAlreadyInstalledError extends Error {
9
+ export declare class AbilityAlreadyInstalledError extends Error {
10
10
  }
11
- export interface PackageEnvDecl {
11
+ export interface AbilityEnvDecl {
12
12
  name: string;
13
13
  description?: string;
14
14
  secret?: boolean;
15
15
  }
16
- export interface PackageManifest {
16
+ /** `name`/`version` deliberately aren't declared here — they're read off
17
+ * the ability's own sibling `package.json` instead (see readManifest),
18
+ * which already has to exist and already has to carry real, valid values
19
+ * for `npm pack` to treat the directory as a fetchable package at all.
20
+ * loopengine.ability.json only ever needs to declare what npm has no
21
+ * vocabulary for. */
22
+ export interface AbilityManifest {
17
23
  name: string;
18
24
  version: string;
19
25
  loopengineVersion: string;
20
26
  tools?: string[];
21
27
  skills?: string[];
22
28
  actauth?: string;
23
- env?: PackageEnvDecl[];
29
+ env?: AbilityEnvDecl[];
24
30
  }
25
- /** One agent's record of one installed package — the merge base a
31
+ /** One agent's record of one installed ability — the merge base a
26
32
  * future upgrade needs, what remove needs to know is safe to delete,
27
33
  * and (env) what the Admin UI's secrets section reads to know which
28
- * vars to prompt for. Written to agents/<agent>/.loopengine-packages.json,
34
+ * vars to prompt for. Written to agents/<agent>/.loopengine-abilities.json,
29
35
  * same role .create-loopengine.json already plays for template files. */
30
- export interface InstalledPackageRecord {
36
+ export interface InstalledAbilityRecord {
31
37
  version: string;
32
38
  /** The exact spec (bare registry name, "name@version", a git+ssh/
33
39
  * git+https URL with a #committish, a file: path, ...) last used to
34
- * successfully install or upgrade this package — see
35
- * oldContentSpec's own doc comment for why upgradePackage needs this
36
- * stored verbatim rather than reconstructed from `packageName`.
37
- * Optional only because a package installed before this field existed
40
+ * successfully install or upgrade this ability — see
41
+ * oldContentSpec's own doc comment for why upgradeAbility needs this
42
+ * stored verbatim rather than reconstructed from `abilityName`.
43
+ * Optional only because an ability installed before this field existed
38
44
  * has no recorded value — see oldContentSpec's own fallback. */
39
45
  spec?: string;
40
46
  tools: string[];
41
47
  skills: string[];
42
48
  actauthRules: string[];
43
- env: PackageEnvDecl[];
49
+ env: AbilityEnvDecl[];
44
50
  /** relative path (e.g. "tools/foo.ts") -> sha256 hex, as of the last
45
- * install/upgrade — remove-package's dirty-check compares against
51
+ * install/upgrade — remove-ability's dirty-check compares against
46
52
  * this rather than storing/refetching full content to diff. */
47
53
  contentHashes: Record<string, string>;
48
54
  }
@@ -52,11 +58,11 @@ export interface InstalledPackageRecord {
52
58
  * accepts anything `npm pack` does: a public/private registry spec, a
53
59
  * git URL (`github:org/repo`, `git+ssh://...`), or a local `file:../path`
54
60
  * — the last of which is what this module's own tests point at, so no
55
- * real network call is needed there; see PACKAGES.md's "Publishing a
56
- * package" section). */
57
- export declare function fetchPackageDir(spec: string): string;
61
+ * real network call is needed there; see ABILITIES.md's "Publishing an
62
+ * ability" section). */
63
+ export declare function fetchAbilityDir(spec: string): string;
58
64
  export interface FetchOptions {
59
- fetchPackageDir?: (spec: string) => string;
65
+ fetchAbilityDir?: (spec: string) => string;
60
66
  /** Test-only override for the installing project's own "loopengine"
61
67
  * dependency range — defaults to a real package.json read. Without
62
68
  * this, checkLoopengineVersion's refusal path is unreachable from
@@ -71,7 +77,7 @@ export interface FetchOptions {
71
77
  * (reusing addToolToIndex exactly as web/http-tool-admin.ts's own
72
78
  * createHttpTool does), copies each skill directory verbatim (a raw
73
79
  * cpSync, not skills-admin.ts's writeSkill — that regenerates
74
- * frontmatter from scratch and can't carry a package's own
80
+ * frontmatter from scratch and can't carry an ability's own
75
81
  * frontmatter/assets), and appends each actauth rule (addActauthRule).
76
82
  *
77
83
  * Deliberately does *not* attempt a live in-memory registry splice the
@@ -79,7 +85,7 @@ export interface FetchOptions {
79
85
  * admin-created tool — that works there because the HTTP request
80
86
  * creating the tool runs *inside the same process* as the already-running
81
87
  * server, so updateAgent's mutation is visible to every later request in
82
- * that same process immediately. `add-package` is a separate, one-off
88
+ * that same process immediately. `add-ability` is a separate, one-off
83
89
  * CLI process with no connection to whatever server might be running
84
90
  * elsewhere — even a successful getEntry/updateAgent call here would
85
91
  * only mutate *this* CLI invocation's own throwaway registry, then
@@ -96,41 +102,41 @@ export interface FetchOptions {
96
102
  *
97
103
  * All-or-nothing: every collision check below runs, and the whole
98
104
  * install is refused, before a single file is written. */
99
- export declare function installPackage(agentName: string, spec: string, options?: FetchOptions): Promise<{
105
+ export declare function installAbility(agentName: string, spec: string, options?: FetchOptions): Promise<{
100
106
  installed: string[];
101
107
  }>;
102
108
  export interface UpgradeFileResult {
103
109
  path: string;
104
110
  status: 'updated' | 'unchanged' | 'conflict';
105
111
  }
106
- /** Upgrades an already-installed package to whatever `spec` (defaulting
107
- * to `packageName`, i.e. "latest") currently resolves to. Tool and skill
112
+ /** Upgrades an already-installed ability to whatever `spec` (defaulting
113
+ * to `abilityName`, i.e. "latest") currently resolves to. Tool and skill
108
114
  * files get a real three-way merge (mine = current file, possibly
109
115
  * hand-edited since install; base = the version recorded at
110
116
  * install/last-upgrade, refetched via oldContentSpec; theirs = newly
111
117
  * fetched) — identical technique to `create-loopengine upgrade`, just
112
- * applied to package-managed files instead of template files. actauth
118
+ * applied to ability-managed files instead of template files. actauth
113
119
  * rules upgrade per-rule instead: the target actauth.yml holds rules
114
- * from other packages and hand-written ones too, so there's no coherent
120
+ * from other abilities and hand-written ones too, so there's no coherent
115
121
  * "whole file" base/theirs to merge — a rule unchanged since install
116
122
  * updates cleanly; a hand-edited one is left alone and reported as a
117
123
  * conflict rather than overwritten. */
118
- export declare function upgradePackage(agentName: string, packageName: string, options?: FetchOptions & {
124
+ export declare function upgradeAbility(agentName: string, abilityName: string, options?: FetchOptions & {
119
125
  spec?: string;
120
126
  }): Promise<{
121
127
  files: UpgradeFileResult[];
122
128
  }>;
123
- /** Removes an installed package's tool and skill files, and its actauth
129
+ /** Removes an installed ability's tool and skill files, and its actauth
124
130
  * rules. A file whose content no longer matches the hash recorded at
125
131
  * install/last-upgrade is refused (reported in `refused`, left on disk)
126
- * unless `force` is passed — a concrete implementation of PACKAGES.md's
132
+ * unless `force` is passed — a concrete implementation of ABILITIES.md's
127
133
  * "refuses if any of them look hand-modified," cheaper than storing full
128
- * content or refetching the package to diff. Does not patch
134
+ * content or refetching the ability to diff. Does not patch
129
135
  * tools/index.ts to remove the now-dangling import — same "refuse
130
136
  * rather than guess a second time" doctrine addToolToIndex's own doc
131
137
  * comment already applies; a stale import surfaces as a clear build
132
138
  * error, not a silent break. */
133
- export declare function removePackage(agentName: string, packageName: string, force?: boolean): {
139
+ export declare function removeAbility(agentName: string, abilityName: string, force?: boolean): {
134
140
  removed: string[];
135
141
  refused: string[];
136
142
  };
@@ -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');
@@ -1110,9 +1110,9 @@ export const agentsConfigPageHtml = `<!doctype html>
1110
1110
  });
1111
1111
  }
1112
1112
 
1113
- // ---- Environment tab: every env var a package (see PACKAGES.md,
1114
- // bin/package-manager.ts) declared it needs for this agent, across
1115
- // every package installed — status only, a value is never shown once
1113
+ // ---- Environment tab: every env var an ability (see ABILITIES.md,
1114
+ // bin/ability-manager.ts) declared it needs for this agent, across
1115
+ // every ability installed — status only, a value is never shown once
1116
1116
  // set, matching env-admin.ts's own never-echo-a-secret rule. Lazily
1117
1117
  // loaded the same way Actauth is, for the same reason (avoid a
1118
1118
  // redundant fetch on every agent switch for a tab that isn't always
@@ -1126,7 +1126,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1126
1126
  return '<tr>' +
1127
1127
  '<td><code>' + escapeHtml(v.name) + '</code></td>' +
1128
1128
  '<td>' + escapeHtml(v.description || '') + '</td>' +
1129
- '<td class="hint">' + escapeHtml(v.packageName) + '</td>' +
1129
+ '<td class="hint">' + escapeHtml(v.abilityName) + '</td>' +
1130
1130
  '<td>' + (v.set ? '<span class="hint">set</span>' : '<span class="error">not set</span>') + '</td>' +
1131
1131
  '<td><form class="add-source env-var-form" data-name="' + escapeHtml(v.name) + '">' +
1132
1132
  '<input type="' + (v.secret ? 'password' : 'text') + '" name="value" placeholder="' + (v.set ? 'unchanged unless you type a new value' : 'value') + '" required>' +
@@ -1136,9 +1136,9 @@ export const agentsConfigPageHtml = `<!doctype html>
1136
1136
  }
1137
1137
 
1138
1138
  function renderEnvConfigHtml(vars) {
1139
- if (!vars.length) return '<p class="hint">No installed package has declared any environment variables for this agent yet.</p>';
1139
+ if (!vars.length) return '<p class="hint">No installed ability has declared any environment variables for this agent yet.</p>';
1140
1140
  var rows = vars.map(renderEnvRow).join('');
1141
- return '<table><thead><tr><th>Name</th><th>Description</th><th>Package</th><th>Status</th><th>Set value</th></tr></thead><tbody>' + rows + '</tbody></table>';
1141
+ return '<table><thead><tr><th>Name</th><th>Description</th><th>Ability</th><th>Status</th><th>Set value</th></tr></thead><tbody>' + rows + '</tbody></table>';
1142
1142
  }
1143
1143
 
1144
1144
  function envContentEl() {
@@ -4,13 +4,13 @@ export interface DeclaredEnvVar {
4
4
  name: string;
5
5
  description?: string;
6
6
  secret: boolean;
7
- packageName: string;
7
+ abilityName: string;
8
8
  set: boolean;
9
9
  }
10
- /** Every env var any package installed for `agentName` declared it
11
- * needs, deduplicated by name — PACKAGES.md's own open question on
12
- * cross-package name collisions applies here too (one .env per
13
- * *project*, not per agent, so two unrelated packages declaring the
10
+ /** Every env var any ability installed for `agentName` declared it
11
+ * needs, deduplicated by name — ABILITIES.md's own open question on
12
+ * cross-ability name collisions applies here too (one .env per
13
+ * *project*, not per agent, so two unrelated abilities declaring the
14
14
  * same name can't actually be told apart; the first one seen wins the
15
15
  * description shown). `set` is read live off `process.env`, not cached,
16
16
  * so it reflects whatever the last `setEnvVar` call — or a plain
@@ -19,7 +19,7 @@ export declare function listDeclaredEnvVars(agentName: string): DeclaredEnvVar[]
19
19
  /** Upserts `NAME=VALUE` into the project's `.env` file — preserving
20
20
  * every other line (comments, blank lines, unrelated keys) — and
21
21
  * applies it to *this* running process immediately via `process.env`,
22
- * so a newly-installed package's tools work without a restart. Callers
22
+ * so a newly-installed ability's tools work without a restart. Callers
23
23
  * (the PUT route in adapters/http.ts) are responsible for refusing to
24
24
  * call this at all when `LOOPENGINE_ADMIN_AUTH` isn't set — this
25
25
  * function itself has no notion of HTTP auth, it just writes. */
@@ -1,6 +1,6 @@
1
- // Backs the Admin UI's "Environment" section — every env var a package
2
- // (see PACKAGES.md, bin/package-manager.ts) declared as required,
3
- // across every package installed for an agent, with set/not-set status
1
+ // Backs the Admin UI's "Environment" section — every env var an ability
2
+ // (see ABILITIES.md, bin/ability-manager.ts) declared as required,
3
+ // across every ability installed for an agent, with set/not-set status
4
4
  // only. A value marked `secret` is never echoed back once set — same
5
5
  // never-echo-a-secret rule web/http-tool-admin.ts's own `{{ENV_VAR}}`
6
6
  // header handling already establishes for a tool's own secrets.
@@ -13,12 +13,12 @@ export class EnvVarNameError extends Error {
13
13
  // validates a {{ENV_VAR}} header reference against.
14
14
  const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
15
15
  function provenancePath(agentName) {
16
- return join(agentDir(agentName), '.loopengine-packages.json');
16
+ return join(agentDir(agentName), '.loopengine-abilities.json');
17
17
  }
18
- /** Every env var any package installed for `agentName` declared it
19
- * needs, deduplicated by name — PACKAGES.md's own open question on
20
- * cross-package name collisions applies here too (one .env per
21
- * *project*, not per agent, so two unrelated packages declaring the
18
+ /** Every env var any ability installed for `agentName` declared it
19
+ * needs, deduplicated by name — ABILITIES.md's own open question on
20
+ * cross-ability name collisions applies here too (one .env per
21
+ * *project*, not per agent, so two unrelated abilities declaring the
22
22
  * same name can't actually be told apart; the first one seen wins the
23
23
  * description shown). `set` is read live off `process.env`, not cached,
24
24
  * so it reflects whatever the last `setEnvVar` call — or a plain
@@ -30,7 +30,7 @@ export function listDeclaredEnvVars(agentName) {
30
30
  const provenance = JSON.parse(readFileSync(path, 'utf8'));
31
31
  const seen = new Set();
32
32
  const result = [];
33
- for (const [packageName, record] of Object.entries(provenance)) {
33
+ for (const [abilityName, record] of Object.entries(provenance)) {
34
34
  for (const decl of record.env) {
35
35
  if (seen.has(decl.name))
36
36
  continue;
@@ -39,7 +39,7 @@ export function listDeclaredEnvVars(agentName) {
39
39
  name: decl.name,
40
40
  description: decl.description,
41
41
  secret: decl.secret === true,
42
- packageName,
42
+ abilityName,
43
43
  set: process.env[decl.name] !== undefined,
44
44
  });
45
45
  }
@@ -68,7 +68,7 @@ function serializeEnvValue(value) {
68
68
  /** Upserts `NAME=VALUE` into the project's `.env` file — preserving
69
69
  * every other line (comments, blank lines, unrelated keys) — and
70
70
  * applies it to *this* running process immediately via `process.env`,
71
- * so a newly-installed package's tools work without a restart. Callers
71
+ * so a newly-installed ability's tools work without a restart. Callers
72
72
  * (the PUT route in adapters/http.ts) are responsible for refusing to
73
73
  * call this at all when `LOOPENGINE_ADMIN_AUTH` isn't set — this
74
74
  * function itself has no notion of HTTP auth, it just writes. */
@@ -40,9 +40,9 @@ export declare function toCamelCase(snakeCase: string): string;
40
40
  export declare function addToolToIndex(toolsIndexPath: string, toolFileName: string, exportName: string): void;
41
41
  /** addToolToIndex's own undo — removes the named tool's import line and
42
42
  * array entry. Best-effort, not "refuse rather than guess" like
43
- * addToolToIndex: this only ever runs against a tool package-manager.ts
43
+ * addToolToIndex: this only ever runs against a tool ability-manager.ts
44
44
  * itself installed (so the exact shape addToolToIndex wrote is known),
45
- * and the caller (removePackage) has already deleted the tool's .ts
45
+ * and the caller (removeAbility) has already deleted the tool's .ts
46
46
  * file — leaving a stale entry behind, the previous behavior, means a
47
47
  * dangling import that fails the next build, which is strictly worse
48
48
  * than a no-op here if the file turns out not to match (hand-edited
@@ -191,8 +191,8 @@ export function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
191
191
  if (!arrayMatch) {
192
192
  throw new HttpToolIndexShapeError(`${toolsIndexPath} doesn't match the expected "export const tools: ToolDefinition[] = [...]" shape — add this tool to it by hand instead.`);
193
193
  }
194
- // Idempotent — a caller that adds the same tool twice (e.g. package-
195
- // manager.ts's removePackage not having cleaned up this same file
194
+ // Idempotent — a caller that adds the same tool twice (e.g. ability-
195
+ // manager.ts's removeAbility not having cleaned up this same file
196
196
  // before a re-add, or simply calling this twice by mistake) gets a
197
197
  // no-op the second time instead of a duplicate `import { X }`
198
198
  // declaration, which is a TypeScript syntax error ("Duplicate
@@ -213,9 +213,9 @@ export function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
213
213
  }
214
214
  /** addToolToIndex's own undo — removes the named tool's import line and
215
215
  * array entry. Best-effort, not "refuse rather than guess" like
216
- * addToolToIndex: this only ever runs against a tool package-manager.ts
216
+ * addToolToIndex: this only ever runs against a tool ability-manager.ts
217
217
  * itself installed (so the exact shape addToolToIndex wrote is known),
218
- * and the caller (removePackage) has already deleted the tool's .ts
218
+ * and the caller (removeAbility) has already deleted the tool's .ts
219
219
  * file — leaving a stale entry behind, the previous behavior, means a
220
220
  * dangling import that fails the next build, which is strictly worse
221
221
  * than a no-op here if the file turns out not to match (hand-edited
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",
@@ -23,8 +23,8 @@
23
23
  "dist/index.d.ts",
24
24
  "dist/bin/cli.js",
25
25
  "dist/bin/cli.d.ts",
26
- "dist/bin/package-manager.js",
27
- "dist/bin/package-manager.d.ts",
26
+ "dist/bin/ability-manager.js",
27
+ "dist/bin/ability-manager.d.ts",
28
28
  "dist/core/run-agent.js",
29
29
  "dist/core/run-agent.d.ts",
30
30
  "dist/core/loop-events.js",