@alphacoder-v0/pi-loops 0.17.3 → 0.17.4

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/CHANGELOG.md CHANGED
@@ -3,6 +3,32 @@
3
3
  All notable changes to pi-loops are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
5
5
 
6
+ ## [0.17.4] - 2026-09-14
7
+
8
+ ### Added
9
+ - **pi-loops is on npm, and the documents say so.** `@alphacoder-v0/pi-loops` was published at
10
+ 0.17.3, installed from the registry into a clean directory, its `pi-loops` bin run from that
11
+ `node_modules` layout, and loaded with `pi -e npm:@alphacoder-v0/pi-loops` before anything here
12
+ changed. `piLoops.publishedToNpm` is now `true`, which is the one line `check:docs` was waiting
13
+ for, and both READMEs lead with `pi install npm:@alphacoder-v0/pi-loops` — without a version,
14
+ because pi treats an npm spec without an exact version as one `pi update --extensions` moves, and
15
+ that is what someone who did not ask for a pin wants. GitHub stays the pinned route. The launcher
16
+ instructions, the upgrade notes, the uninstall line and `docs/troubleshooting.md` name the npm
17
+ install beside the git one.
18
+
19
+ ### Fixed
20
+ - **`pi-loops upgrade` pinned an npm install that was following releases.** It answered every npm
21
+ copy with `npm:<name>@<version>`, and pi skips an exact version in every later
22
+ `pi update --extensions` — so the first upgrade of a bare install would also have been the last one
23
+ anything but `upgrade` ever did. It now reads the entry pi recorded in the `settings.json` beside
24
+ the install root (`~/.pi/agent/settings.json` for `~/.pi/agent/npm`, `.pi/settings.json` for a
25
+ project's `.pi/npm`): an exact pin stays a pin, moved to the new version; a bare name, `@latest` or
26
+ a range gets `npm:<name>@latest`. Not the bare name, which reads as the obvious answer and is wrong:
27
+ over an existing install, `npm install <name>` keeps the range it saved the first time and installs
28
+ nothing, so `upgrade` would have printed "upgraded" over the version it started from — checked
29
+ against npm itself, not assumed. With no entry to read, it pins, as before. The unknown-command hint
30
+ names the same spec.
31
+
6
32
  ## [0.17.3] - 2026-09-13
7
33
 
8
34
  ### Fixed
package/README.md CHANGED
@@ -36,7 +36,8 @@ one message, make sure you get an answer. pi-loops runs sub-agents while you are
36
36
  the first sign of credentials that do not work should not be an empty inbox tomorrow morning.
37
37
 
38
38
  ```bash
39
- pi install git:github.com/alphacoder-v0/pi-loops@v0.17.3 # from GitHub, at a pinned tag
39
+ pi install npm:@alphacoder-v0/pi-loops # from npm, following new releases
40
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.17.4 # or from GitHub, at a pinned tag
40
41
  pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
41
42
  pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
42
43
  ```
@@ -182,7 +183,8 @@ directory rather than on your `PATH`, so the command does not exist yet — the
182
183
 
183
184
  ```bash
184
185
  # or from a shell, in the directory pi installed the package into
185
- cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops
186
+ cd ~/.pi/agent/npm/node_modules/@alphacoder-v0/pi-loops # installed from npm
187
+ cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops # installed from GitHub
186
188
  node src/cli-entry.mjs install-launcher
187
189
  ```
188
190
 
@@ -235,18 +237,23 @@ pi-loops upgrade --check # just say whether there is one
235
237
  ```
236
238
 
237
239
  It reads the release tags from the repository this copy came from, compares them with what you are
238
- running, and installs the newest — which `pi update --extensions` will not do, because pi pins the
239
- ref you asked for and keeps the clone on *that* ref ([docs/cli.md](docs/cli.md)).
240
+ running, and installs the newest the same way this copy was installed. For a GitHub install that is
241
+ the only way to move: `pi update --extensions` keeps the clone on the ref you pinned
242
+ ([docs/cli.md](docs/cli.md)). An npm install made without a version is one `pi update --extensions`
243
+ moves as well, and `upgrade` leaves it that way; one pinned to an exact version stays pinned, to the
244
+ new one.
240
245
 
241
- Restart pi (or run `pi-loops` again) to load it. The launcher does not need reinstalling: pi keeps
242
- each git package at `~/.pi/agent/git/<host>/<owner>/<repo>`, so a version change keeps the path.
246
+ Restart pi (or run `pi-loops` again) to load it. The launcher does not need reinstalling: pi keeps a
247
+ package at the same path whatever its version — `~/.pi/agent/npm/node_modules/<name>` or
248
+ `~/.pi/agent/git/<host>/<owner>/<repo>`.
243
249
 
244
- Releases are tags on GitHub; [CHANGELOG.md](CHANGELOG.md) says what is in each one.
250
+ Releases are tags on GitHub, published to npm under the same version; [CHANGELOG.md](CHANGELOG.md)
251
+ says what is in each one.
245
252
 
246
253
  ### Uninstall
247
254
 
248
255
  ```bash
249
- pi remove git:github.com/alphacoder-v0/pi-loops # or the checkout path — whatever you installed
256
+ pi remove npm:@alphacoder-v0/pi-loops # or git:github.com/alphacoder-v0/pi-loops, or the checkout path — whatever you installed
250
257
  ```
251
258
 
252
259
  State stays in `~/.pi/agent/loops` until you delete it.
package/README.zh-CN.md CHANGED
@@ -27,7 +27,7 @@ pi-loops --tui # 猜错时强制终端
27
27
  pi-loops --continue # 接着这个目录里最新的那个会话
28
28
  pi-loops sessions [--all] # 这个目录(或整台机器)记下的会话
29
29
  pi-loops inspect <file> # 别人发来的 .pisession,导入前先看清里面是什么
30
- pi-loops upgrade # 从 GitHub 装最新的发布版
30
+ pi-loops upgrade # 装最新的发布版,当初从哪装就从哪装
31
31
  pi-loops host status # 看一眼没有 pi 开着时在跑的自动化
32
32
  ```
33
33
 
@@ -44,11 +44,12 @@ pi-loops 自己没有任何运行时依赖。
44
44
  ### 2. 装上
45
45
 
46
46
  ```bash
47
- pi install git:github.com/alphacoder-v0/pi-loops@v0.17.3 # 从 GitHub 装,钉住一个 tag
47
+ pi install npm:@alphacoder-v0/pi-loops # 从 npm 装,跟着新版本走
48
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.17.4 # 或者从 GitHub 装,钉住一个 tag
48
49
  pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
49
50
  ```
50
51
 
51
- **两种装法挑一种,不要装两份。** 两份副本注册同名工具,pi 会拒绝加载第二份并直接退出(`Tool "cron_create" conflicts with …`)。如果你在改这份代码,留本地检出那份。
52
+ **几种装法挑一种,不要装两份。** 两份副本注册同名工具,pi 会拒绝加载第二份并直接退出(`Tool "cron_create" conflicts with …`)。如果你在改这份代码,留本地检出那份。
52
53
 
53
54
  重启 pi,装到这里就够了——那四个斜杠命令现在就能用。下面这步只跟 `pi-loops` 这个命令有关:浏览器窗口和几个 shell 子命令靠它,不要浏览器窗口可以先跳过。
54
55
 
@@ -60,7 +61,8 @@ pi install /path/to/pi-loops # 或者本地检出
60
61
 
61
62
  ```bash
62
63
  # 或者在 shell 里,进到 pi 装包的那个目录
63
- cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops
64
+ cd ~/.pi/agent/npm/node_modules/@alphacoder-v0/pi-loops # 从 npm 装的
65
+ cd ~/.pi/agent/git/github.com/alphacoder-v0/pi-loops # 从 GitHub 装的
64
66
  node src/cli-entry.mjs install-launcher
65
67
  ```
66
68
 
@@ -152,18 +154,19 @@ pi-loops upgrade # 装最新的发布版
152
154
  pi-loops upgrade --check # 只看看有没有新的
153
155
  ```
154
156
 
155
- 它从这份副本的来源仓库读 release tag,和你正在跑的版本比,然后装最新那个。
157
+ 它从这份副本的来源仓库读 release tag,和你正在跑的版本比,然后按这份副本当初的装法装最新那个。
156
158
 
157
- `pi update --extensions` **有意**不做这件事:pi 钉住你写的那个 ref,并且只把克隆对齐到**那个** ref。换版本是另一个决定,而做这个决定得先知道哪个 tag 最新——本该命令替你查,不是你查完再手打回来。
159
+ 从 GitHub 装的,只能靠它换版本:`pi update --extensions` **有意**不做这件事,pi 钉住你写的那个 ref,并且只把克隆对齐到**那个** ref。换版本是另一个决定,而做这个决定得先知道哪个 tag 最新——本该命令替你查,不是你查完再手打回来。从 npm 装、没写版本号的,`pi update --extensions` 本来就会升,`upgrade` 升完也保持不钉版本;钉了精确版本的,升完仍然钉着,钉到新版本上。
158
160
 
159
- 装完重启 pi(或者再跑一次 `pi-loops`)就生效。启动器不用重装:pi 把每个 git 包固定放在 `~/.pi/agent/git/<host>/<owner>/<repo>`,换版本路径不变。
161
+ 装完重启 pi(或者再跑一次 `pi-loops`)就生效。启动器不用重装:pi 放包的路径不随版本变——`~/.pi/agent/npm/node_modules/<包名>` 或 `~/.pi/agent/git/<host>/<owner>/<repo>`。
160
162
 
161
- 版本就是 GitHub 上的 tag,每个 tag 里有什么见 [CHANGELOG.md](CHANGELOG.md)。
163
+ 版本就是 GitHub 上的 tag,同一个版本号也发到 npm,每个 tag 里有什么见 [CHANGELOG.md](CHANGELOG.md)。
162
164
 
163
165
  ## 卸载
164
166
 
165
167
  ```bash
166
- pi remove git:github.com/alphacoder-v0/pi-loops # 当初怎么装的就怎么删:git: ref 或检出路径
168
+ pi remove npm:@alphacoder-v0/pi-loops # 当初怎么装的就怎么删:npm: 包名、git: ref 或检出路径
169
+ pi remove git:github.com/alphacoder-v0/pi-loops
167
170
  pi -e /path/to/pi-loops # 或者:只在这次启动试用,什么都不装
168
171
  pi update --extensions # 对齐已安装的包
169
172
  ```
package/docs/cli.md CHANGED
@@ -184,9 +184,19 @@ It reads release tags from the repository this copy came from (`repository.url`
184
184
  numerically, so `v0.10.0` beats `v0.9.0` — and runs `pi install` for it. Release candidates and
185
185
  branch-shaped tags are ignored: those are not things to move someone onto without being asked.
186
186
 
187
- `pi update --extensions` does something different and both are useful: it reconciles every package
188
- to the ref already pinned in your settings, which is how you repair a clone, not how you take a new
189
- version.
187
+ The spec it installs follows how this copy was installed, because a spec from another source adds a
188
+ second package instead of replacing this one. A git install gets `git:<host>/<owner>/<repo>@<tag>`.
189
+ An npm install gets its spec from the entry pi recorded in `settings.json`: pinned to an exact
190
+ version, it is pinned to the new one (`npm:@alphacoder-v0/pi-loops@<version>`); installed without a
191
+ version, with `@latest` or with a range, it gets `npm:@alphacoder-v0/pi-loops@latest` — not the bare
192
+ name, which over an existing install makes npm keep the range it saved and install nothing, and not
193
+ an exact version, which pi would then skip in every later update. If there is no entry to read, it
194
+ pins.
195
+
196
+ `pi update --extensions` does something different and both are useful: for a git package it
197
+ reconciles the clone to the ref already pinned in your settings, which is how you repair a clone,
198
+ not how you take a new version. For an npm package installed without an exact version it does take
199
+ the newest release, and `upgrade` and it then agree.
190
200
 
191
201
  ## The tools
192
202
 
@@ -15,14 +15,15 @@ card and in `/cron trace <job> 1 checker`.
15
15
 
16
16
  **`Tool "cron_create" conflicts with …`, and pi exits.** The package is installed twice, and this
17
17
  is fatal rather than cosmetic: the second copy fails to load and pi stops. It happens easily —
18
- a local checkout you are working on plus `pi install git:…` of the published one both sit in
19
- `packages` in `~/.pi/agent/settings.json`, and both register the same tools. (An older, milder
20
- symptom of the same thing was commands appearing as `/cron:1`.)
18
+ a local checkout you are working on plus `pi install npm:…` or `pi install git:…` of the published
19
+ one all sit in `packages` in `~/.pi/agent/settings.json`, and each registers the same tools. (An
20
+ older, milder symptom of the same thing was commands appearing as `/cron:1`.)
21
21
 
22
22
  Keep one:
23
23
 
24
24
  ```bash
25
- pi remove git:github.com/alphacoder-v0/pi-loops # keep the checkout you are working on
25
+ pi remove npm:@alphacoder-v0/pi-loops # keep the checkout you are working on (installed from npm)
26
+ pi remove git:github.com/alphacoder-v0/pi-loops # keep the checkout you are working on (installed from GitHub)
26
27
  pi remove /path/to/your/checkout # or keep the installed one
27
28
  ```
28
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphacoder-v0/pi-loops",
3
- "version": "0.17.3",
3
+ "version": "0.17.4",
4
4
  "description": "An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "piLoops": {
44
- "publishedToNpm": false
44
+ "publishedToNpm": true
45
45
  },
46
46
  "scripts": {
47
47
  "ci": "npm run typecheck && npm run lint && npm run check:scripts && npm run check:docs && npm test",
package/src/cli.ts CHANGED
@@ -367,7 +367,7 @@ export async function runCli(argv: string[], out: (line: string) => void = conso
367
367
  out(`unknown command ${JSON.stringify(command)} (this is pi-loops v${PI_LOOPS_VERSION})`);
368
368
  // The route has to match how this copy was installed, for the same reason `upgrade` does: a
369
369
  // spec from the other one installs a second copy rather than replacing this one.
370
- const spec = upgradeSpec(packageRoot(), manifest().name ?? "", agentDir, "<newer release>");
370
+ const spec = upgradeSpec(packageRoot(), manifest().name ?? "", agentDir, "<newer release>", configuredNpmSource(packageRoot(), manifest().name ?? ""));
371
371
  out(`if you expected it, the copy you are running may be older than the command: ${spec ? `pi install ${spec}` : checkoutUpgradeHint()}`);
372
372
  out("");
373
373
  }
@@ -442,11 +442,17 @@ function manifest(): { name?: string; repositoryUrl?: string } {
442
442
  * path — and a local checkout is in neither place. A checkout has no install to redo: it has a
443
443
  * remote, and `git pull` is its upgrade.
444
444
  */
445
- export function upgradeSpec(packageDir: string, packageName: string, agentDir: string, tag: string): string | undefined {
446
- const segments = path.resolve(packageDir).split(path.sep).filter(Boolean);
447
- // A scoped name is two segments on disk, so compare as many as the name has.
448
- const name = packageName.split("/").filter(Boolean);
449
- if (name.length && segments.slice(-name.length).join("/") === name.join("/") && segments[segments.length - name.length - 1] === "node_modules") {
445
+ export function upgradeSpec(packageDir: string, packageName: string, agentDir: string, tag: string, configuredSource?: string): string | undefined {
446
+ if (npmInstallRoot(packageDir, packageName)) {
447
+ // pi skips an exact npm version when it updates packages and moves anything else, so the
448
+ // answer has to keep whichever of the two the install was. Pinning a bare install here would
449
+ // make this upgrade the last one `pi update` ever does. The moving answer is `@latest`, not the
450
+ // bare name: `npm install <name>` over an existing install keeps the range it saved and changes
451
+ // nothing, while `@latest` is what pi's own update asks npm for and is not a pin to pi. Not
452
+ // knowing how it was installed keeps the exact pin: it is the reading that cannot move a
453
+ // package somewhere it was not asked to go.
454
+ const configured = configuredSource === undefined ? undefined : npmSourceVersion(configuredSource, packageName);
455
+ if (configured !== undefined && !isExactNpmVersion(configured)) return `npm:${packageName}@latest`;
450
456
  // Release tags are `v0.17.0`; the version npm knows the same release by is `0.17.0`.
451
457
  return `npm:${packageName}@${tag.replace(/^v/, "")}`;
452
458
  }
@@ -459,6 +465,58 @@ export function upgradeSpec(packageDir: string, packageName: string, agentDir: s
459
465
  return undefined;
460
466
  }
461
467
 
468
+ /** The directory `npm install` ran in (the one holding `node_modules`), if this copy is an npm install. */
469
+ function npmInstallRoot(packageDir: string, packageName: string): string | undefined {
470
+ const segments = path.resolve(packageDir).split(path.sep).filter(Boolean);
471
+ // A scoped name is two segments on disk, so compare as many as the name has.
472
+ const name = packageName.split("/").filter(Boolean);
473
+ if (!name.length || segments.slice(-name.length).join("/") !== name.join("/") || segments[segments.length - name.length - 1] !== "node_modules") return undefined;
474
+ return path.join(path.parse(path.resolve(packageDir)).root, ...segments.slice(0, -name.length - 1));
475
+ }
476
+
477
+ /**
478
+ * The version an `npm:` source gives `packageName` — `""` for none — or undefined if it names some
479
+ * other package. The name is known, so this compares a prefix rather than parsing a spec: a project's
480
+ * `.pi/settings.json` can come from a cloned repository, and a general spec regex backtracks
481
+ * quadratically on an entry built for it.
482
+ */
483
+ function npmSourceVersion(source: string, packageName: string): string | undefined {
484
+ const spec = source.trim().replace(/^npm:/, "").trim();
485
+ if (!spec.startsWith(packageName)) return undefined;
486
+ const rest = spec.slice(packageName.length);
487
+ if (rest === "") return "";
488
+ return rest.startsWith("@") ? rest.slice(1) : undefined;
489
+ }
490
+
491
+ /** What pi counts as pinned: a valid semver, which it also accepts with a leading `v`. */
492
+ function isExactNpmVersion(version: string): boolean {
493
+ return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
494
+ }
495
+
496
+ /**
497
+ * The `npm:` source this install is recorded under, as written. pi keeps a user install in
498
+ * `<agent dir>/npm` and records it in `<agent dir>/settings.json`, and a project one in `.pi/npm`
499
+ * recorded in `.pi/settings.json` — in both, the settings file sits beside the install root.
500
+ */
501
+ export function configuredNpmSource(packageDir: string, packageName: string): string | undefined {
502
+ const root = npmInstallRoot(packageDir, packageName);
503
+ if (!root) return undefined;
504
+ let packages: unknown;
505
+ try {
506
+ packages = JSON.parse(fs.readFileSync(path.join(path.dirname(root), "settings.json"), "utf8"))?.packages;
507
+ } catch {
508
+ // Missing (an older pi's global install has no settings beside it) or unreadable: not knowing
509
+ // how this was installed is an answer `upgradeSpec` handles, not a reason to fail an upgrade.
510
+ return undefined;
511
+ }
512
+ if (!Array.isArray(packages)) return undefined;
513
+ for (const entry of packages) {
514
+ const source = typeof entry === "string" ? entry : typeof entry?.source === "string" ? entry.source : undefined;
515
+ if (source?.trim().startsWith("npm:") && npmSourceVersion(source, packageName) !== undefined) return source;
516
+ }
517
+ return undefined;
518
+ }
519
+
462
520
  /** What to tell someone whose copy is a checkout: there is nothing for `pi install` to do. */
463
521
  function checkoutUpgradeHint(): string {
464
522
  return `git pull in ${packageRoot()}, then restart pi`;
@@ -490,7 +548,7 @@ async function upgrade(checkOnly: boolean, out: (line: string) => void): Promise
490
548
  out("already up to date");
491
549
  return 0;
492
550
  }
493
- const spec = upgradeSpec(packageRoot(), name ?? "", getAgentDir(), latest);
551
+ const spec = upgradeSpec(packageRoot(), name ?? "", getAgentDir(), latest, configuredNpmSource(packageRoot(), name ?? ""));
494
552
  if (!spec) {
495
553
  // Nothing failed, so this is not an error: the newest release is out and the way to it is a
496
554
  // pull, not an install that would leave a second copy beside the checkout.