create-loopengine 0.1.14 → 0.1.16

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
@@ -61,8 +61,8 @@ own `discoverAgents`: it scans `agents/` and keys each entry by
61
61
  one new file, not a rewrite and not a registry edit either. See
62
62
  [loopengine's own README](https://github.com/evanyan22/loopengine) for the
63
63
  full `AgentConfig` surface: permission rules, skills, context-budget
64
- recovery, and how to wire a real model (Anthropic, OpenAI, or DeepSeek —
65
- all three included).
64
+ recovery, and how to wire a real model (Anthropic, OpenAI, DeepSeek,
65
+ Kimi, GLM, or Gemini — all six included).
66
66
 
67
67
  ## Add another agent
68
68
 
@@ -115,10 +115,60 @@ Each of the five template-owned files gets one of:
115
115
  as any git merge conflict — resolve them by hand, then `npx tsc
116
116
  --noEmit` to confirm it builds.
117
117
 
118
- `upgrade` never touches `agents/`, `README.md`, `.env.example`, or
119
- `package.json`'s own dependency versions — run `npm install
120
- loopengine@latest` (and `actauth`/`skillgarden` if you use them directly)
121
- separately to pick those up.
118
+ `upgrade` also bumps whichever of `loopengine`/`actauth`/`skillgarden`
119
+ are already in your `package.json` to their latest published version
120
+ (never adds one you don't already depend on) and runs `npm install` for
121
+ you — a merged file that now calls a newer export is a silent, hard-to-
122
+ notice failure if the installed package hasn't actually caught up, so
123
+ this isn't left as a manual step. If the install itself fails (offline,
124
+ a registry hiccup), the version bump is still written to `package.json`;
125
+ just run `npm install` yourself to finish.
126
+
127
+ `upgrade` never touches `agents/`, `README.md`, `.env.example`, or any
128
+ dependency *other* than those three.
129
+
130
+ ## Running in production
131
+
132
+ `npm run dev` (`tsx watch`) restarts on every file change — fine for
133
+ local iteration, wrong for a real server, since a restart drops every
134
+ in-flight request and any pending live approval/question (see
135
+ loopengine's own README on durable vs. live approvers if that matters to
136
+ you). Use [pm2](https://pm2.keymetrics.io/) to run the plain, non-watch
137
+ server instead, with restart-on-crash:
138
+
139
+ ```bash
140
+ pm2 start npx --name my-agents --interpreter none -- loopengine serve
141
+ pm2 save # persist across reboots
142
+ pm2 startup # (one-time) launch pm2 itself on boot
143
+ ```
144
+
145
+ `--interpreter none` matters — without it pm2 may try to run `npx`
146
+ through `node` directly, which fails since it's a shell-invokable binary,
147
+ not a bare `.js` file. `--` separates pm2's own flags from the command's
148
+ own args (`loopengine serve`).
149
+
150
+ Two gotchas worth knowing before they cost you a debugging session:
151
+
152
+ - **`npm` is not `npx`.** `pm2 start npm --name my-agents -- loopengine
153
+ serve` does *not* work — `npm <args>` treats its args as npm's own
154
+ subcommands, not a binary to run, so it fails with `Unknown command:
155
+ "loopengine"`. If you'd rather invoke via `npm`, add a `"start":
156
+ "loopengine serve"` script to `package.json` first, then `pm2 start npm
157
+ --name my-agents -- start` (npm special-cases `start` as a shorthand
158
+ for running that script, unlike arbitrary script names).
159
+ - **pm2 remembers a name's original `cwd`/script, not whatever you pass
160
+ next time.** Running `pm2 start ... --name my-agents` again after
161
+ moving/recreating the project directory just restarts the *already-
162
+ registered* process with its *original* path — your new files are
163
+ never even read. Run `pm2 delete my-agents` first if the project's
164
+ location changed, then start fresh.
165
+
166
+ `pm2 logs my-agents` tails output; `pm2 restart my-agents` after any
167
+ manual edit to `adapters/http.ts`/`adapters/cli.ts`/`agent-registry.ts`
168
+ (see "Upgrading" above) or an env var change — those don't take effect
169
+ until the process restarts. Adding a skill or tool through the Admin UI
170
+ does *not* need a restart — that's applied to the already-running process
171
+ directly.
122
172
 
123
173
  ## Status
124
174
 
package/dist/cli.d.ts CHANGED
@@ -11,10 +11,29 @@ export interface UpgradeFileResult {
11
11
  file: string;
12
12
  status: UpgradeFileStatus;
13
13
  }
14
+ export interface UpgradeDependencyResult {
15
+ name: string;
16
+ from: string;
17
+ to: string;
18
+ }
14
19
  export interface UpgradeResult {
15
20
  fromVersion: string;
16
21
  toVersion: string;
17
22
  files: UpgradeFileResult[];
23
+ /** Which of loopengine/actauth/skillgarden actually had their
24
+ * package.json range bumped — only ones already present as a
25
+ * dependency, never newly added (see bumpDependencies's own doc
26
+ * comment). Empty if the project has no package.json, or every present
27
+ * one was already at the latest range. */
28
+ dependencies: UpgradeDependencyResult[];
29
+ /** Only attempted when `dependencies` is non-empty — running `npm
30
+ * install` for a project with nothing to bump would be pointless. */
31
+ npmInstall?: {
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ };
18
37
  }
19
38
  export interface UpgradeOptions {
20
39
  /** Defaults to process.cwd(). */
@@ -31,6 +50,13 @@ export interface UpgradeOptions {
31
50
  * (a real `npm pack`). Test-only seam to avoid hitting the real
32
51
  * registry for a fixture "old" template. */
33
52
  resolveBaseTemplateDir?: (version: string) => string;
53
+ /** Resolves a package's current latest version — defaults to a real
54
+ * `npm view <pkg> version`. Test-only seam to avoid hitting the real
55
+ * registry. */
56
+ resolveLatestVersion?: (pkg: string) => string;
57
+ /** Runs `npm install` in projectDir — defaults to a real one. Test-only
58
+ * seam to avoid actually installing anything during tests. */
59
+ runNpmInstall?: (projectDir: string) => void;
34
60
  }
35
61
  export declare function upgradeProject(options?: UpgradeOptions): UpgradeResult;
36
62
  export declare function main(argv?: string[]): void;
package/dist/cli.js CHANGED
@@ -163,8 +163,76 @@ export function upgradeProject(options = {}) {
163
163
  const resolveBase = options.resolveBaseTemplateDir ?? fetchPublishedTemplateDir;
164
164
  const baseTemplateDir = resolveBase(fromVersion);
165
165
  const files = UPGRADE_CANDIDATE_FILES.map((candidate) => upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir));
166
+ const resolveLatestVersion = options.resolveLatestVersion ?? resolveLatestVersionFromNpm;
167
+ const dependencies = bumpDependencies(projectDir, resolveLatestVersion);
168
+ let npmInstall;
169
+ if (dependencies.length > 0) {
170
+ const install = options.runNpmInstall ?? runRealNpmInstall;
171
+ try {
172
+ install(projectDir);
173
+ npmInstall = { ok: true };
174
+ }
175
+ catch (err) {
176
+ // Non-fatal — the file merge and the package.json bump already
177
+ // succeeded and are on disk either way; a failed install (offline,
178
+ // a registry hiccup) just means the operator runs `npm install`
179
+ // themselves, the same single command this was trying to save them
180
+ // from typing, not a reason to unwind everything already done.
181
+ npmInstall = { ok: false, error: err instanceof Error ? err.message : String(err) };
182
+ }
183
+ }
166
184
  writeFileSync(provenancePath, JSON.stringify({ version: toVersion }, null, 2) + '\n');
167
- return { fromVersion, toVersion, files };
185
+ return { fromVersion, toVersion, files, dependencies, npmInstall };
186
+ }
187
+ function resolveLatestVersionFromNpm(pkg) {
188
+ return execFileSync('npm', ['view', pkg, 'version'], { stdio: 'pipe' }).toString().trim();
189
+ }
190
+ function runRealNpmInstall(projectDir) {
191
+ execFileSync('npm', ['install'], { cwd: projectDir, stdio: 'pipe' });
192
+ }
193
+ // Only loopengine/actauth/skillgarden — the three packages a scaffolded
194
+ // project's own adapters/http.ts can import directly (see this repo's own
195
+ // README on why: template code that imports a package must declare it as
196
+ // its own direct dependency, not rely on loopengine's transitive one) —
197
+ // and only ones already present in the project's own package.json.
198
+ // Deliberately never *adds* a dependency a project doesn't already have:
199
+ // this command upgrades what's there, it doesn't decide what a project
200
+ // should depend on.
201
+ const UPGRADABLE_DEPENDENCIES = ['loopengine', 'actauth', 'skillgarden'];
202
+ /** Bumps package.json's dependency ranges for whichever of
203
+ * loopengine/actauth/skillgarden are already present, to `^<latest>` —
204
+ * run automatically as part of `upgrade` (not left as a manual follow-up
205
+ * step) because a project whose merged template code now calls a newer
206
+ * export, but whose installed package is still old, fails silently: no
207
+ * conflict marker, no build error, just a feature that's quietly not
208
+ * there yet (confirmed live — this exact gap is what prompted adding this
209
+ * function in the first place, see this repo's own git history). Returns
210
+ * [] untouched if the project has no package.json at all, same
211
+ * "can't merge what isn't there" reasoning upgradeFile's own missing-file
212
+ * check uses. */
213
+ function bumpDependencies(projectDir, resolveLatestVersion) {
214
+ const pkgPath = join(projectDir, 'package.json');
215
+ if (!existsSync(pkgPath))
216
+ return [];
217
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
218
+ const deps = pkg.dependencies;
219
+ if (!deps)
220
+ return [];
221
+ const changes = [];
222
+ for (const name of UPGRADABLE_DEPENDENCIES) {
223
+ const currentRange = deps[name];
224
+ if (currentRange === undefined)
225
+ continue;
226
+ const latest = resolveLatestVersion(name);
227
+ const newRange = `^${latest}`;
228
+ if (currentRange === newRange)
229
+ continue;
230
+ deps[name] = newRange;
231
+ changes.push({ name, from: currentRange, to: newRange });
232
+ }
233
+ if (changes.length > 0)
234
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
235
+ return changes;
168
236
  }
169
237
  function parseUpgradeArgs(argv) {
170
238
  let from;
@@ -187,7 +255,18 @@ function printUpgradeSummary(result) {
187
255
  console.log(`${conflicted.length} file(s) have <<<<<<< conflict markers to resolve by hand before this will build.`);
188
256
  }
189
257
  else {
190
- console.log('No conflicts. Run `npm install loopengine@latest` to pick up any library-side changes too.');
258
+ console.log('No conflicts.');
259
+ }
260
+ if (result.dependencies.length > 0) {
261
+ console.log();
262
+ for (const dep of result.dependencies)
263
+ console.log(` ${dep.name}: ${dep.from} -> ${dep.to}`);
264
+ if (result.npmInstall?.ok) {
265
+ console.log('npm install completed.');
266
+ }
267
+ else if (result.npmInstall) {
268
+ console.log(`npm install failed (${result.npmInstall.error}) — run it yourself to finish picking up the new versions.`);
269
+ }
191
270
  }
192
271
  }
193
272
  function parseArgs(argv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",
@@ -645,7 +645,14 @@ function parseAgentTemplateOptions(body: Record<string, unknown>): { ok: true; v
645
645
  return { ok: false, error: 'model must be an object' }
646
646
  }
647
647
  const provider = (body.model as Record<string, unknown>).provider
648
- if (provider !== 'anthropic' && provider !== 'openai' && provider !== 'deepseek') {
648
+ if (
649
+ provider !== 'anthropic' &&
650
+ provider !== 'openai' &&
651
+ provider !== 'deepseek' &&
652
+ provider !== 'kimi' &&
653
+ provider !== 'glm' &&
654
+ provider !== 'gemini'
655
+ ) {
649
656
  return { ok: false, error: `unsupported model provider '${String(provider)}'` }
650
657
  }
651
658
  const modelName = (body.model as Record<string, unknown>).model
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.6",
12
+ "loopengine": "^0.1.7",
13
13
  "actauth": "^0.0.12",
14
14
  "skillgarden": "^0.0.9"
15
15
  },