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 +1 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +36 -7
- package/package.json +1 -1
- package/template/.env.example +5 -4
- package/template/README.md +1 -1
- package/template/_gitignore +2 -0
- package/template/adapters/http.ts +50 -0
- package/template/agents/example-agent/index.ts +7 -8
- package/template/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
package/template/.env.example
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
#
|
|
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
|
|
package/template/README.md
CHANGED
package/template/_gitignore
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
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
|
}
|