loopengine 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ export { listQuestions, answerQuestion as resolvePendingQuestion, findQuestion,
33
33
  export type { PendingQuestion } from './core/system-tools/index.js';
34
34
  export { editAgentFile, AgentEditNotSupportedError, AgentFileNotFoundError } from './web/agent-file-admin.js';
35
35
  export type { AgentEditableFields, AgentEditResult } from './web/agent-file-admin.js';
36
- export { createHttpTool, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError } from './web/http-tool-admin.js';
36
+ export { createHttpTool, updateHttpTool, readHttpToolSpec, listEditableHttpToolNames, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError, HttpToolNotFoundError, HttpToolNotEditableError, } from './web/http-tool-admin.js';
37
37
  export type { HttpToolSpec, HttpToolField } from './web/http-tool-admin.js';
38
38
  export { agentsConfigPageHtml } from './web/agents-config-page.js';
39
39
  export { agentsListPageHtml } from './web/agents-list-page.js';
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ export { WebhookNotifier } from './core/http-notify-triggers/webhook.js';
37
37
  // question in-process, aliased to avoid colliding with that name.
38
38
  export { listQuestions, answerQuestion as resolvePendingQuestion, findQuestion, createAskUserTool } from './core/system-tools/index.js';
39
39
  export { editAgentFile, AgentEditNotSupportedError, AgentFileNotFoundError } from './web/agent-file-admin.js';
40
- export { createHttpTool, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError } from './web/http-tool-admin.js';
40
+ export { createHttpTool, updateHttpTool, readHttpToolSpec, listEditableHttpToolNames, HttpToolNameError, HttpToolExistsError, HttpToolIndexShapeError, HttpToolNotFoundError, HttpToolNotEditableError, } from './web/http-tool-admin.js';
41
41
  export { agentsConfigPageHtml } from './web/agents-config-page.js';
42
42
  export { agentsListPageHtml } from './web/agents-list-page.js';
43
43
  export { globalConfigPageHtml } from './web/global-config-page.js';
@@ -581,6 +581,28 @@ export const agentsConfigPageHtml = `<!doctype html>
581
581
  return '<table><thead><tr><th>Name</th><th>Description</th><th>Parallel-safe</th></tr></thead><tbody>' + rows + '</tbody></table>';
582
582
  }
583
583
 
584
+ // Local tools' own table, unlike renderTools above (shared by Agent as
585
+ // Tools, which has nothing editable through this UI) — adds an Actions
586
+ // column with an Edit button, but only for tools describeAgent flagged
587
+ // httpToolEditable: those created through this same Tools tab's HTTP
588
+ // tool builder (web/http-tool-admin.ts's own sidecar spec file is what
589
+ // that flag is actually derived from). A hand-written tools/<name>.ts
590
+ // has no spec to repopulate an edit form from, so it just gets a blank
591
+ // cell — not a button that would 404 the moment it's clicked.
592
+ function renderLocalTools(tools) {
593
+ if (!tools.length) return '<p class="muted">No tools.</p>';
594
+ var rows = tools.map(function (t) {
595
+ var actions = t.httpToolEditable
596
+ ? '<button type="button" class="edit-http-tool-btn" data-name="' + escapeHtml(t.name) + '">Edit</button>'
597
+ : '';
598
+ return '<tr><td><code>' + escapeHtml(t.name) + '</code></td>' +
599
+ '<td>' + escapeHtml(t.description) + '</td>' +
600
+ '<td>' + (t.safe ? 'yes' : 'no') + '</td>' +
601
+ '<td>' + actions + '</td></tr>';
602
+ }).join('');
603
+ return '<table><thead><tr><th>Name</th><th>Description</th><th>Parallel-safe</th><th></th></tr></thead><tbody>' + rows + '</tbody></table>';
604
+ }
605
+
584
606
  // Overview's own tools table, unlike the Tools tab's three separately-
585
607
  // headed sections (Local tools / Gateway Tools / Agent as Tools), is
586
608
  // one flat list (cfg.tools) — without a Type column there's no way to
@@ -1474,8 +1496,8 @@ export const agentsConfigPageHtml = `<!doctype html>
1474
1496
  // describeGatewayTools), a real cost not worth paying up front. ----
1475
1497
 
1476
1498
  function renderToolsTabHtml(cfg) {
1477
- return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderTools(cfg.localTools) +
1478
- '<div class="admin-subsection"><h4>Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
1499
+ return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
1500
+ '<div class="admin-subsection"><h4 id="httpToolFormHeading">Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
1479
1501
  '</section>' +
1480
1502
  '<section id="gatewayToolsSection">' +
1481
1503
  '<h3>Gateway Tools <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
@@ -1521,7 +1543,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1521
1543
  httpToolHeaderRowCount = 0;
1522
1544
  return '<form class="add-source" id="httpToolForm">' +
1523
1545
  '<label>Name <span class="hint">(snake_case, e.g. lookup_order_status)</span>' +
1524
- '<input type="text" name="name" required pattern="[a-z][a-z0-9_]*" placeholder="lookup_order_status">' +
1546
+ '<input type="text" name="name" id="httpToolNameInput" required pattern="[a-z][a-z0-9_]*" placeholder="lookup_order_status">' +
1525
1547
  '</label>' +
1526
1548
  '<label>Description' +
1527
1549
  '<textarea name="description" id="httpToolDescriptionInput" required placeholder="What this tool does"></textarea>' +
@@ -1550,7 +1572,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1550
1572
  '<label>Response JSON path <span class="hint">(optional &mdash; e.g. data.status; leave blank to return the raw response text)</span>' +
1551
1573
  '<input type="text" name="responseJsonPath" placeholder="data.status">' +
1552
1574
  '</label>' +
1553
- '<label>Grant permission <span class="hint">(optional &mdash; leave blank to leave this tool at actauth&#39;s own default, typically deny)</span>' +
1575
+ '<label id="httpToolDecisionField">Grant permission <span class="hint">(optional &mdash; leave blank to leave this tool at actauth&#39;s own default, typically deny)</span>' +
1554
1576
  '<select name="decision">' +
1555
1577
  '<option value="">(leave unset)</option>' +
1556
1578
  '<option value="allow">allow</option>' +
@@ -1559,6 +1581,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1559
1581
  '</select>' +
1560
1582
  '</label>' +
1561
1583
  '<button type="submit" id="httpToolSubmitBtn">Create tool</button>' +
1584
+ '<button type="button" id="httpToolCancelBtn" style="display:none">Cancel</button>' +
1562
1585
  '<div id="httpToolError" class="error"></div>' +
1563
1586
  '</form>';
1564
1587
  }
@@ -1580,6 +1603,15 @@ export const agentsConfigPageHtml = `<!doctype html>
1580
1603
  var addHeaderBtn = detail.querySelector('#httpToolAddHeaderBtn');
1581
1604
  var errorEl = detail.querySelector('#httpToolError');
1582
1605
  var submitBtn = detail.querySelector('#httpToolSubmitBtn');
1606
+ var cancelBtn = detail.querySelector('#httpToolCancelBtn');
1607
+ var nameInput = detail.querySelector('#httpToolNameInput');
1608
+ var decisionField = detail.querySelector('#httpToolDecisionField');
1609
+ var headingEl = detail.querySelector('#httpToolFormHeading');
1610
+ // Non-null only while editing an existing tool — the name field is
1611
+ // disabled then (renaming isn't supported, see updateHttpTool's own
1612
+ // doc comment), so the submitted name always has to come from here,
1613
+ // never from the (excluded-from-FormData) disabled input.
1614
+ var editingHttpToolName = null;
1583
1615
 
1584
1616
  addFieldBtn.addEventListener('click', function () {
1585
1617
  fieldRows.insertAdjacentHTML('beforeend', renderHttpToolFieldRow(httpToolFieldRowCount++));
@@ -1590,6 +1622,84 @@ export const agentsConfigPageHtml = `<!doctype html>
1590
1622
  wireHttpToolRemoveButton(headerRows.lastElementChild);
1591
1623
  });
