@gajae-code/coding-agent 0.11.6 → 0.11.8

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.
Files changed (259) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/commands/daemon.d.ts +2 -2
  6. package/dist/types/commands/sdk.d.ts +20 -2
  7. package/dist/types/config/keybindings.d.ts +34 -4
  8. package/dist/types/config/model-profile-activation.d.ts +4 -3
  9. package/dist/types/config/model-profile-contract.d.ts +38 -0
  10. package/dist/types/config/settings-schema.d.ts +70 -7
  11. package/dist/types/edit/modes/patch.d.ts +13 -0
  12. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  13. package/dist/types/eval/py/executor.d.ts +1 -1
  14. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  16. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
  18. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
  20. package/dist/types/main.d.ts +32 -1
  21. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  22. package/dist/types/modes/components/model-selector.d.ts +2 -0
  23. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  25. package/dist/types/modes/components/welcome.d.ts +2 -0
  26. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  27. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  28. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  29. package/dist/types/modes/interactive-mode.d.ts +7 -2
  30. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  31. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  32. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  33. package/dist/types/modes/types.d.ts +4 -0
  34. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  35. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/sdk/broker/broker.d.ts +2 -0
  40. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  41. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
  42. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  43. package/dist/types/sdk/bus/config.d.ts +2 -0
  44. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  45. package/dist/types/sdk/bus/index.d.ts +26 -1
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  49. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  50. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
  51. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  52. package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
  53. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  54. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  55. package/dist/types/sdk/client/discovery.d.ts +2 -0
  56. package/dist/types/sdk/client/liveness.d.ts +7 -0
  57. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  58. package/dist/types/sdk/host/host.d.ts +1 -1
  59. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  60. package/dist/types/sdk/index.d.ts +2 -0
  61. package/dist/types/sdk/prompt-status.d.ts +88 -0
  62. package/dist/types/sdk/session-directory.d.ts +1 -1
  63. package/dist/types/sdk/startup-capability.d.ts +3 -0
  64. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  65. package/dist/types/sdk/transport/index.d.ts +12 -0
  66. package/dist/types/sdk/transport/relay.d.ts +43 -0
  67. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  68. package/dist/types/sdk/transport/socket.d.ts +7 -0
  69. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  70. package/dist/types/session/agent-session.d.ts +30 -5
  71. package/dist/types/session/btw-contract.d.ts +16 -0
  72. package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
  73. package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
  74. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  75. package/dist/types/session/session-manager.d.ts +25 -4
  76. package/dist/types/session/session-storage.d.ts +18 -0
  77. package/dist/types/setup/credential-import.d.ts +5 -1
  78. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  79. package/dist/types/ssh/utils.d.ts +1 -0
  80. package/dist/types/task/discovery.d.ts +3 -1
  81. package/dist/types/task/index.d.ts +3 -1
  82. package/dist/types/task/receipt.d.ts +2 -0
  83. package/dist/types/task/types.d.ts +94 -0
  84. package/dist/types/tools/ask.d.ts +454 -174
  85. package/dist/types/tools/fetch.d.ts +5 -8
  86. package/dist/types/tools/image-gen.d.ts +63 -1
  87. package/dist/types/tools/index.d.ts +4 -0
  88. package/dist/types/tools/skill-discovery.d.ts +7 -0
  89. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  90. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  91. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  92. package/dist/types/web/insane/bridge.d.ts +1 -0
  93. package/dist/types/web/insane/url-guard.d.ts +17 -0
  94. package/package.json +8 -8
  95. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  96. package/scripts/dogfood-repository-binding.ts +209 -0
  97. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  98. package/scripts/run-test-manifest.ts +4 -4
  99. package/src/capability/index.ts +50 -42
  100. package/src/capability/types.ts +3 -0
  101. package/src/cli/args.ts +2 -92
  102. package/src/cli/notify-cli.ts +5 -38
  103. package/src/cli/setup-cli.ts +10 -4
  104. package/src/cli.ts +52 -9
  105. package/src/commands/daemon.ts +22 -5
  106. package/src/commands/sdk.ts +44 -11
  107. package/src/config/keybindings.ts +158 -25
  108. package/src/config/model-profile-activation.ts +7 -18
  109. package/src/config/model-profile-contract.ts +161 -0
  110. package/src/config/model-profiles.ts +18 -0
  111. package/src/config/model-registry.ts +11 -1
  112. package/src/config/settings-schema.ts +73 -7
  113. package/src/coordinator-mcp/model-preset.ts +21 -74
  114. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  115. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  116. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  117. package/src/discovery/ssh.ts +12 -1
  118. package/src/edit/index.ts +3 -6
  119. package/src/edit/modes/patch.ts +38 -1
  120. package/src/edit/modes/replace.ts +42 -0
  121. package/src/edit/path-mutation-lock.ts +112 -0
  122. package/src/eval/py/executor.ts +30 -11
  123. package/src/eval/py/kernel.ts +1 -21
  124. package/src/eval/py/prelude.py +2 -2
  125. package/src/eval/py/runner-artifact.ts +84 -0
  126. package/src/eval/py/tool-bridge.ts +34 -18
  127. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  128. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  129. package/src/gjc-runtime/launch-tmux.ts +1 -1
  130. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  131. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  132. package/src/gjc-runtime/repository-binding.ts +267 -0
  133. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  134. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  135. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  136. package/src/gjc-runtime/ultragoal-guard.ts +93 -15
  137. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  138. package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
  139. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  140. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  141. package/src/internal-urls/docs-index.generated.ts +12 -10
  142. package/src/internal-urls/local-protocol.ts +64 -21
  143. package/src/internal-urls/mcp-protocol.ts +73 -11
  144. package/src/main.ts +111 -37
  145. package/src/modes/DESIGN.md +5 -0
  146. package/src/modes/acp/acp-agent.ts +50 -16
  147. package/src/modes/action-registry.ts +1 -1
  148. package/src/modes/components/btw-panel.ts +82 -16
  149. package/src/modes/components/model-selector.ts +73 -11
  150. package/src/modes/components/plan-preview-overlay.ts +1 -1
  151. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  152. package/src/modes/components/tool-status-header.ts +25 -4
  153. package/src/modes/components/welcome.ts +22 -18
  154. package/src/modes/controllers/btw-controller.ts +119 -36
  155. package/src/modes/controllers/command-controller.ts +97 -54
  156. package/src/modes/controllers/event-controller.ts +10 -0
  157. package/src/modes/controllers/input-controller.ts +38 -2
  158. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  159. package/src/modes/controllers/selector-controller.ts +149 -63
  160. package/src/modes/interactive-mode.ts +57 -34
  161. package/src/modes/prompt-action-autocomplete.ts +32 -13
  162. package/src/modes/prompt-suggestion-controller.ts +96 -0
  163. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  164. package/src/modes/types.ts +4 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  166. package/src/modes/utils/ui-helpers.ts +5 -3
  167. package/src/prompts/agents/architect.md +0 -1
  168. package/src/prompts/agents/critic.md +0 -1
  169. package/src/prompts/system/btw-user.md +3 -8
  170. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  171. package/src/prompts/system/system-prompt.md +6 -4
  172. package/src/prompts/tools/cron.md +3 -1
  173. package/src/prompts/tools/skill-discovery.md +1 -0
  174. package/src/runtime-mcp/client.ts +41 -55
  175. package/src/runtime-mcp/content-limits.ts +71 -0
  176. package/src/runtime-mcp/json-rpc.ts +23 -9
  177. package/src/runtime-mcp/manager.ts +51 -53
  178. package/src/runtime-mcp/oauth-flow.ts +0 -72
  179. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  180. package/src/runtime-mcp/transports/http.ts +63 -22
  181. package/src/sdk/broker/broker.ts +34 -20
  182. package/src/sdk/broker/lifecycle.ts +185 -36
  183. package/src/sdk/bus/chat-daemon-control.ts +19 -5
  184. package/src/sdk/bus/config-commands.ts +2 -2
  185. package/src/sdk/bus/config.ts +9 -1
  186. package/src/sdk/bus/conversation-store.ts +40 -6
  187. package/src/sdk/bus/index.ts +244 -70
  188. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  189. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  190. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  191. package/src/sdk/bus/notification-orchestration.ts +12 -0
  192. package/src/sdk/bus/notification-service.ts +68 -20
  193. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  194. package/src/sdk/bus/recent-activity.ts +90 -26
  195. package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
  196. package/src/sdk/bus/telegram-daemon-control.ts +147 -35
  197. package/src/sdk/bus/telegram-daemon.ts +2463 -450
  198. package/src/sdk/bus/telegram-reference.ts +20 -2
  199. package/src/sdk/bus/topic-registry.ts +451 -12
  200. package/src/sdk/cli/session-cli.ts +15 -2
  201. package/src/sdk/client/discovery.ts +19 -3
  202. package/src/sdk/client/liveness.ts +49 -0
  203. package/src/sdk/host/control/dispatch.ts +1 -1
  204. package/src/sdk/host/control/operations.ts +1 -1
  205. package/src/sdk/host/host.ts +2 -6
  206. package/src/sdk/host/query/handlers.ts +70 -5
  207. package/src/sdk/index.ts +12 -0
  208. package/src/sdk/prompt-status.ts +97 -0
  209. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  210. package/src/sdk/protocol/operation-registry.ts +18 -4
  211. package/src/sdk/session-directory.ts +5 -1
  212. package/src/sdk/session.ts +31 -5
  213. package/src/sdk/startup-capability.ts +9 -1
  214. package/src/sdk/transport/auth-preface.ts +82 -0
  215. package/src/sdk/transport/index.ts +18 -0
  216. package/src/sdk/transport/relay.ts +252 -0
  217. package/src/sdk/transport/serve-cli.ts +105 -0
  218. package/src/sdk/transport/socket.ts +197 -0
  219. package/src/sdk/transport/stdio.ts +17 -0
  220. package/src/session/agent-session.ts +292 -154
  221. package/src/session/blob-store.ts +25 -7
  222. package/src/session/btw-contract.ts +59 -0
  223. package/src/session/internal/managed-session-scope.ts +1163 -137
  224. package/src/session/internal/managed-session-storage.ts +397 -234
  225. package/src/session/internal/native-publish-outcome.ts +266 -0
  226. package/src/session/session-manager.ts +620 -403
  227. package/src/session/session-storage.ts +128 -11
  228. package/src/setup/credential-import.ts +28 -3
  229. package/src/setup/provider-onboarding.ts +30 -1
  230. package/src/setup/provider-presets.json +2 -1
  231. package/src/slash-commands/builtin-registry.ts +1 -1
  232. package/src/ssh/utils.ts +14 -0
  233. package/src/task/discovery.ts +8 -2
  234. package/src/task/gjc-command.ts +2 -2
  235. package/src/task/index.ts +121 -15
  236. package/src/task/receipt.ts +3 -0
  237. package/src/task/types.ts +34 -3
  238. package/src/tools/ask.ts +199 -89
  239. package/src/tools/ast-edit.ts +2 -2
  240. package/src/tools/bash-pty-selection.ts +2 -2
  241. package/src/tools/browser/screenshot-format.ts +6 -1
  242. package/src/tools/cron.ts +1 -1
  243. package/src/tools/fetch.ts +13 -131
  244. package/src/tools/image-gen.ts +476 -23
  245. package/src/tools/index.ts +4 -0
  246. package/src/tools/read.ts +2 -0
  247. package/src/tools/render-utils.ts +7 -1
  248. package/src/tools/skill-discovery.ts +39 -3
  249. package/src/tools/sqlite-reader.ts +63 -5
  250. package/src/utils/edit-mode.ts +2 -2
  251. package/src/utils/pasted-image-path.ts +15 -6
  252. package/src/utils/prompt-suggestion.ts +265 -0
  253. package/src/utils/shell-snapshot.ts +173 -63
  254. package/src/utils/sixel.ts +3 -3
  255. package/src/web/insane/bridge.ts +5 -1
  256. package/src/web/insane/url-guard.ts +140 -27
  257. package/src/web/scrapers/docs-rs.ts +30 -5
  258. package/src/web/scrapers/types.ts +19 -49
  259. package/src/web/scrapers/utils.ts +12 -26
