create-loopengine 0.1.17 → 0.1.19

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.17",
3
+ "version": "0.1.19",
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",
@@ -36,9 +36,14 @@ import {
36
36
  synthesizeCreateModelCall,
37
37
  editAgentFile,
38
38
  createHttpTool,
39
+ updateHttpTool,
40
+ readHttpToolSpec,
41
+ listEditableHttpToolNames,
39
42
  HttpToolNameError,
40
43
  HttpToolExistsError,
41
44
  HttpToolIndexShapeError,
45
+ HttpToolNotFoundError,
46
+ HttpToolNotEditableError,
42
47
  type HttpToolSpec,
43
48
  AgentEditNotSupportedError,
44
49
  AgentFileNotFoundError,
@@ -290,6 +295,10 @@ function describeTool(t: { name: string; description: string; safe?: boolean; in
290
295
 
291
296
  async function describeAgent(entry: RegistryEntry): Promise<Record<string, unknown>> {
292
297
  const { config } = entry
298
+ // Which localTools the Tools tab's Edit button applies to — only the
299
+ // ones with a saved spec (loopengine's own sidecar spec file), i.e.
300
+ // only tools this same admin UI generated in the first place.
301
+ const editableHttpToolNames = new Set(listEditableHttpToolNames(config.name))
293
302
  // Kept separate (not just concatenated into one `tools` array — though
294
303
  // that's still returned too, below, for Overview's own at-a-glance
295
304
  // count) so the "Tools" tab can show where each one actually comes
@@ -346,7 +355,7 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
346
355
  // schema here — never executed, so the console-prompt fallback its
347
356
  // own doc comment mentions never applies to this call.
348
357
  systemTools: [...systemTools, createAskUserTool({ agent: config.name })].map(describeTool),
349
- localTools: localTools.map(describeTool),
358
+ localTools: localTools.map((t) => ({ ...describeTool(t), httpToolEditable: editableHttpToolNames.has(t.name) })),
350
359
  agentAsTools: agentAsTools.map(describeTool),
351
360
  gatewayTools: gatewayTools.map(describeTool),
352
361
  permissions: {
@@ -562,6 +571,72 @@ async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, age
562
571
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ tool: describeTool(tool) }))
563
572
  }
564
573
 
574
+ // Backs the Tools tab's Edit button on a Local tool: GET populates the
575
+ // form from the spec createHttpTool/updateHttpTool saved alongside the
576
+ // generated file, PUT saves it back. 404s for any tool without a saved
577
+ // spec (hand-written, or created before this sidecar existed) — same
578
+ // signal describeAgent's own httpToolEditable flag uses to decide
579
+ // whether to show the button at all, so this only ever rejects a request
580
+ // the UI itself shouldn't have been able to send.
581
+ function handleHttpToolGet(res: ServerResponse, agentName: string, toolName: string): void {
582
+ if (!getEntry(agentName)) {
583
+ res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
584
+ return
585
+ }
586
+ const spec = readHttpToolSpec(agentName, toolName)
587
+ if (!spec) {
588
+ res.writeHead(404, { 'content-type': 'application/json' }).end(
589
+ JSON.stringify({ error: `No admin-created HTTP tool named '${toolName}' for '${agentName}' — hand-written tools aren't editable through this form.` }),
590
+ )
591
+ return
592
+ }
593
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(spec))
594
+ }
595
+
596
+ async function handleHttpToolPut(req: IncomingMessage, res: ServerResponse, agentName: string, toolName: string): Promise<void> {
597
+ const entry = getEntry(agentName)
598
+ if (!entry) {
599
+ res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
600
+ return
601
+ }
602
+ const body = await readJsonBody(req)
603
+ const parsed = parseHttpToolBody(body)
604
+ if (!parsed.ok) {
605
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsed.error }))
606
+ return
607
+ }
608
+
609
+ // Same ordering reasoning as handleHttpToolPost above: resolved before
610
+ // updateHttpTool touches disk.
611
+ const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config))
612
+
613
+ let tool: ToolDefinition
614
+ try {
615
+ const result = await updateHttpTool(agentName, toolName, parsed.value)
616
+ tool = result.tool
617
+ } catch (err) {
618
+ const status =
619
+ err instanceof HttpToolNotFoundError || err instanceof HttpToolNotEditableError
620
+ ? 409
621
+ : err instanceof HttpToolNameError || err instanceof HttpToolIndexShapeError
622
+ ? 400
623
+ : 500
624
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
625
+ return
626
+ }
627
+
628
+ // Replaces the edited tool in place (same array position, same
629
+ // reference identity for everything else) rather than the create
630
+ // path's append — an edit doesn't seed a new actauth rule either,
631
+ // unlike creation: the tool's permission is unchanged by this, still
632
+ // governed by whatever rule (if any) creation seeded or an operator
633
+ // added since, editable separately via the Actauth tab.
634
+ const nextTools = currentTools.map((t) => (t.name === toolName ? tool : t))
635
+ updateAgent(agentName, { config: { tools: nextTools } })
636
+
637
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ tool: describeTool(tool) }))
638
+ }
639
+
565
640
  // Backs GET /composio/connections and GET /composio/tools — not scoped
566
641
  // under /agents/:name/ like the routes above, deliberately: which apps
567
642
  // are connected and what they offer isn't a property of any one agent,
@@ -2016,6 +2091,18 @@ const server = createServer(async (req, res) => {
2016
2091
  return
2017
2092
  }
2018
2093
 
2094
+ // Backs the Tools tab's Edit button on a Local tool — GET to populate
2095
+ // the form, PUT to save (see handleHttpToolGet/Put above).
2096
+ const httpToolItemMatch = pathname.match(/^\/agents\/([^/]+)\/tools\/http\/([^/]+)$/)
2097
+ if (httpToolItemMatch && req.method === 'GET') {
2098
+ handleHttpToolGet(res, decodeURIComponent(httpToolItemMatch[1]), decodeURIComponent(httpToolItemMatch[2]))
2099
+ return
2100
+ }
2101
+ if (httpToolItemMatch && req.method === 'PUT') {
2102
+ await handleHttpToolPut(req, res, decodeURIComponent(httpToolItemMatch[1]), decodeURIComponent(httpToolItemMatch[2]))
2103
+ return
2104
+ }
2105
+
2019
2106
  // Backs the Skills tab's edit form — see skills-admin.ts for why
2020
2107
  // :skillId is restricted to a flat (non-nested) id.
2021
2108
  const skillMatch = pathname.match(/^\/agents\/([^/]+)\/skills\/([^/]+)$/)
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.7",
12
+ "loopengine": "^0.1.9",
13
13
  "actauth": "^0.0.12",
14
14
  "skillgarden": "^0.0.9"
15
15
  },