create-loopengine 0.1.11 → 0.1.13
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/cli.js +5 -1
- package/package.json +1 -1
- package/template/_gitignore +6 -0
- package/template/adapters/http.ts +251 -10
- package/template/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
// Template ships in `template/` next to `dist/`, not inside it — it's a
|
|
@@ -15,6 +15,10 @@ export function scaffold(options) {
|
|
|
15
15
|
}
|
|
16
16
|
mkdirSync(destination, { recursive: true });
|
|
17
17
|
cpSync(templateDir, destination, { recursive: true });
|
|
18
|
+
// npm strips any .gitignore (even nested ones) when publishing a package,
|
|
19
|
+
// so the template ships it as `_gitignore` and this renames it back —
|
|
20
|
+
// same workaround create-vite/create-react-app use for the same reason.
|
|
21
|
+
renameSync(join(destination, '_gitignore'), join(destination, '.gitignore'));
|
|
18
22
|
const pkgPath = join(destination, 'package.json');
|
|
19
23
|
const pkg = readFileSync(pkgPath, 'utf8').replace('__PROJECT_NAME__', name);
|
|
20
24
|
writeFileSync(pkgPath, pkg);
|
package/package.json
CHANGED
|
@@ -29,14 +29,21 @@
|
|
|
29
29
|
// file's call — see AgentConfig.sessionIdFor and defaultSessionIdFor below.
|
|
30
30
|
import { randomUUID, timingSafeEqual } from 'node:crypto'
|
|
31
31
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
32
|
+
import { pathToFileURL } from 'node:url'
|
|
32
33
|
import { getEntry, listAgents, projectDir, registerAgent, updateAgent, type RegistryEntry } from '../agent-registry.js'
|
|
33
34
|
import {
|
|
34
35
|
loadAgentModule,
|
|
35
36
|
synthesizeCreateModelCall,
|
|
36
37
|
editAgentFile,
|
|
38
|
+
createHttpTool,
|
|
39
|
+
HttpToolNameError,
|
|
40
|
+
HttpToolExistsError,
|
|
41
|
+
HttpToolIndexShapeError,
|
|
42
|
+
type HttpToolSpec,
|
|
37
43
|
AgentEditNotSupportedError,
|
|
38
44
|
AgentFileNotFoundError,
|
|
39
45
|
type AgentEditResult,
|
|
46
|
+
type AgentEditableFields,
|
|
40
47
|
createSessionStore,
|
|
41
48
|
createCheckpointStore,
|
|
42
49
|
type TurnCheckpoint,
|
|
@@ -73,6 +80,10 @@ import {
|
|
|
73
80
|
deleteSkill,
|
|
74
81
|
SkillInvalidIdError,
|
|
75
82
|
SkillNotFoundError,
|
|
83
|
+
listSkillgardenCatalog,
|
|
84
|
+
readSkillgardenCatalogEntry,
|
|
85
|
+
addSkillgardenSkillToAgent,
|
|
86
|
+
SkillgardenUnavailableError,
|
|
76
87
|
readActauthConfig,
|
|
77
88
|
addActauthRule,
|
|
78
89
|
updateActauthRule,
|
|
@@ -308,9 +319,14 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
|
|
|
308
319
|
// configured for *this* agent, so the config page's own Skills tab
|
|
309
320
|
// deliberately doesn't mix it into skills/skillsDirs below.
|
|
310
321
|
const skillsDirs = config.skillsDirs ?? [`agents/${config.name}/skills`]
|
|
311
|
-
|
|
322
|
+
// Same defaults, same reasoning, as loopengine's own run-agent.ts
|
|
323
|
+
// SkillGarden/BudgetTracker construction — kept in sync there and here
|
|
324
|
+
// since this is what a "Skills"/"Limits & budgets" tab shows an
|
|
325
|
+
// operator, and it has to describe what runAgent() would actually
|
|
326
|
+
// resolve, not a second guess (see this function's own header comment).
|
|
327
|
+
const skillGarden = new SkillGarden({ dirs: skillsDirs, indexBudgetTokens: config.skillIndexBudgetTokens ?? 2000 })
|
|
312
328
|
const skillIndex = skillGarden.buildIndex().included
|
|
313
|
-
const systemSkillGarden = new SkillGarden({ dirs: [systemSkillsDir], indexBudgetTokens: config.skillIndexBudgetTokens ??
|
|
329
|
+
const systemSkillGarden = new SkillGarden({ dirs: [systemSkillsDir], indexBudgetTokens: config.skillIndexBudgetTokens ?? 2000 })
|
|
314
330
|
const systemSkillIndex = systemSkillGarden.buildIndex().included
|
|
315
331
|
|
|
316
332
|
return {
|
|
@@ -320,8 +336,8 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
|
|
|
320
336
|
? { provider: config.model.provider, model: config.model.model, maxTokens: config.model.maxTokens }
|
|
321
337
|
: 'custom (module exports its own createModelCall)',
|
|
322
338
|
maxTurns: config.maxTurns ?? 25,
|
|
323
|
-
contextBudgetTokens: config.contextBudgetTokens ??
|
|
324
|
-
skillIndexBudgetTokens: config.skillIndexBudgetTokens ??
|
|
339
|
+
contextBudgetTokens: config.contextBudgetTokens ?? 100000,
|
|
340
|
+
skillIndexBudgetTokens: config.skillIndexBudgetTokens ?? 2000,
|
|
325
341
|
skillsDirs: skillsDirs,
|
|
326
342
|
skills: skillIndex.map((s) => ({ name: s.name, description: s.description })),
|
|
327
343
|
systemSkills: systemSkillIndex.map((s) => ({ name: s.name, description: s.description })),
|
|
@@ -441,6 +457,105 @@ async function handleGatewayToolSlugDelete(res: ServerResponse, agentName: strin
|
|
|
441
457
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ sources }))
|
|
442
458
|
}
|
|
443
459
|
|
|
460
|
+
function parseHttpToolBody(body: Record<string, unknown>): { ok: true; value: HttpToolSpec; decision?: Decision } | { ok: false; error: string } {
|
|
461
|
+
if (typeof body.name !== 'string' || !body.name) return { ok: false, error: 'name is required' }
|
|
462
|
+
if (typeof body.description !== 'string' || !body.description) return { ok: false, error: 'description is required' }
|
|
463
|
+
if (!Array.isArray(body.fields) || !body.fields.every((f) => f && typeof f === 'object')) {
|
|
464
|
+
return { ok: false, error: 'fields must be an array' }
|
|
465
|
+
}
|
|
466
|
+
const fields = body.fields as Record<string, unknown>[]
|
|
467
|
+
for (const f of fields) {
|
|
468
|
+
if (typeof f.name !== 'string' || !f.name) return { ok: false, error: 'every field needs a name' }
|
|
469
|
+
if (f.type !== 'string' && f.type !== 'number' && f.type !== 'boolean') return { ok: false, error: `field "${f.name}" has an invalid type` }
|
|
470
|
+
}
|
|
471
|
+
const method = body.method
|
|
472
|
+
if (method !== 'GET' && method !== 'POST' && method !== 'PUT' && method !== 'PATCH' && method !== 'DELETE') {
|
|
473
|
+
return { ok: false, error: 'method must be one of GET, POST, PUT, PATCH, DELETE' }
|
|
474
|
+
}
|
|
475
|
+
if (typeof body.url !== 'string' || !body.url) return { ok: false, error: 'url is required' }
|
|
476
|
+
if (!Array.isArray(body.headers) || !body.headers.every((h) => h && typeof h === 'object' && typeof (h as Record<string, unknown>).key === 'string' && typeof (h as Record<string, unknown>).value === 'string')) {
|
|
477
|
+
return { ok: false, error: 'headers must be an array of {key, value} strings' }
|
|
478
|
+
}
|
|
479
|
+
if (body.responseJsonPath !== undefined && typeof body.responseJsonPath !== 'string') {
|
|
480
|
+
return { ok: false, error: 'responseJsonPath must be a string' }
|
|
481
|
+
}
|
|
482
|
+
if (body.decision !== undefined && !isDecision(body.decision)) {
|
|
483
|
+
return { ok: false, error: 'decision must be allow, ask, or deny' }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
value: {
|
|
489
|
+
name: body.name,
|
|
490
|
+
description: body.description,
|
|
491
|
+
fields: fields as unknown as HttpToolSpec['fields'],
|
|
492
|
+
method,
|
|
493
|
+
url: body.url,
|
|
494
|
+
headers: body.headers as HttpToolSpec['headers'],
|
|
495
|
+
sendFieldsAsJsonBody: body.sendFieldsAsJsonBody === true,
|
|
496
|
+
responseJsonPath: body.responseJsonPath as string | undefined,
|
|
497
|
+
},
|
|
498
|
+
decision: body.decision as Decision | undefined,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Backs the Tools tab's "Create an HTTP tool" form — generates a real
|
|
503
|
+
// agents/:name/tools/<tool>.ts file (loopengine's own createHttpTool),
|
|
504
|
+
// then splices the freshly-imported ToolDefinition straight into this
|
|
505
|
+
// process's own live registry via updateAgent (../agent-registry.js) —
|
|
506
|
+
// the same "no restart needed" mechanism a newly-created agent already
|
|
507
|
+
// gets (see registerAgent's own doc comment), and deliberately not a
|
|
508
|
+
// reliance on tsx watch picking up the new file: this way the tool is
|
|
509
|
+
// callable immediately under `serve` (no watch) too, not just `dev`. An
|
|
510
|
+
// optional `decision` seeds one actauth.yml rule via the already-existing
|
|
511
|
+
// addActauthRule, mirroring gateway-tools.ts's own "a rule is opt-in,
|
|
512
|
+
// omitting it falls through to default_decision" precedent — never a
|
|
513
|
+
// hardcoded default here.
|
|
514
|
+
async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
515
|
+
const entry = getEntry(agentName)
|
|
516
|
+
if (!entry) {
|
|
517
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
const body = await readJsonBody(req)
|
|
521
|
+
const parsed = parseHttpToolBody(body)
|
|
522
|
+
if (!parsed.ok) {
|
|
523
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsed.error }))
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let tool: ToolDefinition
|
|
528
|
+
try {
|
|
529
|
+
const result = await createHttpTool(agentName, parsed.value)
|
|
530
|
+
tool = result.tool
|
|
531
|
+
} catch (err) {
|
|
532
|
+
const status = err instanceof HttpToolExistsError ? 409 : err instanceof HttpToolNameError || err instanceof HttpToolIndexShapeError ? 400 : 500
|
|
533
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config))
|
|
538
|
+
updateAgent(agentName, { config: { tools: [...currentTools, tool] } })
|
|
539
|
+
|
|
540
|
+
if (parsed.decision) {
|
|
541
|
+
try {
|
|
542
|
+
addActauthRule(agentName, { name: `${parsed.value.name}-admin-created`, scope: '*/*', tool: parsed.value.name, decision: parsed.decision })
|
|
543
|
+
} catch (err) {
|
|
544
|
+
// The tool file and live registration above already succeeded —
|
|
545
|
+
// a rule-seeding failure (near-impossible: only real cause is a
|
|
546
|
+
// same-named rule already existing) shouldn't roll either back,
|
|
547
|
+
// just surface as a warning-shaped success so the operator can
|
|
548
|
+
// add the rule by hand via the Actauth tab instead.
|
|
549
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
550
|
+
JSON.stringify({ tool: describeTool(tool), ruleWarning: err instanceof Error ? err.message : String(err) }),
|
|
551
|
+
)
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ tool: describeTool(tool) }))
|
|
557
|
+
}
|
|
558
|
+
|
|
444
559
|
// Backs GET /composio/connections and GET /composio/tools — not scoped
|
|
445
560
|
// under /agents/:name/ like the routes above, deliberately: which apps
|
|
446
561
|
// are connected and what they offer isn't a property of any one agent,
|
|
@@ -539,6 +654,33 @@ function parseAgentTemplateOptions(body: Record<string, unknown>): { ok: true; v
|
|
|
539
654
|
return { ok: true, value: options }
|
|
540
655
|
}
|
|
541
656
|
|
|
657
|
+
// The Overview tab's "Limits & budgets" edit form — kept separate from
|
|
658
|
+
// parseAgentTemplateOptions since that one's shape (AgentTemplateOptions)
|
|
659
|
+
// is shared with handleCreateAgent/scaffoldAgent, which has no notion of
|
|
660
|
+
// these three at all (a freshly scaffolded agent always starts on
|
|
661
|
+
// loopengine's own defaults — see AgentConfig's own doc comments). Each
|
|
662
|
+
// of the three is optional and independent, same as systemPrompt/model
|
|
663
|
+
// above.
|
|
664
|
+
function parsePositiveInt(value: unknown, field: string): { ok: true; value: number | undefined } | { ok: false; error: string } {
|
|
665
|
+
if (value === undefined || value === null) return { ok: true, value: undefined }
|
|
666
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
667
|
+
return { ok: false, error: `${field} must be a positive integer` }
|
|
668
|
+
}
|
|
669
|
+
return { ok: true, value }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function parseAgentLimitsFields(
|
|
673
|
+
body: Record<string, unknown>,
|
|
674
|
+
): { ok: true; value: Pick<AgentEditableFields, 'maxTurns' | 'contextBudgetTokens' | 'skillIndexBudgetTokens'> } | { ok: false; error: string } {
|
|
675
|
+
const value: Pick<AgentEditableFields, 'maxTurns' | 'contextBudgetTokens' | 'skillIndexBudgetTokens'> = {}
|
|
676
|
+
for (const field of ['maxTurns', 'contextBudgetTokens', 'skillIndexBudgetTokens'] as const) {
|
|
677
|
+
const parsed = parsePositiveInt(body[field], field)
|
|
678
|
+
if (!parsed.ok) return parsed
|
|
679
|
+
if (parsed.value !== undefined) value[field] = parsed.value
|
|
680
|
+
}
|
|
681
|
+
return { ok: true, value }
|
|
682
|
+
}
|
|
683
|
+
|
|
542
684
|
async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
543
685
|
const body = await readJsonBody(req)
|
|
544
686
|
if (typeof body.name !== 'string' || !body.name) {
|
|
@@ -572,9 +714,11 @@ async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
572
714
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ path: indexPath, registered: true }))
|
|
573
715
|
}
|
|
574
716
|
|
|
575
|
-
// Backs the Overview tab's System prompt / Model
|
|
576
|
-
//
|
|
577
|
-
//
|
|
717
|
+
// Backs the Overview tab's System prompt / Model / Limits & budgets edit
|
|
718
|
+
// forms — any subset of systemPrompt, model, maxTurns, contextBudgetTokens,
|
|
719
|
+
// skillIndexBudgetTokens can be sent independently (see
|
|
720
|
+
// parseAgentTemplateOptions/parseAgentLimitsFields), every other field
|
|
721
|
+
// stays untouched both on disk and live. Persists via
|
|
578
722
|
// agent-file-admin.ts's editAgentFile (see its own doc comment for why
|
|
579
723
|
// this is a surgical AST edit, not a full file regeneration), then
|
|
580
724
|
// applies the exact same resolved values to *this* running process via
|
|
@@ -594,14 +738,22 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
|
|
|
594
738
|
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedOptions.error }))
|
|
595
739
|
return
|
|
596
740
|
}
|
|
597
|
-
|
|
598
|
-
|
|
741
|
+
const parsedLimits = parseAgentLimitsFields(body)
|
|
742
|
+
if (!parsedLimits.ok) {
|
|
743
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedLimits.error }))
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
const editFields: AgentEditableFields = { ...parsedOptions.value, ...parsedLimits.value }
|
|
747
|
+
if (Object.keys(editFields).length === 0) {
|
|
748
|
+
res
|
|
749
|
+
.writeHead(400, { 'content-type': 'application/json' })
|
|
750
|
+
.end(JSON.stringify({ error: 'systemPrompt, model, maxTurns, contextBudgetTokens, or skillIndexBudgetTokens is required' }))
|
|
599
751
|
return
|
|
600
752
|
}
|
|
601
753
|
|
|
602
754
|
let result: AgentEditResult
|
|
603
755
|
try {
|
|
604
|
-
result = editAgentFile(agentName,
|
|
756
|
+
result = editAgentFile(agentName, editFields)
|
|
605
757
|
} catch (err) {
|
|
606
758
|
const status =
|
|
607
759
|
err instanceof AgentEditNotSupportedError || err instanceof AgentModelError ? 400 : err instanceof AgentFileNotFoundError ? 404 : 500
|
|
@@ -611,6 +763,9 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
|
|
|
611
763
|
|
|
612
764
|
const configPatch: Partial<AgentConfig> = {}
|
|
613
765
|
if (result.systemPrompt !== undefined) configPatch.systemPrompt = result.systemPrompt
|
|
766
|
+
if (result.maxTurns !== undefined) configPatch.maxTurns = result.maxTurns
|
|
767
|
+
if (result.contextBudgetTokens !== undefined) configPatch.contextBudgetTokens = result.contextBudgetTokens
|
|
768
|
+
if (result.skillIndexBudgetTokens !== undefined) configPatch.skillIndexBudgetTokens = result.skillIndexBudgetTokens
|
|
614
769
|
let createModelCall: RegistryEntry['createModelCall'] | undefined
|
|
615
770
|
if (result.model) {
|
|
616
771
|
configPatch.model = result.model
|
|
@@ -677,6 +832,69 @@ function handleSkillDelete(res: ServerResponse, agentName: string, skillId: stri
|
|
|
677
832
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
|
|
678
833
|
}
|
|
679
834
|
|
|
835
|
+
// Backs the Skills tab's "Browse skillgarden catalog" section — a
|
|
836
|
+
// read-only, agent-independent list of everything skillgarden ships in
|
|
837
|
+
// its own bundled registry (see loopengine's own web/skillgarden-admin.ts).
|
|
838
|
+
// Not wrapped in a per-agent :name segment since the catalog itself
|
|
839
|
+
// isn't scoped to any one agent; only the POST below (actually
|
|
840
|
+
// installing an entry) is.
|
|
841
|
+
function handleSkillgardenCatalogGet(res: ServerResponse): void {
|
|
842
|
+
try {
|
|
843
|
+
// Computed before writeHead, not inlined into .end()'s own argument —
|
|
844
|
+
// res.writeHead(200, ...) commits the status line the instant it's
|
|
845
|
+
// called, so a throw evaluated *after* it (as it would be if it were
|
|
846
|
+
// inlined as .end()'s argument) leaves headers already sent as 200
|
|
847
|
+
// with no body, and the catch block's own writeHead(status, ...)
|
|
848
|
+
// then fails outright (ERR_HTTP_HEADERS_SENT) instead of ever
|
|
849
|
+
// reaching the client.
|
|
850
|
+
const catalog = listSkillgardenCatalog()
|
|
851
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(catalog))
|
|
852
|
+
} catch (err) {
|
|
853
|
+
const status = err instanceof SkillgardenUnavailableError ? 503 : 500
|
|
854
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Full SKILL.md body for the catalog's own "View" toggle — kept as a
|
|
859
|
+
// separate route from the list above (rather than inlining every body
|
|
860
|
+
// into it) for the same reason the Skills tab's own edit form fetches a
|
|
861
|
+
// skill's body lazily instead of shipping it in the initial list.
|
|
862
|
+
function handleSkillgardenCatalogEntryGet(res: ServerResponse, category: string, id: string): void {
|
|
863
|
+
try {
|
|
864
|
+
// See handleSkillgardenCatalogGet's own comment for why this is
|
|
865
|
+
// computed before writeHead rather than inlined into .end()'s
|
|
866
|
+
// argument.
|
|
867
|
+
const entry = readSkillgardenCatalogEntry(category, id)
|
|
868
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(entry))
|
|
869
|
+
} catch (err) {
|
|
870
|
+
const status = err instanceof SkillgardenUnavailableError ? 503 : 404
|
|
871
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Installs a catalog entry into agents/:name/skills/ — delegates to
|
|
876
|
+
// skillgarden's own addSkill (see skillgarden-admin.ts), which throws
|
|
877
|
+
// plain Errors (no typed classes) for "unknown skill"/"already exists",
|
|
878
|
+
// so those two are told apart by message shape rather than instanceof,
|
|
879
|
+
// same as any other dependency that doesn't export typed errors for its
|
|
880
|
+
// own failure modes.
|
|
881
|
+
async function handleSkillgardenCatalogAdd(req: IncomingMessage, res: ServerResponse, agentName: string, category: string, id: string): Promise<void> {
|
|
882
|
+
if (!getEntry(agentName)) {
|
|
883
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
const body = await readJsonBody(req)
|
|
887
|
+
const force = body.force === true
|
|
888
|
+
try {
|
|
889
|
+
const result = addSkillgardenSkillToAgent(agentName, category, id, force)
|
|
890
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, id: result.id }))
|
|
891
|
+
} catch (err) {
|
|
892
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
893
|
+
const status = err instanceof SkillgardenUnavailableError ? 503 : /already exists/.test(message) ? 409 : /^Unknown skill/.test(message) ? 404 : 500
|
|
894
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: message }))
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
680
898
|
// Backs the Actauth tab's rule editor — parses a rule body shared by both
|
|
681
899
|
// the add (POST) and update (PUT, minus `name`) routes below.
|
|
682
900
|
function parseActauthRuleBody(body: Record<string, unknown>, requireName: boolean): { ok: true; value: { name: string; scope: string; tool: string; decision: Decision } } | { ok: false; error: string } {
|
|
@@ -1779,6 +1997,12 @@ const server = createServer(async (req, res) => {
|
|
|
1779
1997
|
return
|
|
1780
1998
|
}
|
|
1781
1999
|
|
|
2000
|
+
const httpToolMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/tools\/http$/)
|
|
2001
|
+
if (httpToolMatch) {
|
|
2002
|
+
await handleHttpToolPost(req, res, decodeURIComponent(httpToolMatch[1]))
|
|
2003
|
+
return
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1782
2006
|
// Backs the Skills tab's edit form — see skills-admin.ts for why
|
|
1783
2007
|
// :skillId is restricted to a flat (non-nested) id.
|
|
1784
2008
|
const skillMatch = pathname.match(/^\/agents\/([^/]+)\/skills\/([^/]+)$/)
|
|
@@ -1795,6 +2019,23 @@ const server = createServer(async (req, res) => {
|
|
|
1795
2019
|
return
|
|
1796
2020
|
}
|
|
1797
2021
|
|
|
2022
|
+
// Backs the Skills tab's "Browse skillgarden catalog" section — see
|
|
2023
|
+
// the three handlers above it.
|
|
2024
|
+
if (req.method === 'GET' && pathname === '/skillgarden-catalog') {
|
|
2025
|
+
handleSkillgardenCatalogGet(res)
|
|
2026
|
+
return
|
|
2027
|
+
}
|
|
2028
|
+
const skillgardenEntryMatch = req.method === 'GET' && pathname.match(/^\/skillgarden-catalog\/([^/]+)\/([^/]+)$/)
|
|
2029
|
+
if (skillgardenEntryMatch) {
|
|
2030
|
+
handleSkillgardenCatalogEntryGet(res, decodeURIComponent(skillgardenEntryMatch[1]), decodeURIComponent(skillgardenEntryMatch[2]))
|
|
2031
|
+
return
|
|
2032
|
+
}
|
|
2033
|
+
const skillgardenAddMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/skillgarden-catalog\/([^/]+)\/([^/]+)$/)
|
|
2034
|
+
if (skillgardenAddMatch) {
|
|
2035
|
+
await handleSkillgardenCatalogAdd(req, res, decodeURIComponent(skillgardenAddMatch[1]), decodeURIComponent(skillgardenAddMatch[2]), decodeURIComponent(skillgardenAddMatch[3]))
|
|
2036
|
+
return
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1798
2039
|
// Backs the Actauth tab's rule editor and default_decision control.
|
|
1799
2040
|
// The default-decision route is checked before the :ruleName routes
|
|
1800
2041
|
// right below since 'default-decision' would otherwise itself match
|
package/template/package.json
CHANGED