@gajae-code/coding-agent 0.12.0 → 0.12.1

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 (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
package/src/tools/find.ts CHANGED
@@ -158,6 +158,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
158
158
  const resource = await internalRouter.resolve(rawPattern, {
159
159
  cwd: this.session.cwd,
160
160
  getArtifactsDir: this.session.getArtifactsDir,
161
+ getAuthorizedArtifactsDirs: this.session.getAuthorizedArtifactsDirs,
161
162
  });
162
163
  if (!resource.sourcePath) {
163
164
  throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
@@ -12,7 +12,7 @@ import {
12
12
  URL_PATHS,
13
13
  } from "@gajae-code/ai/providers/openai-codex/constants";
14
14
  import {
15
- $env,
15
+ $credentialEnv,
16
16
  isEnoent,
17
17
  parseImageMetadata,
18
18
  prompt,
@@ -794,6 +794,23 @@ async function findAlibabaImageCredentials(
794
794
  return { provider: "alibaba", apiKey };
795
795
  }
796
796
 
797
+ /**
798
+ * Google image-provider key fallback, from trusted environment sources only.
799
+ *
800
+ * `$env` merges the caller's `cwd/.env`, so reading the key there would let
801
+ * repository content supply the credential these image requests authenticate
802
+ * with. Provider credentials resolve from the launching shell plus GJC/user-owned
803
+ * `.env` files, never the project `.env`.
804
+ */
805
+ function googleImageApiKeyFromEnv(): string | undefined {
806
+ return $credentialEnv("GOOGLE_API_KEY");
807
+ }
808
+
809
+ /** Test seam: the Google image key fallback as resolved from trusted env. */
810
+ export function googleImageApiKeyFromEnvForTest(): string | undefined {
811
+ return googleImageApiKeyFromEnv();
812
+ }
813
+
797
814
  async function findImageApiKey(
798
815
  modelRegistry?: ModelRegistry,
799
816
  activeModel?: Model,
@@ -827,7 +844,7 @@ async function findImageApiKey(
827
844
  if (config.provider === "gemini") {
828
845
  const geminiKey = getEnvApiKey("google");
829
846
  if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
830
- const googleKey = $env.GOOGLE_API_KEY;
847
+ const googleKey = googleImageApiKeyFromEnv();
831
848
  return googleKey ? { provider: "gemini", apiKey: googleKey } : null;
832
849
  }
833
850
  if (config.provider === "openrouter") {
@@ -850,7 +867,7 @@ async function findImageApiKey(
850
867
  } else if (preferredImageProvider === "gemini") {
851
868
  const geminiKey = getEnvApiKey("google");
852
869
  if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
853
- const googleKey = $env.GOOGLE_API_KEY;
870
+ const googleKey = googleImageApiKeyFromEnv();
854
871
  return googleKey ? { provider: "gemini", apiKey: googleKey } : null;
855
872
  } else if (preferredImageProvider === "openrouter") {
856
873
  const openRouterKey = getEnvApiKey("openrouter");
@@ -874,7 +891,7 @@ async function findImageApiKey(
874
891
  const geminiKey = getEnvApiKey("google");
875
892
  if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
876
893
 
877
- const googleKey = $env.GOOGLE_API_KEY;
894
+ const googleKey = googleImageApiKeyFromEnv();
878
895
  if (googleKey) return { provider: "gemini", apiKey: googleKey };
879
896
 
880
897
  const alibaba = await findAlibabaImageCredentials(modelRegistry, sessionId);
@@ -1106,7 +1123,9 @@ function getOpenAIBaseUrl(model: Model, authCredentialType?: "api_key" | "oauth"
1106
1123
  return (model.baseUrl || CODEX_BASE_URL).replace(/\/+$/, "");
1107
1124
  }
1108
1125
  if (authCredentialType === "oauth") return DEFAULT_OPENAI_BASE_URL;
1109
- const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
1126
+ // Trusted sources only: this base URL becomes the endpoint for authenticated
1127
+ // image requests, and `$env` merges the caller's `cwd/.env`.
1128
+ const envBaseUrl = $credentialEnv("OPENAI_BASE_URL");
1110
1129
  const configuredBaseUrl = model.baseUrl?.trim();
1111
1130
  if (envBaseUrl && (!configuredBaseUrl || configuredBaseUrl.toLowerCase().includes("api.openai.com"))) {
1112
1131
  return envBaseUrl.replace(/\/+$/, "");
@@ -1114,6 +1133,11 @@ function getOpenAIBaseUrl(model: Model, authCredentialType?: "api_key" | "oauth"
1114
1133
  return (configuredBaseUrl || envBaseUrl || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
1115
1134
  }
1116
1135
 
1136
+ /** Test seam: the image-request base URL as resolved from trusted env. */
1137
+ export function getOpenAIImageBaseUrlForTest(model: Model, authCredentialType?: "api_key" | "oauth"): string {
1138
+ return getOpenAIBaseUrl(model, authCredentialType);
1139
+ }
1140
+
1117
1141
  function getOpenAIResponsesUrl(model: Model, authCredentialType?: "api_key" | "oauth"): string {
1118
1142
  const baseUrl = getOpenAIBaseUrl(model, authCredentialType);
1119
1143
  if (model.api !== "openai-codex-responses" && model.provider !== "openai-codex") {
@@ -292,6 +292,8 @@ export interface ToolSession {
292
292
  strictToolNames?: boolean;
293
293
  /** Get artifacts directory for artifact:// URLs */
294
294
  getArtifactsDir?: () => string | null;
295
+ /** Additional artifacts directories explicitly authorized for this session's tree (parent/child/sibling reads). Derived only from the explicitly adopted/shared `ArtifactManager`; never enumerates unrelated live sessions. */
296
+ getAuthorizedArtifactsDirs?: () => readonly string[];
295
297
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
296
298
  getArtifactManager?: () => ArtifactManager | null;
297
299
  /** Allocate a new artifact path and ID for session-scoped truncated output. */
@@ -314,6 +316,8 @@ export interface ToolSession {
314
316
  settings: Settings;
315
317
  /** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
316
318
  serviceTier?: ServiceTier;
319
+ /** Whether the effective subagent tier grants fast mode for a resolved provider. */
320
+ isFastForSubagentProvider?: (provider?: string) => boolean;
317
321
  /** Plan mode state (if active) */
318
322
  getPlanModeState?: () => PlanModeState | undefined;
319
323
  /** Goal mode state (if active or paused) */
@@ -53,8 +53,12 @@ export interface TruncationMeta {
53
53
  elidedBytes?: number;
54
54
  /** Lines elided from the middle. */
55
55
  elidedLines?: number;
56
- /** Artifact ID if full output was saved */
56
+ /** Artifact ID when output was persisted; completeness is tracked separately below. */
57
57
  artifactId?: string;
58
+ /** Bytes omitted from an artifact after its hard storage cap was reached. */
59
+ artifactTruncatedBytes?: number;
60
+ /** Bounded diagnostic when artifact writer creation, write, or finalization failed. */
61
+ artifactFailureDiagnostic?: string;
58
62
  /** Next offset for pagination (head truncation only) */
59
63
  nextOffset?: number;
60
64
  }
@@ -323,13 +327,30 @@ export class OutputMetaBuilder {
323
327
  return this;
324
328
  }
325
329
 
326
- /** Add truncation info from OutputSummary. No-op if not truncated. */
330
+ /** Add truncation info from OutputSummary. No-op if not truncated or artifact evidence is absent. */
327
331
  truncationFromSummary(summary: OutputSummary, options: TruncationSummaryOptions): this {
328
- if (!summary.truncated) return this;
332
+ const artifactFailureDiagnostic = summary.artifactFailureDiagnostic;
333
+ const artifactTruncatedBytes =
334
+ summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
335
+ ? summary.artifactTruncatedBytes
336
+ : undefined;
337
+ const hasArtifactEvidence =
338
+ summary.artifactId !== undefined ||
339
+ artifactFailureDiagnostic !== undefined ||
340
+ artifactTruncatedBytes !== undefined;
341
+ if (!summary.truncated && !hasArtifactEvidence) return this;
329
342
 
330
343
  const { direction, startLine = 1, totalFileLines, noticeOwner } = options;
331
344
  const totalLines = totalFileLines ?? summary.totalLines;
332
- const owner = noticeOwner !== undefined ? { noticeOwner } : {};
345
+ const bodyHasArtifact =
346
+ summary.artifactId !== undefined && summary.output.includes(`artifact://${summary.artifactId}`);
347
+ const bodyOwnsArtifact =
348
+ bodyHasArtifact &&
349
+ (artifactTruncatedBytes === undefined ||
350
+ (summary.artifactId !== undefined &&
351
+ summary.output.includes(formatArtifactReference(summary.artifactId, artifactTruncatedBytes))));
352
+ const owner =
353
+ noticeOwner !== undefined ? { noticeOwner } : bodyOwnsArtifact ? { noticeOwner: "body" as const } : {};
333
354
 
334
355
  // Middle elision: the sink retained head + tail with an elision marker.
335
356
  if (summary.elidedBytes != null && summary.elidedBytes > 0) {
@@ -350,6 +371,8 @@ export class OutputMetaBuilder {
350
371
  elidedBytes: summary.elidedBytes,
351
372
  elidedLines,
352
373
  artifactId: summary.artifactId,
374
+ artifactTruncatedBytes,
375
+ artifactFailureDiagnostic,
353
376
  };
354
377
  return this;
355
378
  }
@@ -382,6 +405,8 @@ export class OutputMetaBuilder {
382
405
  outputBytes: summary.outputBytes,
383
406
  shownRange: { start: shownStart, end: shownEnd },
384
407
  artifactId: summary.artifactId,
408
+ artifactTruncatedBytes,
409
+ artifactFailureDiagnostic,
385
410
  nextOffset: direction === "head" ? shownEnd + 1 : undefined,
386
411
  };
387
412
 
@@ -524,6 +549,37 @@ export function formatFullOutputReference(artifactId: string): string {
524
549
  return `Read artifact://${artifactId} for full output`;
525
550
  }
526
551
 
552
+ /**
553
+ * Format an artifact reference without claiming completeness when storage was hard-capped.
554
+ */
555
+ export function formatArtifactReference(artifactId: string, artifactTruncatedBytes?: number): string {
556
+ if (artifactTruncatedBytes != null && artifactTruncatedBytes > 0) {
557
+ return `Read artifact://${artifactId} for retained output (at least ${formatBytes(artifactTruncatedBytes)} omitted by the artifact storage cap)`;
558
+ }
559
+ return formatFullOutputReference(artifactId);
560
+ }
561
+
562
+ function formatTruncationArtifactNotice(truncation: TruncationMeta): string {
563
+ const reference = truncation.artifactId
564
+ ? formatArtifactReference(truncation.artifactId, truncation.artifactTruncatedBytes)
565
+ : undefined;
566
+ if (truncation.artifactFailureDiagnostic) {
567
+ const failure = `Artifact storage failed: ${truncation.artifactFailureDiagnostic}`;
568
+ if (reference) return `${reference}; ${failure}`;
569
+ return failure;
570
+ }
571
+ if (reference) return reference;
572
+ return `Artifact storage omitted at least ${formatBytes(truncation.artifactTruncatedBytes ?? 0)}; no artifact reference is available`;
573
+ }
574
+
575
+ function hasArtifactNotice(truncation: TruncationMeta): boolean {
576
+ return (
577
+ truncation.artifactId != null ||
578
+ (truncation.artifactTruncatedBytes ?? 0) > 0 ||
579
+ truncation.artifactFailureDiagnostic != null
580
+ );
581
+ }
582
+
527
583
  function formatTruncationRangeTotal(truncation: TruncationMeta): string {
528
584
  return truncation.rangeBase === "window"
529
585
  ? `the selected ${truncation.totalLines}-line range`
@@ -537,8 +593,8 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
537
593
  if (truncation.partialLine.sourceBytes > truncation.partialLine.bytes) {
538
594
  notice += ` (line is ${formatBytes(truncation.partialLine.sourceBytes)})`;
539
595
  }
540
- if (truncation.artifactId != null) {
541
- notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
596
+ if (hasArtifactNotice(truncation)) {
597
+ notice += `. ${formatTruncationArtifactNotice(truncation)}`;
542
598
  }
543
599
  return notice;
544
600
  }
@@ -553,13 +609,15 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
553
609
  const elidedLines = truncation.elidedLines ?? Math.max(0, totalLines - truncation.outputLines);
554
610
  const headPart = head ? `lines ${head.start}-${head.end}` : "";
555
611
  const tailPart = tail ? `${tail.start}-${tail.end}` : "";
556
- if (headPart && tailPart) {
612
+ if (head && tail && elidedLines === 0 && head.start === tail.start && head.end === tail.end) {
613
+ notice = `Showing head and tail of line ${head.start} of ${rangeTotal}; ${formatBytes(elidedBytes)} middle bytes elided`;
614
+ } else if (headPart && tailPart) {
557
615
  notice = `Showing ${headPart} and ${tailPart} of ${rangeTotal}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`;
558
616
  } else {
559
- notice = `Showing ${truncation.outputLines} of ${rangeTotal}${truncation.rangeBase === "window" ? "" : " lines"}; middle elided`;
617
+ notice = `Showing ${truncation.outputLines} of ${rangeTotal}${truncation.rangeBase === "window" ? "" : " lines"}; ${formatBytes(elidedBytes)} middle bytes elided`;
560
618
  }
561
- if (truncation.artifactId != null) {
562
- notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
619
+ if (hasArtifactNotice(truncation)) {
620
+ notice += `. ${formatTruncationArtifactNotice(truncation)}`;
563
621
  }
564
622
  return notice;
565
623
  }
@@ -580,8 +638,8 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
580
638
  notice += `. Use :${truncation.nextOffset} to continue`;
581
639
  }
582
640
 
583
- if (truncation.artifactId != null) {
584
- notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
641
+ if (hasArtifactNotice(truncation)) {
642
+ notice += `. ${formatTruncationArtifactNotice(truncation)}`;
585
643
  }
586
644
 
587
645
  return notice;
@@ -642,8 +700,25 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
642
700
  * Returns null if no truncation metadata present.
643
701
  */
644
702
  export function formatStyledTruncationWarning(meta: OutputMeta | undefined, theme: Theme): string | null {
645
- if (!meta?.truncation) return null;
646
- const message = formatTruncationMetaNotice(meta.truncation);
703
+ const truncation = meta?.truncation;
704
+ if (!truncation) return null;
705
+ if (
706
+ truncation.noticeOwner === "body" &&
707
+ truncation.totalBytes === truncation.outputBytes &&
708
+ truncation.totalLines === truncation.outputLines
709
+ ) {
710
+ return null;
711
+ }
712
+ const warningMeta =
713
+ truncation.noticeOwner === "body"
714
+ ? {
715
+ ...truncation,
716
+ artifactId: undefined,
717
+ artifactTruncatedBytes: undefined,
718
+ artifactFailureDiagnostic: undefined,
719
+ }
720
+ : truncation;
721
+ const message = formatTruncationMetaNotice(warningMeta);
647
722
  return theme.fg("warning", wrapBrackets(message, theme));
648
723
  }
649
724
 
@@ -735,6 +810,25 @@ export function resolveOutputSinkHeadBytes(s: Settings | undefined): number {
735
810
  return getSpillConfig(s).headBytes;
736
811
  }
737
812
 
813
+ export const BASH_DEFAULT_OUTPUT_TAIL_BYTES = 1024;
814
+
815
+ /**
816
+ * Bash uses a deliberately small tail window to nudge callers toward focused
817
+ * commands and dedicated search tools. An explicitly configured shared tail
818
+ * budget still wins.
819
+ */
820
+ export function resolveBashOutputSinkTailBytes(s: Settings): number {
821
+ return s.has("tools.artifactTailBytes") ? s.get("tools.artifactTailBytes") * 1024 : BASH_DEFAULT_OUTPUT_TAIL_BYTES;
822
+ }
823
+ /**
824
+ * Bash keeps only the tail unless the user explicitly opts into the shared
825
+ * head-retention setting. Schema defaults still apply to other streaming
826
+ * tools without silently turning Bash back into middle-elision mode.
827
+ */
828
+ export function resolveBashOutputSinkHeadBytes(s: Settings): number {
829
+ return s.has("tools.artifactHeadBytes") ? resolveOutputSinkHeadBytes(s) : 0;
830
+ }
831
+
738
832
  /**
739
833
  * Resolve the per-line column cap from session settings. Shared by streaming
740
834
  * executors (bash/python/ssh/eval via OutputSink) and the `read` tool's
@@ -637,6 +637,7 @@ export interface ToolScopeOptions {
637
637
  rawPaths: string[];
638
638
  cwd: string;
639
639
  getArtifactsDir?: () => string | null;
640
+ getAuthorizedArtifactsDirs?: () => readonly string[];
640
641
  /** Verb used in the "Cannot {action} internal URL without a backing file: …" message. */
641
642
  internalUrlAction: string;
642
643
  /** Collect absolute paths flagged immutable by their internal-URL handler. */
@@ -667,7 +668,7 @@ export interface ToolScopeResolution {
667
668
  * 5. stat the resolved base path so callers can branch on directory vs file scope.
668
669
  */
669
670
  export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {
670
- const { rawPaths: inputs, cwd, internalUrlAction, getArtifactsDir } = opts;
671
+ const { rawPaths: inputs, cwd, internalUrlAction, getArtifactsDir, getAuthorizedArtifactsDirs } = opts;
671
672
  const rawPaths = inputs.map(normalizePathLikeInput);
672
673
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
673
674
  throw new ToolError("`paths` must contain non-empty paths or globs");
@@ -683,7 +684,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
683
684
  if (hasGlobPathChars(rawPath)) {
684
685
  throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
685
686
  }
686
- const resource = await internalRouter.resolve(rawPath, { cwd, getArtifactsDir });
687
+ const resource = await internalRouter.resolve(rawPath, { cwd, getArtifactsDir, getAuthorizedArtifactsDirs });
687
688
  if (!resource.sourcePath) {
688
689
  throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
689
690
  }
package/src/tools/read.ts CHANGED
@@ -1419,6 +1419,21 @@ interface ResolvedSqliteReadPath {
1419
1419
  * Reads files with support for images, converted documents (via markit), and text.
1420
1420
  * Directories return a formatted listing with modification times.
1421
1421
  */
1422
+ /**
1423
+ * A client-authority denial is a decision, not a transport failure: falling back to
1424
+ * disk would bypass the permission the client just refused. Availability failures
1425
+ * still fall back so an unreachable bridge cannot break local reads.
1426
+ */
1427
+ function isClientAuthorityDenial(error: unknown): boolean {
1428
+ const code =
1429
+ typeof error === "object" && error !== null && "code" in error ? (error as { code?: unknown }).code : undefined;
1430
+ // ACP clients surface refusals as an application error; -32001 is the reserved
1431
+ // client-authority denial code and -32603 covers hosts without a dedicated code.
1432
+ if (code === "permission_denied" || code === "forbidden" || code === -32001 || code === -32603) return true;
1433
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
1434
+ return /permission denied|not permitted|access denied|forbidden/i.test(message);
1435
+ }
1436
+
1422
1437
  export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1423
1438
  readonly name = "read";
1424
1439
  readonly label = "Read";
@@ -1991,6 +2006,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1991
2006
  }
1992
2007
  return { outputText: "", contentLineCount: 0, columnTruncated: 0, bridgeResult };
1993
2008
  } catch (error) {
2009
+ if (isClientAuthorityDenial(error)) throw error;
1994
2010
  logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
1995
2011
  }
1996
2012
  }
@@ -2893,6 +2909,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2893
2909
  }
2894
2910
  return bridgeResult;
2895
2911
  } catch (error) {
2912
+ if (isClientAuthorityDenial(error)) throw error;
2896
2913
  logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
2897
2914
  }
2898
2915
  }
@@ -3396,6 +3413,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
3396
3413
  const resource = await internalRouter.resolve(url, {
3397
3414
  cwd: this.session.cwd,
3398
3415
  getArtifactsDir: this.session.getArtifactsDir,
3416
+ getAuthorizedArtifactsDirs: this.session.getAuthorizedArtifactsDirs,
3399
3417
  settings: this.session.settings,
3400
3418
  signal,
3401
3419
  });
@@ -188,15 +188,13 @@ export interface SubmitReviewDetails {
188
188
  // Re-export types for external use
189
189
  export type { ReportFindingDetails };
190
190
  /**
191
- * Coerce a tool-side `ReportFindingDetails` into the cross-boundary
192
- * `ReviewFinding` shape consumed by the reviewer agent's JTD output schema.
191
+ * Convert tool-side `ReportFindingDetails` into the numeric-priority
192
+ * `ReviewFinding` shape used by caller-owned reviewer payloads.
193
193
  *
194
194
  * The `report_finding` tool exposes `priority` as a string enum (`"P0".."P3"`)
195
- * for ergonomics, but the bundled reviewer schema (and every custom review
196
- * agent that mirrors it) declares `priority: number`. Without this coercion
197
- * the auto-populated `findings[]` fails JTD validation and every review run
198
- * that surfaces a finding is rejected with `findings.0.priority: expected
199
- * number, received string`.
195
+ * for ergonomics, while reviewer schemas may explicitly declare numeric
196
+ * priorities. This pure conversion does not add findings to completion data;
197
+ * callers that include `findings` own both that field and its output schema.
200
198
  */
201
199
  export function toReviewFinding(details: ReportFindingDetails): ReviewFinding {
202
200
  return {
@@ -287,6 +287,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
287
287
  rawPaths: resolvedPaths,
288
288
  cwd: this.session.cwd,
289
289
  getArtifactsDir: this.session.getArtifactsDir,
290
+ getAuthorizedArtifactsDirs: this.session.getAuthorizedArtifactsDirs,
290
291
  internalUrlAction: "search",
291
292
  trackImmutableSources: true,
292
293
  surfaceExactFilePaths: true,
@@ -192,6 +192,9 @@ function renderSubagentSnapshotBody(
192
192
  }
193
193
  if (snapshot.description) lines.push(` ${theme.fg("dim", `Description: ${snapshot.description}`)}`);
194
194
  if (snapshot.outputRef) lines.push(` ${theme.fg("dim", `Output: ${snapshot.outputRef}`)}`);
195
+ if (snapshot.setupFailureSummary) {
196
+ lines.push(` ${theme.fg("error", `Setup failure: ${snapshot.setupFailureSummary}`)}`);
197
+ }
195
198
  if (snapshot.assignment) {
196
199
  lines.push(` ${theme.fg("dim", "Assignment:")}`);
197
200
  for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
@@ -312,8 +315,9 @@ export const subagentToolRenderer = {
312
315
  );
313
316
  }
314
317
 
315
- snapshotSignatures ??= subagents.map(snapshot =>
316
- subagentAwaitRenderedStateSignature([snapshot], result.details),
318
+ snapshotSignatures ??= subagents.map(
319
+ snapshot =>
320
+ `${subagentAwaitRenderedStateSignature([snapshot], result.details)}:${snapshot.setupFailureSummary ?? ""}`,
317
321
  );
318
322
  subagents.forEach((snapshot, index) => {
319
323
  // Fresh per-subagent status line (cheap), then a cached or dynamic body.
@@ -65,6 +65,8 @@ export interface SubagentSnapshot {
65
65
  durationMs: number;
66
66
  resultText?: string;
67
67
  errorText?: string;
68
+ /** Safe setup failure cause retained from the executor launch path. */
69
+ setupFailureSummary?: string;
68
70
  resultPreview?: string;
69
71
  outputRef?: string;
70
72
  truncated?: boolean;
@@ -603,6 +605,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
603
605
  }
604
606
  if (snapshot.description) lines.push(`Description: ${snapshot.description}`);
605
607
  if (snapshot.outputRef) lines.push(`Output: ${snapshot.outputRef}`);
608
+ if (snapshot.setupFailureSummary) lines.push(`Setup failure: ${snapshot.setupFailureSummary}`);
606
609
  if (snapshot.assignment) lines.push("Assignment:", "```", snapshot.assignment, "```");
607
610
  if (snapshot.steerMessage) {
608
611
  lines.push(`Steer (${snapshot.steerState ?? "queued"}):`, "```", snapshot.steerMessage, "```");
@@ -731,6 +734,9 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
731
734
  truncated: output.truncated,
732
735
  }
733
736
  : {}),
737
+ ...(job.setupFailureSummary
738
+ ? { setupFailureSummary: sanitizeText(job.setupFailureSummary, RECEIPT_PREVIEW_WIDTH) }
739
+ : {}),
734
740
  ...(outputRef ? { outputRef } : {}),
735
741
  ...(runningTimeoutGuidance ? { guidance: runningTimeoutGuidance } : {}),
736
742
  ...this.#modelFields(record),
@@ -31,8 +31,10 @@ export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, Te
31
31
  readonly summary = "Send a workspace file to Telegram";
32
32
  readonly loadMode = "discoverable";
33
33
  readonly description =
34
- "Send a file from the current workspace to the connected Telegram chat as a document. The path must resolve " +
35
- "(after following symlinks) to a regular file inside the project root; paths outside the workspace are rejected.";
34
+ "Send a file from the current workspace to the connected Telegram chat. Recognized images are converted to " +
35
+ "Telegram-compatible photos when possible, including WebP; other files are sent as documents with their MIME " +
36
+ "type preserved. The path must resolve (after following symlinks) to a regular file inside the project root; " +
37
+ "paths outside the workspace are rejected.";
36
38
  readonly parameters = telegramSendSchema;
37
39
  readonly strict = true;
38
40
 
@@ -109,6 +111,7 @@ export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, Te
109
111
  };
110
112
  }
111
113
  const abs = contained.path;
114
+ const mime = Bun.file(abs).type;
112
115
 
113
116
  const sink = getTelegramFileSink(sessionId);
114
117
  if (!sink) {
@@ -126,7 +129,7 @@ export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, Te
126
129
  };
127
130
  }
128
131
 
129
- const result = await sink({ path: abs, caption: params.caption });
132
+ const result = await sink({ path: abs, caption: params.caption, mime });
130
133
  if (result.ok) {
131
134
  return {
132
135
  content: [{ type: "text", text: `Sent ${path.basename(abs)} to Telegram.` }],
@@ -0,0 +1,39 @@
1
+ const DEFAULT_GH_TIMEOUT_MS = 5_000;
2
+
3
+ export interface GhResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ timedOut?: boolean;
8
+ }
9
+
10
+ export type RunGh = (args: string[], options?: { timeoutMs?: number }) => Promise<GhResult>;
11
+
12
+ /** Run GitHub CLI without allowing it to consume the parent terminal's input. */
13
+ export async function runGhDefault(args: string[], options?: { timeoutMs?: number }): Promise<GhResult> {
14
+ const ghPath = Bun.which("gh");
15
+ if (!ghPath) {
16
+ return { exitCode: -1, stdout: "", stderr: "gh not found", timedOut: false };
17
+ }
18
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS;
19
+ let timedOut = false;
20
+ try {
21
+ const proc = Bun.spawn([ghPath, ...args], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
22
+ const timer = setTimeout(() => {
23
+ timedOut = true;
24
+ proc.kill();
25
+ }, timeoutMs);
26
+ try {
27
+ const [stdout, stderr] = await Promise.all([
28
+ new Response(proc.stdout).text(),
29
+ new Response(proc.stderr).text(),
30
+ ]);
31
+ const exitCode = await proc.exited;
32
+ return { exitCode, stdout, stderr, timedOut };
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ } catch (err) {
37
+ return { exitCode: -1, stdout: "", stderr: err instanceof Error ? err.message : String(err), timedOut };
38
+ }
39
+ }
@@ -6,6 +6,7 @@ export interface ImageResizeOptions {
6
6
  maxBytes?: number;
7
7
  jpegQuality?: number;
8
8
  excludeWebP?: boolean;
9
+ forceReencode?: boolean;
9
10
  }
10
11
 
11
12
  export interface ResizedImage {
@@ -30,6 +31,7 @@ const DEFAULT_OPTIONS: Required<Omit<ImageResizeOptions, "excludeWebP">> = {
30
31
  maxHeight: 1568,
31
32
  maxBytes: DEFAULT_IMAGE_RESIZE_MAX_BYTES,
32
33
  jpegQuality: 80,
34
+ forceReencode: false,
33
35
  };
34
36
 
35
37
  /**
@@ -94,6 +96,7 @@ export async function resizeImageBuffer(
94
96
  originalWidth <= opts.maxWidth &&
95
97
  originalHeight <= opts.maxHeight &&
96
98
  originalSize <= comfortableSize &&
99
+ !opts.forceReencode &&
97
100
  !(opts.excludeWebP && sourceMime === "image/webp")
98
101
  ) {
99
102
  return {
@@ -14,7 +14,7 @@ import {
14
14
  buildAnthropicUrl,
15
15
  stripClaudeToolPrefix,
16
16
  } from "@gajae-code/ai";
17
- import { $env } from "@gajae-code/utils";
17
+ import { $credentialEnv, $env } from "@gajae-code/utils";
18
18
  import type {
19
19
  AnthropicApiResponse,
20
20
  AnthropicCitation,
@@ -275,8 +275,8 @@ export async function searchAnthropic(
275
275
  params: SearchParams | AnthropicSearchParams,
276
276
  _legacyStorage?: unknown,
277
277
  ): Promise<SearchResponse> {
278
- const searchApiKey = $env.ANTHROPIC_SEARCH_API_KEY;
279
- const searchBaseUrl = $env.ANTHROPIC_SEARCH_BASE_URL;
278
+ const searchApiKey = $credentialEnv("ANTHROPIC_SEARCH_API_KEY");
279
+ const searchBaseUrl = $credentialEnv("ANTHROPIC_SEARCH_BASE_URL");
280
280
  let auth: AnthropicAuthConfig | undefined;
281
281
  // When reusing the active model's own credentials (native search over a
282
282
  // proxy), prefer its wire model id and carry its request headers through.
@@ -360,7 +360,7 @@ export class AnthropicProvider extends SearchProvider {
360
360
  readonly label = "Anthropic";
361
361
 
362
362
  isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean {
363
- return Boolean($env.ANTHROPIC_SEARCH_API_KEY) || authStorage.hasAuth("anthropic");
363
+ return Boolean($credentialEnv("ANTHROPIC_SEARCH_API_KEY")) || authStorage.hasAuth("anthropic");
364
364
  }
365
365
 
366
366
  search(params: SearchParams): Promise<SearchResponse> {
@@ -5,7 +5,7 @@
5
5
  * Endpoint: POST https://api.kimi.com/coding/v1/search
6
6
  */
7
7
  import type { AuthStorage } from "@gajae-code/ai";
8
- import { $env } from "@gajae-code/utils";
8
+ import { $credentialEnv } from "@gajae-code/utils";
9
9
 
10
10
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
11
11
  import { SearchProviderError } from "../../../web/search/types";
@@ -55,8 +55,17 @@ function asTrimmed(value: string | undefined): string | undefined {
55
55
  return trimmed.length > 0 ? trimmed : undefined;
56
56
  }
57
57
 
58
+ /** Test seam: the Kimi search base URL as resolved from trusted env. */
59
+ export function resolveKimiSearchBaseUrlForTest(): string {
60
+ return resolveBaseUrl();
61
+ }
62
+
58
63
  function resolveBaseUrl(): string {
59
- return asTrimmed($env.MOONSHOT_SEARCH_BASE_URL) ?? asTrimmed($env.KIMI_SEARCH_BASE_URL) ?? KIMI_SEARCH_URL;
64
+ return (
65
+ asTrimmed($credentialEnv("MOONSHOT_SEARCH_BASE_URL")) ??
66
+ asTrimmed($credentialEnv("KIMI_SEARCH_BASE_URL")) ??
67
+ KIMI_SEARCH_URL
68
+ );
60
69
  }
61
70
 
62
71
  /** Find Kimi search credentials from environment or AuthStorage. */
@@ -65,7 +74,8 @@ async function findApiKey(
65
74
  sessionId: string | undefined,
66
75
  signal: AbortSignal | undefined,
67
76
  ): Promise<string | null> {
68
- const envKey = asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ?? asTrimmed($env.KIMI_SEARCH_API_KEY);
77
+ const envKey =
78
+ asTrimmed($credentialEnv("MOONSHOT_SEARCH_API_KEY")) ?? asTrimmed($credentialEnv("KIMI_SEARCH_API_KEY"));
69
79
  if (envKey) return envKey;
70
80
 
71
81
  return (
@@ -157,8 +167,8 @@ export class KimiProvider extends SearchProvider {
157
167
 
158
168
  isAvailable(authStorage: AuthStorage): boolean {
159
169
  return (
160
- !!asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ||
161
- !!asTrimmed($env.KIMI_SEARCH_API_KEY) ||
170
+ !!asTrimmed($credentialEnv("MOONSHOT_SEARCH_API_KEY")) ||
171
+ !!asTrimmed($credentialEnv("KIMI_SEARCH_API_KEY")) ||
162
172
  authStorage.hasAuth("moonshot") ||
163
173
  authStorage.hasAuth("kimi-code")
164
174
  );