@maheidem/pi-loop 0.7.0 → 0.7.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/index.ts +21 -4
- package/package.json +3 -1
- package/ui/version.ts +69 -0
- package/version.ts +24 -0
package/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "./state.ts";
|
|
33
33
|
import { buildLoopPanelSnapshot, formatCountdown } from "./ui/loop-panel.ts";
|
|
34
34
|
import { SettingsPanel, type PanelResult } from "./ui/settings-panel.ts";
|
|
35
|
+
import { loopVersion } from "./version.ts";
|
|
35
36
|
|
|
36
37
|
const TAG = "[loop]";
|
|
37
38
|
const TICK_CUSTOM_TYPE = "loop-tick";
|
|
@@ -83,6 +84,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
84
|
const state = rt.state;
|
|
84
85
|
return [
|
|
85
86
|
`${TAG} active`,
|
|
87
|
+
`version: v${loopVersion()}`,
|
|
86
88
|
`interval: ${formatInterval(state.intervalMs)}`,
|
|
87
89
|
`next tick: ${formatCountdown(rt.nextFireAt - Date.now())}`,
|
|
88
90
|
`ticks sent: ${state.tickCount}`,
|
|
@@ -194,7 +196,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
196
|
if (raw === undefined) return false;
|
|
195
197
|
const parsed = parseInterval(raw.trim());
|
|
196
198
|
if (Number.isNaN(parsed)) {
|
|
197
|
-
say(ctx,
|
|
199
|
+
say(ctx, `${TAG} Interval must look like 30s, 10m, 2h, or 1d.`, "error");
|
|
198
200
|
proposed = raw;
|
|
199
201
|
continue;
|
|
200
202
|
}
|
|
@@ -205,7 +207,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
207
|
if (editedPrompt === undefined) return false;
|
|
206
208
|
const prompt = editedPrompt.trim();
|
|
207
209
|
if (!prompt) {
|
|
208
|
-
say(ctx,
|
|
210
|
+
say(ctx, `${TAG} Recurring prompt cannot be empty.`, "error");
|
|
209
211
|
return false;
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -308,9 +310,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
308
310
|
});
|
|
309
311
|
|
|
310
312
|
pi.registerCommand("loop", {
|
|
311
|
-
|
|
313
|
+
description:
|
|
314
|
+
"Open recurring-loop controls or schedule with /loop <interval> <prompt...>. Verbs: status (scriptable status), edit (TUI-only interval/prompt editor), stop.",
|
|
312
315
|
getArgumentCompletions: (prefix) => {
|
|
313
|
-
const values = ["status", "stop", "30s ", "5m ", "10m ", "1h "];
|
|
316
|
+
const values = ["status", "edit", "stop", "30s ", "5m ", "10m ", "1h "];
|
|
314
317
|
const matches = values.filter((value) => value.startsWith(prefix));
|
|
315
318
|
return matches.length ? matches.map((value) => ({ value, label: value })) : null;
|
|
316
319
|
},
|
|
@@ -328,6 +331,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
328
331
|
stopLoop(ctx, "user requested");
|
|
329
332
|
return;
|
|
330
333
|
}
|
|
334
|
+
if (a === "edit") {
|
|
335
|
+
if (ctx.mode !== "tui") {
|
|
336
|
+
say(ctx, `${TAG} /loop edit opens the TUI interval/prompt editor and is panel/TUI-only — in print mode re-arm instead: /loop stop then /loop <interval> <prompt...>. /loop status shows the current schedule.`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
await editSchedule(ctx, true);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
331
342
|
const parsed = parseArgs(a);
|
|
332
343
|
if (typeof parsed === "string") {
|
|
333
344
|
say(ctx, `${TAG} ${parsed}`, "error");
|
|
@@ -357,6 +368,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
357
368
|
interval: Type.Optional(Type.String({ description: "e.g. 30s, 5m, 2h, 1d. Default 10m, min 15s." })),
|
|
358
369
|
prompt: Type.Optional(Type.String({ description: "The prompt to fire each tick, verbatim. Required for action=start." })),
|
|
359
370
|
}),
|
|
371
|
+
promptGuidelines: [
|
|
372
|
+
"Use this tool for unattended recurring work at a fixed cadence — the prompt re-fires every interval without anyone typing it.",
|
|
373
|
+
"The prompt fires verbatim on each tick, prefixed with a one-line provenance header ([loop tick #N · scheduled by /loop, not typed by the user]).",
|
|
374
|
+
"The interval is fixed at arm time; to change it, stop (action: stop) and re-arm with the new interval.",
|
|
375
|
+
"Stop the loop once the goal is reached — leftover loops keep firing until stopped or the session ends.",
|
|
376
|
+
],
|
|
360
377
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
361
378
|
if (params.action === "start") {
|
|
362
379
|
const parsed = parseArgs(`${params.interval ?? ""} ${params.prompt ?? ""}`.trim());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maheidem/pi-loop",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension: /loop \u2014 a generic recurring-prompt loop (Claude Code /loop style) delivered as in-session steer custom messages. Skills (e.g. shepherd) are consumers of /loop, not part of it.",
|
|
6
6
|
"keywords": [
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"index.ts",
|
|
15
15
|
"state.ts",
|
|
16
|
+
"version.ts",
|
|
16
17
|
"ui/settings-panel.ts",
|
|
17
18
|
"ui/loop-panel.ts",
|
|
19
|
+
"ui/version.ts",
|
|
18
20
|
"skills/",
|
|
19
21
|
"README.md",
|
|
20
22
|
"LICENSE"
|
package/ui/version.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui/version.ts — canonical loaded-version provenance (S2, pi-panel-kit
|
|
3
|
+
* stage 1).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* A running Pi session keeps whatever extension code it loaded at startup;
|
|
7
|
+
* newer installs (npm store or by-path) only apply after `/reload`. A stale
|
|
8
|
+
* in-process copy is indistinguishable from a fresh one unless every status
|
|
9
|
+
* line, panel summary, and tool card carries the version actually EXECUTING.
|
|
10
|
+
* This helper reads that version from the extension's own `package.json` —
|
|
11
|
+
* never hard-coded, never baked at build time.
|
|
12
|
+
*
|
|
13
|
+
* Vendored byte-identically into consumers (guard: `node
|
|
14
|
+
* skills/pi-extension-builder/scripts/check-vendored.mjs`); each extension
|
|
15
|
+
* keeps a thin wrapper (`version.ts` at its package root) that exports its
|
|
16
|
+
* original function name and passes the `package.json` path resolved from
|
|
17
|
+
* THE WRAPPER'S module URL — a vendored copy sits in `ui/` and cannot find
|
|
18
|
+
* the package root by itself, which is exactly why the path is a parameter.
|
|
19
|
+
*
|
|
20
|
+
* Pi-free by design: node built-ins only, no imports from sibling modules.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
|
|
25
|
+
export interface ExtensionVersionOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Absolute path to the extension's `package.json`. Resolve it from the
|
|
28
|
+
* wrapper's own module URL (`path.join(dirname(fileURLToPath(import.meta.url)), "package.json")`)
|
|
29
|
+
* so the string is anchored to the code actually executing, not the cwd.
|
|
30
|
+
*/
|
|
31
|
+
packageJsonPath: string;
|
|
32
|
+
/**
|
|
33
|
+
* Short extension name for composed headers. When set, the result is
|
|
34
|
+
* `label vX.Y.Z` (the `[delegate v0.3.2 …]` pattern used in result
|
|
35
|
+
* headers). Default: no label, bare version (status lines and panel
|
|
36
|
+
* summaries render their own prefixes).
|
|
37
|
+
*/
|
|
38
|
+
label?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Verbatim suffix appended after the version, e.g.
|
|
41
|
+
* `" (loaded at session start; /reload picks up newer installs)"`.
|
|
42
|
+
* Default: none. Call sites that interpolate the suffix themselves keep
|
|
43
|
+
* passing nothing — identical output either way.
|
|
44
|
+
*/
|
|
45
|
+
suffix?: string;
|
|
46
|
+
/** Returned when `package.json` is unreadable/unparsable or lacks `version`. Default `"unknown"`. */
|
|
47
|
+
fallback?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Raw versions keyed by resolved package.json path; read once per process. */
|
|
51
|
+
const cache = new Map<string, string>();
|
|
52
|
+
|
|
53
|
+
/** The `version` field of the extension's package.json (never a hardcoded string). */
|
|
54
|
+
export function extensionVersion(options: ExtensionVersionOptions): string {
|
|
55
|
+
const fallback = options.fallback ?? "unknown";
|
|
56
|
+
let version = cache.get(options.packageJsonPath);
|
|
57
|
+
if (version === undefined) {
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(options.packageJsonPath, "utf8")) as {
|
|
60
|
+
version?: string;
|
|
61
|
+
};
|
|
62
|
+
version = pkg.version ?? fallback;
|
|
63
|
+
} catch {
|
|
64
|
+
version = fallback;
|
|
65
|
+
}
|
|
66
|
+
cache.set(options.packageJsonPath, version);
|
|
67
|
+
}
|
|
68
|
+
return `${options.label ? `${options.label} v` : ""}${version}${options.suffix ?? ""}`;
|
|
69
|
+
}
|
package/version.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loop — loaded-version provenance.
|
|
3
|
+
*
|
|
4
|
+
* A running Pi session keeps whatever extension code it loaded at startup;
|
|
5
|
+
* newer npm-store installs only apply after /reload. The /loop status
|
|
6
|
+
* output therefore carries the version actually executing, so a stale copy
|
|
7
|
+
* in this process is self-evident instead of mysterious.
|
|
8
|
+
*
|
|
9
|
+
* Thin wrapper over the canonical helper vendored at `ui/version.ts`
|
|
10
|
+
* (kit source: `skills/pi-extension-builder/assets/control-panel/…`); the
|
|
11
|
+
* package.json path is resolved from THIS module's URL.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
import { extensionVersion } from "./ui/version.ts";
|
|
18
|
+
|
|
19
|
+
export function loopVersion(): string {
|
|
20
|
+
return extensionVersion({
|
|
21
|
+
packageJsonPath: path.join(path.dirname(fileURLToPath(import.meta.url)), "package.json"),
|
|
22
|
+
suffix: " (loaded at session start; /reload picks up newer installs)",
|
|
23
|
+
});
|
|
24
|
+
}
|