@mjasnikovs/pi-task 0.2.0

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +6 -0
  5. package/dist/shared/child-output.d.ts +21 -0
  6. package/dist/shared/child-output.js +40 -0
  7. package/dist/shared/child-process.d.ts +71 -0
  8. package/dist/shared/child-process.js +190 -0
  9. package/dist/shared/pi-invocation.d.ts +7 -0
  10. package/dist/shared/pi-invocation.js +24 -0
  11. package/dist/task/child-runner.d.ts +66 -0
  12. package/dist/task/child-runner.js +157 -0
  13. package/dist/task/enrichment.d.ts +12 -0
  14. package/dist/task/enrichment.js +82 -0
  15. package/dist/task/failure-classifier.d.ts +15 -0
  16. package/dist/task/failure-classifier.js +63 -0
  17. package/dist/task/file-inventory.d.ts +9 -0
  18. package/dist/task/file-inventory.js +44 -0
  19. package/dist/task/loop-detector.d.ts +32 -0
  20. package/dist/task/loop-detector.js +46 -0
  21. package/dist/task/orchestrator.d.ts +54 -0
  22. package/dist/task/orchestrator.js +387 -0
  23. package/dist/task/parsers.d.ts +32 -0
  24. package/dist/task/parsers.js +172 -0
  25. package/dist/task/phases.d.ts +56 -0
  26. package/dist/task/phases.js +477 -0
  27. package/dist/task/prompts.d.ts +21 -0
  28. package/dist/task/prompts.js +346 -0
  29. package/dist/task/service-blocks.d.ts +3 -0
  30. package/dist/task/service-blocks.js +10 -0
  31. package/dist/task/task-file.d.ts +14 -0
  32. package/dist/task/task-file.js +15 -0
  33. package/dist/task/task-io.d.ts +19 -0
  34. package/dist/task/task-io.js +78 -0
  35. package/dist/task/task-parsers.d.ts +12 -0
  36. package/dist/task/task-parsers.js +75 -0
  37. package/dist/task/task-types.d.ts +21 -0
  38. package/dist/task/task-types.js +18 -0
  39. package/dist/task/timings.d.ts +18 -0
  40. package/dist/task/timings.js +36 -0
  41. package/dist/task/widget.d.ts +39 -0
  42. package/dist/task/widget.js +122 -0
  43. package/dist/workers/brave-search.d.ts +17 -0
  44. package/dist/workers/brave-search.js +77 -0
  45. package/dist/workers/docs-cache.d.ts +16 -0
  46. package/dist/workers/docs-cache.js +66 -0
  47. package/dist/workers/docs-core.d.ts +86 -0
  48. package/dist/workers/docs-core.js +329 -0
  49. package/dist/workers/docs-index.d.ts +9 -0
  50. package/dist/workers/docs-index.js +200 -0
  51. package/dist/workers/docs-resolve.d.ts +12 -0
  52. package/dist/workers/docs-resolve.js +126 -0
  53. package/dist/workers/docs-retrieve.d.ts +15 -0
  54. package/dist/workers/docs-retrieve.js +91 -0
  55. package/dist/workers/fetch-core.d.ts +35 -0
  56. package/dist/workers/fetch-core.js +91 -0
  57. package/dist/workers/html-clean.d.ts +17 -0
  58. package/dist/workers/html-clean.js +142 -0
  59. package/dist/workers/index.d.ts +2 -0
  60. package/dist/workers/index.js +10 -0
  61. package/dist/workers/npm-version.d.ts +32 -0
  62. package/dist/workers/npm-version.js +102 -0
  63. package/dist/workers/pi-worker-core.d.ts +28 -0
  64. package/dist/workers/pi-worker-core.js +29 -0
  65. package/dist/workers/pi-worker-docs.d.ts +16 -0
  66. package/dist/workers/pi-worker-docs.js +143 -0
  67. package/dist/workers/pi-worker-fetch.d.ts +20 -0
  68. package/dist/workers/pi-worker-fetch.js +72 -0
  69. package/dist/workers/pi-worker-search.d.ts +7 -0
  70. package/dist/workers/pi-worker-search.js +55 -0
  71. package/dist/workers/pi-worker.d.ts +10 -0
  72. package/dist/workers/pi-worker.js +61 -0
  73. package/dist/workers/search-core.d.ts +19 -0
  74. package/dist/workers/search-core.js +35 -0
  75. package/dist/workers/shared.d.ts +3 -0
  76. package/dist/workers/shared.js +4 -0
  77. package/package.json +50 -0
