@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.
- package/CHANGELOG.md +51 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -0
- package/src/commands/daemon.ts +47 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/migrate.ts +46 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +6 -1
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +247 -124
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +7 -1
- package/src/gjc-runtime/ultragoal-guard.ts +102 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +12 -3
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +9 -6
- package/src/main.ts +30 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +663 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +26 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +128 -24
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/agents.ts +1 -22
- package/src/task/executor.ts +5 -1
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +108 -16
- package/src/tools/computer.ts +58 -4
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- 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,
|
|
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);
|
package/src/modes/types.ts
CHANGED
|
@@ -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
|
+
}
|