@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11
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/CHANGELOG.md +117 -0
- package/dist/cli.js +23087 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +20 -219
- package/dist/types/config/model-resolver.d.ts +16 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +12 -16
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/debug/terminal-info.d.ts +0 -1
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +31 -26
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +7 -12
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +308 -1025
- package/src/config/model-resolver.ts +113 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +18 -14
- package/src/config/settings.ts +37 -1
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +95 -18
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +2 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +49 -23
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +1 -1
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +66 -54
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +373 -141
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -49
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -5
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +5 -19
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +2 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +15 -26
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +8 -10
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +6 -2
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +32 -14
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +54 -13
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +3 -9
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +51 -12
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/file-mentions.ts +2 -1
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/anthropic.ts +8 -2
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/tools/write.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { parseArchivePathCandidates } from "./archive-reader";
|
|
|
25
25
|
import { assertEditableFile } from "./auto-generated-guard";
|
|
26
26
|
import {
|
|
27
27
|
type ConflictEntry,
|
|
28
|
+
conflictRegionPresent,
|
|
29
|
+
conflictRegionsEqual,
|
|
28
30
|
expandContentTokens,
|
|
29
31
|
getConflictHistory,
|
|
30
32
|
parseConflictUri,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
formatErrorDetail,
|
|
40
42
|
formatExpandHint,
|
|
41
43
|
formatMoreItems,
|
|
44
|
+
formatStatusIcon,
|
|
42
45
|
getLspBatchRequest,
|
|
43
46
|
replaceTabs,
|
|
44
47
|
shortenPath,
|
|
@@ -266,7 +269,14 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
266
269
|
readonly name = "write";
|
|
267
270
|
readonly approval = (args: unknown) => {
|
|
268
271
|
const rawPath = (args as Partial<WriteParams>).path;
|
|
269
|
-
|
|
272
|
+
if (typeof rawPath !== "string" || !isInternalUrlPath(rawPath)) return "write";
|
|
273
|
+
// Internal URLs are usually session-local artifacts (read tier), but a
|
|
274
|
+
// scheme whose handler exposes a `write` hook mutates handler-owned
|
|
275
|
+
// user data (e.g. vault:// notes, host-owned mcp:// URIs) and must take
|
|
276
|
+
// the write tier so always-ask mode actually prompts.
|
|
277
|
+
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(rawPath.trim());
|
|
278
|
+
const handler = match ? InternalUrlRouter.instance().getHandler(match[1]!.toLowerCase()) : undefined;
|
|
279
|
+
return handler?.write ? "write" : "read";
|
|
270
280
|
};
|
|
271
281
|
readonly formatApprovalDetails = (args: unknown): string[] => {
|
|
272
282
|
const params = args as Partial<WriteParams>;
|
|
@@ -349,7 +359,18 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
349
359
|
content: string,
|
|
350
360
|
resolvedArchivePath: ResolvedArchiveWritePath,
|
|
351
361
|
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
352
|
-
|
|
362
|
+
// Resolve symlinks before the tmp+rename swap: renaming over a symlink
|
|
363
|
+
// replaces the link itself with a regular file instead of writing
|
|
364
|
+
// through to its target.
|
|
365
|
+
const finalPath = resolvedArchivePath.exists
|
|
366
|
+
? await fs.realpath(resolvedArchivePath.absolutePath).catch(() => resolvedArchivePath.absolutePath)
|
|
367
|
+
: resolvedArchivePath.absolutePath;
|
|
368
|
+
const lowerPath = finalPath.toLowerCase();
|
|
369
|
+
const isZip = lowerPath.endsWith(".zip");
|
|
370
|
+
const isGzip = lowerPath.endsWith(".tar.gz") || lowerPath.endsWith(".tgz");
|
|
371
|
+
// Rewrites are whole-archive: write to a temp file and rename so a
|
|
372
|
+
// crash/disk-full mid-write can't destroy the original archive.
|
|
373
|
+
const tmpPath = `${finalPath}.tmp-${process.pid}`;
|
|
353
374
|
|
|
354
375
|
const parentDir = path.dirname(resolvedArchivePath.absolutePath);
|
|
355
376
|
if (parentDir && parentDir !== ".") {
|
|
@@ -377,8 +398,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
377
398
|
try {
|
|
378
399
|
const { zipSync } = await loadFflate();
|
|
379
400
|
const zipBuffer = zipSync(zipEntries);
|
|
380
|
-
await Bun.write(
|
|
401
|
+
await Bun.write(tmpPath, zipBuffer);
|
|
402
|
+
await fs.rename(tmpPath, finalPath);
|
|
381
403
|
} catch (error) {
|
|
404
|
+
await fs.rm(tmpPath, { force: true }).catch(() => {});
|
|
382
405
|
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
383
406
|
}
|
|
384
407
|
} else {
|
|
@@ -406,8 +429,12 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
406
429
|
archiveEntries[resolvedArchivePath.archiveSubPath] = content;
|
|
407
430
|
|
|
408
431
|
try {
|
|
409
|
-
|
|
432
|
+
// `Bun.Archive.write` never infers compression from the extension;
|
|
433
|
+
// request gzip explicitly so `.tar.gz`/`.tgz` stay compressed.
|
|
434
|
+
await Bun.Archive.write(tmpPath, archiveEntries, isGzip ? { compress: "gzip" } : undefined);
|
|
435
|
+
await fs.rename(tmpPath, finalPath);
|
|
410
436
|
} catch (error) {
|
|
437
|
+
await fs.rm(tmpPath, { force: true }).catch(() => {});
|
|
411
438
|
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
412
439
|
}
|
|
413
440
|
}
|
|
@@ -583,7 +610,24 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
583
610
|
invalidateFsScanAfterWrite(absolutePath);
|
|
584
611
|
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
585
612
|
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
586
|
-
this.session.conflictHistory
|
|
613
|
+
const history = this.session.conflictHistory;
|
|
614
|
+
history?.invalidate(entry.id);
|
|
615
|
+
if (history) {
|
|
616
|
+
// Drop stale duplicate registrations of the same region: a re-read
|
|
617
|
+
// after an out-of-band shift registers a fresh id at the new
|
|
618
|
+
// startLine while the stale twin persists at the old one. A DISTINCT
|
|
619
|
+
// conflict block that is merely byte-identical still occurs in the
|
|
620
|
+
// post-splice content and must stay addressable.
|
|
621
|
+
for (const other of history.entries()) {
|
|
622
|
+
if (
|
|
623
|
+
other.absolutePath === absolutePath &&
|
|
624
|
+
conflictRegionsEqual(other, entry) &&
|
|
625
|
+
!conflictRegionPresent(newContent, other)
|
|
626
|
+
) {
|
|
627
|
+
history.invalidate(other.id);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
587
631
|
|
|
588
632
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, newContent);
|
|
589
633
|
const range =
|
|
@@ -690,17 +734,41 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
690
734
|
fileEntries.sort((a, b) => b.startLine - a.startLine);
|
|
691
735
|
|
|
692
736
|
let text: string;
|
|
737
|
+
const resolvedEntries: ConflictEntry[] = [];
|
|
738
|
+
const staleEntries: ConflictEntry[] = [];
|
|
739
|
+
let failure: string | undefined;
|
|
693
740
|
try {
|
|
694
741
|
text = await Bun.file(absolutePath).text();
|
|
695
|
-
|
|
742
|
+
} catch (error) {
|
|
743
|
+
failedFiles.push({
|
|
744
|
+
displayPath: sample.displayPath,
|
|
745
|
+
count: fileEntries.length,
|
|
746
|
+
error: error instanceof Error ? error.message : String(error),
|
|
747
|
+
});
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
for (const entry of fileEntries) {
|
|
751
|
+
try {
|
|
696
752
|
const expanded = expandContentTokens(replacementContent, entry);
|
|
697
753
|
text = spliceConflict(text, entry, expanded);
|
|
754
|
+
resolvedEntries.push(entry);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
// A locate-miss for a region an earlier entry already spliced
|
|
757
|
+
// in this pass is a stale duplicate registration (re-read after
|
|
758
|
+
// an out-of-band shift) — treat it as already resolved.
|
|
759
|
+
if (resolvedEntries.some(done => conflictRegionsEqual(done, entry))) {
|
|
760
|
+
staleEntries.push(entry);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
764
|
+
break;
|
|
698
765
|
}
|
|
699
|
-
}
|
|
766
|
+
}
|
|
767
|
+
if (failure !== undefined) {
|
|
700
768
|
failedFiles.push({
|
|
701
769
|
displayPath: sample.displayPath,
|
|
702
770
|
count: fileEntries.length,
|
|
703
|
-
error:
|
|
771
|
+
error: failure,
|
|
704
772
|
});
|
|
705
773
|
continue;
|
|
706
774
|
}
|
|
@@ -709,10 +777,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
709
777
|
invalidateFsScanAfterWrite(absolutePath);
|
|
710
778
|
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
711
779
|
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
712
|
-
for (const entry of
|
|
780
|
+
for (const entry of resolvedEntries) history.invalidate(entry.id);
|
|
781
|
+
for (const entry of staleEntries) history.invalidate(entry.id);
|
|
713
782
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, text);
|
|
714
|
-
succeededFiles.push({ displayPath: sample.displayPath, count:
|
|
715
|
-
totalResolvedIds +=
|
|
783
|
+
succeededFiles.push({ displayPath: sample.displayPath, count: resolvedEntries.length, header });
|
|
784
|
+
totalResolvedIds += resolvedEntries.length;
|
|
716
785
|
if (diagnostics) allDiagnostics.push(diagnostics);
|
|
717
786
|
}
|
|
718
787
|
|
|
@@ -751,7 +820,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
751
820
|
if (failedFiles.length > 0 && succeededFiles.length === 0) {
|
|
752
821
|
throw new ToolError(resultText);
|
|
753
822
|
}
|
|
754
|
-
return {
|
|
823
|
+
return {
|
|
824
|
+
content: [{ type: "text", text: resultText }],
|
|
825
|
+
details: {},
|
|
826
|
+
isError: failedFiles.length > 0 ? true : undefined,
|
|
827
|
+
};
|
|
755
828
|
}
|
|
756
829
|
const mergedSummary = allDiagnostics.map(d => d.summary).join("\n");
|
|
757
830
|
const mergedMessages = allDiagnostics.flatMap(d => d.messages ?? []);
|
|
@@ -760,6 +833,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
760
833
|
details: {
|
|
761
834
|
meta: outputMeta().diagnostics(mergedSummary, mergedMessages).get(),
|
|
762
835
|
},
|
|
836
|
+
isError: failedFiles.length > 0 ? true : undefined,
|
|
763
837
|
};
|
|
764
838
|
}
|
|
765
839
|
|
|
@@ -784,6 +858,9 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
784
858
|
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
785
859
|
const handler = internalRouter.getHandler(scheme);
|
|
786
860
|
if (handler?.write) {
|
|
861
|
+
// Handler-owned writes (vault:// notes, host URIs) mutate user
|
|
862
|
+
// data outside the local sandbox — plan mode must reject them.
|
|
863
|
+
enforcePlanModeWrite(this.session, path, { op: "update" });
|
|
787
864
|
await handler.write(parsed, cleanContent, { cwd: this.session.cwd, signal });
|
|
788
865
|
let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
|
|
789
866
|
if (stripped) {
|
|
@@ -872,6 +949,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
872
949
|
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
873
950
|
}
|
|
874
951
|
invalidateFsScanAfterWrite(absolutePath);
|
|
952
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
953
|
+
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
875
954
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
876
955
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
|
|
877
956
|
const writeLine = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
|
|
@@ -881,7 +960,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
881
960
|
}
|
|
882
961
|
return {
|
|
883
962
|
content: [{ type: "text", text: resultText }],
|
|
884
|
-
details: { resolvedPath: absolutePath },
|
|
963
|
+
details: { resolvedPath: absolutePath, madeExecutable: madeExecutable || undefined },
|
|
885
964
|
};
|
|
886
965
|
}
|
|
887
966
|
|
|
@@ -946,11 +1025,23 @@ function normalizeDisplayText(text: string): string {
|
|
|
946
1025
|
return text.replace(/\r/g, "");
|
|
947
1026
|
}
|
|
948
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Minimum line-number gutter width for write previews. The streaming preview's
|
|
1030
|
+
* gutter must stay byte-stable as the line count grows: a width derived purely
|
|
1031
|
+
* from `String(totalLines).length` widens at the 10/100/1000-line crossings,
|
|
1032
|
+
* rewriting every already-rendered row — which forces the transcript's commit
|
|
1033
|
+
* audit to recommit the block's committed prefix (a full duplicate in native
|
|
1034
|
+
* scrollback). Reserving 3 digits keeps the gutter constant through 999 lines
|
|
1035
|
+
* and keeps the streamed rows byte-identical to the final result render.
|
|
1036
|
+
*/
|
|
1037
|
+
const WRITE_GUTTER_MIN_WIDTH = 3;
|
|
1038
|
+
|
|
949
1039
|
function formatStreamingContent(
|
|
950
1040
|
content: string,
|
|
951
1041
|
expanded: boolean,
|
|
952
1042
|
language: string | undefined,
|
|
953
1043
|
uiTheme: Theme,
|
|
1044
|
+
spinnerFrame?: number,
|
|
954
1045
|
): string {
|
|
955
1046
|
if (!content) return "";
|
|
956
1047
|
const lines = normalizeDisplayText(content).split("\n");
|
|
@@ -963,7 +1054,7 @@ function formatStreamingContent(
|
|
|
963
1054
|
const visibleLines = lines.slice(startIndex);
|
|
964
1055
|
const hidden = startIndex;
|
|
965
1056
|
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
966
|
-
const lineNumberWidth = String(totalLines).length;
|
|
1057
|
+
const lineNumberWidth = Math.max(WRITE_GUTTER_MIN_WIDTH, String(totalLines).length);
|
|
967
1058
|
|
|
968
1059
|
let text = "\n\n";
|
|
969
1060
|
if (hidden > 0) {
|
|
@@ -975,7 +1066,12 @@ function formatStreamingContent(
|
|
|
975
1066
|
const body = replaceTabs(highlighted[i] ?? "");
|
|
976
1067
|
text += `${gutter}${body}\n`;
|
|
977
1068
|
}
|
|
978
|
-
|
|
1069
|
+
// The animated glyph lives on this trailing line — inside the transcript's
|
|
1070
|
+
// volatile-tail holdback — never in the header: an animating head row pins
|
|
1071
|
+
// the native-scrollback commit boundary at the top of the block, so a long
|
|
1072
|
+
// expanded preview could never scroll-append mid-stream.
|
|
1073
|
+
const spinner = spinnerFrame !== undefined ? `${formatStatusIcon("running", uiTheme, spinnerFrame)} ` : "";
|
|
1074
|
+
text += `${spinner}${uiTheme.fg("dim", `… (streaming)`)}`;
|
|
979
1075
|
return text;
|
|
980
1076
|
}
|
|
981
1077
|
|
|
@@ -991,7 +1087,7 @@ function renderContentPreview(
|
|
|
991
1087
|
const maxLines = expanded ? totalLines : Math.min(totalLines, WRITE_PREVIEW_LINES);
|
|
992
1088
|
const visibleLines = rawLines.slice(0, maxLines);
|
|
993
1089
|
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
994
|
-
const lineNumberWidth = String(
|
|
1090
|
+
const lineNumberWidth = Math.max(WRITE_GUTTER_MIN_WIDTH, String(totalLines).length);
|
|
995
1091
|
const hidden = totalLines - maxLines;
|
|
996
1092
|
|
|
997
1093
|
let text = "\n\n";
|
|
@@ -1016,10 +1112,14 @@ export const writeToolRenderer = {
|
|
|
1016
1112
|
const lang = getLanguageFromPath(rawPath) ?? "text";
|
|
1017
1113
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1018
1114
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
1115
|
+
// Static pending icon, never the animated glyph: the header is the head
|
|
1116
|
+
// row of the framed block, and native-scrollback commits are prefix-only
|
|
1117
|
+
// — an animating head row would pin the commit boundary at the top and
|
|
1118
|
+
// keep a tall expanded preview from scroll-appending mid-stream. The
|
|
1119
|
+
// liveness cue rides the trailing "(streaming)" line instead.
|
|
1019
1120
|
const header = renderStatusLine(
|
|
1020
1121
|
{
|
|
1021
1122
|
icon: "pending",
|
|
1022
|
-
spinnerFrame: options?.spinnerFrame,
|
|
1023
1123
|
title: "Write",
|
|
1024
1124
|
description: `${langIcon} ${pathDisplay}`,
|
|
1025
1125
|
},
|
|
@@ -1027,7 +1127,7 @@ export const writeToolRenderer = {
|
|
|
1027
1127
|
);
|
|
1028
1128
|
return framedBlock(uiTheme, width => {
|
|
1029
1129
|
const body = args.content
|
|
1030
|
-
? formatStreamingContent(args.content, Boolean(options?.expanded), lang, uiTheme)
|
|
1130
|
+
? formatStreamingContent(args.content, Boolean(options?.expanded), lang, uiTheme, options?.spinnerFrame)
|
|
1031
1131
|
: "";
|
|
1032
1132
|
const bodyLines = body ? body.split("\n") : [];
|
|
1033
1133
|
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
package/src/tui/output-block.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface OutputBlockOptions {
|
|
|
13
13
|
header?: string;
|
|
14
14
|
headerMeta?: string;
|
|
15
15
|
state?: State;
|
|
16
|
-
sections?: Array<{ label?: string; lines: string[]; separator?: boolean }>;
|
|
16
|
+
sections?: Array<{ label?: string; lines: readonly string[]; separator?: boolean }>;
|
|
17
17
|
width: number;
|
|
18
18
|
applyBg?: boolean;
|
|
19
19
|
contentPaddingLeft?: number;
|
|
@@ -186,8 +186,8 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
186
186
|
export class CachedOutputBlock {
|
|
187
187
|
#cache?: RenderCache;
|
|
188
188
|
|
|
189
|
-
/** Render with caching. Returns cached
|
|
190
|
-
render(options: OutputBlockOptions, theme: Theme): string[] {
|
|
189
|
+
/** Render with caching. Returns the cached (shared, caller-immutable) lines if options haven't changed. */
|
|
190
|
+
render(options: OutputBlockOptions, theme: Theme): readonly string[] {
|
|
191
191
|
const key = this.#buildKey(options);
|
|
192
192
|
if (this.#cache?.key === key) return this.#cache.lines;
|
|
193
193
|
const lines = renderOutputBlock(options, theme);
|
|
@@ -234,7 +234,7 @@ export function framedBlock(theme: Theme, build: (width: number) => OutputBlockO
|
|
|
234
234
|
// flush, no extra padding/background) the same way `markFramedBlockComponent`
|
|
235
235
|
// blocks are treated.
|
|
236
236
|
return markFramedBlockComponent({
|
|
237
|
-
render: (width: number): string[] => block.render(build(width), theme),
|
|
237
|
+
render: (width: number): readonly string[] => block.render(build(width), theme),
|
|
238
238
|
invalidate: () => block.invalidate(),
|
|
239
239
|
});
|
|
240
240
|
}
|
package/src/utils/changelog.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { getLastChangelogVersionPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
3
|
export interface ChangelogEntry {
|
|
4
4
|
major: number;
|
|
@@ -104,3 +104,29 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
|
|
|
104
104
|
|
|
105
105
|
// Re-export getChangelogPath from paths.ts for convenience
|
|
106
106
|
export { getChangelogPath } from "../config";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Last omp version whose changelog the user has seen. Stored as a plain-text
|
|
110
|
+
* marker file (`~/.omp/agent/last-changelog-version`) rather than in
|
|
111
|
+
* `config.yml`, so version bumps never dirty user-tracked config files.
|
|
112
|
+
*/
|
|
113
|
+
export async function readLastChangelogVersion(agentDir?: string): Promise<string | undefined> {
|
|
114
|
+
try {
|
|
115
|
+
const value = (await Bun.file(getLastChangelogVersionPath(agentDir)).text()).trim();
|
|
116
|
+
return value || undefined;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!isEnoent(error)) {
|
|
119
|
+
logger.warn("Failed to read last-changelog-version marker", { error: String(error) });
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Persist the last-seen changelog version marker. Best-effort: failures are logged, never thrown. */
|
|
126
|
+
export async function writeLastChangelogVersion(version: string, agentDir?: string): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
await Bun.write(getLastChangelogVersionPath(agentDir), version);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.warn("Failed to persist last-changelog-version marker", { error: String(error) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -11,6 +11,7 @@ import { formatHashlineHeader, formatNumberedLines, type SnapshotStore } from "@
|
|
|
11
11
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
13
13
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { canonicalSnapshotKey } from "../edit/file-snapshot-store";
|
|
14
15
|
import { normalizeToLF } from "../edit/normalize";
|
|
15
16
|
import type { FileMentionMessage } from "../session/messages";
|
|
16
17
|
import {
|
|
@@ -259,7 +260,7 @@ export async function generateFileMentionMessages(
|
|
|
259
260
|
const normalized = snapshotStore ? normalizeToLF(content) : content;
|
|
260
261
|
let { output, lineCount } = buildTextOutput(normalized);
|
|
261
262
|
if (snapshotStore) {
|
|
262
|
-
const tag = snapshotStore.record(absolutePath, normalized);
|
|
263
|
+
const tag = snapshotStore.record(canonicalSnapshotKey(absolutePath), normalized);
|
|
263
264
|
output = `${formatHashlineHeader(resolvedPath, tag)}\n${formatNumberedLines(output)}`;
|
|
264
265
|
}
|
|
265
266
|
files.push({ path: resolvedPath, content: output, lineCount });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseHTML } from "linkedom";
|
|
2
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
2
|
import { buildResult, loadPage } from "./types";
|
|
4
3
|
import { convertWithMarkit, fetchBinary } from "./utils";
|
|
@@ -31,6 +30,7 @@ export const handleArxiv: SpecialHandler = async (
|
|
|
31
30
|
if (!result.ok) return null;
|
|
32
31
|
|
|
33
32
|
// Parse the Atom feed response
|
|
33
|
+
const { parseHTML } = await import("linkedom");
|
|
34
34
|
const doc = parseHTML(result.content).document;
|
|
35
35
|
const entry = doc.querySelector("entry");
|
|
36
36
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import { parseHTML } from "linkedom";
|
|
3
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
4
3
|
import { buildResult, htmlToBasicMarkdown, loadPage } from "./types";
|
|
5
4
|
|
|
@@ -97,6 +96,7 @@ export const handleGoPkg: SpecialHandler = async (
|
|
|
97
96
|
});
|
|
98
97
|
}
|
|
99
98
|
|
|
99
|
+
const { parseHTML } = await import("linkedom");
|
|
100
100
|
const doc = parseHTML(pageResult.content).document;
|
|
101
101
|
|
|
102
102
|
// Extract actual module path from breadcrumb or header
|
package/src/web/scrapers/iacr.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseHTML } from "linkedom";
|
|
2
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
2
|
import { buildResult, loadPage } from "./types";
|
|
4
3
|
import { convertWithMarkit, fetchBinary } from "./utils";
|
|
@@ -30,6 +29,7 @@ export const handleIacr: SpecialHandler = async (
|
|
|
30
29
|
|
|
31
30
|
if (!result.ok) return null;
|
|
32
31
|
|
|
32
|
+
const { parseHTML } = await import("linkedom");
|
|
33
33
|
const doc = parseHTML(result.content).document;
|
|
34
34
|
|
|
35
35
|
// Extract metadata from the page
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read the Docs handler for web-fetch
|
|
3
3
|
*/
|
|
4
|
-
import { parseHTML } from "linkedom";
|
|
5
4
|
import { buildResult, htmlToBasicMarkdown, loadPage, type RenderResult, type SpecialHandler } from "./types";
|
|
6
5
|
|
|
7
6
|
export const handleReadTheDocs: SpecialHandler = async (
|
|
@@ -39,6 +38,7 @@ export const handleReadTheDocs: SpecialHandler = async (
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
// Parse HTML
|
|
41
|
+
const { parseHTML } = await import("linkedom");
|
|
42
42
|
const root = parseHTML(result.content).document;
|
|
43
43
|
|
|
44
44
|
// Extract main content from common Read the Docs selectors
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { HTMLElement } from "linkedom";
|
|
2
2
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
3
3
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
4
4
|
import { buildResult, loadPage } from "./types";
|
|
@@ -33,6 +33,7 @@ export const handleTwitter: SpecialHandler = async (
|
|
|
33
33
|
|
|
34
34
|
if (result.ok && result.content.length > 500) {
|
|
35
35
|
// Parse the Nitter HTML
|
|
36
|
+
const { parseHTML } = await import("linkedom");
|
|
36
37
|
const doc = parseHTML(result.content).document;
|
|
37
38
|
|
|
38
39
|
// Extract tweet content
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types and utilities for web-fetch handlers
|
|
3
3
|
*/
|
|
4
|
+
import { scheduler } from "node:timers/promises";
|
|
4
5
|
import { ptree } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import type TurndownService from "turndown";
|
|
6
7
|
|
|
@@ -70,6 +71,12 @@ export interface LoadPageOptions {
|
|
|
70
71
|
body?: string;
|
|
71
72
|
maxBytes?: number;
|
|
72
73
|
signal?: AbortSignal;
|
|
74
|
+
/**
|
|
75
|
+
* Return true to skip reading the response body for this content type
|
|
76
|
+
* (lowercased mime, no params). The caller is expected to re-fetch the
|
|
77
|
+
* payload as binary; this avoids streaming + decoding huge binaries twice.
|
|
78
|
+
*/
|
|
79
|
+
skipBodyForContentType?: (contentType: string) => boolean;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
export interface LoadPageResult {
|
|
@@ -78,6 +85,51 @@ export interface LoadPageResult {
|
|
|
78
85
|
finalUrl: string;
|
|
79
86
|
ok: boolean;
|
|
80
87
|
status?: number;
|
|
88
|
+
/** True when the body was cut mid-stream at maxBytes. */
|
|
89
|
+
truncated?: boolean;
|
|
90
|
+
/** Last transport-level error message when ok is false. */
|
|
91
|
+
error?: string;
|
|
92
|
+
/** True when the body read was skipped via skipBodyForContentType. */
|
|
93
|
+
bodySkipped?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const RETRY_AFTER_MAX_MS = 10_000;
|
|
97
|
+
|
|
98
|
+
/** Parse a Retry-After header (seconds or HTTP-date) into a bounded delay. */
|
|
99
|
+
function parseRetryAfterMs(value: string | null): number {
|
|
100
|
+
if (!value) return 1_000;
|
|
101
|
+
const seconds = Number(value);
|
|
102
|
+
if (Number.isFinite(seconds)) return Math.min(Math.max(seconds, 0) * 1000, RETRY_AFTER_MAX_MS);
|
|
103
|
+
const date = Date.parse(value);
|
|
104
|
+
if (!Number.isNaN(date)) return Math.min(Math.max(date - Date.now(), 0), RETRY_AFTER_MAX_MS);
|
|
105
|
+
return 1_000;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function charsetFromContentType(header: string): string | undefined {
|
|
109
|
+
return /charset\s*=\s*"?([\w-]+)"?/i.exec(header)?.[1];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Decode a response body honoring the declared charset (Content-Type header,
|
|
114
|
+
* then a cheap <meta charset> sniff), falling back to UTF-8.
|
|
115
|
+
*/
|
|
116
|
+
function decodeBody(bytes: Buffer, contentTypeHeader: string): string {
|
|
117
|
+
let label = charsetFromContentType(contentTypeHeader);
|
|
118
|
+
if (!label) {
|
|
119
|
+
// All charsets we can decode are ASCII-compatible in the prefix, so a
|
|
120
|
+
// latin1 view of the first 2KB is enough to find a <meta charset>.
|
|
121
|
+
label = /<meta[^>]+charset\s*=\s*["']?([\w-]+)/i.exec(bytes.subarray(0, 2048).toString("latin1"))?.[1];
|
|
122
|
+
}
|
|
123
|
+
if (label && !/^utf-?8$/i.test(label)) {
|
|
124
|
+
try {
|
|
125
|
+
// Bun.Encoding's union is narrower than the runtime, which accepts
|
|
126
|
+
// WHATWG labels (shift_jis, euc-kr, gbk, big5, …); unknowns throw here.
|
|
127
|
+
return new TextDecoder(label as Bun.Encoding).decode(bytes);
|
|
128
|
+
} catch {
|
|
129
|
+
// Unknown/unsupported label — fall back to UTF-8.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return bytes.toString("utf-8");
|
|
81
133
|
}
|
|
82
134
|
|
|
83
135
|
/**
|
|
@@ -86,6 +138,8 @@ export interface LoadPageResult {
|
|
|
86
138
|
export async function loadPage(url: string, options: LoadPageOptions = {}): Promise<LoadPageResult> {
|
|
87
139
|
const { timeout = 20, headers = {}, maxBytes = MAX_BYTES, signal, method = "GET", body } = options;
|
|
88
140
|
|
|
141
|
+
let lastError: string | undefined;
|
|
142
|
+
let retried429 = false;
|
|
89
143
|
for (let attempt = 0; attempt < USER_AGENTS.length; attempt++) {
|
|
90
144
|
if (signal?.aborted) {
|
|
91
145
|
throw new ToolAbortError();
|
|
@@ -114,9 +168,31 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
114
168
|
|
|
115
169
|
const response = await fetch(url, requestInit);
|
|
116
170
|
|
|
117
|
-
const
|
|
171
|
+
const rawContentType = response.headers.get("content-type") ?? "";
|
|
172
|
+
const contentType = rawContentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
118
173
|
const finalUrl = response.url;
|
|
119
174
|
|
|
175
|
+
if (response.status === 429 && !retried429) {
|
|
176
|
+
// Rate limited: retry once, honoring a bounded Retry-After. The
|
|
177
|
+
// wait observes the caller's signal so an Esc during the backoff
|
|
178
|
+
// does not stall for up to the full delay.
|
|
179
|
+
retried429 = true;
|
|
180
|
+
const delayMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
181
|
+
void response.body?.cancel().catch(() => {});
|
|
182
|
+
try {
|
|
183
|
+
await scheduler.wait(delayMs, { signal });
|
|
184
|
+
} catch {
|
|
185
|
+
throw new ToolAbortError();
|
|
186
|
+
}
|
|
187
|
+
attempt--; // Reuse the same user agent for the retry.
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (response.ok && options.skipBodyForContentType?.(contentType)) {
|
|
192
|
+
void response.body?.cancel().catch(() => {});
|
|
193
|
+
return { content: "", contentType, finalUrl, ok: true, status: response.status, bodySkipped: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
120
196
|
const reader = response.body?.getReader();
|
|
121
197
|
if (!reader) {
|
|
122
198
|
return { content: "", contentType, finalUrl, ok: false, status: response.status };
|
|
@@ -124,6 +200,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
124
200
|
|
|
125
201
|
const chunks: Uint8Array[] = [];
|
|
126
202
|
let totalSize = 0;
|
|
203
|
+
let truncated = false;
|
|
127
204
|
|
|
128
205
|
while (true) {
|
|
129
206
|
const { done, value } = await reader.read();
|
|
@@ -133,32 +210,34 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
133
210
|
totalSize += value.length;
|
|
134
211
|
|
|
135
212
|
if (totalSize > maxBytes) {
|
|
136
|
-
|
|
213
|
+
truncated = true;
|
|
214
|
+
void reader.cancel().catch(() => {});
|
|
137
215
|
break;
|
|
138
216
|
}
|
|
139
217
|
}
|
|
140
218
|
|
|
141
|
-
const content = Buffer.concat(chunks)
|
|
219
|
+
const content = decodeBody(Buffer.concat(chunks), rawContentType);
|
|
142
220
|
if (isBotBlocked(response.status, content) && attempt < USER_AGENTS.length - 1) {
|
|
143
221
|
continue;
|
|
144
222
|
}
|
|
145
223
|
|
|
146
224
|
if (!response.ok) {
|
|
147
|
-
return { content, contentType, finalUrl, ok: false, status: response.status };
|
|
225
|
+
return { content, contentType, finalUrl, ok: false, status: response.status, truncated };
|
|
148
226
|
}
|
|
149
227
|
|
|
150
|
-
return { content, contentType, finalUrl, ok: true, status: response.status };
|
|
151
|
-
} catch {
|
|
228
|
+
return { content, contentType, finalUrl, ok: true, status: response.status, truncated };
|
|
229
|
+
} catch (error) {
|
|
152
230
|
if (signal?.aborted) {
|
|
153
231
|
throw new ToolAbortError();
|
|
154
232
|
}
|
|
233
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
155
234
|
if (attempt === USER_AGENTS.length - 1) {
|
|
156
|
-
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
235
|
+
return { content: "", contentType: "", finalUrl: url, ok: false, error: lastError };
|
|
157
236
|
}
|
|
158
237
|
}
|
|
159
238
|
}
|
|
160
239
|
|
|
161
|
-
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
240
|
+
return { content: "", contentType: "", finalUrl: url, ok: false, error: lastError };
|
|
162
241
|
}
|
|
163
242
|
|
|
164
243
|
/** Module-level Turndown instance — built lazily on first use. */
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseHTML } from "linkedom";
|
|
2
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
2
|
import { buildResult, loadPage } from "./types";
|
|
4
3
|
|
|
@@ -45,6 +44,7 @@ export const handleWikipedia: SpecialHandler = async (
|
|
|
45
44
|
const contentResult = await loadPage(contentUrl, { timeout, signal });
|
|
46
45
|
|
|
47
46
|
if (contentResult.ok) {
|
|
47
|
+
const { parseHTML } = await import("linkedom");
|
|
48
48
|
const doc = parseHTML(contentResult.content).document;
|
|
49
49
|
|
|
50
50
|
// Extract main content sections
|
|
@@ -288,12 +288,17 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
} finally {
|
|
291
|
-
throwIfAborted(signal);
|
|
292
291
|
// Cleanup temp files (fire-and-forget with error suppression)
|
|
293
292
|
Array.fromAsync(new Bun.Glob(`${tmpBase}*`).scan({ absolute: true }))
|
|
294
293
|
.then(tmpFiles => Promise.all(tmpFiles.map(f => fs.unlink(f).catch(() => {}))))
|
|
295
294
|
.catch(() => {});
|
|
296
295
|
}
|
|
296
|
+
// Only a user-initiated abort is fatal; the per-fetch time budget expiring
|
|
297
|
+
// just means partial metadata/transcript, which we surface as a note.
|
|
298
|
+
throwIfAborted(userSignal);
|
|
299
|
+
if (signal?.aborted) {
|
|
300
|
+
notes.push("Fetch time budget exhausted; metadata/transcript may be incomplete");
|
|
301
|
+
}
|
|
297
302
|
|
|
298
303
|
// Build markdown output
|
|
299
304
|
let md = `# ${title}\n\n`;
|
package/src/web/search/index.ts
CHANGED
|
@@ -150,7 +150,7 @@ async function executeSearch(
|
|
|
150
150
|
lastProvider = provider;
|
|
151
151
|
try {
|
|
152
152
|
const response = await provider.search({
|
|
153
|
-
query: params.query
|
|
153
|
+
query: params.query,
|
|
154
154
|
limit: params.limit,
|
|
155
155
|
recency: params.recency,
|
|
156
156
|
systemPrompt: webSearchSystemPrompt,
|