@@ -0,0 +1,143 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Text } from '@earendil-works/pi-tui';
3
+ import { docsRaw, formatResultText, buildPrompt } from './docs-core.js';
4
+ import { formatNpmVersionSection } from './npm-version.js';
5
+ import { runChild, CHILD_BASE_ARGS } from '../shared/child-process.js';
6
+ import { parseChildOutput, isExcerptInContent } from '../shared/child-output.js';
7
+ import { getPiInvocation } from '../shared/pi-invocation.js';
8
+ import { textResult } from './shared.js';
9
+ const CHILD_ARGS = [...CHILD_BASE_ARGS, '--no-tools'];
10
+ const RENDER_QUERY_MAX = 100;
11
+ const Params = Type.Object({
12
+ module: Type.String({
13
+ description: 'Bare npm module name (e.g. "zod", "@scope/name", "react/jsx-runtime"). Must be installed in the project\'s node_modules.'
14
+ }),
15
+ query: Type.String({
16
+ description: 'What to extract from the module\'s docs. The child pi reads ranked chunks and returns ONLY content answering this.'
17
+ })
18
+ });
19
+ export function registerPiWorkerDocs(pi, internals = {}) {
20
+ pi.registerTool({
21
+ name: 'pi-worker-docs',
22
+ label: 'Pi Worker Docs',
23
+ description: 'Look up an npm package locally and return a focused, version-pinned '
24
+ + 'answer extracted from its .d.ts types and README. No network after install (except a small '
25
+ + 'live npm registry call described below). The cache lives at ~/.cache/pi-worker/docs.sqlite '
26
+ + 'and is keyed by exact installed version.\n'
27
+ + 'If the package is not yet installed, it is installed automatically via bun add or npm install.\n'
28
+ + '\n'
29
+ + 'Live registry data: in addition to the docs answer, the tool fetches the latest published '
30
+ + 'version, recent versions, and publish date from the npm registry and prepends them to the '
31
+ + 'output (and exposes them in details). This is the source of truth for "what is the latest '
32
+ + 'version of X" questions — training-data versions are typically months stale. The lookup is '
33
+ + 'best-effort and silently absent when offline.\n'
34
+ + '\n'
35
+ + 'Good fits:\n'
36
+ + '- "What does library X export?" / "How does function Y work?"\n'
37
+ + '- Confirming generic shapes, overload sets, exported types\n'
38
+ + '- Pulling README configuration prose without burning context on raw markdown\n'
39
+ + '- Checking the current latest published version of a package\n'
40
+ + '\n'
41
+ + 'Skip when:\n'
42
+ + '- You need docs for a specific newer version than what is installed — use pi-worker-fetch on the upstream docs site',
43
+ parameters: Params,
44
+ executionMode: 'parallel',
45
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
46
+ const spawn = internals.spawn
47
+ ?? (globalThis.Bun !== undefined ?
48
+ globalThis.Bun.spawn
49
+ : (await import('node:child_process')).spawn);
50
+ const rawResult = await docsRaw({
51
+ pkg: params.module,
52
+ query: params.query,
53
+ cwd: ctx.cwd,
54
+ resolvePackage: internals.resolvePackage,
55
+ ensureIndexed: internals.ensureIndexed,
56
+ retrieveChunks: internals.retrieveChunks,
57
+ openCache: internals.openCache,
58
+ spawn,
59
+ npmVersionLookup: internals.npmVersionLookup,
60
+ signal
61
+ });
62
+ const npmHeader = rawResult.npmVersion ? `${formatNpmVersionSection(rawResult.npmVersion)}\n\n` : '';
63
+ const npmDetails = rawResult.npmVersion ?
64
+ {
65
+ npmLatest: rawResult.npmVersion.latest,
66
+ npmPublishedAt: rawResult.npmVersion.publishedAt
67
+ }
68
+ : {};
69
+ if (rawResult.kind === 'error') {
70
+ const details = {
71
+ resolveError: rawResult.resolveError,
72
+ installError: rawResult.installError,
73
+ version: rawResult.version,
74
+ hitCache: rawResult.hitCache,
75
+ cacheError: rawResult.cacheError,
76
+ autoInstalled: rawResult.autoInstalled,
77
+ ...npmDetails
78
+ };
79
+ return textResult(npmHeader + rawResult.message, details);
80
+ }
81
+ if (rawResult.kind === 'not_installed') {
82
+ return textResult(npmHeader
83
+ + `Package "${rawResult.pkg}" is not installed and auto-install failed.`, { resolveError: 'not_installed', ...npmDetails });
84
+ }
85
+ if (rawResult.kind === 'no_chunks') {
86
+ return textResult(npmHeader
87
+ + `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no .d.ts files or README. Use pi-worker to read source directly.`, {
88
+ version: rawResult.pkg.version,
89
+ hitCache: rawResult.hitCache,
90
+ indexedFiles: rawResult.indexedFiles ?? 0,
91
+ cacheError: rawResult.cacheError,
92
+ autoInstalled: rawResult.autoInstalled,
93
+ ...npmDetails
94
+ });
95
+ }
96
+ // kind === 'ok'
97
+ const { pkg, chunks, hitCache, indexingMs, cacheError, autoInstalled } = rawResult;
98
+ const baseDetails = {
99
+ version: pkg.version,
100
+ hitCache,
101
+ chunksRetrieved: chunks.length,
102
+ indexingMs,
103
+ cacheError,
104
+ autoInstalled,
105
+ ...npmDetails
106
+ };
107
+ const concatenated = chunks.map(c => c.content).join('\n\n');
108
+ const prompt = buildPrompt(pkg, params.query, concatenated);
109
+ const invocation = getPiInvocation([...CHILD_ARGS, prompt]);
110
+ const child = await runChild(spawn, invocation, ctx.cwd, signal);
111
+ if (child.aborted) {
112
+ return textResult(npmHeader + 'Docs lookup aborted.', {
113
+ ...baseDetails,
114
+ aborted: true,
115
+ childExitCode: child.exitCode
116
+ });
117
+ }
118
+ if (child.exitCode !== 0) {
119
+ const tail = child.stderr.trim().slice(-500) || '(no stderr)';
120
+ return textResult(npmHeader + `Worker exited ${child.exitCode}.\n${tail}`, {
121
+ ...baseDetails,
122
+ childExitCode: child.exitCode
123
+ });
124
+ }
125
+ const parsed = parseChildOutput(child.stdout);
126
+ const verified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
127
+ const text = npmHeader + formatResultText(pkg, parsed, verified);
128
+ return textResult(text, {
129
+ ...baseDetails,
130
+ childExitCode: 0,
131
+ excerptVerified: verified
132
+ });
133
+ },
134
+ renderCall(args, theme) {
135
+ const query = args.query.replace(/\s+/g, ' ').trim();
136
+ const truncated = query.length > RENDER_QUERY_MAX ? `${query.slice(0, RENDER_QUERY_MAX - 1)}…` : query;
137
+ let text = theme.fg('toolTitle', theme.bold('pi-worker-docs '));
138
+ text += theme.fg('accent', args.module);
139
+ text += `\n${theme.fg('dim', ` query: ${truncated}`)}`;
140
+ return new Text(text, 0, 0);
141
+ }
142
+ });
143
+ }
@@ -0,0 +1,20 @@
1
+ import type { EventEmitter } from 'node:events';
2
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
+ import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
4
+ interface ProcLike extends EventEmitter {
5
+ stdout: EventEmitter | null;
6
+ stderr: EventEmitter | null;
7
+ killed: boolean;
8
+ kill(signal: string): boolean | void;
9
+ }
10
+ type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
11
+ cwd: string;
12
+ shell: boolean;
13
+ stdio: ['ignore', 'pipe', 'pipe'];
14
+ }) => ProcLike;
15
+ export interface PiWorkerFetchInternals {
16
+ fetchAndClean?: typeof defaultFetchAndClean;
17
+ spawn?: SpawnFn;
18
+ }
19
+ export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWorkerFetchInternals): void;
20
+ export {};
@@ -0,0 +1,72 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Text } from '@earendil-works/pi-tui';
3
+ import { FetchAndCleanError } from './html-clean.js';
4
+ import { fetchFocused, formatResultText } from './fetch-core.js';
5
+ import { textResult } from './shared.js';
6
+ const RENDER_QUERY_MAX = 100;
7
+ const Params = Type.Object({
8
+ url: Type.String({ description: 'URL to fetch. Must be http or https.' }),
9
+ query: Type.String({
10
+ description: 'What to extract from the page. The child pi reads the page and returns ONLY content answering this.'
11
+ })
12
+ });
13
+ export function registerPiWorkerFetch(pi, internals = {}) {
14
+ pi.registerTool({
15
+ name: 'pi-worker-fetch',
16
+ label: 'Pi Worker Fetch',
17
+ description: 'Fetch an HTML page, clean it to markdown, and hand it to an isolated '
18
+ + 'child Pi session that extracts ONLY content answering `query`. '
19
+ + 'Returns the focused answer. Use after `pi-worker-search` (or with a '
20
+ + 'known URL) to avoid stuffing raw HTML into the main context.',
21
+ parameters: Params,
22
+ executionMode: 'parallel',
23
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
24
+ try {
25
+ new URL(params.url);
26
+ }
27
+ catch {
28
+ return textResult(`Invalid URL: ${params.url}`, {});
29
+ }
30
+ try {
31
+ const result = await fetchFocused({
32
+ url: params.url,
33
+ query: params.query,
34
+ cwd: ctx.cwd,
35
+ signal,
36
+ fetchAndClean: internals.fetchAndClean,
37
+ spawn: internals.spawn
38
+ });
39
+ if (result.aborted) {
40
+ return textResult('Fetch aborted.', { childExitCode: result.childExitCode });
41
+ }
42
+ if (result.childExitCode !== 0) {
43
+ const tail = result.stderr.trim().slice(-500) || '(no stderr)';
44
+ return textResult(`Worker exited ${result.childExitCode}.\n${tail}`, {
45
+ childExitCode: result.childExitCode
46
+ });
47
+ }
48
+ const text = formatResultText({ answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
49
+ return textResult(text, {
50
+ childExitCode: 0,
51
+ answer: result.answer,
52
+ excerpt: result.excerpt,
53
+ excerptVerified: result.excerptVerified
54
+ });
55
+ }
56
+ catch (err) {
57
+ if (err instanceof FetchAndCleanError) {
58
+ return textResult(err.message, {});
59
+ }
60
+ return textResult(`Could not fetch ${params.url}: ${err instanceof Error ? err.message : String(err)}`, {});
61
+ }
62
+ },
63
+ renderCall(args, theme) {
64
+ const query = args.query.replace(/\s+/g, ' ').trim();
65
+ const truncatedQuery = query.length > RENDER_QUERY_MAX ? `${query.slice(0, RENDER_QUERY_MAX - 1)}…` : query;
66
+ let text = theme.fg('toolTitle', theme.bold('pi-worker-fetch '));
67
+ text += theme.fg('accent', args.url);
68
+ text += `\n${theme.fg('dim', ` query: ${truncatedQuery}`)}`;
69
+ return new Text(text, 0, 0);
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,7 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { braveSearch as defaultBraveSearch } from './brave-search.js';
3
+ export interface PiWorkerSearchInternals {
4
+ braveSearch?: typeof defaultBraveSearch;
5
+ getEnv?: (key: string) => string | undefined;
6
+ }
7
+ export declare function registerPiWorkerSearch(pi: ExtensionAPI, internals?: PiWorkerSearchInternals): void;
@@ -0,0 +1,55 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Text } from '@earendil-works/pi-tui';
3
+ import { search } from './search-core.js';
4
+ import { textResult } from './shared.js';
5
+ const Params = Type.Object({
6
+ query: Type.String({ description: 'Search query.' }),
7
+ count: Type.Optional(Type.Integer({
8
+ minimum: 1,
9
+ maximum: 20,
10
+ description: 'How many results to return (default 10, max 20).'
11
+ }))
12
+ });
13
+ export function registerPiWorkerSearch(pi, internals = {}) {
14
+ pi.registerTool({
15
+ name: 'pi-worker-search',
16
+ label: 'Pi Worker Search',
17
+ description: 'Search the web via Brave Search. Returns a compact markdown list of '
18
+ + 'up to 10 results (title, URL, snippet). Use this to find candidate '
19
+ + 'URLs, then call `pi-worker-fetch` on the URL you want to read. '
20
+ + 'Requires BRAVE_SEARCH_API_KEY env var.',
21
+ parameters: Params,
22
+ executionMode: 'parallel',
23
+ async execute(_toolCallId, params, signal) {
24
+ const result = await search({
25
+ query: params.query,
26
+ count: params.count,
27
+ signal,
28
+ getEnv: internals.getEnv,
29
+ braveSearch: internals.braveSearch
30
+ });
31
+ if (result.kind === 'no_key') {
32
+ return textResult(result.message, { resultCount: 0 });
33
+ }
34
+ if (result.kind === 'error') {
35
+ return textResult(result.message, { resultCount: 0 });
36
+ }
37
+ const { results } = result;
38
+ if (results.length === 0) {
39
+ return textResult(`No results for: ${params.query}`, {
40
+ resultCount: 0
41
+ });
42
+ }
43
+ const lines = results.map((r, i) => `${i + 1}. [${r.title}](${r.url}) — ${r.description}`);
44
+ return textResult(lines.join('\n'), { resultCount: results.length });
45
+ },
46
+ renderCall(args, theme) {
47
+ let text = theme.fg('toolTitle', theme.bold('pi-worker-search '));
48
+ text += theme.fg('accent', args.query);
49
+ if (typeof args.count === 'number') {
50
+ text += theme.fg('dim', ` (count=${args.count})`);
51
+ }
52
+ return new Text(text, 0, 0);
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * pi-worker — minimal subagent tool.
3
+ *
4
+ * Spawns a sandboxed child `pi --print` for each call, returns its stdout.
5
+ * Child has read+grep+find+ls only (no bash, write, or edit) — no skills,
6
+ * extensions, prompt templates, context files, or session storage. Cannot
7
+ * recurse into another worker.
8
+ */
9
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
10
+ export declare function registerPiWorker(pi: ExtensionAPI): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * pi-worker — minimal subagent tool.
3
+ *
4
+ * Spawns a sandboxed child `pi --print` for each call, returns its stdout.
5
+ * Child has read+grep+find+ls only (no bash, write, or edit) — no skills,
6
+ * extensions, prompt templates, context files, or session storage. Cannot
7
+ * recurse into another worker.
8
+ */
9
+ import { Text } from '@earendil-works/pi-tui';
10
+ import { Type } from '@sinclair/typebox';
11
+ import { runWorker } from './pi-worker-core.js';
12
+ import { textResult } from './shared.js';
13
+ const RENDER_PROMPT_MAX = 120;
14
+ const WorkerParams = Type.Object({
15
+ prompt: Type.String({ description: 'Task for the worker to perform.' })
16
+ });
17
+ export function registerPiWorker(pi) {
18
+ pi.registerTool({
19
+ name: 'pi-worker',
20
+ label: 'Pi Worker',
21
+ description: 'Dispatch an isolated child Pi to investigate a question and return its '
22
+ + 'conclusion — not the raw evidence. Use when answering requires reading '
23
+ + 'multiple files, running several greps, or scanning large command output '
24
+ + 'you do not need verbatim.\n'
25
+ + '\n'
26
+ + 'Good fits:\n'
27
+ + '- "Where/how is X handled in this repo?" across unfamiliar code\n'
28
+ + '- Audits and pattern scans across many files ("every place we log PII")\n'
29
+ + '- Summarising long test output, logs, or shell output\n'
30
+ + '- Parallel fan-out: dispatch several workers in one turn for independent questions\n'
31
+ + '\n'
32
+ + 'Skip when:\n'
33
+ + '- You already know the exact file — call `read` directly\n'
34
+ + '- The task needs writes/edits (worker is read-only)\n'
35
+ + '- The task needs the web — use `pi-worker-search` / `pi-worker-fetch`',
36
+ parameters: WorkerParams,
37
+ executionMode: 'parallel',
38
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
39
+ const result = await runWorker({ prompt: params.prompt, cwd: ctx.cwd, signal });
40
+ if (result.aborted) {
41
+ return textResult('Worker aborted.', { exitCode: result.exitCode });
42
+ }
43
+ if (result.exitCode !== 0) {
44
+ const tail = result.stderr.slice(-500) || '(no stderr)';
45
+ return textResult(`Worker exited ${result.exitCode}.\n${tail}`, {
46
+ exitCode: result.exitCode
47
+ });
48
+ }
49
+ return textResult(result.text || '(no output)', { exitCode: result.exitCode });
50
+ },
51
+ renderCall(args, theme) {
52
+ const prompt = args.prompt.replace(/\s+/g, ' ').trim();
53
+ const truncated = prompt.length > RENDER_PROMPT_MAX ?
54
+ `${prompt.slice(0, RENDER_PROMPT_MAX - 1)}…`
55
+ : prompt;
56
+ const head = theme.fg('toolTitle', theme.bold('pi-worker '));
57
+ const body = theme.fg('accent', truncated);
58
+ return new Text(head + body, 0, 0);
59
+ }
60
+ });
61
+ }
@@ -0,0 +1,19 @@
1
+ import { braveSearch as defaultBraveSearch, type BraveResult } from './brave-search.js';
2
+ export interface SearchCoreInput {
3
+ query: string;
4
+ count?: number;
5
+ signal?: AbortSignal;
6
+ getEnv?: (key: string) => string | undefined;
7
+ braveSearch?: typeof defaultBraveSearch;
8
+ }
9
+ export type SearchCoreResult = {
10
+ kind: 'ok';
11
+ results: BraveResult[];
12
+ } | {
13
+ kind: 'no_key';
14
+ message: string;
15
+ } | {
16
+ kind: 'error';
17
+ message: string;
18
+ };
19
+ export declare function search(input: SearchCoreInput): Promise<SearchCoreResult>;
@@ -0,0 +1,35 @@
1
+ import { braveSearch as defaultBraveSearch, BraveSearchError } from './brave-search.js';
2
+ function isLikeBraveSearchError(err) {
3
+ return (typeof err === 'object'
4
+ && err !== null
5
+ && err.name === 'BraveSearchError');
6
+ }
7
+ export async function search(input) {
8
+ const getEnv = input.getEnv ?? ((k) => process.env[k]);
9
+ const braveSearch = input.braveSearch ?? defaultBraveSearch;
10
+ const apiKey = getEnv('BRAVE_SEARCH_API_KEY') ?? getEnv('BRAVE_API_KEY');
11
+ if (!apiKey) {
12
+ return {
13
+ kind: 'no_key',
14
+ message: 'Brave Search not configured. Set BRAVE_SEARCH_API_KEY env var. '
15
+ + 'Get a key at https://api.search.brave.com/app/keys'
16
+ };
17
+ }
18
+ try {
19
+ const results = await braveSearch(input.query, {
20
+ apiKey,
21
+ count: input.count,
22
+ signal: input.signal
23
+ });
24
+ return { kind: 'ok', results };
25
+ }
26
+ catch (err) {
27
+ if (err instanceof BraveSearchError || isLikeBraveSearchError(err)) {
28
+ return { kind: 'error', message: err.message };
29
+ }
30
+ return {
31
+ kind: 'error',
32
+ message: `Brave Search request failed: ${err instanceof Error ? err.message : String(err)}`
33
+ };
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentToolResult } from '@earendil-works/pi-agent-core';
2
+ /** Build a plain-text AgentToolResult. */
3
+ export declare function textResult<T>(text: string, details: T): AgentToolResult<T>;
@@ -0,0 +1,4 @@
1
+ /** Build a plain-text AgentToolResult. */
2
+ export function textResult(text, details) {
3
+ return { content: [{ type: 'text', text }], details };
4
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mjasnikovs/pi-task",
3
+ "version": "0.2.0",
4
+ "description": "Deterministic spec-orchestration for local models, with bundled web/docs/fetch/worker subagent tools.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": ["dist", "README.md", "LICENSE"],
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit",
12
+ "test": "bun test src/",
13
+ "prepublishOnly": "bun run build"
14
+ },
15
+ "peerDependencies": {
16
+ "@earendil-works/pi-coding-agent": "0.75.5",
17
+ "@earendil-works/pi-agent-core": "0.75.5",
18
+ "@earendil-works/pi-tui": "0.75.5"
19
+ },
20
+ "dependencies": {
21
+ "@mozilla/readability": "^0.6.0",
22
+ "@sinclair/typebox": "0.34.49",
23
+ "jsdom": "^29.1.1",
24
+ "turndown": "^7.2.4"
25
+ },
26
+ "devDependencies": {
27
+ "@earendil-works/pi-coding-agent": "0.75.5",
28
+ "@earendil-works/pi-agent-core": "0.75.5",
29
+ "@earendil-works/pi-tui": "0.75.5",
30
+ "@eslint/js": "10.0.1",
31
+ "@sinclair/typebox": "0.34.49",
32
+ "@types/bun": "1.3.12",
33
+ "@types/jsdom": "^28.0.3",
34
+ "@types/turndown": "^5.0.6",
35
+ "eslint": "10.2.1",
36
+ "globals": "17.5.0",
37
+ "prettier": "3.8.3",
38
+ "typescript": "6.0.3",
39
+ "typescript-eslint": "8.58.2"
40
+ },
41
+ "keywords": ["pi", "pi-extension", "coding-agent", "task-orchestration", "local-llm"],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "github:mjasnikovs/pi-task"
45
+ },
46
+ "license": "MIT",
47
+ "pi": {
48
+ "extensions": ["src/index.ts"]
49
+ }
50
+ }