@gajae-code/coding-agent 0.6.5 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -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/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/notify.d.ts +41 -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/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
- 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-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/rlm/index.d.ts +12 -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/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/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- 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/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/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
- package/src/gjc-runtime/launch-tmux.ts +10 -2
- package/src/gjc-runtime/state-runtime.ts +18 -4
- package/src/gjc-runtime/state-writer.ts +8 -8
- package/src/gjc-runtime/tmux-common.ts +8 -0
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/gjc-runtime/ultragoal-guard.ts +57 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +11 -1
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +9 -7
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- 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/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-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 +700 -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/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- 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/host-plugin-setup.ts +142 -0
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- 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/src/main.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import type { AgentSession } from "./session/agent-session";
|
|
48
48
|
import type { AuthStorage } from "./session/auth-storage";
|
|
49
49
|
import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
|
|
50
|
+
import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-import";
|
|
50
51
|
import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance";
|
|
51
52
|
import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
|
|
52
53
|
import { resolvePromptInput } from "./system-prompt";
|
|
@@ -856,6 +857,14 @@ export async function runRootCommand(
|
|
|
856
857
|
settingsInstance.get("theme.light"),
|
|
857
858
|
);
|
|
858
859
|
|
|
860
|
+
const credentialAutoImportNotice = isInteractive
|
|
861
|
+
? await logger.time("credentialAutoImport", runStartupCredentialAutoImportIfNeeded, {
|
|
862
|
+
authStorage,
|
|
863
|
+
modelRegistry,
|
|
864
|
+
agentDir: settingsInstance.getAgentDir(),
|
|
865
|
+
})
|
|
866
|
+
: undefined;
|
|
867
|
+
|
|
859
868
|
let scopedModels: ScopedModel[] = [];
|
|
860
869
|
const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
|
|
861
870
|
const modelMatchPreferences = {
|
|
@@ -895,6 +904,24 @@ export async function runRootCommand(
|
|
|
895
904
|
sessionManager = await SessionManager.open(selectedPath);
|
|
896
905
|
}
|
|
897
906
|
|
|
907
|
+
// Restore the resumed session's working directory so the HUD branch, the
|
|
908
|
+
// project path, and the agent's tools all match where the session was
|
|
909
|
+
// created. A `--worktree` session lives in a linked worktree whose path
|
|
910
|
+
// differs from where `--continue`/`--resume` is invoked, which would
|
|
911
|
+
// otherwise leave the HUD pinned to the main checkout's branch.
|
|
912
|
+
if (sessionManager && !parsedArgs.cwd) {
|
|
913
|
+
const sessionCwd = sessionManager.getCwd();
|
|
914
|
+
if (sessionCwd && normalizePathForComparison(sessionCwd) !== normalizePathForComparison(getProjectDir())) {
|
|
915
|
+
try {
|
|
916
|
+
if ((await fs.stat(sessionCwd)).isDirectory()) {
|
|
917
|
+
setProjectDir(sessionCwd);
|
|
918
|
+
}
|
|
919
|
+
} catch {
|
|
920
|
+
// Session cwd no longer exists (e.g. worktree removed); keep current dir.
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
898
925
|
const { options: sessionOptions } = await logger.time(
|
|
899
926
|
"buildSessionOptions",
|
|
900
927
|
buildSessionOptions,
|
|
@@ -976,6 +1003,9 @@ export async function runRootCommand(
|
|
|
976
1003
|
if (modelRegistryError) {
|
|
977
1004
|
notifs.push({ kind: "error", message: modelRegistryError.message });
|
|
978
1005
|
}
|
|
1006
|
+
if (credentialAutoImportNotice) {
|
|
1007
|
+
notifs.push({ kind: "info", message: credentialAutoImportNotice });
|
|
1008
|
+
}
|
|
979
1009
|
|
|
980
1010
|
if (isInteractive && !session.model && !modelFallbackMessage) {
|
|
981
1011
|
notifs.push({
|
|
@@ -244,6 +244,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
244
244
|
case "retry_fallback_succeeded":
|
|
245
245
|
case "ttsr_triggered":
|
|
246
246
|
case "irc_message":
|
|
247
|
+
case "subagent_steer_message":
|
|
247
248
|
case "notice":
|
|
248
249
|
case "thinking_level_changed":
|
|
249
250
|
case "goal_updated":
|
|
@@ -17,6 +17,10 @@ export interface HookEditorOptions {
|
|
|
17
17
|
promptStyle?: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function isWindowsRawLfNewlineInput(keyData: string): boolean {
|
|
21
|
+
return process.platform === "win32" && keyData === "\n";
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
export class HookEditorComponent extends Container {
|
|
21
25
|
#editor: Editor;
|
|
22
26
|
#onSubmitCallback: (value: string) => void;
|
|
@@ -92,8 +96,9 @@ export class HookEditorComponent extends Container {
|
|
|
92
96
|
return;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
// Submit on
|
|
96
|
-
|
|
99
|
+
// Submit on plain Enter encodings. On Windows, raw LF is reserved for terminal
|
|
100
|
+
// newline mappings (Shift+Enter/Ctrl+J/Ctrl+Enter); plain Enter reports CR.
|
|
101
|
+
if (!isWindowsRawLfNewlineInput(keyData) && (matchesKey(keyData, "enter") || matchesKey(keyData, "return"))) {
|
|
97
102
|
this.#onSubmitCallback(this.#editor.getText());
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
@@ -4,6 +4,7 @@ import { Container, matchesKey, Spacer, TruncatedText } from "@gajae-code/tui";
|
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
|
6
6
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
7
|
+
import type { ImportableCredential } from "../../setup/credential-import";
|
|
7
8
|
import { DynamicBorder } from "./dynamic-border";
|
|
8
9
|
|
|
9
10
|
const OAUTH_SELECTOR_MAX_VISIBLE = 10;
|
|
@@ -22,6 +23,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
22
23
|
#validateAuthCallback?: (providerId: string) => Promise<boolean>;
|
|
23
24
|
#requestRenderCallback?: () => void;
|
|
24
25
|
#authState: Map<string, "checking" | "valid" | "invalid"> = new Map();
|
|
26
|
+
#externalCredentialCandidates: ImportableCredential[] = [];
|
|
25
27
|
#spinnerFrame: number = 0;
|
|
26
28
|
#spinnerInterval?: NodeJS.Timeout;
|
|
27
29
|
#validationGeneration: number = 0;
|
|
@@ -33,6 +35,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
33
35
|
options?: {
|
|
34
36
|
validateAuth?: (providerId: string) => Promise<boolean>;
|
|
35
37
|
requestRender?: () => void;
|
|
38
|
+
externalCredentialCandidates?: ImportableCredential[];
|
|
36
39
|
},
|
|
37
40
|
) {
|
|
38
41
|
super();
|
|
@@ -42,6 +45,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
42
45
|
this.#onCancelCallback = onCancel;
|
|
43
46
|
this.#validateAuthCallback = options?.validateAuth;
|
|
44
47
|
this.#requestRenderCallback = options?.requestRender;
|
|
48
|
+
this.#externalCredentialCandidates = options?.externalCredentialCandidates ?? [];
|
|
45
49
|
// Load all OAuth providers
|
|
46
50
|
this.#loadProviders();
|
|
47
51
|
this.addChild(new DynamicBorder());
|
|
@@ -195,6 +199,21 @@ export class OAuthSelectorComponent extends Container {
|
|
|
195
199
|
this.#listContainer.addChild(new Spacer(1));
|
|
196
200
|
this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
|
|
197
201
|
}
|
|
202
|
+
if (this.#mode === "login" && this.#externalCredentialCandidates.length > 0) {
|
|
203
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
204
|
+
for (const credential of this.#externalCredentialCandidates) {
|
|
205
|
+
this.#listContainer.addChild(
|
|
206
|
+
new TruncatedText(
|
|
207
|
+
theme.fg(
|
|
208
|
+
"success",
|
|
209
|
+
` ${theme.status.success} Imported ${credential.provider} from ${credential.source}`,
|
|
210
|
+
),
|
|
211
|
+
0,
|
|
212
|
+
0,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
198
217
|
}
|
|
199
218
|
handleInput(keyData: string): void {
|
|
200
219
|
// Up arrow
|
|
@@ -83,6 +83,7 @@ export class EventController {
|
|
|
83
83
|
todo_reminder: e => this.#handleTodoReminder(e),
|
|
84
84
|
todo_auto_clear: e => this.#handleTodoAutoClear(e),
|
|
85
85
|
irc_message: e => this.#handleIrcMessage(e),
|
|
86
|
+
subagent_steer_message: e => this.#handleSubagentSteerMessage(e),
|
|
86
87
|
notice: e => this.#handleNotice(e),
|
|
87
88
|
thinking_level_changed: async () => {},
|
|
88
89
|
goal_updated: async () => {},
|
|
@@ -284,6 +285,25 @@ export class EventController {
|
|
|
284
285
|
this.ctx.ui.requestRender();
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
async #handleSubagentSteerMessage(
|
|
289
|
+
event: Extract<AgentSessionEvent, { type: "subagent_steer_message" }>,
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
const details = event.message.details as
|
|
292
|
+
| { observationId?: string; from?: string; to?: string; body?: string; state?: string }
|
|
293
|
+
| undefined;
|
|
294
|
+
const obsId = details?.observationId;
|
|
295
|
+
const signature = obsId
|
|
296
|
+
? `steer:${obsId}`
|
|
297
|
+
: `${event.message.role}:${event.message.customType}:${event.message.timestamp}:${details?.from}:${details?.to}:${details?.state}:${details?.body}`;
|
|
298
|
+
if (this.#renderedCustomMessages.has(signature)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.#renderedCustomMessages.add(signature);
|
|
302
|
+
this.#resetReadGroup();
|
|
303
|
+
this.ctx.addMessageToChat(event.message);
|
|
304
|
+
this.ctx.ui.requestRender();
|
|
305
|
+
}
|
|
306
|
+
|
|
287
307
|
#scheduleIrcExpiry(signature: string, components: Component[]): void {
|
|
288
308
|
if (components.length === 0 || this.#ircExpiryTimers.has(signature)) return;
|
|
289
309
|
const timer = setTimeout(() => {
|
|
@@ -29,10 +29,14 @@ import {
|
|
|
29
29
|
setTheme,
|
|
30
30
|
theme,
|
|
31
31
|
} from "../../modes/theme/theme";
|
|
32
|
-
import type { InteractiveModeContext } from "../../modes/types";
|
|
32
|
+
import type { InteractiveModeContext, OAuthSelectorOptions } from "../../modes/types";
|
|
33
33
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
34
34
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING,
|
|
37
|
+
runExternalCredentialAutoImport,
|
|
38
|
+
} from "../../setup/credential-auto-import";
|
|
39
|
+
import { filterAutoImportOAuthCredentials, formatDiscoverySummary } from "../../setup/credential-import";
|
|
36
40
|
import {
|
|
37
41
|
MODEL_ONBOARDING_API_PROVIDER_COMMAND,
|
|
38
42
|
MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
|
|
@@ -156,10 +160,22 @@ export class SelectorController {
|
|
|
156
160
|
|
|
157
161
|
async #handleCredentialImport(): Promise<void> {
|
|
158
162
|
this.ctx.showStatus("Scanning for existing Claude Code / Codex CLI credentials…");
|
|
159
|
-
const
|
|
160
|
-
|
|
163
|
+
const preview = await runExternalCredentialAutoImport({
|
|
164
|
+
authStorage: {
|
|
165
|
+
importCredentialIfAbsent: async () => ({
|
|
166
|
+
inserted: false,
|
|
167
|
+
reason: "skipped-existing",
|
|
168
|
+
provider: "",
|
|
169
|
+
entries: [],
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
trigger: "bare-login",
|
|
173
|
+
});
|
|
174
|
+
const result = preview.discovery ?? { importable: [], skipped: [], environment: [] };
|
|
175
|
+
const candidates = filterAutoImportOAuthCredentials(result.importable);
|
|
176
|
+
const summaryLines = formatDiscoverySummary({ ...result, importable: candidates });
|
|
161
177
|
|
|
162
|
-
if (
|
|
178
|
+
if (candidates.length === 0) {
|
|
163
179
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
164
180
|
for (const line of summaryLines) {
|
|
165
181
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
@@ -168,7 +184,7 @@ export class SelectorController {
|
|
|
168
184
|
new Text(
|
|
169
185
|
theme.fg(
|
|
170
186
|
"warning",
|
|
171
|
-
"No importable Claude/Codex credentials found. Use /login or add a custom provider.",
|
|
187
|
+
"No importable Claude/Codex OAuth credentials found. Use /login or add a custom provider.",
|
|
172
188
|
),
|
|
173
189
|
1,
|
|
174
190
|
0,
|
|
@@ -179,7 +195,7 @@ export class SelectorController {
|
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
const confirmed = await this.ctx.showHookConfirm(
|
|
182
|
-
`Import ${
|
|
198
|
+
`Import ${candidates.length} credential(s)?`,
|
|
183
199
|
summaryLines.join("\n"),
|
|
184
200
|
);
|
|
185
201
|
if (!confirmed) {
|
|
@@ -187,9 +203,10 @@ export class SelectorController {
|
|
|
187
203
|
return;
|
|
188
204
|
}
|
|
189
205
|
|
|
190
|
-
const summary = await
|
|
191
|
-
this.ctx.session.modelRegistry.authStorage
|
|
192
|
-
|
|
206
|
+
const summary = await runExternalCredentialAutoImport({
|
|
207
|
+
authStorage: this.ctx.session.modelRegistry.authStorage,
|
|
208
|
+
trigger: "bare-login",
|
|
209
|
+
});
|
|
193
210
|
await this.ctx.session.modelRegistry.refresh();
|
|
194
211
|
|
|
195
212
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
@@ -202,13 +219,15 @@ export class SelectorController {
|
|
|
202
219
|
),
|
|
203
220
|
);
|
|
204
221
|
}
|
|
205
|
-
for (const
|
|
222
|
+
for (const skip of summary.skipped) {
|
|
206
223
|
this.ctx.chatContainer.addChild(
|
|
207
|
-
new Text(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
new Text(theme.fg("dim", `${theme.status.info} Skipped ${skip.credential.provider}: ${skip.reason}`), 1, 0),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
for (const failure of summary.failures) {
|
|
228
|
+
const provider = failure.credential?.provider ?? failure.origin ?? "credential discovery";
|
|
229
|
+
this.ctx.chatContainer.addChild(
|
|
230
|
+
new Text(theme.fg("error", `${theme.status.error} Failed ${provider}: ${failure.failureClass}`), 1, 0),
|
|
212
231
|
);
|
|
213
232
|
}
|
|
214
233
|
if (summary.imported.length > 0) {
|
|
@@ -1232,7 +1251,11 @@ export class SelectorController {
|
|
|
1232
1251
|
}
|
|
1233
1252
|
}
|
|
1234
1253
|
|
|
1235
|
-
async showOAuthSelector(
|
|
1254
|
+
async showOAuthSelector(
|
|
1255
|
+
mode: "login" | "logout",
|
|
1256
|
+
providerId?: string,
|
|
1257
|
+
options?: OAuthSelectorOptions,
|
|
1258
|
+
): Promise<void> {
|
|
1236
1259
|
if (providerId) {
|
|
1237
1260
|
const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
|
|
1238
1261
|
if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
|
|
@@ -1259,6 +1282,45 @@ export class SelectorController {
|
|
|
1259
1282
|
}
|
|
1260
1283
|
}
|
|
1261
1284
|
|
|
1285
|
+
let externalCredentialCandidates: ReturnType<typeof filterAutoImportOAuthCredentials> = [];
|
|
1286
|
+
if (
|
|
1287
|
+
mode === "login" &&
|
|
1288
|
+
providerId === undefined &&
|
|
1289
|
+
options?.allowExternalCredentialDiscovery === true &&
|
|
1290
|
+
options.trigger === "bare-login"
|
|
1291
|
+
) {
|
|
1292
|
+
const preview = await runExternalCredentialAutoImport({
|
|
1293
|
+
authStorage: {
|
|
1294
|
+
importCredentialIfAbsent: async () => ({
|
|
1295
|
+
inserted: false,
|
|
1296
|
+
reason: "skipped-existing",
|
|
1297
|
+
provider: "",
|
|
1298
|
+
entries: [],
|
|
1299
|
+
}),
|
|
1300
|
+
},
|
|
1301
|
+
trigger: "bare-login",
|
|
1302
|
+
discover: options.externalCredentialDiscover,
|
|
1303
|
+
});
|
|
1304
|
+
const result = preview.discovery ?? { importable: [], skipped: [], environment: [] };
|
|
1305
|
+
const candidates = filterAutoImportOAuthCredentials(result.importable);
|
|
1306
|
+
if (candidates.length > 0) {
|
|
1307
|
+
const confirmed = await this.ctx.showHookConfirm(
|
|
1308
|
+
`Import ${candidates.length} external credential(s)?`,
|
|
1309
|
+
`${formatDiscoverySummary({ ...result, importable: candidates }).join("\n")}\n\n${CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING}`,
|
|
1310
|
+
);
|
|
1311
|
+
if (confirmed) {
|
|
1312
|
+
const summary = await runExternalCredentialAutoImport({
|
|
1313
|
+
authStorage: this.ctx.session.modelRegistry.authStorage,
|
|
1314
|
+
trigger: "bare-login",
|
|
1315
|
+
discover: options.externalCredentialDiscover,
|
|
1316
|
+
});
|
|
1317
|
+
externalCredentialCandidates = summary.imported;
|
|
1318
|
+
if (externalCredentialCandidates.length > 0) {
|
|
1319
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1262
1324
|
this.showSelector(done => {
|
|
1263
1325
|
let selector: OAuthSelectorComponent;
|
|
1264
1326
|
selector = new OAuthSelectorComponent(
|
|
@@ -1289,6 +1351,7 @@ export class SelectorController {
|
|
|
1289
1351
|
requestRender: () => {
|
|
1290
1352
|
this.ctx.ui.requestRender();
|
|
1291
1353
|
},
|
|
1354
|
+
externalCredentialCandidates,
|
|
1292
1355
|
},
|
|
1293
1356
|
);
|
|
1294
1357
|
return { component: selector, focus: selector };
|
|
@@ -2502,8 +2502,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2502
2502
|
return this.#selectorController.handleSessionDeleteCommand();
|
|
2503
2503
|
}
|
|
2504
2504
|
|
|
2505
|
-
showOAuthSelector(
|
|
2506
|
-
|
|
2505
|
+
showOAuthSelector(
|
|
2506
|
+
mode: "login" | "logout",
|
|
2507
|
+
providerId?: string,
|
|
2508
|
+
options?: import("./types").OAuthSelectorOptions,
|
|
2509
|
+
): Promise<void> {
|
|
2510
|
+
return this.#selectorController.showOAuthSelector(mode, providerId, options);
|
|
2507
2511
|
}
|
|
2508
2512
|
|
|
2509
2513
|
showHookConfirm(title: string, message: string): Promise<boolean> {
|
|
@@ -78,6 +78,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
78
78
|
shutdown,
|
|
79
79
|
getContextUsage: () => session.getContextUsage(),
|
|
80
80
|
getSystemPrompt: () => session.systemPrompt,
|
|
81
|
+
getWorkflowGate: () => session.getWorkflowGateEmitter(),
|
|
81
82
|
compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
|
|
82
83
|
},
|
|
83
84
|
// ExtensionCommandContextActions — commands invokable via prompt("/command")
|
|
@@ -50,6 +50,7 @@ export function agentSessionEventType(event: AgentSessionEvent): AgentWireEventT
|
|
|
50
50
|
case "todo_reminder":
|
|
51
51
|
case "todo_auto_clear":
|
|
52
52
|
case "irc_message":
|
|
53
|
+
case "subagent_steer_message":
|
|
53
54
|
case "notice":
|
|
54
55
|
case "thinking_level_changed":
|
|
55
56
|
case "goal_updated":
|
|
@@ -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, {
|
|
@@ -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
|
+
}
|