@mjasnikovs/pi-task 0.18.12 → 0.18.13

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.
@@ -30,6 +30,23 @@ export interface PiTaskConfig {
30
30
  * DEFAULT `exa` so search works out of the box with zero configuration.
31
31
  */
32
32
  searchProvider: SearchProvider;
33
+ /**
34
+ * Absolute entry-point paths of host pi extensions to load into every child
35
+ * pi session via explicit `-e` flags (GitHub issue #4: a provider registered
36
+ * by an extension — e.g. pi-lmstudio — otherwise doesn't exist in children,
37
+ * which then can't resolve the default model and demand OAuth/API keys).
38
+ * Children keep `--no-extensions`, so discovery stays off and ONLY these
39
+ * load — the whitelist is strictly additive. Entries whose file no longer
40
+ * exists (extension uninstalled) are skipped at spawn time, never fatal.
41
+ * DEFAULT empty: child isolation is unchanged until the user opts in via
42
+ * /task-config, which enumerates the currently installed extensions.
43
+ */
44
+ extensionWhitelist: string[];
33
45
  }
46
+ /**
47
+ * A hand-edited config can hold anything; keep only string entries so a stray
48
+ * object/number can't reach the child argv as `-e [object Object]`.
49
+ */
50
+ export declare function sanitizeExtensionWhitelist(value: unknown): string[];
34
51
  export declare function getConfig(): PiTaskConfig;
35
52
  export declare function saveConfig(config: PiTaskConfig): Promise<void>;
@@ -14,8 +14,18 @@ const DEFAULTS = {
14
14
  // ON: the F10 live A/B showed no answer-quality regression (fidelity 3/3, quality
15
15
  // 3/3, 0 collisions; ~14.5s of repeated docs lookups collapse to 0ms on a hit).
16
16
  researchCache: true,
17
- searchProvider: 'exa'
17
+ searchProvider: 'exa',
18
+ extensionWhitelist: []
18
19
  };
20
+ /**
21
+ * A hand-edited config can hold anything; keep only string entries so a stray
22
+ * object/number can't reach the child argv as `-e [object Object]`.
23
+ */
24
+ export function sanitizeExtensionWhitelist(value) {
25
+ if (!Array.isArray(value))
26
+ return [];
27
+ return value.filter((p) => typeof p === 'string' && p.trim().length > 0);
28
+ }
19
29
  const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
20
30
  const _g = globalThis;
