create-loopengine 0.1.59 → 0.1.61

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/README.md CHANGED
@@ -16,7 +16,7 @@ clone of loopengine's own source repo.
16
16
  npx create-loopengine@latest my-agents
17
17
  cd my-agents
18
18
  npm install
19
- cp .env.example .env # fill in ANTHROPIC_API_KEY
19
+ cp .env.example .env # fill in OPENAI_API_KEY (or set it later on the Models page)
20
20
  npm run dev # HTTP server on :8787
21
21
  ```
22
22
 
package/dist/cli.d.ts CHANGED
@@ -6,7 +6,10 @@ export interface ScaffoldOptions {
6
6
  templateDir?: string;
7
7
  }
8
8
  export declare function scaffold(options: ScaffoldOptions): string;
9
- export type UpgradeFileStatus = 'updated' | 'unchanged' | 'conflict' | 'skipped';
9
+ /** `kept`: the project edited a file the template also changed, and
10
+ * `keepEdited` was set, so it was left exactly as it was (no merge, no
11
+ * conflict markers). */
12
+ export type UpgradeFileStatus = 'updated' | 'unchanged' | 'conflict' | 'skipped' | 'kept';
10
13
  export interface UpgradeFileResult {
11
14
  file: string;
12
15
  status: UpgradeFileStatus;
@@ -57,6 +60,11 @@ export interface UpgradeOptions {
57
60
  /** Runs `npm install` in projectDir — defaults to a real one. Test-only
58
61
  * seam to avoid actually installing anything during tests. */
59
62
  runNpmInstall?: (projectDir: string) => void;
63
+ /** Leave a file the project edited untouched (status `kept`) instead of
64
+ * three-way merging it, so nothing ever gets conflict markers. For
65
+ * unattended upgrades (the console's Upgrade button): a half-merged
66
+ * adapters/http.ts would stop the server from starting at all. */
67
+ keepEdited?: boolean;
60
68
  }
61
69
  export declare function upgradeProject(options?: UpgradeOptions): UpgradeResult;
62
70
  export declare function main(argv?: string[]): void;
package/dist/cli.js CHANGED
@@ -116,7 +116,7 @@ function threeWayMerge(minePath, basePath, theirsPath) {
116
116
  }
117
117
  return { merged: readFileSync(minePath, 'utf8'), conflicted };
118
118
  }
119
- function upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir) {
119
+ function upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir, keepEdited = false) {
120
120
  const projectFilePath = join(projectDir, candidate.projectPath);
121
121
  const baseFilePath = join(baseTemplateDir, candidate.templatePath);
122
122
  const theirsFilePath = join(currentTemplateDir, candidate.templatePath);
@@ -132,6 +132,17 @@ function upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir)
132
132
  return { file: candidate.projectPath, status: 'unchanged' };
133
133
  }
134
134
  const mine = readFileSync(projectFilePath, 'utf8');
135
+ // The common cases need no merge (and so no git): a file the project
136
+ // never edited takes the new template as is, and one that already
137
+ // matches it has nothing to do.
138
+ if (mine === theirs)
139
+ return { file: candidate.projectPath, status: 'unchanged' };
140
+ if (mine === base) {
141
+ writeFileSync(projectFilePath, theirs);
142
+ return { file: candidate.projectPath, status: 'updated' };
143
+ }
144
+ if (keepEdited)
145
+ return { file: candidate.projectPath, status: 'kept' };
135
146
  const scratchDir = mkdtempSync(join(tmpdir(), 'create-loopengine-merge-'));
136
147
  const mineTmp = join(scratchDir, 'mine');
137
148
  const baseTmp = join(scratchDir, 'base');
@@ -162,7 +173,7 @@ export function upgradeProject(options = {}) {
162
173
  }
163
174
  const resolveBase = options.resolveBaseTemplateDir ?? fetchPublishedTemplateDir;
164
175
  const baseTemplateDir = resolveBase(fromVersion);
165
- const files = UPGRADE_CANDIDATE_FILES.map((candidate) => upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir));
176
+ const files = UPGRADE_CANDIDATE_FILES.map((candidate) => upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir, options.keepEdited));
166
177
  const resolveLatestVersion = options.resolveLatestVersion ?? resolveLatestVersionFromNpm;
167
178
  const dependencies = bumpDependencies(projectDir, resolveLatestVersion);
168
179
  let npmInstall;
@@ -236,17 +247,23 @@ function bumpDependencies(projectDir, resolveLatestVersion) {
236
247
  }
237
248
  function parseUpgradeArgs(argv) {
238
249
  let from;
250
+ let json = false;
251
+ let keepEdited = false;
239
252
  for (let i = 0; i < argv.length; i++) {
240
253
  if (argv[i] === '--from')
241
254
  from = argv[++i];
255
+ else if (argv[i] === '--json')
256
+ json = true;
257
+ else if (argv[i] === '--keep-edited')
258
+ keepEdited = true;
242
259
  }
243
- return { from };
260
+ return { from, json, keepEdited };
244
261
  }
245
262
  function printUpgradeSummary(result) {
246
263
  console.log(`Upgrading create-loopengine@${result.fromVersion} -> @${result.toVersion}`);
247
264
  console.log();
248
265
  for (const f of result.files) {
249
- const label = f.status === 'updated' ? 'updated ' : f.status === 'conflict' ? 'conflict ' : f.status === 'unchanged' ? 'unchanged' : 'skipped ';
266
+ const label = f.status === 'updated' ? 'updated ' : f.status === 'conflict' ? 'conflict ' : f.status === 'unchanged' ? 'unchanged' : f.status === 'kept' ? 'kept ' : 'skipped ';
250
267
  console.log(` ${label} ${f.file}`);
251
268
  }
252
269
  console.log();
@@ -275,8 +292,20 @@ function parseArgs(argv) {
275
292
  export function main(argv = process.argv.slice(2)) {
276
293
  try {
277
294
  if (argv[0] === 'upgrade') {
278
- const { from } = parseUpgradeArgs(argv.slice(1));
279
- printUpgradeSummary(upgradeProject({ from }));
295
+ const { from, json, keepEdited } = parseUpgradeArgs(argv.slice(1));
296
+ if (json) {
297
+ // One JSON object on stdout, for a program driving the upgrade
298
+ // (loopengine's console); errors become { error } with exit 1.
299
+ try {
300
+ console.log(JSON.stringify(upgradeProject({ from, keepEdited })));
301
+ }
302
+ catch (err) {
303
+ console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
304
+ process.exitCode = 1;
305
+ }
306
+ return;
307
+ }
308
+ printUpgradeSummary(upgradeProject({ from, keepEdited }));
280
309
  return;
281
310
  }
282
311
  const { name } = parseArgs(argv);
@@ -285,7 +314,7 @@ export function main(argv = process.argv.slice(2)) {
285
314
  console.log();
286
315
  console.log(` cd ${destination}`);
287
316
  console.log(' npm install');
288
- console.log(' cp .env.example .env # fill in ANTHROPIC_API_KEY');
317
+ console.log(' cp .env.example .env # fill in OPENAI_API_KEY (or set it on the Models page)');
289
318
  console.log(' npm run dev');
290
319
  }
291
320
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
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",
@@ -1,7 +1,8 @@
1
- ANTHROPIC_API_KEY=
2
- # Using OpenAI instead? Swap agents/example-agent/index.ts's createModelCall
3
- # for createOpenAIModelCall({ model: '...' }) and set this instead:
4
- # OPENAI_API_KEY=
1
+ # The example agent uses OpenAI (agents/example-agent/index.ts's `model`).
2
+ # Set the key here, or on the console's Models page once the server runs.
3
+ OPENAI_API_KEY=
4
+ # Switching the agent to another provider? Set its key instead, e.g.:
5
+ # ANTHROPIC_API_KEY=
5
6
 
6
7
  PORT=8787
7
8
 
@@ -6,7 +6,7 @@ An agent server built with [loopengine](https://www.npmjs.com/package/loopengine
6
6
 
7
7
  ```bash
