create-loopengine 0.1.74 → 0.1.76

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.74",
3
+ "version": "0.1.76",
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",
@@ -197,6 +197,10 @@ import {
197
197
  setModelProviderKey,
198
198
  clearModelProviderKey,
199
199
  ModelKeyError,
200
+ describeTimeZone,
201
+ setWorkspaceTimeZone,
202
+ TimeZoneError,
203
+ workspaceTimeZone,
200
204
  describeGateways,
201
205
  scaffoldAgent,
202
206
  AgentNameError,
@@ -973,23 +977,37 @@ async function handleStarterTemplates(req: IncomingMessage, res: ServerResponse,
973
977
  res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
974
978
  }
975
979
  const [id, action] = pathname.split('/').slice(2).map(decodeURIComponent) // after /starter-templates
976
- const exists = (name: string) => Boolean(getEntry(name)) || existsSync(join(projectDir(), 'agents', name))
980
+ const dirOf = (name: string) => join(projectDir(), 'agents', name)
981
+ const exists = (name: string) => Boolean(getEntry(name)) || existsSync(dirOf(name))
982
+ const envValue = (name: string, variable: string) => createAgentEnv(name, dirOf(name)).get(variable)
977
983
  try {
978
984
  if (!id) {
979
- if (req.method === 'GET') return json(200, { templates: listTemplates() })
985
+ if (req.method === 'GET') return json(200, { templates: listTemplates(), canInstallAbilities: secretsProtected })
980
986
  return json(405, { error: 'method not allowed' })
981
987
  }
982
- if (action === 'preview' && req.method === 'GET') return json(200, previewTemplate(id, { agentExists: exists }))
988
+ if (action === 'preview' && req.method === 'GET') return json(200, previewTemplate(id, { agentExists: exists, envValue }))
983
989
  if (action === 'create' && req.method === 'POST') {
984
990
  const model = pickAgentModel(interviewAgents())
985
991
  const created = await createFromTemplate(id, {
986
992
  agentExists: exists,
987
- createAgent: async (name, systemPrompt) => {
993
+ scaffoldAgent: async (name, systemPrompt) => {
988
994
  const parsed = parseAgentTemplateOptions({ systemPrompt, ...(model ? { model } : {}) })
989
995
  if (!parsed.ok) throw new TemplateError(parsed.error)
990
- const indexPath = await scaffoldAgent(projectDir(), name, parsed.value)
996
+ await scaffoldAgent(projectDir(), name, parsed.value)
997
+ },
998
+ registerAgent: async (name) => {
999
+ const indexPath = join(dirOf(name), 'index.ts')
991
1000
  registerAgent(await loadAgentModule(indexPath, indexPath))
992
1001
  },
1002
+ // Same rule as the Abilities tab: installing runs third-party code,
1003
+ // so only behind the admin login. The agent isn't loaded yet, so
1004
+ // nothing needs a restart.
1005
+ ...(secretsProtected ? { installAbility: async (name: string, spec: string) => void (await installAbility(name, spec)) } : {}),
1006
+ envValue,
1007
+ messagingConfigured: (name) => {
1008
+ const entry = getEntry(name)
1009
+ return entry ? describeAgentNotifier(entry.config, defaultWebhook).source !== 'none' : false
1010
+ },
993
1011
  })
994
1012
  return json(200, created)
995
1013
  }
@@ -1280,7 +1298,7 @@ function scheduleJson(res: ServerResponse, status: number, body: unknown): void
1280
1298
 
1281
1299
  async function handleSchedulesGet(res: ServerResponse, agentName: string): Promise<void> {
1282
1300
  if (!getEntry(agentName)) return scheduleJson(res, 404, { error: `unknown agent '${agentName}'` })
1283
- scheduleJson(res, 200, { tasks: await listScheduledTasks({ agent: agentName }) })
1301
+ scheduleJson(res, 200, { tasks: await listScheduledTasks({ agent: agentName }), time_zone: workspaceTimeZone() })
1284
1302
  }
1285
1303
 
1286
1304
  async function handleSchedulesPost(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
@@ -1290,7 +1308,7 @@ async function handleSchedulesPost(req: IncomingMessage, res: ServerResponse, ag
1290
1308
  const task = await createScheduledTask({
1291
1309
  agent: agentName,
1292
1310
  message: String(body.message ?? ''),
1293
- schedule: (body.schedule ?? {}) as { run_at?: unknown; every?: unknown },
1311
+ schedule: (body.schedule ?? {}) as { run_at?: unknown; every?: unknown; start_at?: unknown; at?: unknown; weekday?: unknown },
1294
1312
  task_name: typeof body.task_name === 'string' ? body.task_name : undefined,
1295
1313
  notify: body.notify === true,
1296
1314
  })
@@ -3360,6 +3378,7 @@ const server = createServer(async (req, res) => {
3360
3378
  environment: process.env.LOOPENGINE_ENV ?? 'production',
3361
3379
  publicUrl: process.env.LOOPENGINE_PUBLIC_URL ?? null,
3362
3380
  scheduler: process.env.LOOPENGINE_SCHEDULER === 'off' ? 'off' : 'on',
3381
+ timeZone: workspaceTimeZone(),
3363
3382
  }),
3364
3383
  )
3365
3384
  return
@@ -3419,6 +3438,29 @@ const server = createServer(async (req, res) => {
3419
3438
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
3420
3439
  return
3421
3440
  }
3441
+ // Settings → Time zone (web/global-config.ts): what "9:00" means in
3442
+ // schedules. Saved like a model key, so refused without auth too.
3443
+ if (req.method === 'GET' && pathname === '/config/time-zone') {
3444
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify({ ...describeTimeZone(), canSave: secretsProtected }))
3445
+ return
3446
+ }
3447
+ if (req.method === 'PUT' && pathname === '/config/time-zone') {
3448
+ if (!secretsProtected) {
3449
+ res.writeHead(403, { 'content-type': 'application/json' }).end(
3450
+ JSON.stringify({ error: 'Refusing to change settings without LOOPENGINE_ADMIN_AUTH configured: set it first, or put LOOPENGINE_TIME_ZONE in .env.' }),
3451
+ )
3452
+ return
3453
+ }
3454
+ try {
3455
+ const body = await readJsonBody(req)
3456
+ const outcome = await setWorkspaceTimeZone(body.timeZone, { moveSchedules: body.moveSchedules === true })
3457
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, ...outcome }))
3458
+ } catch (err) {
3459
+ const status = err instanceof TimeZoneError ? 400 : 500
3460
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
3461
+ }
3462
+ return
3463
+ }
3422
3464
  if (req.method === 'GET' && pathname === '/config/gateways') {
3423
3465
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await describeGateways()))
3424
3466
  return
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.92",
12
+ "loopengine": "^0.1.94",
13
13
  "actauth": "^0.0.12",
14
14
  "tsx": "^4.19.0",
15
15
  "typescript": "^5.7.0"