@gavin-hu/my-pi-agent 0.11.4 → 0.12.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 +13 -8
- package/README.zh-CN.md +5 -3
- package/assets/chrome.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +291 -207
- package/extensions/bytefly/doctor/runtime.ts +16 -0
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/segments.ts +3 -2
- package/extensions/bytefly/footer/types.ts +1 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +13 -3
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +201 -38
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +32 -0
- package/extensions/bytefly/update/register.ts +72 -0
- package/extensions/bytefly/update/state.ts +28 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +2 -2
- package/extensions/document/tool.ts +1 -3
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +39 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/memory/README.md +1 -1
- package/extensions/plan/README.md +24 -8
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +18 -12
- package/extensions/plan/plans.ts +1 -1
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +243 -0
- package/extensions/plan/runtime.ts +4 -0
- package/extensions/plan/steps.ts +34 -23
- package/extensions/plan/todos.ts +5 -58
- package/extensions/plan/tools.ts +66 -222
- package/extensions/plan/tui.ts +17 -9
- package/extensions/plan/types.ts +3 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +5 -0
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +100 -36
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/subagent/index.ts +0 -2
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/fetch/tool.ts +1 -3
- package/extensions/web-access/search/tool.ts +1 -3
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/lib/README.md +2 -1
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/requirements.ts +76 -2
- package/lib/todo-state.ts +62 -0
- package/lib/tui/glyphs.ts +1 -0
- package/package.json +1 -1
- package/assets/bytefly.schema.json +0 -785
package/extensions/todo/nudge.ts
CHANGED
|
@@ -18,12 +18,15 @@ import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
|
18
18
|
import { currentTodo, hasOpenTodos } from "./state.ts";
|
|
19
19
|
import type { Todo } from "./types.ts";
|
|
20
20
|
|
|
21
|
+
/** Tools that run a command, where reading and writing look the same by name. */
|
|
22
|
+
const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set(["bash", "powershell"]);
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* Tools whose calls mean the working tree may have changed: the built-in
|
|
23
|
-
* writers and shells. Kept explicit so a reader or an MCP tool that does not
|
|
26
|
+
* writers and the shells. Kept explicit so a reader or an MCP tool that does not
|
|
24
27
|
* declare a hint cannot force a continuation.
|
|
25
28
|
*/
|
|
26
|
-
export const MUTATING_TOOL_NAMES: ReadonlySet<string> = new Set(["write", "edit",
|
|
29
|
+
export const MUTATING_TOOL_NAMES: ReadonlySet<string> = new Set(["write", "edit", ...SHELL_TOOL_NAMES]);
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Whether a finished tool call counts as work for lag detection. The `todo`
|
|
@@ -37,22 +40,68 @@ export function isMutatingTool(toolName: string, annotations?: ToolAnnotations):
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
|
-
* Whether a
|
|
41
|
-
*
|
|
43
|
+
* Whether a finished tool call can have changed files, as opposed to a shell
|
|
44
|
+
* command that may only have read them. The reminder for a run that never
|
|
45
|
+
* started a list needs this: three `rg` calls are investigation, not unplanned
|
|
46
|
+
* work, and a shell call cannot be told apart from one that wrote. So it is a
|
|
47
|
+
* mutating call that is not a shell.
|
|
48
|
+
*/
|
|
49
|
+
export function isFileWrite(toolName: string, annotations?: ToolAnnotations): boolean {
|
|
50
|
+
if (toolName === TODO_TOOL || SHELL_TOOL_NAMES.has(toolName)) return false;
|
|
51
|
+
return isMutatingTool(toolName, annotations);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Successful mutating calls needed before a run with no list at all is worth a
|
|
56
|
+
* reminder. A plan that was never started has no item to fall behind, and a
|
|
57
|
+
* false positive costs a whole extra model request, so one edit is not enough
|
|
58
|
+
* evidence that the work needed a list.
|
|
59
|
+
*/
|
|
60
|
+
export const EMPTY_LIST_MUTATIONS = 3;
|
|
61
|
+
|
|
62
|
+
/** What the current turn knows about its work and the list. */
|
|
63
|
+
export interface NudgeState {
|
|
64
|
+
/** Successful mutating calls since the turn began or the list was last written. */
|
|
65
|
+
mutations: number;
|
|
66
|
+
/** How many of those can change files, as opposed to a shell command that may have read. */
|
|
67
|
+
writes: number;
|
|
68
|
+
/** Whether the model wrote or cleared the list since the turn began. */
|
|
69
|
+
touched: boolean;
|
|
70
|
+
/** Whether a reminder was already sent this turn. */
|
|
71
|
+
nudged: boolean;
|
|
72
|
+
/** The current list. */
|
|
73
|
+
todos: Todo[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a reminder is due. With an unfinished item, one mutation is enough.
|
|
78
|
+
* With no list at all, the turn must also have written something: repeated
|
|
79
|
+
* inspection with no file change is not unplanned work. A list the model wrote
|
|
80
|
+
* or cleared this turn is its own answer, so an explicit clear is never
|
|
81
|
+
* second-guessed.
|
|
42
82
|
*/
|
|
43
|
-
export function shouldNudge(
|
|
44
|
-
|
|
83
|
+
export function shouldNudge({ mutations, writes, touched, nudged, todos }: NudgeState): boolean {
|
|
84
|
+
if (nudged || mutations <= 0) return false;
|
|
85
|
+
if (hasOpenTodos(todos)) return true;
|
|
86
|
+
return todos.length === 0 && !touched && writes > 0 && mutations >= EMPTY_LIST_MUTATIONS;
|
|
45
87
|
}
|
|
46
88
|
|
|
47
89
|
/** Marker on the injected reminder, so it is recognizable and filterable. */
|
|
48
90
|
export const TODO_NUDGE_MARKER = "[TODO SYNC]";
|
|
49
91
|
|
|
50
92
|
/**
|
|
51
|
-
* Model-facing reminder injected before settlement.
|
|
52
|
-
*
|
|
53
|
-
* whole list
|
|
93
|
+
* Model-facing reminder injected before settlement. For a list with unfinished
|
|
94
|
+
* work it points at the item the list currently considers active, so the model
|
|
95
|
+
* can correct it without rereading the whole list; for no list at all it asks
|
|
96
|
+
* for one.
|
|
54
97
|
*/
|
|
55
98
|
export function formatNudge(todos: Todo[]): string {
|
|
99
|
+
if (todos.length === 0) {
|
|
100
|
+
return (
|
|
101
|
+
`${TODO_NUDGE_MARKER} Work happened but no todo list was recorded. ` +
|
|
102
|
+
"Call the todo tool with the steps of this work, then continue."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
56
105
|
const current = currentTodo(todos);
|
|
57
106
|
const item = current ? (current.activeForm ?? current.content) : undefined;
|
|
58
107
|
const pointer = item ? ` The list still points at "${item}".` : "";
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The list is reconstructed from the active branch on session start and tree
|
|
6
6
|
* navigation, and the persistent widget mirrors it whenever it changes. Only
|
|
7
|
-
* interactive (`tui`) sessions get a widget;
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* interactive (`tui`) sessions get a widget; whether a finished list stays
|
|
8
|
+
* visible comes from the todo config. The same runtime carries the lag-tracking
|
|
9
|
+
* counters the `agent_before_settle` reminder reads, so they reset with the
|
|
10
|
+
* session and the branch.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -27,8 +27,8 @@ export interface TodoRuntime {
|
|
|
27
27
|
reconstruct(ctx: ExtensionContext): void;
|
|
28
28
|
/** Apply the widget config (loaded per session). */
|
|
29
29
|
setConfig(config: TodoConfig): void;
|
|
30
|
-
/** Record
|
|
31
|
-
noteWork(): void;
|
|
30
|
+
/** Record a successful mutating call; `fileWrite` marks one that can change files. */
|
|
31
|
+
noteWork(fileWrite?: boolean): void;
|
|
32
32
|
/** Whether a lag reminder is due and has not been sent for it yet. */
|
|
33
33
|
nudgeDue(): boolean;
|
|
34
34
|
/** Mark the current lag as reminded, activating the injected reminder. */
|
|
@@ -53,11 +53,15 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
53
53
|
let lastTuiCtx: ExtensionContext | undefined;
|
|
54
54
|
// True while a dock screen owns the editor slot; the rail stays hidden.
|
|
55
55
|
let suppressed = false;
|
|
56
|
-
// Lag tracking: `
|
|
57
|
-
//
|
|
58
|
-
// `
|
|
59
|
-
//
|
|
60
|
-
|
|
56
|
+
// Lag tracking: `mutations` counts successful mutating calls since the list
|
|
57
|
+
// was last written and `writes` how many of those can change files,
|
|
58
|
+
// `touched` records that the model wrote or cleared the list this turn (so an
|
|
59
|
+
// explicit clear is not second-guessed), `nudged` caps the reminder to one per
|
|
60
|
+
// user turn, and `nudgeActive` marks an injected reminder as current so
|
|
61
|
+
// `context` keeps it for the continuation and drops it on the next user turn.
|
|
62
|
+
let mutations = 0;
|
|
63
|
+
let writes = 0;
|
|
64
|
+
let touched = false;
|
|
61
65
|
let nudged = false;
|
|
62
66
|
let nudgeActive = false;
|
|
63
67
|
// Screens watching the list (for example the open `/todos` screen) so they
|
|
@@ -104,13 +108,17 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
104
108
|
todos = next;
|
|
105
109
|
// A todo update is the model syncing the list, so the lag is cleared.
|
|
106
110
|
// `nudged` is not: the reminder stays capped at one per user turn.
|
|
107
|
-
|
|
111
|
+
mutations = 0;
|
|
112
|
+
writes = 0;
|
|
113
|
+
touched = true;
|
|
108
114
|
syncWidget(ctx);
|
|
109
115
|
notify();
|
|
110
116
|
},
|
|
111
117
|
reconstruct: (ctx) => {
|
|
112
118
|
todos = reconstructTodos(ctx.sessionManager.getBranch());
|
|
113
|
-
|
|
119
|
+
mutations = 0;
|
|
120
|
+
writes = 0;
|
|
121
|
+
touched = false;
|
|
114
122
|
nudged = false;
|
|
115
123
|
nudgeActive = false;
|
|
116
124
|
syncWidget(ctx);
|
|
@@ -119,10 +127,11 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
119
127
|
setConfig: (next) => {
|
|
120
128
|
config = next;
|
|
121
129
|
},
|
|
122
|
-
noteWork: () => {
|
|
123
|
-
|
|
130
|
+
noteWork: (fileWrite = false) => {
|
|
131
|
+
mutations++;
|
|
132
|
+
if (fileWrite) writes++;
|
|
124
133
|
},
|
|
125
|
-
nudgeDue: () => shouldNudge(
|
|
134
|
+
nudgeDue: () => shouldNudge({ mutations, writes, touched, nudged, todos }),
|
|
126
135
|
markNudged: () => {
|
|
127
136
|
nudged = true;
|
|
128
137
|
nudgeActive = true;
|
|
@@ -132,7 +141,9 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
132
141
|
},
|
|
133
142
|
isNudgeActive: () => nudgeActive,
|
|
134
143
|
resetNudge: () => {
|
|
135
|
-
|
|
144
|
+
mutations = 0;
|
|
145
|
+
writes = 0;
|
|
146
|
+
touched = false;
|
|
136
147
|
nudged = false;
|
|
137
148
|
},
|
|
138
149
|
onChange: (listener) => {
|
|
@@ -96,7 +96,7 @@ and unknown keys are ignored.
|
|
|
96
96
|
{
|
|
97
97
|
"search": {
|
|
98
98
|
"provider": "auto", // "auto" | "duckduckgo" | "searxng" | "brave"
|
|
99
|
-
"endpoint": "
|
|
99
|
+
"endpoint": "", // SearXNG base URL, e.g. "http://localhost:8080"; "" means SearXNG is unavailable
|
|
100
100
|
"language": "auto", // SearXNG: "auto" (zh-CN for Han, else en) | language code
|
|
101
101
|
"safeSearch": 0, // SearXNG: 0 off, 1 moderate, 2 strict
|
|
102
102
|
"categories": "general", // SearXNG categories
|
|
@@ -264,9 +264,9 @@ the content type and size.
|
|
|
264
264
|
|
|
265
265
|
| Integration point | Value |
|
|
266
266
|
|---|---|
|
|
267
|
-
| Tools | `web_search`, `web_fetch`;
|
|
267
|
+
| Tools | `web_search`, `web_fetch`; direct exposure, active on registration (the defaults, so not declared) |
|
|
268
268
|
| Execution mode | `web_search`: `sequential`; `web_fetch`: default |
|
|
269
|
-
| Annotations | both `readOnlyHint: true`, `openWorldHint: true
|
|
269
|
+
| Annotations | both `readOnlyHint: true`, `openWorldHint: true`; a read-only tool declares no `destructiveHint` |
|
|
270
270
|
| Output | `outputSchema` (`WebSearchOutput` / `WebFetchOutput`) plus matching `structuredContent` |
|
|
271
271
|
| State | none in the session branch; one fetch page cache per registration (`web_search` keeps a per-instance throttle) |
|
|
272
272
|
| Lifecycle | the factory only registers (`index.ts`); `registerFetchTool` owns the page cache and clears it on `session_start` and idempotent `session_shutdown` |
|
|
@@ -191,9 +191,7 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
191
191
|
],
|
|
192
192
|
parameters: WebFetchParams,
|
|
193
193
|
outputSchema: WebFetchOutput,
|
|
194
|
-
|
|
195
|
-
defaultActive: true,
|
|
196
|
-
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
194
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
197
195
|
// The page cache is shared in-memory state created per registration, so
|
|
198
196
|
// sibling calls must not run concurrently (matches web_search's throttle).
|
|
199
197
|
executionMode: "sequential",
|
|
@@ -46,9 +46,7 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
46
46
|
],
|
|
47
47
|
parameters: WebSearchParams,
|
|
48
48
|
outputSchema: WebSearchOutput,
|
|
49
|
-
|
|
50
|
-
defaultActive: true,
|
|
51
|
-
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
49
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
52
50
|
executionMode: "sequential",
|
|
53
51
|
|
|
54
52
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -272,7 +272,7 @@ overridden by the project `<cwd>/.pi/bytefly.json` (project wins):
|
|
|
272
272
|
"maxInboundChars": 8000, // inbound text longer than this is truncated
|
|
273
273
|
"typingIndicator": true, // show typing while a turn is generated
|
|
274
274
|
"remotePrompts": true, // answer interactive dialogs on WeChat during a WeChat turn
|
|
275
|
-
"pollHoldMs":
|
|
275
|
+
// "pollHoldMs": 3000 // experimental: request a shorter long-poll hold; unset by default
|
|
276
276
|
}
|
|
277
277
|
```
|
|
278
278
|
|
|
@@ -392,8 +392,9 @@ never touches the stored cursor.
|
|
|
392
392
|
supplies a `context_token` only then).
|
|
393
393
|
- Slash commands are fixed built-ins; there is no user-defined snippet or action
|
|
394
394
|
list yet, and `/new`, `/stop`, `/status`, and `/help` are registered.
|
|
395
|
-
-
|
|
396
|
-
|
|
395
|
+
- Pause is runtime-local. `/wechat pause` stops polling and releases the lock,
|
|
396
|
+
but a reload, `/new`, or restart builds a fresh running bridge; only
|
|
397
|
+
`/wechat logout` stops it persistently (and discards the credentials).
|
|
397
398
|
|
|
398
399
|
## Non-goals
|
|
399
400
|
|
|
@@ -406,7 +407,7 @@ sessions are in-process).
|
|
|
406
407
|
| Integration point | Detail |
|
|
407
408
|
|---|---|
|
|
408
409
|
| Command | `pi.registerCommand("wechat", …)` — `login`, `new`, `status`, `pause`, `resume`, `doctor`, `stop <peer>`, `reset <peer>`, `history <peer> [n]`, `allow [peer]`, `logout`, `help`; bare `/wechat` opens the control screen. |
|
|
409
|
-
| Tools | `send_wechat` — owner-only proactive text; registered at factory time with
|
|
410
|
+
| Tools | `send_wechat` — owner-only proactive text; registered at factory time with direct exposure (the default), `executionMode: "sequential"`, `openWorldHint: true`. |
|
|
410
411
|
| Events | `session_start` (record the live context, auto-start the bridge when an account exists); `session_shutdown` (idempotent stop). No host run events: peer turns are not the host session's runs. |
|
|
411
412
|
| Sessions | `openPeerSession` creates one SDK `AgentSession` per peer (`createAgentSession`, `SessionManager.continueRecent` under `<agent-dir>/wechat/sessions/<peer>/`), inheriting the host's model and thinking level. `DefaultResourceLoader` standard discovery minus this bridge (`withoutWechatExtensions`), so a peer session cannot start a second poller, plus a short channel contract appended to the system prompt so the model knows it is answering in a phone chat window. When `peers[<id>].tools` is set, only those names stay active. |
|
|
412
413
|
| Turn ownership | `peer-worker.ts`: a serial queue per peer, `session.prompt()`/`session.abort()`, `getLastAssistantText()` for the answer, and one idle sweep per session. `concurrency` defaults to 1 so a forwarded dialog has one owner. |
|
|
@@ -68,8 +68,6 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
68
68
|
],
|
|
69
69
|
parameters: WechatSendParams,
|
|
70
70
|
outputSchema: WechatSendResult,
|
|
71
|
-
exposure: "direct",
|
|
72
|
-
defaultActive: true,
|
|
73
71
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
74
72
|
executionMode: "sequential",
|
|
75
73
|
|
package/lib/README.md
CHANGED
|
@@ -16,7 +16,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
16
16
|
| Module | Responsibility |
|
|
17
17
|
|---|---|
|
|
18
18
|
| [`browser.ts`](./browser.ts) | Cross-platform default-browser opener shared by `file-browse` and `plan`: pure `browserCommand(url, platform)` plus best-effort `openInBrowser(url, { platform, spawn })` with an injectable spawn. |
|
|
19
|
-
| [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection
|
|
19
|
+
| [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` (the last two edit one leaf, flat or nested, preserving siblings and metadata). Includes `clampInteger` / `clampNumber` / `cleanString` / `asRecord` / `readJson` / `readJsonResult`. |
|
|
20
20
|
| [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
|
|
21
21
|
| [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
|
|
22
22
|
| [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` and `JobFinishedEvent`, published by `job` and consumed by bytefly's notify capability. |
|
|
@@ -32,6 +32,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
32
32
|
| [`rails.ts`](./rails.ts) | Above-editor widget ordering (`goal` → `todo`) and dock-screen suppression, carried on `pi.events`. |
|
|
33
33
|
| [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. |
|
|
34
34
|
| [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
|
|
35
|
+
| [`todo-state.ts`](./todo-state.ts) | The published `todo` state replayed from the session branch: `ExistingTodo` and `lastTodos(entries)` (the last valid list, skipping a rejected call), so `plan` can merge an approved plan's steps into it and `goal` can name the items still open when a goal is marked achieved — with no extension importing another's modules. |
|
|
35
36
|
| [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
|
|
36
37
|
| [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`), [`web/markup.ts`](./web/markup.ts), and the [`web/index.ts`](./web/index.ts) barrel. |
|
|
37
38
|
|
package/lib/config.ts
CHANGED
|
@@ -139,6 +139,22 @@ export function configSection(
|
|
|
139
139
|
return asRecord(raw?.[key]);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The value at `path` in a document or section, or `undefined` when a step is
|
|
144
|
+
* absent or not an object. A one-element path reads a top-level key, so this
|
|
145
|
+
* also answers "is this setting stated here" for the flat case. `raw` may be an
|
|
146
|
+
* untrusted JSON value or an already-normalized config object.
|
|
147
|
+
*/
|
|
148
|
+
export function configValueAt(raw: unknown, path: readonly string[]): unknown {
|
|
149
|
+
let current: unknown = raw;
|
|
150
|
+
for (const key of path) {
|
|
151
|
+
const record = asRecord(current);
|
|
152
|
+
if (!record) return undefined;
|
|
153
|
+
current = record[key];
|
|
154
|
+
}
|
|
155
|
+
return current;
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
/** Options for {@link loadConfigSection}. */
|
|
143
159
|
export interface SectionOptions {
|
|
144
160
|
/** Agent directory override; `worktree` resolves it explicitly. */
|
|
@@ -197,12 +213,38 @@ function readEditableDocument(path: string): Record<string, unknown> {
|
|
|
197
213
|
* malformed document.
|
|
198
214
|
*/
|
|
199
215
|
export function writeConfigValue(path: string, section: string, key: string, value: unknown): void {
|
|
216
|
+
writeConfigValueAt(path, section, [key], value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Set the leaf at `keyPath` in one top-level section, with the same guarantees
|
|
221
|
+
* as {@link writeConfigValue}: siblings, other sections, and metadata survive,
|
|
222
|
+
* and the file and the section are created when absent. Intermediate objects
|
|
223
|
+
* are created as needed; a non-object in the way is replaced, because a refused
|
|
224
|
+
* edit would leave a hand-written typo unfixable from the screen. Refuses a
|
|
225
|
+
* malformed document and an empty path.
|
|
226
|
+
*/
|
|
227
|
+
export function writeConfigValueAt(path: string, section: string, keyPath: readonly string[], value: unknown): void {
|
|
228
|
+
if (keyPath.length === 0) throw new Error(`Cannot update ${path}: empty key path`);
|
|
200
229
|
const document = readEditableDocument(path);
|
|
201
230
|
const current = configSection(document, section) ?? {};
|
|
202
|
-
document[section] =
|
|
231
|
+
document[section] = setPath(current, keyPath, value);
|
|
203
232
|
writeConfigFile(path, document);
|
|
204
233
|
}
|
|
205
234
|
|
|
235
|
+
/** Copy `source` with the leaf at `path` replaced; every sibling key is kept. */
|
|
236
|
+
function setPath(source: Record<string, unknown>, path: readonly string[], value: unknown): Record<string, unknown> {
|
|
237
|
+
const [head] = path;
|
|
238
|
+
const next = { ...source };
|
|
239
|
+
if (head === undefined) return next;
|
|
240
|
+
if (path.length === 1) {
|
|
241
|
+
next[head] = value;
|
|
242
|
+
return next;
|
|
243
|
+
}
|
|
244
|
+
next[head] = setPath(asRecord(source[head]) ?? {}, path.slice(1), value);
|
|
245
|
+
return next;
|
|
246
|
+
}
|
|
247
|
+
|
|
206
248
|
/**
|
|
207
249
|
* Delete one key from one top-level section, preserving its siblings and the
|
|
208
250
|
* rest of the document. An emptied section is removed rather than left as `{}`.
|
package/lib/env.ts
CHANGED
|
@@ -16,6 +16,9 @@ export const ENV_ROOT = "PI_WORKTREE_ROOT";
|
|
|
16
16
|
*/
|
|
17
17
|
export const ENV_DISABLED_EXTENSIONS = "PI_DISABLED_EXTENSIONS";
|
|
18
18
|
|
|
19
|
+
/** Env var that turns off Pi's automatic network activity, including our own. */
|
|
20
|
+
export const ENV_OFFLINE = "PI_OFFLINE";
|
|
21
|
+
|
|
19
22
|
/** False when `name` appears in `PI_DISABLED_EXTENSIONS`, true otherwise. */
|
|
20
23
|
export function isExtensionEnabled(name: string): boolean {
|
|
21
24
|
const raw = process.env[ENV_DISABLED_EXTENSIONS];
|
|
@@ -29,6 +32,15 @@ export function isExtensionEnabled(name: string): boolean {
|
|
|
29
32
|
return !disabled.has(name.toLowerCase());
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Whether `PI_OFFLINE` is on, read the way Pi reads it: `1`, `true`, or `yes`
|
|
37
|
+
* (case-insensitive). Any other value, including empty, is off.
|
|
38
|
+
*/
|
|
39
|
+
export function isOffline(): boolean {
|
|
40
|
+
const raw = process.env[ENV_OFFLINE]?.trim().toLowerCase();
|
|
41
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/** The active worktree root from the environment, when set and still on disk. */
|
|
33
45
|
export function worktreeRoot(): string | undefined {
|
|
34
46
|
const root = process.env[ENV_ROOT];
|
package/lib/requirements.ts
CHANGED
|
@@ -61,7 +61,19 @@ export interface InvocationCheck {
|
|
|
61
61
|
hint: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The notification delivery bytefly is configured for.
|
|
66
|
+
*
|
|
67
|
+
* The configured method belongs to the notify capability, so it arrives through
|
|
68
|
+
* {@link ProbeSeams.notify}; whether this host can honour it is decided here.
|
|
69
|
+
*/
|
|
70
|
+
export interface NotifyCheck {
|
|
71
|
+
kind: "notify";
|
|
72
|
+
enables: string;
|
|
73
|
+
hint: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type RequirementCheck = BinaryCheck | ModuleCheck | StateCheck | InvocationCheck | NotifyCheck;
|
|
65
77
|
|
|
66
78
|
/** What an extension needs, split by how badly it needs it. */
|
|
67
79
|
export interface ExtensionRequirements {
|
|
@@ -173,7 +185,8 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
173
185
|
"file-open": {
|
|
174
186
|
notes: [
|
|
175
187
|
"The `/open` command and its notification need an interactive UI; the `open_path` tool works without one.",
|
|
176
|
-
"Launching the OS handler is best-effort: a
|
|
188
|
+
"Launching the OS handler is best-effort: a launcher that fails to start, or exits non-zero on macOS and Linux, is reported; Windows `explorer` can exit non-zero after a successful open.",
|
|
189
|
+
"The tool result names the target even when the launch fails.",
|
|
177
190
|
],
|
|
178
191
|
},
|
|
179
192
|
memory: {
|
|
@@ -193,6 +206,13 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
193
206
|
},
|
|
194
207
|
bytefly: {
|
|
195
208
|
configFiles: [SHARED_CONFIG_FILE],
|
|
209
|
+
optional: [
|
|
210
|
+
{
|
|
211
|
+
kind: "notify",
|
|
212
|
+
enables: "the configured notification delivery",
|
|
213
|
+
hint: 'set "notify.method" to "auto" in bytefly.json, or choose one this host can deliver.',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
196
216
|
notes: [
|
|
197
217
|
"TUI only for the header, footer, and turn rule; `/bytefly` prints a listing and `/bytefly doctor` the report in RPC, JSON, and print modes.",
|
|
198
218
|
"The panel writes extension enablement through Pi's own settings; project scope requires a trusted project.",
|
|
@@ -229,6 +249,35 @@ export interface ProbeSeams {
|
|
|
229
249
|
loadModule?: (name: string) => Promise<unknown>;
|
|
230
250
|
/** Resolve the nested-pi command; defaults to {@link getPiInvocation}. */
|
|
231
251
|
invocation?: () => PiInvocation;
|
|
252
|
+
/** The notify capability's configured delivery, or absent when it is not in play. */
|
|
253
|
+
notify?: () => { enabled: boolean; method: string } | undefined;
|
|
254
|
+
/** Whether stdout is a terminal, which the OSC methods need; defaults to true. */
|
|
255
|
+
isTTY?: boolean;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Whether `method` can deliver on this host.
|
|
260
|
+
*
|
|
261
|
+
* `auto` inspects the environment itself, so it always can; the OSC methods need
|
|
262
|
+
* a terminal to write to; the rest are platform-specific. An unknown id counts
|
|
263
|
+
* as deliverable because bytefly's own loader already rejects one.
|
|
264
|
+
*/
|
|
265
|
+
export function canDeliver(method: string, env: { platform: NodeJS.Platform; isTTY: boolean }): boolean {
|
|
266
|
+
switch (method) {
|
|
267
|
+
case "auto":
|
|
268
|
+
return true;
|
|
269
|
+
case "osc777":
|
|
270
|
+
case "osc99":
|
|
271
|
+
return env.isTTY;
|
|
272
|
+
case "toast":
|
|
273
|
+
return env.platform === "win32";
|
|
274
|
+
case "osascript":
|
|
275
|
+
return env.platform === "darwin";
|
|
276
|
+
case "notify-send":
|
|
277
|
+
return env.platform === "linux";
|
|
278
|
+
default:
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
232
281
|
}
|
|
233
282
|
|
|
234
283
|
const defaultLoadModule = (name: string): Promise<unknown> => import(name);
|
|
@@ -317,6 +366,31 @@ async function runCheck(
|
|
|
317
366
|
hint: check.hint,
|
|
318
367
|
};
|
|
319
368
|
}
|
|
369
|
+
case "notify": {
|
|
370
|
+
const notify = seams.notify?.();
|
|
371
|
+
if (!notify?.enabled) {
|
|
372
|
+
return { extension, check: "notify.method", severity: "ok", detail: "notifications are off" };
|
|
373
|
+
}
|
|
374
|
+
const env = { platform: seams.platform, isTTY: seams.isTTY ?? true };
|
|
375
|
+
if (canDeliver(notify.method, env)) {
|
|
376
|
+
return {
|
|
377
|
+
extension,
|
|
378
|
+
check: "notify.method",
|
|
379
|
+
severity: "ok",
|
|
380
|
+
detail:
|
|
381
|
+
notify.method === "auto"
|
|
382
|
+
? "auto picks a route from the environment"
|
|
383
|
+
: `"${notify.method}" delivers on ${seams.platform}`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
extension,
|
|
388
|
+
check: "notify.method",
|
|
389
|
+
severity: failSeverity,
|
|
390
|
+
detail: `"${notify.method}" cannot deliver on ${seams.platform} — ${check.enables}`,
|
|
391
|
+
hint: check.hint,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
320
394
|
}
|
|
321
395
|
}
|
|
322
396
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published `todo` state, read back from the session branch (pure).
|
|
3
|
+
*
|
|
4
|
+
* The todo list is a whole-list replacement stored in each `todo` tool result's
|
|
5
|
+
* `details`, so replaying the branch is how another extension reads it without
|
|
6
|
+
* importing the todo extension. `plan` merges an approved plan's steps into the
|
|
7
|
+
* last list, and `goal` mentions the items still open when a goal is marked
|
|
8
|
+
* achieved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { TODO_TOOL } from "./tool-names.ts";
|
|
12
|
+
|
|
13
|
+
/** One item in a `todo` list, as the todo tool stores it. */
|
|
14
|
+
export interface ExistingTodo {
|
|
15
|
+
content: string;
|
|
16
|
+
status: "pending" | "in_progress" | "completed";
|
|
17
|
+
/** Present-continuous label the todo tool requires for an in-progress item. */
|
|
18
|
+
activeForm?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Minimal shape of a session entry, used for runtime narrowing. */
|
|
22
|
+
interface BranchEntryLike {
|
|
23
|
+
type?: string;
|
|
24
|
+
message?: {
|
|
25
|
+
role?: string;
|
|
26
|
+
toolName?: string;
|
|
27
|
+
details?: unknown;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isStatus(value: unknown): value is ExistingTodo["status"] {
|
|
32
|
+
return value === "pending" || value === "in_progress" || value === "completed";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The last valid todo list written on the branch, or an empty list. A rejected
|
|
37
|
+
* call carries the unchanged list and is not a state write, so it is skipped;
|
|
38
|
+
* an entry that does not re-validate is ignored rather than wiping a good list.
|
|
39
|
+
*/
|
|
40
|
+
export function lastTodos(entries: Iterable<unknown>): ExistingTodo[] {
|
|
41
|
+
let todos: ExistingTodo[] = [];
|
|
42
|
+
for (const raw of entries) {
|
|
43
|
+
const entry = raw as BranchEntryLike;
|
|
44
|
+
if (entry?.type !== "message") continue;
|
|
45
|
+
const message = entry.message;
|
|
46
|
+
if (message?.role !== "toolResult" || message.toolName !== TODO_TOOL) continue;
|
|
47
|
+
const details = message.details as { todos?: unknown; error?: unknown } | undefined;
|
|
48
|
+
if (!details || details.error) continue;
|
|
49
|
+
if (!Array.isArray(details.todos)) continue;
|
|
50
|
+
todos = details.todos.flatMap((item): ExistingTodo[] => {
|
|
51
|
+
if (typeof item !== "object" || item === null) return [];
|
|
52
|
+
const value = item as { content?: unknown; status?: unknown; activeForm?: unknown };
|
|
53
|
+
if (typeof value.content !== "string" || !isStatus(value.status)) return [];
|
|
54
|
+
const content = value.content.trim();
|
|
55
|
+
if (!content) return [];
|
|
56
|
+
const activeForm =
|
|
57
|
+
typeof value.activeForm === "string" && value.activeForm.trim() ? value.activeForm.trim() : undefined;
|
|
58
|
+
return [activeForm ? { content, status: value.status, activeForm } : { content, status: value.status }];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return todos;
|
|
62
|
+
}
|
package/lib/tui/glyphs.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|