@gajae-code/coding-agent 0.18.12 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
|
@@ -2509,7 +2509,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2509
2509
|
"irc": {
|
|
2510
2510
|
"name": "irc",
|
|
2511
2511
|
"label": "IRC",
|
|
2512
|
-
"description": "Sends short text messages to other live agents in this process and receives their prose replies.\n\n<instruction>\n- The main agent is addressable as `0-Main`. Subagents reuse their task id (e.g. `0-AuthLoader`).\n- `op: \"list\"` returns the current set of visible peers. Use it before sending if you are not sure who is live.\n- `op: \"send\"` delivers `message` to `to`. `to` may be a specific id or `\"all\"` to broadcast.\n- `awaitReply` (optional): wait for a prose reply. Defaults to `true` for DMs and `false` for `to: \"all\"` broadcasts.\n- The recipient generates the reply via an ephemeral side-channel turn that uses their current model, system prompt, and history — it does **not** wait for the recipient's main loop to be free, so it is safe to IRC an agent that is currently inside a long-running tool call.\n- The exchange (incoming question + auto-reply) is
|
|
2512
|
+
"description": "Sends short text messages to other live agents in this process and receives their prose replies.\n\n<instruction>\n- The main agent is addressable as `0-Main`. Subagents reuse their task id (e.g. `0-AuthLoader`).\n- `op: \"list\"` returns the current set of visible peers. Use it before sending if you are not sure who is live.\n- `op: \"send\"` delivers `message` to `to`. `to` may be a specific id or `\"all\"` to broadcast.\n- `awaitReply` (optional): wait for a prose reply. Defaults to `true` for DMs and `false` for `to: \"all\"` broadcasts.\n- The recipient generates the reply via an ephemeral side-channel turn that uses their current model, system prompt, and history — it does **not** wait for the recipient's main loop to be free, so it is safe to IRC an agent that is currently inside a long-running tool call.\n- The exchange (incoming question + auto-reply) is injected into the recipient's persisted history as soon as it can be delivered: an idle recipient gets it immediately, and a recipient that is already streaming receives it at its next step boundary instead of at the end of its turn. The recipient can then follow up if needed.\n</instruction>\n\n<conditions>\nYou SHOULD reach for `irc` proactively when continuing alone is wasteful or wrong. When in doubt, prefer messaging.\n- **Unexpected state.** You hit something the original task did not describe — a missing file, a config that contradicts the assignment, an API behaving differently than you were told, a tool failing in a way that suggests the spec is wrong. DM `0-Main` (or the spawning agent) for guidance instead of guessing.\n- **Blocked by another agent.** A peer holds the file/branch/resource you need, has already started the change you are about to make, or owns a decision you depend on. DM that peer (or broadcast to discover who) before duplicating or stepping on work.\n- **Decision points outside your scope.** A genuine fork in the road that the assignment did not pre-decide (e.g. which of two viable APIs to use, whether to refactor adjacent code). Ask the requester rather than picking unilaterally.\n- **Coordination opportunities.** Before editing a shared file or relying on another agent's in-flight API, message the relevant peer; proactively share state that affects their work.\n\nDo **not** use `irc` for: routine progress updates, things you can verify with a tool call, or questions whose answer is already in your assignment / repo / docs.\n</conditions>\n\n<etiquette>\nThese rules apply to both sending and replying.\n- **Plain prose only.** Do not send structured JSON status payloads (e.g. `{\"type\":\"task_completed\",…}`). Write a normal sentence: \"Done with the auth refactor — left a TODO in `src/server/auth.ts` for the rate limiter.\"\n- **Do not quote the message you are replying to.** The sender already saw it; the TUI already renders it. Lead with the answer.\n- **Use IRC, not terminal tools, to learn about peers.** Do not `search` artifacts, read other sessions' JSONL files, or shell-poke around to figure out what another agent is doing. DM them — they have the live answer and you do not.\n- **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. Do not follow up with \"did you get my message?\" — they did. If `delivered` is empty or the result was `failed`, the peer is unavailable; move on or report the blocker, do not retry in a loop.\n- **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `artifact://` URLs instead of pasting blobs.\n- **Address peers by id.** Use the exact id from `op: \"list\"` (e.g. `0-AuthLoader`, `0-Main`). Do not invent friendly names.\n- **Do not IRC for things a tool would answer.** If a `read`, `search`, or build command would resolve the question, do that first.\n- **When you receive an IRC message, answer it before continuing.** The recipient injects the question + your auto-reply into your history; address it directly, do not repeat it back to the user.\n</etiquette>\n\n<output>\n- `send`: returns each recipient that received the message and any prose replies that arrived.\n- `list`: returns peers and channels visible to the caller.\n</output>\n\n<examples>\n# List peers\n`{\"op\": \"list\"}`\n# Direct message to the main agent (waits for prose reply)\n`{\"op\": \"send\", \"to\": \"0-Main\", \"message\": \"Should I prefer JWT or session cookies for the auth flow?\"}`\n# Unexpected state — ask the originator\n`{\"op\": \"send\", \"to\": \"0-Main\", \"message\": \"Assignment says edit src/auth/jwt.ts but the file does not exist. Is the new path src/server/auth/jwt.ts?\"}`\n# Blocked by a peer — ask them directly\n`{\"op\": \"send\", \"to\": \"0-AuthLoader\", \"message\": \"Are you still touching src/server/auth.ts? I need to add a 401 path; OK to proceed or should I wait?\"}`\n# Broadcast to discover who owns something (no replies, just informs them)\n`{\"op\": \"send\", \"to\": \"all\", \"message\": \"About to refactor src/server/middleware/*. Anyone already in there?\", \"awaitReply\": false}`\n</examples>",
|
|
2513
2513
|
"parameters": {
|
|
2514
2514
|
"type": "object",
|
|
2515
2515
|
"properties": {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@gajae-code/agent-core";
|
|
2
|
+
import { resolveRalplanCurrentFinal, selectsRalplanApprovalGate } from "../gjc-runtime/ralplan-current-final";
|
|
2
3
|
import {
|
|
3
4
|
consumeUltragoalAskNudge,
|
|
4
5
|
isUltragoalAskBlocked,
|
|
@@ -33,6 +34,7 @@ export async function assertUltragoalAskAllowed(
|
|
|
33
34
|
cwd: string,
|
|
34
35
|
context: UltragoalAskGuardContext = {},
|
|
35
36
|
agentDir?: string,
|
|
37
|
+
pendingQuestions?: unknown,
|
|
36
38
|
): Promise<void> {
|
|
37
39
|
// Durable Ultragoal state is session-scoped, so only the caller's own run may
|
|
38
40
|
// block `ask`. An anonymous caller has no run attributable to it, so the guard
|
|
@@ -43,6 +45,16 @@ export async function assertUltragoalAskAllowed(
|
|
|
43
45
|
if (!sessionId) return;
|
|
44
46
|
const diagnostic = await isUltragoalAskBlocked(cwd, { sessionId });
|
|
45
47
|
if (!diagnostic.active) return;
|
|
48
|
+
// The one ask that may proceed while a goal is blocked is the execution
|
|
49
|
+
// approval for an authenticated current ralplan final (issue #6578). The
|
|
50
|
+
// request must select that gate and the final must validate on disk; a
|
|
51
|
+
// caller-declared tag alone never lifts the guard. AskTool still captures and
|
|
52
|
+
// authenticates the publication it records consent against.
|
|
53
|
+
if (selectsRalplanApprovalGate(pendingQuestions)) {
|
|
54
|
+
const resolution = await resolveRalplanCurrentFinal({ cwd, sessionId, agentDir });
|
|
55
|
+
if (resolution.ok) return;
|
|
56
|
+
throw new ToolError(`Ultragoal ask guard refused the ralplan execution approval: ${resolution.refusal.reason}`);
|
|
57
|
+
}
|
|
46
58
|
const nudge = await consumeUltragoalAskNudge(cwd, sessionId, agentDir);
|
|
47
59
|
if (nudge.nudged) throw new ToolError(nudge.message);
|
|
48
60
|
throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
|
|
@@ -66,7 +78,7 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(
|
|
|
66
78
|
// budget against the SESSION profile, never the process-global one.
|
|
67
79
|
// The caller session id is the only binding, so a foreign session's
|
|
68
80
|
// durable run can never gate this ask.
|
|
69
|
-
await assertUltragoalAskAllowed(getCwd(), getContext(), getSessionAgentDir());
|
|
81
|
+
await assertUltragoalAskAllowed(getCwd(), getContext(), getSessionAgentDir(), args[1]);
|
|
70
82
|
return Reflect.apply(target.execute, target, args);
|
|
71
83
|
};
|
|
72
84
|
},
|
package/src/tools/write.ts
CHANGED
|
@@ -218,6 +218,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
218
218
|
readonly loadMode = "discoverable";
|
|
219
219
|
readonly summary = "Write content to a file (creates or overwrites)";
|
|
220
220
|
|
|
221
|
+
/** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
|
|
222
|
+
matcherDigest(args: unknown): string | undefined {
|
|
223
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
224
|
+
const content = (args as Partial<WriteParams>).content;
|
|
225
|
+
return typeof content === "string" ? content : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
221
228
|
readonly #writethrough: WritethroughCallback;
|
|
222
229
|
|
|
223
230
|
constructor(private readonly session: ToolSession) {
|
package/src/tui/tree-list.ts
CHANGED
|
@@ -3,11 +3,25 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { replaceTabs } from "@gajae-code/tui";
|
|
6
|
+
import { sanitizeText } from "@gajae-code/utils";
|
|
6
7
|
import type { Theme } from "../modes/theme/theme";
|
|
7
8
|
import { formatMoreItems } from "../tools/render-utils";
|
|
8
9
|
import type { TreeContext } from "./types";
|
|
9
10
|
import { getTreeBranch, getTreeContinuePrefix } from "./utils";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Tool renderers pass raw match lines or line groups into `renderItem`, which
|
|
14
|
+
* then applies theme colors. Strip controls first so OSC bytes are not wrapped
|
|
15
|
+
* into the styled string. Objects (file entries, todos) are left unchanged.
|
|
16
|
+
*/
|
|
17
|
+
function sanitizeTreeItem<T>(item: T): T {
|
|
18
|
+
if (typeof item === "string") return sanitizeText(item) as T;
|
|
19
|
+
if (Array.isArray(item)) {
|
|
20
|
+
return item.map(part => (typeof part === "string" ? sanitizeText(part) : part)) as T;
|
|
21
|
+
}
|
|
22
|
+
return item;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export interface TreeListOptions<T> {
|
|
12
26
|
items: T[];
|
|
13
27
|
expanded?: boolean;
|
|
@@ -32,7 +46,7 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
|
|
|
32
46
|
// renderItem implementations MUST NOT vary line count based on isLast.
|
|
33
47
|
const preRendered: string[][] = [];
|
|
34
48
|
for (let i = 0; i < maxItems; i++) {
|
|
35
|
-
const rendered = renderItem(items[i], {
|
|
49
|
+
const rendered = renderItem(sanitizeTreeItem(items[i]!), {
|
|
36
50
|
index: i,
|
|
37
51
|
isLast: false,
|
|
38
52
|
depth: 0,
|
|
@@ -44,8 +44,9 @@ function redactCommitMessageText(text: string): string {
|
|
|
44
44
|
function getModelCandidates(
|
|
45
45
|
registry: ModelRegistry,
|
|
46
46
|
settings: Settings,
|
|
47
|
+
credentialSessionId?: string,
|
|
47
48
|
): Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }> {
|
|
48
|
-
const availableModels = registry.getAvailable();
|
|
49
|
+
const availableModels = registry.getAvailable(credentialSessionId);
|
|
49
50
|
if (availableModels.length === 0) return [];
|
|
50
51
|
|
|
51
52
|
const candidates: Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }> = [];
|
|
@@ -80,7 +81,7 @@ export async function generateCommitMessage(
|
|
|
80
81
|
settings: Settings,
|
|
81
82
|
sessionId?: string,
|
|
82
83
|
): Promise<string | null> {
|
|
83
|
-
const candidates = getModelCandidates(registry, settings);
|
|
84
|
+
const candidates = getModelCandidates(registry, settings, sessionId);
|
|
84
85
|
if (candidates.length === 0) {
|
|
85
86
|
logger.debug("commit-msg-generator: no model found");
|
|
86
87
|
return null;
|
|
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { $pickCredentialEnv
|
|
9
|
+
import { $pickCredentialEnv } from "@gajae-code/utils";
|
|
10
10
|
|
|
11
11
|
/** Returns the user's preferred editor command, or undefined if not configured. */
|
|
12
12
|
export function getEditorCommand(): string | undefined {
|
|
@@ -119,10 +119,22 @@ export async function openInEditor(
|
|
|
119
119
|
options?: OpenInEditorOptions,
|
|
120
120
|
): Promise<string | null> {
|
|
121
121
|
const ext = options?.extension ?? ".md";
|
|
122
|
-
|
|
122
|
+
// Random directory plus an exclusive create: a preexisting symlink under the
|
|
123
|
+
// temp directory is not opened, and the draft is not left world-readable.
|
|
124
|
+
let tempDir: string | undefined;
|
|
125
|
+
let tmpFile = "";
|
|
123
126
|
|
|
124
127
|
try {
|
|
125
|
-
await
|
|
128
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-editor-"));
|
|
129
|
+
if (process.platform !== "win32") await fs.chmod(tempDir, 0o700);
|
|
130
|
+
tmpFile = path.join(tempDir, `draft${ext}`);
|
|
131
|
+
const handle = await fs.open(tmpFile, "wx", 0o600);
|
|
132
|
+
try {
|
|
133
|
+
await handle.writeFile(content);
|
|
134
|
+
} finally {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
if (process.platform !== "win32") await fs.chmod(tmpFile, 0o600);
|
|
126
138
|
|
|
127
139
|
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
128
140
|
const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
|
|
@@ -147,10 +159,12 @@ export async function openInEditor(
|
|
|
147
159
|
}
|
|
148
160
|
return null;
|
|
149
161
|
} finally {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
if (tempDir) {
|
|
163
|
+
try {
|
|
164
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore cleanup errors
|
|
167
|
+
}
|
|
154
168
|
}
|
|
155
169
|
}
|
|
156
170
|
}
|
|
@@ -11,7 +11,7 @@ import type { AgentMessage } from "@gajae-code/agent-core";
|
|
|
11
11
|
import type { ImageContent } from "@gajae-code/ai/core";
|
|
12
12
|
import type { glob as globFn } from "@gajae-code/natives";
|
|
13
13
|
import { fuzzyMatch } from "@gajae-code/tui";
|
|
14
|
-
import { formatAge, formatBytes, readImageMetadata } from "@gajae-code/utils";
|
|
14
|
+
import { formatAge, formatBytes, pathIsWithin, readImageMetadata, resolveEquivalentPath } from "@gajae-code/utils";
|
|
15
15
|
import { formatHashLines } from "../hashline/hash";
|
|
16
16
|
import type { FileMentionMessage } from "../session/messages";
|
|
17
17
|
import {
|
|
@@ -313,6 +313,26 @@ export function extractFileMentions(text: string): string[] {
|
|
|
313
313
|
return [...new Set(mentions)];
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Compare canonical Windows paths without folding directory names: individual
|
|
318
|
+
* directories can opt into case-sensitive names.
|
|
319
|
+
*/
|
|
320
|
+
export function isCanonicalWindowsPathWithin(root: string, candidate: string): boolean {
|
|
321
|
+
if (!path.win32.isAbsolute(root) || !path.win32.isAbsolute(candidate)) return false;
|
|
322
|
+
|
|
323
|
+
const canonicalRoot = path.win32.resolve(root);
|
|
324
|
+
const canonicalCandidate = path.win32.resolve(candidate);
|
|
325
|
+
const rootParts = path.win32.parse(canonicalRoot);
|
|
326
|
+
const candidateParts = path.win32.parse(canonicalCandidate);
|
|
327
|
+
if (rootParts.root.toLowerCase() !== candidateParts.root.toLowerCase()) return false;
|
|
328
|
+
|
|
329
|
+
const rootSegments = canonicalRoot.slice(rootParts.root.length).split(path.win32.sep).filter(Boolean);
|
|
330
|
+
const candidateSegments = canonicalCandidate.slice(candidateParts.root.length).split(path.win32.sep).filter(Boolean);
|
|
331
|
+
if (candidateSegments.length < rootSegments.length) return false;
|
|
332
|
+
|
|
333
|
+
return rootSegments.every((segment, index) => segment === candidateSegments[index]);
|
|
334
|
+
}
|
|
335
|
+
|
|
316
336
|
/**
|
|
317
337
|
* Generate a FileMentionMessage containing the contents of mentioned files.
|
|
318
338
|
* Returns empty array if no files could be read.
|
|
@@ -336,6 +356,8 @@ export async function generateFileMentionMessages(
|
|
|
336
356
|
if (filePaths.length === 0) return [];
|
|
337
357
|
|
|
338
358
|
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
359
|
+
const canonicalWindowsCwd = process.platform === "win32" ? await fs.realpath(cwd).catch(() => undefined) : undefined;
|
|
360
|
+
if (process.platform === "win32" && canonicalWindowsCwd === undefined) return [];
|
|
339
361
|
|
|
340
362
|
const files: FileMentionMessage["files"] = [];
|
|
341
363
|
let mentionCandidatesPromise: Promise<MentionCandidate[]> | null = null;
|
|
@@ -351,6 +373,24 @@ export async function generateFileMentionMessages(
|
|
|
351
373
|
continue;
|
|
352
374
|
}
|
|
353
375
|
const absolutePath = resolveReadPath(resolvedPath, cwd);
|
|
376
|
+
// Skip `..`, `~`, and outside absolute paths. On Windows, realpath both
|
|
377
|
+
// paths and compare exact components because directories can be case-sensitive.
|
|
378
|
+
// Fail closed if either real path is unavailable. Read the canonical path so
|
|
379
|
+
// replacing the original mention symlink does not retarget this read.
|
|
380
|
+
const canonicalPath =
|
|
381
|
+
process.platform === "win32"
|
|
382
|
+
? await fs.realpath(absolutePath).catch(() => undefined)
|
|
383
|
+
: resolveEquivalentPath(absolutePath);
|
|
384
|
+
if (!canonicalPath) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const staysWithinSession =
|
|
388
|
+
process.platform === "win32"
|
|
389
|
+
? canonicalWindowsCwd !== undefined && isCanonicalWindowsPathWithin(canonicalWindowsCwd, canonicalPath)
|
|
390
|
+
: pathIsWithin(cwd, canonicalPath);
|
|
391
|
+
if (!staysWithinSession) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
354
394
|
// Duplicate suppression, keyed by absolute path: same path shown earlier this
|
|
355
395
|
// batch or recently in the conversation → compact note, no re-read (the path
|
|
356
396
|
// stays referenced so the model can re-fetch via the read tool).
|
|
@@ -364,14 +404,14 @@ export async function generateFileMentionMessages(
|
|
|
364
404
|
}
|
|
365
405
|
shownInBatch.add(absolutePath);
|
|
366
406
|
try {
|
|
367
|
-
const stat = await Bun.file(
|
|
407
|
+
const stat = await Bun.file(canonicalPath).stat();
|
|
368
408
|
if (stat.isDirectory()) {
|
|
369
|
-
const { output, lineCount } = await buildDirectoryListing(
|
|
409
|
+
const { output, lineCount } = await buildDirectoryListing(canonicalPath);
|
|
370
410
|
files.push({ path: resolvedPath, content: output, lineCount });
|
|
371
411
|
continue;
|
|
372
412
|
}
|
|
373
413
|
|
|
374
|
-
const imageMetadata = await readImageMetadata(
|
|
414
|
+
const imageMetadata = await readImageMetadata(canonicalPath);
|
|
375
415
|
const mimeType = imageMetadata?.mimeType;
|
|
376
416
|
if (mimeType) {
|
|
377
417
|
if (stat.size > MAX_AUTO_READ_IMAGE_BYTES) {
|
|
@@ -383,7 +423,7 @@ export async function generateFileMentionMessages(
|
|
|
383
423
|
});
|
|
384
424
|
continue;
|
|
385
425
|
}
|
|
386
|
-
const buffer = await fs.readFile(
|
|
426
|
+
const buffer = await fs.readFile(canonicalPath);
|
|
387
427
|
if (buffer.length === 0) {
|
|
388
428
|
continue;
|
|
389
429
|
}
|
|
@@ -420,7 +460,7 @@ export async function generateFileMentionMessages(
|
|
|
420
460
|
continue;
|
|
421
461
|
}
|
|
422
462
|
|
|
423
|
-
const content = await Bun.file(
|
|
463
|
+
const content = await Bun.file(canonicalPath).text();
|
|
424
464
|
let { output, lineCount } = buildTextOutput(content, options?.maxInlineBytes);
|
|
425
465
|
if (options?.useHashLines) {
|
|
426
466
|
output = formatHashLines(output);
|
package/src/utils/git.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { $which, hasFsCode, isEnoent
|
|
4
|
+
import { $which, hasFsCode, isEnoent } from "@gajae-code/utils";
|
|
5
|
+
import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
|
|
5
6
|
import {
|
|
6
7
|
parseDiffHunks as parseCommitDiffHunks,
|
|
7
8
|
parseFileDiffs,
|
|
@@ -198,7 +199,10 @@ async function runCommand(
|
|
|
198
199
|
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
199
200
|
const child = Bun.spawn(["git", ...commandArgs], {
|
|
200
201
|
cwd,
|
|
201
|
-
|
|
202
|
+
// The inherited environment holds the autoloaded project `.env`; without
|
|
203
|
+
// filtering, a repository could set e.g. `GIT_CONFIG_*` (`core.hooksPath`)
|
|
204
|
+
// or `GIT_SSH_COMMAND` and have GJC's own commit/fetch/push run its code.
|
|
205
|
+
env: options.env ? { ...projectSafeSpawnEnv(), GIT_OPTIONAL_LOCKS: "0", ...options.env } : projectSafeSpawnEnv(),
|
|
202
206
|
signal: options.signal,
|
|
203
207
|
stdin: normalizeStdin(options.stdin),
|
|
204
208
|
stdout: "pipe",
|
|
@@ -365,9 +369,24 @@ function buildApplyArgs(patchPath: string, options: PatchOptions): string[] {
|
|
|
365
369
|
}
|
|
366
370
|
|
|
367
371
|
async function writeTempPatch(content: string): Promise<string> {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
// Random directory plus an exclusive create: a preexisting symlink under the
|
|
373
|
+
// shared temp directory is not opened, and the patch is not left world-readable.
|
|
374
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gjc-git-patch-"));
|
|
375
|
+
try {
|
|
376
|
+
if (process.platform !== "win32") await fs.promises.chmod(tempDir, 0o700);
|
|
377
|
+
const tempPath = path.join(tempDir, "patch");
|
|
378
|
+
const handle = await fs.promises.open(tempPath, "wx", 0o600);
|
|
379
|
+
try {
|
|
380
|
+
await handle.writeFile(content);
|
|
381
|
+
} finally {
|
|
382
|
+
await handle.close();
|
|
383
|
+
}
|
|
384
|
+
if (process.platform !== "win32") await fs.promises.chmod(tempPath, 0o600);
|
|
385
|
+
return tempPath;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -1232,7 +1251,7 @@ export const patch = {
|
|
|
1232
1251
|
try {
|
|
1233
1252
|
await patch.apply(cwd, tempPath, options);
|
|
1234
1253
|
} finally {
|
|
1235
|
-
await fs.promises.rm(tempPath, { force: true });
|
|
1254
|
+
await fs.promises.rm(path.dirname(tempPath), { recursive: true, force: true });
|
|
1236
1255
|
}
|
|
1237
1256
|
},
|
|
1238
1257
|
|
|
@@ -1253,7 +1272,7 @@ export const patch = {
|
|
|
1253
1272
|
try {
|
|
1254
1273
|
return await patch.canApply(cwd, tempPath, options);
|
|
1255
1274
|
} finally {
|
|
1256
|
-
await fs.promises.rm(tempPath, { force: true });
|
|
1275
|
+
await fs.promises.rm(path.dirname(tempPath), { recursive: true, force: true });
|
|
1257
1276
|
}
|
|
1258
1277
|
},
|
|
1259
1278
|
|
|
@@ -1594,6 +1613,14 @@ function formatGhFailure(args: readonly string[], stdout: string, stderr: string
|
|
|
1594
1613
|
return `GitHub CLI command failed: gh ${args.join(" ")}`;
|
|
1595
1614
|
}
|
|
1596
1615
|
|
|
1616
|
+
/** Captured `gh` output never wants a pager or forced-terminal behavior. */
|
|
1617
|
+
function ghSpawnEnv(): Record<string, string> {
|
|
1618
|
+
const env = projectSafeSpawnEnv();
|
|
1619
|
+
delete env.GH_FORCE_TTY;
|
|
1620
|
+
env.GH_PAGER = "cat";
|
|
1621
|
+
return env;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1597
1624
|
export const github = {
|
|
1598
1625
|
/** Check if `gh` CLI is installed. */
|
|
1599
1626
|
available(): boolean {
|
|
@@ -1609,6 +1636,10 @@ export const github = {
|
|
|
1609
1636
|
try {
|
|
1610
1637
|
const child = Bun.spawn(["gh", ...args], {
|
|
1611
1638
|
cwd,
|
|
1639
|
+
// Same project-dotenv filtering as git: `gh` runs git for checkout
|
|
1640
|
+
// and push. Output is captured, so gh must never page or act as if
|
|
1641
|
+
// on a terminal; pin both so a project `.env` cannot select a pager.
|
|
1642
|
+
env: ghSpawnEnv(),
|
|
1612
1643
|
stdin: "ignore",
|
|
1613
1644
|
stdout: "pipe",
|
|
1614
1645
|
stderr: "pipe",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Shared input policy must remain independent of file loading, tools and sessions.
|
|
2
|
+
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
3
|
+
export const MAX_STAGED_PROMPT_IMAGES = 16;
|
|
4
|
+
export const MAX_PASTED_IMAGE_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
export const MAX_PASTED_IMAGE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
export const MAX_PASTED_IMAGE_DIMENSION = 20_000;
|
|
7
|
+
export const MAX_PASTED_IMAGE_PIXELS = 40_000_000;
|
|
8
|
+
export const MAX_PASTED_IMAGE_DECODED_BYTES = 160 * 1024 * 1024;
|
|
@@ -2,9 +2,9 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import type { ImageContent } from "@gajae-code/ai/core";
|
|
3
3
|
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@gajae-code/utils";
|
|
4
4
|
import { resolveReadPath } from "../tools/path-utils";
|
|
5
|
+
import { MAX_IMAGE_INPUT_BYTES } from "./image-limits";
|
|
5
6
|
import { formatDimensionNote, resizeImageBuffer } from "./image-resize";
|
|
6
7
|
|
|
7
|
-
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
8
8
|
export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
|
9
9
|
|
|
10
10
|
const SVG_MIME_TYPE = "image/svg+xml";
|
package/src/utils/open.ts
CHANGED
|
@@ -1,15 +1,84 @@
|
|
|
1
|
-
/** Open
|
|
1
|
+
/** Open an http(s) URL or local path. Best-effort, never throws. */
|
|
2
|
+
const ALLOWED_OPEN_PROTOCOLS = new Set(["http:", "https:"]);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Zero-width and non-ASCII spaces. ASCII controls are rejected, not stripped:
|
|
6
|
+
* the URL parser drops some of them and would turn a hostile string into a clean href.
|
|
7
|
+
*/
|
|
8
|
+
const OUTER_FORMAT = /[\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF\u200B\u2060]/;
|
|
9
|
+
|
|
10
|
+
function hasAsciiControl(value: string): boolean {
|
|
11
|
+
for (let i = 0; i < value.length; i++) {
|
|
12
|
+
const code = value.charCodeAt(i);
|
|
13
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isOuterPadding(ch: string): boolean {
|
|
19
|
+
return ch.trim() === "" || OUTER_FORMAT.test(ch);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Drop every outer space or format character. Classification only; local paths stay raw. */
|
|
23
|
+
function peelPadding(value: string): string {
|
|
24
|
+
let start = 0;
|
|
25
|
+
let end = value.length;
|
|
26
|
+
while (start < end && isOuterPadding(value.charAt(start))) start++;
|
|
27
|
+
while (end > start && isOuterPadding(value.charAt(end - 1))) end--;
|
|
28
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Drive paths parse as one-letter schemes (`c:`). Recognize them before that check. */
|
|
32
|
+
function isWindowsDrivePath(value: string): boolean {
|
|
33
|
+
return /^[A-Za-z]:[\\/]/.test(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SCHEME_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
37
|
+
|
|
38
|
+
/** http(s) href, "reject" for any other URL, or undefined when `value` is not a URL. */
|
|
39
|
+
function classifyUrl(value: string): string | "reject" | undefined {
|
|
40
|
+
if (isWindowsDrivePath(value) || isWindowsDrivePath(value.trim())) return undefined;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = new URL(value);
|
|
43
|
+
if (!ALLOWED_OPEN_PROTOCOLS.has(parsed.protocol) || hasAsciiControl(parsed.href)) return "reject";
|
|
44
|
+
return parsed.href;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveOpenTarget(raw: string): string | undefined {
|
|
51
|
+
if (hasAsciiControl(raw) || raw.length === 0) return undefined;
|
|
52
|
+
// Local paths, including surrounding spaces, are spawned unchanged.
|
|
53
|
+
if (isWindowsDrivePath(raw)) return raw;
|
|
54
|
+
const direct = classifyUrl(raw);
|
|
55
|
+
if (direct === "reject") return undefined;
|
|
56
|
+
if (direct !== undefined) return direct;
|
|
57
|
+
|
|
58
|
+
const peeled = peelPadding(raw);
|
|
59
|
+
if (peeled.length === 0) return undefined;
|
|
60
|
+
// Padding can hide a scheme or a flag. Refuse that instead of opening a different string.
|
|
61
|
+
if (peeled !== raw && !isWindowsDrivePath(peeled)) {
|
|
62
|
+
const hidden = classifyUrl(peeled);
|
|
63
|
+
if (hidden !== undefined || peeled.startsWith("-") || SCHEME_PREFIX.test(peeled)) return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (raw.startsWith("-") || SCHEME_PREFIX.test(raw)) return undefined;
|
|
66
|
+
return raw;
|
|
67
|
+
}
|
|
68
|
+
|
|
2
69
|
export function openPath(urlOrPath: string): void {
|
|
70
|
+
const target = resolveOpenTarget(urlOrPath);
|
|
71
|
+
if (target === undefined) return;
|
|
3
72
|
let cmd: string[];
|
|
4
73
|
switch (process.platform) {
|
|
5
74
|
case "darwin":
|
|
6
|
-
cmd = ["open",
|
|
75
|
+
cmd = ["open", target];
|
|
7
76
|
break;
|
|
8
77
|
case "win32":
|
|
9
|
-
cmd = ["rundll32", "url.dll,FileProtocolHandler",
|
|
78
|
+
cmd = ["rundll32", "url.dll,FileProtocolHandler", target];
|
|
10
79
|
break;
|
|
11
80
|
default:
|
|
12
|
-
cmd = ["xdg-open",
|
|
81
|
+
cmd = ["xdg-open", target];
|
|
13
82
|
break;
|
|
14
83
|
}
|
|
15
84
|
try {
|
|
@@ -4,10 +4,17 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { ImageContent } from "@gajae-code/ai/core";
|
|
6
6
|
import { formatBytes, parseImageMetadata } from "@gajae-code/utils";
|
|
7
|
+
import {
|
|
8
|
+
MAX_IMAGE_INPUT_BYTES,
|
|
9
|
+
MAX_PASTED_IMAGE_DECODED_BYTES,
|
|
10
|
+
MAX_PASTED_IMAGE_DIMENSION,
|
|
11
|
+
MAX_PASTED_IMAGE_OUTPUT_BYTES,
|
|
12
|
+
MAX_PASTED_IMAGE_PIXELS,
|
|
13
|
+
MAX_PASTED_IMAGE_SOURCE_BYTES,
|
|
14
|
+
} from "./image-limits";
|
|
7
15
|
import {
|
|
8
16
|
ImageInputTooLargeError,
|
|
9
17
|
type LoadedImageInput,
|
|
10
|
-
MAX_IMAGE_INPUT_BYTES,
|
|
11
18
|
materializeImageInput,
|
|
12
19
|
type TransformedImageInput,
|
|
13
20
|
transformImageInputBytes,
|
|
@@ -15,12 +22,6 @@ import {
|
|
|
15
22
|
import { DEFAULT_IMAGE_RESIZE_MAX_BYTES } from "./image-resize";
|
|
16
23
|
import { MAX_PASTED_IMAGE_COUNT } from "./pasted-image-path";
|
|
17
24
|
|
|
18
|
-
export const MAX_PASTED_IMAGE_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
19
|
-
export const MAX_PASTED_IMAGE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
20
|
-
export const MAX_PASTED_IMAGE_DIMENSION = 20_000;
|
|
21
|
-
export const MAX_PASTED_IMAGE_PIXELS = 40_000_000;
|
|
22
|
-
export const MAX_PASTED_IMAGE_DECODED_BYTES = 160 * 1024 * 1024;
|
|
23
|
-
|
|
24
25
|
export type PastedImageBatchErrorCode =
|
|
25
26
|
| "too-many"
|
|
26
27
|
| "unsafe-path"
|
|
@@ -319,7 +320,7 @@ function hasWebpAnimation(bytes: Buffer): boolean {
|
|
|
319
320
|
return false;
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
function isAnimatedImage(bytes: Buffer, mimeType: string): boolean | undefined {
|
|
323
|
+
export function isAnimatedImage(bytes: Buffer, mimeType: string): boolean | undefined {
|
|
323
324
|
if (mimeType === "image/gif") {
|
|
324
325
|
const frames = gifFrameCount(bytes);
|
|
325
326
|
return frames === undefined ? undefined : frames > 1;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Predict the user's likely next prompt after an agent turn
|
|
3
|
-
*
|
|
2
|
+
* Predict the user's likely next prompt after an agent turn with the `default`
|
|
3
|
+
* model role. Native GJC port of the ghost-text prompt-suggestion behavior
|
|
4
4
|
* from Claude Code: the prediction renders as dim ghost text in the empty
|
|
5
5
|
* composer and Tab accepts it.
|
|
6
6
|
*/
|
|
@@ -169,8 +169,9 @@ function getSuggestionModel(
|
|
|
169
169
|
registry: ModelRegistry,
|
|
170
170
|
settings: Settings,
|
|
171
171
|
currentModel?: Model<Api>,
|
|
172
|
+
credentialSessionId?: string,
|
|
172
173
|
): Model<Api> | undefined {
|
|
173
|
-
const availableModels = registry.getAvailable();
|
|
174
|
+
const availableModels = registry.getAvailable(credentialSessionId);
|
|
174
175
|
if (availableModels.length === 0) return undefined;
|
|
175
176
|
const model = resolveRoleSelection(["default"], settings, availableModels, registry)?.model;
|
|
176
177
|
if (model) return model;
|
|
@@ -203,7 +204,7 @@ export async function generatePromptSuggestion(
|
|
|
203
204
|
const context = buildPromptSuggestionContext(messages);
|
|
204
205
|
if (!context) return null;
|
|
205
206
|
|
|
206
|
-
const model = getSuggestionModel(registry, settings, currentModel);
|
|
207
|
+
const model = getSuggestionModel(registry, settings, currentModel, sessionId);
|
|
207
208
|
if (!model) {
|
|
208
209
|
logger.debug("prompt-suggestion: no model available");
|
|
209
210
|
return null;
|
|
@@ -31,6 +31,11 @@ function getShellConfigFile(shell: string, home: string): string {
|
|
|
31
31
|
return path.join(home, ".profile");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** One POSIX shell word. Double quotes would still run `$()` and backticks. */
|
|
35
|
+
export function quotePosixShell(value: string): string {
|
|
36
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
/**
|
|
35
40
|
* Generate the snapshot creation script.
|
|
36
41
|
* This script sources the user's rc file and extracts functions, aliases, and options.
|
|
@@ -43,6 +48,9 @@ function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: str
|
|
|
43
48
|
|
|
44
49
|
// Escape the snapshot path for shell
|
|
45
50
|
const escapedPath = snapshotPath.replace(/'/g, "'\\''");
|
|
51
|
+
const rcSource = hasRcFile
|
|
52
|
+
? `source ${quotePosixShell(rcFile)} < /dev/null 2>/dev/null`
|
|
53
|
+
: "# No user config file to source";
|
|
46
54
|
|
|
47
55
|
// Function extraction differs between bash and zsh
|
|
48
56
|
const functionScript = isZsh
|
|
@@ -83,7 +91,7 @@ umask 077
|
|
|
83
91
|
SNAPSHOT_FILE='${escapedPath}'
|
|
84
92
|
|
|
85
93
|
# Source user's rc file if it exists
|
|
86
|
-
${
|
|
94
|
+
${rcSource}
|
|
87
95
|
|
|
88
96
|
# Create/clear the snapshot file
|
|
89
97
|
echo "# Shell snapshot - generated by gjc agent" >| "$SNAPSHOT_FILE"
|