@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 +26 -0
- package/README.md +15 -8
- package/README.zh-CN.md +12 -9
- package/docs/cli.md +13 -3
- package/docs/troubleshooting.md +5 -4
- package/package.json +2 -2
- package/src/cli.ts +65 -7
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
|
|
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/
|
|
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
|
|
239
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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 #
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
159
|
+
从 GitHub 装的,只能靠它换版本:`pi update --extensions` **有意**不做这件事,pi 钉住你写的那个 ref,并且只把克隆对齐到**那个** ref。换版本是另一个决定,而做这个决定得先知道哪个 tag 最新——本该命令替你查,不是你查完再手打回来。从 npm 装、没写版本号的,`pi update --extensions` 本来就会升,`upgrade` 升完也保持不钉版本;钉了精确版本的,升完仍然钉着,钉到新版本上。
|
|
158
160
|
|
|
159
|
-
装完重启 pi(或者再跑一次 `pi-loops`)就生效。启动器不用重装:pi
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
19
|
-
`packages` in `~/.pi/agent/settings.json`, and
|
|
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
|
|
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
|
+
"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":
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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.
|