@mjasnikovs/pi-task 0.7.0 → 0.7.2
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/README.md +37 -39
- package/assets/pipeline.svg +90 -0
- package/assets/task-auto.svg +65 -0
- package/dist/remote/bridge.d.ts +1 -10
- package/dist/remote/bridge.js +3 -19
- package/dist/remote/events.d.ts +3 -2
- package/dist/remote/events.js +25 -50
- package/dist/remote/history.d.ts +18 -5
- package/dist/remote/history.js +11 -4
- package/dist/remote/protocol.d.ts +8 -5
- package/dist/remote/register.js +7 -11
- package/dist/remote/server.d.ts +1 -2
- package/dist/remote/server.js +6 -13
- package/dist/remote/session-state.d.ts +54 -0
- package/dist/remote/session-state.js +179 -0
- package/dist/remote/ui.js +154 -37
- package/dist/task/auto-io.d.ts +7 -0
- package/dist/task/auto-io.js +24 -13
- package/dist/task/auto-orchestrator.d.ts +6 -1
- package/dist/task/auto-orchestrator.js +15 -3
- package/dist/task/orchestrator.d.ts +6 -1
- package/dist/task/orchestrator.js +9 -2
- package/dist/task/widget.js +7 -7
- package/dist/workers/html-clean.js +77 -9
- package/dist/workers/pi-worker-docs.js +14 -11
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +8 -3
- package/dist/workers/pi-worker.js +9 -6
- package/package.json +2 -1
package/dist/task/auto-io.js
CHANGED
|
@@ -53,17 +53,15 @@ export function parseTaskList(body) {
|
|
|
53
53
|
continue;
|
|
54
54
|
const done = m[1].toLowerCase() === 'x';
|
|
55
55
|
const rest = m[2].trim();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
entries.push({ index, title: rest, done: true });
|
|
63
|
-
}
|
|
56
|
+
// A line carries a stamped TASK_NNNN id both when done (the completed
|
|
57
|
+
// inner task) and when merely started — an unchecked, stamped line is an
|
|
58
|
+
// in-progress entry whose inner task can be resumed.
|
|
59
|
+
const idm = PRODUCED_ID_RE.exec(rest);
|
|
60
|
+
if (idm) {
|
|
61
|
+
entries.push({ index, title: idm[2].trim(), done, producedId: idm[1] });
|
|
64
62
|
}
|
|
65
63
|
else {
|
|
66
|
-
entries.push({ index, title: rest, done
|
|
64
|
+
entries.push({ index, title: rest, done });
|
|
67
65
|
}
|
|
68
66
|
index++;
|
|
69
67
|
}
|
|
@@ -76,8 +74,8 @@ export function buildAutoBody(feature, clarifications, titles) {
|
|
|
76
74
|
+ `## clarifications\n\n${clarifications.trim() || '(none)'}\n\n`
|
|
77
75
|
+ `## tasks\n\n${tasks}\n`);
|
|
78
76
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
77
|
+
/** Rewrite the Nth checkbox line of the "## tasks" section in place. */
|
|
78
|
+
async function rewriteTaskLine(cwd, id, index, render, label) {
|
|
81
79
|
const { body } = await readTaskFile(cwd, id);
|
|
82
80
|
const section = extractSection(body, 'tasks') ?? '';
|
|
83
81
|
const lines = section.split('\n');
|
|
@@ -87,15 +85,28 @@ export async function checkOffTask(cwd, id, index, producedId, title) {
|
|
|
87
85
|
continue;
|
|
88
86
|
seen++;
|
|
89
87
|
if (seen === index) {
|
|
90
|
-
lines[i] =
|
|
88
|
+
lines[i] = render();
|
|
91
89
|
break;
|
|
92
90
|
}
|
|
93
91
|
}
|
|
94
92
|
if (seen < index) {
|
|
95
|
-
throw new Error(
|
|
93
|
+
throw new Error(`${label}: index ${index} out of range in ${id} (only ${seen + 1} checkboxes found)`);
|
|
96
94
|
}
|
|
97
95
|
await setTaskSection(cwd, id, 'tasks', lines.join('\n'));
|
|
98
96
|
}
|
|
97
|
+
/** Check off the Nth checkbox line, stamping the produced TASK_NNNN id. */
|
|
98
|
+
export async function checkOffTask(cwd, id, index, producedId, title) {
|
|
99
|
+
await rewriteTaskLine(cwd, id, index, () => (producedId ? `- [x] ${producedId} ${title}` : `- [x] ${title}`), 'checkOffTask');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Stamp the inner TASK_NNNN id onto the Nth (still-unchecked) entry the moment
|
|
103
|
+
* the inner task is allocated. This links the AUTO entry to its in-progress
|
|
104
|
+
* inner task so /task-auto-resume can continue it from its saved phase instead
|
|
105
|
+
* of starting a brand-new task — matching how /task-resume behaves.
|
|
106
|
+
*/
|
|
107
|
+
export async function stampTaskInProgress(cwd, id, index, producedId, title) {
|
|
108
|
+
await rewriteTaskLine(cwd, id, index, () => `- [ ] ${producedId} ${title}`, 'stampTaskInProgress');
|
|
109
|
+
}
|
|
99
110
|
/** Find the most-recently-updated resumable TASK_AUTO_* file, or null. */
|
|
100
111
|
export async function findResumableAuto(cwd) {
|
|
101
112
|
await ensureTasksDir(cwd);
|
|
@@ -7,7 +7,12 @@ import { type CommitResult } from './auto-commit.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export interface AutoDeps {
|
|
9
9
|
runChild: (name: string, tools: string, prompt: string) => Promise<string>;
|
|
10
|
-
runTask: (ctx: ExtensionCommandContext, cwd: string, title: string
|
|
10
|
+
runTask: (ctx: ExtensionCommandContext, cwd: string, title: string, opts?: {
|
|
11
|
+
/** Resume this inner task id instead of allocating a fresh one. */
|
|
12
|
+
resumeId?: string;
|
|
13
|
+
/** Called with the inner task id once its file exists, before phases. */
|
|
14
|
+
onStart?: (taskId: string) => void | Promise<void>;
|
|
15
|
+
}) => Promise<RunSingleTaskResult>;
|
|
11
16
|
/** Snapshot the working tree into one commit after a task passes. */
|
|
12
17
|
commit: (cwd: string, message: string) => Promise<CommitResult>;
|
|
13
18
|
}
|
|
@@ -11,7 +11,7 @@ import { runSingleTask } from './orchestrator.js';
|
|
|
11
11
|
import { parseClarifyList, deriveTitle } from './parsers.js';
|
|
12
12
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
13
13
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT } from './auto-prompts.js';
|
|
14
|
-
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, findResumableAuto } from './auto-io.js';
|
|
14
|
+
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
15
15
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter } from './task-io.js';
|
|
16
16
|
import { gitCommitAll } from './auto-commit.js';
|
|
17
17
|
import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
|
|
@@ -176,7 +176,11 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
176
176
|
stopLoader();
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
|
-
runTask: (c, cwd2, t) => runSingleTask(c, cwd2, t, {
|
|
179
|
+
runTask: (c, cwd2, t, opts) => runSingleTask(c, cwd2, t, {
|
|
180
|
+
waitForImplementation: true,
|
|
181
|
+
resumeId: opts?.resumeId,
|
|
182
|
+
onStart: opts?.onStart
|
|
183
|
+
}),
|
|
180
184
|
commit: (cwd2, message) => gitCommitAll(cwd2, message, signal)
|
|
181
185
|
};
|
|
182
186
|
}
|
|
@@ -208,7 +212,15 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
208
212
|
return;
|
|
209
213
|
}
|
|
210
214
|
active.ui.notify(`${id}: task ${next.index + 1}/${entries.length} — ${next.title}`, 'info');
|
|
211
|
-
|
|
215
|
+
// If this entry already has a stamped inner id, it was started in a
|
|
216
|
+
// previous (interrupted) run — resume it from its saved phase rather
|
|
217
|
+
// than spawning a fresh task. Otherwise stamp the freshly-allocated id
|
|
218
|
+
// onto the entry the moment it exists, so an interruption here is
|
|
219
|
+
// resumable too. This mirrors /task-resume's continue-don't-restart.
|
|
220
|
+
const res = await deps.runTask(active, cwd, next.title, {
|
|
221
|
+
resumeId: next.producedId,
|
|
222
|
+
onStart: next.producedId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
223
|
+
});
|
|
212
224
|
active = res.ctx ?? active;
|
|
213
225
|
if (res.sessionCancelled) {
|
|
214
226
|
active.ui.notify(`${id} paused — could not start a session. Run /task-auto-resume to retry.`, 'warning');
|
|
@@ -25,6 +25,7 @@ export declare class TaskRunner {
|
|
|
25
25
|
private readonly _rawPrompt;
|
|
26
26
|
private readonly _resumeId;
|
|
27
27
|
private readonly _sendSpec;
|
|
28
|
+
private readonly _onStart;
|
|
28
29
|
private readonly _abort;
|
|
29
30
|
private readonly _startedAt;
|
|
30
31
|
private readonly _widgetState;
|
|
@@ -40,7 +41,7 @@ export declare class TaskRunner {
|
|
|
40
41
|
*/
|
|
41
42
|
private readonly _timings;
|
|
42
43
|
private _currentPhaseChildren;
|
|
43
|
-
constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn);
|
|
44
|
+
constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>);
|
|
44
45
|
get taskId(): string;
|
|
45
46
|
get signal(): AbortSignal;
|
|
46
47
|
/** Return the current widget state, or null if not started. */
|
|
@@ -64,6 +65,10 @@ export interface RunSingleTaskOptions {
|
|
|
64
65
|
resumeId?: string;
|
|
65
66
|
/** Test seam: spawn function forwarded to TaskRunner. */
|
|
66
67
|
spawnFn?: SpawnFn;
|
|
68
|
+
/** Called with the resolved task id once its file exists, before any phase
|
|
69
|
+
* work. Lets callers record the id (e.g. stamp the /task-auto entry) so an
|
|
70
|
+
* interrupted run can be resumed instead of restarted. */
|
|
71
|
+
onStart?: (taskId: string) => void | Promise<void>;
|
|
67
72
|
}
|
|
68
73
|
export interface RunSingleTaskResult {
|
|
69
74
|
taskId: string;
|
|
@@ -45,6 +45,7 @@ export class TaskRunner {
|
|
|
45
45
|
_rawPrompt;
|
|
46
46
|
_resumeId;
|
|
47
47
|
_sendSpec;
|
|
48
|
+
_onStart;
|
|
48
49
|
_abort = new AbortController();
|
|
49
50
|
_startedAt;
|
|
50
51
|
_widgetState;
|
|
@@ -60,12 +61,13 @@ export class TaskRunner {
|
|
|
60
61
|
*/
|
|
61
62
|
_timings = [];
|
|
62
63
|
_currentPhaseChildren = null;
|
|
63
|
-
constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn) {
|
|
64
|
+
constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart) {
|
|
64
65
|
this._ctx = ctx;
|
|
65
66
|
this._cwd = cwd;
|
|
66
67
|
this._rawPrompt = rawPrompt;
|
|
67
68
|
this._resumeId = resumeId;
|
|
68
69
|
this._sendSpec = sendSpec;
|
|
70
|
+
this._onStart = onStart;
|
|
69
71
|
this._startedAt = Date.now();
|
|
70
72
|
// We'll populate id/title/phase lazily in run().
|
|
71
73
|
// Placeholder — real values set in run().
|
|
@@ -157,6 +159,11 @@ export class TaskRunner {
|
|
|
157
159
|
};
|
|
158
160
|
await writeTaskFile(cwd, fm, `\n## raw prompt\n\n${this._rawPrompt.trim() || '(none)'}\n`);
|
|
159
161
|
}
|
|
162
|
+
// Surface the resolved id now that the task file exists, so callers (e.g.
|
|
163
|
+
// the /task-auto loop) can link this run to their own bookkeeping before
|
|
164
|
+
// any phase work — and recover it if the session dies mid-pipeline.
|
|
165
|
+
if (this._onStart)
|
|
166
|
+
await this._onStart(id);
|
|
160
167
|
// Register as active.
|
|
161
168
|
this._widgetState.taskId = id;
|
|
162
169
|
this._widgetState.title = title;
|
|
@@ -279,7 +286,7 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
279
286
|
await newCtx.sendUserMessage(spec);
|
|
280
287
|
if (opts.waitForImplementation)
|
|
281
288
|
await newCtx.waitForIdle();
|
|
282
|
-
}, opts.spawnFn);
|
|
289
|
+
}, opts.spawnFn, opts.onStart);
|
|
283
290
|
await runner.run();
|
|
284
291
|
taskId = runner.taskId;
|
|
285
292
|
}
|
package/dist/task/widget.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* context usage, and the latest child-process line.
|
|
6
6
|
*/
|
|
7
7
|
import { PHASE_INDEX, PHASE_ORDER } from './task-file.js';
|
|
8
|
-
import {
|
|
8
|
+
import { setTaskWidget } from '../remote/session-state.js';
|
|
9
9
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
10
10
|
export const WIDGET_KEY = 'pi-tasks';
|
|
11
11
|
export const AUTO_WIDGET_KEY = 'pi-task-auto';
|
|
@@ -101,7 +101,7 @@ export function startWidget(ctx, getState) {
|
|
|
101
101
|
catch {
|
|
102
102
|
/* stale ctx */
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
setTaskWidget(plain);
|
|
105
105
|
};
|
|
106
106
|
render();
|
|
107
107
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
@@ -114,7 +114,7 @@ export function startWidget(ctx, getState) {
|
|
|
114
114
|
catch {
|
|
115
115
|
/* stale ctx */
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
setTaskWidget(undefined);
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
export function buildAutoLoaderLines(s, theme) {
|
|
@@ -150,7 +150,7 @@ export function startAutoLoader(ctx, getState) {
|
|
|
150
150
|
catch {
|
|
151
151
|
/* stale ctx */
|
|
152
152
|
}
|
|
153
|
-
|
|
153
|
+
setTaskWidget(plain);
|
|
154
154
|
};
|
|
155
155
|
render();
|
|
156
156
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
@@ -163,7 +163,7 @@ export function startAutoLoader(ctx, getState) {
|
|
|
163
163
|
catch {
|
|
164
164
|
/* stale ctx */
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
setTaskWidget(undefined);
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
@@ -185,7 +185,7 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
|
185
185
|
: `✘ ${taskId} failed${reason ? ': ' + reason : ''}`;
|
|
186
186
|
try {
|
|
187
187
|
ctx.ui.setWidget(WIDGET_KEY, [line]);
|
|
188
|
-
|
|
188
|
+
setTaskWidget([plainLine]);
|
|
189
189
|
}
|
|
190
190
|
catch {
|
|
191
191
|
/* stale ctx */
|
|
@@ -193,7 +193,7 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
|
193
193
|
setTimeout(() => {
|
|
194
194
|
try {
|
|
195
195
|
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
196
|
-
|
|
196
|
+
setTaskWidget(undefined);
|
|
197
197
|
}
|
|
198
198
|
catch {
|
|
199
199
|
/* stale ctx */
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
1
4
|
import { JSDOM } from 'jsdom';
|
|
2
5
|
import { Readability } from '@mozilla/readability';
|
|
3
6
|
import TurndownService from 'turndown';
|
|
@@ -29,8 +32,57 @@ export function cleanHtml(html, baseUrl) {
|
|
|
29
32
|
}
|
|
30
33
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
31
34
|
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024; // 2 MB
|
|
32
|
-
const PKG_VERSION =
|
|
35
|
+
const PKG_VERSION = readPkgVersion();
|
|
33
36
|
const USER_AGENT = `pi-worker/${PKG_VERSION} (+https://npmjs.com/package/@mjasnikovs/pi-worker)`;
|
|
37
|
+
// Read the version from package.json at runtime so the User-Agent never drifts
|
|
38
|
+
// out of sync with releases. Two levels up holds for both src/workers (tests)
|
|
39
|
+
// and dist/workers (build) since tsc preserves the layout under rootDir.
|
|
40
|
+
function readPkgVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8'));
|
|
44
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return '0.0.0';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Decide how to handle a response based on its content-type. HTML is run through
|
|
51
|
+
// the readability/turndown pipeline; text-ish formats (markdown, plain text,
|
|
52
|
+
// JSON, XML/feeds) are already clean and pass through verbatim; binary formats
|
|
53
|
+
// (PDF, images, octet-stream, …) are rejected. A missing content-type is treated
|
|
54
|
+
// as text — many plain-text endpoints (llms.txt, robots.txt) omit the header.
|
|
55
|
+
function classifyContentType(contentType) {
|
|
56
|
+
const mime = contentType.split(';')[0].trim().toLowerCase();
|
|
57
|
+
if (mime === '')
|
|
58
|
+
return 'text';
|
|
59
|
+
if (mime === 'text/html' || mime === 'application/xhtml+xml')
|
|
60
|
+
return 'html';
|
|
61
|
+
if (mime.startsWith('text/'))
|
|
62
|
+
return 'text';
|
|
63
|
+
if (mime === 'application/json' || mime.endsWith('+json'))
|
|
64
|
+
return 'text';
|
|
65
|
+
if (mime === 'application/xml' || mime.endsWith('+xml'))
|
|
66
|
+
return 'text';
|
|
67
|
+
if (mime === 'application/javascript' || mime === 'application/ecmascript')
|
|
68
|
+
return 'text';
|
|
69
|
+
return 'reject';
|
|
70
|
+
}
|
|
71
|
+
// Extract the charset from a content-type header, if present and supported by
|
|
72
|
+
// TextDecoder; otherwise fall back to UTF-8 so non-UTF-8 pages aren't mangled.
|
|
73
|
+
function decoderFor(contentType) {
|
|
74
|
+
const match = /charset=([^;]+)/i.exec(contentType);
|
|
75
|
+
const charset = match?.[1]?.trim().replace(/^["']|["']$/g, '');
|
|
76
|
+
if (charset) {
|
|
77
|
+
try {
|
|
78
|
+
return new TextDecoder(charset, { fatal: false });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Unknown/unsupported label — fall through to UTF-8.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return new TextDecoder('utf-8', { fatal: false });
|
|
85
|
+
}
|
|
34
86
|
export class FetchAndCleanError extends Error {
|
|
35
87
|
kind;
|
|
36
88
|
cause;
|
|
@@ -77,15 +129,16 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
77
129
|
throw new FetchAndCleanError(`Fetch failed: HTTP ${response.status} ${response.statusText} for ${url}`, 'http-error');
|
|
78
130
|
}
|
|
79
131
|
const contentType = response.headers.get('content-type') ?? '';
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
const kind = classifyContentType(contentType);
|
|
133
|
+
if (kind === 'reject') {
|
|
134
|
+
throw new FetchAndCleanError(`${url} is ${contentType || 'unknown content type'}, not a text or HTML page that pi-worker-fetch can read.`, 'not-html');
|
|
82
135
|
}
|
|
83
136
|
const reader = response.body?.getReader();
|
|
84
137
|
if (!reader) {
|
|
85
138
|
throw new FetchAndCleanError(`Could not fetch ${url}: empty response body`, 'network');
|
|
86
139
|
}
|
|
87
|
-
const decoder =
|
|
88
|
-
let
|
|
140
|
+
const decoder = decoderFor(contentType);
|
|
141
|
+
let text = '';
|
|
89
142
|
let bytesRead = 0;
|
|
90
143
|
try {
|
|
91
144
|
while (true) {
|
|
@@ -99,10 +152,10 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
99
152
|
internalController.abort();
|
|
100
153
|
break;
|
|
101
154
|
}
|
|
102
|
-
|
|
155
|
+
text += decoder.decode(value, { stream: true });
|
|
103
156
|
}
|
|
104
157
|
}
|
|
105
|
-
|
|
158
|
+
text += decoder.decode();
|
|
106
159
|
}
|
|
107
160
|
catch (err) {
|
|
108
161
|
if (sizeExceeded) {
|
|
@@ -119,8 +172,15 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
119
172
|
throw new FetchAndCleanError(`${url} exceeds ${formatBytes(maxBytes)} size cap. Try a more specific URL.`, 'too-large');
|
|
120
173
|
}
|
|
121
174
|
const finalUrl = response.url || url;
|
|
122
|
-
|
|
123
|
-
|
|
175
|
+
if (kind === 'html') {
|
|
176
|
+
return cleanHtml(text, finalUrl);
|
|
177
|
+
}
|
|
178
|
+
// text-ish formats are already clean — return them verbatim.
|
|
179
|
+
return {
|
|
180
|
+
title: hostnameOf(finalUrl),
|
|
181
|
+
markdown: text.trim(),
|
|
182
|
+
finalUrl
|
|
183
|
+
};
|
|
124
184
|
}
|
|
125
185
|
finally {
|
|
126
186
|
clearTimeout(timeoutHandle);
|
|
@@ -128,6 +188,14 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
128
188
|
opts.signal.removeEventListener('abort', onUserAbort);
|
|
129
189
|
}
|
|
130
190
|
}
|
|
191
|
+
function hostnameOf(url) {
|
|
192
|
+
try {
|
|
193
|
+
return new URL(url).hostname;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return url;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
131
199
|
function describeError(err) {
|
|
132
200
|
if (err instanceof Error)
|
|
133
201
|
return err.message;
|
|
@@ -20,17 +20,20 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
20
20
|
pi.registerTool({
|
|
21
21
|
name: 'pi-worker-docs',
|
|
22
22
|
label: 'Pi Worker Docs',
|
|
23
|
-
description: 'Look up an npm package
|
|
24
|
-
+ 'answer
|
|
25
|
-
+ 'live npm registry call
|
|
26
|
-
+ '
|
|
27
|
-
+ '
|
|
28
|
-
+ '
|
|
29
|
-
+ '
|
|
30
|
-
+ '
|
|
31
|
-
+ '
|
|
32
|
-
+ '
|
|
33
|
-
+ '
|
|
23
|
+
description: 'Look up an INSTALLED npm package and return a focused, version-pinned '
|
|
24
|
+
+ 'answer from its .d.ts types and README, PLUS the latest published version '
|
|
25
|
+
+ 'from a live npm registry call. USE THIS BEFORE ANSWERING any question '
|
|
26
|
+
+ 'about how to use a library, what it exports, its types/overloads/config, '
|
|
27
|
+
+ 'or the latest published version of an npm package. Do NOT answer package '
|
|
28
|
+
+ 'APIs from memory, do NOT run `npm view`/bash to get a package version, and '
|
|
29
|
+
+ 'do NOT web-search for an installed package — this tool is the source of '
|
|
30
|
+
+ 'truth and is version-pinned to what is actually installed (training-data '
|
|
31
|
+
+ 'versions and APIs are typically months stale).\n'
|
|
32
|
+
+ 'For a non-package framework/runtime version (e.g. Node.js, Ubuntu), use '
|
|
33
|
+
+ '`pi-worker-search` instead. If the package is not installed it is '
|
|
34
|
+
+ 'auto-installed via bun add or npm install. The cache lives at '
|
|
35
|
+
+ '~/.cache/pi-worker/docs.sqlite, keyed by exact installed version; the '
|
|
36
|
+
+ 'registry lookup is best-effort and silently absent when offline.\n'
|
|
34
37
|
+ '\n'
|
|
35
38
|
+ 'Good fits:\n'
|
|
36
39
|
+ '- "What does library X export?" / "How does function Y work?"\n'
|
|
@@ -14,10 +14,11 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
14
14
|
pi.registerTool({
|
|
15
15
|
name: 'pi-worker-fetch',
|
|
16
16
|
label: 'Pi Worker Fetch',
|
|
17
|
-
description: 'Fetch
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
20
|
-
+ '
|
|
17
|
+
description: 'Fetch a web page or text resource (HTML, markdown, plain text, JSON, '
|
|
18
|
+
+ 'XML/feeds), clean HTML to markdown, and hand it to an isolated child '
|
|
19
|
+
+ 'Pi session that extracts ONLY content answering `query`. Returns the '
|
|
20
|
+
+ 'focused answer. Use after `pi-worker-search` (or with a known URL) to '
|
|
21
|
+
+ 'avoid stuffing raw content into the main context.',
|
|
21
22
|
parameters: Params,
|
|
22
23
|
executionMode: 'parallel',
|
|
23
24
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -14,9 +14,14 @@ export function registerPiWorkerSearch(pi, internals = {}) {
|
|
|
14
14
|
pi.registerTool({
|
|
15
15
|
name: 'pi-worker-search',
|
|
16
16
|
label: 'Pi Worker Search',
|
|
17
|
-
description: 'Search the web via Brave Search.
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
17
|
+
description: 'Search the live web via Brave Search. CALL THIS BEFORE ANSWERING any '
|
|
18
|
+
+ 'question about current or version-specific external facts: '
|
|
19
|
+
+ 'library/framework versions and their APIs, latest releases, recently '
|
|
20
|
+
+ 'shipped features, current events, prices, or who currently holds a '
|
|
21
|
+
+ 'role. Your built-in knowledge is out of date — do NOT answer such '
|
|
22
|
+
+ 'questions from memory and do NOT shell out with bash to guess. Returns '
|
|
23
|
+
+ 'a compact markdown list of up to 10 results (title, URL, snippet); then '
|
|
24
|
+
+ 'call `pi-worker-fetch` on the URL you want to read. '
|
|
20
25
|
+ 'Requires BRAVE_SEARCH_API_KEY env var.',
|
|
21
26
|
parameters: Params,
|
|
22
27
|
executionMode: 'parallel',
|
|
@@ -18,16 +18,19 @@ export function registerPiWorker(pi) {
|
|
|
18
18
|
pi.registerTool({
|
|
19
19
|
name: 'pi-worker',
|
|
20
20
|
label: 'Pi Worker',
|
|
21
|
-
description: 'Dispatch an isolated child Pi to investigate
|
|
22
|
-
+ '
|
|
23
|
-
+ '
|
|
24
|
-
+ 'you
|
|
21
|
+
description: 'Dispatch an isolated child Pi to investigate and return its CONCLUSION — '
|
|
22
|
+
+ 'not the raw evidence. USE THIS FIRST, instead of running your own '
|
|
23
|
+
+ 'ls/grep/find/read, whenever a question spans MULTIPLE files or means '
|
|
24
|
+
+ 'searching/scanning code you have not already located. Doing it yourself '
|
|
25
|
+
+ 'floods your context with raw file output; the worker reads in isolation '
|
|
26
|
+
+ 'and returns only the answer. You can dispatch several in one turn for '
|
|
27
|
+
+ 'independent questions.\n'
|
|
25
28
|
+ '\n'
|
|
26
29
|
+ 'Good fits:\n'
|
|
27
30
|
+ '- "Where/how is X handled in this repo?" across unfamiliar code\n'
|
|
28
31
|
+ '- Audits and pattern scans across many files ("every place we log PII")\n'
|
|
29
|
-
+ '-
|
|
30
|
-
+ '-
|
|
32
|
+
+ '- Tracing a flow across layers (router → service → database)\n'
|
|
33
|
+
+ '- Summarising long test output, logs, or shell output you do not need verbatim\n'
|
|
31
34
|
+ '\n'
|
|
32
35
|
+ 'Skip when:\n'
|
|
33
36
|
+ '- You already know the exact file — call `read` directly\n'
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
|
+
"assets",
|
|
10
11
|
"README.md",
|
|
11
12
|
"LICENSE"
|
|
12
13
|
],
|