@gavin-hu/my-pi-agent 0.11.3 → 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 +22 -11
- package/extensions/wechat/bridge.ts +90 -22
- package/extensions/wechat/commands.ts +45 -1
- package/extensions/wechat/doctor.ts +1 -0
- package/extensions/wechat/index.ts +3 -1
- package/extensions/wechat/status-screen.ts +32 -2
- 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
|
@@ -24,7 +24,7 @@ import { peerLabel } from "./format.ts";
|
|
|
24
24
|
import type { BotCredentials } from "./types.ts";
|
|
25
25
|
|
|
26
26
|
/** User-facing connection word; the internal `BridgeState` stays running/stopped. */
|
|
27
|
-
export type StatusConnection = "open" | "expired" | "closed";
|
|
27
|
+
export type StatusConnection = "open" | "expired" | "paused" | "closed";
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* What the status screen closed with: a global action, a peer history request,
|
|
@@ -69,7 +69,7 @@ const MAX_PEER_ROWS = 5;
|
|
|
69
69
|
|
|
70
70
|
/** The connection word for a bridge status, or `closed` when there is none. */
|
|
71
71
|
export function connectionOf(status: BridgeStatus | undefined): StatusConnection {
|
|
72
|
-
return status?.open ? "open" : status?.expired ? "expired" : "closed";
|
|
72
|
+
return status?.paused ? "paused" : status?.open ? "open" : status?.expired ? "expired" : "closed";
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** Build the view from the stored account and the live bridge status. */
|
|
@@ -146,6 +146,8 @@ function connectionText(connection: StatusConnection): string {
|
|
|
146
146
|
return "open";
|
|
147
147
|
case "expired":
|
|
148
148
|
return "expired";
|
|
149
|
+
case "paused":
|
|
150
|
+
return "paused";
|
|
149
151
|
case "closed":
|
|
150
152
|
return "closed";
|
|
151
153
|
}
|
|
@@ -158,6 +160,8 @@ function connectionRole(connection: StatusConnection): TuiRole {
|
|
|
158
160
|
return "ok";
|
|
159
161
|
case "expired":
|
|
160
162
|
return "bad";
|
|
163
|
+
case "paused":
|
|
164
|
+
return "warn";
|
|
161
165
|
case "closed":
|
|
162
166
|
return "meta";
|
|
163
167
|
}
|
|
@@ -193,6 +197,9 @@ export function statusBody(view: StatusView, theme: Theme, width: number, select
|
|
|
193
197
|
if (view.connection === "expired") {
|
|
194
198
|
lines.push("");
|
|
195
199
|
lines.push(clip(` ${paint(theme, "bad", "Session expired — press l to log in again.")}`, target));
|
|
200
|
+
} else if (view.connection === "paused") {
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push(clip(` ${paint(theme, "warn", "Paused — press p to resume polling.")}`, target));
|
|
196
203
|
} else if (view.connection === "closed" && view.account) {
|
|
197
204
|
lines.push("");
|
|
198
205
|
lines.push(clip(` ${paint(theme, "meta", "Not connected — press l to log in again.")}`, target));
|
|
@@ -251,6 +258,8 @@ export interface StatusActions {
|
|
|
251
258
|
reset(peer: string): string;
|
|
252
259
|
/** Withdraw the waiting prompt; returns the line to flash. */
|
|
253
260
|
cancelPrompt(): string;
|
|
261
|
+
/** Pause or resume polling; async because it settles the poll loop first. */
|
|
262
|
+
togglePause(): string | Promise<string>;
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
export interface StatusScreenOptions {
|
|
@@ -325,6 +334,27 @@ export class StatusScreenComponent implements Component {
|
|
|
325
334
|
this.setFlash(undefined);
|
|
326
335
|
return;
|
|
327
336
|
}
|
|
337
|
+
if (data === "p" || data === "P") {
|
|
338
|
+
if (!view.bridge) {
|
|
339
|
+
this.setFlash("WeChat is not running.");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const outcome = this.options.actions?.togglePause();
|
|
343
|
+
if (!outcome) {
|
|
344
|
+
this.setFlash("Pause is unavailable here.");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (typeof outcome === "string") {
|
|
348
|
+
this.setFlash(outcome);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.setFlash(view.connection === "paused" ? "Resuming…" : "Pausing…");
|
|
352
|
+
void outcome.then(
|
|
353
|
+
(line) => this.setFlash(line),
|
|
354
|
+
() => this.setFlash("Pause action failed."),
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
328
358
|
if (data === "a" || data === "A") {
|
|
329
359
|
this.options.onClose({ kind: "allow" });
|
|
330
360
|
return;
|
|
@@ -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",
|