create-loopengine 0.1.73 → 0.1.75

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.73",
3
+ "version": "0.1.75",
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",
@@ -82,10 +82,16 @@ import {
82
82
  interviewTools,
83
83
  listInterviews,
84
84
  pickInterviewModel,
85
+ pickAgentModel,
85
86
  previewCandidate,
86
87
  startInterview,
87
88
  setInterviewProject,
88
89
  type InterviewAgentSource,
90
+ templatesPageHtml,
91
+ TemplateError,
92
+ createFromTemplate,
93
+ listTemplates,
94
+ previewTemplate,
89
95
  globalConfigPageHtml,
90
96
  addGatewayTool,
91
97
  agentDir,
@@ -958,6 +964,58 @@ async function handleInterviews(req: IncomingMessage, res: ServerResponse, pathn
958
964
  }
959
965
  }
960
966
 
967
+ // Templates (loopengine's web/templates-admin.ts): ready-made agents,
968
+ // created through the same scaffold and registration as Agents → + New.
969
+ // A template agent takes the model of an agent that already works here,
970
+ // as an interview's does; with none, it starts on the scaffold's default.
971
+ async function handleStarterTemplates(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
972
+ const json = (status: number, body: unknown): void => {
973
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
974
+ }
975
+ const [id, action] = pathname.split('/').slice(2).map(decodeURIComponent) // after /starter-templates
976
+ const dirOf = (name: string) => join(projectDir(), 'agents', name)
977
+ const exists = (name: string) => Boolean(getEntry(name)) || existsSync(dirOf(name))
978
+ const envValue = (name: string, variable: string) => createAgentEnv(name, dirOf(name)).get(variable)
979
+ try {
980
+ if (!id) {
981
+ if (req.method === 'GET') return json(200, { templates: listTemplates(), canInstallAbilities: secretsProtected })
982
+ return json(405, { error: 'method not allowed' })
983
+ }
984
+ if (action === 'preview' && req.method === 'GET') return json(200, previewTemplate(id, { agentExists: exists, envValue }))
985
+ if (action === 'create' && req.method === 'POST') {
986
+ const model = pickAgentModel(interviewAgents())
987
+ const created = await createFromTemplate(id, {
988
+ agentExists: exists,
989
+ scaffoldAgent: async (name, systemPrompt) => {
990
+ const parsed = parseAgentTemplateOptions({ systemPrompt, ...(model ? { model } : {}) })
991
+ if (!parsed.ok) throw new TemplateError(parsed.error)
992
+ await scaffoldAgent(projectDir(), name, parsed.value)
993
+ },
994
+ registerAgent: async (name) => {
995
+ const indexPath = join(dirOf(name), 'index.ts')
996
+ registerAgent(await loadAgentModule(indexPath, indexPath))
997
+ },
998
+ // Same rule as the Abilities tab: installing runs third-party code,
999
+ // so only behind the admin login. The agent isn't loaded yet, so
1000
+ // nothing needs a restart.
1001
+ ...(secretsProtected ? { installAbility: async (name: string, spec: string) => void (await installAbility(name, spec)) } : {}),
1002
+ envValue,
1003
+ messagingConfigured: (name) => {
1004
+ const entry = getEntry(name)
1005
+ return entry ? describeAgentNotifier(entry.config, defaultWebhook).source !== 'none' : false
1006
+ },
1007
+ })
1008
+ return json(200, created)
1009
+ }
1010
+ return json(404, { error: 'not found' })
1011
+ } catch (err) {
1012
+ const message = err instanceof Error ? err.message : String(err)
1013
+ if (err instanceof TemplateError) return json(message === 'No such template' ? 404 : 400, { error: message })
1014
+ console.error('[loopengine] template failed:', err)
1015
+ return json(500, { error: `Couldn't create it: ${message}` })
1016
+ }
1017
+ }
1018
+
961
1019
  async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
962
1020
  const body = await readJsonBody(req)
963
1021
  if (typeof body.name !== 'string' || !body.name) {
@@ -1246,7 +1304,7 @@ async function handleSchedulesPost(req: IncomingMessage, res: ServerResponse, ag
1246
1304
  const task = await createScheduledTask({
1247
1305
  agent: agentName,
1248
1306
  message: String(body.message ?? ''),
1249
- schedule: (body.schedule ?? {}) as { run_at?: unknown; every?: unknown },
1307
+ schedule: (body.schedule ?? {}) as { run_at?: unknown; every?: unknown; start_at?: unknown },
1250
1308
  task_name: typeof body.task_name === 'string' ? body.task_name : undefined,
1251
1309
  notify: body.notify === true,
1252
1310
  })
@@ -3272,6 +3330,15 @@ const server = createServer(async (req, res) => {
3272
3330
  await handleInterviews(req, res, pathname)
3273
3331
  return
3274
3332
  }
3333
+ // Templates: the page, and its routes.
3334
+ if (req.method === 'GET' && pathname === '/templates') {
3335
+ res.writeHead(200, { 'content-type': 'text/html' }).end(templatesPageHtml)
3336
+ return
3337
+ }
3338
+ if (pathname === '/starter-templates' || pathname.startsWith('/starter-templates/')) {
3339
+ await handleStarterTemplates(req, res, pathname)
3340
+ return
3341
+ }
3275
3342
  const editAgentMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)$/)
3276
3343
  if (editAgentMatch) {
3277
3344
  await handleEditAgent(req, res, decodeURIComponent(editAgentMatch[1]))
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.91",
12
+ "loopengine": "^0.1.93",
13
13
  "actauth": "^0.0.12",
14
14
  "tsx": "^4.19.0",
15
15
  "typescript": "^5.7.0"