create-loopengine 0.1.16 → 0.1.18

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.16",
3
+ "version": "0.1.18",
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: {
@@ -524,6 +533,13 @@ async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, age
524
533
  return
525
534
  }
526
535
 
536
+ // Resolved *before* createHttpTool writes anything to disk. Reading it
537
+ // after would risk a cold import of the freshly-written tools/index.ts
538
+ // (folder-form agents don't cache config.tools) — which already contains
539
+ // the new tool by then, so the ...currentTools, tool below would append
540
+ // it a second time.
541
+ const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config))
542
+
527
543
  let tool: ToolDefinition
528
544
  try {
529
545
  const result = await createHttpTool(agentName, parsed.value)
@@ -534,7 +550,6 @@ async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, age
534
550
  return
535
551
  }
536
552
 
537
- const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config))
538
553
  updateAgent(agentName, { config: { tools: [...currentTools, tool] } })
539
554
 
540
555
  if (parsed.decision) {
@@ -556,6 +571,72 @@ async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, age
556
571
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ tool: describeTool(tool) }))
557
572
  }
558
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
+
559
640
  // Backs GET /composio/connections and GET /composio/tools — not scoped
560
641
  // under /agents/:name/ like the routes above, deliberately: which apps
561
642
  // are connected and what they offer isn't a property of any one agent,
@@ -2010,6 +2091,18 @@ const server = createServer(async (req, res) => {
2010
2091
  return
2011
2092
  }
2012
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
+
2013
2106
  // Backs the Skills tab's edit form — see skills-admin.ts for why
2014
2107
  // :skillId is restricted to a flat (non-nested) id.
2015
2108
  const skillMatch = pathname.match(/^\/agents\/([^/]+)\/skills\/([^/]+)$/)