loopengine 0.1.14 → 0.1.15

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.
@@ -24,7 +24,7 @@ import { tmpdir } from 'node:os';
24
24
  import { parse as parseYaml } from 'yaml';
25
25
  import semver from 'semver';
26
26
  import { agentDir } from '../core/gateway-tools.js';
27
- import { addToolToIndex, toCamelCase } from '../web/http-tool-admin.js';
27
+ import { addToolToIndex, removeToolFromIndex, toCamelCase } from '../web/http-tool-admin.js';
28
28
  import { addActauthRule, updateActauthRule, removeActauthRule, readActauthConfig } from '../web/actauth-admin.js';
29
29
  export class PackageManifestError extends Error {
30
30
  }
@@ -484,6 +484,7 @@ export function removePackage(agentName, packageName, force = false) {
484
484
  const refused = [];
485
485
  const remainingTools = [];
486
486
  const remainingSkills = [];
487
+ const toolsIndexPath = join(agentDir(agentName), 'tools', 'index.ts');
487
488
  for (const toolName of record.tools) {
488
489
  const relPath = `tools/${toolName}.ts`;
489
490
  const fullPath = join(agentDir(agentName), relPath);
@@ -493,6 +494,14 @@ export function removePackage(agentName, packageName, force = false) {
493
494
  continue;
494
495
  }
495
496
  rmSync(fullPath, { force: true });
497
+ // Undoes installPackage's own addToolToIndex call — without this, a
498
+ // deleted tool's import survives in tools/index.ts, and either fails
499
+ // 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
501
+ // tool (addToolToIndex used to have no idempotence check at all —
502
+ // now it does, but this is the actual fix: the stale entry shouldn't
503
+ // be there to begin with).
504
+ removeToolFromIndex(toolsIndexPath, toolName, toCamelCase(toolName));
496
505
  removed.push(relPath);
497
506
  }
498
507
  for (const skillId of record.skills) {
@@ -38,6 +38,17 @@ export interface HttpToolSpec {
38
38
  }
39
39
  export declare function toCamelCase(snakeCase: string): string;
40
40
  export declare function addToolToIndex(toolsIndexPath: string, toolFileName: string, exportName: string): void;
41
+ /** addToolToIndex's own undo — removes the named tool's import line and
42
+ * array entry. Best-effort, not "refuse rather than guess" like
43
+ * addToolToIndex: this only ever runs against a tool package-manager.ts
44
+ * itself installed (so the exact shape addToolToIndex wrote is known),
45
+ * and the caller (removePackage) has already deleted the tool's .ts
46
+ * file — leaving a stale entry behind, the previous behavior, means a
47
+ * dangling import that fails the next build, which is strictly worse
48
+ * than a no-op here if the file turns out not to match (hand-edited
49
+ * since). No-ops (rather than throwing) when the file is missing
50
+ * entirely, or the expected import/array-entry shape isn't found. */
51
+ export declare function removeToolFromIndex(toolsIndexPath: string, toolFileName: string, exportName: string): void;
41
52
  /** Generates agents/<agentName>/tools/<spec.name>.ts, registers it in
42
53
  * that agent's tools/index.ts, then imports the file it just wrote and
43
54
  * returns the real, callable ToolDefinition object — so the route
@@ -191,15 +191,52 @@ 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
196
+ // before a re-add, or simply calling this twice by mistake) gets a
197
+ // no-op the second time instead of a duplicate `import { X }`
198
+ // declaration, which is a TypeScript syntax error ("Duplicate
199
+ // identifier"), not just a harmless extra line.
200
+ const arrayEntries = arrayMatch[1]
201
+ .split(',')
202
+ .map((e) => e.trim())
203
+ .filter(Boolean);
204
+ if (arrayEntries.includes(exportName))
205
+ return;
194
206
  const importLine = `import { ${exportName} } from './${toolFileName}.js'\n`;
195
207
  const lastImportMatch = [...source.matchAll(/^import .+\n/gm)].pop();
196
208
  const insertAt = lastImportMatch ? lastImportMatch.index + lastImportMatch[0].length : 0;
197
209
  const withImport = source.slice(0, insertAt) + importLine + source.slice(insertAt);
198
- const existingEntries = arrayMatch[1].trim();
199
- const newEntries = existingEntries ? `${existingEntries}, ${exportName}` : exportName;
210
+ const newEntries = [...arrayEntries, exportName].join(', ');
200
211
  const withEntry = withImport.replace(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/, `export const tools: ToolDefinition[] = [${newEntries}]`);
201
212
  writeFileSync(toolsIndexPath, withEntry);
202
213
  }
214
+ /** addToolToIndex's own undo — removes the named tool's import line and
215
+ * array entry. Best-effort, not "refuse rather than guess" like
216
+ * addToolToIndex: this only ever runs against a tool package-manager.ts
217
+ * itself installed (so the exact shape addToolToIndex wrote is known),
218
+ * and the caller (removePackage) has already deleted the tool's .ts
219
+ * file — leaving a stale entry behind, the previous behavior, means a
220
+ * dangling import that fails the next build, which is strictly worse
221
+ * than a no-op here if the file turns out not to match (hand-edited
222
+ * since). No-ops (rather than throwing) when the file is missing
223
+ * entirely, or the expected import/array-entry shape isn't found. */
224
+ export function removeToolFromIndex(toolsIndexPath, toolFileName, exportName) {
225
+ if (!existsSync(toolsIndexPath))
226
+ return;
227
+ const source = readFileSync(toolsIndexPath, 'utf8');
228
+ const importLineRe = new RegExp(`^import \\{ ${exportName} \\} from '\\./${toolFileName}\\.js'\\n`, 'm');
229
+ const withoutImport = source.replace(importLineRe, '');
230
+ const arrayMatch = withoutImport.match(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/);
231
+ if (!arrayMatch)
232
+ return;
233
+ const remainingEntries = arrayMatch[1]
234
+ .split(',')
235
+ .map((e) => e.trim())
236
+ .filter((e) => e && e !== exportName);
237
+ const withEntry = withoutImport.replace(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/, `export const tools: ToolDefinition[] = [${remainingEntries.join(', ')}]`);
238
+ writeFileSync(toolsIndexPath, withEntry);
239
+ }
203
240
  // The spec is persisted alongside the generated .ts file, under the same
204
241
  // name — this is what lets the Tools tab's Edit button repopulate the
205
242
  // form later without reverse-parsing generated code back into a spec,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",