@mjasnikovs/pi-task 0.13.6 → 0.13.8
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/dist/config/register.js +1 -1
- package/dist/remote/push.d.ts +12 -3
- package/dist/remote/push.js +63 -9
- package/dist/remote/register.js +7 -3
- package/dist/remote/server.d.ts +4 -2
- package/dist/remote/server.js +7 -3
- package/dist/remote/tailscale.d.ts +8 -2
- package/dist/remote/tailscale.js +13 -6
- package/dist/remote/ui-script.d.ts +3 -0
- package/dist/remote/ui-script.js +804 -0
- package/dist/remote/ui-styles.d.ts +1 -0
- package/dist/remote/ui-styles.js +202 -0
- package/dist/remote/ui.js +4 -1000
- package/dist/shared/child-process.d.ts +27 -0
- package/dist/shared/child-process.js +151 -139
- package/dist/task/auto-orchestrator.js +43 -13
- package/dist/task/auto-prompts.d.ts +4 -3
- package/dist/task/auto-prompts.js +9 -6
- package/dist/task/child-runner.js +1 -1
- package/dist/task/context-usage.d.ts +16 -0
- package/dist/task/context-usage.js +22 -0
- package/dist/task/external-context.d.ts +27 -0
- package/dist/task/external-context.js +93 -0
- package/dist/task/failure-classifier.js +1 -1
- package/dist/task/orchestrator.js +7 -13
- package/dist/task/parsers.d.ts +4 -15
- package/dist/task/parsers.js +48 -87
- package/dist/task/phases.d.ts +5 -7
- package/dist/task/phases.js +29 -84
- package/dist/task/prompts.d.ts +1 -0
- package/dist/task/prompts.js +9 -0
- package/dist/task/spec-validation.d.ts +23 -0
- package/dist/task/spec-validation.js +90 -0
- package/dist/task/widget.d.ts +1 -1
- package/dist/task/widget.js +1 -1
- package/dist/workers/html-clean.js +7 -4
- package/dist/workers/pi-worker-docs.js +69 -58
- package/dist/workers/pi-worker-fetch.js +25 -21
- package/dist/workers/pi-worker-search.js +7 -13
- package/dist/workers/pi-worker.js +8 -14
- package/dist/workers/shared.d.ts +40 -0
- package/dist/workers/shared.js +31 -0
- package/package.json +1 -1
package/dist/config/register.js
CHANGED
|
@@ -11,7 +11,7 @@ const ITEMS = [
|
|
|
11
11
|
{
|
|
12
12
|
id: 'autoCommit',
|
|
13
13
|
label: 'auto-commit',
|
|
14
|
-
description: 'git commit
|
|
14
|
+
description: 'git commit around each /task-auto sub-task (checkpoint before, snapshot after)'
|
|
15
15
|
}
|
|
16
16
|
];
|
|
17
17
|
function makeTheme(theme) {
|
package/dist/remote/push.d.ts
CHANGED
|
@@ -11,10 +11,16 @@ export interface VapidKeys {
|
|
|
11
11
|
publicKey: string;
|
|
12
12
|
privateKey: string;
|
|
13
13
|
}
|
|
14
|
-
/** Where the VAPID keypair is persisted
|
|
15
|
-
*
|
|
16
|
-
* losing these keys invalidates every existing browser subscription. */
|
|
14
|
+
/** Where the VAPID keypair is persisted — losing these keys invalidates every
|
|
15
|
+
* existing browser subscription. */
|
|
17
16
|
export declare function vapidStorePath(): string;
|
|
17
|
+
/** Where browser push subscriptions are mirrored to disk (next to vapid.json).
|
|
18
|
+
* Without this, a server restart — e.g. after a rebuild — silently drops every
|
|
19
|
+
* device: the in-memory store is empty, and a backgrounded/suspended PWA won't
|
|
20
|
+
* re-register until the user next foregrounds it, which is exactly when the push
|
|
21
|
+
* is no longer useful. The in-memory store stays authoritative within a process;
|
|
22
|
+
* this is its durable mirror. */
|
|
23
|
+
export declare function subscriptionsStorePath(): string;
|
|
18
24
|
/** Diagnostic log file. Defaults to /tmp for easy tailing; override with
|
|
19
25
|
* PI_REMOTE_PUSH_LOG. (The VAPID key stays in its durable XDG location.) */
|
|
20
26
|
export declare function pushLogPath(): string;
|
|
@@ -30,6 +36,9 @@ export declare function logPush(line: string): void;
|
|
|
30
36
|
/** Load the persisted VAPID keypair, generating and saving one on first use or
|
|
31
37
|
* if the stored file is missing/corrupt. Stable across restarts. */
|
|
32
38
|
export declare function loadOrCreateVapidKeys(file?: string): VapidKeys;
|
|
39
|
+
/** Load persisted subscriptions into the in-memory store. Best-effort: a missing
|
|
40
|
+
* or corrupt file just leaves the store as-is. Returns the resulting count. */
|
|
41
|
+
export declare function loadSubscriptions(file?: string): number;
|
|
33
42
|
export declare function addSubscription(sub: PushSubscriptionJSON): void;
|
|
34
43
|
export declare function removeSubscription(endpoint: string): void;
|
|
35
44
|
export declare function getSubscriptions(): PushSubscriptionJSON[];
|
package/dist/remote/push.js
CHANGED
|
@@ -2,12 +2,25 @@ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import webpush from 'web-push';
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
/** Resolve the XDG data-home base. Under data-home (not cache) so the files it
|
|
6
|
+
* roots survive cache clears. Follows the repo's XDG convention; see
|
|
7
|
+
* workers/docs-core.ts. */
|
|
8
|
+
function dataHome() {
|
|
9
|
+
return process.env.XDG_DATA_HOME?.trim() || path.join(os.homedir(), '.local', 'share');
|
|
10
|
+
}
|
|
11
|
+
/** Where the VAPID keypair is persisted — losing these keys invalidates every
|
|
12
|
+
* existing browser subscription. */
|
|
8
13
|
export function vapidStorePath() {
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
return path.join(dataHome(), 'pi-task', 'vapid.json');
|
|
15
|
+
}
|
|
16
|
+
/** Where browser push subscriptions are mirrored to disk (next to vapid.json).
|
|
17
|
+
* Without this, a server restart — e.g. after a rebuild — silently drops every
|
|
18
|
+
* device: the in-memory store is empty, and a backgrounded/suspended PWA won't
|
|
19
|
+
* re-register until the user next foregrounds it, which is exactly when the push
|
|
20
|
+
* is no longer useful. The in-memory store stays authoritative within a process;
|
|
21
|
+
* this is its durable mirror. */
|
|
22
|
+
export function subscriptionsStorePath() {
|
|
23
|
+
return path.join(dataHome(), 'pi-task', 'subscriptions.json');
|
|
11
24
|
}
|
|
12
25
|
/** Diagnostic log file. Defaults to /tmp for easy tailing; override with
|
|
13
26
|
* PI_REMOTE_PUSH_LOG. (The VAPID key stays in its durable XDG location.) */
|
|
@@ -69,26 +82,67 @@ export function loadOrCreateVapidKeys(file = vapidStorePath()) {
|
|
|
69
82
|
return keys;
|
|
70
83
|
}
|
|
71
84
|
// In-memory subscription store, keyed by endpoint. Persisted on globalThis so it
|
|
72
|
-
// survives jiti re-evaluation on session switches (same pattern as broadcast.ts)
|
|
73
|
-
//
|
|
74
|
-
//
|
|
85
|
+
// survives jiti re-evaluation on session switches (same pattern as broadcast.ts),
|
|
86
|
+
// and mirrored to disk (subscriptionsStorePath) so it also survives a full
|
|
87
|
+
// process restart. A re-subscribe on page load can't be relied on for that: the
|
|
88
|
+
// devices that need server push are backgrounded/suspended and won't reload.
|
|
75
89
|
const g = globalThis;
|
|
90
|
+
const freshStore = !g.__piRemoteSubs;
|
|
76
91
|
if (!g.__piRemoteSubs)
|
|
77
92
|
g.__piRemoteSubs = new Map();
|
|
78
93
|
const subs = g.__piRemoteSubs;
|
|
94
|
+
// Hydrate once per process: a restarted server reloads the devices it knew so it
|
|
95
|
+
// can keep reaching them without waiting for each to re-register.
|
|
96
|
+
if (freshStore)
|
|
97
|
+
loadSubscriptions();
|
|
98
|
+
function isSubscription(x) {
|
|
99
|
+
return (typeof x === 'object'
|
|
100
|
+
&& x !== null
|
|
101
|
+
&& typeof x.endpoint === 'string');
|
|
102
|
+
}
|
|
103
|
+
/** Load persisted subscriptions into the in-memory store. Best-effort: a missing
|
|
104
|
+
* or corrupt file just leaves the store as-is. Returns the resulting count. */
|
|
105
|
+
export function loadSubscriptions(file = subscriptionsStorePath()) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
108
|
+
if (Array.isArray(parsed)) {
|
|
109
|
+
for (const s of parsed)
|
|
110
|
+
if (isSubscription(s))
|
|
111
|
+
subs.set(s.endpoint, s);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// missing or corrupt — keep whatever is already in memory
|
|
116
|
+
}
|
|
117
|
+
return subs.size;
|
|
118
|
+
}
|
|
119
|
+
/** Mirror the current store to disk. Best-effort; never throws, so a failed write
|
|
120
|
+
* can't break a subscribe request or a push. */
|
|
121
|
+
function saveSubscriptions(file = subscriptionsStorePath()) {
|
|
122
|
+
try {
|
|
123
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
124
|
+
writeFileSync(file, JSON.stringify([...subs.values()]), 'utf8');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// persistence is best-effort; the in-memory store remains authoritative
|
|
128
|
+
}
|
|
129
|
+
}
|
|
79
130
|
export function addSubscription(sub) {
|
|
80
131
|
if (!sub.endpoint)
|
|
81
132
|
return;
|
|
82
133
|
subs.set(sub.endpoint, sub);
|
|
134
|
+
saveSubscriptions();
|
|
83
135
|
}
|
|
84
136
|
export function removeSubscription(endpoint) {
|
|
85
|
-
subs.delete(endpoint)
|
|
137
|
+
if (subs.delete(endpoint))
|
|
138
|
+
saveSubscriptions();
|
|
86
139
|
}
|
|
87
140
|
export function getSubscriptions() {
|
|
88
141
|
return [...subs.values()];
|
|
89
142
|
}
|
|
90
143
|
export function clearSubscriptions() {
|
|
91
144
|
subs.clear();
|
|
145
|
+
saveSubscriptions();
|
|
92
146
|
}
|
|
93
147
|
/** True when the push service says the subscription is permanently gone and
|
|
94
148
|
* should be dropped (404 Not Found, 410 Gone). */
|
package/dist/remote/register.js
CHANGED
|
@@ -5,7 +5,7 @@ import { reset, addUserTurn } from './session-state.js';
|
|
|
5
5
|
import { html } from './ui.js';
|
|
6
6
|
import { qrLines } from './qr.js';
|
|
7
7
|
import { startServer, formatAddresses } from './server.js';
|
|
8
|
-
import { ensureTailscaleServe, teardownTailscaleServe, planRemoteUrls } from './tailscale.js';
|
|
8
|
+
import { ensureTailscaleServe, teardownTailscaleServe, planRemoteUrls, hostFromResult } from './tailscale.js';
|
|
9
9
|
import { isAgentIdle } from './state.js';
|
|
10
10
|
const _g = globalThis;
|
|
11
11
|
if (!_g.__piRemote)
|
|
@@ -105,10 +105,14 @@ export function registerRemote(pi) {
|
|
|
105
105
|
const server = await ensureServer();
|
|
106
106
|
const httpPrimary = `http://${server.ip}:${server.port}`;
|
|
107
107
|
const result = S.serveResult ?? { state: 'unavailable' };
|
|
108
|
-
const plan = planRemoteUrls(httpPrimary, result);
|
|
108
|
+
const plan = planRemoteUrls(httpPrimary, result, server.port);
|
|
109
109
|
const primaryUrl = plan.primaryUrl;
|
|
110
110
|
const qr = await qrLines(primaryUrl);
|
|
111
|
-
const
|
|
111
|
+
const tsHost = hostFromResult(result);
|
|
112
|
+
const addrs = [
|
|
113
|
+
...plan.urlLines,
|
|
114
|
+
...formatAddresses(server.ips, server.port, tsHost)
|
|
115
|
+
];
|
|
112
116
|
const labelW = addrs.reduce((m, a) => Math.max(m, a.label.length), 0);
|
|
113
117
|
const addrLines = [
|
|
114
118
|
...addrs.map(a => (a.label ? `${a.label.padEnd(labelW)} ${a.url}` : a.url)),
|
package/dist/remote/server.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export interface ServerHandle {
|
|
|
24
24
|
type MessageCallback = (text: string) => void;
|
|
25
25
|
export declare function getLocalIPs(nets?: NodeJS.Dict<import("node:os").NetworkInterfaceInfo[]>): LocalIPs;
|
|
26
26
|
/** Build the labeled URL lines shown under the QR code. Both Tailscale and LAN
|
|
27
|
-
* when present; a single unlabeled primary URL when neither resolves.
|
|
28
|
-
|
|
27
|
+
* when present; a single unlabeled primary URL when neither resolves. The
|
|
28
|
+
* Tailscale line uses the MagicDNS host when known (resolves to the same node,
|
|
29
|
+
* but is what SSH and webpush certs need), falling back to the raw IP. */
|
|
30
|
+
export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: string): AddressLine[];
|
|
29
31
|
export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string): Promise<ServerHandle>;
|
|
30
32
|
export {};
|
package/dist/remote/server.js
CHANGED
|
@@ -33,10 +33,14 @@ export function getLocalIPs(nets = networkInterfaces()) {
|
|
|
33
33
|
return { tailscale, lan, primary: tailscale ?? lan ?? '127.0.0.1' };
|
|
34
34
|
}
|
|
35
35
|
/** Build the labeled URL lines shown under the QR code. Both Tailscale and LAN
|
|
36
|
-
* when present; a single unlabeled primary URL when neither resolves.
|
|
37
|
-
|
|
36
|
+
* when present; a single unlabeled primary URL when neither resolves. The
|
|
37
|
+
* Tailscale line uses the MagicDNS host when known (resolves to the same node,
|
|
38
|
+
* but is what SSH and webpush certs need), falling back to the raw IP. */
|
|
39
|
+
export function formatAddresses(ips, port, tsHost) {
|
|
38
40
|
const out = [];
|
|
39
|
-
if (
|
|
41
|
+
if (tsHost)
|
|
42
|
+
out.push({ label: 'Tailscale', url: `http://${tsHost}:${port}` });
|
|
43
|
+
else if (ips.tailscale)
|
|
40
44
|
out.push({ label: 'Tailscale', url: `http://${ips.tailscale}:${port}` });
|
|
41
45
|
if (ips.lan)
|
|
42
46
|
out.push({ label: 'LAN', url: `http://${ips.lan}:${port}` });
|
|
@@ -9,6 +9,7 @@ export interface Run {
|
|
|
9
9
|
export type ServeResult = {
|
|
10
10
|
state: 'served';
|
|
11
11
|
url: string;
|
|
12
|
+
host: string;
|
|
12
13
|
} | {
|
|
13
14
|
state: 'foreign-conflict';
|
|
14
15
|
host: string;
|
|
@@ -18,6 +19,8 @@ export type ServeResult = {
|
|
|
18
19
|
} | {
|
|
19
20
|
state: 'unavailable';
|
|
20
21
|
};
|
|
22
|
+
/** The MagicDNS host of a serve result, or undefined when the daemon had no name. */
|
|
23
|
+
export declare function hostFromResult(result: ServeResult): string | undefined;
|
|
21
24
|
export interface RemoteUrlPlan {
|
|
22
25
|
/** URL to encode in the QR and announce; the https one when serve is live. */
|
|
23
26
|
primaryUrl: string;
|
|
@@ -41,5 +44,8 @@ export declare function ensureTailscaleServe(port: number, run?: Run): Promise<S
|
|
|
41
44
|
* Best-effort; callers should additionally `.catch()` since it may run during
|
|
42
45
|
* shutdown. */
|
|
43
46
|
export declare function teardownTailscaleServe(port: number, run?: Run): Promise<void>;
|
|
44
|
-
/** Pure: pick the primary URL and any hint lines from a
|
|
45
|
-
|
|
47
|
+
/** Pure: pick the primary URL (what the QR encodes) and any hint lines from a
|
|
48
|
+
* serve result. Prefers the MagicDNS host over the raw IP whenever it's known —
|
|
49
|
+
* the https URL when serve is live, else http://<host>:<port> — so the QR and
|
|
50
|
+
* the announced URL carry the tailnet name (needed for SSH and webpush certs). */
|
|
51
|
+
export declare function planRemoteUrls(httpPrimary: string, result: ServeResult, port: number): RemoteUrlPlan;
|
package/dist/remote/tailscale.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
/** The MagicDNS host of a serve result, or undefined when the daemon had no name. */
|
|
3
|
+
export function hostFromResult(result) {
|
|
4
|
+
return result.state === 'unavailable' ? undefined : result.host;
|
|
5
|
+
}
|
|
2
6
|
const defaultRun = (cmd, args) => new Promise(resolve => {
|
|
3
7
|
execFile(cmd, args, { timeout: 5000 }, (err, stdout, stderr) => {
|
|
4
8
|
const exitCode = err && typeof err.code === 'number' ?
|
|
@@ -54,7 +58,7 @@ export async function ensureTailscaleServe(port, run = defaultRun) {
|
|
|
54
58
|
const serve = await run('tailscale', ['serve', 'status', '--json']);
|
|
55
59
|
const target = serve443Target(serve.stdout);
|
|
56
60
|
if (target === ours)
|
|
57
|
-
return { state: 'served', url: `https://${host}
|
|
61
|
+
return { state: 'served', url: `https://${host}`, host };
|
|
58
62
|
if (target !== undefined)
|
|
59
63
|
return { state: 'foreign-conflict', host };
|
|
60
64
|
// No :443 handler yet — check cert capability before trying to create one.
|
|
@@ -64,7 +68,7 @@ export async function ensureTailscaleServe(port, run = defaultRun) {
|
|
|
64
68
|
}
|
|
65
69
|
const set = await run('tailscale', ['serve', '--bg', '--https=443', ours]);
|
|
66
70
|
if (set.exitCode === 0)
|
|
67
|
-
return { state: 'served', url: `https://${host}
|
|
71
|
+
return { state: 'served', url: `https://${host}`, host };
|
|
68
72
|
return { state: 'certs-disabled', host };
|
|
69
73
|
}
|
|
70
74
|
/** Tear down the :443 serve handler ONLY if it currently points at our port.
|
|
@@ -76,8 +80,11 @@ export async function teardownTailscaleServe(port, run = defaultRun) {
|
|
|
76
80
|
await run('tailscale', ['serve', '--https=443', 'off']);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
/** Pure: pick the primary URL and any hint lines from a
|
|
80
|
-
|
|
83
|
+
/** Pure: pick the primary URL (what the QR encodes) and any hint lines from a
|
|
84
|
+
* serve result. Prefers the MagicDNS host over the raw IP whenever it's known —
|
|
85
|
+
* the https URL when serve is live, else http://<host>:<port> — so the QR and
|
|
86
|
+
* the announced URL carry the tailnet name (needed for SSH and webpush certs). */
|
|
87
|
+
export function planRemoteUrls(httpPrimary, result, port) {
|
|
81
88
|
switch (result.state) {
|
|
82
89
|
case 'served':
|
|
83
90
|
return {
|
|
@@ -87,7 +94,7 @@ export function planRemoteUrls(httpPrimary, result) {
|
|
|
87
94
|
};
|
|
88
95
|
case 'foreign-conflict':
|
|
89
96
|
return {
|
|
90
|
-
primaryUrl:
|
|
97
|
+
primaryUrl: `http://${result.host}:${port}`,
|
|
91
98
|
urlLines: [],
|
|
92
99
|
hintLines: [
|
|
93
100
|
'HTTPS: port 443 is already used by another tailscale serve config; not touching it.',
|
|
@@ -96,7 +103,7 @@ export function planRemoteUrls(httpPrimary, result) {
|
|
|
96
103
|
};
|
|
97
104
|
case 'certs-disabled':
|
|
98
105
|
return {
|
|
99
|
-
primaryUrl:
|
|
106
|
+
primaryUrl: `http://${result.host}:${port}`,
|
|
100
107
|
urlLines: [],
|
|
101
108
|
hintLines: [
|
|
102
109
|
'HTTPS (for phone notifications): enable HTTPS in the Tailscale admin console, then restart the remote.'
|