@@ -23,7 +23,6 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
23
23
  - Do not invent problems; report no issues found when the plan passes.
24
24
  - Escalate routing needs upward: planner for plan revision, the deep-interview skill for requirements gathering, architect for code analysis.
25
25
  - For consensus planning, reject shallow alternatives, driver contradictions, vague risks, weak verification, missing acceptance criteria, or under-specified areas needing expansion before execution.
26
- - Prefer practical sufficiency over endless logical completeness: do not demand preemptive logic for edge cases that are not actually observed or evidenced, yet still require extensibility and robustness when observability-based evidence shows the investment is worth the token and time.
27
26
  </constraints>
28
27
 
29
28
  <execution_loop>
@@ -1,8 +1,3 @@
1
- <btw>
2
- This is an ephemeral side question for the current interactive session.
3
- Answer briefly and directly using the conversation context already provided.
4
- Do not use tools.
5
- Do not ask follow-up questions.
6
- Question:
7
- {{question}}
8
- </btw>
1
+ You are answering inside an ephemeral multi-turn side chat for the interactive session.
2
+ Use only the sanitized visible-text conversation scope and prior side-chat turns provided as messages.
3
+ Answer directly. Do not use tools or request hidden session state.
@@ -0,0 +1,32 @@
1
+ [SUGGESTION MODE: Suggest what the user might naturally type next into the coding agent.]
2
+
3
+ FIRST: Look at the user's recent messages and original request.
4
+
5
+ Your job is to predict what THEY would type - not what you think they should do.
6
+
7
+ THE TEST: Would they think "I was just about to type that"?
8
+
9
+ EXAMPLES:
10
+ User asked "fix the bug and run tests", bug is fixed → "run the tests"
11
+ After code written → "try it out"
12
+ Agent offers options → suggest the one the user would likely pick, based on conversation
13
+ Agent asks to continue → "yes" or "go ahead"
14
+ Task complete, obvious follow-up → "commit this" or "push it"
15
+ After error or misunderstanding → silence (let them assess/correct)
16
+
17
+ Be specific: "run the tests" beats "continue".
18
+
19
+ NEVER SUGGEST:
20
+ - Evaluative ("looks good", "thanks")
21
+ - Questions ("what about…?")
22
+ - Agent-voice ("Let me…", "I'll…", "Here's…")
23
+ - New ideas they didn't ask about
24
+ - Multiple sentences
25
+
26
+ Stay silent if the next step isn't obvious from what the user said.
27
+
28
+ Stay silent if a suggestion could be unsafe or inappropriate — including any sensitive topic (security incidents, credentials, harm, private data). Even when the user is doing legitimate security or cybersecurity work, do not predict potentially unsafe actions.
29
+
30
+ Format: 2-12 words, match the user's style. Or nothing.
31
+
32
+ Reply with ONLY the suggestion, no quotes or explanation.
@@ -19,12 +19,15 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
19
19
  {{#unless subagent}}
20
20
  <gjc-runtime>
21
21
  <routing>
22
- - Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony.
22
+ - Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony. Small verification needs do not turn a clear request into a planning workflow.
23
+ - Ambiguous implementation asks with a missing target, scope, acceptance criteria, or safety boundary require clarification or the appropriate planning workflow before mutation.
23
24
  - Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
24
- - Vague requirements use `/skill:deep-interview`; clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
25
+ - Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
26
+ - Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
25
27
  - Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
26
28
  - Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
27
- - Active skills are authoritative: read and follow them; planning and read-only skills do not mutate before approval.
29
+ - Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
30
+ - Before explicit execution approval, planning and interview workflows NEVER edit product source, run mutating shell commands, commit, push, open PRs, or delegate implementation.
28
31
  </routing>
29
32
  </gjc-runtime>
30
33
  {{/unless}}
@@ -206,7 +209,6 @@ Guardrails? None. Restrictions? None. Freedom? Unlimited.
206
209
  - **Do your homework first** - Read the files, search, and understand the context before asking
207
210
  - **Don’t Stop at Analysis** - Once you’ve assessed the situation, take the next step (session, comment, merge, fix, cleanup) on your own
208
211
  - **No Inflating Operational/Meta Issues** - Just because the backlog is empty doesn’t mean you should force operational follow-ups, verifiability checks, or meta issues. Only raise issues for actual repository problems; otherwise, stop.
209
- - **Practical sufficiency over completeness** - Don’t add logic that preemptively handles edge cases you haven’t actually observed; endless logical completeness loses to practical sufficiency. Extensibility and robustness matter, but they must be observability-based and evidence-based—any engineering without underlying evidence that it’s worth the token and time is over-engineering.
210
212
  - **Always maintain execution** - Do not end your turn with just reporting, summarizing, or analyzing. If an action is possible, execute it immediately during the same turn.
211
213
  - **Always be in action mode** - The default stance is intervention, not observation. If you see something that needs cleaning up, clean it up; if you see a session that needs opening, open it; if you see a PR that needs blocking, block it; if you see a PR that needs merging, review it and merge it.
212
214
  - **No begging for permission** - For obvious follow-up actions (cleaning up sessions, marking items as stale, fixing routing, updating documentation to reflect Doctrine), don’t ask—just do it.
@@ -1,4 +1,6 @@
1
- Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks let you re-run a prompt automatically on an interval — poll a deployment, babysit a PR, check back on a long-running build, or remind yourself to do something later in the session.
1
+ Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks re-run an agent prompt automatically on an interval when every firing is allowed to produce a normal assistant response, such as a reminder or a scheduled status report.
2
+
3
+ Cron is not a silent polling primitive. Every firing starts an agent turn, and hiding the injected cron message does not hide that turn's assistant response. For ongoing logs, file watching, or PR/CI polling that should report only meaningful state changes, use `monitor` with a script that writes a line only when there is an event to process, and set `persistent: true` so the monitor survives the first emitted event. Do not schedule a cron prompt that asks the agent to suppress routine polls; prompt wording cannot make a cron-triggered assistant turn reliably silent.
2
4
 
3
5
  Use a single `op` field to select the operation:
4
6
 
@@ -3,6 +3,7 @@ Discover project and user runtime skills without loading full skill content.
3
3
  <instruction>
4
4
  - Searches only custom runtime skill locations: nearest project `.gjc/skills`; then, under the home directory, canonical `<config>/agent/skills`, configured legacy `<config>/skills`, and historical legacy `.gjc/skills`. `<config>` is the home-relative directory name from `GJC_CONFIG_DIR`, then `PI_CONFIG_DIR`, then `.gjc`; even an absolute-looking configured name is joined beneath `<home>`. Duplicate names use that exact precedence. Built-in, bundled, and internal workflow skills are intentionally excluded.
5
5
  - Returns thin metadata only: name, description, source scope, path, and use conditions when present.
6
+ - When zero candidates are returned because discovery config is disabled (`skills.enabled`, `skills.enablePiProject`, `skills.enablePiUser`), the result carries a `notice` explaining which setting blocked the search — an empty result without a `notice` means the searched scopes genuinely contain no matching skills.
6
7
  - To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.
7
8
  </instruction>
8
9
 
@@ -15,15 +15,12 @@ import type {
15
15
  MCPInitializeParams,
16
16
  MCPInitializeResult,
17
17
  MCPPrompt,
18
- MCPPromptsListResult,
19
18
  MCPRequestOptions,
20
19
  MCPResource,
21
20
  MCPResourceReadParams,
22
21
  MCPResourceReadResult,
23
22
  MCPResourceSubscribeParams,
24
- MCPResourcesListResult,
25
23
  MCPResourceTemplate,
26
- MCPResourceTemplatesListResult,
27
24
  MCPServerCapabilities,
28
25
  MCPServerConfig,
29
26
  MCPServerConnection,
@@ -43,6 +40,9 @@ const PROTOCOL_VERSION = "2025-03-26";
43
40
  /** Default connection timeout in ms */
44
41
  const CONNECTION_TIMEOUT_MS = 30_000;
45
42
 
43
+ const MAX_PAGINATION_PAGES = 100,
44
+ MAX_PAGINATION_ITEMS = 10_000;
45
+
46
46
  /** Client info sent during initialization */
47
47
  const CLIENT_INFO = {
48
48
  name: "gjc-coding-agent",
@@ -109,6 +109,40 @@ function decodeToolsListResult(value: unknown): MCPToolsListResult {
109
109
  return value.nextCursor === undefined ? { tools } : { tools, nextCursor: value.nextCursor };
110
110
  }
111
111
 
112
+ async function collectPaginated<T>(
113
+ connection: MCPServerConnection,
114
+ options: MCPRequestOptions | undefined,
115
+ method: string,
116
+ itemKey: string,
117
+ items: T[],
118
+ decode?: (value: unknown) => unknown,
119
+ ): Promise<void> {
120
+ const seenCursors = new Set<string>();
121
+ const failure = (detail: string) => new MCPExpectedFailure(new Error(`MCP ${method} pagination ${detail}`));
122
+ let cursor: string | undefined;
123
+ for (let page = 1; page <= MAX_PAGINATION_PAGES; page++) {
124
+ const value = await connection.transport.request<unknown>(method, cursor ? { cursor } : {}, options);
125
+ const result = decode ? decode(value) : value;
126
+ if (
127
+ !isRecord(result) ||
128
+ !Array.isArray(result[itemKey]) ||
129
+ (result.nextCursor !== undefined && typeof result.nextCursor !== "string")
130
+ )
131
+ throw new MCPExpectedFailure();
132
+ const nextCursor = result.nextCursor as string | undefined;
133
+ if (nextCursor && seenCursors.has(nextCursor)) throw failure("repeated a cursor");
134
+ const pageItems = result[itemKey] as T[];
135
+ const itemCount = items.length + pageItems.length;
136
+ if (itemCount > MAX_PAGINATION_ITEMS || (itemCount === MAX_PAGINATION_ITEMS && nextCursor))
137
+ throw failure("did not complete within the 10000-item budget");
138
+ items.push(...pageItems);
139
+ if (!nextCursor) return;
140
+ if (page === MAX_PAGINATION_PAGES) throw failure("did not complete within the 100-page budget");
141
+ seenCursors.add(nextCursor);
142
+ cursor = nextCursor;
143
+ }
144
+ }
145
+
112
146
  /**
113
147
  * Default handler for standard MCP server-to-client requests.
114
148
  * Handles `ping` and `roots/list`; rejects unknown methods with -32601.
@@ -292,18 +326,7 @@ export async function listTools(
292
326
  }
293
327
 
294
328
  const allTools: MCPToolDefinition[] = [];
295
- let cursor: string | undefined;
296
-
297
- do {
298
- const params: Record<string, unknown> = {};
299
- if (cursor) {
300
- params.cursor = cursor;
301
- }
302
-
303
- const result = decodeToolsListResult(await connection.transport.request<unknown>("tools/list", params, options));
304
- allTools.push(...result.tools);
305
- cursor = result.nextCursor;
306
- } while (cursor);
329
+ await collectPaginated(connection, options, "tools/list", "tools", allTools, decodeToolsListResult);
307
330
 
308
331
  // Cache tools
309
332
  connection.tools = allTools;
@@ -362,18 +385,7 @@ export async function listResources(
362
385
  }
363
386
 
364
387
  const allResources: MCPResource[] = [];
365
- let cursor: string | undefined;
366
-
367
- do {
368
- const params: Record<string, unknown> = {};
369
- if (cursor) {
370
- params.cursor = cursor;
371
- }
372
-
373
- const result = await connection.transport.request<MCPResourcesListResult>("resources/list", params, options);
374
- allResources.push(...result.resources);
375
- cursor = result.nextCursor;
376
- } while (cursor);
388
+ await collectPaginated(connection, options, "resources/list", "resources", allResources);
377
389
 
378
390
  connection.resources = allResources;
379
391
  return allResources;
@@ -395,22 +407,7 @@ export async function listResourceTemplates(
395
407
  }
396
408
 
397
409
  const allTemplates: MCPResourceTemplate[] = [];
398
- let cursor: string | undefined;
399
-
400
- do {
401
- const params: Record<string, unknown> = {};
402
- if (cursor) {
403
- params.cursor = cursor;
404
- }
405
-
406
- const result = await connection.transport.request<MCPResourceTemplatesListResult>(
407
- "resources/templates/list",
408
- params,
409
- options,
410
- );
411
- allTemplates.push(...result.resourceTemplates);
412
- cursor = result.nextCursor;
413
- } while (cursor);
410
+ await collectPaginated(connection, options, "resources/templates/list", "resourceTemplates", allTemplates);
414
411
 
415
412
  connection.resourceTemplates = allTemplates;
416
413
  return allTemplates;
@@ -514,18 +511,7 @@ export async function listPrompts(
514
511
  }
515
512
 
516
513
  const allPrompts: MCPPrompt[] = [];
517
- let cursor: string | undefined;
518
-
519
- do {
520
- const params: Record<string, unknown> = {};
521
- if (cursor) {
522
- params.cursor = cursor;
523
- }
524
-
525
- const result = await connection.transport.request<MCPPromptsListResult>("prompts/list", params, options);
526
- allPrompts.push(...result.prompts);
527
- cursor = result.nextCursor;
528
- } while (cursor);
514
+ await collectPaginated(connection, options, "prompts/list", "prompts", allPrompts);
529
515
 
530
516
  connection.prompts = allPrompts;
531
517
  return allPrompts;
@@ -0,0 +1,71 @@
1
+ export const MCP_MAX_CONTENT_BYTES = 16 * 1024 * 1024;
2
+ export const MCP_MAX_ERROR_BYTES = 16 * 1024;
3
+ export const MCP_MAX_SSE_REQUEST_MESSAGES = 1024;
4
+ export const MCP_MAX_SSE_BATCH_MESSAGES = 1024;
5
+ export const MCP_HTTP_TIMEOUT_MS = 30_000;
6
+
7
+ function ignoreCancellation(cancel: () => Promise<unknown>): void {
8
+ try {
9
+ void cancel().catch(() => {});
10
+ } catch {}
11
+ }
12
+
13
+ export function cancelMCPStream(stream: ReadableStream<Uint8Array> | null): void {
14
+ if (stream) ignoreCancellation(() => stream.cancel());
15
+ }
16
+
17
+ type ByteReader = Pick<ReadableStreamDefaultReader<Uint8Array>, "read">;
18
+
19
+ async function readChunk(reader: ByteReader, signal?: AbortSignal) {
20
+ if (!signal) return reader.read();
21
+ signal.throwIfAborted();
22
+ const aborted = Promise.withResolvers<never>();
23
+ const onAbort = () => aborted.reject(signal.reason);
24
+ signal.addEventListener("abort", onAbort, { once: true });
25
+ try {
26
+ return await Promise.race([reader.read(), aborted.promise]);
27
+ } finally {
28
+ signal.removeEventListener("abort", onAbort);
29
+ }
30
+ }
31
+
32
+ export async function readMCPResponseText(response: Response, limit: number, truncate = false, signal?: AbortSignal) {
33
+ const declared = response.headers.get("Content-Length");
34
+ if (!truncate && declared && /^\d+$/.test(declared) && Number(declared) > limit) {
35
+ cancelMCPStream(response.body);
36
+ throw new Error("MCP response exceeds size limit");
37
+ }
38
+ if (!response.body) return "";
39
+ const reader = response.body.getReader();
40
+ let buffer = new Uint8Array(Math.min(limit, 64 * 1024));
41
+ let total = 0;
42
+ try {
43
+ while (true) {
44
+ const { value, done } = await readChunk(reader, signal);
45
+ if (done) break;
46
+ if (value.length === 0) continue;
47
+ const remaining = limit - total;
48
+ if (value.length > remaining) {
49
+ if (truncate && remaining > 0) buffer.set(value.subarray(0, remaining), total);
50
+ ignoreCancellation(() => reader.cancel());
51
+ if (!truncate) throw new Error("MCP response exceeds size limit");
52
+ return `${new TextDecoder().decode(buffer)}\u2026`;
53
+ }
54
+ if (total + value.length > buffer.length) {
55
+ const grown = new Uint8Array(Math.min(limit, Math.max(total + value.length, buffer.length * 2)));
56
+ grown.set(buffer);
57
+ buffer = grown;
58
+ }
59
+ buffer.set(value, total);
60
+ total += value.length;
61
+ }
62
+ return new TextDecoder().decode(buffer.subarray(0, total));
63
+ } catch (error) {
64
+ ignoreCancellation(() => reader.cancel());
65
+ throw error;
66
+ } finally {
67
+ try {
68
+ reader.releaseLock();
69
+ } catch {}
70
+ }
71
+ }
@@ -4,6 +4,8 @@
4
4
  * Lightweight utilities for calling MCP servers directly via HTTP
5
5
  * without maintaining persistent connections.
6
6
  */
7
+ // biome-ignore assist/source/organizeImports: Keep independent MCP security imports on separate merge anchors.
8
+ import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
7
9
  import { logger } from "@gajae-code/utils";
8
10
 
9
11
  /** Parse SSE response format (lines starting with "data: ") */
@@ -25,6 +27,15 @@ export function parseSSE(text: string): unknown {
25
27
  }
26
28
  }
27
29
 
30
+ async function translateMCPTimeout<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
31
+ try {
32
+ return await operation;
33
+ } catch (error) {
34
+ if (signal.aborted && error === signal.reason) throw new Error("MCP request timed out");
35
+ throw error;
36
+ }
37
+ }
38
+
28
39
  /** JSON-RPC 2.0 response structure */
29
40
  export interface JsonRpcResponse<T = unknown> {
30
41
  jsonrpc: "2.0";
@@ -57,22 +68,25 @@ export async function callMCP<T = unknown>(
57
68
  params: params ?? {},
58
69
  };
59
70
 
60
- const response = await fetch(url, {
61
- method: "POST",
62
- headers: {
63
- "Content-Type": "application/json",
64
- Accept: "application/json, text/event-stream",
65
- },
66
- body: JSON.stringify(body),
67
- });
71
+ const signal = AbortSignal.timeout(MCP_HTTP_TIMEOUT_MS);
72
+ const response = await translateMCPTimeout(
73
+ fetch(url, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
76
+ body: JSON.stringify(body),
77
+ signal,
78
+ }),
79
+ signal,
80
+ );
68
81
 
69
82
  if (!response.ok) {
83
+ cancelMCPStream(response.body);
70
84
  const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
71
85
  logger.error(errorMsg, { url, method, params });
72
86
  throw new Error(errorMsg);
73
87
  }
74
88
 
75
- const text = await response.text();
89
+ const text = await translateMCPTimeout(readMCPResponseText(response, MCP_MAX_CONTENT_BYTES, false, signal), signal);
76
90
  const result = parseSSE(text) as JsonRpcResponse<T> | null;
77
91
 
78
92
  if (!result) {
@@ -6,12 +6,12 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import * as url from "node:url";
9
- import type { TSchema } from "@gajae-code/ai";
9
+ import { isCanonicalMCPOAuthBinding, resolveMCPOAuthResourceOrigin, type TSchema } from "@gajae-code/ai";
10
10
  import { logger } from "@gajae-code/utils";
11
11
  import type { SourceMeta } from "../capability/types";
12
12
  import * as configValue from "../config/resolve-config-value";
13
13
  import type { CustomTool } from "../extensibility/custom-tools/types";
14
- import type { AuthStorage } from "../session/auth-storage";
14
+ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
15
15
  import {
16
16
  connectToServer,
17
17
  disconnectServer,
@@ -27,7 +27,6 @@ import {
27
27
  unsubscribeFromResources,
28
28
  } from "./client";
29
29
  import { loadAllMCPConfigs, validateServerConfig } from "./config";
30
- import { refreshMCPOAuthToken } from "./oauth-flow";
31
30
  import type { MCPToolDetails } from "./tool-bridge";
32
31
  import { DeferredMCPTool, MCPTool } from "./tool-bridge";
33
32
  import type { MCPToolCache } from "./tool-cache";
@@ -1469,64 +1468,63 @@ export class MCPManager {
1469
1468
  let resolved: MCPServerConfig = { ...config };
1470
1469
 
1471
1470
  const auth = config.auth;
1472
- if (auth?.type === "oauth" && auth.credentialId && this.#authStorage) {
1471
+ if (auth?.type === "oauth") {
1472
+ if (!auth.credentialId || !this.#authStorage || (config.type !== "http" && config.type !== "sse")) {
1473
+ throw new MCPExpectedFailure();
1474
+ }
1473
1475
  const credentialId = auth.credentialId;
1474
1476
  let credential = this.#authStorage.get(credentialId);
1475
- if (credential?.type === "oauth") {
1476
- // Proactive refresh: 5-minute buffer before expiry
1477
- // Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
1478
- const REFRESH_BUFFER_MS = 5 * 60_000;
1479
- const shouldRefresh =
1480
- forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1481
- let refreshedCredential:
1482
- | {
1483
- type: "oauth";
1484
- access: string;
1485
- refresh: string;
1486
- expires: number;
1487
- }
1488
- | undefined;
1489
- if (shouldRefresh && credential.refresh && auth.tokenUrl) {
1490
- try {
1491
- const refreshed = await refreshMCPOAuthToken(
1492
- auth.tokenUrl,
1493
- credential.refresh,
1494
- auth.clientId,
1495
- auth.clientSecret,
1496
- );
1497
- refreshedCredential = { type: "oauth", ...refreshed };
1498
- } catch (error) {
1499
- if (!(error instanceof MCPExpectedFailure)) throw error;
1500
- if (this.#toolsOnly) {
1501
- logger.debug("MCP OAuth refresh failed");
1502
- } else {
1503
- logger.warn("MCP OAuth refresh failed, using existing token");
1504
- }
1477
+ const binding = credential?.type === "oauth" ? credential.mcpBinding : undefined;
1478
+ const endpointOrigin = resolveMCPOAuthResourceOrigin(config.url);
1479
+ if (
1480
+ credential?.type !== "oauth" ||
1481
+ !binding ||
1482
+ !isCanonicalMCPOAuthBinding(binding) ||
1483
+ binding.resourceOrigin !== endpointOrigin ||
1484
+ (auth.tokenUrl !== undefined && auth.tokenUrl !== binding.tokenEndpoint)
1485
+ ) {
1486
+ throw new MCPExpectedFailure();
1487
+ }
1488
+
1489
+ // Proactive refresh: 5-minute buffer before expiry
1490
+ // Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
1491
+ const REFRESH_BUFFER_MS = 5 * 60_000;
1492
+ const shouldRefresh =
1493
+ forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1494
+ if (shouldRefresh && credential.refresh) {
1495
+ let refreshedCredential: OAuthCredential | undefined;
1496
+ try {
1497
+ refreshedCredential = await this.#authStorage.forceRefreshOAuthCredential(credentialId, credential, {
1498
+ clientId: auth.clientId,
1499
+ clientSecret: auth.clientSecret,
1500
+ });
1501
+ } catch {
1502
+ if (forceRefresh) throw new MCPExpectedFailure();
1503
+ if (this.#toolsOnly) {
1504
+ logger.debug("MCP OAuth refresh failed");
1505
+ } else {
1506
+ logger.warn("MCP OAuth refresh failed, using existing token");
1505
1507
  }
1506
1508
  }
1507
1509
  if (refreshedCredential) {
1508
- await this.#authStorage.set(credentialId, refreshedCredential);
1510
+ const refreshedBinding = refreshedCredential.mcpBinding;
1511
+ if (
1512
+ !refreshedBinding ||
1513
+ refreshedBinding.resourceOrigin !== binding.resourceOrigin ||
1514
+ refreshedBinding.tokenEndpoint !== binding.tokenEndpoint
1515
+ ) {
1516
+ throw new MCPExpectedFailure();
1517
+ }
1509
1518
  credential = refreshedCredential;
1510
1519
  }
1511
-
1512
- if (resolved.type === "http" || resolved.type === "sse") {
1513
- resolved = {
1514
- ...resolved,
1515
- headers: {
1516
- ...resolved.headers,
1517
- Authorization: `Bearer ${credential.access}`,
1518
- },
1519
- };
1520
- } else {
1521
- resolved = {
1522
- ...resolved,
1523
- env: {
1524
- ...resolved.env,
1525
- OAUTH_ACCESS_TOKEN: credential.access,
1526
- },
1527
- };
1528
- }
1529
1520
  }
1521
+ resolved = {
1522
+ ...config,
1523
+ headers: {
1524
+ ...config.headers,
1525
+ Authorization: `Bearer ${credential.access}`,
1526
+ },
1527
+ };
1530
1528
  }
1531
1529
  if (this.#toolsOnly && resolved.type === "stdio") {
1532
1530
  resolved = { ...resolved, noInheritEnv: true };
@@ -8,29 +8,10 @@
8
8
  import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callback-server";
9
9
  import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
10
10
  import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
11
- import { MCPExpectedFailure } from "./types";
12
11
 
13
12
  const DEFAULT_PORT = 3000;
14
13
  const CALLBACK_PATH = "/callback";
15
14
  const CALLBACK_BIND_HOSTNAME = "127.0.0.1";
16
- type RefreshTokenResponse = {
17
- access_token: string;
18
- refresh_token?: string;
19
- expires_in?: number;
20
- };
21
-
22
- function isRefreshTokenResponse(value: unknown): value is RefreshTokenResponse {
23
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
24
- const payload = value as Record<string, unknown>;
25
- return (
26
- typeof payload.access_token === "string" &&
27
- payload.access_token.length > 0 &&
28
- (payload.refresh_token === undefined || typeof payload.refresh_token === "string") &&
29
- (payload.expires_in === undefined ||
30
- (typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in >= 0))
31
- );
32
- }
33
-
34
15
  function isLoopbackHostname(hostname: string): boolean {
35
16
  return hostname === "localhost" || hostname === "127.0.0.1";
36
17
  }
@@ -384,56 +365,3 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
384
365
  }
385
366
  }
386
367
  }
387
-
388
- /**
389
- * Refresh an MCP OAuth token using the standard refresh_token grant.
390
- * Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
391
- */
392
- export async function refreshMCPOAuthToken(
393
- tokenUrl: string,
394
- refreshToken: string,
395
- clientId?: string,
396
- clientSecret?: string,
397
- ): Promise<OAuthCredentials> {
398
- const params = new URLSearchParams({
399
- grant_type: "refresh_token",
400
- refresh_token: refreshToken,
401
- });
402
- if (clientId) params.set("client_id", clientId);
403
- if (clientSecret) params.set("client_secret", clientSecret);
404
-
405
- let response: Response;
406
- try {
407
- response = await fetch(tokenUrl, {
408
- method: "POST",
409
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
410
- body: params.toString(),
411
- });
412
- } catch (error) {
413
- throw new MCPExpectedFailure(error);
414
- }
415
-
416
- if (!response.ok) {
417
- let text: string;
418
- try {
419
- text = await response.text();
420
- } catch (error) {
421
- throw new MCPExpectedFailure(error);
422
- }
423
- throw new MCPExpectedFailure(new Error(`MCP OAuth refresh failed: ${response.status} ${text}`));
424
- }
425
-
426
- let data: unknown;
427
- try {
428
- data = await response.json();
429
- } catch {
430
- throw new MCPExpectedFailure();
431
- }
432
- if (!isRefreshTokenResponse(data)) throw new MCPExpectedFailure();
433
- const expiresIn = data.expires_in ?? 3600;
434
- return {
435
- access: data.access_token,
436
- refresh: data.refresh_token ?? refreshToken,
437
- expires: Date.now() + expiresIn * 1000,
438
- };
439
- }