@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/rlm/index.ts
CHANGED
|
@@ -240,8 +240,27 @@ async function writeRlmMetadata(input: {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* RLM artifacts are scoped under a GJC session directory and resolving their
|
|
245
|
+
* paths is a *write* (it must pick a concrete session). When `gjc rlm` runs
|
|
246
|
+
* standalone — no parent agent, no `GJC_SESSION_ID` in the environment — there is
|
|
247
|
+
* no session to resolve and `resolveGjcSessionForWrite` throws
|
|
248
|
+
* `missing_for_write`. Establish a dedicated GJC session id in that case and pin
|
|
249
|
+
* it into the environment so artifact-path resolution, the per-session activity
|
|
250
|
+
* marker, and the child agent's workflow state all share one writable session.
|
|
251
|
+
*
|
|
252
|
+
* Returns the resolved (existing or freshly generated) GJC session id.
|
|
253
|
+
*/
|
|
254
|
+
export function ensureRlmGjcSessionId(): string {
|
|
255
|
+
const existing = resolveSessionIdFromSources({ envSessionId: process.env.GJC_SESSION_ID })?.gjcSessionId;
|
|
256
|
+
if (existing) return existing;
|
|
257
|
+
const generated = `rlm-${generateRlmSessionId()}`;
|
|
258
|
+
process.env.GJC_SESSION_ID = generated;
|
|
259
|
+
return generated;
|
|
260
|
+
}
|
|
243
261
|
export async function runRlmCommand(argv: string[]): Promise<void> {
|
|
244
262
|
const cwd = getProjectDir();
|
|
263
|
+
ensureRlmGjcSessionId();
|
|
245
264
|
const { dataPath, resumeSessionId, minSuccessfulRuns, rest } = extractRlmFlags(argv);
|
|
246
265
|
const dataContext = await loadRlmDataContext(cwd, dataPath);
|
|
247
266
|
|
package/src/sdk.ts
CHANGED
|
@@ -79,6 +79,12 @@ import type { HindsightSessionState } from "./hindsight/state";
|
|
|
79
79
|
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
80
80
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
81
81
|
import { resolveMemoryBackend } from "./memory-backend";
|
|
82
|
+
import { createNotificationsExtension } from "./notifications";
|
|
83
|
+
import {
|
|
84
|
+
getNotificationConfig,
|
|
85
|
+
type NotificationConfig,
|
|
86
|
+
shouldRegisterNotificationsExtension,
|
|
87
|
+
} from "./notifications/config";
|
|
82
88
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
83
89
|
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
84
90
|
import { MCPManager } from "./runtime-mcp";
|
|
@@ -1238,6 +1244,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1238
1244
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
1239
1245
|
getGoalModeState: () => session?.getGoalModeState(),
|
|
1240
1246
|
getWorkflowGateEmitter: () => session?.getWorkflowGateEmitter(),
|
|
1247
|
+
getAskAnswerSource: () => session?.getAskAnswerSource(),
|
|
1241
1248
|
getGoalRuntime: () => session?.goalRuntime,
|
|
1242
1249
|
getClientBridge: () => session?.clientBridge,
|
|
1243
1250
|
getCompactContext: () => session.formatCompactContext(),
|
|
@@ -1386,6 +1393,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1386
1393
|
if (customTools.length > 0) {
|
|
1387
1394
|
inlineExtensions.push(createCustomToolsExtension(customTools));
|
|
1388
1395
|
}
|
|
1396
|
+
let notificationCfg: NotificationConfig | undefined;
|
|
1397
|
+
try {
|
|
1398
|
+
notificationCfg = getNotificationConfig(Settings.instance);
|
|
1399
|
+
} catch {
|
|
1400
|
+
notificationCfg = undefined;
|
|
1401
|
+
}
|
|
1402
|
+
if (shouldRegisterNotificationsExtension({ env: process.env, cfg: notificationCfg })) {
|
|
1403
|
+
inlineExtensions.push(createNotificationsExtension);
|
|
1404
|
+
}
|
|
1389
1405
|
|
|
1390
1406
|
// Extension/module discovery is quarantined; retain only the private
|
|
1391
1407
|
// runtime needed for bundled product extensions, explicitly supplied SDK
|
|
@@ -238,8 +238,9 @@ import {
|
|
|
238
238
|
type DiscoverableTool,
|
|
239
239
|
type DiscoverableToolSearchIndex,
|
|
240
240
|
} from "../tool-discovery/tool-index";
|
|
241
|
-
import type { ToolSession } from "../tools";
|
|
241
|
+
import type { AskAnswerSource, ToolSession } from "../tools";
|
|
242
242
|
import { AskTool } from "../tools/ask";
|
|
243
|
+
import { getAskAnswerSource as getAskAnswerSourceFromRegistry } from "../tools/ask-answer-registry";
|
|
243
244
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
244
245
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
245
246
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
@@ -313,6 +314,7 @@ export type AgentSessionEvent =
|
|
|
313
314
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
314
315
|
| { type: "todo_auto_clear" }
|
|
315
316
|
| { type: "irc_message"; message: CustomMessage }
|
|
317
|
+
| { type: "subagent_steer_message"; message: CustomMessage }
|
|
316
318
|
| { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string }
|
|
317
319
|
| { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
|
|
318
320
|
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
@@ -4385,6 +4387,10 @@ export class AgentSession {
|
|
|
4385
4387
|
return this.#workflowGateEmitter;
|
|
4386
4388
|
}
|
|
4387
4389
|
|
|
4390
|
+
getAskAnswerSource(): AskAnswerSource | undefined {
|
|
4391
|
+
return getAskAnswerSourceFromRegistry(this.sessionId);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4388
4394
|
setWorkflowGateEmitter(emitter: WorkflowGateEmitter | undefined): void {
|
|
4389
4395
|
this.#workflowGateEmitter = emitter;
|
|
4390
4396
|
if (emitter) {
|
|
@@ -5453,6 +5459,16 @@ export class AgentSession {
|
|
|
5453
5459
|
return;
|
|
5454
5460
|
}
|
|
5455
5461
|
|
|
5462
|
+
// No explicit delivery mode: only a live stream makes prompt() throw
|
|
5463
|
+
// AgentBusyError, so queue the message as steering while streaming.
|
|
5464
|
+
// Compaction is intentionally NOT diverted here: prompt() handles an
|
|
5465
|
+
// in-flight compaction internally, and #queueSteer would otherwise park
|
|
5466
|
+
// the message in the steering queue with no turn to consume it.
|
|
5467
|
+
if (this.isStreaming) {
|
|
5468
|
+
await this.#queueSteer(text, images);
|
|
5469
|
+
return;
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5456
5472
|
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
|
|
5457
5473
|
await this.prompt(text, {
|
|
5458
5474
|
expandPromptTemplates: false,
|
|
@@ -5856,12 +5872,46 @@ export class AgentSession {
|
|
|
5856
5872
|
return this.#activeModelProfile;
|
|
5857
5873
|
}
|
|
5858
5874
|
|
|
5875
|
+
/**
|
|
5876
|
+
* The model selector ("provider/id") that resume restores as the session
|
|
5877
|
+
* default — the latest session-log `model_change` with role="default".
|
|
5878
|
+
* Model-profile activation snapshots this before mutating the session so a
|
|
5879
|
+
* failed-activation rollback can restore the pre-activation resume default
|
|
5880
|
+
* instead of promoting a transient runtime model to the resume default.
|
|
5881
|
+
*/
|
|
5882
|
+
getSessionDefaultModelSelector(): string | undefined {
|
|
5883
|
+
return this.sessionManager.buildSessionContext().models.default;
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
/**
|
|
5887
|
+
* Re-assert the session resume default ("provider/id") in the session log
|
|
5888
|
+
* WITHOUT touching the live runtime model. Appends a `model_change` with
|
|
5889
|
+
* role="default"; never writes to global settings (apply-for-this-session
|
|
5890
|
+
* semantics). Used by model-profile activation rollback to neutralize the
|
|
5891
|
+
* profile main model the failed activation already recorded as the default.
|
|
5892
|
+
*/
|
|
5893
|
+
recordResumeDefaultModel(selector: string): void {
|
|
5894
|
+
this.sessionManager.appendModelChange(selector, "default");
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5859
5897
|
/**
|
|
5860
5898
|
* Set model temporarily (for this session only).
|
|
5861
5899
|
* Validates API key, saves to session log but NOT to settings.
|
|
5900
|
+
*
|
|
5901
|
+
* The change is recorded in the session log as `role: "temporary"` by
|
|
5902
|
+
* default, which means it is NOT restored as the session default on resume —
|
|
5903
|
+
* transient retry/fallback/context-promotion/plan switches must not clobber
|
|
5904
|
+
* the user's explicit pick (issue #849). Model-profile activation passes
|
|
5905
|
+
* `persistAsSessionDefault: true` so the profile's main model becomes the
|
|
5906
|
+
* session default and survives resume, while still not being written to
|
|
5907
|
+
* global settings (new sessions keep the global default).
|
|
5862
5908
|
* @throws Error if no API key available for the model
|
|
5863
5909
|
*/
|
|
5864
|
-
async setModelTemporary(
|
|
5910
|
+
async setModelTemporary(
|
|
5911
|
+
model: Model,
|
|
5912
|
+
thinkingLevel?: ThinkingLevel,
|
|
5913
|
+
options?: { persistAsSessionDefault?: boolean },
|
|
5914
|
+
): Promise<void> {
|
|
5865
5915
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
5866
5916
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
5867
5917
|
if (!apiKey) {
|
|
@@ -5870,7 +5920,10 @@ export class AgentSession {
|
|
|
5870
5920
|
|
|
5871
5921
|
this.#clearActiveRetryFallback();
|
|
5872
5922
|
this.#setModelWithProviderSessionReset(model);
|
|
5873
|
-
this.sessionManager.appendModelChange(
|
|
5923
|
+
this.sessionManager.appendModelChange(
|
|
5924
|
+
`${model.provider}/${model.id}`,
|
|
5925
|
+
options?.persistAsSessionDefault ? "default" : "temporary",
|
|
5926
|
+
);
|
|
5874
5927
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
5875
5928
|
|
|
5876
5929
|
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
@@ -9089,6 +9142,63 @@ export class AgentSession {
|
|
|
9089
9142
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9090
9143
|
}
|
|
9091
9144
|
|
|
9145
|
+
emitSubagentSteerObservation(args: { from: string; to: string; body: string; timestamp?: number }): void {
|
|
9146
|
+
const timestamp = args.timestamp ?? Date.now();
|
|
9147
|
+
const observationId = crypto.randomUUID();
|
|
9148
|
+
const message: CustomMessage = {
|
|
9149
|
+
role: "custom",
|
|
9150
|
+
customType: "subagent:steer",
|
|
9151
|
+
content: `[Steer \`${args.from}\` ⇨ \`${args.to}\` (queued)]\n\n${args.body}`,
|
|
9152
|
+
display: true,
|
|
9153
|
+
details: { observationId, from: args.from, to: args.to, body: args.body, state: "queued" },
|
|
9154
|
+
attribution: "agent",
|
|
9155
|
+
timestamp,
|
|
9156
|
+
};
|
|
9157
|
+
void this.#emitSessionEvent({ type: "subagent_steer_message", message });
|
|
9158
|
+
this.#forwardSubagentSteerRelayToMain({
|
|
9159
|
+
from: args.from,
|
|
9160
|
+
to: args.to,
|
|
9161
|
+
body: args.body,
|
|
9162
|
+
observationId,
|
|
9163
|
+
timestamp,
|
|
9164
|
+
});
|
|
9165
|
+
}
|
|
9166
|
+
|
|
9167
|
+
#forwardSubagentSteerRelayToMain(args: {
|
|
9168
|
+
from: string;
|
|
9169
|
+
to: string;
|
|
9170
|
+
body: string;
|
|
9171
|
+
observationId: string;
|
|
9172
|
+
timestamp: number;
|
|
9173
|
+
}): void {
|
|
9174
|
+
const registry = this.#agentRegistry;
|
|
9175
|
+
if (!registry) return;
|
|
9176
|
+
if (this.#agentId === MAIN_AGENT_ID) return;
|
|
9177
|
+
const mainRef = registry.get(MAIN_AGENT_ID);
|
|
9178
|
+
const mainSession = mainRef?.session;
|
|
9179
|
+
if (!mainSession || mainSession === this) return;
|
|
9180
|
+
const record: CustomMessage = {
|
|
9181
|
+
role: "custom",
|
|
9182
|
+
customType: "subagent:steer:relay",
|
|
9183
|
+
content: `[Steer \`${args.from}\` ⇨ \`${args.to}\` (queued)]\n\n${args.body}`,
|
|
9184
|
+
display: true,
|
|
9185
|
+
details: {
|
|
9186
|
+
observationId: args.observationId,
|
|
9187
|
+
from: args.from,
|
|
9188
|
+
to: args.to,
|
|
9189
|
+
body: args.body,
|
|
9190
|
+
state: "queued",
|
|
9191
|
+
},
|
|
9192
|
+
attribution: "agent",
|
|
9193
|
+
timestamp: args.timestamp,
|
|
9194
|
+
};
|
|
9195
|
+
mainSession.emitSubagentSteerRelayObservation(record);
|
|
9196
|
+
}
|
|
9197
|
+
|
|
9198
|
+
emitSubagentSteerRelayObservation(record: CustomMessage): void {
|
|
9199
|
+
void this.#emitSessionEvent({ type: "subagent_steer_message", message: record });
|
|
9200
|
+
}
|
|
9201
|
+
|
|
9092
9202
|
/**
|
|
9093
9203
|
* Run a single ephemeral side-channel turn against this session's current
|
|
9094
9204
|
* model + system prompt + history. No tools are used; the side request
|
|
@@ -47,13 +47,54 @@ function stripTypeBoxFields(obj: unknown): unknown {
|
|
|
47
47
|
return obj;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function escapeXmlAttribute(input: string): string {
|
|
51
|
+
return input.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeXmlText(input: string): string {
|
|
55
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decodeCodePoint(hex: string): string {
|
|
59
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
60
|
+
if (
|
|
61
|
+
!Number.isFinite(codePoint) ||
|
|
62
|
+
codePoint < 0x20 ||
|
|
63
|
+
(codePoint >= 0x7f && codePoint <= 0x9f) ||
|
|
64
|
+
(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
65
|
+
) {
|
|
66
|
+
return `\\u${hex}`;
|
|
67
|
+
}
|
|
68
|
+
return String.fromCharCode(codePoint);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function decodeUnicodeEscapeText(input: string): string {
|
|
72
|
+
return input
|
|
73
|
+
.replace(/\\\\u([0-9a-fA-F]{4})/g, (_match, hex: string) => decodeCodePoint(hex))
|
|
74
|
+
.replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex: string) => decodeCodePoint(hex));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatParameterValue(value: unknown): string {
|
|
78
|
+
const raw = typeof value === "string" ? value : (JSON.stringify(value, null, "\t") ?? "null");
|
|
79
|
+
return escapeXmlText(decodeUnicodeEscapeText(raw));
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
/** Serialize an object as XML parameter elements, one per key. */
|
|
51
83
|
function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
|
|
52
84
|
const parts: string[] = [];
|
|
53
85
|
for (const [key, value] of Object.entries(args)) {
|
|
54
86
|
if (key === INTENT_FIELD) continue;
|
|
55
|
-
const
|
|
56
|
-
|
|
87
|
+
const escapedKey = escapeXmlAttribute(key);
|
|
88
|
+
const text = formatParameterValue(value);
|
|
89
|
+
if (text.includes("\n")) {
|
|
90
|
+
const indentedText = text
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map(line => `${indent}\t${line}`)
|
|
93
|
+
.join("\n");
|
|
94
|
+
parts.push(`${indent}<parameter name="${escapedKey}">\n${indentedText}\n${indent}</parameter>`);
|
|
95
|
+
} else {
|
|
96
|
+
parts.push(`${indent}<parameter name="${escapedKey}">${text}</parameter>`);
|
|
97
|
+
}
|
|
57
98
|
}
|
|
58
99
|
return parts.join("\n");
|
|
59
100
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
toError,
|
|
29
29
|
} from "@gajae-code/utils";
|
|
30
30
|
import { writeTextAtomic } from "../gjc-runtime/state-writer";
|
|
31
|
+
import * as git from "../utils/git";
|
|
31
32
|
import { ArtifactManager } from "./artifacts";
|
|
32
33
|
import {
|
|
33
34
|
type BlobPutResult,
|
|
@@ -791,6 +792,25 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
|
|
|
791
792
|
write.catch(() => {});
|
|
792
793
|
}
|
|
793
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Two paths belong to linked worktrees of the same repository when they share a
|
|
797
|
+
* git common dir but resolve to different git dirs (i.e. one is a `git worktree`
|
|
798
|
+
* of the other). `--worktree` sessions run from such a linked worktree, so a
|
|
799
|
+
* `--continue` from the main checkout should still resolve their breadcrumb.
|
|
800
|
+
*/
|
|
801
|
+
function isLinkedWorktreePeer(a: string, b: string): boolean {
|
|
802
|
+
const ra = git.repo.resolveSync(a);
|
|
803
|
+
const rb = git.repo.resolveSync(b);
|
|
804
|
+
if (ra === null || rb === null) return false;
|
|
805
|
+
// Canonicalize: a worktree's commondir is stored as an absolute path that may
|
|
806
|
+
// differ from the main checkout only by a symlink prefix (e.g. macOS
|
|
807
|
+
// /tmp -> /private/tmp), so compare resolved-equivalent paths.
|
|
808
|
+
return (
|
|
809
|
+
resolveEquivalentPath(ra.commonDir) === resolveEquivalentPath(rb.commonDir) &&
|
|
810
|
+
resolveEquivalentPath(ra.gitDir) !== resolveEquivalentPath(rb.gitDir)
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
794
814
|
/**
|
|
795
815
|
* Read the terminal breadcrumb for the current terminal, scoped to a cwd.
|
|
796
816
|
* Returns the session file path if it exists and matches the cwd, null otherwise.
|
|
@@ -808,8 +828,12 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
808
828
|
const breadcrumbCwd = lines[0];
|
|
809
829
|
const sessionFile = lines[1];
|
|
810
830
|
|
|
811
|
-
//
|
|
812
|
-
|
|
831
|
+
// Honor the breadcrumb when the cwd matches, or when it points to a linked
|
|
832
|
+
// worktree of the same repository (e.g. a `--worktree` session resumed from
|
|
833
|
+
// the main checkout). A genuinely different project is still ignored.
|
|
834
|
+
if (path.resolve(breadcrumbCwd) !== path.resolve(cwd) && !isLinkedWorktreePeer(breadcrumbCwd, cwd)) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
813
837
|
|
|
814
838
|
// Verify the session file still exists
|
|
815
839
|
const stat = fs.statSync(sessionFile, { throwIfNoEntry: false });
|
|
@@ -4010,12 +4034,22 @@ export class SessionManager {
|
|
|
4010
4034
|
// Prefer terminal-scoped breadcrumb (handles concurrent sessions correctly)
|
|
4011
4035
|
const terminalSession = await readTerminalBreadcrumb(cwd);
|
|
4012
4036
|
const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
|
|
4013
|
-
const manager = new SessionManager(cwd, dir, true, storage);
|
|
4014
4037
|
if (mostRecent) {
|
|
4038
|
+
// Adopt the resumed session's recorded cwd and its own directory. A
|
|
4039
|
+
// `--worktree` session lives in a linked worktree whose path differs from
|
|
4040
|
+
// the invocation cwd; binding the manager (and HUD) to `cwd` would leave
|
|
4041
|
+
// it on the main checkout instead of the worktree it was created in.
|
|
4042
|
+
const header = (await loadEntriesFromFile(mostRecent, storage)).find(e => e.type === "session") as
|
|
4043
|
+
| SessionHeader
|
|
4044
|
+
| undefined;
|
|
4045
|
+
const resumeCwd = header?.cwd || cwd;
|
|
4046
|
+
const resumeDir = sessionDir ?? path.resolve(mostRecent, "..");
|
|
4047
|
+
const manager = new SessionManager(resumeCwd, resumeDir, true, storage);
|
|
4015
4048
|
await manager.#initSessionFile(mostRecent);
|
|
4016
|
-
|
|
4017
|
-
manager.#initNewSession();
|
|
4049
|
+
return manager;
|
|
4018
4050
|
}
|
|
4051
|
+
const manager = new SessionManager(cwd, dir, true, storage);
|
|
4052
|
+
manager.#initNewSession();
|
|
4019
4053
|
return manager;
|
|
4020
4054
|
}
|
|
4021
4055
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
AuthCredential,
|
|
5
|
+
AuthCredentialIfAbsentReason,
|
|
6
|
+
AuthCredentialIfAbsentSnapshotResult,
|
|
7
|
+
AuthStorage,
|
|
8
|
+
} from "@gajae-code/ai";
|
|
9
|
+
import { getAgentDir, logger, VERSION } from "@gajae-code/utils";
|
|
10
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type CredentialDiscoveryResult,
|
|
14
|
+
type CredentialOrigin,
|
|
15
|
+
type DiscoveryOptions,
|
|
16
|
+
discoverExternalCredentials,
|
|
17
|
+
EXTERNAL_PROVIDER_LABELS,
|
|
18
|
+
type ExternalProvider,
|
|
19
|
+
filterAutoImportOAuthCredentials,
|
|
20
|
+
formatCredentialSummary,
|
|
21
|
+
type ImportableCredential,
|
|
22
|
+
} from "./credential-import";
|
|
23
|
+
|
|
24
|
+
export const CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING =
|
|
25
|
+
"Refreshing in gjc may log out the Claude/Codex CLI because OAuth refresh tokens can rotate.";
|
|
26
|
+
|
|
27
|
+
export type CredentialAutoImportSourceLabel = "claude-code-file" | "claude-code-keychain" | "codex-file";
|
|
28
|
+
export type CredentialAutoImportTrigger = "startup" | "bare-login" | "setup-cli";
|
|
29
|
+
|
|
30
|
+
const CREDENTIAL_AUTO_IMPORT_STATE_FILENAME = "credential-auto-import-state.json";
|
|
31
|
+
|
|
32
|
+
interface CredentialAutoImportStateFile {
|
|
33
|
+
lastImportVersion?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getCredentialAutoImportStatePath(agentDir: string = getAgentDir()): string {
|
|
37
|
+
return path.join(agentDir, CREDENTIAL_AUTO_IMPORT_STATE_FILENAME);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function readCredentialImportMarker(agentDir?: string): Promise<string | undefined> {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await fs.readFile(getCredentialAutoImportStatePath(agentDir), "utf-8");
|
|
43
|
+
const parsed = JSON.parse(raw) as CredentialAutoImportStateFile;
|
|
44
|
+
return typeof parsed.lastImportVersion === "string" ? parsed.lastImportVersion : undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function writeCredentialImportMarker(version: string, agentDir?: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const statePath = getCredentialAutoImportStatePath(agentDir);
|
|
53
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
54
|
+
await fs.writeFile(statePath, `${JSON.stringify({ lastImportVersion: version })}\n`);
|
|
55
|
+
return true;
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
logger.warn("Failed to persist credential auto-import state", { error });
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export enum CredentialAutoImportFailureClass {
|
|
63
|
+
DiscoveryUnavailable = "discovery-unavailable",
|
|
64
|
+
SourceUnreadable = "source-unreadable",
|
|
65
|
+
SourceMalformed = "source-malformed",
|
|
66
|
+
KeychainDenied = "keychain-denied",
|
|
67
|
+
WriteInvalid = "write-invalid",
|
|
68
|
+
WriteConflict = "write-conflict",
|
|
69
|
+
BrokerUnavailable = "broker-unavailable",
|
|
70
|
+
BrokerUnsupported = "broker-unsupported",
|
|
71
|
+
Unknown = "unknown",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CredentialAutoImportSkipped {
|
|
75
|
+
credential: ImportableCredential;
|
|
76
|
+
reason: AuthCredentialIfAbsentReason;
|
|
77
|
+
entries: AuthCredentialIfAbsentSnapshotResult["entries"];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CredentialAutoImportFailure {
|
|
81
|
+
credential?: ImportableCredential;
|
|
82
|
+
origin?: CredentialOrigin;
|
|
83
|
+
source?: string;
|
|
84
|
+
failureClass: CredentialAutoImportFailureClass;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CredentialAutoImportResult {
|
|
88
|
+
imported: ImportableCredential[];
|
|
89
|
+
skipped: CredentialAutoImportSkipped[];
|
|
90
|
+
failures: CredentialAutoImportFailure[];
|
|
91
|
+
discovered: boolean;
|
|
92
|
+
discovery?: CredentialDiscoveryResult;
|
|
93
|
+
globalDiscoveryFailure?: CredentialAutoImportFailure;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type CredentialAutoImportAuthStorage = Pick<AuthStorage, "importCredentialIfAbsent">;
|
|
97
|
+
|
|
98
|
+
export interface CredentialAutoImportOptions {
|
|
99
|
+
authStorage: CredentialAutoImportAuthStorage;
|
|
100
|
+
discover?: (options?: DiscoveryOptions) => Promise<CredentialDiscoveryResult>;
|
|
101
|
+
discoveryOptions?: DiscoveryOptions;
|
|
102
|
+
trigger: CredentialAutoImportTrigger;
|
|
103
|
+
sourceLabel?: CredentialAutoImportSourceLabel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function classifyDiscoverySkip(reason: string, origin: CredentialOrigin): CredentialAutoImportFailureClass {
|
|
107
|
+
const lower = reason.toLowerCase();
|
|
108
|
+
if (
|
|
109
|
+
origin === "claude-code-keychain" &&
|
|
110
|
+
(lower.includes("eacces") || lower.includes("eperm") || lower.includes("denied"))
|
|
111
|
+
) {
|
|
112
|
+
return CredentialAutoImportFailureClass.KeychainDenied;
|
|
113
|
+
}
|
|
114
|
+
if (lower.includes("malformed")) return CredentialAutoImportFailureClass.SourceMalformed;
|
|
115
|
+
if (lower.includes("unreadable")) return CredentialAutoImportFailureClass.SourceUnreadable;
|
|
116
|
+
return CredentialAutoImportFailureClass.Unknown;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function classifyWriteFailure(error: unknown): CredentialAutoImportFailureClass {
|
|
120
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
121
|
+
if (message.includes("invalid")) return CredentialAutoImportFailureClass.WriteInvalid;
|
|
122
|
+
if (message.includes("conflict") || message.includes("constraint"))
|
|
123
|
+
return CredentialAutoImportFailureClass.WriteConflict;
|
|
124
|
+
if (
|
|
125
|
+
message.includes("broker") &&
|
|
126
|
+
(message.includes("unsupported") || message.includes("404") || message.includes("501"))
|
|
127
|
+
) {
|
|
128
|
+
return CredentialAutoImportFailureClass.BrokerUnsupported;
|
|
129
|
+
}
|
|
130
|
+
if (message.includes("broker") || message.includes("fetch") || message.includes("network")) {
|
|
131
|
+
return CredentialAutoImportFailureClass.BrokerUnavailable;
|
|
132
|
+
}
|
|
133
|
+
return CredentialAutoImportFailureClass.Unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function runExternalCredentialAutoImport({
|
|
137
|
+
authStorage,
|
|
138
|
+
discover = discoverExternalCredentials,
|
|
139
|
+
discoveryOptions,
|
|
140
|
+
}: CredentialAutoImportOptions): Promise<CredentialAutoImportResult> {
|
|
141
|
+
let discovery: CredentialDiscoveryResult;
|
|
142
|
+
try {
|
|
143
|
+
discovery = await discover(discoveryOptions);
|
|
144
|
+
} catch {
|
|
145
|
+
const globalDiscoveryFailure = { failureClass: CredentialAutoImportFailureClass.DiscoveryUnavailable };
|
|
146
|
+
return {
|
|
147
|
+
imported: [],
|
|
148
|
+
skipped: [],
|
|
149
|
+
failures: [globalDiscoveryFailure],
|
|
150
|
+
discovered: false,
|
|
151
|
+
globalDiscoveryFailure,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const candidates = filterAutoImportOAuthCredentials(discovery.importable);
|
|
156
|
+
const failures: CredentialAutoImportFailure[] = discovery.skipped.map(skip => ({
|
|
157
|
+
origin: skip.origin,
|
|
158
|
+
source: skip.source,
|
|
159
|
+
failureClass: classifyDiscoverySkip(skip.reason, skip.origin),
|
|
160
|
+
}));
|
|
161
|
+
const imported: ImportableCredential[] = [];
|
|
162
|
+
const skipped: CredentialAutoImportSkipped[] = [];
|
|
163
|
+
const importIfAbsent = authStorage.importCredentialIfAbsent;
|
|
164
|
+
|
|
165
|
+
for (const credential of candidates) {
|
|
166
|
+
try {
|
|
167
|
+
const outcome = await importIfAbsent.call(
|
|
168
|
+
authStorage,
|
|
169
|
+
credential.provider,
|
|
170
|
+
credential.credential as AuthCredential,
|
|
171
|
+
);
|
|
172
|
+
if (outcome.inserted === true) {
|
|
173
|
+
imported.push(credential);
|
|
174
|
+
} else {
|
|
175
|
+
skipped.push({ credential, reason: outcome.reason, entries: outcome.entries });
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
failures.push({ credential, failureClass: classifyWriteFailure(error) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { imported, skipped, failures, discovered: true, discovery };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildCredentialAutoImportNotice(
|
|
186
|
+
result: Pick<CredentialAutoImportResult, "imported">,
|
|
187
|
+
): string | undefined {
|
|
188
|
+
if (result.imported.length === 0) return undefined;
|
|
189
|
+
const providers = [
|
|
190
|
+
...new Set(result.imported.map(c => EXTERNAL_PROVIDER_LABELS[c.provider as ExternalProvider] ?? c.provider)),
|
|
191
|
+
];
|
|
192
|
+
const success = `Imported ${result.imported.length} external OAuth credential(s) into gjc: ${providers.join(", ")}.`;
|
|
193
|
+
return `${success}\n${CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatCredentialAutoImportResult(result: CredentialAutoImportResult): string[] {
|
|
197
|
+
const lines: string[] = [];
|
|
198
|
+
for (const credential of result.imported) lines.push(`imported ${formatCredentialSummary(credential)}`);
|
|
199
|
+
for (const skip of result.skipped) lines.push(`skipped ${skip.credential.source}: ${skip.reason}`);
|
|
200
|
+
for (const failure of result.failures) {
|
|
201
|
+
const label = failure.credential?.source ?? failure.source ?? "external credential discovery";
|
|
202
|
+
lines.push(`failed ${label}: ${failure.failureClass}`);
|
|
203
|
+
}
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface CredentialImportMarkerStore {
|
|
208
|
+
read: () => Promise<string | undefined> | string | undefined;
|
|
209
|
+
write: (version: string) => Promise<boolean> | boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface StartupCredentialAutoImportOptions {
|
|
213
|
+
authStorage: CredentialAutoImportOptions["authStorage"];
|
|
214
|
+
modelRegistry: Pick<ModelRegistry, "refresh">;
|
|
215
|
+
discover?: CredentialAutoImportOptions["discover"];
|
|
216
|
+
version?: string;
|
|
217
|
+
agentDir?: string;
|
|
218
|
+
markerStore?: CredentialImportMarkerStore;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function runStartupCredentialAutoImportIfNeeded({
|
|
222
|
+
authStorage: activeAuthStorage,
|
|
223
|
+
modelRegistry: activeModelRegistry,
|
|
224
|
+
discover,
|
|
225
|
+
version = VERSION,
|
|
226
|
+
agentDir,
|
|
227
|
+
markerStore,
|
|
228
|
+
}: StartupCredentialAutoImportOptions): Promise<string | undefined> {
|
|
229
|
+
const store = markerStore ?? {
|
|
230
|
+
read: () => readCredentialImportMarker(agentDir),
|
|
231
|
+
write: (nextVersion: string) => writeCredentialImportMarker(nextVersion, agentDir),
|
|
232
|
+
};
|
|
233
|
+
const lastVersion = await store.read();
|
|
234
|
+
if (lastVersion === version) {
|
|
235
|
+
// Steady state: user already completed this version's auto-import gate. Skip all file/Keychain reads.
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await runExternalCredentialAutoImport({
|
|
240
|
+
authStorage: activeAuthStorage,
|
|
241
|
+
discover,
|
|
242
|
+
trigger: "startup",
|
|
243
|
+
});
|
|
244
|
+
if (!result.discovered) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const candidates = filterAutoImportOAuthCredentials(result.discovery?.importable ?? []);
|
|
249
|
+
if (candidates.length > 0 && result.imported.length === 0 && result.skipped.length === 0) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
await store.write(version);
|
|
253
|
+
|
|
254
|
+
if (result.imported.length > 0) {
|
|
255
|
+
await activeModelRegistry.refresh("offline");
|
|
256
|
+
}
|
|
257
|
+
return buildCredentialAutoImportNotice(result);
|
|
258
|
+
}
|
|
@@ -25,6 +25,11 @@ export type ExternalProvider = "anthropic" | "openai-codex";
|
|
|
25
25
|
/** Where a discovered credential came from. */
|
|
26
26
|
export type CredentialOrigin = "claude-code-file" | "claude-code-keychain" | "codex-file";
|
|
27
27
|
|
|
28
|
+
export const AUTO_IMPORT_OAUTH_PROVIDER_ORIGINS: Record<ExternalProvider, ReadonlySet<CredentialOrigin>> = {
|
|
29
|
+
anthropic: new Set<CredentialOrigin>(["claude-code-file", "claude-code-keychain"]),
|
|
30
|
+
"openai-codex": new Set<CredentialOrigin>(["codex-file"]),
|
|
31
|
+
};
|
|
32
|
+
|
|
28
33
|
/** Human labels for providers, used in redacted summaries. */
|
|
29
34
|
export const EXTERNAL_PROVIDER_LABELS: Record<ExternalProvider, string> = {
|
|
30
35
|
anthropic: "Claude (Anthropic)",
|
|
@@ -408,6 +413,18 @@ export function formatDiscoverySummary(result: CredentialDiscoveryResult): strin
|
|
|
408
413
|
return lines;
|
|
409
414
|
}
|
|
410
415
|
|
|
416
|
+
export function isAutoImportOAuthCredential(credential: ImportableCredential): boolean {
|
|
417
|
+
return (
|
|
418
|
+
AUTO_IMPORT_OAUTH_PROVIDER_ORIGINS[credential.provider]?.has(credential.origin) === true &&
|
|
419
|
+
credential.kind === "oauth" &&
|
|
420
|
+
credential.credential.type === "oauth"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function filterAutoImportOAuthCredentials(credentials: readonly ImportableCredential[]): ImportableCredential[] {
|
|
425
|
+
return credentials.filter(isAutoImportOAuthCredential);
|
|
426
|
+
}
|
|
427
|
+
|
|
411
428
|
/**
|
|
412
429
|
* Persist discovered credentials via `upsert`. Each credential is imported
|
|
413
430
|
* independently; a failure on one is recorded without aborting the rest.
|
|
@@ -15,6 +15,16 @@ These instructions teach a Hermes-style coordinator how to operate GJC through t
|
|
|
15
15
|
6. Use `{{TOOL_PREFIX}}_report_status` for coordinator-visible status and final reports.
|
|
16
16
|
7. Use `{{TOOL_PREFIX}}_read_tail` only as advisory debug output when structured turn state is insufficient.
|
|
17
17
|
|
|
18
|
+
## Prefer high-level delegation
|
|
19
|
+
|
|
20
|
+
When the goal is to hand GJC a whole workflow rather than micro-manage one prompt, prefer the first-class delegate tools over manual `{{TOOL_PREFIX}}_start_session` + `{{TOOL_PREFIX}}_send_prompt` sequencing:
|
|
21
|
+
|
|
22
|
+
- `gjc_delegate_plan` — run consensus planning (`/skill:ralplan`) to a pending-approval plan.
|
|
23
|
+
- `gjc_delegate_execute` — run execution (`/skill:ultragoal`) to completion with verification.
|
|
24
|
+
- `gjc_delegate_team` — run parallel team execution (`/skill:team`) with internal tmux workers.
|
|
25
|
+
|
|
26
|
+
Each delegate starts (or reuses) a session, sends one workflow-tagged turn, and returns a durable `turn_id`. Pass `cwd` and `task`; set `allow_mutation: true` only when the bridge startup mutation class is enabled and the user has approved changes. Poll the returned `turn_id` with `{{TOOL_PREFIX}}_await_turn` or watch for the `delegation.started` event, exactly as with `send_prompt`. Drop to the manual start/send tools only for fine-grained control the delegate tools do not cover.
|
|
27
|
+
|
|
18
28
|
## Event watch
|
|
19
29
|
|
|
20
30
|
`{{TOOL_PREFIX}}_watch_events` is a bounded long-poll read tool. Call it with `after_seq` set to the last stored sequence number, optional `session_id` or `event_types`, `timeout_ms` up to 30000, and `limit` up to 100. Store the returned `latest_seq` before the next wait. A timeout with no events is not failure; call again or use the turn/status read tools for a snapshot.
|