@alphacoder-v0/pi-loops 0.22.0 → 0.22.1
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 +13 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/cli.md +7 -2
- package/package.json +1 -1
- package/src/cli.ts +57 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to pi-loops are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.22.1] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **The launcher survives the package being installed by the other route.** The `sh` script
|
|
12
|
+
`install-launcher` writes named one entry file and nothing else, so `pi install npm:…` over a
|
|
13
|
+
`git:` copy — or the reverse — left every `pi-loops` ending in Node's `Cannot find module
|
|
14
|
+
…/src/cli-entry.mjs` stack trace, `pi-loops upgrade` included; the comment claiming it "survives
|
|
15
|
+
the package moving" was the only thing that did. It now carries both places pi installs this
|
|
16
|
+
package to (`npm/node_modules/<name>` and `git/<host>/<owner>/<repo>`, under the agent directory
|
|
17
|
+
it was written with) as literal paths and runs the first one that is there, falls back to the
|
|
18
|
+
`node` on `PATH` when the node it recorded has gone too, and when no copy is in any of them says
|
|
19
|
+
so in one line — write the launcher again with `install-launcher` — instead of a stack trace.
|
|
20
|
+
|
|
8
21
|
## [0.22.0] - 2026-09-16
|
|
9
22
|
|
|
10
23
|
### Added
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ the first sign of credentials that do not work should not be an empty inbox tomo
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
pi install npm:@alphacoder-v0/pi-loops # from npm, following new releases
|
|
41
|
-
pi install git:github.com/alphacoder-v0/pi-loops@v0.22.
|
|
41
|
+
pi install git:github.com/alphacoder-v0/pi-loops@v0.22.1 # or from GitHub, at a pinned tag
|
|
42
42
|
pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
|
|
43
43
|
pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
|
|
44
44
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -45,7 +45,7 @@ pi-loops 自己没有任何运行时依赖。
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
pi install npm:@alphacoder-v0/pi-loops # 从 npm 装,跟着新版本走
|
|
48
|
-
pi install git:github.com/alphacoder-v0/pi-loops@v0.22.
|
|
48
|
+
pi install git:github.com/alphacoder-v0/pi-loops@v0.22.1 # 或者从 GitHub 装,钉住一个 tag
|
|
49
49
|
pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
|
|
50
50
|
```
|
|
51
51
|
|
package/docs/cli.md
CHANGED
|
@@ -169,8 +169,13 @@ node src/cli-entry.mjs install-launcher
|
|
|
169
169
|
node src/cli-entry.mjs install-launcher --dir ~/bin
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
It writes a
|
|
173
|
-
|
|
172
|
+
It writes a small `sh` script that names the node you ran it with and the package it lives in — a
|
|
173
|
+
launcher rather than a symlink. Neither path is trusted for ever: if the package is not where it
|
|
174
|
+
was, the launcher looks in both of the directories above (the ones pi installs into, under the agent
|
|
175
|
+
directory this was written with), so installing again by the other route does not break it, and if
|
|
176
|
+
that node has gone it falls back to the `node` on your `PATH`. When no copy is in any of them it
|
|
177
|
+
prints one line saying to run `install-launcher` again — from inside pi, or from wherever pi has put
|
|
178
|
+
the package now — instead of a Node stack trace about a missing file.
|
|
174
179
|
|
|
175
180
|
## Upgrading
|
|
176
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alphacoder-v0/pi-loops",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
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",
|
package/src/cli.ts
CHANGED
|
@@ -579,12 +579,21 @@ function packageRoot(): string {
|
|
|
579
579
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
-
/**
|
|
583
|
-
|
|
582
|
+
/**
|
|
583
|
+
* Where this copy came from and what it is called, so an upgrade goes back to the same place —
|
|
584
|
+
* plus the `host/owner/repo` inside that URL, which is how pi spells the same repository in a
|
|
585
|
+
* `git:` spec and on disk. The URL is read here and nowhere else, so the two cannot drift apart.
|
|
586
|
+
*/
|
|
587
|
+
function manifest(): { name?: string; repositoryUrl?: string; repoPath?: string } {
|
|
584
588
|
try {
|
|
585
589
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(), "package.json"), "utf8"));
|
|
586
590
|
const url = String(pkg?.repository?.url ?? "").replace(/^git\+/, "").replace(/\.git$/, "");
|
|
587
|
-
|
|
591
|
+
const at = /(?:^|@|\/\/)([^/@\s]+\.[a-z]{2,})[/:]([^/\s]+)\/([^/\s]+)$/.exec(url);
|
|
592
|
+
return {
|
|
593
|
+
name: typeof pkg?.name === "string" ? pkg.name : undefined,
|
|
594
|
+
repositoryUrl: url || undefined,
|
|
595
|
+
repoPath: at ? `${at[1]}/${at[2]}/${at[3]}` : undefined,
|
|
596
|
+
};
|
|
588
597
|
} catch {
|
|
589
598
|
return {}; // installed without its package.json: nothing to ask
|
|
590
599
|
}
|
|
@@ -774,17 +783,41 @@ export async function installLauncherWithConfirm(args: string[], confirm: (title
|
|
|
774
783
|
const target = launcherTarget(dir);
|
|
775
784
|
if (!target) return installLauncher(undefined, out);
|
|
776
785
|
const reach = isOnPath(target) ? ["Afterwards `pi-loops` starts a session from any directory."] : [`${target} is not on your PATH, so \`pi-loops\` will not be found until it is.`];
|
|
777
|
-
const ok = await confirm("Put `pi-loops` on your PATH?", [`This writes a launcher into ${target}.`, "", "It is a
|
|
786
|
+
const ok = await confirm("Put `pi-loops` on your PATH?", [`This writes a launcher into ${target}.`, "", "It is a small shell script that runs this package with this node.", ...reach].join("\n"));
|
|
778
787
|
if (!ok) return undefined;
|
|
779
788
|
// The absolute directory the question named, not the flag again: resolving a relative --dir a
|
|
780
789
|
// second time, after the dialog, would follow a working directory that may have moved meanwhile.
|
|
781
790
|
return installLauncher(target, out);
|
|
782
791
|
}
|
|
783
792
|
|
|
793
|
+
/**
|
|
794
|
+
* What a launcher says when no copy of this package is at any of the paths it knows. One line and
|
|
795
|
+
* an exit code, because the alternative a person actually saw was Node's `Cannot find module` stack
|
|
796
|
+
* trace, which names a file that is gone and nothing they can do about it. It is written into the
|
|
797
|
+
* script inside single quotes, so it must contain none.
|
|
798
|
+
*/
|
|
799
|
+
const LAUNCHER_LOST =
|
|
800
|
+
"pi-loops: the package this launcher was written for is not where it was — write the launcher again with `/pi-loops install-launcher` in pi, or `node <the directory pi installed it into>/src/cli-entry.mjs install-launcher`";
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* The other places this package can be, for a launcher whose recorded entry has gone: the two
|
|
804
|
+
* directories `pi install` puts it in, under the agent directory the launcher is written with. They
|
|
805
|
+
* are baked into the script as literal paths — the launcher looks, it does not work anything out.
|
|
806
|
+
*/
|
|
807
|
+
function piInstallEntries(agentDir: string): string[] {
|
|
808
|
+
const { name, repoPath } = manifest();
|
|
809
|
+
const entries: string[] = [];
|
|
810
|
+
// `<agent dir>/npm/node_modules/<name>` and `<agent dir>/git/<host>/<owner>/<repo>`: the same two
|
|
811
|
+
// layouts `upgradeSpec` reads backwards to work out which route this copy came down.
|
|
812
|
+
if (name) entries.push(path.join(agentDir, "npm", "node_modules", ...name.split("/"), "src", "cli-entry.mjs"));
|
|
813
|
+
if (repoPath) entries.push(path.join(agentDir, "git", ...repoPath.split("/"), "src", "cli-entry.mjs"));
|
|
814
|
+
return entries;
|
|
815
|
+
}
|
|
816
|
+
|
|
784
817
|
/**
|
|
785
818
|
* `pi install` puts this package under pi's managed directory rather than on your PATH, so the
|
|
786
819
|
* command that is supposed to start your sessions is reachable only by absolute path. This writes
|
|
787
|
-
* a
|
|
820
|
+
* a small launcher into a directory that is already on your PATH, which is the smallest thing
|
|
788
821
|
* that fixes it without asking you to publish or install anything else.
|
|
789
822
|
*/
|
|
790
823
|
export async function installLauncher(dir: string | undefined, out: (line: string) => void): Promise<number> {
|
|
@@ -797,9 +830,25 @@ export async function installLauncher(dir: string | undefined, out: (line: strin
|
|
|
797
830
|
if (dir && !isOnPath(target)) out(`note: ${target} is not on your PATH, so the command will not be found there yet`);
|
|
798
831
|
const entry = path.join(path.dirname(fileURLToPath(import.meta.url)), "cli-entry.mjs");
|
|
799
832
|
const file = path.join(target, "pi-loops");
|
|
800
|
-
// A launcher rather than a symlink: it
|
|
801
|
-
//
|
|
802
|
-
|
|
833
|
+
// A launcher rather than a symlink: it names the node that is running now, which is the one known
|
|
834
|
+
// to be new enough for this code. What it survives is this package being installed again by the
|
|
835
|
+
// other route — `pi install npm:` over a `git:` copy moves it, and a launcher that knew only where
|
|
836
|
+
// it used to be died in Node's `Cannot find module`. So the entry it recorded is the first place
|
|
837
|
+
// it looks and not the only one, and the recorded node is a path it checks rather than trusts.
|
|
838
|
+
const candidates = [entry, ...piInstallEntries(getAgentDir())].map((e) => JSON.stringify(e)).join(" ");
|
|
839
|
+
const script = [
|
|
840
|
+
"#!/bin/sh",
|
|
841
|
+
"# pi-loops launcher, written by `pi-loops install-launcher`.",
|
|
842
|
+
`node=${JSON.stringify(process.execPath)}`,
|
|
843
|
+
'[ -x "$node" ] || node=$(command -v node) || node=node',
|
|
844
|
+
`for entry in ${candidates}; do`,
|
|
845
|
+
'\t[ -f "$entry" ] || continue',
|
|
846
|
+
'\texec "$node" "$entry" "$@"',
|
|
847
|
+
"done",
|
|
848
|
+
`echo '${LAUNCHER_LOST}' >&2`,
|
|
849
|
+
"exit 1",
|
|
850
|
+
"",
|
|
851
|
+
].join("\n");
|
|
803
852
|
try {
|
|
804
853
|
fs.mkdirSync(target, { recursive: true });
|
|
805
854
|
fs.writeFileSync(file, script, { mode: 0o755 });
|