@gajae-code/coding-agent 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/migrate-cli.d.ts +20 -0
  5. package/dist/types/cli/notify-cli.d.ts +23 -0
  6. package/dist/types/cli/setup-cli.d.ts +20 -1
  7. package/dist/types/commands/daemon.d.ts +41 -0
  8. package/dist/types/commands/migrate.d.ts +33 -0
  9. package/dist/types/commands/notify.d.ts +41 -0
  10. package/dist/types/config/keybindings.d.ts +4 -0
  11. package/dist/types/config/model-profile-activation.d.ts +12 -0
  12. package/dist/types/config/model-profiles.d.ts +2 -1
  13. package/dist/types/config/model-registry.d.ts +3 -3
  14. package/dist/types/config/models-config-schema.d.ts +5 -0
  15. package/dist/types/config/settings-schema.d.ts +38 -0
  16. package/dist/types/coordinator/contract.d.ts +1 -1
  17. package/dist/types/daemon/builtin.d.ts +20 -0
  18. package/dist/types/daemon/control-types.d.ts +57 -0
  19. package/dist/types/daemon/runtime.d.ts +25 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  21. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  22. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  23. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  24. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  25. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  26. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  27. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  28. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  29. package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
  30. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  31. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  32. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  33. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  34. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  35. package/dist/types/hooks/skill-state.d.ts +12 -4
  36. package/dist/types/migrate/action-planner.d.ts +11 -0
  37. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  38. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  39. package/dist/types/migrate/adapters/index.d.ts +45 -0
  40. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  41. package/dist/types/migrate/executor.d.ts +2 -0
  42. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  43. package/dist/types/migrate/report.d.ts +18 -0
  44. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  45. package/dist/types/migrate/types.d.ts +126 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  47. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  48. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  49. package/dist/types/modes/interactive-mode.d.ts +1 -1
  50. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  51. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  52. package/dist/types/modes/types.d.ts +7 -1
  53. package/dist/types/notifications/config-commands.d.ts +26 -0
  54. package/dist/types/notifications/config.d.ts +61 -0
  55. package/dist/types/notifications/helpers.d.ts +55 -0
  56. package/dist/types/notifications/html-format.d.ts +62 -0
  57. package/dist/types/notifications/index.d.ts +28 -0
  58. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  59. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  60. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  61. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  62. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  63. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  64. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  65. package/dist/types/notifications/threaded-render.d.ts +66 -0
  66. package/dist/types/notifications/topic-registry.d.ts +67 -0
  67. package/dist/types/research-plan/index.d.ts +1 -0
  68. package/dist/types/research-plan/ledger.d.ts +33 -0
  69. package/dist/types/rlm/artifacts.d.ts +1 -1
  70. package/dist/types/rlm/index.d.ts +12 -0
  71. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  72. package/dist/types/session/agent-session.d.ts +39 -2
  73. package/dist/types/session/auth-storage.d.ts +1 -1
  74. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  75. package/dist/types/setup/credential-import.d.ts +3 -0
  76. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  77. package/dist/types/skill-state/active-state.d.ts +6 -11
  78. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  79. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  80. package/dist/types/task/spawn-gate.d.ts +1 -10
  81. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  82. package/dist/types/tools/index.d.ts +18 -0
  83. package/dist/types/tools/subagent.d.ts +3 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +3 -0
  86. package/src/async/job-manager.ts +5 -1
  87. package/src/cli/daemon-cli.ts +122 -0
  88. package/src/cli/migrate-cli.ts +106 -0
  89. package/src/cli/notify-cli.ts +274 -0
  90. package/src/cli/setup-cli.ts +173 -84
  91. package/src/cli.ts +3 -0
  92. package/src/commands/daemon.ts +47 -0
  93. package/src/commands/deep-interview.ts +2 -2
  94. package/src/commands/migrate.ts +46 -0
  95. package/src/commands/notify.ts +61 -0
  96. package/src/commands/setup.ts +11 -1
  97. package/src/commands/state.ts +2 -1
  98. package/src/commands/team.ts +7 -3
  99. package/src/config/model-profile-activation.ts +74 -5
  100. package/src/config/model-profiles.ts +7 -4
  101. package/src/config/model-registry.ts +6 -3
  102. package/src/config/models-config-schema.ts +1 -1
  103. package/src/config/settings-schema.ts +29 -0
  104. package/src/coordinator/contract.ts +3 -0
  105. package/src/coordinator-mcp/policy.ts +10 -2
  106. package/src/coordinator-mcp/server.ts +270 -1
  107. package/src/daemon/builtin.ts +46 -0
  108. package/src/daemon/control-types.ts +65 -0
  109. package/src/daemon/runtime.ts +51 -0
  110. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  111. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  112. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  113. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  114. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
  115. package/src/extensibility/custom-commands/loader.ts +0 -7
  116. package/src/extensibility/extensions/runner.ts +4 -0
  117. package/src/extensibility/extensions/types.ts +8 -0
  118. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  119. package/src/extensibility/gjc-plugins/state.ts +16 -1
  120. package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  123. package/src/gjc-runtime/launch-tmux.ts +6 -1
  124. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/gjc-runtime/session-layout.ts +180 -0
  126. package/src/gjc-runtime/session-resolution.ts +217 -0
  127. package/src/gjc-runtime/state-graph.ts +1 -2
  128. package/src/gjc-runtime/state-migrations.ts +1 -0
  129. package/src/gjc-runtime/state-runtime.ts +247 -124
  130. package/src/gjc-runtime/state-schema.ts +2 -0
  131. package/src/gjc-runtime/state-writer.ts +289 -41
  132. package/src/gjc-runtime/team-runtime.ts +43 -19
  133. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  134. package/src/gjc-runtime/ultragoal-guard.ts +102 -4
  135. package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
  136. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  138. package/src/gjc-runtime/workflow-manifest.ts +12 -3
  139. package/src/goals/tools/goal-tool.ts +11 -2
  140. package/src/harness-control-plane/storage.ts +14 -4
  141. package/src/hooks/native-skill-hook.ts +38 -12
  142. package/src/hooks/skill-state.ts +178 -83
  143. package/src/internal-urls/docs-index.generated.ts +9 -6
  144. package/src/main.ts +30 -0
  145. package/src/migrate/action-planner.ts +318 -0
  146. package/src/migrate/adapters/claude-code.ts +39 -0
  147. package/src/migrate/adapters/codex.ts +70 -0
  148. package/src/migrate/adapters/index.ts +277 -0
  149. package/src/migrate/adapters/opencode.ts +52 -0
  150. package/src/migrate/executor.ts +81 -0
  151. package/src/migrate/mcp-mapper.ts +152 -0
  152. package/src/migrate/report.ts +104 -0
  153. package/src/migrate/skill-normalizer.ts +80 -0
  154. package/src/migrate/types.ts +163 -0
  155. package/src/modes/acp/acp-event-mapper.ts +1 -0
  156. package/src/modes/bridge/bridge-mode.ts +2 -2
  157. package/src/modes/components/custom-editor.ts +30 -20
  158. package/src/modes/components/hook-editor.ts +7 -2
  159. package/src/modes/components/oauth-selector.ts +19 -0
  160. package/src/modes/controllers/event-controller.ts +20 -0
  161. package/src/modes/controllers/selector-controller.ts +80 -17
  162. package/src/modes/interactive-mode.ts +6 -2
  163. package/src/modes/rpc/rpc-mode.ts +2 -2
  164. package/src/modes/runtime-init.ts +1 -0
  165. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  166. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  167. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  168. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  169. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  170. package/src/modes/types.ts +7 -1
  171. package/src/modes/utils/ui-helpers.ts +23 -0
  172. package/src/notifications/config-commands.ts +50 -0
  173. package/src/notifications/config.ts +107 -0
  174. package/src/notifications/helpers.ts +135 -0
  175. package/src/notifications/html-format.ts +389 -0
  176. package/src/notifications/index.ts +663 -0
  177. package/src/notifications/rate-limit-pool.ts +179 -0
  178. package/src/notifications/telegram-cli.ts +194 -0
  179. package/src/notifications/telegram-daemon-cli.ts +74 -0
  180. package/src/notifications/telegram-daemon-control.ts +370 -0
  181. package/src/notifications/telegram-daemon.ts +1370 -0
  182. package/src/notifications/telegram-reference.ts +335 -0
  183. package/src/notifications/threaded-inbound.ts +80 -0
  184. package/src/notifications/threaded-render.ts +155 -0
  185. package/src/notifications/topic-registry.ts +133 -0
  186. package/src/prompts/agents/init.md +1 -1
  187. package/src/prompts/system/plan-mode-active.md +1 -1
  188. package/src/prompts/tools/ast-grep.md +1 -1
  189. package/src/prompts/tools/search.md +1 -1
  190. package/src/prompts/tools/task.md +1 -2
  191. package/src/research-plan/index.ts +1 -0
  192. package/src/research-plan/ledger.ts +177 -0
  193. package/src/rlm/artifacts.ts +12 -3
  194. package/src/rlm/index.ts +26 -0
  195. package/src/runtime-mcp/config-writer.ts +46 -0
  196. package/src/sdk.ts +16 -0
  197. package/src/session/agent-session.ts +128 -24
  198. package/src/session/auth-storage.ts +3 -0
  199. package/src/session/session-dump-format.ts +43 -2
  200. package/src/session/session-manager.ts +39 -5
  201. package/src/setup/credential-auto-import.ts +258 -0
  202. package/src/setup/credential-import.ts +17 -0
  203. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  204. package/src/setup/hermes-setup.ts +1 -1
  205. package/src/setup/host-plugin-setup.ts +142 -0
  206. package/src/skill-state/active-state.ts +72 -108
  207. package/src/skill-state/canonical-skills.ts +4 -0
  208. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  209. package/src/skill-state/workflow-hud.ts +4 -2
  210. package/src/skill-state/workflow-state-contract.ts +3 -3
  211. package/src/slash-commands/builtin-registry.ts +4 -1
  212. package/src/task/agents.ts +1 -22
  213. package/src/task/executor.ts +5 -1
  214. package/src/task/index.ts +1 -41
  215. package/src/task/spawn-gate.ts +1 -38
  216. package/src/task/types.ts +1 -1
  217. package/src/tools/ask-answer-registry.ts +25 -0
  218. package/src/tools/ask.ts +108 -16
  219. package/src/tools/computer.ts +58 -4
  220. package/src/tools/image-gen.ts +5 -8
  221. package/src/tools/index.ts +19 -0
  222. package/src/tools/inspect-image.ts +16 -11
  223. package/src/tools/subagent-render.ts +7 -0
  224. package/src/tools/subagent.ts +38 -7
  225. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  226. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  227. package/src/prompts/agents/explore.md +0 -58
  228. package/src/prompts/agents/plan.md +0 -49
  229. package/src/prompts/agents/reviewer.md +0 -141
  230. package/src/prompts/agents/task.md +0 -16
  231. package/src/prompts/review-request.md +0 -70