21
31
  if (!_g.__piTaskConfig) {
@@ -32,6 +42,7 @@ if (!G.loaded) {
32
42
  // into the dispatch switch — fall back to the default.
33
43
  if (!isSearchProvider(parsed.searchProvider))
34
44
  delete parsed.searchProvider;
45
+ parsed.extensionWhitelist = sanitizeExtensionWhitelist(parsed.extensionWhitelist);
35
46
  G.config = { ...DEFAULTS, ...parsed };
36
47
  }
37
48
  catch {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Enumerates the host's installed pi extensions for the /task-config whitelist
3
+ * (GitHub issue #4) using pi's OWN resolver — the same DefaultPackageManager
4
+ * the host runtime discovers extensions with — so the menu always mirrors what
5
+ * `pi list`/discovery would load: install a package and it appears, uninstall
6
+ * it and it vanishes (a stale whitelist entry is then skipped at spawn time by
7
+ * extensionArgs' existence filter, never fatal).
8
+ */
9
+ export interface InstalledExtension {
10
+ /** Absolute entry-point path — the whitelist identity AND the `-e` value. */
11
+ path: string;
12
+ /** Short display name: package name for package sources, file stem for loose files. */
13
+ label: string;
14
+ /** Provenance shown in the menu, e.g. "npm:pi-lmstudio (user)" or "discovered (user)". */
15
+ origin: string;
16
+ }
17
+ /**
18
+ * pi-task's own package root. Anything resolve() reports under it is pi-task
19
+ * itself (`dist/index.js` shows up as a discovered extension) and MUST be
20
+ * excluded: whitelisting pi-task into its own children would recursively boot
21
+ * the remote server and re-register every command in each child.
22
+ */
23
+ export declare function selfPackageRoot(fromDir?: string): string;
24
+ /** Display label for a resolved extension entry. */
25
+ export declare function extensionLabel(entryPath: string, source: string): string;
26
+ export interface ListInstalledOptions {
27
+ cwd: string;
28
+ /** Override for tests; defaults to the host's real agent dir (~/.pi/agent). */
29
+ agentDir?: string;
30
+ /** Override for tests; defaults to pi-task's own package root. */
31
+ selfRoot?: string;
32
+ }
33
+ /**
34
+ * List every extension the host pi would load, excluding pi-task itself.
35
+ * Only enabled entries are offered: an extension the user disabled in
36
+ * `pi config` shouldn't be sneaked back in through a child whitelist.
37
+ * Errors (unreadable settings, missing package dirs) surface as an empty
38
+ * list at the caller — the menu then simply shows no extension toggles.
39
+ */
40
+ export declare function listInstalledExtensions(opts: ListInstalledOptions): Promise<InstalledExtension[]>;
@@ -0,0 +1,66 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { DefaultPackageManager, SettingsManager, getAgentDir } from '@earendil-works/pi-coding-agent';
5
+ /**
6
+ * pi-task's own package root. Anything resolve() reports under it is pi-task
7
+ * itself (`dist/index.js` shows up as a discovered extension) and MUST be
8
+ * excluded: whitelisting pi-task into its own children would recursively boot
9
+ * the remote server and re-register every command in each child.
10
+ */
11
+ export function selfPackageRoot(fromDir) {
12
+ let dir = fromDir ?? path.dirname(fileURLToPath(import.meta.url));
13
+ for (;;) {
14
+ if (fs.existsSync(path.join(dir, 'package.json')))
15
+ return dir;
16
+ const parent = path.dirname(dir);
17
+ if (parent === dir)
18
+ return dir;
19
+ dir = parent;
20
+ }
21
+ }
22
+ function isUnder(p, root) {
23
+ const rel = path.relative(root, p);
24
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
25
+ }
26
+ /** Display label for a resolved extension entry. */
27
+ export function extensionLabel(entryPath, source) {
28
+ // Package sources keep their identity ("npm:pi-lmstudio" → "pi-lmstudio");
29
+ // discovered/local files read best as the file or directory stem.
30
+ if (source.includes(':'))
31
+ return source.slice(source.indexOf(':') + 1);
32
+ const base = path.basename(entryPath).replace(/\.(ts|js|mts|mjs|cts|cjs)$/, '');
33
+ return base === 'index' ? path.basename(path.dirname(entryPath)) : base;
34
+ }
35
+ /**
36
+ * List every extension the host pi would load, excluding pi-task itself.
37
+ * Only enabled entries are offered: an extension the user disabled in
38
+ * `pi config` shouldn't be sneaked back in through a child whitelist.
39
+ * Errors (unreadable settings, missing package dirs) surface as an empty
40
+ * list at the caller — the menu then simply shows no extension toggles.
41
+ */
42
+ export async function listInstalledExtensions(opts) {
43
+ const agentDir = opts.agentDir ?? getAgentDir();
44
+ const selfRoot = opts.selfRoot ?? selfPackageRoot();
45
+ const settingsManager = SettingsManager.create(opts.cwd, agentDir);
46
+ const pm = new DefaultPackageManager({ cwd: opts.cwd, agentDir, settingsManager });
47
+ // "skip": a package in settings whose install dir is missing must not
48
+ // trigger an interactive install prompt from inside a config menu.
49
+ const resolved = await pm.resolve(() => Promise.resolve('skip'));
50
+ const out = [];
51
+ for (const r of resolved.extensions) {
52
+ if (!r.enabled)
53
+ continue;
54
+ const abs = path.resolve(r.path);
55
+ if (isUnder(abs, selfRoot))
56
+ continue;
57
+ out.push({
58
+ path: abs,
59
+ label: extensionLabel(abs, r.metadata.source),
60
+ origin: r.metadata.source === 'auto' ?
61
+ `discovered (${r.metadata.scope})`
62
+ : `${r.metadata.source} (${r.metadata.scope})`
63
+ });
64
+ }
65
+ return out;
66
+ }
@@ -1,2 +1,12 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { type InstalledExtension } from './extension-list.js';
3
+ export declare function extensionItems(extensions: InstalledExtension[], whitelist: readonly string[]): {
4
+ id: string;
5
+ label: string;
6
+ description: string;
7
+ currentValue: string;
8
+ values: string[];
9
+ }[];
10
+ /** Apply an extension toggle to the config's whitelist (idempotent both ways). */
11
+ export declare function applyExtensionToggle(whitelist: readonly string[], entryPath: string, on: boolean): string[];
2
12
  export declare function registerConfig(pi: ExtensionAPI): void;
@@ -2,6 +2,7 @@ import { SettingsList, visibleWidth } from '@earendil-works/pi-tui';
2
2
  import { registerBridgeCommand } from '../remote/bridge.js';
3
3
  import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
4
4
  import { getConfig, saveConfig } from './config.js';
5
+ import { listInstalledExtensions } from './extension-list.js';
5
6
  const CONFIG_TITLE = 'pi-task settings';
6
7
  /**
7
8
  * Frames a child component (the settings list) in a rounded border with a title
@@ -104,6 +105,29 @@ function displayValue(cfg, id, isEnum) {
104
105
  return String(cfg[id]);
105
106
  return cfg[id] ? 'on' : 'off';
106
107
  }
108
+ /**
109
+ * One /task-config toggle per installed host extension (GitHub issue #4).
110
+ * The id carries the entry path behind a prefix so the shared onChange handler
111
+ * can tell an extension toggle from a PiTaskConfig field.
112
+ */
113
+ const EXT_ID_PREFIX = 'ext:';
114
+ export function extensionItems(extensions, whitelist) {
115
+ return extensions.map(e => ({
116
+ id: EXT_ID_PREFIX + e.path,
117
+ label: `ext: ${e.label}`,
118
+ description: `Also load this extension (${e.origin}) in pi-task child sessions — needed when it `
119
+ + 'registers the model provider the children must use (e.g. pi-lmstudio). Children '
120
+ + 'otherwise run with extensions off; whitelist only provider-type extensions you '
121
+ + `trust, since children also get its tools and hooks. ${e.path}`,
122
+ currentValue: whitelist.includes(e.path) ? 'on' : 'off',
123
+ values: ['on', 'off']
124
+ }));
125
+ }
126
+ /** Apply an extension toggle to the config's whitelist (idempotent both ways). */
127
+ export function applyExtensionToggle(whitelist, entryPath, on) {
128
+ const rest = whitelist.filter(p => p !== entryPath);
129
+ return on ? [...rest, entryPath] : rest;
130
+ }
107
131
  function makeTheme(theme) {
108
132
  return {
109
133
  label: (text, selected) => (selected ? theme.fg('accent', text) : text),
@@ -114,23 +138,37 @@ function makeTheme(theme) {
114
138
  };
115
139
  }
116
140
  async function handleTaskConfig(_args, ctx) {
117
- const cfg = { ...getConfig() };
141
+ const cfg = { ...getConfig(), extensionWhitelist: [...getConfig().extensionWhitelist] };
142
+ // Enumerated live at open so an installed extension appears and an
143
+ // uninstalled one vanishes without pi-task doing any bookkeeping. A failed
144
+ // enumeration only costs the extension toggles, never the whole menu.
145
+ const installed = await listInstalledExtensions({ cwd: ctx.cwd }).catch(() => []);
118
146
  if (ctx.mode !== 'tui') {
119
147
  const lines = ITEMS.map(({ id, label, values }) => `${label.padEnd(22)} ${displayValue(cfg, id, Boolean(values))}`);
148
+ for (const e of installed) {
149
+ const state = cfg.extensionWhitelist.includes(e.path) ? 'on' : 'off';
150
+ lines.push(`${('ext: ' + e.label).padEnd(22)} ${state}`);
151
+ }
120
152
  ctx.ui.notify(lines.join(' | '), 'info');
121
153
  return;
122
154
  }
123
155
  await ctx.ui.custom((_tui, theme, _kb, done) => {
124
156
  const listTheme = makeTheme(theme);
125
- const items = ITEMS.map(({ id, label, description, values }) => ({
126
- id,
127
- label,
128
- description,
129
- currentValue: displayValue(cfg, id, Boolean(values)),
130
- values: values ?? ['on', 'off']
131
- }));
157
+ const items = [
158
+ ...ITEMS.map(({ id, label, description, values }) => ({
159
+ id: id,
160
+ label,
161
+ description,
162
+ currentValue: displayValue(cfg, id, Boolean(values)),
163
+ values: values ?? ['on', 'off']
164
+ })),
165
+ ...extensionItems(installed, cfg.extensionWhitelist)
166
+ ];
132
167
  const list = new SettingsList(items, 10, listTheme, (id, newValue) => {
133
- if (id === 'searchProvider') {
168
+ if (id.startsWith(EXT_ID_PREFIX)) {
169
+ cfg.extensionWhitelist = applyExtensionToggle(cfg.extensionWhitelist, id.slice(EXT_ID_PREFIX.length), newValue === 'on');
170
+ }
171
+ else if (id === 'searchProvider') {
134
172
  const provider = providerForLabel(newValue);
135
173
  if (provider)
136
174
  cfg.searchProvider = provider;
@@ -146,7 +184,8 @@ async function handleTaskConfig(_args, ctx) {
146
184
  }
147
185
  export function registerConfig(pi) {
148
186
  registerBridgeCommand(pi, 'task-config', {
149
- description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation, enforce guidelines).',
187
+ description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation, '
188
+ + 'enforce guidelines, extension whitelist for child sessions).',
150
189
  handler: handleTaskConfig
151
190
  });
152
191
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Extension whitelist → child argv (GitHub issue #4).
3
+ *
4
+ * Child pi sessions run with `--no-extensions`, so a provider registered by a
5
+ * host extension (e.g. pi-lmstudio) doesn't exist in them — the child can't
6
+ * resolve the default model and demands OAuth/API keys. pi's `--no-extensions`
7
+ * explicitly preserves explicit `-e <path>` loads (only discovery is disabled),
8
+ * so injecting the user's whitelisted entry paths restores the provider while
9
+ * keeping every non-whitelisted extension out. Same mechanism pi-task already
10
+ * uses for its own worker-tool extensions (see runWorker's `extensions` input).
11
+ */
12
+ /**
13
+ * Map whitelisted entry paths to `-e` argv pairs. Pure given `exists`.
14
+ *
15
+ * A path whose file is gone (extension uninstalled since it was whitelisted)
16
+ * is skipped silently: pi itself would record "Extension path does not exist"
17
+ * as a load ERROR, and a stale toggle must never break every child spawn.
18
+ * Duplicates collapse so a path can't be loaded twice.
19
+ */
20
+ export declare function extensionArgs(paths: readonly string[], exists?: (p: string) => boolean): string[];
21
+ /**
22
+ * The base argv every child pi invocation starts from: internal worker
23
+ * extensions (verbatim — they always exist, shipped in dist), then the user's
24
+ * whitelisted extensions (existence-filtered, deduped against the internal
25
+ * ones), then CHILD_BASE_ARGS. Computed per spawn, not at module load, so a
26
+ * /task-config toggle takes effect for the next child without a restart.
27
+ */
28
+ export declare function childBaseArgs(internalExtensions?: readonly string[]): string[];
@@ -0,0 +1,47 @@
1
+ import * as fs from 'node:fs';
2
+ import { CHILD_BASE_ARGS } from './child-process.js';
3
+ import { getConfig } from '../config/config.js';
4
+ /**
5
+ * Extension whitelist → child argv (GitHub issue #4).
6
+ *
7
+ * Child pi sessions run with `--no-extensions`, so a provider registered by a
8
+ * host extension (e.g. pi-lmstudio) doesn't exist in them — the child can't
9
+ * resolve the default model and demands OAuth/API keys. pi's `--no-extensions`
10
+ * explicitly preserves explicit `-e <path>` loads (only discovery is disabled),
11
+ * so injecting the user's whitelisted entry paths restores the provider while
12
+ * keeping every non-whitelisted extension out. Same mechanism pi-task already
13
+ * uses for its own worker-tool extensions (see runWorker's `extensions` input).
14
+ */
15
+ /**
16
+ * Map whitelisted entry paths to `-e` argv pairs. Pure given `exists`.
17
+ *
18
+ * A path whose file is gone (extension uninstalled since it was whitelisted)
19
+ * is skipped silently: pi itself would record "Extension path does not exist"
20
+ * as a load ERROR, and a stale toggle must never break every child spawn.
21
+ * Duplicates collapse so a path can't be loaded twice.
22
+ */
23
+ export function extensionArgs(paths, exists = fs.existsSync) {
24
+ const seen = new Set();
25
+ const args = [];
26
+ for (const p of paths) {
27
+ if (typeof p !== 'string' || p.trim().length === 0 || seen.has(p))
28
+ continue;
29
+ seen.add(p);
30
+ if (!exists(p))
31
+ continue;
32
+ args.push('-e', p);
33
+ }
34
+ return args;
35
+ }
36
+ /**
37
+ * The base argv every child pi invocation starts from: internal worker
38
+ * extensions (verbatim — they always exist, shipped in dist), then the user's
39
+ * whitelisted extensions (existence-filtered, deduped against the internal
40
+ * ones), then CHILD_BASE_ARGS. Computed per spawn, not at module load, so a
41
+ * /task-config toggle takes effect for the next child without a restart.
42
+ */
43
+ export function childBaseArgs(internalExtensions = []) {
44
+ const internal = internalExtensions.flatMap(e => ['-e', e]);
45
+ const whitelisted = extensionArgs(getConfig().extensionWhitelist.filter(p => !internalExtensions.includes(p)));
46
+ return [...internal, ...whitelisted, ...CHILD_BASE_ARGS];
47
+ }
@@ -7,7 +7,8 @@
7
7
  */
8
8
  import { spawn } from 'node:child_process';
9
9
  import { getPiInvocation } from '../shared/pi-invocation.js';
10
- import { runChild as runChildUnified, CHILD_BASE_ARGS } from '../shared/child-process.js';
10
+ import { runChild as runChildUnified } from '../shared/child-process.js';
11
+ import { childBaseArgs } from '../shared/child-extensions.js';
11
12
  import { LoopDetector } from './loop-detector.js';
12
13
  import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../shared/leaked-tool-call.js';
13
14
  import { readSection, setTaskSection } from './task-io.js';
@@ -60,7 +61,7 @@ export function childArgs(tools) {
60
61
  // runChild below / getPiInvocation), so a large inlined-design prompt can't
61
62
  // overflow the OS command-line limit (Windows `spawn ENAMETOOLONG`).
62
63
  const toolFlags = tools === '' ? ['--no-tools'] : ['--tools', tools];
63
- return [...CHILD_BASE_ARGS, '--mode', 'json', ...toolFlags];
64
+ return [...childBaseArgs(), '--mode', 'json', ...toolFlags];
64
65
  }
65
66
  // Sentinel error thrown when the user dismisses a grill-me dialog.
66
67
  // Defined here (not in failure-classifier.ts) to avoid circular dependency.
@@ -8,7 +8,8 @@ import { resolvePackage as defaultResolvePackage, ResolveError, detectTypesRedir
8
8
  import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
9
9
  import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
10
10
  import { getPiInvocation } from '../shared/pi-invocation.js';
11
- import { CHILD_BASE_ARGS, runChild } from '../shared/child-process.js';
11
+ import { runChild } from '../shared/child-process.js';
12
+ import { childBaseArgs } from '../shared/child-extensions.js';
12
13
  import { parseChildOutput, isExcerptInContent, formatResultText as formatResultTextShared } from '../shared/child-output.js';
13
14
  const DEFAULT_LIMIT = 8;
14
15
  const DEFAULT_BUDGET = 24_000;
@@ -16,7 +17,7 @@ const NO_CACHE_HEAD = 25_000;
16
17
  const NO_CACHE_TAIL = 5_000;
17
18
  const NO_CACHE_TOTAL = NO_CACHE_HEAD + NO_CACHE_TAIL;
18
19
  const NO_CACHE_MARKER = '\n\n[...content continues, truncated...]\n\n';
19
- const CHILD_ARGS = [...CHILD_BASE_ARGS, '--no-tools'];
20
+ const childArgs = () => [...childBaseArgs(), '--no-tools'];
20
21
  export function extractParentPackage(moduleName) {
21
22
  if (moduleName.startsWith('@')) {
22
23
  const parts = moduleName.split('/');
@@ -423,7 +424,7 @@ export async function docsFocused(input) {
423
424
  const { pkg, chunks, hitCache, indexingMs } = rawResult;
424
425
  const concatenated = chunks.map(c => c.content).join('\n\n');
425
426
  const prompt = buildPrompt(pkg, input.query, concatenated);
426
- const invocation = getPiInvocation([...CHILD_ARGS], prompt);
427
+ const invocation = getPiInvocation(childArgs(), prompt);
427
428
  const child = await runChild(spawn, invocation, input.cwd, input.signal);
428
429
  const parsed = parseChildOutput(child.stdout);
429
430
  const excerptVerified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
@@ -1,13 +1,14 @@
1
1
  import { spawn as defaultSpawn } from 'node:child_process';
2
2
  import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
3
3
  import { getPiInvocation } from '../shared/pi-invocation.js';
4
- import { CHILD_BASE_ARGS, runChild } from '../shared/child-process.js';
4
+ import { runChild } from '../shared/child-process.js';
5
+ import { childBaseArgs } from '../shared/child-extensions.js';
5
6
  import { parseChildOutput, isExcerptInContent, formatResultText as formatResultTextShared } from '../shared/child-output.js';
6
7
  const CONTENT_BUDGET = 30_000;
7
8
  const HEAD_CHARS = 25_000;
8
9
  const TAIL_CHARS = 5_000;
9
10
  const TRUNCATION_MARKER = '\n\n[...page continues, truncated...]\n\n';
10
- const CHILD_ARGS = [...CHILD_BASE_ARGS, '--no-tools'];
11
+ const childArgs = () => [...childBaseArgs(), '--no-tools'];
11
12
  export async function fetchRaw(input) {
12
13
  const fetchAndCleanFn = input.fetchAndClean ?? defaultFetchAndClean;
13
14
  const cleaned = await fetchAndCleanFn(input.url, { signal: input.signal });
@@ -24,7 +25,7 @@ export async function fetchFocused(input) {
24
25
  title: cleaned.title,
25
26
  content: truncated
26
27
  });
27
- const invocation = getPiInvocation([...CHILD_ARGS], prompt);
28
+ const invocation = getPiInvocation(childArgs(), prompt);
28
29
  const childResult = await runChild(spawnFn, invocation, input.cwd, input.signal);
29
30
  if (childResult.aborted) {
30
31
  return {
@@ -7,7 +7,7 @@ export interface RunWorkerInput {
7
7
  spawn?: SpawnFn;
8
8
  /** Comma-separated tool whitelist passed to `pi --tools`. Defaults to read,grep,find,ls. */
9
9
  tools?: string;
10
- /** Extension entry-point paths to load via `-e <path>` before CHILD_BASE_ARGS. */
10
+ /** Internal extension entry-point paths to load via `-e <path>` (see childBaseArgs). */
11
11
  extensions?: string[];
12
12
  /** Called for each tool execution start and text-writing event inside the worker. */
13
13
  onLine?: (line: string) => void;
@@ -1,5 +1,6 @@
1
1
  import { getPiInvocation } from '../shared/pi-invocation.js';
2
- import { CHILD_BASE_ARGS, runChildDefault } from '../shared/child-process.js';
2
+ import { runChildDefault } from '../shared/child-process.js';
3
+ import { childBaseArgs } from '../shared/child-extensions.js';
3
4
  import { LoopDetector } from '../task/loop-detector.js';
4
5
  import { LOOP_WINDOW, LOOP_THRESHOLD, MAX_LOOP_RESTARTS, formatLoopHint } from '../task/child-runner.js';
5
6
  import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../shared/leaked-tool-call.js';
@@ -70,8 +71,7 @@ function workerTimeout(external, ms) {
70
71
  }
71
72
  export async function runWorker(input) {
72
73
  const tools = input.tools ?? DEFAULT_TOOLS;
73
- const extensionArgs = (input.extensions ?? []).flatMap(e => ['-e', e]);
74
- const baseArgs = [...extensionArgs, ...CHILD_BASE_ARGS, '--mode', 'json', '--tools', tools];
74
+ const baseArgs = [...childBaseArgs(input.extensions ?? []), '--mode', 'json', '--tools', tools];
75
75
  const timeoutMs = input.timeoutMs ?? RESEARCH_WORKER_TIMEOUT_MS;
76
76
  let hint = null;
77
77
  // Loop-kill and timeout share one restart budget, mirroring
@@ -4,13 +4,14 @@ import { openCache as defaultOpenCache } from './docs-cache.js';
4
4
  import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
5
5
  import { docsRaw, formatResultText, buildPrompt, buildVersionBanner } from './docs-core.js';
6
6
  import { formatNpmVersionSection } from './npm-version.js';
7
- import { runChild, CHILD_BASE_ARGS } from '../shared/child-process.js';
7
+ import { runChild } from '../shared/child-process.js';
8
+ import { childBaseArgs } from '../shared/child-extensions.js';
8
9
  import { parseChildOutput, isExcerptInContent } from '../shared/child-output.js';
9
10
  import { getPiInvocation } from '../shared/pi-invocation.js';
10
11
  import { formatChildFailure, makeWorkerTool } from './shared.js';
11
12
  import { normalizeQuery } from './research-cache.js';
12
13
  import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
13
- const CHILD_ARGS = [...CHILD_BASE_ARGS, '--no-tools'];
14
+ const childArgs = () => [...childBaseArgs(), '--no-tools'];
14
15
  const RENDER_QUERY_MAX = 100;
15
16
  const Params = Type.Object({
16
17
  module: Type.String({
@@ -105,7 +106,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
105
106
  };
106
107
  const concatenated = chunks.map(c => c.content).join('\n\n');
107
108
  const prompt = buildProjectPrompt(projectName, params.query, concatenated);
108
- const invocation = getPiInvocation([...CHILD_ARGS], prompt);
109
+ const invocation = getPiInvocation(childArgs(), prompt);
109
110
  const child = await runChild(spawn, invocation, ctx.cwd, signal);
110
111
  const failure = formatChildFailure(child, 'Project docs lookup aborted.');
111
112
  if (failure !== null) {
@@ -207,7 +208,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
207
208
  };
208
209
  const concatenated = chunks.map(c => c.content).join('\n\n');
209
210
  const prompt = buildPrompt(pkg, params.query, concatenated);
210
- const invocation = getPiInvocation([...CHILD_ARGS], prompt);
211
+ const invocation = getPiInvocation(childArgs(), prompt);
211
212
  const child = await runChild(spawn, invocation, ctx.cwd, signal);
212
213
  const failure = formatChildFailure(child, 'Docs lookup aborted.');
213
214
  if (failure !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.12",
3
+ "version": "0.18.13",
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",