@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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/shared/child-output.d.ts +21 -0
- package/dist/shared/child-output.js +40 -0
- package/dist/shared/child-process.d.ts +71 -0
- package/dist/shared/child-process.js +190 -0
- package/dist/shared/pi-invocation.d.ts +7 -0
- package/dist/shared/pi-invocation.js +24 -0
- package/dist/task/child-runner.d.ts +66 -0
- package/dist/task/child-runner.js +157 -0
- package/dist/task/enrichment.d.ts +12 -0
- package/dist/task/enrichment.js +82 -0
- package/dist/task/failure-classifier.d.ts +15 -0
- package/dist/task/failure-classifier.js +63 -0
- package/dist/task/file-inventory.d.ts +9 -0
- package/dist/task/file-inventory.js +44 -0
- package/dist/task/loop-detector.d.ts +32 -0
- package/dist/task/loop-detector.js +46 -0
- package/dist/task/orchestrator.d.ts +54 -0
- package/dist/task/orchestrator.js +387 -0
- package/dist/task/parsers.d.ts +32 -0
- package/dist/task/parsers.js +172 -0
- package/dist/task/phases.d.ts +56 -0
- package/dist/task/phases.js +477 -0
- package/dist/task/prompts.d.ts +21 -0
- package/dist/task/prompts.js +346 -0
- package/dist/task/service-blocks.d.ts +3 -0
- package/dist/task/service-blocks.js +10 -0
- package/dist/task/task-file.d.ts +14 -0
- package/dist/task/task-file.js +15 -0
- package/dist/task/task-io.d.ts +19 -0
- package/dist/task/task-io.js +78 -0
- package/dist/task/task-parsers.d.ts +12 -0
- package/dist/task/task-parsers.js +75 -0
- package/dist/task/task-types.d.ts +21 -0
- package/dist/task/task-types.js +18 -0
- package/dist/task/timings.d.ts +18 -0
- package/dist/task/timings.js +36 -0
- package/dist/task/widget.d.ts +39 -0
- package/dist/task/widget.js +122 -0
- package/dist/workers/brave-search.d.ts +17 -0
- package/dist/workers/brave-search.js +77 -0
- package/dist/workers/docs-cache.d.ts +16 -0
- package/dist/workers/docs-cache.js +66 -0
- package/dist/workers/docs-core.d.ts +86 -0
- package/dist/workers/docs-core.js +329 -0
- package/dist/workers/docs-index.d.ts +9 -0
- package/dist/workers/docs-index.js +200 -0
- package/dist/workers/docs-resolve.d.ts +12 -0
- package/dist/workers/docs-resolve.js +126 -0
- package/dist/workers/docs-retrieve.d.ts +15 -0
- package/dist/workers/docs-retrieve.js +91 -0
- package/dist/workers/fetch-core.d.ts +35 -0
- package/dist/workers/fetch-core.js +91 -0
- package/dist/workers/html-clean.d.ts +17 -0
- package/dist/workers/html-clean.js +142 -0
- package/dist/workers/index.d.ts +2 -0
- package/dist/workers/index.js +10 -0
- package/dist/workers/npm-version.d.ts +32 -0
- package/dist/workers/npm-version.js +102 -0
- package/dist/workers/pi-worker-core.d.ts +28 -0
- package/dist/workers/pi-worker-core.js +29 -0
- package/dist/workers/pi-worker-docs.d.ts +16 -0
- package/dist/workers/pi-worker-docs.js +143 -0
- package/dist/workers/pi-worker-fetch.d.ts +20 -0
- package/dist/workers/pi-worker-fetch.js +72 -0
- package/dist/workers/pi-worker-search.d.ts +7 -0
- package/dist/workers/pi-worker-search.js +55 -0
- package/dist/workers/pi-worker.d.ts +10 -0
- package/dist/workers/pi-worker.js +61 -0
- package/dist/workers/search-core.d.ts +19 -0
- package/dist/workers/search-core.js +35 -0
- package/dist/workers/shared.d.ts +3 -0
- package/dist/workers/shared.js +4 -0
- package/package.json +50 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase timing data — captures how long each pipeline phase took so we can
|
|
3
|
+
* spot regressions and target future speed improvements.
|
|
4
|
+
*
|
|
5
|
+
* Top-level entries are the five phases (refine, research, grill, compose,
|
|
6
|
+
* critique). Each phase may attach optional sub-step children (e.g. research
|
|
7
|
+
* → workers + verify-tooling, grill → gen + auto-answers + user input).
|
|
8
|
+
*
|
|
9
|
+
* `formatTimings` produces the human-readable block we write to the
|
|
10
|
+
* `## phase timings` section of the TASK_NNNN.md file.
|
|
11
|
+
*/
|
|
12
|
+
const TOP_LABEL_WIDTH = 18;
|
|
13
|
+
const SUB_LABEL_WIDTH = 20;
|
|
14
|
+
const MS_COLUMN_WIDTH = 8;
|
|
15
|
+
export function formatMs(ms) {
|
|
16
|
+
if (ms < 0)
|
|
17
|
+
ms = 0;
|
|
18
|
+
if (ms < 1000)
|
|
19
|
+
return `${ms}ms`;
|
|
20
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
}
|
|
22
|
+
export function formatTimings(entries) {
|
|
23
|
+
if (entries.length === 0)
|
|
24
|
+
return '(no phases recorded)';
|
|
25
|
+
const lines = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
total += e.ms;
|
|
29
|
+
lines.push(`${e.label.padEnd(TOP_LABEL_WIDTH)}${formatMs(e.ms).padStart(MS_COLUMN_WIDTH)}`);
|
|
30
|
+
for (const c of e.children) {
|
|
31
|
+
lines.push(` ${c.label.padEnd(SUB_LABEL_WIDTH)}${formatMs(c.ms).padStart(MS_COLUMN_WIDTH)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
lines.push(`${'total'.padEnd(TOP_LABEL_WIDTH)}${formatMs(total).padStart(MS_COLUMN_WIDTH)}`);
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal widget for the pi-task orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Renders a live-updating status block showing task id, phase, elapsed time,
|
|
5
|
+
* context usage, and the latest child-process line.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
import { type PhaseName, type TaskState } from './task-file.js';
|
|
9
|
+
export interface WidgetState {
|
|
10
|
+
taskId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
phase: PhaseName;
|
|
13
|
+
startedAt: number;
|
|
14
|
+
lastLine?: string;
|
|
15
|
+
contextUsage?: ContextSnapshot;
|
|
16
|
+
}
|
|
17
|
+
export interface ContextSnapshot {
|
|
18
|
+
tokens: number;
|
|
19
|
+
contextWindow: number;
|
|
20
|
+
percent: number;
|
|
21
|
+
}
|
|
22
|
+
export type WidgetTheme = ExtensionCommandContext['ui']['theme'];
|
|
23
|
+
export declare const WIDGET_KEY = "pi-tasks";
|
|
24
|
+
export declare const WIDGET_REFRESH_MS = 500;
|
|
25
|
+
export declare const WIDGET_LAST_LINE_MAX = 120;
|
|
26
|
+
export declare const NOTIFY_CLEAR_MS = 3000;
|
|
27
|
+
export declare const FAIL_CLEAR_MS = 4000;
|
|
28
|
+
export declare const CTX_BAR_WIDTH = 8;
|
|
29
|
+
export declare const CTX_BAR_FILLED = "\u2593";
|
|
30
|
+
export declare const CTX_BAR_EMPTY = "\u2591";
|
|
31
|
+
export declare const CTX_WARN_PERCENT = 80;
|
|
32
|
+
export declare const CTX_ERROR_PERCENT = 90;
|
|
33
|
+
export declare function formatDuration(ms: number): string;
|
|
34
|
+
export declare function formatContextTokens(count: number): string;
|
|
35
|
+
export declare function contextProgressBar(percent: number): string;
|
|
36
|
+
export declare function contextThresholdColor(theme: WidgetTheme, percent: number, text: string): string;
|
|
37
|
+
export declare function buildWidgetLines(s: WidgetState, theme?: WidgetTheme): string[];
|
|
38
|
+
export declare function startWidget(ctx: ExtensionCommandContext, getState: () => WidgetState | null): () => void;
|
|
39
|
+
export declare function flashTerminalWidget(ctx: ExtensionCommandContext, state: Exclude<TaskState, 'pending' | 'in_progress' | 'completed'>, taskId: string, reason: string | undefined): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal widget for the pi-task orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Renders a live-updating status block showing task id, phase, elapsed time,
|
|
5
|
+
* context usage, and the latest child-process line.
|
|
6
|
+
*/
|
|
7
|
+
import { PHASE_INDEX, PHASE_ORDER } from './task-file.js';
|
|
8
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
9
|
+
export const WIDGET_KEY = 'pi-tasks';
|
|
10
|
+
export const WIDGET_REFRESH_MS = 500;
|
|
11
|
+
export const WIDGET_LAST_LINE_MAX = 120;
|
|
12
|
+
export const NOTIFY_CLEAR_MS = 3000;
|
|
13
|
+
export const FAIL_CLEAR_MS = 4000;
|
|
14
|
+
export const CTX_BAR_WIDTH = 8;
|
|
15
|
+
export const CTX_BAR_FILLED = '▓';
|
|
16
|
+
export const CTX_BAR_EMPTY = '░';
|
|
17
|
+
export const CTX_WARN_PERCENT = 80;
|
|
18
|
+
export const CTX_ERROR_PERCENT = 90;
|
|
19
|
+
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
|
20
|
+
export function formatDuration(ms) {
|
|
21
|
+
const total = Math.floor(ms / 1000);
|
|
22
|
+
const m = Math.floor(total / 60);
|
|
23
|
+
const s = total % 60;
|
|
24
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
25
|
+
}
|
|
26
|
+
export function formatContextTokens(count) {
|
|
27
|
+
if (count < 1_000)
|
|
28
|
+
return count.toString();
|
|
29
|
+
if (count < 10_000)
|
|
30
|
+
return `${(count / 1_000).toFixed(1)}k`;
|
|
31
|
+
if (count < 1_000_000)
|
|
32
|
+
return `${Math.round(count / 1_000)}k`;
|
|
33
|
+
if (count < 10_000_000)
|
|
34
|
+
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
35
|
+
return `${Math.round(count / 1_000_000)}M`;
|
|
36
|
+
}
|
|
37
|
+
export function contextProgressBar(percent) {
|
|
38
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
39
|
+
const filled = Math.round((clamped / 100) * CTX_BAR_WIDTH);
|
|
40
|
+
return `[${CTX_BAR_FILLED.repeat(filled)}${CTX_BAR_EMPTY.repeat(CTX_BAR_WIDTH - filled)}]`;
|
|
41
|
+
}
|
|
42
|
+
export function contextThresholdColor(theme, percent, text) {
|
|
43
|
+
if (percent >= CTX_ERROR_PERCENT)
|
|
44
|
+
return theme.fg('error', text);
|
|
45
|
+
if (percent >= CTX_WARN_PERCENT)
|
|
46
|
+
return theme.fg('warning', text);
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
export function buildWidgetLines(s, theme) {
|
|
50
|
+
const elapsed = formatDuration(Date.now() - s.startedAt);
|
|
51
|
+
const head = `${s.taskId} · ${s.title}`;
|
|
52
|
+
const idx = PHASE_INDEX[s.phase];
|
|
53
|
+
const total = PHASE_ORDER.length;
|
|
54
|
+
const stepNum = Math.min(idx + 1, total);
|
|
55
|
+
let detail = `phase ${stepNum}/${total} ${s.phase} · ${elapsed}`;
|
|
56
|
+
if (s.contextUsage) {
|
|
57
|
+
const { tokens, contextWindow, percent } = s.contextUsage;
|
|
58
|
+
if (contextWindow > 0) {
|
|
59
|
+
const text = `${formatContextTokens(tokens)}/${formatContextTokens(contextWindow)} ${contextProgressBar(percent)}`;
|
|
60
|
+
detail += ` · ${theme ? contextThresholdColor(theme, percent, text) : text}`;
|
|
61
|
+
}
|
|
62
|
+
else if (tokens > 0) {
|
|
63
|
+
detail += ` · ${formatContextTokens(tokens)}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const lines = [head, detail];
|
|
67
|
+
if (s.lastLine) {
|
|
68
|
+
const t = s.lastLine.length > WIDGET_LAST_LINE_MAX ?
|
|
69
|
+
s.lastLine.slice(0, WIDGET_LAST_LINE_MAX - 1) + '…'
|
|
70
|
+
: s.lastLine;
|
|
71
|
+
const raw = `↳ ${t}`;
|
|
72
|
+
lines.push(theme ? theme.fg('muted', raw) : raw);
|
|
73
|
+
}
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
// ─── Widget lifecycle ────────────────────────────────────────────────────────
|
|
77
|
+
export function startWidget(ctx, getState) {
|
|
78
|
+
if (!ctx.hasUI)
|
|
79
|
+
return () => { };
|
|
80
|
+
const render = () => {
|
|
81
|
+
const s = getState();
|
|
82
|
+
try {
|
|
83
|
+
ctx.ui.setWidget(WIDGET_KEY, s ? buildWidgetLines(s, ctx.ui.theme) : undefined);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* stale ctx */
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
render();
|
|
90
|
+
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
91
|
+
timer.unref?.();
|
|
92
|
+
return () => clearInterval(timer);
|
|
93
|
+
}
|
|
94
|
+
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
95
|
+
if (!ctx.hasUI)
|
|
96
|
+
return;
|
|
97
|
+
const theme = ctx.ui.theme;
|
|
98
|
+
let line;
|
|
99
|
+
let clearMs;
|
|
100
|
+
if (state === 'cancelled') {
|
|
101
|
+
line = theme.fg('warning', `⚠ ${taskId} cancelled`);
|
|
102
|
+
clearMs = NOTIFY_CLEAR_MS;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
line = theme.fg('error', `✘ ${taskId} failed${reason ? ': ' + reason : ''}`);
|
|
106
|
+
clearMs = FAIL_CLEAR_MS;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
ctx.ui.setWidget(WIDGET_KEY, [line]);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* stale ctx */
|
|
113
|
+
}
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
try {
|
|
116
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* stale ctx */
|
|
120
|
+
}
|
|
121
|
+
}, clearMs);
|
|
122
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface BraveResult {
|
|
2
|
+
title: string;
|
|
3
|
+
url: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BraveSearchOpts {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
count?: number;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
export declare class BraveSearchError extends Error {
|
|
13
|
+
readonly kind: 'auth' | 'rate-limit' | 'http' | 'network' | 'aborted';
|
|
14
|
+
readonly status?: number | undefined;
|
|
15
|
+
constructor(message: string, kind: 'auth' | 'rate-limit' | 'http' | 'network' | 'aborted', status?: number | undefined);
|
|
16
|
+
}
|
|
17
|
+
export declare function braveSearch(query: string, opts: BraveSearchOpts): Promise<BraveResult[]>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const BRAVE_ENDPOINT = 'https://api.search.brave.com/res/v1/web/search';
|
|
2
|
+
const DEFAULT_COUNT = 10;
|
|
3
|
+
const MAX_COUNT = 20;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
5
|
+
export class BraveSearchError extends Error {
|
|
6
|
+
kind;
|
|
7
|
+
status;
|
|
8
|
+
constructor(message, kind, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.kind = kind;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.name = 'BraveSearchError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function braveSearch(query, opts) {
|
|
16
|
+
const count = Math.max(1, Math.min(MAX_COUNT, opts.count ?? DEFAULT_COUNT));
|
|
17
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
18
|
+
const url = `${BRAVE_ENDPOINT}?q=${encodeURIComponent(query)}&count=${count}`;
|
|
19
|
+
const internalController = new AbortController();
|
|
20
|
+
let userAborted = false;
|
|
21
|
+
const timeoutHandle = setTimeout(() => internalController.abort(), timeoutMs);
|
|
22
|
+
const onUserAbort = () => {
|
|
23
|
+
userAborted = true;
|
|
24
|
+
internalController.abort();
|
|
25
|
+
};
|
|
26
|
+
if (opts.signal) {
|
|
27
|
+
if (opts.signal.aborted)
|
|
28
|
+
onUserAbort();
|
|
29
|
+
else
|
|
30
|
+
opts.signal.addEventListener('abort', onUserAbort, { once: true });
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await fetch(url, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers: {
|
|
38
|
+
accept: 'application/json',
|
|
39
|
+
'x-subscription-token': opts.apiKey
|
|
40
|
+
},
|
|
41
|
+
signal: internalController.signal
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (userAborted) {
|
|
46
|
+
throw new BraveSearchError('Search aborted.', 'aborted');
|
|
47
|
+
}
|
|
48
|
+
throw new BraveSearchError(`Brave Search request failed: ${describeError(err)}`, 'network');
|
|
49
|
+
}
|
|
50
|
+
if (response.status === 401 || response.status === 403) {
|
|
51
|
+
throw new BraveSearchError(`Brave Search rejected the key (HTTP ${response.status}). Check BRAVE_SEARCH_API_KEY.`, 'auth', response.status);
|
|
52
|
+
}
|
|
53
|
+
if (response.status === 429) {
|
|
54
|
+
throw new BraveSearchError('Brave Search rate limit hit (HTTP 429). Try again in a moment.', 'rate-limit', 429);
|
|
55
|
+
}
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new BraveSearchError(`Brave Search HTTP ${response.status} ${response.statusText}`, 'http', response.status);
|
|
58
|
+
}
|
|
59
|
+
const body = (await response.json());
|
|
60
|
+
const rawResults = body.web?.results ?? [];
|
|
61
|
+
return rawResults
|
|
62
|
+
.filter((r) => typeof r.title === 'string'
|
|
63
|
+
&& typeof r.url === 'string'
|
|
64
|
+
&& typeof r.description === 'string')
|
|
65
|
+
.map(r => ({ title: r.title, url: r.url, description: r.description }));
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timeoutHandle);
|
|
69
|
+
if (opts.signal)
|
|
70
|
+
opts.signal.removeEventListener('abort', onUserAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function describeError(err) {
|
|
74
|
+
if (err instanceof Error)
|
|
75
|
+
return err.message;
|
|
76
|
+
return String(err);
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SyncStatement {
|
|
2
|
+
get(...args: unknown[]): unknown;
|
|
3
|
+
all(...args: unknown[]): unknown[];
|
|
4
|
+
run(...args: unknown[]): unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface SyncDb {
|
|
7
|
+
exec(sql: string): void;
|
|
8
|
+
prepare(sql: string): SyncStatement;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface CacheHandle {
|
|
12
|
+
db: SyncDb;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function defaultCachePath(): string;
|
|
16
|
+
export declare function openCache(dbPath?: string): CacheHandle;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
const SCHEMA_SQL = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS packages (
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
version TEXT NOT NULL,
|
|
9
|
+
content_hash TEXT NOT NULL,
|
|
10
|
+
indexed_at INTEGER NOT NULL,
|
|
11
|
+
PRIMARY KEY (name, version)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
name TEXT NOT NULL,
|
|
17
|
+
version TEXT NOT NULL,
|
|
18
|
+
file_path TEXT NOT NULL,
|
|
19
|
+
kind TEXT NOT NULL CHECK (kind IN ('dts','readme')),
|
|
20
|
+
content TEXT NOT NULL
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS chunks_pkg ON chunks(name, version);
|
|
23
|
+
|
|
24
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
25
|
+
content,
|
|
26
|
+
content='chunks',
|
|
27
|
+
content_rowid='id'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
|
|
31
|
+
INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);
|
|
32
|
+
END;
|
|
33
|
+
CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
|
|
34
|
+
INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES('delete', old.id, old.content);
|
|
35
|
+
END;
|
|
36
|
+
`;
|
|
37
|
+
const req = createRequire(import.meta.url);
|
|
38
|
+
function openDb(dbPath) {
|
|
39
|
+
if (typeof globalThis.Bun !== 'undefined') {
|
|
40
|
+
// Bun runtime: use bun:sqlite
|
|
41
|
+
const { Database } = req('bun:sqlite');
|
|
42
|
+
return new Database(dbPath);
|
|
43
|
+
}
|
|
44
|
+
// Node.js runtime: use node:sqlite
|
|
45
|
+
const { DatabaseSync } = req('node:sqlite');
|
|
46
|
+
return new DatabaseSync(dbPath);
|
|
47
|
+
}
|
|
48
|
+
export function defaultCachePath() {
|
|
49
|
+
const base = process.env.XDG_CACHE_HOME?.trim() || path.join(os.homedir(), '.cache');
|
|
50
|
+
return path.join(base, 'pi-worker', 'docs.sqlite');
|
|
51
|
+
}
|
|
52
|
+
export function openCache(dbPath) {
|
|
53
|
+
const resolved = dbPath ?? defaultCachePath();
|
|
54
|
+
if (resolved !== ':memory:') {
|
|
55
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
const db = openDb(resolved);
|
|
58
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
59
|
+
db.exec('PRAGMA synchronous = NORMAL;');
|
|
60
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
61
|
+
db.exec(SCHEMA_SQL);
|
|
62
|
+
return {
|
|
63
|
+
db,
|
|
64
|
+
close: () => db.close()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
2
|
+
import { ensureIndexed as defaultEnsureIndexed } from './docs-index.js';
|
|
3
|
+
import { resolvePackage as defaultResolvePackage, type ResolvedPackage } from './docs-resolve.js';
|
|
4
|
+
import { retrieveChunks as defaultRetrieveChunks, type RetrievedChunk } from './docs-retrieve.js';
|
|
5
|
+
import { npmVersionLookup as defaultNpmVersionLookup, type NpmVersionInfo } from './npm-version.js';
|
|
6
|
+
import { type SpawnFn } from '../shared/child-process.js';
|
|
7
|
+
export type DocsRawResult = {
|
|
8
|
+
kind: 'ok';
|
|
9
|
+
pkg: ResolvedPackage;
|
|
10
|
+
chunks: RetrievedChunk[];
|
|
11
|
+
hitCache: boolean;
|
|
12
|
+
indexingMs?: number;
|
|
13
|
+
indexedFiles?: number;
|
|
14
|
+
cacheError?: string;
|
|
15
|
+
autoInstalled?: boolean;
|
|
16
|
+
npmVersion?: NpmVersionInfo | null;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'not_installed';
|
|
19
|
+
pkg: string;
|
|
20
|
+
npmVersion?: NpmVersionInfo | null;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'no_chunks';
|
|
23
|
+
pkg: ResolvedPackage;
|
|
24
|
+
hitCache: boolean;
|
|
25
|
+
indexedFiles?: number;
|
|
26
|
+
cacheError?: string;
|
|
27
|
+
autoInstalled?: boolean;
|
|
28
|
+
npmVersion?: NpmVersionInfo | null;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'error';
|
|
31
|
+
message: string;
|
|
32
|
+
resolveError?: 'not_installed' | 'invalid_name';
|
|
33
|
+
installError?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
hitCache?: boolean;
|
|
36
|
+
cacheError?: string;
|
|
37
|
+
autoInstalled?: boolean;
|
|
38
|
+
npmVersion?: NpmVersionInfo | null;
|
|
39
|
+
};
|
|
40
|
+
export interface DocsRawInput {
|
|
41
|
+
pkg: string;
|
|
42
|
+
query: string;
|
|
43
|
+
cwd: string;
|
|
44
|
+
autoInstall?: boolean;
|
|
45
|
+
resolvePackage?: typeof defaultResolvePackage;
|
|
46
|
+
ensureIndexed?: typeof defaultEnsureIndexed;
|
|
47
|
+
retrieveChunks?: typeof defaultRetrieveChunks;
|
|
48
|
+
openCache?: typeof defaultOpenCache;
|
|
49
|
+
spawn?: SpawnFn;
|
|
50
|
+
npmVersionLookup?: typeof defaultNpmVersionLookup;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
export interface DocsFocusedResult {
|
|
54
|
+
answer: string;
|
|
55
|
+
excerpt?: string;
|
|
56
|
+
excerptVerified?: boolean;
|
|
57
|
+
pkg: ResolvedPackage;
|
|
58
|
+
version: string;
|
|
59
|
+
exitCode: number;
|
|
60
|
+
aborted: boolean;
|
|
61
|
+
stderr: string;
|
|
62
|
+
hitCache?: boolean;
|
|
63
|
+
indexingMs?: number;
|
|
64
|
+
indexedFiles?: number;
|
|
65
|
+
chunksRetrieved?: number;
|
|
66
|
+
cacheError?: string;
|
|
67
|
+
autoInstalled?: boolean;
|
|
68
|
+
npmVersion?: NpmVersionInfo | null;
|
|
69
|
+
}
|
|
70
|
+
export type DocsFocusedInput = DocsRawInput;
|
|
71
|
+
export declare function extractParentPackage(moduleName: string): string;
|
|
72
|
+
export declare function getDocsModulesDir(): string;
|
|
73
|
+
export declare function ensureDocsModulesDir(dir: string): void;
|
|
74
|
+
export declare function runAutoInstall(spawn: SpawnFn, packageName: string, signal: AbortSignal | undefined): Promise<{
|
|
75
|
+
success: boolean;
|
|
76
|
+
installDir: string;
|
|
77
|
+
stderr: string;
|
|
78
|
+
}>;
|
|
79
|
+
export declare function docsRaw(input: DocsRawInput): Promise<DocsRawResult>;
|
|
80
|
+
export declare function docsFocused(input: DocsFocusedInput): Promise<DocsFocusedResult>;
|
|
81
|
+
export declare function buildPrompt(pkg: ResolvedPackage, query: string, content: string): string;
|
|
82
|
+
/** Thin wrapper so existing callers using the pkg-based signature still work. */
|
|
83
|
+
export declare function formatResultText(pkg: ResolvedPackage, parsed: {
|
|
84
|
+
answer: string;
|
|
85
|
+
excerpt?: string;
|
|
86
|
+
}, verified: boolean | undefined): string;
|