@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
@@ -21,6 +21,13 @@ export type SkillDiscoveryToolInput = z.infer<typeof skillDiscoverySchema>;
21
21
  export interface SkillDiscoveryToolDetails {
22
22
  candidates: RuntimeSkillDiscoveryCandidate[];
23
23
  count: number;
24
+ /**
25
+ * Present only when zero candidates were returned AND discovery config gates
26
+ * (`skills.enabled` / `skills.enablePiProject` / `skills.enablePiUser`)
27
+ * prevented some or all of the requested scope from being searched. Without
28
+ * this, a disabled config is indistinguishable from "no skills exist".
29
+ */
30
+ notice?: string;
24
31
  }
25
32
 
26
33
  export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema, SkillDiscoveryToolDetails> {
@@ -57,17 +64,46 @@ export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema
57
64
  signal?: AbortSignal,
58
65
  ): Promise<AgentToolResult<SkillDiscoveryToolDetails>> {
59
66
  return untilAborted(signal, async () => {
67
+ const source = input.source ?? "all";
60
68
  const candidates = await discoverRuntimeSkills({
61
69
  cwd: this.#session.cwd,
62
70
  query: input.query,
63
- source: input.source ?? "all",
71
+ source,
64
72
  limit: input.limit,
65
73
  policy: this.#getRuntimeSkillPolicy(),
66
74
  });
75
+ const details: SkillDiscoveryToolDetails = { candidates, count: candidates.length };
76
+ if (candidates.length === 0) {
77
+ const notice = this.#disabledPolicyNotice(source);
78
+ if (notice) details.notice = notice;
79
+ }
67
80
  return {
68
- content: [{ type: "text", text: JSON.stringify({ candidates, count: candidates.length }, null, 2) }],
69
- details: { candidates, count: candidates.length },
81
+ content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
82
+ details,
70
83
  };
71
84
  });
72
85
  }
86
+
87
+ /**
88
+ * Explain an empty result that is caused by disabled discovery config rather
89
+ * than by an actually empty skill catalog.
90
+ */
91
+ #disabledPolicyNotice(source: "all" | "project" | "user"): string | undefined {
92
+ const policy = this.#getRuntimeSkillPolicy();
93
+ if (policy.enabled !== true) {
94
+ return "Runtime skill discovery is disabled: `skills.enabled` is false, so no skill directories were searched. Enable it with `gjc config set skills.enabled true` (project and user scopes additionally require `skills.enablePiProject` / `skills.enablePiUser`).";
95
+ }
96
+ const skipped: string[] = [];
97
+ const commands: string[] = [];
98
+ if ((source === "all" || source === "project") && policy.enablePiProject !== true) {
99
+ skipped.push("project (`skills.enablePiProject` is false)");
100
+ commands.push("`gjc config set skills.enablePiProject true`");
101
+ }
102
+ if ((source === "all" || source === "user") && policy.enablePiUser !== true) {
103
+ skipped.push("user (`skills.enablePiUser` is false)");
104
+ commands.push("`gjc config set skills.enablePiUser true`");
105
+ }
106
+ if (skipped.length === 0) return undefined;
107
+ return `Skill discovery skipped disabled scope(s): ${skipped.join(", ")}. Enable them with ${commands.join(" and ")}.`;
108
+ }
73
109
  }
@@ -272,6 +272,7 @@ const COMMENT_OR_TERMINATOR_ERROR =
272
272
  "SQLite 'where' clause must not contain comments or statement terminators; use '?q=SELECT ...' for raw SQL";
273
273
  const FORBIDDEN_KEYWORD_ERROR =
274
274
  "SQLite 'where' clause must not contain LIMIT/OFFSET/UNION/INTERSECT/EXCEPT/ATTACH/DETACH/PRAGMA; use '?q=SELECT ...' for raw SQL";
275
+ const RAW_QUERY_ERROR = "SQLite raw queries must contain exactly one explicit SELECT statement";
275
276
 
