@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
package/lib/http/router.ts
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The request-envelope helpers both
|
|
3
|
-
*
|
|
2
|
+
* The request-envelope helpers both servers apply before a route runs: the
|
|
3
|
+
* request target, and the `Host` allowlist.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* DNS-rebinding attempt against a
|
|
7
|
-
* treated is the caller's decision: the
|
|
8
|
-
* router is also driven directly from
|
|
9
|
-
* not a rebinding, while the review
|
|
5
|
+
* A server answers to the names it was bound on and nothing else, so a `Host`
|
|
6
|
+
* naming anything else is a DNS-rebinding attempt against a page the owner
|
|
7
|
+
* opened. How strictly a missing `Host` is treated is the caller's decision: the
|
|
8
|
+
* browse server accepts one, because its router is also driven directly from
|
|
9
|
+
* tests and a plain `GET` with no header is not a rebinding, while the review
|
|
10
|
+
* server fails closed.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
/** The
|
|
13
|
-
const
|
|
13
|
+
/** The names a loopback-bound server answers to. */
|
|
14
|
+
export const LOOPBACK_HOSTS: readonly string[] = ["127.0.0.1", "localhost", "[::1]"];
|
|
15
|
+
|
|
16
|
+
/** The host name in a `Host` header, without its port, lowercased. */
|
|
17
|
+
export function hostHeaderName(host: string): string {
|
|
18
|
+
const text = host.trim().toLowerCase();
|
|
19
|
+
if (text.startsWith("[")) {
|
|
20
|
+
const end = text.indexOf("]");
|
|
21
|
+
return end > 0 ? text.slice(0, end + 1) : text;
|
|
22
|
+
}
|
|
23
|
+
const first = text.indexOf(":");
|
|
24
|
+
// No colon, or an unbracketed IPv6 address whose colons are not a port.
|
|
25
|
+
if (first <= 0 || first !== text.lastIndexOf(":")) return text;
|
|
26
|
+
return /^\d+$/.test(text.slice(first + 1)) ? text.slice(0, first) : text;
|
|
27
|
+
}
|
|
14
28
|
|
|
15
29
|
/** A request target split into the parts a router matches on. */
|
|
16
30
|
export interface ParsedRequestUrl {
|
|
@@ -35,18 +49,31 @@ export function parseRequestUrl(rawUrl: string): ParsedRequestUrl | undefined {
|
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
/**
|
|
38
|
-
* Whether the request's `Host` names this
|
|
52
|
+
* Whether the request's `Host` names one of the hosts this server answers to.
|
|
39
53
|
*
|
|
40
54
|
* With `required: false`, only a present, non-empty foreign name is refused, so
|
|
41
55
|
* a header-less request from another caller still routes. With `required: true`,
|
|
42
56
|
* a missing or non-string `Host` is refused too, so the guard cannot be skipped
|
|
43
57
|
* by omitting the header.
|
|
44
58
|
*/
|
|
45
|
-
export function
|
|
59
|
+
export function isAllowedHostHeader(
|
|
46
60
|
headers: Record<string, string | string[] | undefined>,
|
|
47
|
-
options: { required: boolean },
|
|
61
|
+
options: { hosts: readonly string[]; required: boolean },
|
|
48
62
|
): boolean {
|
|
49
63
|
const host = headers.host;
|
|
50
64
|
if (typeof host !== "string" || host === "") return !options.required;
|
|
51
|
-
|
|
65
|
+
const name = hostHeaderName(host);
|
|
66
|
+
return options.hosts.some((allowed) => hostHeaderName(allowed) === name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether the request's `Host` names this loopback server. A caller that binds
|
|
71
|
+
* anything other than loopback passes its own names to
|
|
72
|
+
* {@link isAllowedHostHeader} instead.
|
|
73
|
+
*/
|
|
74
|
+
export function isLocalHostHeader(
|
|
75
|
+
headers: Record<string, string | string[] | undefined>,
|
|
76
|
+
options: { required: boolean },
|
|
77
|
+
): boolean {
|
|
78
|
+
return isAllowedHostHeader(headers, { hosts: LOOPBACK_HOSTS, required: options.required });
|
|
52
79
|
}
|
package/lib/http/server.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The socket seam shared by the
|
|
3
|
-
*
|
|
2
|
+
* The socket seam shared by the browser servers: bind, write a response out,
|
|
3
|
+
* and close.
|
|
4
4
|
*
|
|
5
5
|
* Everything here is behavior a caller cannot get right by accident: a bind on
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* an address the caller names (loopback unless it asked for more), an
|
|
7
|
+
* accept-level `error` listener so a transient failure cannot take the host
|
|
8
|
+
* down, a `HEAD` that sends headers and no body, a `filePath` response streamed
|
|
9
|
+
* instead of buffered, a `stream` response opened as an event stream, and a
|
|
10
|
+
* close that is safe to call twice and drops keep-alive connections. What each
|
|
11
|
+
* server does *around* this — which frames an event stream carries, request
|
|
12
|
+
* bodies, session settling — stays with that server.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { createReadStream } from "node:fs";
|
|
@@ -16,15 +17,19 @@ import type { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
|
16
17
|
import type { ServeResponse } from "./response.ts";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* Bind `server` to `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Bind `server` to `host:port` and resolve the port it actually bound (`0` asks
|
|
21
|
+
* the OS to pick one). Rejects when the bind fails, so a caller that wants a
|
|
22
|
+
* fallback can retry with another port.
|
|
23
|
+
*
|
|
24
|
+
* `host` is the address the caller is about to print, never a wildcard by
|
|
25
|
+
* default: a review shared on a LAN opens exactly one address, so the link the
|
|
26
|
+
* owner sees is the whole surface.
|
|
22
27
|
*/
|
|
23
|
-
export async function
|
|
28
|
+
export async function bindHost(server: Server, host: string, port: number): Promise<number> {
|
|
24
29
|
await new Promise<void>((resolve, reject) => {
|
|
25
30
|
const onError = (error: Error) => reject(error);
|
|
26
31
|
server.once("error", onError);
|
|
27
|
-
server.listen(port,
|
|
32
|
+
server.listen(port, host, () => {
|
|
28
33
|
server.off("error", onError);
|
|
29
34
|
resolve();
|
|
30
35
|
});
|
|
@@ -37,6 +42,15 @@ export async function bindLoopback(server: Server, port: number): Promise<number
|
|
|
37
42
|
return typeof address === "object" && address ? address.port : port;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Bind `server` to `127.0.0.1:port` and resolve the port it actually bound
|
|
47
|
+
* (`0` asks the OS to pick one). Rejects when the bind fails, so a caller that
|
|
48
|
+
* wants a fallback can retry with another port.
|
|
49
|
+
*/
|
|
50
|
+
export async function bindLoopback(server: Server, port: number): Promise<number> {
|
|
51
|
+
return bindHost(server, "127.0.0.1", port);
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* Write one routed response: headers first, then no body for `HEAD`, the file
|
|
42
56
|
* for a `filePath` response, or the buffered body.
|
package/lib/interaction.ts
CHANGED
|
@@ -20,6 +20,9 @@ import type { ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-co
|
|
|
20
20
|
/** The host run modes, derived because the package does not export the union. */
|
|
21
21
|
export type InteractionMode = ExtensionContext["mode"];
|
|
22
22
|
|
|
23
|
+
/** The two languages a remote prompt is rendered in. */
|
|
24
|
+
export type InteractionLanguage = "zh" | "en";
|
|
25
|
+
|
|
23
26
|
/** The `ctx.ui` property a remote transport marks with the channel that owns its turn. */
|
|
24
27
|
export const INTERACTION_TURN_KEY = "isRemoteTurn";
|
|
25
28
|
|
|
@@ -59,6 +62,14 @@ export interface InteractionChannel {
|
|
|
59
62
|
post(text: string, signal?: AbortSignal): Promise<void>;
|
|
60
63
|
/** Send a local file to the human; resolves true when delivered. */
|
|
61
64
|
postFile(path: string, name: string, signal?: AbortSignal): Promise<boolean>;
|
|
65
|
+
/** The language this channel renders prompts in, when it knows one. */
|
|
66
|
+
language?(): InteractionLanguage;
|
|
67
|
+
/**
|
|
68
|
+
* Widen the bound on the prompts asked inside the returned scope: use it for a
|
|
69
|
+
* dialog the human must read something before answering. Returns the restore,
|
|
70
|
+
* and a transport with no such policy omits the method.
|
|
71
|
+
*/
|
|
72
|
+
setPromptTimeout?(ms: number): () => void;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
/** The slice of an extension context {@link remoteChannel} reads. */
|
|
@@ -85,6 +96,11 @@ export function isRemoteTurn(ctx: UIContextCarrier): boolean {
|
|
|
85
96
|
return remoteChannel(ctx) !== undefined;
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
/** The remote channel's prompt language, or undefined for a local turn. */
|
|
100
|
+
export function remoteLanguage(ctx: UIContextCarrier): InteractionLanguage | undefined {
|
|
101
|
+
return remoteChannel(ctx)?.language?.();
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
/** Whether a full-screen `ctx.ui.custom` component may be used for this turn. */
|
|
89
105
|
export function supportsCustomUI(ctx: UIContextCarrier): boolean {
|
|
90
106
|
return ctx.mode === "tui" && !isRemoteTurn(ctx);
|
|
@@ -105,6 +121,12 @@ export interface AskHumanOptions<T> {
|
|
|
105
121
|
* attachment may never be opened (a phone client can hide a file bubble).
|
|
106
122
|
*/
|
|
107
123
|
note?: string;
|
|
124
|
+
/**
|
|
125
|
+
* How long a remote channel may wait for the answer before dismissing the
|
|
126
|
+
* dialog. Raise it when the human has to read an artifact first; the channel's
|
|
127
|
+
* own bound applies when it is omitted or unsupported.
|
|
128
|
+
*/
|
|
129
|
+
timeoutMs?: number;
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
/**
|
|
@@ -138,5 +160,10 @@ export async function askHuman<T>(
|
|
|
138
160
|
// The preface is best-effort; the dialogs still run.
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
|
-
|
|
163
|
+
const restore = options.timeoutMs === undefined ? undefined : channel?.setPromptTimeout?.(options.timeoutMs);
|
|
164
|
+
try {
|
|
165
|
+
return await options.dialogs();
|
|
166
|
+
} finally {
|
|
167
|
+
restore?.();
|
|
168
|
+
}
|
|
142
169
|
}
|
package/lib/notice.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*
|
|
12
12
|
* `content` is what the model reads; `details` is never sent to the model and
|
|
13
13
|
* is the renderer's whole input, so a producer states the facts once and the
|
|
14
|
-
* renderer decides the look.
|
|
15
|
-
*
|
|
14
|
+
* renderer decides the look. `burstText` is the one assembly of that content for
|
|
15
|
+
* a burst of pending outcomes, so a job completion, a watch match, and a bus
|
|
16
|
+
* message are laid out by the same code. Value-only: constants and pure
|
|
17
|
+
* functions, no state.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
@@ -68,6 +70,31 @@ export function noticeMessage(input: NoticeInput): NoticeMessage {
|
|
|
68
70
|
return { customType: NOTICE_TYPE, content: input.content, display: true, details: input.details };
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/** The parts of one burst of pending outcomes, as the model reads them. */
|
|
74
|
+
export interface BurstTextInput {
|
|
75
|
+
/** The marker embedded in the injected context, e.g. `[BACKGROUND JOBS]`. */
|
|
76
|
+
marker: string;
|
|
77
|
+
/** The counted headline without its colon, e.g. `2 background jobs finished`. */
|
|
78
|
+
headline: string;
|
|
79
|
+
/** One line per outcome, already formatted by the caller. */
|
|
80
|
+
lines: readonly string[];
|
|
81
|
+
/** A trailing count of the rows the caller left out, e.g. `… 3 more matches`. */
|
|
82
|
+
more?: string;
|
|
83
|
+
/** How to read the rest, e.g. 'Run `job logs <id>` for output.'. */
|
|
84
|
+
hint?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The model-facing text of a burst: marker, counted headline, one line per
|
|
89
|
+
* outcome, and the caller's own tail. The joins and the colon live here so a
|
|
90
|
+
* `job` completion, a `watch` match, and a bus message read alike; the wording,
|
|
91
|
+
* the cap, and the hint stay with the extension that knows them.
|
|
92
|
+
*/
|
|
93
|
+
export function burstText(input: BurstTextInput): string {
|
|
94
|
+
const { marker, headline, lines, more, hint } = input;
|
|
95
|
+
return [marker, `${headline}:`, ...lines, ...(more ? [more] : []), ...(hint ? [hint] : [])].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
/** Whether a conversation message is a notice. */
|
|
72
99
|
export function isNotice(message: AgentMessage): message is NoticeEntry {
|
|
73
100
|
return message.role === "custom" && (message as { customType?: string }).customType === NOTICE_TYPE;
|
package/lib/plan-entry.ts
CHANGED
|
@@ -25,8 +25,8 @@ export const PLAN_ENTER_REQUEST = "my-pi-agent/plan-mode/enter";
|
|
|
25
25
|
/** One clause describing the read-only guarantee, reused across prompts. */
|
|
26
26
|
export const READ_ONLY_SUMMARY =
|
|
27
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 (
|
|
29
|
-
"directory
|
|
28
|
+
"subagent delegation is forced read-only, and the only file write is write_plan (a plan file in the agent " +
|
|
29
|
+
"directory)";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* The consent question, as the user sees it. Shown by `enter_plan_mode` and by
|
|
@@ -36,6 +36,11 @@ export const PLAN_CONSENT =
|
|
|
36
36
|
`Plan mode is read-only exploration: ${READ_ONLY_SUMMARY}. ` +
|
|
37
37
|
"Enter plan mode to investigate and propose an approach before making changes?";
|
|
38
38
|
|
|
39
|
+
/** The short remote consent; the full read-only summary stays in the tool docs. */
|
|
40
|
+
export const PLAN_CONSENT_SHORT =
|
|
41
|
+
"Enter read-only plan mode? Writes and mutating tools are disabled until you approve a plan.";
|
|
42
|
+
export const PLAN_CONSENT_SHORT_ZH = "进入只读 plan 模式?批准计划前会禁用写入和修改。";
|
|
43
|
+
|
|
39
44
|
/** What a caller sends on {@link PLAN_ENTER_REQUEST}. */
|
|
40
45
|
export interface PlanEnterRequest {
|
|
41
46
|
/** The caller's context; `plan` uses it for the footer status. */
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-document sections (pure).
|
|
3
|
+
*
|
|
4
|
+
* One fence-aware scan of a plan's markdown, and one rewrite of a section in
|
|
5
|
+
* place. Two features need it: plan mode, whose review screen navigates by
|
|
6
|
+
* section and whose `set_plan_steps` rewrites `## Steps`, and the split, which
|
|
7
|
+
* writes and replaces `## Team` at approval. A second scanner would disagree
|
|
8
|
+
* about a heading inside a fence, so both go through this module.
|
|
9
|
+
*
|
|
10
|
+
* The scan is deliberately smaller than CommonMark: a heading is up to three
|
|
11
|
+
* leading spaces, a space after the hashes, and an optional closing run; a fence
|
|
12
|
+
* is three backticks or tildes anywhere a fence may start. Inside a fence
|
|
13
|
+
* nothing is a heading, which is what keeps a `# comment` in a shell example out
|
|
14
|
+
* of the outline. Every section keeps its raw source lines, so a caller can edit
|
|
15
|
+
* exactly what it read.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A heading-delimited chunk of a plan, for navigation and in-place edits. */
|
|
19
|
+
export interface PlanSection {
|
|
20
|
+
/** Plain heading text, or `""` for the content before the first heading. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** The section's markdown, including its heading line. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** 0-based source line of the section's heading (0 for the preamble). */
|
|
25
|
+
line: number;
|
|
26
|
+
/** Heading level, or 0 for the preamble before the first heading. */
|
|
27
|
+
level: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A markdown heading outside a fence, capturing its level and text: up to three
|
|
32
|
+
* leading spaces, a space after the hashes, and a closing hash run only when a
|
|
33
|
+
* space separates it. Shared with the block model, so the section split and the
|
|
34
|
+
* browser outline read a heading line the same way, and `## C#` keeps its title.
|
|
35
|
+
*/
|
|
36
|
+
export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A markdown code-fence opener or closer (backticks or tildes). Shared with the
|
|
40
|
+
* block model: a tab-indented fence is prose for both parsers, never code for one
|
|
41
|
+
* and prose for the other.
|
|
42
|
+
*/
|
|
43
|
+
export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A heading or step label as the review surfaces compare it: inline emphasis and
|
|
47
|
+
* code removed, whitespace collapsed. Exported so a renderer labels an outline
|
|
48
|
+
* entry the same way {@link extractSections} labels its section, which is what
|
|
49
|
+
* lets the browser mark a changed section.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeHeadingText(text: string): string {
|
|
52
|
+
return text
|
|
53
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
54
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
55
|
+
.replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
|
|
56
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sections of a plan, split at heading lines outside code fences. The content
|
|
63
|
+
* before the first heading is a section titled `""`. Pi's `Markdown` renderer
|
|
64
|
+
* exposes no source-line map, so the review screen renders these chunks
|
|
65
|
+
* separately to know where each section begins.
|
|
66
|
+
*/
|
|
67
|
+
export function extractSections(plan: string): PlanSection[] {
|
|
68
|
+
const sections: PlanSection[] = [];
|
|
69
|
+
let current: PlanSection | undefined;
|
|
70
|
+
let fence: string | undefined;
|
|
71
|
+
|
|
72
|
+
const flush = (): void => {
|
|
73
|
+
if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
|
|
74
|
+
current = undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
plan.split("\n").forEach((line, index) => {
|
|
78
|
+
const fenceMatch = line.match(FENCE);
|
|
79
|
+
if (fenceMatch) {
|
|
80
|
+
if (fence === undefined) fence = fenceMatch[1][0];
|
|
81
|
+
else if (fence === fenceMatch[1][0]) fence = undefined;
|
|
82
|
+
} else if (fence === undefined) {
|
|
83
|
+
const heading = line.match(HEADING);
|
|
84
|
+
if (heading) {
|
|
85
|
+
flush();
|
|
86
|
+
current = { title: normalizeHeadingText(heading[2]), text: line, line: index, level: heading[1].length };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
|
|
91
|
+
else current.text += `\n${line}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
flush();
|
|
95
|
+
return sections;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The index one past a section's last line: the next heading at its level or
|
|
100
|
+
* shallower, outside a fence, or the end of the document.
|
|
101
|
+
*/
|
|
102
|
+
function sectionEnd(lines: readonly string[], start: number): number {
|
|
103
|
+
const level = lines[start].match(HEADING)?.[1].length ?? 2;
|
|
104
|
+
let fence: string | undefined;
|
|
105
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
106
|
+
const line = lines[index];
|
|
107
|
+
const fenceMatch = line.match(FENCE);
|
|
108
|
+
if (fenceMatch) {
|
|
109
|
+
if (fence === undefined) fence = fenceMatch[1][0];
|
|
110
|
+
else if (fence === fenceMatch[1][0]) fence = undefined;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (fence !== undefined) continue;
|
|
114
|
+
const heading = line.match(HEADING);
|
|
115
|
+
if (heading && heading[1].length <= level) return index;
|
|
116
|
+
}
|
|
117
|
+
return lines.length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Replace a level-2 section's body, or remove the section, or append one.
|
|
122
|
+
*
|
|
123
|
+
* `body` is the section's markdown after its heading, without the heading line —
|
|
124
|
+
* the caller keeps the heading text so a refinement cannot rename the section
|
|
125
|
+
* that other code looks for. An empty `body` removes the whole section, which is
|
|
126
|
+
* how a team stops existing; a missing section with a body appends one, so the
|
|
127
|
+
* write is idempotent rather than order-dependent.
|
|
128
|
+
*/
|
|
129
|
+
export function replaceSection(plan: string, title: string, body: string | undefined): string {
|
|
130
|
+
const lines = plan.split("\n");
|
|
131
|
+
const wanted = normalizeHeadingText(title).toLowerCase();
|
|
132
|
+
const sections = extractSections(plan);
|
|
133
|
+
const found = sections.find(
|
|
134
|
+
(candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === wanted,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!found) {
|
|
138
|
+
if (body === undefined) return plan;
|
|
139
|
+
const prefix = plan.trim() === "" ? "" : `${plan.replace(/\s+$/, "")}\n\n`;
|
|
140
|
+
return `${prefix}## ${title}\n${body.replace(/\s+$/, "")}\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const end = sectionEnd(lines, found.line);
|
|
144
|
+
const head = lines.slice(0, found.line);
|
|
145
|
+
const tail = lines.slice(end);
|
|
146
|
+
if (body === undefined) {
|
|
147
|
+
// Drop the blank line that separated the section from what follows, so a
|
|
148
|
+
// removed section leaves one gap rather than two.
|
|
149
|
+
const joined = [...head, ...tail];
|
|
150
|
+
return (
|
|
151
|
+
joined
|
|
152
|
+
.join("\n")
|
|
153
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
154
|
+
.replace(/\s+$/, "") + (plan.endsWith("\n") ? "\n" : "")
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const replacement = [`## ${title}`, "", ...body.replace(/\s+$/, "").split("\n")];
|
|
158
|
+
// Keep one blank line between the rewritten section and whatever followed it.
|
|
159
|
+
const gap = tail.length > 0 && tail[0].trim() !== "" ? [""] : [];
|
|
160
|
+
return [...head, ...replacement, ...gap, ...tail].join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The `## Team` section's body, without its heading or surrounding blank lines. */
|
|
164
|
+
export function teamSectionBody(plan: string): string | undefined {
|
|
165
|
+
const lines = plan.split("\n");
|
|
166
|
+
const section = extractSections(plan).find(
|
|
167
|
+
(candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === "team",
|
|
168
|
+
);
|
|
169
|
+
if (!section) return undefined;
|
|
170
|
+
const body = lines.slice(section.line + 1, sectionEnd(lines, section.line)).join("\n");
|
|
171
|
+
return body.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
172
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The switch that turns the split on, and the words the user flips it with.
|
|
3
|
+
*
|
|
4
|
+
* It lives in `lib/` because three places must agree: the settings row that
|
|
5
|
+
* writes it, plan approval that reads it, and the bus that refuses a join while
|
|
6
|
+
* it is off. A second copy of the key would mean a panel row that changes
|
|
7
|
+
* nothing — the failure this module exists to prevent.
|
|
8
|
+
*
|
|
9
|
+
* Off by default: the split changes what approval means and adds work nobody
|
|
10
|
+
* asked for, so it ships closed and an existing user upgrades into the single
|
|
11
|
+
* flow they already know.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { asBoolean, configValueAt, loadConfigSection } from "../config.ts";
|
|
15
|
+
|
|
16
|
+
/** The shared document's section and key that hold the switch. */
|
|
17
|
+
export const SPLIT_SECTION = "plan";
|
|
18
|
+
export const SPLIT_KEY = "split";
|
|
19
|
+
export const SPLIT_DEFAULT = false;
|
|
20
|
+
|
|
21
|
+
/** The settings-panel row, so a refusal can name what the user flips. */
|
|
22
|
+
export const SPLIT_SETTING_ID = "plan.split";
|
|
23
|
+
export const SPLIT_SETTING_LABEL = "Split plans into slices";
|
|
24
|
+
|
|
25
|
+
/** The section's shape and its default, so a loader and its documentation agree. */
|
|
26
|
+
export interface SplitConfig {
|
|
27
|
+
split: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_SPLIT_CONFIG: SplitConfig = { split: SPLIT_DEFAULT };
|
|
31
|
+
|
|
32
|
+
/** Read the section over `base`, ignoring a value that is not a boolean. */
|
|
33
|
+
export function normalizeSplitConfig(raw: Record<string, unknown> | undefined, base: SplitConfig): SplitConfig {
|
|
34
|
+
return { split: asBoolean(configValueAt(raw, [SPLIT_KEY]), base.split) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether this repository (or the user's global config) has the split on.
|
|
39
|
+
*
|
|
40
|
+
* The project file is read at `root` when the caller knows it: a split belongs to
|
|
41
|
+
* the repository, so a session started in a subdirectory must honour what the
|
|
42
|
+
* repository turned on. Without a root the session's cwd is used, which is what a
|
|
43
|
+
* caller outside a repository has.
|
|
44
|
+
*/
|
|
45
|
+
export function splitEnabled(cwd: string, root?: string): boolean {
|
|
46
|
+
return loadConfigSection(root ?? cwd, SPLIT_SECTION, DEFAULT_SPLIT_CONFIG, normalizeSplitConfig).split;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The one line a refusal or a note uses to say the switch is off and where it lives. */
|
|
50
|
+
export const SPLIT_OFF_NOTE =
|
|
51
|
+
`Parallel slices are off (${SPLIT_SETTING_ID}). ` +
|
|
52
|
+
`Enable "${SPLIT_SETTING_LABEL}" in /bytefly settings to split a plan.`;
|