@mjasnikovs/pi-task 0.18.46 → 0.18.48

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.
@@ -1,5 +1,42 @@
1
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
1
+ import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
+ import type { Component } from '@earendil-works/pi-tui';
3
+ import { type PiTaskConfig } from './config.js';
2
4
  import { type InstalledExtension } from './extension-list.js';
5
+ type Theme = ExtensionCommandContext['ui']['theme'];
6
+ /**
7
+ * Frames a child component (the settings list) in a rounded border with a title
8
+ * woven into the top edge. Without this the overlay's content sits flush against
9
+ * the chat scrollback and reads as just more console text; the border gives it a
10
+ * distinct panel. Layout mirrors {@link renderQuestionBox}'s box chrome: render
11
+ * the child at the inner width, pad each line so the right edge lines up, then
12
+ * frame. Input and invalidation are forwarded straight through to the child.
13
+ */
14
+ declare class BorderedBox implements Component {
15
+ private readonly child;
16
+ private readonly title;
17
+ private readonly border;
18
+ private readonly titleColor;
19
+ /**
20
+ * Body lines to pad out to. The settings list grows and shrinks with the
21
+ * selected item's description, so without a floor the whole panel jumps
22
+ * a few rows every time the cursor moves. Padding to the tallest layout
23
+ * keeps the border still while the contents change.
24
+ */
25
+ private readonly minBodyLines;
26
+ constructor(child: Component & {
27
+ handleInput(data: string): void;
28
+ }, title: string, border: (s: string) => string, titleColor: (s: string) => string,
29
+ /**
30
+ * Body lines to pad out to. The settings list grows and shrinks with the
31
+ * selected item's description, so without a floor the whole panel jumps
32
+ * a few rows every time the cursor moves. Padding to the tallest layout
33
+ * keeps the border still while the contents change.
34
+ */
35
+ minBodyLines?: number);
36
+ render(width: number): string[];
37
+ invalidate(): void;
38
+ handleInput(data: string): void;
39
+ }
3
40
  export declare function extensionItems(extensions: InstalledExtension[], whitelist: readonly string[]): {
4
41
  id: string;
5
42
  label: string;
@@ -9,4 +46,28 @@ export declare function extensionItems(extensions: InstalledExtension[], whiteli
9
46
  }[];
10
47
  /** Apply an extension toggle to the config's whitelist (idempotent both ways). */
11
48
  export declare function applyExtensionToggle(whitelist: readonly string[], entryPath: string, on: boolean): string[];
49
+ /**
50
+ * Tallest body the settings list can render, so {@link BorderedBox} can pad
51
+ * every frame to it and hold the border still. Mirrors SettingsList's own
52
+ * layout: one pad row, the visible rows, the scroll counter, a blank, the
53
+ * selected item's wrapped description, a blank, the key hint.
54
+ */
55
+ export declare function settingsBodyHeight(descriptions: string[], maxVisible: number, wrapWidth: number): number;
56
+ /** A settings row as {@link SettingsList} wants it. */
57
+ export type PanelItem = {
58
+ id: string;
59
+ label: string;
60
+ description: string;
61
+ currentValue: string;
62
+ values: string[];
63
+ };
64
+ /**
65
+ * Builds the framed settings panel. Split out of the command handler so the
66
+ * exact component the overlay shows can be rendered to a string in a test or a
67
+ * preview script, rather than only being inspectable by opening the TUI.
68
+ */
69
+ export declare function createSettingsPanel(items: PanelItem[], theme: Theme, onChange: (id: string, newValue: string) => void, onCancel: () => void): BorderedBox;
70
+ /** The full settings row list for the current config, in menu order. */
71
+ export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[]): PanelItem[];
12
72
  export declare function registerConfig(pi: ExtensionAPI): void;
