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 +1 -1
- package/dist/index.js +1 -1
- package/dist/web/agents-config-page.js +120 -7
- package/dist/web/http-tool-admin.d.ts +27 -0
- package/dist/web/http-tool-admin.js +81 -4
- package/package.json +1 -1
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>' +
|
|
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 — 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 — leave blank to leave this tool at actauth's own default, typically deny)</span>' +
|
|
1575
|
+
'<label id="httpToolDecisionField">Grant permission <span class="hint">(optional — leave blank to leave this tool at actauth'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(
|
|
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
|
|
146
|
-
//
|
|
147
|
-
//
|
|
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
|
+
}
|