@@ -235,6 +235,22 @@ export function observeAgentSessionEvent(event: AgentSessionEvent): AgentWireOwn
235
235
  semantic: false,
236
236
  coalesceKey: null,
237
237
  });
238
+ case "subagent_steer_message": {
239
+ const details = recordObject(event.message.details);
240
+ return obs(event, {
241
+ kind: "rpc_subagent_steer",
242
+ signal: null,
243
+ evidence: {
244
+ from: str(details?.from) ?? null,
245
+ to: str(details?.to) ?? null,
246
+ state: str(details?.state) ?? null,
247
+ observationId: str(details?.observationId) ?? null,
248
+ },
249
+ severity: "info",
250
+ semantic: false,
251
+ coalesceKey: null,
252
+ });
253
+ }
238
254
  case "notice": {
239
255
  const level = event.level;
240
256
  return obs(event, {
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
14
14
  import * as path from "node:path";
15
+ import { sessionAuditDir } from "../../../gjc-runtime/session-layout";
15
16
  import type { RpcBudgetExceeded, RpcWorkflowGateKind, RpcWorkflowStage } from "../../rpc/rpc-types";
16
17
  import { answerHashOf } from "./workflow-gate-schema";
17
18
 
@@ -69,9 +70,9 @@ function defaultId(): string {
69
70
  return `ae_${Date.now().toString(36)}_${idCounter.toString(36)}`;
70
71
  }
71
72
 
72
- export function defaultAuditPath(runId: string, root = process.cwd()): string {
73
+ export function defaultAuditPath(runId: string, root = process.cwd(), gjcSessionId = runId): string {
73
74
  const safe = runId.replace(/[^a-zA-Z0-9_.-]/g, "_");
74
- return path.join(root, ".gjc", "audit", "unattended", `${safe}.jsonl`);
75
+ return path.join(sessionAuditDir(root, gjcSessionId), "unattended", `${safe}.jsonl`);
75
76
  }
76
77
 
77
78
  /** Append-only audit log writer + reader for one unattended run. */
@@ -59,6 +59,14 @@ export interface WorkflowGateEmitter {
59
59
  isUnattended(): boolean;
60
60
  /** Open + emit a gate; resolves with the agent's answer (from workflow_gate_response). */
61
61
  emitGate(input: OpenGateInput): Promise<unknown>;
62
+ /**
63
+ * Optional bridge surface (present on {@link UnattendedSessionControlPlane}) that
64
+ * lets an in-process extension observe emitted gates and answer them — used by
65
+ * the notifications SDK to resolve a real ask gate from a remote reply.
66
+ */
67
+ onGateEmitted?(listener: (gate: RpcWorkflowGate) => void): () => void;
68
+ resolveGate?(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution>;
69
+ listPendingGates?(): RpcWorkflowGate[];
62
70
  }
63
71
 
64
72
  export interface UnattendedSessionOptions {
@@ -82,6 +90,7 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
82
90
  #broker: WorkflowGateBroker | undefined;
83
91
  readonly #pending = new Map<string, { resolve: (answer: unknown) => void; reject: (err: Error) => void }>();
84
92
  readonly #earlyAnswers = new Map<string, unknown>();
93
+ readonly #gateListeners = new Set<(gate: RpcWorkflowGate) => void>();
85
94
 
86
95
  constructor(private readonly opts: UnattendedSessionOptions) {}
87
96
 
@@ -89,6 +98,12 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
89
98
  return this.#controller !== undefined;
90
99
  }
91
100
 
101
+ /** Observe every emitted gate (e.g. so an extension can map an ask to its gate_id). */
102
+ onGateEmitted(listener: (gate: RpcWorkflowGate) => void): () => void {
103
+ this.#gateListeners.add(listener);
104
+ return () => this.#gateListeners.delete(listener);
105
+ }
106
+
92
107
  get controller(): UnattendedRunController | undefined {
93
108
  return this.#controller;
94
109
  }
@@ -195,6 +210,13 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
195
210
  return Promise.reject(new Error("cannot emit a workflow gate before unattended mode is negotiated"));
196
211
  }
197
212
  const gate = this.#broker.openGate(input);
213
+ for (const listener of this.#gateListeners) {
214
+ try {
215
+ listener(gate);
216
+ } catch {
217
+ // A misbehaving observer must never break gate emission.
218
+ }
219
+ }
198
220
  if (this.#earlyAnswers.has(gate.gate_id)) {
199
221
  const answer = this.#earlyAnswers.get(gate.gate_id);
200
222
  this.#earlyAnswers.delete(gate.gate_id);
@@ -17,6 +17,7 @@ import type { MCPManager } from "../runtime-mcp";
17
17
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
18
18
  import type { HistoryStorage } from "../session/history-storage";
19
19
  import type { SessionContext, SessionManager } from "../session/session-manager";
20
+ import type { CredentialAutoImportOptions } from "../setup/credential-auto-import";
20
21
  import type { LspStartupServerInfo } from "../tools";
21
22
  import type { AssistantMessageComponent } from "./components/assistant-message";
22
23
  import type { BashExecutionComponent } from "./components/bash-execution";
@@ -247,7 +248,7 @@ export interface InteractiveModeContext {
247
248
  showSessionSelector(): void;
248
249
  handleResumeSession(sessionPath: string): Promise<void>;
249
250
  handleSessionDeleteCommand(): Promise<void>;
250
- showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
251
+ showOAuthSelector(mode: "login" | "logout", providerId?: string, options?: OAuthSelectorOptions): Promise<void>;
251
252
  showHookConfirm(title: string, message: string): Promise<boolean>;
252
253
  showDebugSelector(): void;
253
254
  showSessionObserver(): void;
@@ -311,3 +312,8 @@ export interface InteractiveModeContext {
311
312
  showExtensionError(extensionPath: string, error: string): void;
312
313
  showToolError(toolName: string, error: string): void;
313
314
  }
315
+ export interface OAuthSelectorOptions {
316
+ allowExternalCredentialDiscovery?: boolean;
317
+ trigger?: "bare-login";
318
+ externalCredentialDiscover?: CredentialAutoImportOptions["discover"];
319
+ }
@@ -203,6 +203,29 @@ export class UiHelpers {
203
203
  }
204
204
  return components;
205
205
  }
206
+ if (message.customType === "subagent:steer" || message.customType === "subagent:steer:relay") {
207
+ const details = (
208
+ message as CustomMessage<{
209
+ from?: string;
210
+ to?: string;
211
+ body?: string;
212
+ state?: string;
213
+ }>
214
+ ).details;
215
+ const components: Component[] = [];
216
+ const header = `${theme.fg("accent", `[Steer ${details?.state ?? "queued"}] ${details?.from ?? "?"} ⇨ ${details?.to ?? "?"}`)}`;
217
+ const headerComponent = new Text(header, 1, 0);
218
+ this.ctx.chatContainer.addChild(headerComponent);
219
+ components.push(headerComponent);
220
+ if (details?.body) {
221
+ for (const line of details.body.split("\n")) {
222
+ const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
223
+ this.ctx.chatContainer.addChild(lineComponent);
224
+ components.push(lineComponent);
225
+ }
226
+ }
227
+ return components;
228
+ }
206
229
  const renderer = this.ctx.session.extensionRunner?.getMessageRenderer(message.customType);
207
230
  // Both HookMessage and CustomMessage have the same structure, cast for compatibility
208
231
  const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * In-thread configuration slash commands for the threaded session surface.
3
+ *
4
+ * Replies are thread-native now (the old `/answer <sessionId> …` command is
5
+ * removed), but the user can still adjust per-surface behaviour from inside a
6
+ * session thread with small slash commands:
7
+ *
8
+ * - `/verbose` switch the mirror to verbose (full tool output + reasoning)
9
+ * - `/lean` switch back to lean (assistant text + tool names)
10
+ * - `/verbosity lean|verbose`
11
+ * - `/redact on|off` toggle redaction of streamed content
12
+ *
13
+ * This parser is pure so the command grammar is unit-testable; the daemon maps
14
+ * the returned change onto a `config_command` frame / settings update.
15
+ */
16
+
17
+ /** A parsed in-thread configuration change. */
18
+ export interface ConfigCommandChange {
19
+ verbosity?: "lean" | "verbose";
20
+ redact?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Parse an in-thread config command. Returns the requested change, or
25
+ * `undefined` when the text is not a recognised config command (so the daemon
26
+ * can fall through to treating it as a free-text injection).
27
+ */
28
+ export function parseInThreadConfigCommand(text: string): ConfigCommandChange | undefined {
29
+ const trimmed = text.trim();
30
+ if (!trimmed.startsWith("/")) return undefined;
31
+ const [rawCommand, ...rest] = trimmed.slice(1).split(/\s+/);
32
+ const command = rawCommand?.toLowerCase();
33
+ const arg = rest[0]?.toLowerCase();
34
+
35
+ switch (command) {
36
+ case "verbose":
37
+ return { verbosity: "verbose" };
38
+ case "lean":
39
+ return { verbosity: "lean" };
40
+ case "verbosity":
41
+ if (arg === "lean" || arg === "verbose") return { verbosity: arg };
42
+ return undefined;
43
+ case "redact":
44
+ if (arg === "on" || arg === "true" || arg === "1") return { redact: true };
45
+ if (arg === "off" || arg === "false" || arg === "0") return { redact: false };
46
+ return undefined;
47
+ default:
48
+ return undefined;
49
+ }
50
+ }
@@ -0,0 +1,107 @@
1
+ import * as crypto from "node:crypto";
2
+ import type { Settings } from "../config/settings";
3
+
4
+ export interface NotificationConfig {
5
+ enabled: boolean;
6
+ botToken?: string;
7
+ chatId?: string;
8
+ redact: boolean;
9
+ verbosity: "lean" | "verbose";
10
+ idleTimeoutMs: number;
11
+ }
12
+
13
+ /** Read typed config from Settings. */
14
+ export function getNotificationConfig(settings: Settings): NotificationConfig {
15
+ return {
16
+ enabled: settings.get("notifications.enabled"),
17
+ botToken: settings.get("notifications.telegram.botToken"),
18
+ chatId: settings.get("notifications.telegram.chatId"),
19
+ redact: settings.get("notifications.redact"),
20
+ verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
21
+ idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
22
+ };
23
+ }
24
+
25
+ /** Is global config sufficient for auto-on (enabled + botToken + chatId all present)? */
26
+ export function isGloballyConfigured(cfg: NotificationConfig): boolean {
27
+ return cfg.enabled && Boolean(cfg.botToken) && Boolean(cfg.chatId);
28
+ }
29
+
30
+ /** Resolve whether the notifications extension should be registered at SDK startup. */
31
+ export function shouldRegisterNotificationsExtension(input: {
32
+ env: NodeJS.ProcessEnv;
33
+ cfg?: NotificationConfig;
34
+ }): boolean {
35
+ if (input.env.GJC_NOTIFICATIONS === "0") return false;
36
+ if (input.env.GJC_NOTIFICATIONS === "1" || input.env.GJC_NOTIFICATIONS_TOKEN) return true;
37
+ return input.cfg ? isGloballyConfigured(input.cfg) : false;
38
+ }
39
+
40
+ /**
41
+ * Resolve whether THIS session should run notifications.
42
+ * Precedence (highest first):
43
+ * 1) env.GJC_NOTIFICATIONS === "0" -> false (hard opt-out)
44
+ * 2) sessionDisabled === true -> false (local /notify off)
45
+ * 3) env.GJC_NOTIFICATIONS === "1" || env.GJC_NOTIFICATIONS_TOKEN present -> true (legacy explicit)
46
+ * 4) isGloballyConfigured(cfg) -> true (global auto-on)
47
+ * 5) otherwise false
48
+ */
49
+ export function isSessionNotificationsEnabled(input: {
50
+ cfg: NotificationConfig;
51
+ env: NodeJS.ProcessEnv;
52
+ sessionDisabled: boolean;
53
+ }): boolean {
54
+ if (input.env.GJC_NOTIFICATIONS === "0") return false;
55
+ if (input.sessionDisabled) return false;
56
+ if (input.env.GJC_NOTIFICATIONS === "1" || input.env.GJC_NOTIFICATIONS_TOKEN) return true;
57
+ return isGloballyConfigured(input.cfg);
58
+ }
59
+
60
+ /** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
61
+ export function maskToken(token: string | undefined): string {
62
+ if (!token) return "(unset)";
63
+ return `${token.slice(0, 4)}…(len ${token.length})`;
64
+ }
65
+
66
+ /** Stable non-reversible fingerprint of a token: sha256 hex, first 12 chars. */
67
+ export function tokenFingerprint(token: string): string {
68
+ return crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
69
+ }
70
+
71
+ /** Short session tag for display, e.g. last 6 chars of sessionId. */
72
+ export function sessionTag(sessionId: string): string {
73
+ return sessionId.slice(-6);
74
+ }
75
+
76
+ export interface RedactableAction {
77
+ id: string;
78
+ kind: string;
79
+ sessionId: string;
80
+ question?: string;
81
+ options?: string[];
82
+ summary?: string;
83
+ }
84
+
85
+ /**
86
+ * When redact is true, strip sensitive content for remote delivery:
87
+ * - ask: NOT redacted. An ask is an interactive prompt the human must read and
88
+ * answer on the remote surface; redacting its question/options would make it
89
+ * unanswerable, defeating remote answering. Asks are returned unchanged.
90
+ * - idle: summary removed, (no question/options).
91
+ * When redact is false, return the action unchanged.
92
+ *
93
+ * Redaction still applies to streamed content frames (turn_stream, context_update,
94
+ * image_attachment) which are suppressed at their emit sites, not here.
95
+ */
96
+ export function buildRedactedAction(
97
+ action: RedactableAction,
98
+ opts: { redact: boolean; sessionTag: string },
99
+ ): RedactableAction {
100
+ if (!opts.redact) return action;
101
+
102
+ // Asks stay fully readable/answerable even under redaction.
103
+ if (action.kind === "ask") return action;
104
+
105
+ const { summary: _summary, question: _question, ...base } = action;
106
+ return base;
107
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Pure helpers for the notifications extension.
3
+ *
4
+ * Kept side-effect-free so the mapping logic (ask extraction, idle summary,
5
+ * dedupe keys) is unit-testable without a live session or the native server.
6
+ */
7
+
8
+ import { buildRedactedAction, type RedactableAction } from "./config";
9
+
10
+ /** A pending ask derived from an `ask` tool call. */
11
+ export interface PendingAsk {
12
+ /** Action id: `${toolCallId}:${questionId}`. */
13
+ id: string;
14
+ /** Question text. */
15
+ question: string;
16
+ /** Option labels (may be empty for free-text questions). */
17
+ options: string[];
18
+ }
19
+
20
+ /** Truncate text to `max` chars, appending an ellipsis when cut. */
21
+ export function truncate(text: string, max = 280): string {
22
+ if (max <= 0) return "";
23
+ return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
24
+ }
25
+
26
+ /** Stable per-turn idle dedupe key so exactly one idle action fires per turn. */
27
+ export function idleDedupeKey(sessionId: string, turnIndex: number): string {
28
+ return `${sessionId}#${turnIndex}`;
29
+ }
30
+
31
+ /**
32
+ * Extract pending asks from an `ask` tool call input.
33
+ *
34
+ * Defensive: tolerates partial/unknown shapes and always returns an array.
35
+ */
36
+ export function asksFromAskInput(toolCallId: string, input: unknown): PendingAsk[] {
37
+ const questions = (input as { questions?: unknown } | null | undefined)?.questions;
38
+ if (!Array.isArray(questions)) return [];
39
+ const asks: PendingAsk[] = [];
40
+ for (const raw of questions) {
41
+ if (!raw || typeof raw !== "object") continue;
42
+ const q = raw as { id?: unknown; question?: unknown; options?: unknown };
43
+ const qid = typeof q.id === "string" && q.id.length > 0 ? q.id : String(asks.length);
44
+ const question = typeof q.question === "string" ? q.question : "";
45
+ const options = Array.isArray(q.options)
46
+ ? q.options.map(opt => {
47
+ if (opt && typeof opt === "object" && typeof (opt as { label?: unknown }).label === "string") {
48
+ return (opt as { label: string }).label;
49
+ }
50
+ return String(opt);
51
+ })
52
+ : [];
53
+ asks.push({ id: `${toolCallId}:${qid}`, question, options });
54
+ }
55
+ return asks;
56
+ }
57
+
58
+ /** Prepare an action JSON payload for remote notification delivery. */
59
+ export function notificationActionPayload<T extends RedactableAction>(
60
+ action: T,
61
+ opts: { redact: boolean; sessionTag: string },
62
+ ): RedactableAction {
63
+ return buildRedactedAction(action, opts);
64
+ }
65
+
66
+ /** Extract a plain-text summary from an agent message's content, if any. */
67
+ export function summaryFromMessage(message: unknown, max = 280): string | undefined {
68
+ const content = (message as { content?: unknown } | null | undefined)?.content;
69
+ if (typeof content === "string") {
70
+ const trimmed = content.trim();
71
+ return trimmed ? truncate(trimmed, max) : undefined;
72
+ }
73
+ if (!Array.isArray(content)) return undefined;
74
+ const parts: string[] = [];
75
+ for (const block of content) {
76
+ if (block && typeof block === "object" && (block as { type?: unknown }).type === "text") {
77
+ const text = (block as { text?: unknown }).text;
78
+ if (typeof text === "string") parts.push(text);
79
+ }
80
+ }
81
+ const joined = parts.join("").trim();
82
+ return joined ? truncate(joined, max) : undefined;
83
+ }
84
+
85
+ /**
86
+ * Extract an idle summary from an `agent_end` event's settled message list: the
87
+ * last message that yields text (i.e. the final assistant message; tool-result
88
+ * messages have no text and are skipped).
89
+ *
90
+ * `agent_end` fires exactly once when the agent loop settles to await the user,
91
+ * so emitting idle from this — instead of per-`turn_end` — produces exactly one
92
+ * idle notification per genuine idle, eliminating the multi-turn flood.
93
+ */
94
+ export function summaryFromMessages(messages: unknown, max = 280): string | undefined {
95
+ if (!Array.isArray(messages)) return undefined;
96
+ for (let i = messages.length - 1; i >= 0; i--) {
97
+ const summary = summaryFromMessage(messages[i], max);
98
+ if (summary) return summary;
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ /** An agent-produced image extracted from a message's content. */
104
+ export interface ExtractedImage {
105
+ source: string;
106
+ mime: string;
107
+ data: string;
108
+ }
109
+
110
+ /**
111
+ * Extract agent-produced images (`{ type: "image", data, mimeType }` blocks)
112
+ * from a message's content — e.g. computer-use/browser screenshots or tool
113
+ * image outputs — for `image_attachment` delivery.
114
+ */
115
+ export function imageAttachmentsFromMessage(message: unknown, source = "agent"): ExtractedImage[] {
116
+ const content = (message as { content?: unknown } | null | undefined)?.content;
117
+ if (!Array.isArray(content)) return [];
118
+ const out: ExtractedImage[] = [];
119
+ for (const block of content) {
120
+ if (
121
+ block &&
122
+ typeof block === "object" &&
123
+ (block as { type?: unknown }).type === "image" &&
124
+ typeof (block as { data?: unknown }).data === "string" &&
125
+ typeof (block as { mimeType?: unknown }).mimeType === "string"
126
+ ) {
127
+ out.push({
128
+ source,
129
+ mime: (block as { mimeType: string }).mimeType,
130
+ data: (block as { data: string }).data,
131
+ });
132
+ }
133
+ }
134
+ return out;
135
+ }