73
+ export {};
@@ -1,9 +1,12 @@
1
- import { SettingsList, visibleWidth } from '@earendil-works/pi-tui';
1
+ import { SettingsList, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
2
  import { registerBridgeCommand } from '../remote/bridge.js';
3
+ import { readPkgVersion } from '../shared/pkg-version.js';
3
4
  import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
4
5
  import { COMMAND_TIMEOUT_OPTIONS, getConfig, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
5
6
  import { listInstalledExtensions } from './extension-list.js';
6
- const CONFIG_TITLE = 'pi-task settings';
7
+ // Version in the title so a bug report or screenshot says which build it came
8
+ // from without anyone having to go look it up.
9
+ const CONFIG_TITLE = `pi-task ${readPkgVersion()} settings`;
7
10
  /**
8
11
  * Frames a child component (the settings list) in a rounded border with a title
9
12
  * woven into the top edge. Without this the overlay's content sits flush against
@@ -17,11 +20,20 @@ class BorderedBox {
17
20
  title;
18
21
  border;
19
22
  titleColor;
20
- constructor(child, title, border, titleColor) {
23
+ minBodyLines;
24
+ constructor(child, title, border, titleColor,
25
+ /**
26
+ * Body lines to pad out to. The settings list grows and shrinks with the
27
+ * selected item's description, so without a floor the whole panel jumps
28
+ * a few rows every time the cursor moves. Padding to the tallest layout
29
+ * keeps the border still while the contents change.
30
+ */
31
+ minBodyLines = 0) {
21
32
  this.child = child;
22
33
  this.title = title;
23
34
  this.border = border;
24
35
  this.titleColor = titleColor;
36
+ this.minBodyLines = minBodyLines;
25
37
  }
26
38
  render(width) {
27
39
  const innerWidth = Math.max(1, width - 4); // "│ " + content + " │"
@@ -33,7 +45,13 @@ class BorderedBox {
33
45
  const top = rest >= 0 ?
34
46
  this.border(`╭${dash(lead)}`) + this.titleColor(tag) + this.border(`${dash(rest)}╮`)
35
47
  : this.border(`╭${dash(width - 2)}╮`);
36
- const body = this.child.render(innerWidth).map(line => {
48
+ // One blank row under the title so the first setting is not welded to
49
+ // the border, then the child, then padding up to the stable height.
50
+ const childLines = this.child.render(innerWidth);
51
+ const raw = ['', ...childLines];
52
+ while (raw.length < this.minBodyLines)
53
+ raw.push('');
54
+ const body = raw.map(line => {
37
55
  const pad = innerWidth - visibleWidth(line);
38
56
  const padded = pad > 0 ? line + ' '.repeat(pad) : line;
39
57
  return this.border('│ ') + padded + this.border(' │');
@@ -53,83 +71,95 @@ class BorderedBox {
53
71
  * toggle on/off; enum settings list their values and cycle through them.
54
72
  */
55
73
  const ITEMS = [
56
- { id: 'remote', label: 'remote', description: 'Remote UI server (QR code, phone access)' },
74
+ {
75
+ id: 'remote',
76
+ label: 'remote control',
77
+ description: 'Serve the task UI on your local network so you can follow and steer a run from '
78
+ + 'your phone. Prints a QR code to scan when it starts'
79
+ },
57
80
  {
58
81
  id: 'compressReasoning',
59
- label: 'compress reasoning',
60
- description: 'Compress <think> blocks after each message'
82
+ label: 'compress thinking',
83
+ description: "Shrink the model's thinking blocks once it has moved on, so a long run keeps more "
84
+ + 'room for the work itself'
61
85
  },
62
86
  {
63
87
  id: 'autoCommit',
64
88
  label: 'auto-commit',
65
- description: 'git commit around each /task-auto sub-task (checkpoint before, snapshot after)'
66
- },
67
- {
68
- id: 'orientation',
69
- label: 'orientation',
70
- description: 'Pre-supply the project core (manifest, types, schema…) to the read-heavy research workers'
89
+ description: 'Make a git commit before and after every sub-task, so each step is a checkpoint '
90
+ + 'you can read back or roll back to'
71
91
  },
72
92
  {
73
93
  id: 'verifyWork',
74
94
  label: 'verify work',
75
- description: "After each /task (and /task-auto task), RUN its spec's VERIFY block in the workspace and report a PASS/FAIL verdict (also the signal that lets 'enforce guidelines' fix safely). Enabling it makes /task wait for the implementation"
95
+ description: 'When a task says it is done, actually run the checks its spec asks for and report '
96
+ + "PASS or FAIL instead of taking the model's word for it. This is also what lets "
97
+ + '"enforce guidelines" fix things safely. /task waits for the work to finish'
76
98
  },
77
99
  {
78
100
  id: 'enforceGuidelines',
79
101
  label: 'enforce guidelines',
80
- description: "Check each /task and /task-auto commit against AGENTS.md/CLAUDE.md. Needs 'verify work' to FIX drift (fixes are reverted if they regress verification); without it, only reports violations. Enabling it makes /task wait for the implementation"
102
+ description: 'Check what each task committed against your AGENTS.md / CLAUDE.md rules. With '
103
+ + '"verify work" on it also fixes what it finds, undoing any fix that breaks the '
104
+ + 'checks; on its own it only reports. /task waits for the work to finish'
105
+ },
106
+ {
107
+ id: 'orientation',
108
+ label: 'project tour',
109
+ description: 'Show the research workers the shape of the project first — package manifest, '
110
+ + 'types, schema — so they spend their steps on the question instead of on finding '
111
+ + 'their way around'
81
112
  },
82
113
  {
83
114
  id: 'parallelResearchWorkers',
84
115
  label: 'parallel research',
85
- description: 'Run the 4 research workers concurrently. Leave OFF on a single-GPU local server (serial is measurably faster there); turn on only for a parallel-capable model backend'
116
+ description: 'Run the 4 research workers at once instead of one after another. Only faster if '
117
+ + 'your model backend can answer several requests at the same time — on a single '
118
+ + 'local GPU it is measurably slower, so leave it off there'
86
119
  },
87
120
  {
88
121
  id: 'researchCache',
89
122
  label: 'research cache',
90
- description: 'Cache docs/search/fetch results within one /task-auto run so sibling tasks reuse the first pipeline’s digest instead of re-fetching the same external docs. Per-run isolated, external-only, success-only'
123
+ description: 'Remember docs and web pages for the length of one run, so later tasks reuse what '
124
+ + 'the first one already fetched instead of downloading it again. Only external '
125
+ + 'sources, only successful fetches, and it is dropped when the run ends'
91
126
  },
92
127
  {
93
128
  id: 'searchProvider',
94
- label: 'search provider',
95
- description: 'Engine behind web search (pi-worker-search + freshness checks). Exa and DuckDuckGo need no API key; Brave needs BRAVE_SEARCH_API_KEY',
129
+ label: 'search engine',
130
+ description: 'Which engine backs web search. Exa and DuckDuckGo work with no setup; Brave needs '
131
+ + 'a BRAVE_SEARCH_API_KEY in your environment',
96
132
  // Display full engine names; the stored config value stays the short id.
97
133
  values: SEARCH_PROVIDERS.map(p => SEARCH_PROVIDER_LABELS[p])
98
134
  },
99
135
  {
100
136
  id: 'requestTimeoutMs',
101
137
  label: 'command timeout',
102
- description: 'Cancel a single command that runs longer than this and remind the model to set '
103
- + 'its own timeout. Catches a local model that runs a command which never returns '
104
- + '(hung build, dev server, no-timeout check) so the run stops itself instead of '
105
- + 'waiting for a manual abort. One knob for both surfaces: the main session AND '
106
- + 'the verify/fix gate children. off disables it everywhere — gates can then hang '
107
- + 'unbounded',
138
+ description: 'Give up on any single command that runs this long, and tell the model to set its '
139
+ + 'own timeout next time. Stops a run from waiting forever on a dev server or a '
140
+ + 'hung build. Covers the main session and the checking steps; "off" lets a stuck '
141
+ + 'command hang the run until you notice',
108
142
  // Display human labels; the stored config value stays the ms number.
109
143
  values: COMMAND_TIMEOUT_OPTIONS.map(o => o.label)
110
144
  },
111
145
  {
112
146
  id: 'streamInactivityMs',
113
- label: 'stream watchdog',
114
- description: 'Abort and retry a model request whose stream goes SILENT for this long a hung '
115
- + 'or dropped stream reports no error at all, so nothing else catches it (mx5 run '
116
- + '14 lost ~2.9h to three of them while the model server stayed healthy). Counts '
117
- + 'time since the last stream event of any kind, so a slow model that keeps '
118
- + 'emitting tokens is never touched, and it pauses while a tool runs. Keep it '
119
- + 'generous on local backends: prompt processing on a large context legitimately '
120
- + 'emits nothing for minutes. off disables it on both the main session and children',
147
+ label: 'stuck reply retry',
148
+ description: 'Give up on a model reply that has sent nothing for this long and ask again. A '
149
+ + 'dropped connection looks exactly like a model thinking hard and reports no '
150
+ + 'error, so a run can sit dead for hours. Only total silence counts, and the clock '
151
+ + 'pauses while a command runs local models go quiet for minutes on long prompts, '
152
+ + 'so leave room',
121
153
  // Display human labels; the stored config value stays the ms number.
122
154
  values: STREAM_INACTIVITY_OPTIONS.map(o => o.label)
123
155
  },
124
156
  {
125
157
  id: 'yoloMode',
126
158
  label: 'yolo mode',
127
- description: 'UNATTENDED: auto-answer every question with the option pi already recommends, '
128
- + 'and show no prompts at all clarify/grill answers, the verify-FAIL picker '
129
- + '(auto-ACCEPT, recorded as a yolo debt), and the final-gate picker (autofix '
130
- + 'while the budget lasts, then leave the run FAILED). Every auto-pick is stamped '
131
- + '(YOLO) in the task file and debt ledger. For THROWAWAY/TEST projects you are '
132
- + 'not watching — a real run should decide these itself'
159
+ description: 'Stop asking you anything: every question takes the option pi recommends, a failed '
160
+ + 'check is accepted and written down as debt, and a failed final check is retried '
161
+ + 'until the budget runs out. Each auto-answer is marked (YOLO) in the task file. '
162
+ + 'For throwaway projects you are not watching'
133
163
  }
134
164
  ];
135
165
  /** Human label for the stored command-timeout ms (falls back to the raw ms). */
@@ -162,10 +192,10 @@ export function extensionItems(extensions, whitelist) {
162
192
  return extensions.map(e => ({
163
193
  id: EXT_ID_PREFIX + e.path,
164
194
  label: `ext: ${e.label}`,
165
- description: `Also load this extension (${e.origin}) in pi-task child sessions needed when it `
166
- + 'registers the model provider the children must use (e.g. pi-lmstudio). Children '
167
- + 'otherwise run with extensions off; whitelist only provider-type extensions you '
168
- + `trust, since children also get its tools and hooks. ${e.path}`,
195
+ description: `Load this ${e.origin} extension in the helper sessions pi-task spawns. They run `
196
+ + 'with extensions off by default, so turn this on when the extension provides the '
197
+ + 'model they need (pi-lmstudio, for example). They also inherit its tools and '
198
+ + `hooks, so only enable ones you trust. ${e.path}`,
169
199
  currentValue: whitelist.includes(e.path) ? 'on' : 'off',
170
200
  values: ['on', 'off']
171
201
  }));
@@ -175,15 +205,62 @@ export function applyExtensionToggle(whitelist, entryPath, on) {
175
205
  const rest = whitelist.filter(p => p !== entryPath);
176
206
  return on ? [...rest, entryPath] : rest;
177
207
  }
208
+ /** Overlay width; the list gets `- 4` of it, the description `- 4` again. */
209
+ const OVERLAY_WIDTH = 68;
210
+ /** Settings rows shown at once before the list scrolls. */
211
+ const MAX_VISIBLE = 9;
212
+ /**
213
+ * Tallest body the settings list can render, so {@link BorderedBox} can pad
214
+ * every frame to it and hold the border still. Mirrors SettingsList's own
215
+ * layout: one pad row, the visible rows, the scroll counter, a blank, the
216
+ * selected item's wrapped description, a blank, the key hint.
217
+ */
218
+ export function settingsBodyHeight(descriptions, maxVisible, wrapWidth) {
219
+ const tallestDescription = Math.max(0, ...descriptions.map(d => wrapTextWithAnsi(d, wrapWidth).length));
220
+ return 1 + maxVisible + 1 + 1 + tallestDescription + 1 + 1;
221
+ }
178
222
  function makeTheme(theme) {
179
223
  return {
180
- label: (text, selected) => (selected ? theme.fg('accent', text) : text),
181
- value: text => (text === 'on' ? theme.fg('success', text) : theme.fg('muted', text)),
224
+ label: (text, selected) => selected ? theme.fg('accent', theme.bold(text)) : theme.fg('text', text),
225
+ // A filled/hollow dot makes the on/off column scannable at a glance
226
+ // without reading a word on every row. Enum values (an engine name, a
227
+ // duration) are real content, so they stay readable rather than muted.
228
+ value: (text, selected) => {
229
+ if (text === 'on')
230
+ return theme.fg('success', '● on');
231
+ if (text === 'off')
232
+ return theme.fg('dim', '○ off');
233
+ return theme.fg(selected ? 'accent' : 'muted', text);
234
+ },
182
235
  description: text => theme.fg('muted', text),
183
- cursor: theme.fg('accent', '>'),
236
+ // Two cells wide to match the unselected row's " " indent — a
237
+ // single-cell cursor shifts the selected label one column left.
238
+ cursor: theme.fg('accent', '❯') + ' ',
184
239
  hint: text => theme.fg('dim', text)
185
240
  };
186
241
  }
242
+ /**
243
+ * Builds the framed settings panel. Split out of the command handler so the
244
+ * exact component the overlay shows can be rendered to a string in a test or a
245
+ * preview script, rather than only being inspectable by opening the TUI.
246
+ */
247
+ export function createSettingsPanel(items, theme, onChange, onCancel) {
248
+ const list = new SettingsList(items, MAX_VISIBLE, makeTheme(theme), onChange, onCancel);
249
+ return new BorderedBox(list, CONFIG_TITLE, s => theme.fg('borderMuted', s), s => theme.fg('accent', theme.bold(s)), settingsBodyHeight(items.map(i => i.description), MAX_VISIBLE, OVERLAY_WIDTH - 8));
250
+ }
251
+ /** The full settings row list for the current config, in menu order. */
252
+ export function panelItems(cfg, installed) {
253
+ return [
254
+ ...ITEMS.map(({ id, label, description, values }) => ({
255
+ id: id,
256
+ label,
257
+ description,
258
+ currentValue: displayValue(cfg, id, Boolean(values)),
259
+ values: values ?? ['on', 'off']
260
+ })),
261
+ ...extensionItems(installed, cfg.extensionWhitelist)
262
+ ];
263
+ }
187
264
  async function handleTaskConfig(_args, ctx) {
188
265
  const cfg = { ...getConfig(), extensionWhitelist: [...getConfig().extensionWhitelist] };
189
266
  // Enumerated live at open so an installed extension appears and an
@@ -199,50 +276,36 @@ async function handleTaskConfig(_args, ctx) {
199
276
  ctx.ui.notify(lines.join(' | '), 'info');
200
277
  return;
201
278
  }
202
- await ctx.ui.custom((_tui, theme, _kb, done) => {
203
- const listTheme = makeTheme(theme);
204
- const items = [
205
- ...ITEMS.map(({ id, label, description, values }) => ({
206
- id: id,
207
- label,
208
- description,
209
- currentValue: displayValue(cfg, id, Boolean(values)),
210
- values: values ?? ['on', 'off']
211
- })),
212
- ...extensionItems(installed, cfg.extensionWhitelist)
213
- ];
214
- const list = new SettingsList(items, 10, listTheme, (id, newValue) => {
215
- if (id.startsWith(EXT_ID_PREFIX)) {
216
- cfg.extensionWhitelist = applyExtensionToggle(cfg.extensionWhitelist, id.slice(EXT_ID_PREFIX.length), newValue === 'on');
217
- }
218
- else if (id === 'searchProvider') {
219
- const provider = providerForLabel(newValue);
220
- if (provider)
221
- cfg.searchProvider = provider;
222
- }
223
- else if (id === 'requestTimeoutMs') {
224
- const opt = COMMAND_TIMEOUT_OPTIONS.find(o => o.label === newValue);
225
- if (opt)
226
- cfg.requestTimeoutMs = opt.ms;
227
- }
228
- else if (id === 'streamInactivityMs') {
229
- const opt = STREAM_INACTIVITY_OPTIONS.find(o => o.label === newValue);
230
- if (opt)
231
- cfg.streamInactivityMs = opt.ms;
232
- }
233
- else {
234
- ;
235
- cfg[id] = newValue === 'on';
236
- }
237
- saveConfig(cfg).catch(() => { });
238
- }, () => done(undefined));
239
- return new BorderedBox(list, CONFIG_TITLE, s => theme.fg('borderMuted', s), s => theme.fg('accent', theme.bold(s)));
240
- }, { overlay: true, overlayOptions: { width: 58 } });
279
+ await ctx.ui.custom((_tui, theme, _kb, done) => createSettingsPanel(panelItems(cfg, installed), theme, (id, newValue) => {
280
+ if (id.startsWith(EXT_ID_PREFIX)) {
281
+ cfg.extensionWhitelist = applyExtensionToggle(cfg.extensionWhitelist, id.slice(EXT_ID_PREFIX.length), newValue === 'on');
282
+ }
283
+ else if (id === 'searchProvider') {
284
+ const provider = providerForLabel(newValue);
285
+ if (provider)
286
+ cfg.searchProvider = provider;
287
+ }
288
+ else if (id === 'requestTimeoutMs') {
289
+ const opt = COMMAND_TIMEOUT_OPTIONS.find(o => o.label === newValue);
290
+ if (opt)
291
+ cfg.requestTimeoutMs = opt.ms;
292
+ }
293
+ else if (id === 'streamInactivityMs') {
294
+ const opt = STREAM_INACTIVITY_OPTIONS.find(o => o.label === newValue);
295
+ if (opt)
296
+ cfg.streamInactivityMs = opt.ms;
297
+ }
298
+ else {
299
+ ;
300
+ cfg[id] = newValue === 'on';
301
+ }
302
+ saveConfig(cfg).catch(() => { });
303
+ }, () => done(undefined)), { overlay: true, overlayOptions: { width: OVERLAY_WIDTH } });
241
304
  }
242
305
  export function registerConfig(pi) {
243
306
  registerBridgeCommand(pi, 'task-config', {
244
- description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation, '
245
- + 'enforce guidelines, command timeout, extension whitelist for child sessions).',
307
+ description: 'Configure pi-task settings (remote control, auto-commit, verify work, enforce '
308
+ + 'guidelines, research, timeouts, extensions for helper sessions).',
246
309
  handler: handleTaskConfig
247
310
  });
248
311
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The installed pi-task version, read from package.json at runtime so nothing
3
+ * has to be regenerated on release. Two levels up holds for both src/<dir>
4
+ * (tests) and dist/<dir> (build), since tsc preserves the layout under rootDir.
5
+ * Falls back to '0.0.0' rather than throwing: every caller here is cosmetic
6
+ * (a User-Agent, a title bar) and none is worth failing over.
7
+ */
8
+ export declare function readPkgVersion(): string;
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * The installed pi-task version, read from package.json at runtime so nothing
6
+ * has to be regenerated on release. Two levels up holds for both src/<dir>
7
+ * (tests) and dist/<dir> (build), since tsc preserves the layout under rootDir.
8
+ * Falls back to '0.0.0' rather than throwing: every caller here is cosmetic
9
+ * (a User-Agent, a title bar) and none is worth failing over.
10
+ */
11
+ export function readPkgVersion() {
12
+ try {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8'));
15
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
16
+ }
17
+ catch {
18
+ return '0.0.0';
19
+ }
20
+ }
@@ -1,9 +1,7 @@
1
- import { readFileSync } from 'node:fs';
2
- import { fileURLToPath } from 'node:url';
3
- import { dirname, join } from 'node:path';
4
1
  import { parseHTML } from 'linkedom';
5
2
  import { Readability } from '@mozilla/readability';
6
3
  import TurndownService from 'turndown';
4
+ import { readPkgVersion } from '../shared/pkg-version.js';
7
5
  const turndown = new TurndownService({
8
6
  codeBlockStyle: 'fenced',
9
7
  headingStyle: 'atx',
@@ -33,21 +31,9 @@ export function cleanHtml(html, baseUrl) {
33
31
  }
34
32
  const DEFAULT_TIMEOUT_MS = 15_000;
35
33
  const DEFAULT_MAX_BYTES = 2 * 1024 * 1024; // 2 MB
34
+ // Read at runtime so the User-Agent never drifts out of sync with releases.
36
35
  const PKG_VERSION = readPkgVersion();
37
36
  const USER_AGENT = `pi-worker/${PKG_VERSION} (+https://npmjs.com/package/@mjasnikovs/pi-worker)`;
38
- // Read the version from package.json at runtime so the User-Agent never drifts
39
- // out of sync with releases. Two levels up holds for both src/workers (tests)
40
- // and dist/workers (build) since tsc preserves the layout under rootDir.
41
- function readPkgVersion() {
42
- try {
43
- const here = dirname(fileURLToPath(import.meta.url));
44
- const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8'));
45
- return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
46
- }
47
- catch {
48
- return '0.0.0';
49
- }
50
- }
51
37
  // Decide how to handle a response based on its content-type. HTML is run through
52
38
  // the readability/turndown pipeline; text-ish formats (markdown, plain text,
53
39
  // JSON, XML/feeds) are already clean and pass through verbatim; binary formats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.46",
3
+ "version": "0.18.48",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",