@mjasnikovs/pi-task 0.13.7 → 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/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/task/auto-orchestrator.js +40 -7
- package/dist/task/auto-prompts.d.ts +4 -3
- package/dist/task/auto-prompts.js +9 -6
- package/dist/task/parsers.d.ts +3 -0
- package/dist/task/parsers.js +31 -3
- package/dist/workers/html-clean.js +7 -4
- 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/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.'
|
|
@@ -55,8 +55,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
55
55
|
// clarify — sequential & adaptive: ask one question at a time, feeding every
|
|
56
56
|
// answer back into the next call so later questions react to earlier ones
|
|
57
57
|
// (e.g. a framework choice reshapes what gets asked). Each question is shown
|
|
58
|
-
//
|
|
59
|
-
//
|
|
58
|
+
// exactly like /task's grill dialog: a binary fork offers two options (A/B),
|
|
59
|
+
// otherwise the model's recommendation is shown as the input placeholder and
|
|
60
|
+
// in the title. Nothing is pre-filled into the editor — submitting an empty
|
|
61
|
+
// field is what accepts the recommendation (see the typed.length === 0 branch
|
|
62
|
+
// below); typing overrides it. We never auto-answer; the model emits NONE when
|
|
63
|
+
// nothing remains.
|
|
60
64
|
const theme = ctx.ui.theme;
|
|
61
65
|
const ui = new SessionUI(ctx);
|
|
62
66
|
// Inline any @file spec the user referenced so clarify/decompose reason over
|
|
@@ -69,26 +73,38 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
69
73
|
const parsed = parseClarifyList(qRaw);
|
|
70
74
|
if (parsed.length === 0)
|
|
71
75
|
break; // NONE / nothing left to ask
|
|
72
|
-
const { question, suggested } = parsed[0];
|
|
76
|
+
const { question, suggested, alt } = parsed[0];
|
|
73
77
|
// Render markdown (bold/code) for the displayed prompt; keep plain text
|
|
74
78
|
// for the editable default and the persisted file.
|
|
75
79
|
const shownQ = renderInlineMarkdown(question, theme);
|
|
76
80
|
const plainQ = stripInlineMarkdown(question);
|
|
77
81
|
const plainSuggested = suggested === undefined ? undefined : stripInlineMarkdown(suggested);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const plainAlt = alt === undefined ? undefined : stripInlineMarkdown(alt);
|
|
83
|
+
// Compact A/B presentation, identical to /task's grill dialog: a binary
|
|
84
|
+
// fork shows both options labelled A/B; a single recommendation shows just
|
|
85
|
+
// the default; an open question shows the bare prompt. No verbose
|
|
86
|
+
// "Recommended:" / "press Enter to accept" scaffolding.
|
|
87
|
+
const title = plainSuggested ?
|
|
88
|
+
plainAlt ?
|
|
89
|
+
`${shownQ}\nA: ${renderInlineMarkdown(suggested, theme)}\nB: ${renderInlineMarkdown(alt, theme)}`
|
|
90
|
+
: `${shownQ}\n${renderInlineMarkdown(suggested, theme)}`
|
|
91
|
+
: shownQ;
|
|
81
92
|
const a = await ui.ask({
|
|
82
93
|
localTitle: title,
|
|
83
94
|
question: plainQ,
|
|
84
95
|
recommended: plainSuggested,
|
|
85
|
-
|
|
96
|
+
...(plainAlt !== undefined && { recommended2: plainAlt }),
|
|
97
|
+
allowSkip: plainSuggested === undefined && plainAlt === undefined
|
|
86
98
|
});
|
|
87
99
|
if (a === undefined) {
|
|
88
100
|
ctx.ui.notify('/task-auto cancelled.', 'warning');
|
|
89
101
|
return null;
|
|
90
102
|
}
|
|
91
103
|
const typed = a.trim();
|
|
104
|
+
// Two-option mode labels the choices "A:"/"B:", so a user (local TUI or
|
|
105
|
+
// remote "Manual answer") naturally types the bare letter to pick. Map it
|
|
106
|
+
// back to the option's full text. Mirrors phaseGrill's answer mapping.
|
|
107
|
+
const twoOption = plainSuggested !== undefined && plainAlt !== undefined;
|
|
92
108
|
let answer;
|
|
93
109
|
if (typed.length === 0 && plainSuggested) {
|
|
94
110
|
answer = `${plainSuggested} (accepted recommendation)`;
|
|
@@ -96,6 +112,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
96
112
|
else if (typed.length === 0) {
|
|
97
113
|
answer = '(skipped)';
|
|
98
114
|
}
|
|
115
|
+
else if (twoOption && /^a[.)]?$/i.test(typed)) {
|
|
116
|
+
answer = plainSuggested;
|
|
117
|
+
}
|
|
118
|
+
else if (twoOption && /^b[.)]?$/i.test(typed)) {
|
|
119
|
+
answer = plainAlt;
|
|
120
|
+
}
|
|
99
121
|
else {
|
|
100
122
|
answer = typed;
|
|
101
123
|
}
|
|
@@ -230,6 +252,17 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
230
252
|
resumeId = undefined;
|
|
231
253
|
}
|
|
232
254
|
}
|
|
255
|
+
// Before starting, fold any uncommitted work into its own checkpoint
|
|
256
|
+
// commit so a dirty tree at the start of the run — or edits left behind
|
|
257
|
+
// by an interrupted/failed task — land separately instead of being swept
|
|
258
|
+
// into this task's snapshot. Best-effort and a no-op on a clean tree
|
|
259
|
+
// (gitCommitAll commits nothing), so it only ever produces a commit when
|
|
260
|
+
// there is stray work; the matching post-task commit below is the "after"
|
|
261
|
+
// half. Only the success path is announced to keep the common no-op quiet.
|
|
262
|
+
const checkpoint = await deps.commit(cwd, `chore: checkpoint before "${next.title}"`);
|
|
263
|
+
if (checkpoint.committed) {
|
|
264
|
+
active.ui.notify(`${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
265
|
+
}
|
|
233
266
|
const res = await deps.runTask(active, cwd, next.title, {
|
|
234
267
|
resumeId,
|
|
235
268
|
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
7
|
-
* single numbered question followed by a "SUGGESTED: <default>" line,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
8
|
+
* "ALT: <alternative>" line for binary "A or B?" forks, or the literal token NONE
|
|
9
|
+
* when no clarification remains. priorQA carries the questions already answered so
|
|
10
|
+
* each next question adapts to them.
|
|
10
11
|
*/
|
|
11
12
|
export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) => string;
|
|
12
13
|
/**
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Clarify: asks ONE question at a time. Output MUST match parseClarifyList — a
|
|
7
|
-
* single numbered question followed by a "SUGGESTED: <default>" line,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* single numbered question followed by a "SUGGESTED: <default>" line, an optional
|
|
8
|
+
* "ALT: <alternative>" line for binary "A or B?" forks, or the literal token NONE
|
|
9
|
+
* when no clarification remains. priorQA carries the questions already answered so
|
|
10
|
+
* each next question adapts to them.
|
|
10
11
|
*/
|
|
11
12
|
export const AUTO_CLARIFY_PROMPT = (feature, priorQA) => `You are planning how to split a feature into separate implementation tasks, one clarifying question at a time.
|
|
12
13
|
|
|
@@ -33,14 +34,16 @@ fork the breakdown). Account for the answers so far:
|
|
|
33
34
|
real-time vs polling transport, search, deployment).
|
|
34
35
|
- Skip anything /task will naturally resolve per-task during its own research.
|
|
35
36
|
|
|
36
|
-
Also propose the
|
|
37
|
-
|
|
37
|
+
Also propose the most sensible default answer for this question, inferred from
|
|
38
|
+
the repo, the referenced docs, and any stated philosophy or constraints —
|
|
38
39
|
concrete and decisive, shown to the user as a recommendation they can accept or
|
|
39
|
-
override.
|
|
40
|
+
override. When the question is a genuine binary "A or B?" fork, also give the
|
|
41
|
+
single best alternative as a second option; otherwise offer only the one default.
|
|
40
42
|
|
|
41
43
|
OUTPUT FORMAT (exact):
|
|
42
44
|
- One clarifying question as a single numbered line: "1. ...".
|
|
43
45
|
- On the NEXT line (never inline), a line that begins with "SUGGESTED: <your recommended default>".
|
|
46
|
+
- ONLY for a binary "A or B?" fork, on the line after that, a line beginning with "ALT: <the alternative option>". Omit the ALT line entirely for open-ended questions.
|
|
44
47
|
- Put the core question in **bold**, followed by a short one-line rationale in plain prose. Backticks around code/identifiers are fine. Avoid other markdown (headings, bullet lists, links).
|
|
45
48
|
- Only when the spec already pins down every choice that would change the task breakdown — nothing decision-changing is left to ask — output exactly:
|
|
46
49
|
NONE`;
|
package/dist/task/parsers.d.ts
CHANGED
|
@@ -17,9 +17,12 @@ export type AutoAnswer = {
|
|
|
17
17
|
export interface ClarifyQuestion {
|
|
18
18
|
question: string;
|
|
19
19
|
suggested?: string;
|
|
20
|
+
/** Secondary option for a binary "A or B?" fork; mirrors grill's ALT line. */
|
|
21
|
+
alt?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare const GRILL_LINE_RE: RegExp;
|
|
22
24
|
export declare const SUGGESTED_LINE_RE: RegExp;
|
|
25
|
+
export declare const ALT_LINE_RE: RegExp;
|
|
23
26
|
export declare function parseGrillQuestions(raw: string): string[];
|
|
24
27
|
export declare function parseClarifyList(raw: string): ClarifyQuestion[];
|
|
25
28
|
export declare function autoAnswerHasTag(raw: string): boolean;
|
package/dist/task/parsers.js
CHANGED
|
@@ -7,6 +7,7 @@ import { MAX_GRILL_QUESTIONS } from './phases.js';
|
|
|
7
7
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
8
8
|
export const GRILL_LINE_RE = /^\s*\d+[.)]\s+(.+)$/;
|
|
9
9
|
export const SUGGESTED_LINE_RE = /^\s*SUGGESTED:\s*(.*)$/i;
|
|
10
|
+
export const ALT_LINE_RE = /^\s*ALT:\s*(.*)$/i;
|
|
10
11
|
// ─── Grill questions parser ──────────────────────────────────────────────────
|
|
11
12
|
// The grill-gen prompt instructs the worker to emit the literal token `NONE`
|
|
12
13
|
// when it has zero questions, so the runner's empty-output guard can still
|
|
@@ -30,14 +31,32 @@ export function parseGrillQuestions(raw) {
|
|
|
30
31
|
// (e.g. "1. ...so this must be resolved. SUGGESTED: use polling.") rather than
|
|
31
32
|
// on its own line.
|
|
32
33
|
const INLINE_SUGGESTED_RE = /\bSUGGESTED:\s*/i;
|
|
33
|
-
|
|
34
|
+
const INLINE_ALT_RE = /\bALT:\s*/i;
|
|
35
|
+
/**
|
|
36
|
+
* Split a question line's text into the question, any inline SUGGESTED default,
|
|
37
|
+
* and any inline ALT secondary option (the model may write both on one line:
|
|
38
|
+
* "1. A or B? SUGGESTED: A ALT: B").
|
|
39
|
+
*/
|
|
34
40
|
function splitInlineSuggested(text) {
|
|
35
41
|
const m = INLINE_SUGGESTED_RE.exec(text);
|
|
36
42
|
if (!m)
|
|
37
43
|
return { question: text.trim() };
|
|
38
44
|
const question = text.slice(0, m.index).trim();
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
let rest = text.slice(m.index + m[0].length);
|
|
46
|
+
let alt;
|
|
47
|
+
const altM = INLINE_ALT_RE.exec(rest);
|
|
48
|
+
if (altM) {
|
|
49
|
+
const altText = rest.slice(altM.index + altM[0].length).trim();
|
|
50
|
+
if (altText.length > 0)
|
|
51
|
+
alt = altText;
|
|
52
|
+
rest = rest.slice(0, altM.index);
|
|
53
|
+
}
|
|
54
|
+
const suggested = rest.trim();
|
|
55
|
+
return {
|
|
56
|
+
question,
|
|
57
|
+
...(suggested.length > 0 && { suggested }),
|
|
58
|
+
...(alt !== undefined && { alt })
|
|
59
|
+
};
|
|
41
60
|
}
|
|
42
61
|
// Parses the /task-auto clarify output: a numbered question list where each
|
|
43
62
|
// question carries a "SUGGESTED: <default>" recommendation — either on its own
|
|
@@ -67,6 +86,15 @@ export function parseClarifyList(raw) {
|
|
|
67
86
|
if (suggested.length > 0 && last.suggested === undefined) {
|
|
68
87
|
last.suggested = suggested;
|
|
69
88
|
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const altLine = ALT_LINE_RE.exec(line);
|
|
92
|
+
if (altLine && out.length > 0) {
|
|
93
|
+
const alt = altLine[1].trim();
|
|
94
|
+
const last = out[out.length - 1];
|
|
95
|
+
if (alt.length > 0 && last.alt === undefined) {
|
|
96
|
+
last.alt = alt;
|
|
97
|
+
}
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
return out;
|
|
@@ -11,21 +11,22 @@ const turndown = new TurndownService({
|
|
|
11
11
|
});
|
|
12
12
|
export function cleanHtml(html, baseUrl) {
|
|
13
13
|
const { document } = parseHTML(html);
|
|
14
|
+
const doc = document;
|
|
14
15
|
const reader = new Readability(document);
|
|
15
16
|
const parsed = reader.parse();
|
|
16
17
|
if (parsed && parsed.content) {
|
|
17
18
|
return {
|
|
18
|
-
title: parsed.title ||
|
|
19
|
+
title: parsed.title || doc.title || new URL(baseUrl).hostname,
|
|
19
20
|
markdown: turndown.turndown(parsed.content).trim(),
|
|
20
21
|
finalUrl: baseUrl
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
// Fallback: turndown the body
|
|
24
|
-
const body =
|
|
25
|
+
const body = doc.body;
|
|
25
26
|
const bodyHtml = body ? body.innerHTML : '';
|
|
26
27
|
const markdown = turndown.turndown(bodyHtml).trim();
|
|
27
28
|
return {
|
|
28
|
-
title:
|
|
29
|
+
title: doc.title || new URL(baseUrl).hostname,
|
|
29
30
|
markdown,
|
|
30
31
|
finalUrl: baseUrl
|
|
31
32
|
};
|
|
@@ -147,7 +148,9 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
147
148
|
let bytesRead = 0;
|
|
148
149
|
try {
|
|
149
150
|
while (true) {
|
|
150
|
-
|
|
151
|
+
// response.body's stream type doesn't resolve here, so the chunk
|
|
152
|
+
// surfaces as `any`; pin it to the Uint8Array the reader yields.
|
|
153
|
+
const { value, done } = (await reader.read());
|
|
151
154
|
if (done)
|
|
152
155
|
break;
|
|
153
156
|
if (value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.8",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|