1592
1624
 
1625
+ // Repopulates the (blank, create-mode-shaped) form from a saved spec
1626
+ // — same field/header row-building addFieldBtn/addHeaderBtn already
1627
+ // do, just filled in immediately instead of left blank for typing.
1628
+ function populateHttpToolForm(spec) {
1629
+ nameInput.value = spec.name;
1630
+ form.querySelector('#httpToolDescriptionInput').value = spec.description;
1631
+
1632
+ fieldRows.innerHTML = '';
1633
+ for (var i = 0; i < spec.fields.length; i++) {
1634
+ var f = spec.fields[i];
1635
+ fieldRows.insertAdjacentHTML('beforeend', renderHttpToolFieldRow(httpToolFieldRowCount++));
1636
+ var row = fieldRows.lastElementChild;
1637
+ row.querySelector('.http-tool-field-name').value = f.name;
1638
+ row.querySelector('.http-tool-field-type').value = f.type;
1639
+ row.querySelector('.http-tool-field-description').value = f.description || '';
1640
+ row.querySelector('.http-tool-field-required').checked = f.required;
1641
+ wireHttpToolRemoveButton(row);
1642
+ }
1643
+
1644
+ form.querySelector('[name="method"]').value = spec.method;
1645
+ form.querySelector('[name="url"]').value = spec.url;
1646
+
1647
+ headerRows.innerHTML = '';
1648
+ for (var j = 0; j < spec.headers.length; j++) {
1649
+ var h = spec.headers[j];
1650
+ headerRows.insertAdjacentHTML('beforeend', renderHttpToolHeaderRow(httpToolHeaderRowCount++));
1651
+ var hrow = headerRows.lastElementChild;
1652
+ hrow.querySelector('.http-tool-header-key').value = h.key;
1653
+ hrow.querySelector('.http-tool-header-value').value = h.value;
1654
+ wireHttpToolRemoveButton(hrow);
1655
+ }
1656
+
1657
+ form.querySelector('[name="sendFieldsAsJsonBody"]').checked = spec.sendFieldsAsJsonBody;
1658
+ form.querySelector('[name="responseJsonPath"]').value = spec.responseJsonPath || '';
1659
+ }
1660
+
1661
+ function resetHttpToolForm() {
1662
+ form.reset();
1663
+ fieldRows.innerHTML = '';
1664
+ headerRows.innerHTML = '';
1665
+ nameInput.disabled = false;
1666
+ if (headingEl) headingEl.textContent = 'Create an HTTP tool';
1667
+ submitBtn.textContent = 'Create tool';
1668
+ cancelBtn.style.display = 'none';
1669
+ decisionField.style.display = '';
1670
+ errorEl.textContent = '';
1671
+ editingHttpToolName = null;
1672
+ }
1673
+
1674
+ cancelBtn.addEventListener('click', resetHttpToolForm);
1675
+
1676
+ // Local tools table's own Edit buttons (renderLocalTools) — only
1677
+ // present for tools describeAgent flagged httpToolEditable, so this
1678
+ // GET should never actually 404 in normal use.
1679
+ var editButtons = detail.querySelectorAll('.edit-http-tool-btn');
1680
+ for (var k = 0; k < editButtons.length; k++) {
1681
+ editButtons[k].addEventListener('click', function (ev) {
1682
+ var toolName = ev.currentTarget.getAttribute('data-name');
1683
+ errorEl.textContent = '';
1684
+ fetch('/agents/' + encodeURIComponent(name) + '/tools/http/' + encodeURIComponent(toolName))
1685
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
1686
+ .then(function (result) {
1687
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
1688
+ populateHttpToolForm(result.body);
1689
+ nameInput.disabled = true;
1690
+ if (headingEl) headingEl.textContent = 'Edit HTTP tool "' + toolName + '"';
1691
+ submitBtn.textContent = 'Save';
1692
+ cancelBtn.style.display = '';
1693
+ decisionField.style.display = 'none';
1694
+ editingHttpToolName = toolName;
1695
+ form.scrollIntoView({ block: 'nearest' });
1696
+ })
1697
+ .catch(function (err) {
1698
+ errorEl.textContent = 'Could not load tool "' + toolName + '": ' + err.message;
1699
+ });
1700
+ });
1701
+ }
1702
+
1593
1703
  form.addEventListener('submit', function (ev) {
1594
1704
  ev.preventDefault();
1595
1705
  errorEl.textContent = '';
@@ -1619,7 +1729,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1619
1729
 
1620
1730
  var formData = new FormData(form);
1621
1731
  var payload = {
1622
- name: formData.get('name'),
1732
+ name: editingHttpToolName || formData.get('name'),
1623
1733
  description: formData.get('description'),
1624
1734
  fields: fields,
1625
1735
  method: formData.get('method'),
@@ -1630,9 +1740,12 @@ export const agentsConfigPageHtml = `<!doctype html>
1630
1740
  decision: formData.get('decision') ? formData.get('decision') : undefined,
1631
1741
  };
1632
1742
 
1743
+ var isEdit = !!editingHttpToolName;
1744
+ var url = '/agents/' + encodeURIComponent(name) + '/tools/http' + (isEdit ? '/' + encodeURIComponent(editingHttpToolName) : '');
1745
+
1633
1746
  submitBtn.disabled = true;
1634
- fetch('/agents/' + encodeURIComponent(name) + '/tools/http', {
1635
- method: 'POST',
1747
+ fetch(url, {
1748
+ method: isEdit ? 'PUT' : 'POST',
1636
1749
  headers: { 'content-type': 'application/json' },
1637
1750
  body: JSON.stringify(payload),
1638
1751
  })
@@ -5,6 +5,10 @@ export declare class HttpToolExistsError extends Error {
5
5
  }
6
6
  export declare class HttpToolIndexShapeError extends Error {
7
7
  }
8
+ export declare class HttpToolNotFoundError extends Error {
9
+ }
10
+ export declare class HttpToolNotEditableError extends Error {
11
+ }
8
12
  export interface HttpToolField {
9
13
  name: string;
10
14
  type: 'string' | 'number' | 'boolean';
@@ -35,3 +39,26 @@ export declare function createHttpTool(agentName: string, spec: HttpToolSpec): P
35
39
  path: string;
36
40
  tool: ToolDefinition;
37
41
  }>;
42
+ /** Reads back the spec a previous createHttpTool/updateHttpTool call
43
+ * persisted, for the Tools tab's Edit form to repopulate itself from —
44
+ * or null if this tool was never created through this module (a
45
+ * hand-written tools/<name>.ts, or one from before this sidecar existed),
46
+ * which is exactly the signal the route handler uses to decide whether
47
+ * to even show an Edit button for it. */
48
+ export declare function readHttpToolSpec(agentName: string, toolName: string): HttpToolSpec | null;
49
+ /** Every tool name in this agent's tools/ dir with a saved spec — i.e.
50
+ * every tool the Tools tab can offer an Edit button for. Cheap (one
51
+ * readdirSync), safe to call on every GET /agents/:name/config. */
52
+ export declare function listEditableHttpToolNames(agentName: string): string[];
53
+ /** Overwrites an existing admin-created tool in place — same generated-
54
+ * code shape createHttpTool produces, just written to the file that's
55
+ * already there instead of a new one, and without touching tools/index.ts
56
+ * (the import/export names are unchanged, since renaming isn't supported
57
+ * here: spec.name must match toolName). Refuses a tool this module didn't
58
+ * create (no sidecar spec) rather than silently clobbering hand-written
59
+ * logic — same "refuse rather than guess" rule addToolToIndex's own doc
60
+ * comment already applies to index.ts. */
61
+ export declare function updateHttpTool(agentName: string, toolName: string, spec: HttpToolSpec): Promise<{
62
+ path: string;
63
+ tool: ToolDefinition;
64
+ }>;
@@ -21,7 +21,7 @@
21
21
  // generated file, or echoed back through the admin API, the same
22
22
  // "always env-sourced, never a config literal" rule SendEmail/
23
23
  // HttpNotifierConfig's own secrets already follow (core/agent-config.ts).
24
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
24
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
26
  import { pathToFileURL } from 'node:url';
27
27
  import { agentDir } from '../core/gateway-tools.js';
@@ -31,6 +31,10 @@ export class HttpToolExistsError extends Error {
31
31
  }
32
32
  export class HttpToolIndexShapeError extends Error {
33
33
  }
34
+ export class HttpToolNotFoundError extends Error {
35
+ }
36
+ export class HttpToolNotEditableError extends Error {
37
+ }
34
38
  const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
35
39
  const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
36
40
  const ENV_VAR_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
@@ -142,9 +146,13 @@ function generateToolCode(spec) {
142
146
  : ' return await res.text()\n';
143
147
  return `// Generated by the Admin UI's HTTP tool builder (Tools tab) — a
144
148
  // parameterized HTTP call, not hand-written logic. Edit freely for
145
- // anything this template can't express; the Admin UI only ever
146
- // regenerates a tool it created itself, never overwrites a file it
147
- // doesn't recognize (see web/http-tool-admin.ts's own HttpToolExistsError).
149
+ // anything this template can't express, but note that hand-editing this
150
+ // file directly (rather than through the Tools tab's own Edit button)
151
+ // leaves the sidecar ${spec.name}.http-tool.json out of sync — the next
152
+ // edit through the UI will overwrite your hand-edits with whatever that
153
+ // sidecar still says. The Admin UI only ever regenerates a tool it
154
+ // created itself, never overwrites a file it doesn't recognize (see
155
+ // web/http-tool-admin.ts's own HttpToolExistsError/HttpToolNotEditableError).
148
156
  import type { ToolDefinition } from 'loopengine'
149
157
 
150
158
  export const ${toCamelCase(spec.name)}: ToolDefinition = {
@@ -191,6 +199,15 @@ function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
191
199
  const withEntry = withImport.replace(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/, `export const tools: ToolDefinition[] = [${newEntries}]`);
192
200
  writeFileSync(toolsIndexPath, withEntry);
193
201
  }
202
+ // The spec is persisted alongside the generated .ts file, under the same
203
+ // name — this is what lets the Tools tab's Edit button repopulate the
204
+ // form later without reverse-parsing generated code back into a spec,
205
+ // and (its second job) is how readHttpToolSpec/listEditableHttpToolNames
206
+ // tell an admin-created tool apart from a hand-written one: only a tool
207
+ // this module itself created has one.
208
+ function specSidecarPath(toolsDir, name) {
209
+ return join(toolsDir, `${name}.http-tool.json`);
210
+ }
194
211
  /** Generates agents/<agentName>/tools/<spec.name>.ts, registers it in
195
212
  * that agent's tools/index.ts, then imports the file it just wrote and
196
213
  * returns the real, callable ToolDefinition object — so the route
@@ -210,6 +227,7 @@ export async function createHttpTool(agentName, spec) {
210
227
  const code = generateToolCode(spec);
211
228
  mkdirSync(toolsDir, { recursive: true });
212
229
  writeFileSync(toolPath, code);
230
+ writeFileSync(specSidecarPath(toolsDir, spec.name), JSON.stringify(spec, null, 2));
213
231
  const indexPath = join(toolsDir, 'index.ts');
214
232
  if (existsSync(indexPath)) {
215
233
  addToolToIndex(indexPath, spec.name, toCamelCase(spec.name));
@@ -221,3 +239,62 @@ export async function createHttpTool(agentName, spec) {
221
239
  const mod = (await import(pathToFileURL(toolPath).href));
222
240
  return { path: toolPath, tool: mod[exportName] };
223
241
  }
242
+ /** Reads back the spec a previous createHttpTool/updateHttpTool call
243
+ * persisted, for the Tools tab's Edit form to repopulate itself from —
244
+ * or null if this tool was never created through this module (a
245
+ * hand-written tools/<name>.ts, or one from before this sidecar existed),
246
+ * which is exactly the signal the route handler uses to decide whether
247
+ * to even show an Edit button for it. */
248
+ export function readHttpToolSpec(agentName, toolName) {
249
+ const sidecarPath = specSidecarPath(join(agentDir(agentName), 'tools'), toolName);
250
+ if (!existsSync(sidecarPath))
251
+ return null;
252
+ return JSON.parse(readFileSync(sidecarPath, 'utf8'));
253
+ }
254
+ /** Every tool name in this agent's tools/ dir with a saved spec — i.e.
255
+ * every tool the Tools tab can offer an Edit button for. Cheap (one
256
+ * readdirSync), safe to call on every GET /agents/:name/config. */
257
+ export function listEditableHttpToolNames(agentName) {
258
+ const toolsDir = join(agentDir(agentName), 'tools');
259
+ if (!existsSync(toolsDir))
260
+ return [];
261
+ return readdirSync(toolsDir)
262
+ .filter((f) => f.endsWith('.http-tool.json'))
263
+ .map((f) => f.slice(0, -'.http-tool.json'.length));
264
+ }
265
+ /** Overwrites an existing admin-created tool in place — same generated-
266
+ * code shape createHttpTool produces, just written to the file that's
267
+ * already there instead of a new one, and without touching tools/index.ts
268
+ * (the import/export names are unchanged, since renaming isn't supported
269
+ * here: spec.name must match toolName). Refuses a tool this module didn't
270
+ * create (no sidecar spec) rather than silently clobbering hand-written
271
+ * logic — same "refuse rather than guess" rule addToolToIndex's own doc
272
+ * comment already applies to index.ts. */
273
+ export async function updateHttpTool(agentName, toolName, spec) {
274
+ if (spec.name !== toolName) {
275
+ throw new HttpToolNameError(`A tool's name can't be changed by editing it — "${toolName}" vs "${spec.name}".`);
276
+ }
277
+ if (!TOOL_NAME_PATTERN.test(spec.name)) {
278
+ throw new HttpToolNameError(`Tool name must be lowercase snake_case (e.g. "lookup_order_status") — got "${spec.name}"`);
279
+ }
280
+ const toolsDir = join(agentDir(agentName), 'tools');
281
+ const toolPath = join(toolsDir, `${spec.name}.ts`);
282
+ if (!existsSync(toolPath)) {
283
+ throw new HttpToolNotFoundError(`agents/${agentName}/tools/${spec.name}.ts doesn't exist — nothing to edit.`);
284
+ }
285
+ const sidecarPath = specSidecarPath(toolsDir, spec.name);
286
+ if (!existsSync(sidecarPath)) {
287
+ throw new HttpToolNotEditableError(`agents/${agentName}/tools/${spec.name}.ts wasn't created by this form — edit the file directly instead.`);
288
+ }
289
+ const code = generateToolCode(spec);
290
+ writeFileSync(toolPath, code);
291
+ writeFileSync(sidecarPath, JSON.stringify(spec, null, 2));
292
+ const exportName = toCamelCase(spec.name);
293
+ // Cache-busted: this exact file URL was already imported once before
294
+ // (at creation, or whenever this agent's tools were last resolved), and
295
+ // Node's ESM cache is keyed by the literal URL string — a plain
296
+ // re-import would silently hand back the stale pre-edit module instead
297
+ // of the code just written above.
298
+ const mod = (await import(`${pathToFileURL(toolPath).href}?edited=${Date.now()}`));
299
+ return { path: toolPath, tool: mod[exportName] };
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",