@gavin-hu/my-pi-agent 0.18.0 → 0.19.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 +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +554 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +76 -92
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/guard.ts +3 -5
- package/lib/README.md +8 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a structured body — JSON, CSV, or XML — as readable text.
|
|
3
|
+
*
|
|
4
|
+
* `web_fetch` used to hand these back as the blob it downloaded: a page a reader
|
|
5
|
+
* has to parse in their head. This turns one into the shape a model reads best —
|
|
6
|
+
* a table for records, indented keys for an object, an indented tree for XML — and
|
|
7
|
+
* declines anything it cannot read, so the caller keeps the raw body and nothing
|
|
8
|
+
* usable is lost.
|
|
9
|
+
*
|
|
10
|
+
* A source cap keeps an oversized response out: past it the raw body, which pages,
|
|
11
|
+
* reads better than a rendered one. A rendered-character cap keeps one answer from
|
|
12
|
+
* doubling the cache, and the note says where it stopped. Nothing here throws —
|
|
13
|
+
* every failure is `undefined`, which `page.ts` reads as "return the body
|
|
14
|
+
* unchanged".
|
|
15
|
+
*
|
|
16
|
+
* The declared content type decides first. A body that declares nothing usable is
|
|
17
|
+
* sniffed only when it has a shape — an opening brace, a uniform comma count, a
|
|
18
|
+
* root tag — because a plain-text page is the common case and a mangled table is
|
|
19
|
+
* worse than a blob.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { asRecord } from "../../../lib/config.ts";
|
|
23
|
+
import { decodeEntities } from "../html.ts";
|
|
24
|
+
import { charLength, sliceChars } from "../text.ts";
|
|
25
|
+
|
|
26
|
+
/** Past this, the raw body — which pages — is the better answer. */
|
|
27
|
+
const MAX_SOURCE_CHARS = 1_048_576;
|
|
28
|
+
|
|
29
|
+
/** Rendered characters before the cut; the cached text pays this too. */
|
|
30
|
+
const MAX_RENDERED_CHARS = 60_000;
|
|
31
|
+
|
|
32
|
+
/** One level of nesting, and the gap between two table columns. */
|
|
33
|
+
const INDENT = " ";
|
|
34
|
+
|
|
35
|
+
/** A table cell is one line, so a nested value is compacted to about this width. */
|
|
36
|
+
const CELL_CHARS = 40;
|
|
37
|
+
|
|
38
|
+
/** A list short enough to sit on one line with its key. */
|
|
39
|
+
const INLINE_LIST_CHARS = 80;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render a body as text, or `undefined` when it is not one this module reads.
|
|
43
|
+
*/
|
|
44
|
+
export function renderStructured(body: string, contentType: string): string | undefined {
|
|
45
|
+
if (body.length > MAX_SOURCE_CHARS) return undefined;
|
|
46
|
+
const declared = contentType.toLowerCase();
|
|
47
|
+
if (isJsonType(declared)) return renderJson(body);
|
|
48
|
+
if (isCsvType(declared)) return renderCsv(body);
|
|
49
|
+
if (isXmlType(declared)) return renderXml(body);
|
|
50
|
+
|
|
51
|
+
if (looksLikeJson(body)) return renderJson(body);
|
|
52
|
+
if (looksLikeCsv(body)) return renderCsv(body);
|
|
53
|
+
if (looksLikeXml(body)) return renderXml(body);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isJsonType(contentType: string): boolean {
|
|
58
|
+
return /json/.test(contentType);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isCsvType(contentType: string): boolean {
|
|
62
|
+
return /csv/.test(contentType);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isXmlType(contentType: string): boolean {
|
|
66
|
+
return /xml/.test(contentType);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function looksLikeJson(body: string): boolean {
|
|
70
|
+
return /^\s*[[{]/.test(body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Uniformly comma-separated lines, which is the shape a plain-text export has. */
|
|
74
|
+
function looksLikeCsv(body: string): boolean {
|
|
75
|
+
const lines = body.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
76
|
+
if (lines.length < 2) return false;
|
|
77
|
+
const commas = lines[0].split(",").length - 1;
|
|
78
|
+
return commas > 0 && lines.every((line) => line.split(",").length - 1 === commas);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function looksLikeXml(body: string): boolean {
|
|
82
|
+
return /^\s*<(\?xml\b|[A-Za-z_])/.test(body) && body.includes("</");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The rendered lines, what they cost, and whether the cap cut anything. */
|
|
86
|
+
interface Output {
|
|
87
|
+
lines: string[];
|
|
88
|
+
used: number;
|
|
89
|
+
cut: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createOutput(): Output {
|
|
93
|
+
return { lines: [], used: 0, cut: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Append one line within the cap; `false` means the caller should stop. */
|
|
97
|
+
function emit(out: Output, line: string): boolean {
|
|
98
|
+
const cost = line.length + 1;
|
|
99
|
+
if (out.used + cost > MAX_RENDERED_CHARS) {
|
|
100
|
+
out.cut = true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
out.lines.push(line);
|
|
104
|
+
out.used += cost;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function emitAt(out: Output, text: string, depth: number): boolean {
|
|
109
|
+
return emit(out, `${INDENT.repeat(depth)}${text}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The rendered text, with one note when the cap stopped it early. */
|
|
113
|
+
function finish(out: Output, body: string): string {
|
|
114
|
+
const text = out.lines.join("\n");
|
|
115
|
+
if (!out.cut) return text;
|
|
116
|
+
return `${text}\n\n… (rendered ${out.used} characters of a ${body.length}-character body; the rest was cut)`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One line for a value: a string's own line breaks become spaces. */
|
|
120
|
+
function oneLine(text: string): string {
|
|
121
|
+
return text.replace(/\s*\n\s*/g, " ");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Collapse a text node or an attribute to one line. */
|
|
125
|
+
function collapse(text: string): string {
|
|
126
|
+
return text.replace(/\s+/g, " ").trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A value JSON stores as one atom: no nesting to walk. */
|
|
130
|
+
function isScalar(value: unknown): boolean {
|
|
131
|
+
return value === null || typeof value !== "object";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isScalarList(value: readonly unknown[]): boolean {
|
|
135
|
+
return value.every(isScalar);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isRecordList(value: readonly unknown[]): value is readonly Record<string, unknown>[] {
|
|
139
|
+
return value.every((item) => {
|
|
140
|
+
const record = asRecord(item);
|
|
141
|
+
return record !== undefined && Object.keys(record).length > 0;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One line of text for a scalar; a string's own line breaks become spaces. */
|
|
146
|
+
function scalarText(value: unknown): string {
|
|
147
|
+
if (value === null) return "null";
|
|
148
|
+
if (typeof value === "string") return oneLine(value);
|
|
149
|
+
return String(value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A table cell: a nested value is compacted and clipped. */
|
|
153
|
+
function cellText(value: unknown): string {
|
|
154
|
+
if (value === undefined) return "";
|
|
155
|
+
if (isScalar(value)) return scalarText(value);
|
|
156
|
+
const compact = JSON.stringify(value) ?? "";
|
|
157
|
+
return charLength(compact) <= CELL_CHARS ? compact : `${sliceChars(compact, 0, CELL_CHARS)}…`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Lines as a table: each column padded to its widest cell, two spaces between
|
|
162
|
+
* columns, no padding after the last one. Ragged rows are padded with empty cells,
|
|
163
|
+
* so a short record still lines up instead of shifting the columns after it.
|
|
164
|
+
*/
|
|
165
|
+
function renderGrid(rows: readonly (readonly string[])[], depth: number, out: Output): void {
|
|
166
|
+
const columns = rows.reduce((widest, row) => Math.max(widest, row.length), 0);
|
|
167
|
+
const widths = Array.from({ length: columns }, (_unused, column) =>
|
|
168
|
+
rows.reduce((widest, row) => Math.max(widest, charLength(row[column] ?? "")), 0),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
for (const row of rows) {
|
|
172
|
+
const line: string[] = [];
|
|
173
|
+
for (let column = 0; column < columns; column++) {
|
|
174
|
+
const value = oneLine(row[column] ?? "");
|
|
175
|
+
line.push(column === columns - 1 ? value : value + " ".repeat(widths[column] - charLength(value)));
|
|
176
|
+
}
|
|
177
|
+
if (!emitAt(out, line.join(INDENT).trimEnd(), depth)) return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Records as a table: the union of their keys, so one record's extra key shows. */
|
|
182
|
+
function renderTable(rows: readonly Record<string, unknown>[], depth: number, out: Output): void {
|
|
183
|
+
const header: string[] = [];
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
for (const key of Object.keys(row)) if (!header.includes(key)) header.push(key);
|
|
186
|
+
}
|
|
187
|
+
const cells = rows.map((row) => header.map((key) => cellText(row[key])));
|
|
188
|
+
renderGrid([header, ...cells], depth, out);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The one-line form of a value, when it has one: a scalar, an empty container, or
|
|
193
|
+
* a short list. `undefined` means it needs its own block.
|
|
194
|
+
*/
|
|
195
|
+
function inlineValue(value: unknown): string | undefined {
|
|
196
|
+
if (isScalar(value)) return scalarText(value);
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
if (value.length === 0) return "[]";
|
|
199
|
+
if (!isScalarList(value)) return undefined;
|
|
200
|
+
const text = `[${value.map(scalarText).join(", ")}]`;
|
|
201
|
+
return charLength(text) <= INLINE_LIST_CHARS ? text : undefined;
|
|
202
|
+
}
|
|
203
|
+
// Only an empty object has a one-line form; anything else needs a block.
|
|
204
|
+
const record = asRecord(value);
|
|
205
|
+
return record !== undefined && Object.keys(record).length === 0 ? "{}" : undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderValue(value: unknown, depth: number, out: Output): void {
|
|
209
|
+
if (isScalar(value)) {
|
|
210
|
+
emitAt(out, scalarText(value), depth);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(value)) {
|
|
214
|
+
renderArray(value, depth, out);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const record = asRecord(value);
|
|
218
|
+
if (record) {
|
|
219
|
+
renderObject(record, depth, out);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderObject(value: Record<string, unknown>, depth: number, out: Output): void {
|
|
224
|
+
const keys = Object.keys(value);
|
|
225
|
+
if (keys.length === 0) {
|
|
226
|
+
emitAt(out, "{}", depth);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const key of keys) {
|
|
230
|
+
const child = value[key];
|
|
231
|
+
const inline = inlineValue(child);
|
|
232
|
+
if (inline !== undefined) {
|
|
233
|
+
if (!emitAt(out, `${key}: ${inline}`, depth)) return;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!emitAt(out, key, depth)) return;
|
|
237
|
+
renderValue(child, depth + 1, out);
|
|
238
|
+
if (out.cut) return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderArray(value: readonly unknown[], depth: number, out: Output): void {
|
|
243
|
+
if (value.length === 0) {
|
|
244
|
+
emitAt(out, "[]", depth);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (isRecordList(value)) {
|
|
248
|
+
renderTable(value, depth, out);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (isScalarList(value)) {
|
|
252
|
+
for (const item of value) if (!emitAt(out, `- ${scalarText(item)}`, depth)) return;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// Mixed shapes keep their order and their nesting rather than pretending to be
|
|
256
|
+
// a table they are not.
|
|
257
|
+
for (const item of value) {
|
|
258
|
+
if (!emitAt(out, "-", depth)) return;
|
|
259
|
+
renderValue(item, depth + 1, out);
|
|
260
|
+
if (out.cut) return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderJson(body: string): string | undefined {
|
|
265
|
+
let value: unknown;
|
|
266
|
+
try {
|
|
267
|
+
value = JSON.parse(body);
|
|
268
|
+
} catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const out = createOutput();
|
|
272
|
+
renderValue(value, 0, out);
|
|
273
|
+
return finish(out, body);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* RFC 4180: commas between fields, a quoted field may hold commas and line breaks,
|
|
278
|
+
* and a doubled quote inside one is a literal quote. An unterminated quote means
|
|
279
|
+
* this is not CSV we can read, so the caller keeps the raw body.
|
|
280
|
+
*/
|
|
281
|
+
function parseCsv(body: string): string[][] | undefined {
|
|
282
|
+
const rows: string[][] = [];
|
|
283
|
+
let row: string[] = [];
|
|
284
|
+
let field = "";
|
|
285
|
+
let quoted = false;
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < body.length; i++) {
|
|
288
|
+
const char = body[i];
|
|
289
|
+
if (quoted) {
|
|
290
|
+
if (char !== '"') field += char;
|
|
291
|
+
else if (body[i + 1] === '"') {
|
|
292
|
+
field += '"';
|
|
293
|
+
i++;
|
|
294
|
+
} else quoted = false;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (char === '"') quoted = true;
|
|
298
|
+
else if (char === ",") {
|
|
299
|
+
row.push(field);
|
|
300
|
+
field = "";
|
|
301
|
+
} else if (char === "\n" || char === "\r") {
|
|
302
|
+
if (char === "\r" && body[i + 1] === "\n") i++;
|
|
303
|
+
row.push(field);
|
|
304
|
+
rows.push(row);
|
|
305
|
+
row = [];
|
|
306
|
+
field = "";
|
|
307
|
+
} else field += char;
|
|
308
|
+
}
|
|
309
|
+
if (quoted) return undefined;
|
|
310
|
+
if (field !== "" || row.length > 0) {
|
|
311
|
+
row.push(field);
|
|
312
|
+
rows.push(row);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const filled = rows.filter((cells) => cells.some((cell) => cell.trim() !== ""));
|
|
316
|
+
const columns = filled.reduce((widest, cells) => Math.max(widest, cells.length), 0);
|
|
317
|
+
return filled.length >= 2 && columns >= 2 ? filled : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderCsv(body: string): string | undefined {
|
|
321
|
+
const rows = parseCsv(body);
|
|
322
|
+
if (!rows) return undefined;
|
|
323
|
+
const out = createOutput();
|
|
324
|
+
renderGrid(rows, 0, out);
|
|
325
|
+
return finish(out, body);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** One element: its attributes, its children, and the text directly inside it. */
|
|
329
|
+
interface XmlNode {
|
|
330
|
+
name: string;
|
|
331
|
+
attributes: Array<readonly [string, string]>;
|
|
332
|
+
children: XmlNode[];
|
|
333
|
+
text: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Attributes of one tag; a bare attribute carries no value to show, so it is left out. */
|
|
337
|
+
function parseAttributes(source: string): Array<readonly [string, string]> {
|
|
338
|
+
const attributes: Array<readonly [string, string]> = [];
|
|
339
|
+
for (const match of source.matchAll(/([^\s=]+)\s*=\s*("([^"]*)"|'([^']*)')/g)) {
|
|
340
|
+
attributes.push([match[1], decodeEntities(match[3] ?? match[4] ?? "")]);
|
|
341
|
+
}
|
|
342
|
+
return attributes;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Index just past `end` where it closes a construct opened at `from`, or -1. */
|
|
346
|
+
function after(body: string, from: number, end: string): number {
|
|
347
|
+
const at = body.indexOf(end, from + 1);
|
|
348
|
+
return at < 0 ? -1 : at + end.length;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The smallest XML reader that answers "what is the tree": tags, attributes, text,
|
|
353
|
+
* comments, CDATA, and the declaration. It returns `undefined` on anything it
|
|
354
|
+
* cannot balance — two roots, a stray close, an unterminated quote or comment — so
|
|
355
|
+
* a body that is not really XML keeps its raw form.
|
|
356
|
+
*/
|
|
357
|
+
function parseXml(body: string): XmlNode | undefined {
|
|
358
|
+
const stack: XmlNode[] = [];
|
|
359
|
+
let root: XmlNode | undefined;
|
|
360
|
+
let index = 0;
|
|
361
|
+
|
|
362
|
+
const appendText = (text: string): void => {
|
|
363
|
+
const node = stack[stack.length - 1];
|
|
364
|
+
if (node) node.text += text;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
while (index < body.length) {
|
|
368
|
+
const open = body.indexOf("<", index);
|
|
369
|
+
if (open < 0) {
|
|
370
|
+
appendText(decodeEntities(body.slice(index)));
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
if (open > index) appendText(decodeEntities(body.slice(index, open)));
|
|
374
|
+
|
|
375
|
+
if (body.startsWith("<!--", open)) {
|
|
376
|
+
const next = after(body, open, "-->");
|
|
377
|
+
if (next < 0) return undefined;
|
|
378
|
+
index = next;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (body.startsWith("<![CDATA[", open)) {
|
|
382
|
+
const at = body.indexOf("]]>", open);
|
|
383
|
+
if (at < 0) return undefined;
|
|
384
|
+
appendText(body.slice(open + "<![CDATA[".length, at));
|
|
385
|
+
index = at + "]]>".length;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (body.startsWith("<?", open) || body.startsWith("<!")) {
|
|
389
|
+
const next = after(body, open, body.startsWith("<?", open) ? "?>" : ">");
|
|
390
|
+
if (next < 0) return undefined;
|
|
391
|
+
index = next;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const close = body.indexOf(">", open + 1);
|
|
396
|
+
if (close < 0) return undefined;
|
|
397
|
+
const tag = body.slice(open + 1, close).trim();
|
|
398
|
+
if (tag.startsWith("/")) {
|
|
399
|
+
const node = stack.pop();
|
|
400
|
+
if (!node || node.name !== tag.slice(1).trim()) return undefined;
|
|
401
|
+
index = close + 1;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const selfClosing = tag.endsWith("/");
|
|
406
|
+
const inner = selfClosing ? tag.slice(0, -1).trim() : tag;
|
|
407
|
+
const name = /^[^\s/>]+/.exec(inner)?.[0];
|
|
408
|
+
if (!name) return undefined;
|
|
409
|
+
const node: XmlNode = { name, attributes: parseAttributes(inner.slice(name.length)), children: [], text: "" };
|
|
410
|
+
const parent = stack[stack.length - 1];
|
|
411
|
+
if (parent) parent.children.push(node);
|
|
412
|
+
else if (root) return undefined;
|
|
413
|
+
else root = node;
|
|
414
|
+
if (!selfClosing) stack.push(node);
|
|
415
|
+
index = close + 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (stack.length > 0 || !root) return undefined;
|
|
419
|
+
return root;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* One element as an indented block: a leaf with only text reads `name: text`,
|
|
424
|
+
* anything with attributes or children gets a line of its own and indented detail.
|
|
425
|
+
*/
|
|
426
|
+
function renderXmlNode(node: XmlNode, depth: number, out: Output): void {
|
|
427
|
+
const text = collapse(node.text);
|
|
428
|
+
if (node.children.length === 0 && node.attributes.length === 0) {
|
|
429
|
+
emitAt(out, text ? `${node.name}: ${text}` : node.name, depth);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!emitAt(out, node.name, depth)) return;
|
|
433
|
+
for (const [name, value] of node.attributes) if (!emitAt(out, `@${name}: ${value}`, depth + 1)) return;
|
|
434
|
+
for (const child of node.children) {
|
|
435
|
+
renderXmlNode(child, depth + 1, out);
|
|
436
|
+
if (out.cut) return;
|
|
437
|
+
}
|
|
438
|
+
if (text) emitAt(out, text, depth + 1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderXml(body: string): string | undefined {
|
|
442
|
+
const root = parseXml(body);
|
|
443
|
+
if (!root) return undefined;
|
|
444
|
+
const out = createOutput();
|
|
445
|
+
renderXmlNode(root, 0, out);
|
|
446
|
+
return finish(out, body);
|
|
447
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { formatTokens } from "../../../lib/format.ts";
|
|
14
|
+
import { WEB_FETCH_TOOL } from "../../../lib/tool-names.ts";
|
|
14
15
|
import { reuseText } from "../../../lib/tui/chrome.ts";
|
|
15
16
|
import {
|
|
16
17
|
errorLine,
|
|
@@ -34,7 +35,7 @@ import { resolveRequest, WebFetchOutput, WebFetchParams, type FetchResponse, typ
|
|
|
34
35
|
import type { FetchRequest } from "./types.ts";
|
|
35
36
|
import { loadFetchConfig } from "../config.ts";
|
|
36
37
|
|
|
37
|
-
export const TOOL_NAME =
|
|
38
|
+
export const TOOL_NAME = WEB_FETCH_TOOL;
|
|
38
39
|
|
|
39
40
|
/** The host shown while a fetch is in flight, or `page` for an unparseable URL. */
|
|
40
41
|
function hostOf(raw: string | undefined): string {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { WEB_SEARCH_PROVIDER_CHANNEL } from "../../../lib/events.ts";
|
|
12
|
+
import { WEB_SEARCH_TOOL } from "../../../lib/tool-names.ts";
|
|
12
13
|
import { loadSearchConfig } from "../config.ts";
|
|
13
14
|
import { oneLine } from "../transcript.ts";
|
|
14
15
|
import { reuseText } from "../../../lib/tui/chrome.ts";
|
|
@@ -27,7 +28,7 @@ import { formatResults } from "./format.ts";
|
|
|
27
28
|
import { createThrottle, runSearch, type SearchDeps } from "./search.ts";
|
|
28
29
|
import { resolveRequest, WebSearchOutput, WebSearchParams, type SearchResponse, type WebSearchArgs } from "./schema.ts";
|
|
29
30
|
|
|
30
|
-
export const TOOL_NAME =
|
|
31
|
+
export const TOOL_NAME = WEB_SEARCH_TOOL;
|
|
31
32
|
|
|
32
33
|
export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): void {
|
|
33
34
|
// One throttle per extension instance, so repeated searches stay polite
|