276
277
  /**
277
278
  * Scans a `where=` clause character-by-character, tracking single- and double-quoted
@@ -363,6 +364,66 @@ function validateWhereClause(where: string | undefined): string | undefined {
363
364
  return trimmed;
364
365
  }
365
366
 
367
+ /**
368
+ * Keep raw reads deliberately narrower than SQLite's full grammar. Comments are
369
+ * rejected, and a statement terminator is accepted only as the final
370
+ * non-whitespace character. Quoted semicolons remain ordinary query data.
371
+ */
372
+ export function validateSqliteReadQuery(sql: string): string {
373
+ const trimmed = sql.trim();
374
+ if (!/^SELECT\b/i.test(trimmed)) {
375
+ throw new ToolError(RAW_QUERY_ERROR);
376
+ }
377
+
378
+ let quoteEnd: "'" | '"' | "`" | "]" | null = null;
379
+ for (let index = 0; index < trimmed.length; index++) {
380
+ const char = trimmed[index];
381
+ const next = trimmed[index + 1];
382
+ if (char === "\0") {
383
+ throw new ToolError(RAW_QUERY_ERROR);
384
+ }
385
+
386
+ if (quoteEnd) {
387
+ if (char === quoteEnd) {
388
+ if (quoteEnd !== "]" && next === quoteEnd) {
389
+ index += 1;
390
+ } else {
391
+ quoteEnd = null;
392
+ }
393
+ }
394
+ continue;
395
+ }
396
+
397
+ if (char === "'" || char === '"' || char === "`") {
398
+ quoteEnd = char;
399
+ continue;
400
+ }
401
+ if (char === "[") {
402
+ quoteEnd = "]";
403
+ continue;
404
+ }
405
+ if ((char === "-" && next === "-") || (char === "/" && next === "*") || (char === "*" && next === "/")) {
406
+ throw new ToolError(RAW_QUERY_ERROR);
407
+ }
408
+ if (char === ";" && trimmed.slice(index + 1).trim().length > 0) {
409
+ throw new ToolError(RAW_QUERY_ERROR);
410
+ }
411
+ }
412
+
413
+ if (quoteEnd) {
414
+ throw new ToolError(RAW_QUERY_ERROR);
415
+ }
416
+ return trimmed;
417
+ }
418
+
419
+ export function enforceSqliteQueryOnly(db: Database): void {
420
+ db.run("PRAGMA query_only = ON");
421
+ const enabled = db.prepare<{ query_only: number }, []>("PRAGMA query_only").get()?.query_only;
422
+ if (enabled !== 1) {
423
+ throw new ToolError("SQLite read connection could not enable query-only mode");
424
+ }
425
+ }
426
+
366
427
  function normalizeWriteValue(value: unknown, column: string): SqliteBinding {
367
428
  if (value === null) return null;
368
429
  if (
@@ -444,10 +505,7 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
444
505
  if (normalizedSubPath || otherKeys.length > 0) {
445
506
  throw new ToolError("SQLite raw queries cannot be combined with table selectors or pagination");
446
507
  }
447
- if (!rawQuery.trim()) {
448
- throw new ToolError("SQLite query parameter 'q' cannot be empty");
449
- }
450
- return { kind: "raw", sql: rawQuery };
508
+ return { kind: "raw", sql: validateSqliteReadQuery(rawQuery) };
451
509
  }
452
510
 
453
511
  if (!normalizedSubPath) {
@@ -602,7 +660,7 @@ export function executeReadQuery(
602
660
  sql: string,
603
661
  maxRows: number = MAX_RAW_QUERY_ROWS,
604
662
  ): { columns: string[]; rows: Record<string, unknown>[]; truncated: boolean } {
605
- const statement = db.prepare<SqliteRow, []>(sql);
663
+ const statement = db.prepare<SqliteRow, []>(validateSqliteReadQuery(sql));
606
664
  if (statement.paramsCount > 0) {
607
665
  throw new ToolError("SQLite raw queries do not support bound parameters");
608
666
  }
@@ -1,4 +1,4 @@
1
- import { $env } from "@gajae-code/utils/env";
1
+ import { $pickenv } from "@gajae-code/utils/env";
2
2
 
3
3
  export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
4
4
 
@@ -34,7 +34,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
34
34
  const modelVariant = session.settings.getEditVariantForModel?.(activeModel);
35
35
  if (modelVariant) return modelVariant;
36
36
 
37
- const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
37
+ const envMode = normalizeEditMode($pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT"));
38
38
  if (envMode) return envMode;
39
39
 
40
40
  const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
@@ -143,6 +143,7 @@ function tokenizePastedPathCandidates(
143
143
  text: string,
144
144
  options: DecodePastedPathOptions,
145
145
  maxCandidates: number,
146
+ requireCompletePayload = false,
146
147
  ): TokenizePastedPathsResult {
147
148
  if (text.length > MAX_PASTED_IMAGE_PASTE_CHARACTERS) return { kind: "invalid" };
148
149
  const platform = options.platform ?? process.platform;
@@ -154,7 +155,9 @@ function tokenizePastedPathCandidates(
154
155
 
155
156
  const startCandidate = (): TokenizePastedPathsResult | undefined => {
156
157
  if (candidateStarted) return undefined;
157
- if (candidates.length >= maxCandidates) return { kind: "too-many", maxCandidates, candidates: [...candidates] };
158
+ if (!requireCompletePayload && candidates.length >= maxCandidates) {
159
+ return { kind: "too-many", maxCandidates, candidates: [...candidates] };
160
+ }
158
161
  candidateStarted = true;
159
162
  return undefined;
160
163
  };
@@ -167,7 +170,9 @@ function tokenizePastedPathCandidates(
167
170
  const finishCandidate = (): TokenizePastedPathsResult | undefined => {
168
171
  if (!candidateStarted) return undefined;
169
172
  if (!candidate) return { kind: "invalid" };
170
- if (candidates.length >= maxCandidates) return { kind: "too-many", maxCandidates, candidates: [...candidates] };
173
+ if (!requireCompletePayload && candidates.length >= maxCandidates) {
174
+ return { kind: "too-many", maxCandidates, candidates: [...candidates] };
175
+ }
171
176
  const decoded = decodeListToken(candidate, options, platform);
172
177
  if (decoded === undefined) return { kind: "invalid" };
173
178
  candidates.push(decoded);
@@ -225,6 +230,7 @@ function tokenizePastedPathCandidates(
225
230
  if (state !== "normal") return { kind: "invalid" };
226
231
  const finalResult = finishCandidate();
227
232
  if (finalResult) return finalResult;
233
+ if (candidates.length > maxCandidates) return { kind: "too-many", maxCandidates, candidates };
228
234
  return candidates.length === 0 ? { kind: "invalid" } : { kind: "candidates", candidates };
229
235
  }
230
236
 
@@ -257,8 +263,12 @@ function isRecognizedClipboardTempPath(filePath: string, platform: NodeJS.Platfo
257
263
  /** Resolve one recognized clipboard-temp image path lexically, before any filesystem access. */
258
264
  export function resolvePastedImagePath(text: string, options: ResolvePastedImagePathOptions = {}): string | undefined {
259
265
  const platform = options.platform ?? process.platform;
260
- const candidate = decodePastedPathCandidate(text, options);
261
- if (!candidate || !IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
266
+ const terminalNewlinesTrimmed = text.replace(/(?:\r\n|\r|\n)+$/, "");
267
+ if (/\r|\n/.test(terminalNewlinesTrimmed)) return undefined;
268
+ const candidates = decodePastedPathCandidates(terminalNewlinesTrimmed, options, 1);
269
+ if (candidates?.length !== 1) return undefined;
270
+ const candidate = candidates[0];
271
+ if (!IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
262
272
  if (platform === "win32" && isRemoteWindowsPath(candidate)) return undefined;
263
273
  const resolved = resolveCandidate(candidate, options, platform);
264
274
  return isRecognizedClipboardTempPath(resolved, platform) ? resolved : undefined;
@@ -274,10 +284,9 @@ export function parsePastedImagePaths(
274
284
  options: ResolvePastedImagePathOptions = {},
275
285
  ): PastedImagePathParseResult | undefined {
276
286
  const platform = options.platform ?? process.platform;
277
- const tokenized = tokenizePastedPathCandidates(text, options, MAX_PASTED_IMAGE_COUNT);
287
+ const tokenized = tokenizePastedPathCandidates(text, options, MAX_PASTED_IMAGE_COUNT, true);
278
288
  if (tokenized.kind === "too-many") {
279
289
  if (
280
- tokenized.candidates.length !== MAX_PASTED_IMAGE_COUNT ||
281
290
  tokenized.candidates.some(
282
291
  candidate =>
283
292
  !IMAGE_FILE_EXTENSION_PATTERN.test(candidate) ||
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Predict the user's likely next prompt after an agent turn using a smol,
3
+ * fast model. Native GJC port of the ghost-text prompt-suggestion behavior
4
+ * from Claude Code: the prediction renders as dim ghost text in the empty
5
+ * composer and Tab accepts it.
6
+ */
7
+ import type { AgentMessage } from "@gajae-code/agent-core";
8
+ import { type Api, type AssistantMessage, completeSimple, type Model } from "@gajae-code/ai";
9
+ import { logger, prompt } from "@gajae-code/utils";
10
+ import type { ModelRegistry } from "../config/model-registry";
11
+ import { resolveRoleSelection } from "../config/model-resolver";
12
+ import type { Settings } from "../config/settings";
13
+ import suggestionSystemPrompt from "../prompts/system/prompt-suggestion-system.md" with { type: "text" };
14
+
15
+ const SUGGESTION_SYSTEM_PROMPT = prompt.render(suggestionSystemPrompt);
16
+
17
+ export const PROMPT_SUGGESTION_MAX_WORDS = 12;
18
+ export const PROMPT_SUGGESTION_MAX_CHARS = 100;
19
+
20
+ const SUGGESTION_MAX_TOKENS = 64;
21
+ const REASONING_SAFE_MAX_TOKENS = 1024;
22
+ const MAX_CONTEXT_MESSAGES = 12;
23
+ const MAX_MESSAGE_CHARS = 1500;
24
+ const MAX_CONTEXT_CHARS = 8000;
25
+
26
+ /** Single words a user plausibly types on their own; anything else needs >= 2 words. */
27
+ const SINGLE_WORD_ALLOWLIST = new Set([
28
+ "yes",
29
+ "yeah",
30
+ "yep",
31
+ "yea",
32
+ "yup",
33
+ "sure",
34
+ "ok",
35
+ "okay",
36
+ "push",
37
+ "commit",
38
+ "deploy",
39
+ "stop",
40
+ "continue",
41
+ "check",
42
+ "exit",
43
+ "quit",
44
+ "no",
45
+ ]);
46
+
47
+ /** Model responses that are meta-statements about staying silent, not suggestions. */
48
+ const META_TEXT_PATTERN = /\bsilence is\b|\bstay(s|ing)? silent\b/;
49
+
50
+ /**
51
+ * Strip wrapper tags and label prefixes a model sometimes adds despite being
52
+ * told to reply with only the suggestion.
53
+ */
54
+ export function sanitizePromptSuggestion(raw: string): string {
55
+ return raw
56
+ .trim()
57
+ .replace(/^<(suggestion|response|output|answer|result)>([\s\S]*)<\/\1>$/i, (match, tag: string, inner: string) =>
58
+ // Keep the original when the inner text itself contains the closing
59
+ // tag (nested/ambiguous markup) instead of producing a mangled strip.
60
+ inner.includes(`</${tag.toLowerCase()}>`) || inner.includes(`</${tag.toUpperCase()}>`) ? match : inner,
61
+ )
62
+ .replace(
63
+ /^\s*(suggested\s+(response|reply|input|prompt)|suggestion|response|reply|answer|output|result)\s*:\s*/i,
64
+ "",
65
+ )
66
+ .trim();
67
+ }
68
+
69
+ /**
70
+ * Heuristic gate rejecting model output that would make a bad ghost-text
71
+ * suggestion. Returns the rejection reason, or null when the text is usable.
72
+ */
73
+ export function suppressPromptSuggestionReason(text: string): string | null {
74
+ if (!text) return "empty";
75
+ const lower = text.toLowerCase();
76
+ const wordCount = text.trim().split(/\s+/).length;
77
+
78
+ if (lower === "done") return "done";
79
+ if (
80
+ lower === "nothing found" ||
81
+ lower === "nothing found." ||
82
+ lower.startsWith("nothing to suggest") ||
83
+ lower.startsWith("no suggestion") ||
84
+ META_TEXT_PATTERN.test(lower) ||
85
+ /^\W*silence\W*$/.test(lower)
86
+ ) {
87
+ return "meta_text";
88
+ }
89
+ if (/^\(.*\)$|^\[.*\]$/.test(text)) return "meta_wrapped";
90
+ if (
91
+ lower.startsWith("api error:") ||
92
+ lower.startsWith("prompt is too long") ||
93
+ lower.startsWith("request timed out") ||
94
+ lower.startsWith("invalid api key")
95
+ ) {
96
+ return "error_message";
97
+ }
98
+ if (/^\w+:\s/.test(text)) return "prefixed_label";
99
+ if (wordCount < 2 && !text.startsWith("/") && !SINGLE_WORD_ALLOWLIST.has(lower)) return "too_few_words";
100
+ if (wordCount > PROMPT_SUGGESTION_MAX_WORDS) return "too_many_words";
101
+ if (text.length >= PROMPT_SUGGESTION_MAX_CHARS) return "too_long";
102
+ if (/[.!?]\s+[A-Z]/.test(text)) return "multiple_sentences";
103
+ if (/[\n*]|\*\*/.test(text)) return "has_formatting";
104
+ if (
105
+ /thanks|thank you|looks good|sounds good|that works|that worked|that's all|nice|great|perfect|makes sense|awesome|excellent/.test(
106
+ lower,
107
+ )
108
+ ) {
109
+ return "evaluative";
110
+ }
111
+ if (
112
+ /^(let me|i'll|i've|i'm|i can|i would|i think|i notice|here's|here is|here are|that's|this is|this will|you can|you should|you could|sure,|of course|certainly)/i.test(
113
+ text,
114
+ )
115
+ ) {
116
+ return "claude_voice";
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function extractTranscriptEntry(message: AgentMessage): { role: "User" | "Assistant"; text: string } | null {
122
+ if (message.role === "user") {
123
+ const content = message.content;
124
+ const text =
125
+ typeof content === "string"
126
+ ? content
127
+ : content
128
+ .filter(block => block.type === "text")
129
+ .map(block => block.text)
130
+ .join("\n");
131
+ const trimmed = text.trim();
132
+ return trimmed ? { role: "User", text: trimmed } : null;
133
+ }
134
+ if (message.role === "assistant") {
135
+ const text = (message as AssistantMessage).content
136
+ .filter(block => block.type === "text")
137
+ .map(block => block.text)
138
+ .join("\n")
139
+ .trim();
140
+ return text ? { role: "Assistant", text } : null;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Build a compact recent-conversation transcript for the prediction request.
147
+ * Returns null when there is no user message to predict from.
148
+ */
149
+ export function buildPromptSuggestionContext(messages: AgentMessage[]): string | null {
150
+ const entries: { role: "User" | "Assistant"; text: string }[] = [];
151
+ let totalChars = 0;
152
+ for (let index = messages.length - 1; index >= 0 && entries.length < MAX_CONTEXT_MESSAGES; index--) {
153
+ const message = messages[index];
154
+ if (!message) continue;
155
+ const entry = extractTranscriptEntry(message);
156
+ if (!entry) continue;
157
+ const text = entry.text.length > MAX_MESSAGE_CHARS ? `${entry.text.slice(0, MAX_MESSAGE_CHARS)}…` : entry.text;
158
+ if (totalChars + text.length > MAX_CONTEXT_CHARS && entries.length > 0) break;
159
+ entries.push({ role: entry.role, text });
160
+ totalChars += text.length;
161
+ }
162
+ if (!entries.some(entry => entry.role === "User")) return null;
163
+ entries.reverse();
164
+ const transcript = entries.map(entry => `${entry.role}: ${entry.text}`).join("\n\n");
165
+ return `<conversation>\n${transcript}\n</conversation>\n\nPredict the user's next message.`;
166
+ }
167
+
168
+ function getSuggestionModel(
169
+ registry: ModelRegistry,
170
+ settings: Settings,
171
+ currentModel?: Model<Api>,
172
+ ): Model<Api> | undefined {
173
+ const availableModels = registry.getAvailable();
174
+ if (availableModels.length === 0) return undefined;
175
+ const model = resolveRoleSelection(["default"], settings, availableModels, registry)?.model;
176
+ if (model) return model;
177
+ return currentModel;
178
+ }
179
+
180
+ function extractSuggestionText(contentBlocks: AssistantMessage["content"]): string {
181
+ let text = "";
182
+ for (const content of contentBlocks) {
183
+ if (content.type === "text") {
184
+ text += content.text;
185
+ }
186
+ }
187
+ return text;
188
+ }
189
+
190
+ /**
191
+ * Generate a predicted next user prompt from the recent conversation.
192
+ * Returns null when no usable suggestion is produced (silence is the
193
+ * expected steady state).
194
+ */
195
+ export async function generatePromptSuggestion(
196
+ messages: AgentMessage[],
197
+ registry: ModelRegistry,
198
+ settings: Settings,
199
+ sessionId?: string,
200
+ currentModel?: Model<Api>,
201
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
202
+ ): Promise<string | null> {
203
+ const context = buildPromptSuggestionContext(messages);
204
+ if (!context) return null;
205
+
206
+ const model = getSuggestionModel(registry, settings, currentModel);
207
+ if (!model) {
208
+ logger.debug("prompt-suggestion: no model available");
209
+ return null;
210
+ }
211
+
212
+ const apiKey = await registry.getApiKey(model, sessionId);
213
+ if (!apiKey) {
214
+ logger.debug("prompt-suggestion: no API key", { provider: model.provider, id: model.id });
215
+ return null;
216
+ }
217
+ // Resolve metadata after getApiKey so the session-sticky credential for
218
+ // this request is already recorded (same contract as title-generator).
219
+ const metadata = metadataResolver?.(model.provider);
220
+
221
+ // A suggestion is a 2-12 word task, but some reasoning backends ignore
222
+ // disableReasoning; reserve output room for the answer after any
223
+ // unavoidable thinking tokens.
224
+ const maxTokens = model.reasoning
225
+ ? Math.max(SUGGESTION_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS)
226
+ : SUGGESTION_MAX_TOKENS;
227
+
228
+ try {
229
+ const response = await completeSimple(
230
+ model,
231
+ {
232
+ systemPrompt: [SUGGESTION_SYSTEM_PROMPT],
233
+ messages: [{ role: "user", content: context, timestamp: Date.now() }],
234
+ },
235
+ {
236
+ apiKey,
237
+ maxTokens,
238
+ disableReasoning: true,
239
+ metadata,
240
+ },
241
+ );
242
+
243
+ if (response.stopReason === "error") {
244
+ logger.debug("prompt-suggestion: response error", {
245
+ model: `${model.provider}/${model.id}`,
246
+ errorMessage: response.errorMessage,
247
+ });
248
+ return null;
249
+ }
250
+
251
+ const suggestion = sanitizePromptSuggestion(extractSuggestionText(response.content));
252
+ const suppressReason = suppressPromptSuggestionReason(suggestion);
253
+ if (suppressReason) {
254
+ logger.debug("prompt-suggestion: suppressed", { reason: suppressReason });
255
+ return null;
256
+ }
257
+ return suggestion;
258
+ } catch (err) {
259
+ logger.debug("prompt-suggestion: error", {
260
+ model: `${model.provider}/${model.id}`,
261
+ error: err instanceof Error ? err.message : String(err),
262
+ });
263
+ return null;
264
+ }
265
+ }