8
8
  npm install
9
- cp .env.example .env # fill in ANTHROPIC_API_KEY
9
+ cp .env.example .env # fill in OPENAI_API_KEY (or set it later on the Models page)
10
10
  ```
11
11
 
12
12
  ## Run
@@ -6,3 +6,5 @@ dist
6
6
  .env
7
7
  *.log
8
8
  .DS_Store
9
+ # Backups the console Upgrade button keeps of the files it changed.
10
+ .upgrade-backups
@@ -153,6 +153,10 @@ import {
153
153
  searchPublicAbilities,
154
154
  fetchLatestAbilityVersion,
155
155
  describeModelProviders,
156
+ loopengineVersion,
157
+ describeUpgrade,
158
+ runUpgrade,
159
+ UpgradeError,
156
160
  setModelProviderKey,
157
161
  clearModelProviderKey,
158
162
  ModelKeyError,
@@ -2984,6 +2988,52 @@ const server = createServer(async (req, res) => {
2984
2988
  res.writeHead(200, { 'content-type': 'text/html' }).end(globalConfigPageHtml)
2985
2989
  return
2986
2990
  }
2991
+ // What the sidebar's sign-in line and the Settings section show. In
2992
+ // hosted mode the email and dashboard link come from the router's
2993
+ // signed identity, so they can't be set by the caller.
2994
+ if (req.method === 'GET' && pathname === '/config/settings') {
2995
+ const identity = trustedProxySecret ? verifyProxyIdentity(req.headers[TRUSTED_PROXY_IDENTITY_HEADER], trustedProxySecret) : undefined
2996
+ const signIn = identity
2997
+ ? {
2998
+ mode: 'hosted',
2999
+ email: identity.email ?? null,
3000
+ dashboardUrl: identity.dashboardUrl && /^https?:\/\//.test(identity.dashboardUrl) ? identity.dashboardUrl : null,
3001
+ }
3002
+ : { mode: adminAuth ? 'basic' : 'open' }
3003
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(
3004
+ JSON.stringify({
3005
+ signIn,
3006
+ version: loopengineVersion(),
3007
+ environment: process.env.LOOPENGINE_ENV ?? 'production',
3008
+ publicUrl: process.env.LOOPENGINE_PUBLIC_URL ?? null,
3009
+ scheduler: process.env.LOOPENGINE_SCHEDULER === 'off' ? 'off' : 'on',
3010
+ }),
3011
+ )
3012
+ return
3013
+ }
3014
+ // The Settings section's Upgrade button (web/upgrade-admin.ts).
3015
+ if (req.method === 'GET' && pathname === '/config/upgrade') {
3016
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(
3017
+ JSON.stringify({ ...(await describeUpgrade()), canUpgrade: secretsProtected, restartsItself: exitOnCodeChange }),
3018
+ )
3019
+ return
3020
+ }
3021
+ if (req.method === 'POST' && pathname === '/config/upgrade') {
3022
+ if (!secretsProtected) {
3023
+ res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Refusing to upgrade without LOOPENGINE_ADMIN_AUTH configured.' }))
3024
+ return
3025
+ }
3026
+ try {
3027
+ const outcome = await runUpgrade({ busy: () => sessionTurns.size + scheduler.running > 0 })
3028
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, outcome, restarting: exitOnCodeChange }))
3029
+ } catch (err) {
3030
+ const status = err instanceof UpgradeError ? 409 : 500
3031
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
3032
+ return
3033
+ }
3034
+ exitIfCodeChanged(res)
3035
+ return
3036
+ }
2987
3037
  if (req.method === 'GET' && pathname === '/config/models') {
2988
3038
  res.writeHead(200, { 'content-type': 'application/json' }).end(
2989
3039
  JSON.stringify({ ...describeModelProviders(listAgents(), (name) => getEntry(name)?.config), canSave: secretsProtected }),
@@ -12,18 +12,17 @@
12
12
  // resolves relative to loopengine's own installed location, not your
13
13
  // project, so it only works inside loopengine's own repo — see
14
14
  // tools/index.ts's own comment.
15
- import { createAnthropicModelCall, type AgentConfig, type ModelCall } from 'loopengine'
15
+ import type { AgentConfig } from 'loopengine'
16
16
  import { tools } from './tools/index.js'
17
17
 
18
18
  export const config: AgentConfig = {
19
19
  name: 'example-agent',
20
20
  systemPrompt: 'You are a helpful assistant.',
21
21
  tools,
22
- }
23
-
24
- // Reads ANTHROPIC_API_KEY from the env. Swap for
25
- // createOpenAIModelCall({ model: '...' }) to use OpenAI instead — see
26
- // node_modules/loopengine/README.md's "Wiring a real model" section.
27
- export function createModelCall(): ModelCall {
28
- return createAnthropicModelCall({ model: 'claude-sonnet-5' })
22
+ // Reads OPENAI_API_KEY: set it on the console's Models page, or in .env.
23
+ // For another provider, change `provider` and `model` (e.g.
24
+ // { provider: 'anthropic', model: 'claude-sonnet-5' } with
25
+ // ANTHROPIC_API_KEY). reasoningEffort 'none' lets this gpt-5.x model use
26
+ // function tools; see AgentModelConfig in loopengine's agent-config.
27
+ model: { provider: 'openai', model: 'gpt-5.6-luna', reasoningEffort: 'none' },
29
28
  }
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.74",
12
+ "loopengine": "^0.1.76",
13
13
  "actauth": "^0.0.12"
14
14
  },
15
15
  "devDependencies": {