@armadra/agent 0.4.0 → 0.5.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/CHANGELOG.md +174 -1
- package/README.md +201 -82
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/dist/acp.d.ts +18 -0
- package/dist/acp.js +19 -0
- package/dist/agent/limits.d.ts +38 -0
- package/dist/agent/limits.js +127 -0
- package/dist/agent/loop-guard.d.ts +38 -0
- package/dist/agent/loop-guard.js +95 -0
- package/dist/agent/loop.d.ts +4 -0
- package/dist/agent/loop.js +7 -0
- package/dist/agent/prompt-rules.d.ts +12 -0
- package/dist/agent/prompt-rules.js +18 -0
- package/dist/agent/reminders.d.ts +59 -0
- package/dist/agent/reminders.js +327 -0
- package/dist/agent/session-cache.d.ts +5 -0
- package/dist/agent/session-cache.js +17 -1
- package/dist/agent/session-classifier.d.ts +1 -1
- package/dist/agent/session-classifier.js +3 -1
- package/dist/agent/session-compaction.d.ts +35 -6
- package/dist/agent/session-compaction.js +157 -31
- package/dist/agent/session-core.d.ts +27 -1
- package/dist/agent/session-extensions.d.ts +61 -0
- package/dist/agent/session-extensions.js +129 -0
- package/dist/agent/session-images.d.ts +23 -0
- package/dist/agent/session-images.js +97 -0
- package/dist/agent/session-plan.d.ts +21 -0
- package/dist/agent/session-plan.js +524 -0
- package/dist/agent/session-rewind.d.ts +81 -0
- package/dist/agent/session-rewind.js +354 -0
- package/dist/agent/session-run.d.ts +28 -1
- package/dist/agent/session-run.js +157 -6
- package/dist/agent/session-settings.d.ts +49 -0
- package/dist/agent/session-settings.js +95 -0
- package/dist/agent/session-subagent.d.ts +36 -24
- package/dist/agent/session-subagent.js +259 -129
- package/dist/agent/session-sync.d.ts +10 -3
- package/dist/agent/session-sync.js +37 -8
- package/dist/agent/session-telemetry.d.ts +39 -0
- package/dist/agent/session-telemetry.js +231 -0
- package/dist/agent/session-tools.js +40 -1
- package/dist/agent/session.d.ts +30 -29
- package/dist/agent/session.js +79 -123
- package/dist/agent/subagent-registry.d.ts +91 -0
- package/dist/agent/subagent-registry.js +494 -0
- package/dist/agent/system-prompt.d.ts +6 -3
- package/dist/agent/system-prompt.js +7 -2
- package/dist/agent/tool-runner.d.ts +8 -1
- package/dist/agent/tool-runner.js +57 -4
- package/dist/agent/types-w5.d.ts +192 -0
- package/dist/agent/types-w5.js +10 -0
- package/dist/agent/types.d.ts +47 -4
- package/dist/agent/types.js +2 -0
- package/dist/agent/worktree.d.ts +36 -0
- package/dist/agent/worktree.js +86 -0
- package/dist/agents/builtin.d.ts +14 -0
- package/dist/agents/builtin.js +39 -0
- package/dist/agents/catalog.d.ts +43 -0
- package/dist/agents/catalog.js +82 -0
- package/dist/agents/discover.d.ts +35 -0
- package/dist/agents/discover.js +83 -0
- package/dist/agents/external.d.ts +82 -0
- package/dist/agents/external.js +326 -0
- package/dist/agents/parse.d.ts +18 -0
- package/dist/agents/parse.js +116 -0
- package/dist/agents/result.d.ts +32 -0
- package/dist/agents/result.js +67 -0
- package/dist/agents/task-control.d.ts +17 -0
- package/dist/agents/task-control.js +17 -0
- package/dist/agents/task-record.d.ts +78 -0
- package/dist/agents/task-record.js +141 -0
- package/dist/agents/types.d.ts +46 -0
- package/dist/agents/types.js +8 -0
- package/dist/ai/apis/anthropic-compat.d.ts +36 -0
- package/dist/ai/apis/anthropic-compat.js +92 -0
- package/dist/ai/apis/anthropic-request.d.ts +7 -6
- package/dist/ai/apis/anthropic-request.js +10 -28
- package/dist/ai/apis/cache-params.d.ts +11 -2
- package/dist/ai/apis/cache-params.js +25 -6
- package/dist/ai/apis/openai-compat.d.ts +4 -1
- package/dist/ai/apis/openai-compat.js +8 -1
- package/dist/ai/image-limits.d.ts +45 -0
- package/dist/ai/image-limits.js +68 -0
- package/dist/ai/providers/builtin.d.ts +18 -6
- package/dist/ai/providers/builtin.js +135 -6
- package/dist/ai/providers/catalog-data.js +17 -13
- package/dist/ai/providers/catalog.d.ts +60 -7
- package/dist/ai/providers/catalog.js +197 -38
- package/dist/ai/providers/channels.d.ts +33 -3
- package/dist/ai/providers/channels.js +108 -7
- package/dist/ai/providers/enrich.d.ts +6 -3
- package/dist/ai/providers/enrich.js +19 -4
- package/dist/ai/providers/models-dev-cache.d.ts +28 -24
- package/dist/ai/providers/models-dev-cache.js +92 -78
- package/dist/ai/providers/models-dev-data.d.ts +7 -0
- package/dist/ai/providers/models-dev-data.js +32 -0
- package/dist/ai/providers/models-dev-snapshot.d.ts +50 -0
- package/dist/ai/providers/models-dev-snapshot.js +137 -0
- package/dist/ai/providers/models-dev.d.ts +51 -10
- package/dist/ai/providers/models-dev.js +132 -39
- package/dist/ai/providers/registry.d.ts +20 -10
- package/dist/ai/providers/registry.js +82 -43
- package/dist/ai/types.d.ts +28 -2
- package/dist/ai/types.js +5 -0
- package/dist/bundle/ama.cjs +36203 -20499
- package/dist/checkpoints/backend.d.ts +61 -0
- package/dist/checkpoints/backend.js +135 -0
- package/dist/checkpoints/blobs.d.ts +60 -0
- package/dist/checkpoints/blobs.js +203 -0
- package/dist/checkpoints/gc.d.ts +47 -0
- package/dist/checkpoints/gc.js +144 -0
- package/dist/checkpoints/git-head.d.ts +15 -0
- package/dist/checkpoints/git-head.js +89 -0
- package/dist/checkpoints/index.d.ts +17 -0
- package/dist/checkpoints/index.js +17 -0
- package/dist/checkpoints/replay.d.ts +48 -0
- package/dist/checkpoints/replay.js +154 -0
- package/dist/checkpoints/restore.d.ts +86 -0
- package/dist/checkpoints/restore.js +263 -0
- package/dist/checkpoints/settings.d.ts +14 -0
- package/dist/checkpoints/settings.js +23 -0
- package/dist/checkpoints/shadow-git.d.ts +108 -0
- package/dist/checkpoints/shadow-git.js +491 -0
- package/dist/checkpoints/shadow-restore.d.ts +25 -0
- package/dist/checkpoints/shadow-restore.js +128 -0
- package/dist/checkpoints/tracker.d.ts +64 -0
- package/dist/checkpoints/tracker.js +192 -0
- package/dist/checkpoints/types.d.ts +99 -0
- package/dist/checkpoints/types.js +12 -0
- package/dist/cli/args.d.ts +8 -4
- package/dist/cli/args.js +17 -97
- package/dist/cli/bootstrap.js +13 -1
- package/dist/cli/codemode-notice.d.ts +3 -2
- package/dist/cli/codemode-notice.js +3 -2
- package/dist/cli/compose-agents.d.ts +27 -0
- package/dist/cli/compose-agents.js +111 -0
- package/dist/cli/compose-extensions.d.ts +29 -0
- package/dist/cli/compose-extensions.js +44 -0
- package/dist/cli/compose-session.d.ts +3 -0
- package/dist/cli/compose-session.js +47 -4
- package/dist/cli/compose.d.ts +3 -0
- package/dist/cli/compose.js +33 -6
- package/dist/cli/deps.d.ts +6 -0
- package/dist/cli/exit-codes.d.ts +10 -0
- package/dist/cli/exit-codes.js +12 -0
- package/dist/cli/help-text.d.ts +4 -0
- package/dist/cli/help-text.js +102 -0
- package/dist/cli/startup-screen.js +5 -3
- package/dist/cli/startup-steps.js +7 -1
- package/dist/cli/subcommands/config.d.ts +4 -1
- package/dist/cli/subcommands/config.js +17 -6
- package/dist/cli/subcommands/doctor.js +21 -0
- package/dist/cli/subcommands/models-discover.js +5 -6
- package/dist/cli/subcommands/models.d.ts +2 -0
- package/dist/cli/subcommands/models.js +33 -14
- package/dist/cli/subcommands/providers.js +5 -6
- package/dist/cli/subcommands/sessions.d.ts +3 -1
- package/dist/cli/subcommands/sessions.js +36 -2
- package/dist/codemode/capability.d.ts +24 -7
- package/dist/codemode/capability.js +38 -10
- package/dist/codemode/host-side.d.ts +14 -0
- package/dist/codemode/host-side.js +24 -1
- package/dist/codemode/tool.d.ts +1 -1
- package/dist/codemode/tool.js +6 -4
- package/dist/compaction/breaker.d.ts +25 -9
- package/dist/compaction/breaker.js +47 -17
- package/dist/compaction/estimate.d.ts +11 -3
- package/dist/compaction/estimate.js +64 -22
- package/dist/compaction/image-budget.d.ts +45 -0
- package/dist/compaction/image-budget.js +115 -0
- package/dist/compaction/post-compact.d.ts +38 -0
- package/dist/compaction/post-compact.js +152 -0
- package/dist/compaction/protect.d.ts +27 -0
- package/dist/compaction/protect.js +56 -0
- package/dist/compaction/prune-tier.d.ts +58 -12
- package/dist/compaction/prune-tier.js +120 -54
- package/dist/compaction/summarize-tier.d.ts +23 -4
- package/dist/compaction/summarize-tier.js +100 -29
- package/dist/config/checker.d.ts +28 -0
- package/dist/config/checker.js +97 -0
- package/dist/config/json-schema.js +63 -2
- package/dist/config/key-docs.js +72 -3
- package/dist/config/merge.d.ts +5 -3
- package/dist/config/merge.js +81 -7
- package/dist/config/profile.d.ts +2 -0
- package/dist/config/profile.js +2 -0
- package/dist/config/schema-w5.d.ts +18 -0
- package/dist/config/schema-w5.js +109 -0
- package/dist/config/schema.d.ts +5 -9
- package/dist/config/schema.js +68 -114
- package/dist/config/types-w5.d.ts +118 -0
- package/dist/config/types-w5.js +20 -0
- package/dist/config/types.d.ts +50 -1
- package/dist/config/types.js +12 -0
- package/dist/drivers/acp/client.d.ts +57 -0
- package/dist/drivers/acp/client.js +160 -0
- package/dist/drivers/acp/driver.d.ts +31 -0
- package/dist/drivers/acp/driver.js +274 -0
- package/dist/drivers/acp/testing/fake-agent-main.d.ts +5 -0
- package/dist/drivers/acp/testing/fake-agent-main.js +7 -0
- package/dist/drivers/acp/testing/fake-agent.d.ts +27 -0
- package/dist/drivers/acp/testing/fake-agent.js +211 -0
- package/dist/drivers/acp/types.d.ts +285 -0
- package/dist/drivers/acp/types.js +36 -0
- package/dist/drivers/agents.d.ts +58 -0
- package/dist/drivers/agents.js +142 -0
- package/dist/drivers/base.d.ts +35 -0
- package/dist/drivers/base.js +59 -0
- package/dist/drivers/catalog.d.ts +46 -0
- package/dist/drivers/catalog.js +167 -0
- package/dist/drivers/env.d.ts +20 -0
- package/dist/drivers/env.js +46 -0
- package/dist/drivers/host-runners.d.ts +18 -0
- package/dist/drivers/host-runners.js +45 -0
- package/dist/drivers/jsonrpc.d.ts +58 -0
- package/dist/drivers/jsonrpc.js +180 -0
- package/dist/drivers/native/claude-normalize.d.ts +46 -0
- package/dist/drivers/native/claude-normalize.js +106 -0
- package/dist/drivers/native/claude-stream.d.ts +29 -0
- package/dist/drivers/native/claude-stream.js +456 -0
- package/dist/drivers/native/codex-app-server.d.ts +26 -0
- package/dist/drivers/native/codex-app-server.js +407 -0
- package/dist/drivers/native/codex-normalize.d.ts +38 -0
- package/dist/drivers/native/codex-normalize.js +124 -0
- package/dist/drivers/native/oneshot.d.ts +28 -0
- package/dist/drivers/native/oneshot.js +312 -0
- package/dist/drivers/permissions.d.ts +40 -0
- package/dist/drivers/permissions.js +92 -0
- package/dist/drivers/pids.d.ts +32 -0
- package/dist/drivers/pids.js +93 -0
- package/dist/drivers/pool.d.ts +28 -0
- package/dist/drivers/pool.js +96 -0
- package/dist/drivers/probe.d.ts +47 -0
- package/dist/drivers/probe.js +172 -0
- package/dist/drivers/process.d.ts +48 -0
- package/dist/drivers/process.js +100 -0
- package/dist/drivers/runner.d.ts +61 -0
- package/dist/drivers/runner.js +388 -0
- package/dist/drivers/store.d.ts +49 -0
- package/dist/drivers/store.js +91 -0
- package/dist/drivers/turn.d.ts +41 -0
- package/dist/drivers/turn.js +99 -0
- package/dist/drivers/types.d.ts +122 -0
- package/dist/drivers/types.js +11 -0
- package/dist/git/info.d.ts +75 -0
- package/dist/git/info.js +191 -0
- package/dist/hooks/protocol.js +6 -1
- package/dist/hooks/types.d.ts +10 -1
- package/dist/hooks/types.js +2 -0
- package/dist/host/api-impl.d.ts +7 -0
- package/dist/host/api-impl.js +15 -0
- package/dist/host/types.d.ts +24 -2
- package/dist/host/types.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/modes/acp/acp-events.d.ts +46 -0
- package/dist/modes/acp/acp-events.js +233 -0
- package/dist/modes/acp/acp-mode.d.ts +17 -0
- package/dist/modes/acp/acp-mode.js +47 -0
- package/dist/modes/acp/acp-server.d.ts +79 -0
- package/dist/modes/acp/acp-server.js +315 -0
- package/dist/modes/commands-core.d.ts +7 -1
- package/dist/modes/commands-core.js +55 -0
- package/dist/modes/image-input.d.ts +12 -4
- package/dist/modes/image-input.js +22 -5
- package/dist/modes/interactive/agent-panels.d.ts +14 -0
- package/dist/modes/interactive/agent-panels.js +103 -0
- package/dist/modes/interactive/agent-ui.d.ts +73 -0
- package/dist/modes/interactive/agent-ui.js +234 -0
- package/dist/modes/interactive/approval-dialog.d.ts +21 -4
- package/dist/modes/interactive/approval-dialog.js +98 -20
- package/dist/modes/interactive/approval-merge.d.ts +41 -0
- package/dist/modes/interactive/approval-merge.js +89 -0
- package/dist/modes/interactive/clipboard-paste.d.ts +18 -0
- package/dist/modes/interactive/clipboard-paste.js +28 -0
- package/dist/modes/interactive/commands.d.ts +9 -1
- package/dist/modes/interactive/commands.js +19 -0
- package/dist/modes/interactive/double-esc.d.ts +20 -0
- package/dist/modes/interactive/double-esc.js +34 -0
- package/dist/modes/interactive/event-notices.d.ts +32 -0
- package/dist/modes/interactive/event-notices.js +79 -0
- package/dist/modes/interactive/external-editor.d.ts +16 -0
- package/dist/modes/interactive/external-editor.js +39 -0
- package/dist/modes/interactive/interactive-mode.d.ts +14 -1
- package/dist/modes/interactive/interactive-mode.js +134 -136
- package/dist/modes/interactive/key-dispatch.d.ts +16 -2
- package/dist/modes/interactive/key-dispatch.js +49 -1
- package/dist/modes/interactive/line/line-mode.d.ts +2 -1
- package/dist/modes/interactive/line/line-mode.js +22 -5
- package/dist/modes/interactive/line/line-render.d.ts +2 -1
- package/dist/modes/interactive/line/line-render.js +40 -4
- package/dist/modes/interactive/message-view.d.ts +1 -1
- package/dist/modes/interactive/message-view.js +24 -2
- package/dist/modes/interactive/panels.d.ts +18 -1
- package/dist/modes/interactive/panels.js +19 -5
- package/dist/modes/interactive/plan-command.d.ts +40 -0
- package/dist/modes/interactive/plan-command.js +129 -0
- package/dist/modes/interactive/plan-dialog.d.ts +98 -0
- package/dist/modes/interactive/plan-dialog.js +290 -0
- package/dist/modes/interactive/plan-flow.d.ts +34 -0
- package/dist/modes/interactive/plan-flow.js +98 -0
- package/dist/modes/interactive/rewind-command.d.ts +31 -0
- package/dist/modes/interactive/rewind-command.js +106 -0
- package/dist/modes/interactive/rewind-flow.d.ts +37 -0
- package/dist/modes/interactive/rewind-flow.js +151 -0
- package/dist/modes/interactive/rewind-list.d.ts +36 -0
- package/dist/modes/interactive/rewind-list.js +123 -0
- package/dist/modes/interactive/rewind-panel.d.ts +77 -0
- package/dist/modes/interactive/rewind-panel.js +309 -0
- package/dist/modes/interactive/rewind-text.d.ts +40 -0
- package/dist/modes/interactive/rewind-text.js +139 -0
- package/dist/modes/interactive/session-events.d.ts +24 -0
- package/dist/modes/interactive/session-events.js +96 -0
- package/dist/modes/interactive/status-area.d.ts +73 -0
- package/dist/modes/interactive/status-area.js +190 -0
- package/dist/modes/interactive/status-bar.d.ts +99 -11
- package/dist/modes/interactive/status-bar.js +273 -90
- package/dist/modes/interactive/status-line.d.ts +33 -0
- package/dist/modes/interactive/status-line.js +113 -0
- package/dist/modes/interactive/subagent-view.d.ts +56 -0
- package/dist/modes/interactive/subagent-view.js +154 -0
- package/dist/modes/interactive/tasks-report.d.ts +27 -0
- package/dist/modes/interactive/tasks-report.js +114 -0
- package/dist/modes/interactive/tool-view.d.ts +12 -2
- package/dist/modes/interactive/tool-view.js +34 -4
- package/dist/modes/print/print-mode.d.ts +13 -4
- package/dist/modes/print/print-mode.js +56 -12
- package/dist/modes/rpc/commands.d.ts +9 -0
- package/dist/modes/rpc/commands.js +44 -0
- package/dist/modes/rpc/rpc-mode.js +7 -1
- package/dist/modes/session-report.d.ts +11 -0
- package/dist/modes/session-report.js +48 -1
- package/dist/permissions/auto-safe.d.ts +5 -0
- package/dist/permissions/auto-safe.js +18 -0
- package/dist/permissions/classifier.d.ts +4 -0
- package/dist/permissions/classifier.js +2 -0
- package/dist/permissions/modes.js +1 -1
- package/dist/permissions/pipeline.d.ts +43 -4
- package/dist/permissions/pipeline.js +114 -9
- package/dist/permissions/readonly-bash.d.ts +30 -0
- package/dist/permissions/readonly-bash.js +118 -0
- package/dist/permissions/types.d.ts +42 -0
- package/dist/plan/compose.d.ts +9 -0
- package/dist/plan/compose.js +19 -0
- package/dist/plan/controller.d.ts +71 -0
- package/dist/plan/controller.js +27 -0
- package/dist/plan/done-markers.d.ts +17 -0
- package/dist/plan/done-markers.js +51 -0
- package/dist/plan/extract.d.ts +29 -0
- package/dist/plan/extract.js +160 -0
- package/dist/plan/prompts.d.ts +36 -0
- package/dist/plan/prompts.js +89 -0
- package/dist/plan/store.d.ts +37 -0
- package/dist/plan/store.js +90 -0
- package/dist/rpc.d.ts +58 -2
- package/dist/rpc.js +7 -0
- package/dist/sandbox/bash.d.ts +74 -0
- package/dist/sandbox/bash.js +126 -0
- package/dist/sandbox/detect.d.ts +48 -0
- package/dist/sandbox/detect.js +149 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/profile.d.ts +33 -0
- package/dist/sandbox/profile.js +76 -0
- package/dist/sandbox/wrap.d.ts +29 -0
- package/dist/sandbox/wrap.js +117 -0
- package/dist/sdk.d.ts +33 -2
- package/dist/sdk.js +24 -2
- package/dist/session/types.d.ts +3 -1
- package/dist/session/types.js +1 -0
- package/dist/skills/builtin.d.ts +31 -0
- package/dist/skills/builtin.js +109 -0
- package/dist/skills/discover.d.ts +2 -1
- package/dist/skills/index-prompt.js +5 -8
- package/dist/tools/background-jobs.d.ts +71 -0
- package/dist/tools/background-jobs.js +198 -0
- package/dist/tools/bash.d.ts +53 -1
- package/dist/tools/bash.js +158 -7
- package/dist/tools/clipboard-image.d.ts +44 -0
- package/dist/tools/clipboard-image.js +148 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.js +18 -3
- package/dist/tools/image-file.d.ts +38 -3
- package/dist/tools/image-file.js +81 -10
- package/dist/tools/image-resize.d.ts +58 -0
- package/dist/tools/image-resize.js +97 -0
- package/dist/tools/presets.d.ts +7 -2
- package/dist/tools/presets.js +11 -4
- package/dist/tools/read.d.ts +5 -2
- package/dist/tools/read.js +13 -10
- package/dist/tools/registry.d.ts +7 -1
- package/dist/tools/registry.js +27 -4
- package/dist/tools/task-ctl.d.ts +22 -0
- package/dist/tools/task-ctl.js +116 -0
- package/dist/tools/task.d.ts +22 -10
- package/dist/tools/task.js +66 -26
- package/dist/tools/todo.d.ts +22 -2
- package/dist/tools/todo.js +58 -14
- package/dist/tools/truncate.d.ts +15 -0
- package/dist/tools/truncate.js +25 -0
- package/dist/tools/types.d.ts +113 -0
- package/dist/tools/types.js +5 -0
- package/dist/tools/write.js +3 -0
- package/dist/tui/glyphs.d.ts +4 -0
- package/dist/tui/glyphs.js +4 -0
- package/dist/tui/keybindings.d.ts +5 -0
- package/dist/tui/keybindings.js +5 -0
- package/docs/acp.md +78 -0
- package/docs/agents.md +250 -0
- package/docs/codemode.md +12 -8
- package/docs/hooks.md +26 -22
- package/docs/permissions.md +77 -13
- package/docs/plan.md +107 -0
- package/docs/providers.md +228 -52
- package/docs/rewind-plan.md +181 -0
- package/docs/rpc.md +92 -5
- package/docs/sandbox.md +215 -0
- package/docs/session-format.md +41 -21
- package/docs/sessions.md +41 -1
- package/docs/tui-design.md +751 -0
- package/docs/tui.md +125 -6
- package/package.json +13 -2
package/dist/tools/bash.js
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* - 结构化结果 `{ output, exit_code, truncated, full_output_path?, wall_time_seconds }`;
|
|
8
8
|
* 信号退出 `128 + signo`;非零退出码 → isError;
|
|
9
9
|
* - 注入 `AMA_*` 环境变量(shell.ts)。shell 退出后不等仍占着管道的后台进程。
|
|
10
|
+
* - [W5-H2] 后台命令(§8.3 H6,不加新工具):`{ command, background: true }` 立即返回
|
|
11
|
+
* `{ jobId, outputPath, pid }`;`{ job, action: "wait" | "output" | "stop" }` 查询 / 结束
|
|
12
|
+
* (background-jobs.ts,按会话登记,会话 dispose 时回收进程树;退出经提醒通道通知)。
|
|
13
|
+
* - **进程只在 `spawnShell` 里创建**(前台与后台共用)。[S2] `sandbox.bash: auto` 且有能限制写入的
|
|
14
|
+
* OS 沙箱时在这一处包一层(src/sandbox/bash.ts);`sandbox: false` 不包装(权限管线照常审批)。
|
|
15
|
+
* 失败输出像是被沙箱拒绝时末尾追加一行说明。
|
|
10
16
|
*/
|
|
11
17
|
import { spawn } from "node:child_process";
|
|
12
18
|
import { stat } from "node:fs/promises";
|
|
@@ -16,11 +22,32 @@ import { buildShellArgs, buildToolEnv, resolveShell } from "./shell.js";
|
|
|
16
22
|
import { exitCodeOf, isGroupAlive, killProcessTree, trackProcessGroup, untrackProcessGroup, } from "./process-tree.js";
|
|
17
23
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
18
24
|
import { formatSize, resolveOutputDir, safeFileName } from "./truncate.js";
|
|
25
|
+
import { jobsForSession } from "./background-jobs.js";
|
|
26
|
+
import { looksLikeSandboxDenial, runsSandboxed, sandboxDenialHint, wrapBashCommand, } from "../sandbox/bash.js";
|
|
19
27
|
export const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
|
20
28
|
export const MAX_BASH_TIMEOUT_MS = 600_000;
|
|
21
29
|
const UPDATE_THROTTLE_MS = 100;
|
|
22
30
|
/** shell 退出后等管道关闭的上限(后台子进程可能一直占着)。 */
|
|
23
31
|
const STDIO_DRAIN_MS = 200;
|
|
32
|
+
/** [W5-H2] `wait` 的缺省等待(毫秒)。 */
|
|
33
|
+
export const DEFAULT_JOB_WAIT_MS = 30_000;
|
|
34
|
+
/**
|
|
35
|
+
* bash 工具唯一的进程创建点(前台 / 后台共用);[S2] 给了 `sandbox` 时经 OS 沙箱包装(包不上抛错,
|
|
36
|
+
* 不悄悄裸跑)。
|
|
37
|
+
*/
|
|
38
|
+
export function spawnShell(shell, command, options) {
|
|
39
|
+
const args = buildShellArgs(shell, command);
|
|
40
|
+
const wrapped = options.sandbox === undefined
|
|
41
|
+
? { command: shell.shell, args }
|
|
42
|
+
: wrapBashCommand(options.sandbox.settings, shell.shell, args, options.sandbox.place);
|
|
43
|
+
return spawn(wrapped.command, wrapped.args, {
|
|
44
|
+
cwd: options.cwd,
|
|
45
|
+
env: options.env,
|
|
46
|
+
detached: options.detached,
|
|
47
|
+
stdio: options.stdio,
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
24
51
|
function fail(message) {
|
|
25
52
|
return { content: message, isError: true };
|
|
26
53
|
}
|
|
@@ -56,9 +83,14 @@ function waitForExit(child) {
|
|
|
56
83
|
});
|
|
57
84
|
}
|
|
58
85
|
export async function executeBash(input, ctx, options = {}) {
|
|
86
|
+
if (input.job !== undefined)
|
|
87
|
+
return jobAction(input, ctx, options);
|
|
88
|
+
if (input.action !== undefined)
|
|
89
|
+
return fail("action needs job (a background job id)");
|
|
59
90
|
if (typeof input.command !== "string" || input.command.trim() === "") {
|
|
60
91
|
return fail("command must be a non-empty string");
|
|
61
92
|
}
|
|
93
|
+
const command = input.command;
|
|
62
94
|
const timeoutMs = input.timeoutMs ?? options.defaultTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS;
|
|
63
95
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_BASH_TIMEOUT_MS) {
|
|
64
96
|
return fail(`timeoutMs must be between 1 and ${MAX_BASH_TIMEOUT_MS}`);
|
|
@@ -83,18 +115,29 @@ export async function executeBash(input, ctx, options = {}) {
|
|
|
83
115
|
depth: ctx.depth,
|
|
84
116
|
});
|
|
85
117
|
const isWindows = (options.processDeps?.platform ?? process.platform) === "win32";
|
|
118
|
+
const sandbox = options.sandbox !== undefined && runsSandboxed(options.sandbox, input)
|
|
119
|
+
? {
|
|
120
|
+
settings: options.sandbox,
|
|
121
|
+
place: {
|
|
122
|
+
workspace: ctx.cwd,
|
|
123
|
+
outputDir: resolveOutputDir(ctx.outputDir),
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
: undefined;
|
|
127
|
+
if (input.background === true)
|
|
128
|
+
return startBackground(command, { cwd, env, shell, isWindows, sandbox }, ctx, options);
|
|
86
129
|
const started = Date.now();
|
|
87
130
|
const acc = new OutputAccumulator({
|
|
88
131
|
spillPath: () => join(resolveOutputDir(ctx.outputDir), safeFileName(`bash-${ctx.toolCallId}.log`)),
|
|
89
132
|
});
|
|
90
133
|
let child;
|
|
91
134
|
try {
|
|
92
|
-
child =
|
|
135
|
+
child = spawnShell(shell, command, {
|
|
93
136
|
cwd,
|
|
94
137
|
env,
|
|
95
138
|
detached: !isWindows,
|
|
96
139
|
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
-
|
|
140
|
+
...(sandbox === undefined ? {} : { sandbox }),
|
|
98
141
|
});
|
|
99
142
|
}
|
|
100
143
|
catch (err) {
|
|
@@ -165,6 +208,7 @@ export async function executeBash(input, ctx, options = {}) {
|
|
|
165
208
|
truncated: out.truncated,
|
|
166
209
|
...(out.fullOutputPath !== undefined ? { full_output_path: out.fullOutputPath } : {}),
|
|
167
210
|
wall_time_seconds: wall,
|
|
211
|
+
...(sandbox === undefined ? {} : { sandboxed: true }),
|
|
168
212
|
};
|
|
169
213
|
const details = {
|
|
170
214
|
...structured,
|
|
@@ -188,6 +232,10 @@ export async function executeBash(input, ctx, options = {}) {
|
|
|
188
232
|
parts.push(`[Command timed out after ${timeoutMs} ms and was killed]`);
|
|
189
233
|
if (exitCode !== 0)
|
|
190
234
|
parts.push(`[exit code: ${exitCode}]`);
|
|
235
|
+
const networkDenied = sandbox?.settings.network === "deny";
|
|
236
|
+
if (sandbox !== undefined && exitCode !== 0 && !aborted && !timedOut)
|
|
237
|
+
if (looksLikeSandboxDenial(out.output, networkDenied))
|
|
238
|
+
parts.push(sandboxDenialHint(networkDenied));
|
|
191
239
|
return {
|
|
192
240
|
content: parts.join("\n\n"),
|
|
193
241
|
isError: exitCode !== 0 || timedOut || aborted,
|
|
@@ -195,22 +243,125 @@ export async function executeBash(input, ctx, options = {}) {
|
|
|
195
243
|
details,
|
|
196
244
|
};
|
|
197
245
|
}
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// [W5-H2] 后台命令
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
function jobsOf(ctx, options) {
|
|
250
|
+
return options.jobs ? options.jobs(ctx.sessionId) : jobsForSession(ctx.sessionId);
|
|
251
|
+
}
|
|
252
|
+
function startBackground(command, spawnInfo, ctx, options) {
|
|
253
|
+
const { cwd, env, shell, isWindows, sandbox } = spawnInfo;
|
|
254
|
+
const outputPath = join(resolveOutputDir(ctx.outputDir), safeFileName(`bash-bg-${ctx.toolCallId}.log`));
|
|
255
|
+
let job;
|
|
256
|
+
try {
|
|
257
|
+
job = jobsOf(ctx, options).start(command, {
|
|
258
|
+
cwd,
|
|
259
|
+
outputPath,
|
|
260
|
+
...(options.processDeps === undefined ? {} : { processDeps: options.processDeps }),
|
|
261
|
+
spawn: (fd) => spawnShell(shell, command, {
|
|
262
|
+
cwd,
|
|
263
|
+
env,
|
|
264
|
+
detached: !isWindows,
|
|
265
|
+
stdio: ["ignore", fd, fd],
|
|
266
|
+
...(sandbox === undefined ? {} : { sandbox }),
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
return fail(`Failed to start shell ${shell.shell}: ${err.message}`);
|
|
272
|
+
}
|
|
273
|
+
const structured = { jobId: job.id, outputPath: job.outputPath };
|
|
274
|
+
if (job.pid !== undefined)
|
|
275
|
+
structured.pid = job.pid;
|
|
276
|
+
if (sandbox !== undefined)
|
|
277
|
+
structured.sandboxed = true;
|
|
278
|
+
return {
|
|
279
|
+
content: `Started background job ${job.id}${job.pid !== undefined ? ` (pid ${job.pid})` : ""}; ` +
|
|
280
|
+
`output: ${job.outputPath}. Check it with bash {"job":"${job.id}","action":"wait"|"output"|"stop"}; ` +
|
|
281
|
+
"you will be told when it exits.",
|
|
282
|
+
structured,
|
|
283
|
+
details: { ...structured, background: true, command, cwd },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function jobStatusLine(job) {
|
|
287
|
+
const state = job.status === "running"
|
|
288
|
+
? "still running"
|
|
289
|
+
: job.status === "stopped"
|
|
290
|
+
? `stopped (exit code ${job.exitCode ?? "?"})`
|
|
291
|
+
: `exited with code ${job.exitCode ?? "?"}`;
|
|
292
|
+
return `[job ${job.id} ${state}; output: ${job.outputPath}]`;
|
|
293
|
+
}
|
|
294
|
+
async function jobAction(input, ctx, options) {
|
|
295
|
+
const id = input.job;
|
|
296
|
+
if (input.command !== undefined || input.background === true || input.sandbox !== undefined)
|
|
297
|
+
return fail("job queries take only job, action and timeoutMs");
|
|
298
|
+
const action = input.action ?? "output";
|
|
299
|
+
const jobs = jobsOf(ctx, options);
|
|
300
|
+
if (jobs.get(id) === undefined) {
|
|
301
|
+
const known = jobs.list().map((job) => job.id);
|
|
302
|
+
return fail(`Unknown background job ${id}${known.length > 0 ? ` (jobs: ${known.join(", ")})` : ""}`);
|
|
303
|
+
}
|
|
304
|
+
if (action === "stop") {
|
|
305
|
+
const job = (await jobs.stop(id));
|
|
306
|
+
const out = jobs.output(id);
|
|
307
|
+
const text = out?.content ?? "";
|
|
308
|
+
return {
|
|
309
|
+
content: `${text === "" ? "(no output)" : text}\n\n${jobStatusLine(job)}`,
|
|
310
|
+
structured: { jobId: id, status: job.status, exit_code: job.exitCode ?? null },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (action === "wait") {
|
|
314
|
+
const waitMs = input.timeoutMs ?? DEFAULT_JOB_WAIT_MS;
|
|
315
|
+
if (!Number.isFinite(waitMs) || waitMs <= 0 || waitMs > MAX_BASH_TIMEOUT_MS)
|
|
316
|
+
return fail(`timeoutMs must be between 1 and ${MAX_BASH_TIMEOUT_MS}`);
|
|
317
|
+
await jobs.wait(id, waitMs, ctx.signal);
|
|
318
|
+
}
|
|
319
|
+
const out = jobs.output(id);
|
|
320
|
+
if (out === undefined)
|
|
321
|
+
return fail(`Unknown background job ${id}`);
|
|
322
|
+
const job = out.job;
|
|
323
|
+
if (job.status !== "running")
|
|
324
|
+
jobs.markSeen(id);
|
|
325
|
+
const parts = [out.content === "" ? "(no output)" : out.content];
|
|
326
|
+
if (out.truncated)
|
|
327
|
+
parts.push(`[Output truncated: showing the last ${out.outputLines} of ${out.totalLines} lines ` +
|
|
328
|
+
`(${formatSize(out.totalBytes)} total). Full output: ${job.outputPath}]`);
|
|
329
|
+
parts.push(jobStatusLine(job));
|
|
330
|
+
return {
|
|
331
|
+
content: parts.join("\n\n"),
|
|
332
|
+
isError: job.status === "exited" && job.exitCode !== 0,
|
|
333
|
+
structured: { jobId: id, status: job.status, exit_code: job.exitCode ?? null },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/** [S2] 沙箱生效时追加到描述末尾的一句(会话内不变)。 */
|
|
337
|
+
export function sandboxDescription(sandbox) {
|
|
338
|
+
if (sandbox?.active !== true)
|
|
339
|
+
return "";
|
|
340
|
+
const net = sandbox.network === "deny" ? ", no network" : "";
|
|
341
|
+
return ` Runs in an OS sandbox (writes only in workspace/temp${net}); sandbox:false needs approval.`;
|
|
342
|
+
}
|
|
198
343
|
export function createBashTool(options = {}) {
|
|
344
|
+
const sandboxed = options.sandbox?.active === true;
|
|
199
345
|
return {
|
|
200
346
|
name: "bash",
|
|
201
347
|
label: "Bash",
|
|
202
|
-
description: "Run a shell command
|
|
203
|
-
|
|
204
|
-
|
|
348
|
+
description: "Run a shell command; stdout+stderr combined, keeps the last 2000 lines / 50 KB (full " +
|
|
349
|
+
`output saved). Timeout ${DEFAULT_BASH_TIMEOUT_MS / 1000} s, max ${MAX_BASH_TIMEOUT_MS / 1000} s. ` +
|
|
350
|
+
"background:true gives a job id for {job, action}." +
|
|
351
|
+
sandboxDescription(options.sandbox),
|
|
205
352
|
parameters: {
|
|
206
353
|
type: "object",
|
|
207
354
|
properties: {
|
|
208
355
|
command: { type: "string" },
|
|
209
356
|
timeoutMs: { type: "integer" },
|
|
210
357
|
cwd: { type: "string", description: "Default: session cwd" },
|
|
211
|
-
description: { type: "string"
|
|
358
|
+
description: { type: "string" },
|
|
359
|
+
background: { type: "boolean" },
|
|
360
|
+
job: { type: "string" },
|
|
361
|
+
action: { type: "string", enum: ["wait", "output", "stop"] },
|
|
362
|
+
// [S2] 只在沙箱生效时出现:沙箱关闭的会话工具 schema 与以前逐字节相同
|
|
363
|
+
...(sandboxed ? { sandbox: { type: "boolean" } } : {}),
|
|
212
364
|
},
|
|
213
|
-
required: ["command"],
|
|
214
365
|
additionalProperties: false,
|
|
215
366
|
},
|
|
216
367
|
permission: "execute",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 剪贴板图片(docs/wave5-plan.md §4、D12)。[W5-I] 界面接线(`Ctrl+V`、`/paste`)归 W5-U。
|
|
3
|
+
*
|
|
4
|
+
* 零依赖读不了剪贴板位图,按平台依次调系统命令,成功写 `<dataDir>/clipboard/<时间戳>.png` 并返回
|
|
5
|
+
* 路径(编辑器插入 `@<路径>`,之后走 loadImageFile);没有工具或剪贴板里没有图返回 undefined:
|
|
6
|
+
*
|
|
7
|
+
* | 平台 | 依次尝试 |
|
|
8
|
+
* | ------- | --------------------------------------------------------------------------- |
|
|
9
|
+
* | macOS | `osascript`(`the clipboard as «class PNGf»` 写文件)、`pngpaste <文件>` |
|
|
10
|
+
* | Linux | Wayland(有 WAYLAND_DISPLAY)`wl-paste -t image/png`;`xclip -selection clipboard -t image/png -o` |
|
|
11
|
+
* | Windows | `powershell` 的 `Get-Clipboard -Format Image` 存 PNG(路径经环境变量传入) |
|
|
12
|
+
*
|
|
13
|
+
* 写出的文件必须以 PNG 文件头开头,否则删掉按「没有图」处理。`ama sessions prune` 清理超过 7 天的
|
|
14
|
+
* 剪贴板文件(`gcClipboardImages`)。
|
|
15
|
+
*/
|
|
16
|
+
import { type RunCommand } from "./image-resize.js";
|
|
17
|
+
export declare const CLIPBOARD_DIR = "clipboard";
|
|
18
|
+
export declare const CLIPBOARD_MAX_AGE_DAYS = 7;
|
|
19
|
+
export interface ClipboardDeps {
|
|
20
|
+
platform?: NodeJS.Platform;
|
|
21
|
+
run?: RunCommand;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
now?(): number;
|
|
24
|
+
}
|
|
25
|
+
export type ClipboardResult = {
|
|
26
|
+
ok: true;
|
|
27
|
+
path: string;
|
|
28
|
+
tool: string;
|
|
29
|
+
}
|
|
30
|
+
/** `no_tool`:一个可用命令都没有;`no_image`:命令能用但剪贴板里没有图。 */
|
|
31
|
+
| {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: "no_tool" | "no_image";
|
|
34
|
+
};
|
|
35
|
+
/** 读剪贴板图片到数据目录;返回结果与原因(界面据此给一行提示)。 */
|
|
36
|
+
export declare function readClipboardImage(dataDir: string, deps?: ClipboardDeps): Promise<ClipboardResult>;
|
|
37
|
+
/** §10.2 的签名:成功返回 PNG 路径,没有工具或没有图返回 undefined。 */
|
|
38
|
+
export declare function pasteClipboardImage(dataDir: string, deps?: ClipboardDeps): Promise<string | undefined>;
|
|
39
|
+
/** 清理 `<dataDir>/clipboard/` 里超过 `maxAgeDays`(缺省 7)天的文件;返回(将)删除的路径。 */
|
|
40
|
+
export declare function gcClipboardImages(dataDir: string, options?: {
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
maxAgeDays?: number;
|
|
43
|
+
now?: number;
|
|
44
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 剪贴板图片(docs/wave5-plan.md §4、D12)。[W5-I] 界面接线(`Ctrl+V`、`/paste`)归 W5-U。
|
|
3
|
+
*
|
|
4
|
+
* 零依赖读不了剪贴板位图,按平台依次调系统命令,成功写 `<dataDir>/clipboard/<时间戳>.png` 并返回
|
|
5
|
+
* 路径(编辑器插入 `@<路径>`,之后走 loadImageFile);没有工具或剪贴板里没有图返回 undefined:
|
|
6
|
+
*
|
|
7
|
+
* | 平台 | 依次尝试 |
|
|
8
|
+
* | ------- | --------------------------------------------------------------------------- |
|
|
9
|
+
* | macOS | `osascript`(`the clipboard as «class PNGf»` 写文件)、`pngpaste <文件>` |
|
|
10
|
+
* | Linux | Wayland(有 WAYLAND_DISPLAY)`wl-paste -t image/png`;`xclip -selection clipboard -t image/png -o` |
|
|
11
|
+
* | Windows | `powershell` 的 `Get-Clipboard -Format Image` 存 PNG(路径经环境变量传入) |
|
|
12
|
+
*
|
|
13
|
+
* 写出的文件必须以 PNG 文件头开头,否则删掉按「没有图」处理。`ama sessions prune` 清理超过 7 天的
|
|
14
|
+
* 剪贴板文件(`gcClipboardImages`)。
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { mkdir, readdir, rm, stat, writeFile, readFile } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { sniffImageMime } from "./image-file.js";
|
|
20
|
+
import { runCommand } from "./image-resize.js";
|
|
21
|
+
export const CLIPBOARD_DIR = "clipboard";
|
|
22
|
+
export const CLIPBOARD_MAX_AGE_DAYS = 7;
|
|
23
|
+
const OSASCRIPT = [
|
|
24
|
+
"on run argv",
|
|
25
|
+
"try",
|
|
26
|
+
"set png to the clipboard as «class PNGf»",
|
|
27
|
+
"on error",
|
|
28
|
+
'return "none"',
|
|
29
|
+
"end try",
|
|
30
|
+
"set f to open for access (POSIX file (item 1 of argv)) with write permission",
|
|
31
|
+
"set eof f to 0",
|
|
32
|
+
"write png to f",
|
|
33
|
+
"close access f",
|
|
34
|
+
'return "ok"',
|
|
35
|
+
"end run",
|
|
36
|
+
];
|
|
37
|
+
const POWERSHELL = "$img = Get-Clipboard -Format Image; " +
|
|
38
|
+
"if ($null -eq $img) { exit 3 }; " +
|
|
39
|
+
"$img.Save($env:AMA_CLIPBOARD_OUT, [System.Drawing.Imaging.ImageFormat]::Png)";
|
|
40
|
+
function attempts(platform, file, env) {
|
|
41
|
+
if (platform === "darwin") {
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
tool: "osascript",
|
|
45
|
+
command: "osascript",
|
|
46
|
+
args: [...OSASCRIPT.flatMap((line) => ["-e", line]), file],
|
|
47
|
+
via: "file",
|
|
48
|
+
},
|
|
49
|
+
{ tool: "pngpaste", command: "pngpaste", args: [file], via: "file" },
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
if (platform === "win32") {
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
tool: "powershell",
|
|
56
|
+
command: "powershell",
|
|
57
|
+
args: ["-NoProfile", "-NonInteractive", "-STA", "-Command", POWERSHELL],
|
|
58
|
+
via: "file",
|
|
59
|
+
env: { ...env, AMA_CLIPBOARD_OUT: file },
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
const list = [];
|
|
64
|
+
if (env["WAYLAND_DISPLAY"]) {
|
|
65
|
+
list.push({ tool: "wl-paste", command: "wl-paste", args: ["-t", "image/png"], via: "stdout" });
|
|
66
|
+
}
|
|
67
|
+
list.push({
|
|
68
|
+
tool: "xclip",
|
|
69
|
+
command: "xclip",
|
|
70
|
+
args: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
71
|
+
via: "stdout",
|
|
72
|
+
});
|
|
73
|
+
return list;
|
|
74
|
+
}
|
|
75
|
+
async function isPng(file) {
|
|
76
|
+
try {
|
|
77
|
+
return sniffImageMime(await readFile(file)) === "image/png";
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** 读剪贴板图片到数据目录;返回结果与原因(界面据此给一行提示)。 */
|
|
84
|
+
export async function readClipboardImage(dataDir, deps = {}) {
|
|
85
|
+
const platform = deps.platform ?? process.platform;
|
|
86
|
+
const env = deps.env ?? process.env;
|
|
87
|
+
const run = deps.run ?? runCommand;
|
|
88
|
+
const dir = join(dataDir, CLIPBOARD_DIR);
|
|
89
|
+
await mkdir(dir, { recursive: true });
|
|
90
|
+
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[:.]/g, "-");
|
|
91
|
+
// 同一毫秒再贴:加序号,不能把上一张当成这次的结果
|
|
92
|
+
let file = join(dir, `${stamp}.png`);
|
|
93
|
+
for (let n = 1; existsSync(file); n++)
|
|
94
|
+
file = join(dir, `${stamp}-${n}.png`);
|
|
95
|
+
let anyTool = false;
|
|
96
|
+
for (const attempt of attempts(platform, file, env)) {
|
|
97
|
+
const options = attempt.via === "file" && attempt.env !== undefined ? { env: attempt.env } : {};
|
|
98
|
+
const result = await run(attempt.command, attempt.args, options);
|
|
99
|
+
if (result.missing === true)
|
|
100
|
+
continue;
|
|
101
|
+
anyTool = true;
|
|
102
|
+
if (result.code !== 0)
|
|
103
|
+
continue;
|
|
104
|
+
if (attempt.via === "stdout") {
|
|
105
|
+
if (sniffImageMime(result.stdout) !== "image/png")
|
|
106
|
+
continue;
|
|
107
|
+
await writeFile(file, result.stdout);
|
|
108
|
+
}
|
|
109
|
+
if (await isPng(file))
|
|
110
|
+
return { ok: true, path: file, tool: attempt.tool };
|
|
111
|
+
await rm(file, { force: true });
|
|
112
|
+
}
|
|
113
|
+
await rm(file, { force: true });
|
|
114
|
+
return { ok: false, reason: anyTool ? "no_image" : "no_tool" };
|
|
115
|
+
}
|
|
116
|
+
/** §10.2 的签名:成功返回 PNG 路径,没有工具或没有图返回 undefined。 */
|
|
117
|
+
export async function pasteClipboardImage(dataDir, deps = {}) {
|
|
118
|
+
const result = await readClipboardImage(dataDir, deps);
|
|
119
|
+
return result.ok ? result.path : undefined;
|
|
120
|
+
}
|
|
121
|
+
/** 清理 `<dataDir>/clipboard/` 里超过 `maxAgeDays`(缺省 7)天的文件;返回(将)删除的路径。 */
|
|
122
|
+
export async function gcClipboardImages(dataDir, options = {}) {
|
|
123
|
+
const dir = join(dataDir, CLIPBOARD_DIR);
|
|
124
|
+
let names;
|
|
125
|
+
try {
|
|
126
|
+
names = await readdir(dir);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const cutoff = (options.now ?? Date.now()) - (options.maxAgeDays ?? CLIPBOARD_MAX_AGE_DAYS) * 86_400_000;
|
|
132
|
+
const removed = [];
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
const file = join(dir, name);
|
|
135
|
+
try {
|
|
136
|
+
const info = await stat(file);
|
|
137
|
+
if (!info.isFile() || info.mtimeMs >= cutoff)
|
|
138
|
+
continue;
|
|
139
|
+
if (options.dryRun !== true)
|
|
140
|
+
await rm(file, { force: true });
|
|
141
|
+
removed.push(file);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// 并发删除等:跳过
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return removed;
|
|
148
|
+
}
|
package/dist/tools/edit.d.ts
CHANGED
|
@@ -34,5 +34,7 @@ export interface EditPlan {
|
|
|
34
34
|
export declare function planEdits(content: string, edits: readonly EditOp[], replaceAll?: boolean): EditPlan;
|
|
35
35
|
/** 统一 diff(LF 文本,3 行上下文);无差异返回空串。 */
|
|
36
36
|
export declare function unifiedDiff(oldText: string, newText: string, path: string, context?: number): string;
|
|
37
|
+
/** 检查点:写前备份(docs/rewind-plan.md §2);钩子按约定不抛,这里再兜一层,不让编辑失败。 */
|
|
38
|
+
export declare function beforeWrite(ctx: ToolContext, abs: string): Promise<void>;
|
|
37
39
|
export declare function executeEdit(input: EditInput, ctx: ToolContext): Promise<ToolResult>;
|
|
38
40
|
export declare function createEditTool(): ToolDefinition<EditInput>;
|
package/dist/tools/edit.js
CHANGED
|
@@ -164,6 +164,17 @@ function firstChangedLine(oldText, newText) {
|
|
|
164
164
|
// ---------------------------------------------------------------------------
|
|
165
165
|
// 工具
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
|
+
/** 检查点:写前备份(docs/rewind-plan.md §2);钩子按约定不抛,这里再兜一层,不让编辑失败。 */
|
|
168
|
+
export async function beforeWrite(ctx, abs) {
|
|
169
|
+
if (ctx.checkpoint === undefined)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
await ctx.checkpoint.beforeWrite(abs);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
ctx.log("warn", `checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
167
178
|
export async function executeEdit(input, ctx) {
|
|
168
179
|
const abs = resolvePath(input.path, ctx.cwd);
|
|
169
180
|
const shown = displayPath(abs, ctx.cwd);
|
|
@@ -192,7 +203,10 @@ export async function executeEdit(input, ctx) {
|
|
|
192
203
|
return { content: `${shown}: ${err.message}`, isError: true };
|
|
193
204
|
throw err;
|
|
194
205
|
}
|
|
195
|
-
|
|
206
|
+
const output = bom + restoreLineEndings(plan.result, ending);
|
|
207
|
+
await beforeWrite(ctx, abs);
|
|
208
|
+
await writeFile(abs, output, "utf8");
|
|
209
|
+
ctx.checkpoint?.afterWrite(abs, output);
|
|
196
210
|
const details = {
|
|
197
211
|
path: abs,
|
|
198
212
|
diff: unifiedDiff(content, plan.result, shown),
|
|
@@ -209,8 +223,9 @@ export function createEditTool() {
|
|
|
209
223
|
return {
|
|
210
224
|
name: "edit",
|
|
211
225
|
label: "Edit",
|
|
212
|
-
description: "Replace text in a file (read it first).
|
|
213
|
-
"(unless replaceAll)
|
|
226
|
+
description: "Replace text in a file (read it first). Give all changes to a file as edits[] in one call; " +
|
|
227
|
+
"each oldText matches the original file and must be unique (unless replaceAll), short and " +
|
|
228
|
+
"non-overlapping.",
|
|
214
229
|
parameters: {
|
|
215
230
|
type: "object",
|
|
216
231
|
properties: {
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
|
|
3
3
|
*
|
|
4
4
|
* - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
|
|
5
|
-
* -
|
|
5
|
+
* - [W5-I] 单张上限按 **base64 后**计算,按端点分档(ai/image-limits.ts,缺省 5 MB);任一边
|
|
6
|
+
* > 8000 px 拒绝;超限时按 `images.resize`(缺省 auto)用系统工具缩放(image-resize.ts),
|
|
7
|
+
* 没有工具则拒绝并提示;
|
|
6
8
|
* - 不解码像素,只读文件头取尺寸。
|
|
7
9
|
*/
|
|
8
10
|
import type { ImageBlock } from "../ai/types.js";
|
|
9
|
-
|
|
11
|
+
import { type ResizeDeps } from "./image-resize.js";
|
|
12
|
+
/** 读进内存前的硬上限(原始字节):再大的文件缩放也不现实,直接拒绝。 */
|
|
13
|
+
export declare const MAX_IMAGE_FILE_BYTES: number;
|
|
10
14
|
export declare const IMAGE_EXTENSIONS: Readonly<Record<string, string>>;
|
|
11
15
|
/** 按扩展名(不分大小写);不是图片返回 undefined。 */
|
|
12
16
|
export declare function imageMimeFromPath(path: string): string | undefined;
|
|
@@ -22,10 +26,41 @@ export interface LoadedImage {
|
|
|
22
26
|
block: ImageBlock;
|
|
23
27
|
path: string;
|
|
24
28
|
mimeType: string;
|
|
29
|
+
/** 实际附上的字节数(缩放后为缩放结果的大小)。 */
|
|
25
30
|
bytes: number;
|
|
26
31
|
size?: ImageSize;
|
|
27
32
|
}
|
|
33
|
+
export interface ImageFitOptions {
|
|
34
|
+
/** 单张 base64 后的上限(字节);缺省 5 MB(未知端点 / 中转)。 */
|
|
35
|
+
maxBase64Bytes?: number;
|
|
36
|
+
/** 超限时缩放:`auto`(缺省)找 sips / magick,`off` 从不缩放。 */
|
|
37
|
+
resize?: "auto" | "off";
|
|
38
|
+
/** 测试注入(平台、命令执行器)。 */
|
|
39
|
+
resizeDeps?: ResizeDeps;
|
|
40
|
+
}
|
|
41
|
+
export type ImageFit = {
|
|
42
|
+
ok: true;
|
|
43
|
+
buf: Buffer;
|
|
44
|
+
mimeType: string;
|
|
45
|
+
size?: ImageSize;
|
|
46
|
+
/** 缩放过:用的工具与原尺寸。 */
|
|
47
|
+
resized?: {
|
|
48
|
+
tool: string;
|
|
49
|
+
from?: ImageSize;
|
|
50
|
+
};
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
mimeType: string;
|
|
54
|
+
size?: ImageSize;
|
|
55
|
+
reason: "too_large" | "too_wide";
|
|
56
|
+
/** 给模型 / 日志的英文说明(read 工具原样放进结果)。 */
|
|
57
|
+
note: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* 检查一张图是否在上限内(base64 后大小、边长);超限且给了 `path`、`resize` 不是 off 时先缩放再查。
|
|
61
|
+
*/
|
|
62
|
+
export declare function fitImage(buf: Buffer, mimeType: string, options?: ImageFitOptions, path?: string): Promise<ImageFit>;
|
|
28
63
|
/**
|
|
29
64
|
* 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
|
|
30
65
|
*/
|
|
31
|
-
export declare function loadImageFile(path: string): Promise<LoadedImage>;
|
|
66
|
+
export declare function loadImageFile(path: string, options?: ImageFitOptions): Promise<LoadedImage>;
|
package/dist/tools/image-file.js
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
|
|
3
3
|
*
|
|
4
4
|
* - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
|
|
5
|
-
* -
|
|
5
|
+
* - [W5-I] 单张上限按 **base64 后**计算,按端点分档(ai/image-limits.ts,缺省 5 MB);任一边
|
|
6
|
+
* > 8000 px 拒绝;超限时按 `images.resize`(缺省 auto)用系统工具缩放(image-resize.ts),
|
|
7
|
+
* 没有工具则拒绝并提示;
|
|
6
8
|
* - 不解码像素,只读文件头取尺寸。
|
|
7
9
|
*/
|
|
8
10
|
import { readFile, stat } from "node:fs/promises";
|
|
9
11
|
import { extname } from "node:path";
|
|
10
12
|
import { AmaError } from "../errors.js";
|
|
11
|
-
|
|
13
|
+
import { DEFAULT_IMAGE_BASE64_LIMIT, MAX_IMAGE_EDGE, base64Size, formatMb, } from "../ai/image-limits.js";
|
|
14
|
+
import { resizeImage } from "./image-resize.js";
|
|
15
|
+
/** 读进内存前的硬上限(原始字节):再大的文件缩放也不现实,直接拒绝。 */
|
|
16
|
+
export const MAX_IMAGE_FILE_BYTES = 64 * 1024 * 1024;
|
|
12
17
|
export const IMAGE_EXTENSIONS = {
|
|
13
18
|
".png": "image/png",
|
|
14
19
|
".jpg": "image/jpeg",
|
|
@@ -84,10 +89,73 @@ function webpSize(buf) {
|
|
|
84
89
|
}
|
|
85
90
|
return undefined;
|
|
86
91
|
}
|
|
92
|
+
function longEdge(size) {
|
|
93
|
+
return size === undefined ? 0 : Math.max(size.width, size.height);
|
|
94
|
+
}
|
|
95
|
+
function checkFit(buf, mimeType, limit) {
|
|
96
|
+
const size = imageSize(buf, mimeType);
|
|
97
|
+
const sized = size !== undefined ? { size } : {};
|
|
98
|
+
if (longEdge(size) > MAX_IMAGE_EDGE) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
mimeType,
|
|
102
|
+
...sized,
|
|
103
|
+
reason: "too_wide",
|
|
104
|
+
note: `Image is larger than ${MAX_IMAGE_EDGE}px on a side; not attached.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (base64Size(buf.length) > limit) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
mimeType,
|
|
111
|
+
...sized,
|
|
112
|
+
reason: "too_large",
|
|
113
|
+
note: `Image exceeds the ${formatMb(limit)} attachment limit (base64); not attached.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, buf, mimeType, ...sized };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 检查一张图是否在上限内(base64 后大小、边长);超限且给了 `path`、`resize` 不是 off 时先缩放再查。
|
|
120
|
+
*/
|
|
121
|
+
export async function fitImage(buf, mimeType, options = {}, path) {
|
|
122
|
+
const limit = options.maxBase64Bytes ?? DEFAULT_IMAGE_BASE64_LIMIT;
|
|
123
|
+
const fit = checkFit(buf, mimeType, limit);
|
|
124
|
+
if (fit.ok || path === undefined || options.resize === "off")
|
|
125
|
+
return fit;
|
|
126
|
+
const resized = await resizeImage(path, { maxEdge: MAX_IMAGE_EDGE, maxBase64Bytes: limit, mimeType, size: fit.size }, options.resizeDeps);
|
|
127
|
+
if (!resized.ok) {
|
|
128
|
+
const hint = resized.reason === "no_tool"
|
|
129
|
+
? " (no resize tool found: install ImageMagick, or sips on macOS)"
|
|
130
|
+
: " (resizing did not bring it under the limit)";
|
|
131
|
+
return { ...fit, note: fit.note.replace(/; not attached\.$/, `${hint}; not attached.`) };
|
|
132
|
+
}
|
|
133
|
+
const again = checkFit(resized.buf, resized.mimeType, limit);
|
|
134
|
+
if (!again.ok)
|
|
135
|
+
return fit;
|
|
136
|
+
return {
|
|
137
|
+
...again,
|
|
138
|
+
resized: { tool: resized.tool, ...(fit.size !== undefined ? { from: fit.size } : {}) },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function fitError(path, fit, limit, bytes, resizeOff) {
|
|
142
|
+
const hint = resizeOff
|
|
143
|
+
? "(images.resize 为 off)"
|
|
144
|
+
: fit.note.includes("no resize tool")
|
|
145
|
+
? "(没找到缩放工具:装 ImageMagick,macOS 自带 sips)"
|
|
146
|
+
: fit.note.includes("resizing did not")
|
|
147
|
+
? "(缩放后仍超限)"
|
|
148
|
+
: "";
|
|
149
|
+
if (fit.reason === "too_wide") {
|
|
150
|
+
const dims = fit.size !== undefined ? `(${fit.size.width}×${fit.size.height})` : "";
|
|
151
|
+
return new AmaError("invalid_arguments", `图片任一边超过 ${MAX_IMAGE_EDGE} px:${path}${dims}${hint}`);
|
|
152
|
+
}
|
|
153
|
+
return new AmaError("invalid_arguments", `图片超过 ${formatMb(limit)} 上限(按 base64 后计算):${path}(${formatMb(base64Size(bytes))})${hint}`);
|
|
154
|
+
}
|
|
87
155
|
/**
|
|
88
156
|
* 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
|
|
89
157
|
*/
|
|
90
|
-
export async function loadImageFile(path) {
|
|
158
|
+
export async function loadImageFile(path, options = {}) {
|
|
91
159
|
let info;
|
|
92
160
|
try {
|
|
93
161
|
info = await stat(path);
|
|
@@ -97,18 +165,21 @@ export async function loadImageFile(path) {
|
|
|
97
165
|
}
|
|
98
166
|
if (!info.isFile())
|
|
99
167
|
throw new AmaError("invalid_arguments", `不是文件:${path}`);
|
|
100
|
-
|
|
101
|
-
|
|
168
|
+
const limit = options.maxBase64Bytes ?? DEFAULT_IMAGE_BASE64_LIMIT;
|
|
169
|
+
if (info.size > MAX_IMAGE_FILE_BYTES)
|
|
170
|
+
throw new AmaError("invalid_arguments", `图片超过 ${formatMb(limit)} 上限(按 base64 后计算):${path}(${formatMb(base64Size(info.size))})`);
|
|
102
171
|
const buf = await readFile(path);
|
|
103
172
|
const mimeType = sniffImageMime(buf) ?? imageMimeFromPath(path);
|
|
104
173
|
if (mimeType === undefined)
|
|
105
174
|
throw new AmaError("invalid_arguments", `不是支持的图片(PNG / JPEG / GIF / WebP):${path}`);
|
|
106
|
-
const
|
|
175
|
+
const fit = await fitImage(buf, mimeType, options, path);
|
|
176
|
+
if (!fit.ok)
|
|
177
|
+
throw fitError(path, fit, limit, buf.length, options.resize === "off");
|
|
107
178
|
return {
|
|
108
|
-
block: { type: "image", data: buf.toString("base64"), mimeType },
|
|
179
|
+
block: { type: "image", data: fit.buf.toString("base64"), mimeType: fit.mimeType },
|
|
109
180
|
path,
|
|
110
|
-
mimeType,
|
|
111
|
-
bytes: buf.length,
|
|
112
|
-
...(size ? { size } : {}),
|
|
181
|
+
mimeType: fit.mimeType,
|
|
182
|
+
bytes: fit.buf.length,
|
|
183
|
+
...(fit.size ? { size: fit.size } : {}),
|
|
113
184
|
};
|
|
114
185
|
}
|