@alphacoder-v0/pi-loops 0.24.0 → 0.24.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 +53 -0
- package/README.md +35 -27
- package/README.zh-CN.md +4 -3
- package/docs/cli.md +96 -24
- package/docs/configuration.md +1 -1
- package/docs/design.md +9 -5
- package/docs/troubleshooting.md +4 -2
- package/docs/web-ui-parity.md +11 -0
- package/package.json +1 -1
- package/src/browser-open.ts +169 -0
- package/src/cli.ts +275 -79
- package/src/pi-loops.ts +74 -28
- package/src/ui-prefs.ts +6 -2
- package/src/web.mjs +2 -1
package/src/pi-loops.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { ProjectTrustStore, SessionManager, VERSION as PI_VERSION, getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { ProjectTrustStore, SessionManager, VERSION as PI_VERSION, copyToClipboard, getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { ARCHIVE_EXT, type ActivateMode, applyImport, automationOf, defaultExportPath, exportSession, importSession, offerToEnable, parseActivateMode } from "./archive.ts";
|
|
21
21
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
22
22
|
import { Type } from "typebox";
|
|
@@ -38,7 +38,8 @@ import { cardLines, cardOrigin, claimPrompt, dismissOutcome, dismissTail, inboxC
|
|
|
38
38
|
import { shouldEmitSnapshot, snapshotFingerprint } from "./snapshot.ts";
|
|
39
39
|
import { FRESH_RUNS_READ, burstLine, freshRuns, panelRuns, placeRunCards, runCardDelivery, runCardLines, runCardsHere, runLine } from "./run-card.ts";
|
|
40
40
|
import { type ShareMessage, renderShare, shareSummary, shareableMessages } from "./share.ts";
|
|
41
|
-
import { installLauncherWithConfirm, installedByPi, launcherSource, launcherState, launcherTarget, refreshLauncher, stopAndWait, stopService } from "./cli.ts";
|
|
41
|
+
import { UsageError, addLauncher, installLauncherWithConfirm, installedByPi, launcherAction, launcherSource, launcherState, launcherTarget, openPage, pageLaunchHost, refreshLauncher, slashDir, stopAndWait, stopService, uninstallLauncher, webPageRequest } from "./cli.ts";
|
|
42
|
+
import { copyText, isRemoteTty } from "./browser-open.ts";
|
|
42
43
|
import { execFile } from "node:child_process";
|
|
43
44
|
import { createHash } from "node:crypto";
|
|
44
45
|
import { computeDue, computeNext, formatLocal, formatSchedule, localOffset, parseSchedule, stamp } from "./schedule.ts";
|
|
@@ -2464,7 +2465,7 @@ export default function piLoops(pi: ExtensionAPI) {
|
|
|
2464
2465
|
const ARCHIVE_WARNING = `warning: ${ARCHIVE_EXT} archives include transcript and tool history. They do not include separate auth stores, provider credentials, OAuth tokens, MCP config, or the inbox.`;
|
|
2465
2466
|
|
|
2466
2467
|
pi.registerCommand("pi-loops", {
|
|
2467
|
-
description: "About this extension; start | stop | restart | status the part that runs loops while no pi is open; install-launcher puts the `pi-loops` command on your PATH",
|
|
2468
|
+
description: "About this extension; start | stop | restart | status the part that runs loops while no pi is open; web opens this conversation in the browser page; install-launcher puts the `pi-loops` command on your PATH, uninstall-launcher removes it",
|
|
2468
2469
|
handler: async (args, ctx) => {
|
|
2469
2470
|
lastCtx = ctx;
|
|
2470
2471
|
const sub = args.trim().split(/\s+/)[0] ?? "";
|
|
@@ -2491,6 +2492,37 @@ export default function piLoops(pi: ExtensionAPI) {
|
|
|
2491
2492
|
await drainRequests({ tick: true });
|
|
2492
2493
|
return;
|
|
2493
2494
|
}
|
|
2495
|
+
if (sub === "web") {
|
|
2496
|
+
// The page on this conversation with no `pi-loops` on PATH: the same page-open and opener
|
|
2497
|
+
// the command line uses. Shown as a notice, not written into the conversation: the link
|
|
2498
|
+
// carries a one-use key.
|
|
2499
|
+
const lines: string[] = [];
|
|
2500
|
+
const host = { ...pageLaunchHost(dir), packageDir: PACKAGE_DIR, piPackage: piPackageDir(), startedBy: `the pi window pid ${process.pid}`, myPiVersion: PI_VERSION };
|
|
2501
|
+
const req = webPageRequest({ cwd: session.cwd, sessionId: binding.here().sessionId ?? session.sessionId, sessionFile: ctx.sessionManager.getSessionFile() });
|
|
2502
|
+
// The link is shown and copied before the browser is tried: the wait for a browser to spend
|
|
2503
|
+
// the key can take seconds, and the person may paste the link sooner.
|
|
2504
|
+
const onLink = async (link: string) => {
|
|
2505
|
+
// Said only when a clipboard command took the link. pi's copy can still get there through
|
|
2506
|
+
// OSC 52, which no terminal confirms, so that try says nothing either way.
|
|
2507
|
+
if (await copyText(link)) lines.push(" copied to the clipboard");
|
|
2508
|
+
else await copyToClipboard(link).catch((err: any) => log.info(`/pi-loops web: copy: ${err?.message ?? err}`));
|
|
2509
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
2510
|
+
};
|
|
2511
|
+
const opened = await openPage(dir, req, { key: true, browser: isRemoteTty() ? "remote" : "yes", host }, (l) => lines.push(l), undefined, onLink);
|
|
2512
|
+
// pi shows one notice at a time, so the last one repeats the link with the outcome.
|
|
2513
|
+
ctx.ui.notify(lines.join("\n"), opened.code === 0 ? "info" : "warning");
|
|
2514
|
+
refreshBadge();
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (sub === "install-launcher" || sub === "uninstall-launcher") {
|
|
2518
|
+
try {
|
|
2519
|
+
slashDir(tokenize(splitCommand(args).rest).map((t) => t.value));
|
|
2520
|
+
} catch (err: any) {
|
|
2521
|
+
if (!(err instanceof UsageError)) throw err;
|
|
2522
|
+
ctx.ui.notify(`${err.message}. usage: /pi-loops ${sub} [--dir <dir>]`, "warning");
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2494
2526
|
if (sub === "install-launcher") {
|
|
2495
2527
|
// The command line's own `install-launcher` cannot be reached until it has run once —
|
|
2496
2528
|
// `pi install` does not put the package on your PATH. You are already inside pi with
|
|
@@ -2505,12 +2537,20 @@ export default function piLoops(pi: ExtensionAPI) {
|
|
|
2505
2537
|
show(ctx, code === 0 ? "installed the pi-loops launcher" : "could not install the launcher", lines.map((l) => ` ${l.trim()}`));
|
|
2506
2538
|
return;
|
|
2507
2539
|
}
|
|
2540
|
+
if (sub === "uninstall-launcher") {
|
|
2541
|
+
const lines: string[] = [];
|
|
2542
|
+
const code = uninstallLauncher(slashDir(tokenize(splitCommand(args).rest).map((t) => t.value)), (l) => lines.push(l));
|
|
2543
|
+
show(ctx, code === 0 ? "the pi-loops launcher is gone" : "left the launcher alone", lines.map((l) => ` ${l.trim()}`));
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2508
2546
|
show(ctx, `pi-loops ${PI_LOOPS_VERSION}`, [
|
|
2509
2547
|
` loops directory: ${homeRel(dir)}`,
|
|
2510
2548
|
` package: ${homeRel(PACKAGE_DIR)}`,
|
|
2511
2549
|
"",
|
|
2512
2550
|
" /pi-loops start | stop | restart | status",
|
|
2551
|
+
" /pi-loops web open this conversation in the browser page",
|
|
2513
2552
|
" /pi-loops install-launcher [--dir <dir>] put the `pi-loops` command on your PATH",
|
|
2553
|
+
" /pi-loops uninstall-launcher [--dir <dir>] remove it, so no session writes it again",
|
|
2514
2554
|
" /cron · /triggers · /inbox · /goal",
|
|
2515
2555
|
]);
|
|
2516
2556
|
},
|
|
@@ -3160,7 +3200,7 @@ export default function piLoops(pi: ExtensionAPI) {
|
|
|
3160
3200
|
/* -------------------------------------------------------- lifecycle */
|
|
3161
3201
|
|
|
3162
3202
|
/**
|
|
3163
|
-
* The `pi-loops` command,
|
|
3203
|
+
* The `pi-loops` command, put on the PATH and kept working there.
|
|
3164
3204
|
*
|
|
3165
3205
|
* `pi install` puts this package under pi's directory and nothing on the PATH, so until 0.22.1
|
|
3166
3206
|
* the first thing a new user had to do was type a bare `node …/cli-entry.mjs install-launcher`
|
|
@@ -3169,40 +3209,47 @@ export default function piLoops(pi: ExtensionAPI) {
|
|
|
3169
3209
|
* it runs and where a launcher would go, so the step belongs here.
|
|
3170
3210
|
*
|
|
3171
3211
|
* This is the extension's only write outside the loops directory (AGENTS.md). Its limit is one
|
|
3172
|
-
* file, `pi-loops` in a directory already on the PATH
|
|
3173
|
-
*
|
|
3174
|
-
*
|
|
3175
|
-
*
|
|
3176
|
-
*
|
|
3177
|
-
*
|
|
3178
|
-
*
|
|
3212
|
+
* file, `pi-loops` in a directory already on the PATH. With nothing there, the first interactive
|
|
3213
|
+
* session of a copy `pi install` put in place writes it with no question and says so in one line,
|
|
3214
|
+
* as pi's own installer does for `pi` (#180); after that only a launcher of ours is rewritten. A
|
|
3215
|
+
* file that does not open with our header is somebody's own wrapper and is left alone; one naming another
|
|
3216
|
+
* copy of this package that is still on disk is that copy's to keep current, so an install and a
|
|
3217
|
+
* checkout both being opened never trade the file back and forth; `uninstall-launcher` is
|
|
3218
|
+
* remembered in `ui.json`, so no later session writes it again; a write that failed is
|
|
3219
|
+
* remembered with its directory, so it is not tried in every window; and a checkout never writes
|
|
3220
|
+
* a launcher that is not there at all — nobody ran `pi install`, so nothing is missing from their
|
|
3221
|
+
* PATH — though one written from a checkout is still kept working. The decision is
|
|
3222
|
+
* `launcherAction` in src/cli.ts.
|
|
3179
3223
|
*/
|
|
3180
3224
|
async function keepLauncher(ctx: ExtensionContext): Promise<void> {
|
|
3181
3225
|
const { entryDir, node, agentDir } = launcherSource();
|
|
3182
3226
|
const target = launcherTarget(undefined);
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
if (
|
|
3227
|
+
const prefs = readUiPrefs(dir);
|
|
3228
|
+
const state = target ? launcherState(entryDir, node, agentDir, target).state : undefined;
|
|
3229
|
+
const action = launcherAction({ target, state, hasUI: ctx.hasUI, declined: prefs.launcher === "declined", failedIn: prefs.launcher === "failed" ? prefs.launcherFailedIn : undefined, installedByPi: state === "absent" && installedByPi() });
|
|
3230
|
+
if (!target || action === "none") return;
|
|
3231
|
+
if (action === "refresh") {
|
|
3188
3232
|
const done = refreshLauncher(entryDir, node, agentDir, target);
|
|
3189
3233
|
if (!done) return;
|
|
3190
3234
|
log.info(`launcher: ${done.message}`);
|
|
3191
|
-
//
|
|
3192
|
-
//
|
|
3235
|
+
// They have a launcher of ours, and keeping that one working is what it is for. The line is
|
|
3236
|
+
// so that a file changing under them is never a silent change.
|
|
3193
3237
|
if (ctx.hasUI) ctx.ui.notify(`[pi-loops] ${done.message}`, done.ok ? "info" : "warning");
|
|
3194
3238
|
return;
|
|
3195
3239
|
}
|
|
3196
|
-
|
|
3197
|
-
if (
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3240
|
+
const added = addLauncher(entryDir, node, agentDir, target);
|
|
3241
|
+
if (added.kind === "none") return;
|
|
3242
|
+
if (added.kind === "added") {
|
|
3243
|
+
// A failed write into another directory is not news any more.
|
|
3244
|
+
clearUiPref(dir, "launcher");
|
|
3245
|
+
clearUiPref(dir, "launcherFailedIn");
|
|
3246
|
+
log.info(`launcher: wrote ${added.file}`);
|
|
3247
|
+
ctx.ui.notify(`[pi-loops] added pi-loops to ${homeRel(target)}; /pi-loops uninstall-launcher removes it`, "info");
|
|
3203
3248
|
return;
|
|
3204
3249
|
}
|
|
3205
|
-
|
|
3250
|
+
writeUiPref(dir, "launcher", "failed");
|
|
3251
|
+
writeUiPref(dir, "launcherFailedIn", target);
|
|
3252
|
+
ctx.ui.notify(`[pi-loops] ${added.message}; /pi-loops install-launcher --dir <dir> writes it elsewhere`, "warning");
|
|
3206
3253
|
}
|
|
3207
3254
|
|
|
3208
3255
|
/**
|
|
@@ -3277,8 +3324,7 @@ export default function piLoops(pi: ExtensionAPI) {
|
|
|
3277
3324
|
loadMcpConfig(windowTrusted(getAgentDir(), ctx.cwd, ctx.isProjectTrusted(), APPROVED));
|
|
3278
3325
|
for (const d of mcpDiagnostics) if (ctx.hasUI) ctx.ui.notify(`[pi-loops] ${d}`, "warning");
|
|
3279
3326
|
await startMcpSources(); // tools for every process; pushes are consumed by interactive processes only
|
|
3280
|
-
//
|
|
3281
|
-
// has walked away from it.
|
|
3327
|
+
// A launcher that cannot be written or kept is logged, and never stops the session starting.
|
|
3282
3328
|
void keepLauncher(ctx).catch((err: any) => log.info(`launcher: ${err?.message ?? err}`));
|
|
3283
3329
|
// tui and rpc processes stay alive and run the clock; `PI_LOOPS_HOST=1` starts it in a `pi -p`
|
|
3284
3330
|
// run too, which queues its conversation's due prompts but hands none to pi (the window
|
package/src/ui-prefs.ts
CHANGED
|
@@ -25,10 +25,13 @@ export interface UiPrefs {
|
|
|
25
25
|
model?: string;
|
|
26
26
|
thinking?: string;
|
|
27
27
|
/**
|
|
28
|
-
* `"declined"` once someone has
|
|
29
|
-
*
|
|
28
|
+
* `"declined"` once someone has removed the launcher with `uninstall-launcher`, so no session
|
|
29
|
+
* start writes it again; `"failed"` once a session start could not write it. `/pi-loops
|
|
30
|
+
* install-launcher` clears it when it writes one.
|
|
30
31
|
*/
|
|
31
32
|
launcher?: string;
|
|
33
|
+
/** The directory a session start could not write the launcher into: tried again only elsewhere. */
|
|
34
|
+
launcherFailedIn?: string;
|
|
32
35
|
/** `"stopped"` once a person stopped pi-loops by hand: no window starts it again until `pi-loops start`. */
|
|
33
36
|
host?: string;
|
|
34
37
|
/** `"shown"` once a session start has said that closed conversations' plain jobs now run in the host. */
|
|
@@ -65,6 +68,7 @@ export function readUiPrefs(dir: string): UiPrefs {
|
|
|
65
68
|
if (typeof doc.model === "string") prefs.model = doc.model;
|
|
66
69
|
if (typeof doc.thinking === "string") prefs.thinking = doc.thinking;
|
|
67
70
|
if (typeof doc.launcher === "string") prefs.launcher = doc.launcher;
|
|
71
|
+
if (typeof doc.launcherFailedIn === "string") prefs.launcherFailedIn = doc.launcherFailedIn;
|
|
68
72
|
if (typeof doc.host === "string") prefs.host = doc.host;
|
|
69
73
|
if (typeof doc.heldJobsNotice === "string") prefs.heldJobsNotice = doc.heldJobsNotice;
|
|
70
74
|
if (typeof doc.toolsCapNotice === "string") prefs.toolsCapNotice = doc.toolsCapNotice;
|
package/src/web.mjs
CHANGED
|
@@ -11,7 +11,8 @@ export const DOOR = `<!doctype html><meta charset="utf-8"><title>pi-loops</title
|
|
|
11
11
|
<meta name="color-scheme" content="light dark">
|
|
12
12
|
<body style="font:15px/1.6 system-ui,sans-serif;max-width:34rem;margin:12vh auto;padding:0 1.5rem;background:Canvas;color:CanvasText">
|
|
13
13
|
<h2 style="font-weight:600">This browser has not been here before.</h2>
|
|
14
|
-
<p>Run <code>pi-loops</code> in a terminal on this machine
|
|
14
|
+
<p>Run <code>pi-loops open</code> in a terminal on this machine for a link that signs this browser in.</p>
|
|
15
|
+
<p>Pairing another device? <code>pi-loops pair</code> shows a code.</p>
|
|
15
16
|
<form method="get" style="display:flex;gap:.5rem">
|
|
16
17
|
<input name="pair" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" maxlength="6"
|
|
17
18
|
placeholder="000000" style="font:inherit;letter-spacing:.3em;padding:.55rem .7rem;flex:1;min-width:0;border:1px solid #8886;border-radius:6px;background:transparent;color:inherit">
|