@gajae-code/coding-agent 0.15.4 → 0.15.6
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 +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -8385,13 +8385,13 @@ export class SessionManager {
|
|
|
8385
8385
|
this.#usageStatistics = commit.usageStatistics;
|
|
8386
8386
|
this.#flushed = true;
|
|
8387
8387
|
this.#ensuredOnDisk = true;
|
|
8388
|
-
this.#managedPersistExpectedIdentity =
|
|
8388
|
+
this.#managedPersistExpectedIdentity =
|
|
8389
|
+
this.destination.kind === "managed" ? this.#captureManagedPersistIdentity(sessionFile) : undefined;
|
|
8389
8390
|
this.#lazyReopenSucceeded = true;
|
|
8390
8391
|
this.#lazyReopenFallbackReason = undefined;
|
|
8391
8392
|
initialized = true;
|
|
8392
8393
|
return true;
|
|
8393
|
-
} catch
|
|
8394
|
-
SessionManagerTestHooks.lastSidecarInitError = toError(error).message;
|
|
8394
|
+
} catch {
|
|
8395
8395
|
return false;
|
|
8396
8396
|
} finally {
|
|
8397
8397
|
if (!initialized) {
|
|
@@ -8572,7 +8572,8 @@ export class SessionManager {
|
|
|
8572
8572
|
this.#lazyReopenFallbackReason = "bounded_first_open_descriptor_changed";
|
|
8573
8573
|
return false;
|
|
8574
8574
|
}
|
|
8575
|
-
this.#managedPersistExpectedIdentity =
|
|
8575
|
+
this.#managedPersistExpectedIdentity =
|
|
8576
|
+
this.destination.kind === "managed" ? this.#captureManagedPersistIdentity(sessionFile) : undefined;
|
|
8576
8577
|
this.#sessionId = discovery.header.id;
|
|
8577
8578
|
this.#sessionName = discovery.header.title;
|
|
8578
8579
|
this.#titleSource = discovery.header.titleSource;
|
|
@@ -14945,9 +14946,27 @@ export class SessionManager {
|
|
|
14945
14946
|
this.#managedPersistExpectedIdentity = undefined;
|
|
14946
14947
|
return;
|
|
14947
14948
|
}
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14949
|
+
this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
|
|
14950
|
+
}
|
|
14951
|
+
|
|
14952
|
+
/** Capture one descriptor-bound digest for a future metadata-drift comparison. */
|
|
14953
|
+
#captureManagedPersistIdentity(sessionFile: string): ManagedFileIdentity {
|
|
14954
|
+
const store = this.#managedTranscriptStore(sessionFile);
|
|
14955
|
+
const relativePath = path.basename(sessionFile);
|
|
14956
|
+
const bounded = store.captureBoundedAppendExpectation(relativePath);
|
|
14957
|
+
const descriptor = store.descriptorExpected(relativePath);
|
|
14958
|
+
if (!bounded || !descriptor) throw new Error("managed_persist_identity_unavailable");
|
|
14959
|
+
const identity = managedIdentityFromDescriptor(descriptor);
|
|
14960
|
+
if (
|
|
14961
|
+
bounded.dev !== identity.dev.toString() ||
|
|
14962
|
+
bounded.ino !== identity.ino.toString() ||
|
|
14963
|
+
bounded.nlink !== identity.nlink.toString() ||
|
|
14964
|
+
bounded.size !== String(identity.size) ||
|
|
14965
|
+
bounded.mtimeNs !== identity.mtimeNs.toString() ||
|
|
14966
|
+
bounded.ctimeNs !== identity.ctimeNs.toString()
|
|
14967
|
+
)
|
|
14968
|
+
throw new Error("managed_persist_identity_unavailable");
|
|
14969
|
+
return { ...identity, sha256: bounded.sha256 };
|
|
14951
14970
|
}
|
|
14952
14971
|
/** Current transcript descriptor, or null when unavailable. */
|
|
14953
14972
|
#managedDescriptorSnapshotOrNull(): DescriptorSnapshot | null {
|
|
@@ -14997,7 +15016,7 @@ export class SessionManager {
|
|
|
14997
15016
|
} else store.replaceSync(relativePath, bytes);
|
|
14998
15017
|
const descriptor = store.descriptorExpected(relativePath);
|
|
14999
15018
|
if (!descriptor) throw new Error("managed_replace_identity_unavailable");
|
|
15000
|
-
this.#managedPersistExpectedIdentity =
|
|
15019
|
+
this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
|
|
15001
15020
|
this.#publishCommitMarkerFromCurrentTranscriptSync();
|
|
15002
15021
|
return;
|
|
15003
15022
|
}
|
|
@@ -19961,7 +19980,7 @@ export class SessionManager {
|
|
|
19961
19980
|
const returnManagedDescriptor = managedInspectionStore?.descriptorExpected(path.basename(filePath));
|
|
19962
19981
|
if (!returnManagedDescriptor || !sameDescriptor(managedBoundedDescriptor, returnManagedDescriptor))
|
|
19963
19982
|
throw new Error("Could not open session: unstable");
|
|
19964
|
-
manager.#managedPersistExpectedIdentity =
|
|
19983
|
+
manager.#managedPersistExpectedIdentity = manager.#captureManagedPersistIdentity(filePath);
|
|
19965
19984
|
const header = manager.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
19966
19985
|
if (header?.cwd) manager.cwd = header.cwd;
|
|
19967
19986
|
manager.buildSessionContext();
|
|
@@ -20079,7 +20098,7 @@ export class SessionManager {
|
|
|
20079
20098
|
const finalDescriptor = store.descriptorExpected(path.basename(resolved));
|
|
20080
20099
|
if (!finalDescriptor || !sameDescriptor(capturedDescriptor, finalDescriptor))
|
|
20081
20100
|
throw new Error("source_changed");
|
|
20082
|
-
boundedManager.#managedPersistExpectedIdentity =
|
|
20101
|
+
boundedManager.#managedPersistExpectedIdentity = boundedManager.#captureManagedPersistIdentity(resolved);
|
|
20083
20102
|
boundedManager.buildSessionContext();
|
|
20084
20103
|
return boundedManager;
|
|
20085
20104
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}},
|
|
5
5
|
{"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","qwen3.8-max","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","qwen3.8-max":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}},
|
|
6
6
|
{"id":"cline-pass","aliases":["clinepass","cline"],"name":"ClinePass","description":"ClinePass subscription models discovered from Cline's upstream models.dev catalog","compatibility":"openai","api":"openai-completions","providerId":"cline-pass","baseUrl":"https://api.cline.bot/api/v1","apiKeyEnv":"CLINE_API_KEY","discovery":{"type":"models-dev","modelsDevProvider":"cline-pass"},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning"}},
|
|
7
|
-
{"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude-":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
|
|
7
|
+
{"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude":"anthropic-messages","anthropic/claude":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","reasoningContentField":"reasoning_content"}},
|
|
8
8
|
{"id":"litellm","aliases":["litellm-proxy"],"name":"LiteLLM Proxy","description":"OpenAI-compatible LiteLLM proxy endpoint (user-supplied base URL) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"litellm","parameterized":true,"apiKeyEnv":"LITELLM_API_KEY","discovery":{"type":"openai-models-list"}},
|
|
9
9
|
{"id":"openai-compatible-proxy","aliases":["openai-proxy","compatible-proxy","custom-proxy"],"name":"OpenAI-Compatible Proxy","description":"Generic OpenAI-compatible gateway (OpenRouter, OpenCode Go, custom) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"openai-compatible-proxy","parameterized":true,"apiKeyEnv":"OPENAI_COMPATIBLE_API_KEY","discovery":{"type":"openai-models-list"}}
|
|
10
10
|
]
|
|
@@ -131,7 +131,12 @@ export async function switchSessionCredentialCommand(runtime: SlashCommandRuntim
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
try {
|
|
134
|
-
authStorage.switchSessionCredential(
|
|
134
|
+
authStorage.switchSessionCredential(
|
|
135
|
+
provider,
|
|
136
|
+
runtime.session.credentialSessionId,
|
|
137
|
+
parsed.selector,
|
|
138
|
+
runtime.session.modelRegistry.getAuthStorageOwner?.(),
|
|
139
|
+
);
|
|
135
140
|
} catch (error) {
|
|
136
141
|
return error instanceof Error ? error.message : String(error);
|
|
137
142
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -1121,6 +1121,8 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
|
|
|
1121
1121
|
const abortController = new AbortController();
|
|
1122
1122
|
const abortSignal = abortController.signal;
|
|
1123
1123
|
let activeSession: AgentSession | null = null;
|
|
1124
|
+
let ownedModelRegistry: ModelRegistry | undefined;
|
|
1125
|
+
let ownedAuthStorage: AuthStorage | undefined;
|
|
1124
1126
|
let unsubscribe: (() => void) | null = null;
|
|
1125
1127
|
let yieldCalled = false;
|
|
1126
1128
|
let pauseRequested = false;
|
|
@@ -1703,9 +1705,19 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
|
|
|
1703
1705
|
checkAbort();
|
|
1704
1706
|
// Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
|
|
1705
1707
|
const registryFromParent = options.modelRegistry !== undefined;
|
|
1706
|
-
const
|
|
1707
|
-
options.
|
|
1708
|
-
|
|
1708
|
+
const registryAuthStorage =
|
|
1709
|
+
options.authStorage ??
|
|
1710
|
+
options.modelRegistry?.authStorage ??
|
|
1711
|
+
(await awaitAbortable(discoverAuthStorage(settings.getAgentDir())));
|
|
1712
|
+
if (options.modelRegistry === undefined && options.authStorage === undefined)
|
|
1713
|
+
ownedAuthStorage = registryAuthStorage;
|
|
1714
|
+
ownedModelRegistry = options.modelRegistry
|
|
1715
|
+
? undefined
|
|
1716
|
+
: new ModelRegistry(registryAuthStorage, path.join(settings.getAgentDir(), "models.yml"), settings, {
|
|
1717
|
+
agentDir: settings.getAgentDir(),
|
|
1718
|
+
automaticRefresh: false,
|
|
1719
|
+
});
|
|
1720
|
+
const modelRegistry = options.modelRegistry ?? ownedModelRegistry!;
|
|
1709
1721
|
const authStorage = modelRegistry.authStorage;
|
|
1710
1722
|
if (options.authStorage && options.authStorage !== authStorage) {
|
|
1711
1723
|
throw new Error(
|
|
@@ -2500,6 +2512,10 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
|
|
|
2500
2512
|
// Ignore cleanup errors
|
|
2501
2513
|
}
|
|
2502
2514
|
}
|
|
2515
|
+
ownedModelRegistry?.dispose();
|
|
2516
|
+
ownedModelRegistry = undefined;
|
|
2517
|
+
ownedAuthStorage?.close();
|
|
2518
|
+
ownedAuthStorage = undefined;
|
|
2503
2519
|
}
|
|
2504
2520
|
|
|
2505
2521
|
return {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Process-wide emergency stop; it cannot be enabled by project dotenv files. */
|
|
2
|
+
export const TELEMETRY_KILL_SWITCH_ENV = "GJC_DISABLE_TELEMETRY" as const;
|
|
3
|
+
|
|
4
|
+
function isTruthy(value: string | undefined): boolean {
|
|
5
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TelemetryStatus {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
disabledByKillSwitch: boolean;
|
|
11
|
+
reason: "enabled" | "setting_disabled" | "kill_switch";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TelemetrySettingsReader {
|
|
15
|
+
get(path: "telemetry.enabled"): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getTelemetryStatus(settings: TelemetrySettingsReader): TelemetryStatus {
|
|
19
|
+
if (isTruthy(process.env[TELEMETRY_KILL_SWITCH_ENV])) {
|
|
20
|
+
return { enabled: false, disabledByKillSwitch: true, reason: "kill_switch" };
|
|
21
|
+
}
|
|
22
|
+
if (settings.get("telemetry.enabled") !== true) {
|
|
23
|
+
return { enabled: false, disabledByKillSwitch: false, reason: "setting_disabled" };
|
|
24
|
+
}
|
|
25
|
+
return { enabled: true, disabledByKillSwitch: false, reason: "enabled" };
|
|
26
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getTrustedAgentFile } from "@gajae-code/utils";
|
|
5
|
+
|
|
6
|
+
export const TELEMETRY_SCHEMA_VERSION = 1 as const;
|
|
7
|
+
export const TELEMETRY_INSTALL_ID_FILE = "telemetry-install-id" as const;
|
|
8
|
+
|
|
9
|
+
export const TELEMETRY_EVENT_NAMES = [
|
|
10
|
+
"update_check_started",
|
|
11
|
+
"update_check_completed",
|
|
12
|
+
"update_install_started",
|
|
13
|
+
"update_install_completed",
|
|
14
|
+
"update_install_failed",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type TelemetryEventName = (typeof TELEMETRY_EVENT_NAMES)[number];
|
|
18
|
+
|
|
19
|
+
export interface TelemetryEvent {
|
|
20
|
+
schemaVersion: typeof TELEMETRY_SCHEMA_VERSION;
|
|
21
|
+
event: TelemetryEventName;
|
|
22
|
+
installId: string;
|
|
23
|
+
occurredAt: string;
|
|
24
|
+
channel?: "stable" | "nightly";
|
|
25
|
+
result?: "available" | "up_to_date" | "installed" | "failed" | "skipped";
|
|
26
|
+
installMethod?: "bun" | "npm" | "binary" | "migrate";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type EventInput = {
|
|
30
|
+
event?: unknown;
|
|
31
|
+
installId?: unknown;
|
|
32
|
+
occurredAt?: unknown;
|
|
33
|
+
channel?: unknown;
|
|
34
|
+
result?: unknown;
|
|
35
|
+
installMethod?: unknown;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const EVENT_NAMES = new Set<string>(TELEMETRY_EVENT_NAMES);
|
|
40
|
+
const CHANNELS = new Set(["stable", "nightly"]);
|
|
41
|
+
const RESULTS = new Set(["available", "up_to_date", "installed", "failed", "skipped"]);
|
|
42
|
+
const INSTALL_METHODS = new Set(["bun", "npm", "binary", "migrate"]);
|
|
43
|
+
const FORBIDDEN_KEY = /(?:prompt|argv|path|env|secret|account|model|provider|repo|error|hostname|username|machine|ip)/i;
|
|
44
|
+
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
45
|
+
|
|
46
|
+
function hasForbiddenKey(value: unknown, seen = new Set<object>()): boolean {
|
|
47
|
+
if (value === null || typeof value !== "object") return false;
|
|
48
|
+
if (seen.has(value)) return false;
|
|
49
|
+
seen.add(value);
|
|
50
|
+
for (const [key, child] of Object.entries(value)) {
|
|
51
|
+
if (FORBIDDEN_KEY.test(key) || hasForbiddenKey(child, seen)) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requireString(value: unknown, field: string): string {
|
|
57
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128) {
|
|
58
|
+
throw new Error(`invalid telemetry ${field}`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Serialize only the versioned telemetry allowlist. Unknown fields are never
|
|
65
|
+
* emitted; forbidden fields anywhere in the input fail closed.
|
|
66
|
+
*/
|
|
67
|
+
export function serializeTelemetryEvent(input: unknown): string {
|
|
68
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
69
|
+
throw new Error("telemetry event must be an object");
|
|
70
|
+
}
|
|
71
|
+
if (hasForbiddenKey(input)) throw new Error("telemetry event contains forbidden data");
|
|
72
|
+
const value = input as EventInput;
|
|
73
|
+
const event = requireString(value.event, "event");
|
|
74
|
+
if (!EVENT_NAMES.has(event)) throw new Error("invalid telemetry event");
|
|
75
|
+
const installId = requireString(value.installId, "installId");
|
|
76
|
+
if (!UUID_V4.test(installId)) throw new Error("invalid telemetry installId");
|
|
77
|
+
const occurredAt = requireString(value.occurredAt, "occurredAt");
|
|
78
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(occurredAt) || Number.isNaN(Date.parse(occurredAt))) {
|
|
79
|
+
throw new Error("invalid telemetry occurredAt");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const output: TelemetryEvent = {
|
|
83
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
84
|
+
event: event as TelemetryEventName,
|
|
85
|
+
installId,
|
|
86
|
+
occurredAt,
|
|
87
|
+
};
|
|
88
|
+
if (value.channel !== undefined) {
|
|
89
|
+
if (typeof value.channel !== "string" || !CHANNELS.has(value.channel))
|
|
90
|
+
throw new Error("invalid telemetry channel");
|
|
91
|
+
output.channel = value.channel as TelemetryEvent["channel"];
|
|
92
|
+
}
|
|
93
|
+
if (value.result !== undefined) {
|
|
94
|
+
if (typeof value.result !== "string" || !RESULTS.has(value.result)) throw new Error("invalid telemetry result");
|
|
95
|
+
output.result = value.result as TelemetryEvent["result"];
|
|
96
|
+
}
|
|
97
|
+
if (value.installMethod !== undefined) {
|
|
98
|
+
if (typeof value.installMethod !== "string" || !INSTALL_METHODS.has(value.installMethod)) {
|
|
99
|
+
throw new Error("invalid telemetry installMethod");
|
|
100
|
+
}
|
|
101
|
+
output.installMethod = value.installMethod as TelemetryEvent["installMethod"];
|
|
102
|
+
}
|
|
103
|
+
return `${JSON.stringify(output)}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function writeNewInstallId(filePath: string, installId: string): Promise<void> {
|
|
107
|
+
const handle = await fs.open(filePath, "wx", 0o600);
|
|
108
|
+
try {
|
|
109
|
+
await handle.writeFile(`${installId}\n`, "utf8");
|
|
110
|
+
} finally {
|
|
111
|
+
await handle.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Load or create a random UUIDv4 that is not derived from machine data. */
|
|
116
|
+
export async function getTelemetryInstallId(
|
|
117
|
+
filePath = getTrustedAgentFile(TELEMETRY_INSTALL_ID_FILE),
|
|
118
|
+
): Promise<string> {
|
|
119
|
+
try {
|
|
120
|
+
const existing = (await Bun.file(filePath).text()).trim();
|
|
121
|
+
if (UUID_V4.test(existing)) {
|
|
122
|
+
await fs.chmod(filePath, 0o600);
|
|
123
|
+
return existing;
|
|
124
|
+
}
|
|
125
|
+
throw new Error("telemetry install ID is malformed");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
131
|
+
const generated = randomUUID();
|
|
132
|
+
try {
|
|
133
|
+
await writeNewInstallId(filePath, generated);
|
|
134
|
+
return generated;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
137
|
+
const existing = (await Bun.file(filePath).text()).trim();
|
|
138
|
+
if (!UUID_V4.test(existing)) throw new Error("telemetry install ID is malformed");
|
|
139
|
+
await fs.chmod(filePath, 0o600);
|
|
140
|
+
return existing;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Settings } from "../config/settings";
|
|
2
|
+
import { getTelemetryStatus } from "./control";
|
|
3
|
+
import { getTelemetryInstallId, serializeTelemetryEvent, type TelemetryEvent, type TelemetryEventName } from "./events";
|
|
4
|
+
|
|
5
|
+
export const TELEMETRY_ENDPOINT = "https://telemetry.gajae.dev/v1/events" as const;
|
|
6
|
+
export const TELEMETRY_MAX_IN_FLIGHT = 2 as const;
|
|
7
|
+
export const TELEMETRY_TIMEOUT_MS = 1500 as const;
|
|
8
|
+
export const TELEMETRY_FLUSH_TIMEOUT_MS = 2000 as const;
|
|
9
|
+
|
|
10
|
+
interface TelemetryTransportDependencies {
|
|
11
|
+
fetchImpl?: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TelemetryDetails = Pick<TelemetryEvent, "channel" | "result" | "installMethod">;
|
|
16
|
+
|
|
17
|
+
let inFlight = 0;
|
|
18
|
+
const pendingTelemetry = new Set<Promise<void>>();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Best-effort transport. The caller only schedules work; disabled telemetry,
|
|
22
|
+
* queue saturation, network errors, and non-2xx responses are all ignored.
|
|
23
|
+
*/
|
|
24
|
+
export function sendTelemetryEvent(
|
|
25
|
+
settings: Parameters<typeof getTelemetryStatus>[0],
|
|
26
|
+
event: TelemetryEvent,
|
|
27
|
+
deps: TelemetryTransportDependencies = {},
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
if (!getTelemetryStatus(settings).enabled || inFlight >= TELEMETRY_MAX_IN_FLIGHT) return Promise.resolve();
|
|
30
|
+
let body: string;
|
|
31
|
+
try {
|
|
32
|
+
body = serializeTelemetryEvent(event);
|
|
33
|
+
} catch {
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
inFlight++;
|
|
37
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
40
|
+
timeout.unref?.();
|
|
41
|
+
const delivery = fetchImpl(deps.endpoint ?? TELEMETRY_ENDPOINT, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
body,
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
redirect: "error",
|
|
47
|
+
})
|
|
48
|
+
.catch(() => undefined)
|
|
49
|
+
.then(() => undefined)
|
|
50
|
+
.finally(() => {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
inFlight--;
|
|
53
|
+
});
|
|
54
|
+
pendingTelemetry.add(delivery);
|
|
55
|
+
void delivery.finally(() => pendingTelemetry.delete(delivery));
|
|
56
|
+
return delivery;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function resetTelemetryTransportForTest(): Promise<void> {
|
|
60
|
+
await Promise.allSettled([...pendingTelemetry]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Schedule an event without making telemetry part of the caller's lifecycle. */
|
|
64
|
+
export function recordTelemetryEvent(event: TelemetryEventName, details: TelemetryDetails): Promise<void> {
|
|
65
|
+
const pending = (async () => {
|
|
66
|
+
const settings = await Settings.init();
|
|
67
|
+
if (!getTelemetryStatus(settings).enabled) return;
|
|
68
|
+
const installId = await getTelemetryInstallId();
|
|
69
|
+
await sendTelemetryEvent(settings, {
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
event,
|
|
72
|
+
installId,
|
|
73
|
+
occurredAt: new Date().toISOString(),
|
|
74
|
+
...details,
|
|
75
|
+
});
|
|
76
|
+
})().catch(() => undefined);
|
|
77
|
+
pendingTelemetry.add(pending);
|
|
78
|
+
void pending.finally(() => pendingTelemetry.delete(pending));
|
|
79
|
+
return pending;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Wait briefly for accepted events so explicit process exits do not drop them. */
|
|
83
|
+
export async function flushTelemetryEvents(timeoutMs = TELEMETRY_FLUSH_TIMEOUT_MS): Promise<void> {
|
|
84
|
+
const pending = [...pendingTelemetry];
|
|
85
|
+
if (pending.length === 0) return;
|
|
86
|
+
await Promise.race([Promise.allSettled(pending), Bun.sleep(timeoutMs)]);
|
|
87
|
+
}
|