@gavin-hu/my-pi-agent 0.13.1 → 0.15.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/README.md +66 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/config.ts +3 -2
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +118 -8
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +16 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +3 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +23 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
package/lib/pdf.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional `unpdf` seam: lazy load, module-shape handling, and text
|
|
3
|
+
* extraction, shared by `web-access` and `document`.
|
|
4
|
+
*
|
|
5
|
+
* `unpdf` bundles a server-friendly build of PDF.js, so it is imported lazily:
|
|
6
|
+
* an extension registers on every session, and a parser should load only when a
|
|
7
|
+
* read actually needs it. `web-access` treats the package as optional and
|
|
8
|
+
* reports {@link pdfGuidance} when it is absent; `document` ships it as a
|
|
9
|
+
* dependency. Both get the same loader, the same merged-pages call, and the same
|
|
10
|
+
* result unwrapping, so a PDF reads the same way through either surface.
|
|
11
|
+
*
|
|
12
|
+
* Value-only: no module state, and no import of `unpdf` until a caller asks.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Install command for the optional PDF extractor package. */
|
|
16
|
+
const UNPDF_INSTALL = "npm i unpdf";
|
|
17
|
+
|
|
18
|
+
/** Model-facing guidance when the optional `unpdf` package is absent. */
|
|
19
|
+
export function pdfGuidance(): string {
|
|
20
|
+
return `PDF extraction requires the optional 'unpdf' package (${UNPDF_INSTALL}).`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Raised when `unpdf` is missing or does not expose `extractText`. */
|
|
24
|
+
export class PdfUnavailableError extends Error {
|
|
25
|
+
constructor(message: string) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "PdfUnavailableError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract merged page text from PDF bytes. */
|
|
32
|
+
export type PdfExtractor = (bytes: Uint8Array) => Promise<string>;
|
|
33
|
+
|
|
34
|
+
/** Pull the merged text out of `unpdf`'s `{ totalPages, text }` result. Pure. */
|
|
35
|
+
export function pdfTextFromResult(result: unknown): string {
|
|
36
|
+
if (typeof result === "string") return result;
|
|
37
|
+
const text = (result as { text?: unknown } | undefined)?.text;
|
|
38
|
+
return typeof text === "string" ? text : "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The `extractText` function of an imported `unpdf` module, when it exposes one. */
|
|
42
|
+
function extractTextOf(module: unknown): ((bytes: Uint8Array, options?: unknown) => Promise<unknown>) | undefined {
|
|
43
|
+
if (typeof module !== "object" || module === null) return undefined;
|
|
44
|
+
const named = (module as { extractText?: unknown }).extractText;
|
|
45
|
+
if (typeof named === "function") return named as (bytes: Uint8Array, options?: unknown) => Promise<unknown>;
|
|
46
|
+
const fallback = (module as { default?: unknown }).default;
|
|
47
|
+
if (typeof fallback !== "object" || fallback === null) return undefined;
|
|
48
|
+
const nested = (fallback as { extractText?: unknown }).extractText;
|
|
49
|
+
if (typeof nested !== "function") return undefined;
|
|
50
|
+
return nested as (bytes: Uint8Array, options?: unknown) => Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build an extractor from an imported `unpdf` module.
|
|
55
|
+
*
|
|
56
|
+
* `unpdf` exports `extractText` directly; the default-export fallback tolerates
|
|
57
|
+
* a build that wraps it. Anything else is a broken install, reported with the
|
|
58
|
+
* same error the missing-package path uses.
|
|
59
|
+
*/
|
|
60
|
+
export function pdfExtractorFromModule(module: unknown): PdfExtractor {
|
|
61
|
+
const extractText = extractTextOf(module);
|
|
62
|
+
if (!extractText) throw new PdfUnavailableError("The installed 'unpdf' package does not export extractText.");
|
|
63
|
+
return async (bytes) => pdfTextFromResult(await extractText(bytes, { mergePages: true }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Load `unpdf` lazily, or throw {@link PdfUnavailableError} with install guidance. */
|
|
67
|
+
export async function loadPdfExtractor(): Promise<PdfExtractor> {
|
|
68
|
+
let module: unknown;
|
|
69
|
+
try {
|
|
70
|
+
module = await import("unpdf");
|
|
71
|
+
} catch {
|
|
72
|
+
throw new PdfUnavailableError(pdfGuidance());
|
|
73
|
+
}
|
|
74
|
+
return pdfExtractorFromModule(module);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Extract merged page text from PDF bytes, using `extractor` when given. */
|
|
78
|
+
export async function extractPdfText(bytes: Uint8Array, extractor?: PdfExtractor): Promise<string> {
|
|
79
|
+
if (extractor) return extractor(bytes);
|
|
80
|
+
const loaded = await loadPdfExtractor();
|
|
81
|
+
return loaded(bytes);
|
|
82
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-mode entry, shared by the extension that owns the mode and the one that
|
|
3
|
+
* offers it as a phase of the goal.
|
|
4
|
+
*
|
|
5
|
+
* Plan mode has one owner: `plan` gates the tools, persists the flag, and draws
|
|
6
|
+
* the footer. A tool context can reach it through `ctx.executeTool`, but a
|
|
7
|
+
* command context cannot (`ExtensionCommandContext` has no `executeTool`), so the
|
|
8
|
+
* request travels on `pi.events` as an in-process request/response. The caller
|
|
9
|
+
* asks the user — with the consent sentence below — and `plan` enables without a
|
|
10
|
+
* second dialog, so the session never asks twice for one decision.
|
|
11
|
+
*
|
|
12
|
+
* The consent sentence and the read-only summary live here because two
|
|
13
|
+
* extensions show them: one copy each is how a trust guarantee drifts apart.
|
|
14
|
+
* Value-only, as `lib/` must be: the state is the caller's context and a closure,
|
|
15
|
+
* never a module variable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { isPlanModeEnabled } from "./plan-state.ts";
|
|
20
|
+
import { PLAN_ENTER_TOOL } from "./tool-names.ts";
|
|
21
|
+
|
|
22
|
+
/** EventBus channel on which a caller asks `plan` to enable plan mode. */
|
|
23
|
+
export const PLAN_ENTER_REQUEST = "my-pi-agent/plan-mode/enter";
|
|
24
|
+
|
|
25
|
+
/** One clause describing the read-only guarantee, reused across prompts. */
|
|
26
|
+
export const READ_ONLY_SUMMARY =
|
|
27
|
+
"write and edit are disabled, bash is limited to read-only git commands, and other mutating tools are blocked; " +
|
|
28
|
+
"subagent delegation is forced read-only, and the only file write is write_plan (under .pi/plans, or the agent " +
|
|
29
|
+
"directory outside a repository)";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The consent question, as the user sees it. Shown by `enter_plan_mode` and by
|
|
33
|
+
* any caller that offers plan mode itself, so both describe the same guarantee.
|
|
34
|
+
*/
|
|
35
|
+
export const PLAN_CONSENT =
|
|
36
|
+
`Plan mode is read-only exploration: ${READ_ONLY_SUMMARY}. ` +
|
|
37
|
+
"Enter plan mode to investigate and propose an approach before making changes?";
|
|
38
|
+
|
|
39
|
+
/** What a caller sends on {@link PLAN_ENTER_REQUEST}. */
|
|
40
|
+
export interface PlanEnterRequest {
|
|
41
|
+
/** The caller's context; `plan` uses it for the footer status. */
|
|
42
|
+
ctx: ExtensionContext;
|
|
43
|
+
/** Called once, synchronously, with whether plan mode is on. */
|
|
44
|
+
respond(entered: boolean): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ask `plan` to turn plan mode on, and report whether it is on afterwards.
|
|
49
|
+
*
|
|
50
|
+
* Resolves `true` without asking when the branch is already planning, and
|
|
51
|
+
* `false` when the `plan` tool is not registered — so `goal` loaded alone keeps
|
|
52
|
+
* working and nothing waits on a listener that is not there. The listener answers
|
|
53
|
+
* synchronously; the microtask is the fallback that guarantees resolution.
|
|
54
|
+
*/
|
|
55
|
+
export function requestPlanMode(
|
|
56
|
+
pi: Pick<ExtensionAPI, "events" | "getAllTools">,
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
): Promise<boolean> {
|
|
59
|
+
if (isPlanModeEnabled(ctx.sessionManager.getBranch())) return Promise.resolve(true);
|
|
60
|
+
if (!pi.getAllTools().some((tool) => tool.name === PLAN_ENTER_TOOL)) return Promise.resolve(false);
|
|
61
|
+
return new Promise<boolean>((resolve) => {
|
|
62
|
+
let settled = false;
|
|
63
|
+
const respond = (entered: boolean): void => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
resolve(entered);
|
|
67
|
+
};
|
|
68
|
+
pi.events.emit(PLAN_ENTER_REQUEST, { ctx, respond } satisfies PlanEnterRequest);
|
|
69
|
+
queueMicrotask(() => respond(false));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan-mode flag as the session branch carries it.
|
|
3
|
+
*
|
|
4
|
+
* Plan mode persists one custom entry (`plan-mode`) whose `enabled` field is the
|
|
5
|
+
* last word on the branch. `plan` writes it; `rewind` reads it through this
|
|
6
|
+
* module so `/review` cannot write while planning, without importing another
|
|
7
|
+
* extension's modules.
|
|
8
|
+
*
|
|
9
|
+
* Value-only: a constant and a pure replay over untrusted branch entries.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Custom-entry type plan mode persists. */
|
|
13
|
+
export const PLAN_MODE_ENTRY_TYPE = "plan-mode";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether the branch's last valid plan-mode entry is enabled.
|
|
17
|
+
*
|
|
18
|
+
* Branch data is untrusted: an entry with the wrong type, a malformed `data`, or
|
|
19
|
+
* a non-boolean `enabled` is skipped rather than read as false. The last valid
|
|
20
|
+
* entry wins, so a later disable turns it off and a `/tree` restore follows the
|
|
21
|
+
* branch it lands on.
|
|
22
|
+
*/
|
|
23
|
+
export function isPlanModeEnabled(entries: readonly unknown[]): boolean {
|
|
24
|
+
let enabled = false;
|
|
25
|
+
for (const raw of entries) {
|
|
26
|
+
if (!raw || typeof raw !== "object") continue;
|
|
27
|
+
const entry = raw as { type?: unknown; customType?: unknown; data?: unknown };
|
|
28
|
+
if (entry.type !== "custom" || entry.customType !== PLAN_MODE_ENTRY_TYPE) continue;
|
|
29
|
+
if (!entry.data || typeof entry.data !== "object") continue;
|
|
30
|
+
const value = (entry.data as { enabled?: unknown }).enabled;
|
|
31
|
+
if (typeof value === "boolean") enabled = value;
|
|
32
|
+
}
|
|
33
|
+
return enabled;
|
|
34
|
+
}
|
package/lib/report.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending-report coordinator: how a live feature tells the model what
|
|
3
|
+
* happened without interrupting it.
|
|
4
|
+
*
|
|
5
|
+
* A watcher match and a finished job follow one rule — report at the next turn,
|
|
6
|
+
* and raise at most one triggered turn when the work opted into `wake`. The two
|
|
7
|
+
* traps are subtle enough to have been re-derived once per extension:
|
|
8
|
+
*
|
|
9
|
+
* - a wake is only safe while the agent is idle, so an outcome that arrives
|
|
10
|
+
* mid-turn must be queued rather than sent;
|
|
11
|
+
* - between the triggered send and the `agent_start` it causes, the agent still
|
|
12
|
+
* looks idle, so a second outcome in that window must not trigger a second
|
|
13
|
+
* turn. `waking` closes that window.
|
|
14
|
+
*
|
|
15
|
+
* Delivery is one shared transcript notice per burst, and a burst is marked
|
|
16
|
+
* reported only after the send succeeded, so a throw leaves it for the next
|
|
17
|
+
* turn. Value-only: every flag lives in the returned closure, never in a module
|
|
18
|
+
* variable, because each extension gets its own module instance.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
22
|
+
import { pruneStaleNotices, type NoticeMessage } from "./notice.ts";
|
|
23
|
+
|
|
24
|
+
export interface PendingReporterOptions<T> {
|
|
25
|
+
/** Send a notice and ask the host to start one turn for it. */
|
|
26
|
+
wake(notice: NoticeMessage): void;
|
|
27
|
+
/** The outcomes not yet reported, oldest first. Does not mark them. */
|
|
28
|
+
pending(): T[];
|
|
29
|
+
/** Mark outcomes as reported, once the send has succeeded. */
|
|
30
|
+
markReported(items: T[]): void;
|
|
31
|
+
/** Build the notice for one burst: model text plus renderer details. */
|
|
32
|
+
buildNotice(items: T[]): NoticeMessage;
|
|
33
|
+
/** The notice's source, so only its own stale entries are pruned. */
|
|
34
|
+
source: string;
|
|
35
|
+
/** Runs after a wake turn was successfully requested, for an extra announcement. */
|
|
36
|
+
onWoke?(items: T[]): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The handlers an extension wires into its lifecycle hooks. */
|
|
40
|
+
export interface PendingReporter {
|
|
41
|
+
/** An outcome arrived: wake a turn if the agent is idle, else leave it queued. */
|
|
42
|
+
report(): void;
|
|
43
|
+
/** A new session starts idle. */
|
|
44
|
+
sessionStart(): void;
|
|
45
|
+
/** A turn began: outcomes queue up, and none of them triggers a turn. */
|
|
46
|
+
agentStart(): void;
|
|
47
|
+
/** The turn settled: a later outcome may wake again. */
|
|
48
|
+
agentSettled(): void;
|
|
49
|
+
/** Report what is queued at the start of a turn, without triggering one. */
|
|
50
|
+
beforeAgentStart(): { message: NoticeMessage } | undefined;
|
|
51
|
+
/** Drop this source's older notices from the outgoing context. */
|
|
52
|
+
filterContext(messages: readonly AgentMessage[]): { messages: AgentMessage[] } | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createPendingReporter<T>(options: PendingReporterOptions<T>): PendingReporter {
|
|
56
|
+
const { wake, pending, markReported, buildNotice, source, onWoke } = options;
|
|
57
|
+
let busy = false;
|
|
58
|
+
let waking = false;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
report: () => {
|
|
62
|
+
if (busy || waking) return;
|
|
63
|
+
const items = pending();
|
|
64
|
+
if (items.length === 0) return;
|
|
65
|
+
try {
|
|
66
|
+
wake(buildNotice(items));
|
|
67
|
+
} catch {
|
|
68
|
+
// Reporting is best-effort; the next turn reports the same items.
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Mark only after the send succeeded, so a throw changes nothing.
|
|
72
|
+
markReported(items);
|
|
73
|
+
waking = true;
|
|
74
|
+
onWoke?.(items);
|
|
75
|
+
},
|
|
76
|
+
sessionStart: () => {
|
|
77
|
+
busy = false;
|
|
78
|
+
waking = false;
|
|
79
|
+
},
|
|
80
|
+
agentStart: () => {
|
|
81
|
+
busy = true;
|
|
82
|
+
},
|
|
83
|
+
agentSettled: () => {
|
|
84
|
+
busy = false;
|
|
85
|
+
waking = false;
|
|
86
|
+
},
|
|
87
|
+
beforeAgentStart: () => {
|
|
88
|
+
const items = pending();
|
|
89
|
+
if (items.length === 0) return undefined;
|
|
90
|
+
const notice = buildNotice(items);
|
|
91
|
+
markReported(items);
|
|
92
|
+
return { message: notice };
|
|
93
|
+
},
|
|
94
|
+
filterContext: (messages) => {
|
|
95
|
+
const pruned = pruneStaleNotices(messages, source);
|
|
96
|
+
return pruned ? { messages: pruned } : undefined;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/lib/requirements.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { configSection, configValueAt, type JsonReadResult, SHARED_CONFIG_FILE } from "./config.ts";
|
|
19
19
|
import type { WebSearchProviderEvent } from "./events.ts";
|
|
20
|
+
import { pdfGuidance } from "./pdf.ts";
|
|
20
21
|
import { getPiInvocation, type PiInvocation } from "./pi-invocation.ts";
|
|
21
22
|
|
|
22
23
|
/** How a check came out. Only `missing` is worth interrupting a session for. */
|
|
@@ -167,19 +168,6 @@ export interface ExtensionReport {
|
|
|
167
168
|
findings: Finding[];
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
// --- Install guidance -------------------------------------------------------
|
|
171
|
-
//
|
|
172
|
-
// Moved here from the extensions that raise it, so `doctor` and the tool error
|
|
173
|
-
// say the same thing. Formatting only: no extension-specific logic.
|
|
174
|
-
|
|
175
|
-
/** Install command for the optional PDF extractor package. */
|
|
176
|
-
const UNPDF_INSTALL = "npm i unpdf";
|
|
177
|
-
|
|
178
|
-
/** Model-facing guidance when the optional `unpdf` package is absent. */
|
|
179
|
-
export function pdfGuidance(): string {
|
|
180
|
-
return `PDF extraction requires the optional 'unpdf' package (${UNPDF_INSTALL}).`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
171
|
// --- The table --------------------------------------------------------------
|
|
184
172
|
|
|
185
173
|
/**
|
|
@@ -224,6 +212,9 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
224
212
|
},
|
|
225
213
|
todo: {},
|
|
226
214
|
goal: {},
|
|
215
|
+
check: {
|
|
216
|
+
notes: ["Runs the project's detected command through a shell; a stack with no detector needs `check.command`."],
|
|
217
|
+
},
|
|
227
218
|
plan: {
|
|
228
219
|
optional: [gitCheck("project-local `.pi/plans` (otherwise plans land in the agent directory)")],
|
|
229
220
|
notes: ["Needs an interactive UI to enter plan mode."],
|
|
@@ -769,19 +760,34 @@ export async function probeExtension(extension: string, seams: ProbeSeams): Prom
|
|
|
769
760
|
return { extension, enabled, findings };
|
|
770
761
|
}
|
|
771
762
|
|
|
763
|
+
/** Probes in flight at once: enough to hide spawn latency, not a burst. */
|
|
764
|
+
export const PROBE_CONCURRENCY = 4;
|
|
765
|
+
|
|
772
766
|
/**
|
|
773
|
-
* Probe every declared extension, or just `names`,
|
|
767
|
+
* Probe every declared extension, or just `names`, keeping declaration order.
|
|
774
768
|
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
769
|
+
* The caller is a user-facing report (`run_doctor`) or the panel's refresh, so
|
|
770
|
+
* the probes run a few at a time: a serial pass pays the sum of every spawn and
|
|
771
|
+
* read, while one `Promise.all` over the whole table starts a burst of child
|
|
772
|
+
* processes. The startup check does not come through here; it probes only the
|
|
773
|
+
* hard requirements, all at once (see `probeRequired`).
|
|
778
774
|
*/
|
|
779
775
|
export async function probeAll(
|
|
780
776
|
seams: ProbeSeams,
|
|
781
777
|
names: readonly string[] = Object.keys(REQUIREMENTS),
|
|
782
778
|
): Promise<ExtensionReport[]> {
|
|
783
|
-
const reports
|
|
784
|
-
|
|
779
|
+
const reports = new Array<ExtensionReport>(names.length);
|
|
780
|
+
let cursor = 0;
|
|
781
|
+
/** Take the next name until the queue is empty; each worker keeps its slot. */
|
|
782
|
+
const drain = async (): Promise<void> => {
|
|
783
|
+
while (cursor < names.length) {
|
|
784
|
+
const index = cursor;
|
|
785
|
+
cursor += 1;
|
|
786
|
+
reports[index] = await probeExtension(names[index], seams);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
const workers = Array.from({ length: Math.min(PROBE_CONCURRENCY, names.length) }, () => drain());
|
|
790
|
+
await Promise.all(workers);
|
|
785
791
|
return reports;
|
|
786
792
|
}
|
|
787
793
|
|
package/lib/spawn.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process seam shared by the extensions that run a shell command.
|
|
3
|
+
*
|
|
4
|
+
* `job`, `watch`, and `check` all start a command in its own process group,
|
|
5
|
+
* stream or discard its output, and kill the whole tree — a signal to the POSIX
|
|
6
|
+
* group, or `taskkill /T` on Windows. Only the policy on top differs (durable
|
|
7
|
+
* jobs, a tailed watcher, a bounded check), so the seam lives here.
|
|
8
|
+
*
|
|
9
|
+
* IO is split from the decision: {@link spawnOptions} and {@link killPlan} are
|
|
10
|
+
* pure and testable on every platform, while {@link defaultSpawn} and
|
|
11
|
+
* {@link defaultKillTree} execute them. Value-only, like every `lib/` module:
|
|
12
|
+
* no module state, and each caller keeps its own children.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn as nodeSpawn, spawnSync } from "node:child_process";
|
|
16
|
+
import type { Readable } from "node:stream";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The writable end of a child's stdin pipe.
|
|
20
|
+
*
|
|
21
|
+
* Only the operations a caller needs, so tests can hand the runtime a recording
|
|
22
|
+
* double instead of a real socket.
|
|
23
|
+
*/
|
|
24
|
+
export interface StdinSink {
|
|
25
|
+
write(chunk: string): boolean;
|
|
26
|
+
end(): void;
|
|
27
|
+
destroy?(): void;
|
|
28
|
+
on(event: "error" | "close", listener: (error?: Error) => void): unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The subset of a spawned child the extensions use. */
|
|
32
|
+
export interface SpawnedProcess {
|
|
33
|
+
pid?: number;
|
|
34
|
+
stdout: Readable | null;
|
|
35
|
+
stderr: Readable | null;
|
|
36
|
+
/** Present only when the child was spawned with `stdin: true`. */
|
|
37
|
+
stdin?: StdinSink | null;
|
|
38
|
+
on(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
39
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
40
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
41
|
+
/** Detach from the parent's event loop, so a child cannot hold Pi open. */
|
|
42
|
+
unref?(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SpawnOptions {
|
|
46
|
+
cwd: string;
|
|
47
|
+
env?: NodeJS.ProcessEnv;
|
|
48
|
+
/** Give the child a stdin pipe instead of a closed stdin (no immediate EOF). */
|
|
49
|
+
stdin?: boolean;
|
|
50
|
+
/** The command writes its own output to a log; keep no parent pipe. */
|
|
51
|
+
detachOutput?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SpawnFn = (command: string, options: SpawnOptions) => SpawnedProcess;
|
|
55
|
+
|
|
56
|
+
/** Send `signal` to a pid's process tree, escalating to SIGKILL when `force`. */
|
|
57
|
+
export type KillTreeFn = (pid: number, signal: NodeJS.Signals, force?: boolean) => void;
|
|
58
|
+
|
|
59
|
+
/** The `stdio` triple a spawn needs: stdin, stdout, stderr. */
|
|
60
|
+
export type SpawnStdio = ["pipe" | "ignore", "pipe" | "ignore", "pipe" | "ignore"];
|
|
61
|
+
|
|
62
|
+
/** The `child_process.spawn` options this seam produces. */
|
|
63
|
+
export interface NodeSpawnOptions {
|
|
64
|
+
cwd: string;
|
|
65
|
+
shell: boolean;
|
|
66
|
+
detached: boolean;
|
|
67
|
+
stdio: SpawnStdio;
|
|
68
|
+
env?: NodeJS.ProcessEnv;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The spawn options for one command: a shell in its own process group, so a
|
|
73
|
+
* signal can reach the shell and its descendants together.
|
|
74
|
+
*
|
|
75
|
+
* Stdin stays closed unless asked for, because a closed stdin gives the child
|
|
76
|
+
* EOF — a `read` exits instead of blocking forever. `detachOutput` keeps no
|
|
77
|
+
* parent pipe, for a child that writes its own log file.
|
|
78
|
+
*/
|
|
79
|
+
export function spawnOptions(options: SpawnOptions, platform: NodeJS.Platform = process.platform): NodeSpawnOptions {
|
|
80
|
+
return {
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
shell: true,
|
|
83
|
+
detached: platform !== "win32",
|
|
84
|
+
stdio: [
|
|
85
|
+
options.stdin ? "pipe" : "ignore",
|
|
86
|
+
options.detachOutput ? "ignore" : "pipe",
|
|
87
|
+
options.detachOutput ? "ignore" : "pipe",
|
|
88
|
+
],
|
|
89
|
+
env: options.env,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What killing a pid's tree means on this platform. */
|
|
94
|
+
export interface KillPlan {
|
|
95
|
+
/** A program to run instead of signalling (Windows has no process group). */
|
|
96
|
+
command?: { file: string; args: string[] };
|
|
97
|
+
/**
|
|
98
|
+
* Pids to signal in order: the process group first, then the pid itself for a
|
|
99
|
+
* child that is not a group leader (a reattached pid, for example).
|
|
100
|
+
*/
|
|
101
|
+
targets: number[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The kill plan for `pid`: `taskkill /T` on Windows, group-then-pid otherwise. */
|
|
105
|
+
export function killPlan(pid: number, force: boolean, platform: NodeJS.Platform = process.platform): KillPlan {
|
|
106
|
+
if (platform === "win32") {
|
|
107
|
+
const args = ["/pid", String(pid), "/T", ...(force ? ["/F"] : [])];
|
|
108
|
+
return { command: { file: "taskkill", args }, targets: [] };
|
|
109
|
+
}
|
|
110
|
+
return { targets: [-pid, pid] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A signal-0 probe; injectable so the EPERM branch is testable. */
|
|
114
|
+
export type KillProbe = (pid: number, signal: number) => unknown;
|
|
115
|
+
|
|
116
|
+
/** Whether a pid is alive on this host. EPERM means it exists but is not ours. */
|
|
117
|
+
export function isPidAlive(pid: number, probe: KillProbe = process.kill): boolean {
|
|
118
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
119
|
+
try {
|
|
120
|
+
probe(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The host seams {@link applyKillPlan} runs through. */
|
|
128
|
+
export interface KillSeams {
|
|
129
|
+
/** Signal one pid; throws when the pid is gone or not ours. */
|
|
130
|
+
kill(pid: number, signal: NodeJS.Signals): unknown;
|
|
131
|
+
/** Run a program (the Windows `taskkill`). */
|
|
132
|
+
run(file: string, args: string[]): unknown;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Execute a {@link KillPlan}: run the command, or signal each target in order.
|
|
137
|
+
*
|
|
138
|
+
* The group attempt may answer `ESRCH` for a pid that is not a group leader, and
|
|
139
|
+
* the final attempt is the fallback, so its failure means "already gone" — a
|
|
140
|
+
* kill of a dead process is not an error a caller can act on.
|
|
141
|
+
*/
|
|
142
|
+
export function applyKillPlan(plan: KillPlan, signal: NodeJS.Signals, seams: KillSeams): void {
|
|
143
|
+
if (plan.command) {
|
|
144
|
+
seams.run(plan.command.file, plan.command.args);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const [index, target] of plan.targets.entries()) {
|
|
148
|
+
const isFallback = index === plan.targets.length - 1;
|
|
149
|
+
try {
|
|
150
|
+
seams.kill(target, signal);
|
|
151
|
+
return;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (!isFallback && (error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The real seams, so a caller needs no adapter. */
|
|
159
|
+
const processSeams: KillSeams = {
|
|
160
|
+
kill: (pid, signal) => process.kill(pid, signal),
|
|
161
|
+
run: (file, args) => spawnSync(file, args, { stdio: "ignore" }),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Default spawn: a shell command in its own process group, output piped. */
|
|
165
|
+
export const defaultSpawn: SpawnFn = (command, options) => nodeSpawn(command, spawnOptions(options));
|
|
166
|
+
|
|
167
|
+
/** Default tree kill: process group on POSIX, `taskkill /T` on Windows. */
|
|
168
|
+
export const defaultKillTree: KillTreeFn = (pid, signal, force = false) => {
|
|
169
|
+
applyKillPlan(killPlan(pid, force), signal, processSeams);
|
|
170
|
+
};
|