create-loopengine 0.1.47 → 0.1.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -104,6 +104,7 @@ import {
104
104
  ActauthRuleNotFoundError,
105
105
  installAbility,
106
106
  upgradeAbility,
107
+ removeAbility,
107
108
  listInstalledAbilities,
108
109
  backfillDependencies,
109
110
  AbilityAlreadyInstalledError,
@@ -1228,6 +1229,34 @@ async function handleAbilityUpgradePost(req: IncomingMessage, res: ServerRespons
1228
1229
  }
1229
1230
  }
1230
1231
 
1232
+ // Uninstalls an ability — deletes its tool/skill files and actauth
1233
+ // rules (bin/ability-manager.ts's own removeAbility). Same admin-auth
1234
+ // posture as install/upgrade above: this mutates the agent's own tree,
1235
+ // even though — unlike install/upgrade — it never runs new third-party
1236
+ // code. `removed`/`refused` pass straight through on success (not an
1237
+ // error: a dirty file left in place, same as remove-ability's own CLI
1238
+ // behavior, is a normal partial outcome the Admin UI shows and offers
1239
+ // to retry with force, not a failure).
1240
+ async function handleAbilityUninstallDelete(res: ServerResponse, agentName: string, abilityName: string, force: boolean): Promise<void> {
1241
+ if (!adminAuth) {
1242
+ res.writeHead(403, { 'content-type': 'application/json' }).end(
1243
+ JSON.stringify({ error: 'Refusing to uninstall an ability without LOOPENGINE_ADMIN_AUTH configured — set it before uninstalling abilities through this route.' }),
1244
+ )
1245
+ return
1246
+ }
1247
+ if (!getEntry(agentName)) {
1248
+ res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1249
+ return
1250
+ }
1251
+ try {
1252
+ const result = removeAbility(agentName, abilityName, force)
1253
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(result))
1254
+ } catch (err) {
1255
+ const status = err instanceof AbilityNotInstalledError ? 404 : 500
1256
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
1257
+ }
1258
+ }
1259
+
1231
1260
  const execFileAsync = promisify(execFile)
1232
1261
  const NPM_INSTALL_DEPS_TIMEOUT_MS = 180_000
1233
1262
 
@@ -2624,6 +2653,12 @@ const server = createServer(async (req, res) => {
2624
2653
  await handleAbilityInstallDepsPost(req, res, decodeURIComponent(abilityInstallDepsMatch[1]), decodeURIComponent(abilityInstallDepsMatch[2]))
2625
2654
  return
2626
2655
  }
2656
+ const abilityMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/([^/]+)$/)
2657
+ if (abilityMatch && req.method === 'DELETE') {
2658
+ const force = new URL(req.url ?? '/', 'http://localhost').searchParams.get('force') === 'true'
2659
+ await handleAbilityUninstallDelete(res, decodeURIComponent(abilityMatch[1]), decodeURIComponent(abilityMatch[2]), force)
2660
+ return
2661
+ }
2627
2662
  const abilitiesMatch = pathname.match(/^\/agents\/([^/]+)\/abilities$/)
2628
2663
  if (abilitiesMatch && req.method === 'GET') {
2629
2664
  await handleAbilitiesGet(res, decodeURIComponent(abilitiesMatch[1]))