loopengine 0.1.3 → 0.1.5
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/core/durable-approvals.d.ts +106 -0
- package/dist/core/durable-approvals.js +158 -0
- package/dist/core/gateway-tools.js +10 -9
- package/dist/index.d.ts +26 -1
- package/dist/index.js +27 -1
- package/dist/web/actauth-admin.d.ts +36 -0
- package/dist/web/actauth-admin.js +108 -0
- package/dist/web/agent-file-admin.d.ts +30 -0
- package/dist/web/agent-file-admin.js +165 -0
- package/dist/web/agents-config-page.d.ts +1 -0
- package/dist/web/agents-config-page.js +1807 -0
- package/dist/web/agents-list-page.d.ts +1 -0
- package/dist/web/agents-list-page.js +246 -0
- package/dist/web/dev-ui-styles.d.ts +1 -0
- package/dist/web/dev-ui-styles.js +63 -0
- package/dist/web/global-config-page.d.ts +1 -0
- package/dist/web/global-config-page.js +303 -0
- package/dist/web/global-config.d.ts +57 -0
- package/dist/web/global-config.js +67 -0
- package/dist/web/http-tool-admin.d.ts +37 -0
- package/dist/web/http-tool-admin.js +223 -0
- package/dist/web/playground.d.ts +1 -0
- package/dist/web/playground.js +1271 -0
- package/dist/web/skills-admin.d.ts +23 -0
- package/dist/web/skills-admin.js +67 -0
- package/dist/web/web-approver.d.ts +35 -0
- package/dist/web/web-approver.js +88 -0
- package/package.json +5 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getComposioAuthStatus } from '../core/gateway-tools.js';
|
|
2
|
+
// One entry per provider agent-config.ts's own AgentModelConfig union
|
|
3
|
+
// actually supports (see its own doc comment) — kept here, not derived
|
|
4
|
+
// from that type, since there's no runtime list to introspect a TS
|
|
5
|
+
// union from; adding a fourth provider means one more line here and one
|
|
6
|
+
// more arm in that union, not a rewrite.
|
|
7
|
+
const KNOWN_PROVIDERS = [
|
|
8
|
+
{ provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY' },
|
|
9
|
+
{ provider: 'openai', envVar: 'OPENAI_API_KEY' },
|
|
10
|
+
{ provider: 'deepseek', envVar: 'DEEPSEEK_API_KEY' },
|
|
11
|
+
];
|
|
12
|
+
/** Every registered agent's resolved model, alongside whether each
|
|
13
|
+
* supported provider actually has credentials available in this
|
|
14
|
+
* process's environment — an operator's first question when a request
|
|
15
|
+
* fails isn't "what does this one agent request?" (Overview's own Model
|
|
16
|
+
* section already answers that) but "is this deployment even set up to
|
|
17
|
+
* call that provider at all?". Read-only: providers/env vars are wired
|
|
18
|
+
* into the actual model-call modules (model-calls/*.ts), not something
|
|
19
|
+
* this page can add support for by itself. An agent with a custom
|
|
20
|
+
* `createModelCall` (no `config.model`) is skipped — there's no
|
|
21
|
+
* provider/model pair to report for it, the same "custom" case
|
|
22
|
+
* describeAgent's own model field already carves out.
|
|
23
|
+
*
|
|
24
|
+
* Takes the registered agent names and a config lookup as parameters,
|
|
25
|
+
* rather than importing core/agent-registry.ts's listAgents/getEntry
|
|
26
|
+
* directly — that module is this repo's own reference app, not part of
|
|
27
|
+
* the published package (see index.ts's own header comment), and its
|
|
28
|
+
* top-level-await agent scan is resolved relative to *its own* file
|
|
29
|
+
* location, which would be meaningless run from inside node_modules.
|
|
30
|
+
* Every caller already has its own local registry's listAgents/getEntry
|
|
31
|
+
* on hand (adapters/http.ts, in this repo or a scaffolded project alike)
|
|
32
|
+
* to pass straight through. */
|
|
33
|
+
export function describeModelProviders(agentNames, getConfig) {
|
|
34
|
+
const providers = KNOWN_PROVIDERS.map(({ provider, envVar }) => ({
|
|
35
|
+
provider,
|
|
36
|
+
envVar,
|
|
37
|
+
configured: Boolean(process.env[envVar]),
|
|
38
|
+
}));
|
|
39
|
+
const agents = [];
|
|
40
|
+
for (const name of agentNames) {
|
|
41
|
+
const config = getConfig(name);
|
|
42
|
+
if (config?.model) {
|
|
43
|
+
agents.push({ agent: name, provider: config.model.provider, model: config.model.model ?? '(provider default)' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { providers, agents };
|
|
47
|
+
}
|
|
48
|
+
/** The provider-level counterpart to gateway-tools.ts's own registry
|
|
49
|
+
* (which is scoped to one agent's chosen tools within an already-
|
|
50
|
+
* connected provider) — this is "is Composio itself usable from this
|
|
51
|
+
* machine at all", not "what has agent X registered". Nango/Arcade/
|
|
52
|
+
* Scalekit are listed as not-yet-supported placeholders, matching
|
|
53
|
+
* gateway-tools.ts's own header comment about them slotting in later as
|
|
54
|
+
* thin mcpplug ToolSource adapters once this mechanism is proven —
|
|
55
|
+
* shown so the Gateways panel isn't silently missing them, not because
|
|
56
|
+
* there's anything to check for them yet. */
|
|
57
|
+
export async function describeGateways(cliCommand = 'composio') {
|
|
58
|
+
const composio = await getComposioAuthStatus(cliCommand);
|
|
59
|
+
return {
|
|
60
|
+
gateways: [
|
|
61
|
+
{ provider: 'composio', supported: true, connected: composio.connected, email: composio.email, org: composio.org },
|
|
62
|
+
{ provider: 'scalekit', supported: false, connected: false },
|
|
63
|
+
{ provider: 'nango', supported: false, connected: false },
|
|
64
|
+
{ provider: 'arcade', supported: false, connected: false },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../core/agent-config.js';
|
|
2
|
+
export declare class HttpToolNameError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export declare class HttpToolExistsError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export declare class HttpToolIndexShapeError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export interface HttpToolField {
|
|
9
|
+
name: string;
|
|
10
|
+
type: 'string' | 'number' | 'boolean';
|
|
11
|
+
description?: string;
|
|
12
|
+
required: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface HttpToolSpec {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
fields: HttpToolField[];
|
|
18
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
19
|
+
url: string;
|
|
20
|
+
headers: {
|
|
21
|
+
key: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}[];
|
|
24
|
+
sendFieldsAsJsonBody: boolean;
|
|
25
|
+
responseJsonPath?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Generates agents/<agentName>/tools/<spec.name>.ts, registers it in
|
|
28
|
+
* that agent's tools/index.ts, then imports the file it just wrote and
|
|
29
|
+
* returns the real, callable ToolDefinition object — so the route
|
|
30
|
+
* handler (adapters/http.ts's POST /agents/:name/tools/http) can splice
|
|
31
|
+
* it straight into the live agent registry (core/agent-registry.ts's
|
|
32
|
+
* updateAgent) without guessing an export name or re-deriving anything
|
|
33
|
+
* this module already knows. */
|
|
34
|
+
export declare function createHttpTool(agentName: string, spec: HttpToolSpec): Promise<{
|
|
35
|
+
path: string;
|
|
36
|
+
tool: ToolDefinition;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Lets an operator generate a real, hand-editable tool file from a
|
|
2
|
+
// constrained form — the Tools tab's fourth section, alongside Local
|
|
3
|
+
// tools/Subagents/Gateway Tools — instead of writing agents/<name>/
|
|
4
|
+
// tools/<tool>.ts by hand. Deliberately narrow: the only thing this
|
|
5
|
+
// generates is a parameterized HTTP call (method, a {field}-templated
|
|
6
|
+
// URL, headers, an optional JSON body/response-path mapping), never
|
|
7
|
+
// arbitrary code. A SKILL.md-style "store code as markdown, eval it at
|
|
8
|
+
// request time" version of this was considered and rejected: SKILL.md
|
|
9
|
+
// is safe specifically because its content is only ever read by the
|
|
10
|
+
// model, never executed — this generates a normal, readable, type-
|
|
11
|
+
// checked .ts file instead, the same class of artifact a human would
|
|
12
|
+
// have written by hand, just templated for one well-understood shape.
|
|
13
|
+
//
|
|
14
|
+
// Field/URL/header substitution is compiled into the *generated code*
|
|
15
|
+
// (a template literal with ${...} interpolations), not resolved at
|
|
16
|
+
// generation time — so a value is only ever combined with the URL via
|
|
17
|
+
// real encodeURIComponent, at call time, off whatever the model actually
|
|
18
|
+
// passed as input. A header value may also reference {{ENV_VAR}} — the
|
|
19
|
+
// generated code reads that from process.env at call time; the literal
|
|
20
|
+
// secret itself is never accepted by this module, persisted to the
|
|
21
|
+
// generated file, or echoed back through the admin API, the same
|
|
22
|
+
// "always env-sourced, never a config literal" rule SendEmail/
|
|
23
|
+
// HttpNotifierConfig's own secrets already follow (core/agent-config.ts).
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
import { agentDir } from '../core/gateway-tools.js';
|
|
28
|
+
export class HttpToolNameError extends Error {
|
|
29
|
+
}
|
|
30
|
+
export class HttpToolExistsError extends Error {
|
|
31
|
+
}
|
|
32
|
+
export class HttpToolIndexShapeError extends Error {
|
|
33
|
+
}
|
|
34
|
+
const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
35
|
+
const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
36
|
+
const ENV_VAR_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
37
|
+
// Same escaping bin/cli.ts's own local tsStringLiteral uses for a
|
|
38
|
+
// single-quoted literal — duplicated here rather than imported, since
|
|
39
|
+
// it's two lines and not worth a cross-module export for.
|
|
40
|
+
function tsStringLiteral(value) {
|
|
41
|
+
return "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
|
|
42
|
+
}
|
|
43
|
+
// snake_case tool name -> camelCase export identifier, e.g.
|
|
44
|
+
// "lookup_order_status" -> "lookupOrderStatus" — matches every
|
|
45
|
+
// hand-written tool file's own convention (see agents/customer-service/
|
|
46
|
+
// tools/*.ts: issue_refund.ts exports `issueRefund`, etc.).
|
|
47
|
+
function toCamelCase(snakeCase) {
|
|
48
|
+
return snakeCase.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
49
|
+
}
|
|
50
|
+
// Escapes a literal segment for safe interpolation inside a generated
|
|
51
|
+
// JS template literal (backtick string) — the three characters that
|
|
52
|
+
// would otherwise break out of it or start a nested interpolation.
|
|
53
|
+
function escapeTemplateLiteralSegment(segment) {
|
|
54
|
+
return segment.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
55
|
+
}
|
|
56
|
+
/** Compiles a "{field}" template string into the *inside* of a JS
|
|
57
|
+
* template literal (caller wraps it in backticks) — each {field}
|
|
58
|
+
* becomes `${wrap(fieldExpr)}`, everything else is a literal segment,
|
|
59
|
+
* escaped for backtick safety. Throws if a referenced field isn't one
|
|
60
|
+
* of `fieldNames` — a typo'd placeholder should fail at generation
|
|
61
|
+
* time, not silently produce "undefined" in a live request later. */
|
|
62
|
+
function compileFieldTemplate(template, fieldNames, wrap) {
|
|
63
|
+
let out = '';
|
|
64
|
+
let lastIndex = 0;
|
|
65
|
+
const pattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = pattern.exec(template))) {
|
|
68
|
+
out += escapeTemplateLiteralSegment(template.slice(lastIndex, match.index));
|
|
69
|
+
const field = match[1];
|
|
70
|
+
if (!fieldNames.has(field)) {
|
|
71
|
+
throw new HttpToolNameError(`Template references {${field}}, which isn't one of this tool's own fields.`);
|
|
72
|
+
}
|
|
73
|
+
out += '${' + wrap(`input.${field}`) + '}';
|
|
74
|
+
lastIndex = pattern.lastIndex;
|
|
75
|
+
}
|
|
76
|
+
out += escapeTemplateLiteralSegment(template.slice(lastIndex));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function generateToolCode(spec) {
|
|
80
|
+
const fieldNames = new Set(spec.fields.map((f) => f.name));
|
|
81
|
+
for (const field of spec.fields) {
|
|
82
|
+
if (!FIELD_NAME_PATTERN.test(field.name)) {
|
|
83
|
+
throw new HttpToolNameError(`Field name "${field.name}" must be a valid identifier (letters, digits, underscore, not starting with a digit).`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Every distinct {{ENV_VAR}} referenced across header values becomes
|
|
87
|
+
// one `const NAME = process.env.NAME` read (with a clear failure if
|
|
88
|
+
// unset), generated once up front — cleaner than inlining a
|
|
89
|
+
// read-and-check expression at every point of use, and it means a
|
|
90
|
+
// missing secret fails loudly the moment the tool actually runs, not
|
|
91
|
+
// silently as an empty header value.
|
|
92
|
+
const envVars = new Set();
|
|
93
|
+
const headerEntries = spec.headers.map(({ key, value }) => {
|
|
94
|
+
let compiled = '';
|
|
95
|
+
let lastIndex = 0;
|
|
96
|
+
const pattern = /\{\{([A-Z_][A-Z0-9_]*)\}\}|\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = pattern.exec(value))) {
|
|
99
|
+
compiled += escapeTemplateLiteralSegment(value.slice(lastIndex, match.index));
|
|
100
|
+
if (match[1]) {
|
|
101
|
+
if (!ENV_VAR_PATTERN.test(match[1])) {
|
|
102
|
+
throw new HttpToolNameError(`"{{${match[1]}}}" isn't a valid env var name (uppercase letters, digits, underscore).`);
|
|
103
|
+
}
|
|
104
|
+
envVars.add(match[1]);
|
|
105
|
+
compiled += '${' + match[1] + '}';
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const field = match[2];
|
|
109
|
+
if (!fieldNames.has(field)) {
|
|
110
|
+
throw new HttpToolNameError(`Header "${key}" references {${field}}, which isn't one of this tool's own fields.`);
|
|
111
|
+
}
|
|
112
|
+
compiled += '${String(input.' + field + ')}';
|
|
113
|
+
}
|
|
114
|
+
lastIndex = pattern.lastIndex;
|
|
115
|
+
}
|
|
116
|
+
compiled += escapeTemplateLiteralSegment(value.slice(lastIndex));
|
|
117
|
+
return { key, compiled };
|
|
118
|
+
});
|
|
119
|
+
const urlInner = compileFieldTemplate(spec.url, fieldNames, (expr) => `encodeURIComponent(String(${expr}))`);
|
|
120
|
+
const properties = spec.fields
|
|
121
|
+
.map((f) => ` ${JSON.stringify(f.name)}: { type: ${JSON.stringify(f.type)}${f.description ? `, description: ${JSON.stringify(f.description)}` : ''} },`)
|
|
122
|
+
.join('\n');
|
|
123
|
+
const required = spec.fields.filter((f) => f.required).map((f) => JSON.stringify(f.name));
|
|
124
|
+
const envReads = [...envVars]
|
|
125
|
+
.map((name) => ` const ${name} = process.env.${name}\n if (!${name}) throw new Error(${tsStringLiteral(`${spec.name}: ${name} is not set`)})\n`)
|
|
126
|
+
.join('');
|
|
127
|
+
const headersObj = headerEntries.length === 0
|
|
128
|
+
? ''
|
|
129
|
+
: ` headers: {\n${headerEntries.map(({ key, compiled }) => ` ${JSON.stringify(key)}: \`${compiled}\`,`).join('\n')}\n },\n`;
|
|
130
|
+
const bodyLine = spec.sendFieldsAsJsonBody && spec.method !== 'GET' && spec.method !== 'DELETE' ? ' body: JSON.stringify(input),\n' : '';
|
|
131
|
+
const responseHandling = spec.responseJsonPath
|
|
132
|
+
? (() => {
|
|
133
|
+
const segments = spec.responseJsonPath.split('.').filter(Boolean);
|
|
134
|
+
for (const seg of segments) {
|
|
135
|
+
if (!FIELD_NAME_PATTERN.test(seg)) {
|
|
136
|
+
throw new HttpToolNameError(`responseJsonPath segment "${seg}" must be a valid identifier.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const access = segments.map((s) => `?.${s}`).join('');
|
|
140
|
+
return ` const body = await res.json()\n return JSON.stringify(body${access} ?? null)\n`;
|
|
141
|
+
})()
|
|
142
|
+
: ' return await res.text()\n';
|
|
143
|
+
return `// Generated by the Admin UI's HTTP tool builder (Tools tab) — a
|
|
144
|
+
// 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).
|
|
148
|
+
import type { ToolDefinition } from 'loopengine'
|
|
149
|
+
|
|
150
|
+
export const ${toCamelCase(spec.name)}: ToolDefinition = {
|
|
151
|
+
name: ${tsStringLiteral(spec.name)},
|
|
152
|
+
description: ${tsStringLiteral(spec.description)},
|
|
153
|
+
input_schema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
${properties}
|
|
157
|
+
},
|
|
158
|
+
required: [${required.join(', ')}],
|
|
159
|
+
},
|
|
160
|
+
execute: async (input) => {
|
|
161
|
+
${envReads} const res = await fetch(\`${urlInner}\`, {
|
|
162
|
+
method: ${tsStringLiteral(spec.method)},
|
|
163
|
+
${headersObj}${bodyLine} })
|
|
164
|
+
if (!res.ok) throw new Error(\`${spec.name}: HTTP \${res.status}\`)
|
|
165
|
+
${responseHandling} },
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
// tools/index.ts's own shape is deliberately simple and consistent
|
|
170
|
+
// across every real example in this repo (agents/customer-service/
|
|
171
|
+
// tools/index.ts, create-loopengine's own template) — a handful of
|
|
172
|
+
// named imports, then one `export const tools: ToolDefinition[] = [...]`
|
|
173
|
+
// array literal. This patches that exact shape with two small string
|
|
174
|
+
// edits (one new import line, one new array entry) rather than a full
|
|
175
|
+
// TS-AST rewrite; if the file has been hand-edited into something this
|
|
176
|
+
// pattern doesn't recognize, it refuses instead of risking corrupting it
|
|
177
|
+
// — same "refuse rather than guess" discipline agent-file-admin.ts's own
|
|
178
|
+
// doc comment already establishes for index.ts edits.
|
|
179
|
+
function addToolToIndex(toolsIndexPath, toolFileName, exportName) {
|
|
180
|
+
const source = readFileSync(toolsIndexPath, 'utf8');
|
|
181
|
+
const arrayMatch = source.match(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/);
|
|
182
|
+
if (!arrayMatch) {
|
|
183
|
+
throw new HttpToolIndexShapeError(`${toolsIndexPath} doesn't match the expected "export const tools: ToolDefinition[] = [...]" shape — add this tool to it by hand instead.`);
|
|
184
|
+
}
|
|
185
|
+
const importLine = `import { ${exportName} } from './${toolFileName}.js'\n`;
|
|
186
|
+
const lastImportMatch = [...source.matchAll(/^import .+\n/gm)].pop();
|
|
187
|
+
const insertAt = lastImportMatch ? lastImportMatch.index + lastImportMatch[0].length : 0;
|
|
188
|
+
const withImport = source.slice(0, insertAt) + importLine + source.slice(insertAt);
|
|
189
|
+
const existingEntries = arrayMatch[1].trim();
|
|
190
|
+
const newEntries = existingEntries ? `${existingEntries}, ${exportName}` : exportName;
|
|
191
|
+
const withEntry = withImport.replace(/export const tools: ToolDefinition\[\] = \[([^\]]*)\]/, `export const tools: ToolDefinition[] = [${newEntries}]`);
|
|
192
|
+
writeFileSync(toolsIndexPath, withEntry);
|
|
193
|
+
}
|
|
194
|
+
/** Generates agents/<agentName>/tools/<spec.name>.ts, registers it in
|
|
195
|
+
* that agent's tools/index.ts, then imports the file it just wrote and
|
|
196
|
+
* returns the real, callable ToolDefinition object — so the route
|
|
197
|
+
* handler (adapters/http.ts's POST /agents/:name/tools/http) can splice
|
|
198
|
+
* it straight into the live agent registry (core/agent-registry.ts's
|
|
199
|
+
* updateAgent) without guessing an export name or re-deriving anything
|
|
200
|
+
* this module already knows. */
|
|
201
|
+
export async function createHttpTool(agentName, spec) {
|
|
202
|
+
if (!TOOL_NAME_PATTERN.test(spec.name)) {
|
|
203
|
+
throw new HttpToolNameError(`Tool name must be lowercase snake_case (e.g. "lookup_order_status") — got "${spec.name}"`);
|
|
204
|
+
}
|
|
205
|
+
const toolsDir = join(agentDir(agentName), 'tools');
|
|
206
|
+
const toolPath = join(toolsDir, `${spec.name}.ts`);
|
|
207
|
+
if (existsSync(toolPath)) {
|
|
208
|
+
throw new HttpToolExistsError(`agents/${agentName}/tools/${spec.name}.ts already exists — pick a different name or edit it directly.`);
|
|
209
|
+
}
|
|
210
|
+
const code = generateToolCode(spec);
|
|
211
|
+
mkdirSync(toolsDir, { recursive: true });
|
|
212
|
+
writeFileSync(toolPath, code);
|
|
213
|
+
const indexPath = join(toolsDir, 'index.ts');
|
|
214
|
+
if (existsSync(indexPath)) {
|
|
215
|
+
addToolToIndex(indexPath, spec.name, toCamelCase(spec.name));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
writeFileSync(indexPath, `import type { ToolDefinition } from 'loopengine'\nimport { ${toCamelCase(spec.name)} } from './${spec.name}.js'\n\nexport const tools: ToolDefinition[] = [${toCamelCase(spec.name)}]\n`);
|
|
219
|
+
}
|
|
220
|
+
const exportName = toCamelCase(spec.name);
|
|
221
|
+
const mod = (await import(pathToFileURL(toolPath).href));
|
|
222
|
+
return { path: toolPath, tool: mod[exportName] };
|
|
223
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const playgroundHtml: string;
|