@gajae-code/coding-agent 0.11.6 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/commands/daemon.d.ts +2 -2
  5. package/dist/types/commands/sdk.d.ts +20 -2
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +56 -6
  8. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
  10. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
  12. package/dist/types/main.d.ts +24 -1
  13. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  14. package/dist/types/modes/components/model-selector.d.ts +2 -0
  15. package/dist/types/modes/components/tool-status-header.d.ts +1 -0
  16. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  21. package/dist/types/sdk/bus/index.d.ts +1 -1
  22. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  23. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  27. package/dist/types/sdk/client/discovery.d.ts +2 -0
  28. package/dist/types/sdk/client/liveness.d.ts +7 -0
  29. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  30. package/dist/types/sdk/transport/index.d.ts +12 -0
  31. package/dist/types/sdk/transport/relay.d.ts +43 -0
  32. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  33. package/dist/types/sdk/transport/socket.d.ts +7 -0
  34. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  35. package/dist/types/session/agent-session.d.ts +28 -5
  36. package/dist/types/session/btw-contract.d.ts +16 -0
  37. package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
  38. package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
  39. package/dist/types/session/session-manager.d.ts +25 -4
  40. package/dist/types/setup/credential-import.d.ts +5 -1
  41. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  42. package/dist/types/ssh/utils.d.ts +1 -0
  43. package/dist/types/task/discovery.d.ts +3 -1
  44. package/dist/types/tools/fetch.d.ts +5 -8
  45. package/dist/types/tools/image-gen.d.ts +16 -0
  46. package/dist/types/tools/skill-discovery.d.ts +7 -0
  47. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  48. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  49. package/dist/types/web/insane/bridge.d.ts +1 -0
  50. package/dist/types/web/insane/url-guard.d.ts +17 -0
  51. package/package.json +8 -8
  52. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  53. package/src/capability/index.ts +50 -42
  54. package/src/capability/types.ts +3 -0
  55. package/src/cli/setup-cli.ts +10 -4
  56. package/src/cli.ts +34 -9
  57. package/src/commands/daemon.ts +22 -5
  58. package/src/commands/sdk.ts +42 -9
  59. package/src/config/keybindings.ts +1 -1
  60. package/src/config/model-profiles.ts +18 -0
  61. package/src/config/settings-schema.ts +57 -6
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  63. package/src/discovery/ssh.ts +12 -1
  64. package/src/eval/py/kernel.ts +1 -21
  65. package/src/eval/py/runner-artifact.ts +84 -0
  66. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  67. package/src/gjc-runtime/ultragoal-guard.ts +88 -10
  68. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  69. package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
  70. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  71. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  72. package/src/internal-urls/docs-index.generated.ts +8 -7
  73. package/src/internal-urls/local-protocol.ts +32 -6
  74. package/src/main.ts +86 -27
  75. package/src/modes/action-registry.ts +1 -1
  76. package/src/modes/components/btw-panel.ts +82 -16
  77. package/src/modes/components/model-selector.ts +31 -3
  78. package/src/modes/components/tool-status-header.ts +6 -1
  79. package/src/modes/controllers/btw-controller.ts +119 -36
  80. package/src/modes/controllers/input-controller.ts +31 -0
  81. package/src/modes/controllers/selector-controller.ts +106 -12
  82. package/src/modes/interactive-mode.ts +16 -7
  83. package/src/modes/types.ts +1 -0
  84. package/src/prompts/system/btw-user.md +3 -8
  85. package/src/prompts/tools/skill-discovery.md +1 -0
  86. package/src/runtime-mcp/client.ts +41 -55
  87. package/src/runtime-mcp/content-limits.ts +71 -0
  88. package/src/runtime-mcp/json-rpc.ts +23 -9
  89. package/src/runtime-mcp/transports/http.ts +50 -16
  90. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  91. package/src/sdk/bus/index.ts +4 -5
  92. package/src/sdk/bus/lifecycle-commands.ts +16 -7
  93. package/src/sdk/bus/notification-service.ts +41 -18
  94. package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
  95. package/src/sdk/bus/telegram-daemon-control.ts +2 -2
  96. package/src/sdk/bus/telegram-daemon.ts +1663 -375
  97. package/src/sdk/bus/topic-registry.ts +451 -12
  98. package/src/sdk/client/discovery.ts +19 -3
  99. package/src/sdk/client/liveness.ts +49 -0
  100. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  101. package/src/sdk/session.ts +29 -5
  102. package/src/sdk/transport/auth-preface.ts +82 -0
  103. package/src/sdk/transport/index.ts +18 -0
  104. package/src/sdk/transport/relay.ts +252 -0
  105. package/src/sdk/transport/serve-cli.ts +105 -0
  106. package/src/sdk/transport/socket.ts +197 -0
  107. package/src/sdk/transport/stdio.ts +17 -0
  108. package/src/session/agent-session.ts +240 -149
  109. package/src/session/blob-store.ts +19 -6
  110. package/src/session/btw-contract.ts +59 -0
  111. package/src/session/internal/managed-session-scope.ts +313 -57
  112. package/src/session/internal/managed-session-storage.ts +146 -35
  113. package/src/session/session-manager.ts +319 -85
  114. package/src/setup/credential-import.ts +28 -3
  115. package/src/setup/provider-onboarding.ts +30 -1
  116. package/src/setup/provider-presets.json +2 -1
  117. package/src/slash-commands/builtin-registry.ts +1 -1
  118. package/src/ssh/utils.ts +14 -0
  119. package/src/task/discovery.ts +8 -2
  120. package/src/tools/ask.ts +1 -1
  121. package/src/tools/fetch.ts +13 -131
  122. package/src/tools/image-gen.ts +301 -20
  123. package/src/tools/read.ts +2 -0
  124. package/src/tools/skill-discovery.ts +39 -3
  125. package/src/tools/sqlite-reader.ts +63 -5
  126. package/src/utils/pasted-image-path.ts +15 -6
  127. package/src/utils/shell-snapshot.ts +173 -63
  128. package/src/web/insane/bridge.ts +5 -1
  129. package/src/web/insane/url-guard.ts +138 -27
  130. package/src/web/scrapers/types.ts +19 -49
  131. package/src/web/scrapers/utils.ts +12 -26
@@ -92,6 +92,7 @@ import type { FileSlashCommand } from "../extensibility/slash-commands";
92
92
  import type { HindsightSessionState } from "../hindsight/state";
93
93
  import { initializeLocalRoot, LocalProtocolHandler, type LocalProtocolOptions } from "../internal-urls";
94
94
  import { resolveMemoryBackend } from "../memory-backend";
95
+ import btwUserPrompt from "../prompts/system/btw-user.md" with { type: "text" };
95
96
  import asyncResultTemplate from "../prompts/tools/async-result.md" with { type: "text" };
96
97
  import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
97
98
  import { MCPManager } from "../runtime-mcp";
@@ -149,6 +150,7 @@ import {
149
150
  ReadTool,
150
151
  ResolveTool,
151
152
  SearchTool,
153
+ setConfiguredImageModel,
152
154
  setPreferredImageProvider,
153
155
  setPreferredSearchProvider,
154
156
  setSearchFallbackProviders,
@@ -1158,14 +1160,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1158
1160
  applyConfiguredSearchTimeout(settings);
1159
1161
 
1160
1162
  const imageProvider = settings.get("providers.image");
1163
+ const imageModel = settings.get("providers.imageModel");
1164
+ const imageCustomUrl = settings.get("providers.imageCustomUrl");
1165
+ const imageCustomKey = settings.get("providers.imageCustomKey");
1166
+ const imageCustomKeyEnv = settings.get("providers.imageCustomKeyEnv");
1161
1167
  if (
1162
1168
  imageProvider === "auto" ||
1163
1169
  imageProvider === "openai" ||
1164
1170
  imageProvider === "gemini" ||
1165
1171
  imageProvider === "openrouter" ||
1166
- imageProvider === "antigravity"
1172
+ imageProvider === "antigravity" ||
1173
+ imageProvider === "custom"
1167
1174
  ) {
1168
- setPreferredImageProvider(imageProvider);
1175
+ setPreferredImageProvider(imageProvider === "custom" ? "auto" : imageProvider);
1176
+ setConfiguredImageModel({
1177
+ provider: imageProvider,
1178
+ model: imageModel ?? null,
1179
+ customUrl: imageCustomUrl,
1180
+ customKey: imageCustomKey,
1181
+ customKeyEnv: imageCustomKeyEnv,
1182
+ });
1169
1183
  }
1170
1184
 
1171
1185
  const sessionManager =
@@ -1590,13 +1604,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1590
1604
  // which collapses to the parent's dir for subagents (they adopt the
1591
1605
  // parent's ArtifactManager) so one lookup hits everything.
1592
1606
  const getArtifactsDir = () => sessionManager.getArtifactsDir();
1607
+ const localProtocolOptions = options.localProtocolOptions ?? {
1608
+ getArtifactsDir,
1609
+ isManagedDestination: () => sessionManager.isManagedDestination(),
1610
+ getManagedLegacyLocalMigrationSource: () => sessionManager.getManagedLegacyLocalMigrationSource(),
1611
+ getSessionId: () => sessionManager.getSessionId(),
1612
+ };
1593
1613
  if (!options.parentTaskPrefix) {
1594
1614
  setActiveSkills(skills);
1595
1615
  setActiveRules([...rulebookRules, ...alwaysApplyRules]);
1596
1616
  if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
1597
1617
  }
1618
+ await initializeLocalRoot(localProtocolOptions);
1598
1619
  if (options.localProtocolOptions) {
1599
- await initializeLocalRoot(options.localProtocolOptions);
1600
1620
  disposeLocalProtocolOverride = LocalProtocolHandler.installOverride(options.localProtocolOptions);
1601
1621
  }
1602
1622
  toolSession.getArtifactsDir = getArtifactsDir;
@@ -1849,9 +1869,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1849
1869
  spawnedByGjc,
1850
1870
  sdkHostModeSupported: options.sdkHostModeSupported,
1851
1871
  ensureProviderDaemon: options.ensureNotificationProviderDaemon,
1852
- runEphemeralTurn: async (promptText, signal) => {
1872
+ runBtwTurn: async (question, signal) => {
1853
1873
  if (!session) throw new Error("Ephemeral turns are unavailable.");
1854
- const { replyText } = await session.runEphemeralTurn({ promptText, signal });
1874
+ const { replyText } = await session.runEphemeralTurn({
1875
+ purpose: "btw",
1876
+ turn: { question, scope: session.createBtwConversationScope(btwUserPrompt) },
1877
+ signal,
1878
+ });
1855
1879
  return { replyText };
1856
1880
  },
1857
1881
  });
@@ -0,0 +1,82 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import type * as net from "node:net";
3
+
4
+ const PREFACE_LIMIT_BYTES = 4 * 1024;
5
+ const PREFACE_TIMEOUT_MS = 1_000;
6
+ const VERSION = "gjc-sdk-transport/1";
7
+
8
+ type AuthFailureCode =
9
+ | "auth_missing"
10
+ | "auth_malformed"
11
+ | "auth_invalid"
12
+ | "auth_oversize"
13
+ | "auth_timeout"
14
+ | "unsupported_version";
15
+
16
+ export class AuthPrefaceError extends Error {
17
+ constructor(readonly code: AuthFailureCode) {
18
+ super(code);
19
+ this.name = "AuthPrefaceError";
20
+ }
21
+ }
22
+
23
+ function digest(token: string): Buffer {
24
+ return createHash("sha256").update(token).digest();
25
+ }
26
+
27
+ function validToken(received: string, expected: string): boolean {
28
+ return timingSafeEqual(digest(received), digest(expected));
29
+ }
30
+
31
+ function failureFrame(): string {
32
+ return `${JSON.stringify({ type: "transport_error", code: "auth_failed" })}\n`;
33
+ }
34
+
35
+ /** Consumes the one-shot socket authentication preface and leaves later bytes readable. */
36
+ export async function authenticatePreface(socket: net.Socket, token: string): Promise<Buffer> {
37
+ return await new Promise<Buffer>((resolve, reject) => {
38
+ let buffer = Buffer.alloc(0);
39
+ let settled = false;
40
+ const finish = (error?: AuthPrefaceError, remainder = Buffer.alloc(0)): void => {
41
+ if (settled) return;
42
+ settled = true;
43
+ clearTimeout(timeout);
44
+ socket.removeListener("data", onData);
45
+ socket.removeListener("end", onEnd);
46
+ socket.removeListener("error", onSocketError);
47
+ if (error) {
48
+ socket.end(failureFrame(), () => reject(error));
49
+ return;
50
+ }
51
+ socket.pause();
52
+ resolve(remainder);
53
+ };
54
+ const fail = (code: AuthFailureCode): void => finish(new AuthPrefaceError(code));
55
+ const onSocketError = (): void => fail("auth_missing");
56
+ const onEnd = (): void => fail(buffer.length === 0 ? "auth_missing" : "auth_malformed");
57
+ const onData = (chunk: Buffer): void => {
58
+ buffer = Buffer.concat([buffer, chunk]);
59
+ const newline = buffer.indexOf(0x0a);
60
+ if (newline < 0) {
61
+ if (buffer.length > PREFACE_LIMIT_BYTES) fail("auth_oversize");
62
+ return;
63
+ }
64
+ if (newline > PREFACE_LIMIT_BYTES) return fail("auth_oversize");
65
+ const line = buffer.subarray(0, newline).toString("utf8");
66
+ const remainder = buffer.subarray(newline + 1);
67
+ const prefix = `${VERSION} token=`;
68
+ if (!line.startsWith(prefix)) {
69
+ return fail(line.startsWith("gjc-sdk-transport/") ? "unsupported_version" : "auth_malformed");
70
+ }
71
+ const received = line.slice(prefix.length);
72
+ if (received.length === 0 || /\s/.test(received)) return fail("auth_malformed");
73
+ if (!validToken(received, token)) return fail("auth_invalid");
74
+ finish(undefined, remainder);
75
+ };
76
+ const timeout = setTimeout(() => fail("auth_timeout"), PREFACE_TIMEOUT_MS);
77
+ socket.on("data", onData);
78
+ socket.once("end", onEnd);
79
+ socket.once("error", onSocketError);
80
+ socket.resume();
81
+ });
82
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ DEFAULT_PENDING_CEILING_BYTES,
3
+ MIN_PENDING_CEILING_BYTES,
4
+ REQUEST_FRAME_BYTES,
5
+ } from "./relay";
6
+ export { startSocketServe } from "./socket";
7
+ export { startStdioServe } from "./stdio";
8
+
9
+ export type ServeOptions = {
10
+ url: string;
11
+ token: string;
12
+ pendingCeilingBytes: number;
13
+ };
14
+
15
+ export type ServeHandle = {
16
+ close(): Promise<void>;
17
+ readonly done: Promise<void>;
18
+ };
@@ -0,0 +1,252 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+
3
+ export const REQUEST_FRAME_BYTES = 256 * 1024;
4
+ export const DEFAULT_PENDING_CEILING_BYTES = 8 * 1024 * 1024;
5
+ export const MIN_PENDING_CEILING_BYTES = REQUEST_FRAME_BYTES;
6
+
7
+ export type RelayDirection = "downstream->ws" | "ws->downstream";
8
+ export type TransportError = {
9
+ type: "transport_error";
10
+ code: "frame_oversize" | "pending_overflow" | "protocol_error";
11
+ direction?: RelayDirection;
12
+ };
13
+
14
+ export type RelayPair = {
15
+ close(): Promise<void>;
16
+ readonly done: Promise<void>;
17
+ };
18
+
19
+ export class RelayOpenAbortedError extends Error {
20
+ constructor() {
21
+ super("relay_open_aborted");
22
+ this.name = "RelayOpenAbortedError";
23
+ }
24
+ }
25
+
26
+ export type RelayWebSocketEvent = { data?: unknown };
27
+
28
+ export type RelayWebSocket = {
29
+ readonly bufferedAmount: number;
30
+ readonly readyState: number;
31
+ close(): void;
32
+ send(data: string): void;
33
+ addEventListener(type: string, listener: (event: RelayWebSocketEvent) => void, options?: { once?: boolean }): void;
34
+ removeEventListener(type: string, listener: (event: RelayWebSocketEvent) => void): void;
35
+ };
36
+
37
+ export type RelayOptions = {
38
+ url: string;
39
+ token: string;
40
+ pendingCeilingBytes: number;
41
+ downstream: Readable;
42
+ downstreamSink: Writable;
43
+ initialDownstreamBytes?: Buffer;
44
+ onTransportError: (error: TransportError) => void;
45
+ webSocketFactory?: (url: string) => RelayWebSocket;
46
+ signal?: AbortSignal;
47
+ };
48
+
49
+ type QueuedFrame = { bytes: Buffer; accounted: boolean };
50
+
51
+ function upstreamUrl(url: string, token: string): string {
52
+ const endpoint = new URL(url);
53
+ endpoint.pathname = `${endpoint.pathname.replace(/\/$/, "")}/`;
54
+ endpoint.searchParams.set("token", token);
55
+ return endpoint.toString();
56
+ }
57
+
58
+ function waitForDrain(stream: Writable): Promise<void> {
59
+ const { promise, reject, resolve } = Promise.withResolvers<void>();
60
+ const onDrain = (): void => done(resolve);
61
+ const onError = (error: Error): void => done(() => reject(error));
62
+ const done = (callback: () => void): void => {
63
+ stream.removeListener("drain", onDrain);
64
+ stream.removeListener("error", onError);
65
+ callback();
66
+ };
67
+ stream.once("drain", onDrain);
68
+ stream.once("error", onError);
69
+ return promise;
70
+ }
71
+
72
+ const WEBSOCKET_CONNECTING = 0;
73
+ const WEBSOCKET_OPEN = 1;
74
+
75
+ function waitForWebSocketDrain(ws: RelayWebSocket, isClosed: () => boolean): Promise<void> {
76
+ const { promise, resolve } = Promise.withResolvers<void>();
77
+ const poll = (): void => {
78
+ if (isClosed() || ws.readyState !== WEBSOCKET_OPEN || ws.bufferedAmount === 0) {
79
+ resolve();
80
+ return;
81
+ }
82
+ setTimeout(poll, 10);
83
+ };
84
+ poll();
85
+ return promise;
86
+ }
87
+
88
+ /** Starts a dedicated raw-WebSocket relay for exactly one downstream stream. */
89
+ export async function startRelayPair(options: RelayOptions): Promise<RelayPair> {
90
+ if (options.signal?.aborted) throw new RelayOpenAbortedError();
91
+ const ws: RelayWebSocket =
92
+ options.webSocketFactory?.(upstreamUrl(options.url, options.token)) ??
93
+ new WebSocket(upstreamUrl(options.url, options.token));
94
+ const opened = Promise.withResolvers<void>();
95
+ const finished = Promise.withResolvers<void>();
96
+ let closed = false;
97
+ let completed = false;
98
+ let downstreamBuffer = Buffer.alloc(0);
99
+ const toWs: QueuedFrame[] = [];
100
+ const toDownstream: QueuedFrame[] = [];
101
+ let pendingToWs = 0;
102
+ let pendingToDownstream = 0;
103
+ let writingWs = false;
104
+ let writingDownstream = false;
105
+
106
+ const settle = (error?: Error): void => {
107
+ if (completed) return;
108
+ completed = true;
109
+ if (error) finished.reject(error);
110
+ else finished.resolve();
111
+ };
112
+ void finished.promise.catch(() => undefined);
113
+ const detach = (): void => {
114
+ options.downstream.removeListener("data", onData);
115
+ options.downstream.removeListener("end", onEnd);
116
+ options.downstream.removeListener("error", onDownstreamError);
117
+ ws.removeEventListener("message", onMessage);
118
+ ws.removeEventListener("close", onClose);
119
+ ws.removeEventListener("error", onWebSocketError);
120
+ cleanupOpening();
121
+ };
122
+ const close = async (error?: Error): Promise<void> => {
123
+ if (closed) return finished.promise.catch(() => undefined);
124
+ closed = true;
125
+ detach();
126
+ options.downstream.pause();
127
+ if (ws.readyState === WEBSOCKET_OPEN || ws.readyState === WEBSOCKET_CONNECTING) ws.close();
128
+ settle(error);
129
+ return await finished.promise.catch(() => undefined);
130
+ };
131
+ const fail = (transportError: TransportError): void => {
132
+ if (closed) return;
133
+ options.onTransportError(transportError);
134
+ void close(new Error(transportError.code));
135
+ };
136
+ const enqueue = (queue: QueuedFrame[], direction: RelayDirection, bytes: Buffer): void => {
137
+ const isWsDirection = direction === "downstream->ws";
138
+ const active = isWsDirection ? writingWs : writingDownstream;
139
+ const pending = isWsDirection ? pendingToWs : pendingToDownstream;
140
+ if (active && pending + bytes.length > options.pendingCeilingBytes) {
141
+ fail({ type: "transport_error", code: "pending_overflow", direction });
142
+ return;
143
+ }
144
+ queue.push({ bytes, accounted: active });
145
+ if (active) {
146
+ if (isWsDirection) pendingToWs += bytes.length;
147
+ else pendingToDownstream += bytes.length;
148
+ }
149
+ if (isWsDirection) void pumpWs();
150
+ else void pumpDownstream();
151
+ };
152
+ const pumpWs = async (): Promise<void> => {
153
+ if (writingWs || closed) return;
154
+ const frame = toWs.shift();
155
+ if (!frame) return;
156
+ writingWs = true;
157
+ if (frame.accounted) pendingToWs -= frame.bytes.length;
158
+ try {
159
+ if (ws.readyState !== WEBSOCKET_OPEN) throw new Error("upstream_closed");
160
+ ws.send(frame.bytes.toString("utf8"));
161
+ await waitForWebSocketDrain(ws, () => closed);
162
+ } catch (error) {
163
+ await close(error instanceof Error ? error : new Error(String(error)));
164
+ return;
165
+ } finally {
166
+ writingWs = false;
167
+ }
168
+ void pumpWs();
169
+ };
170
+ const pumpDownstream = async (): Promise<void> => {
171
+ if (writingDownstream || closed) return;
172
+ const frame = toDownstream.shift();
173
+ if (!frame) return;
174
+ writingDownstream = true;
175
+ if (frame.accounted) pendingToDownstream -= frame.bytes.length;
176
+ try {
177
+ if (!options.downstreamSink.write(frame.bytes)) await waitForDrain(options.downstreamSink);
178
+ } catch (error) {
179
+ await close(error instanceof Error ? error : new Error(String(error)));
180
+ return;
181
+ } finally {
182
+ writingDownstream = false;
183
+ }
184
+ void pumpDownstream();
185
+ };
186
+ const consumeLines = (chunk: Buffer): void => {
187
+ downstreamBuffer = Buffer.concat([downstreamBuffer, chunk]);
188
+ let newline = downstreamBuffer.indexOf(0x0a);
189
+ while (newline >= 0) {
190
+ const line = downstreamBuffer.subarray(0, newline);
191
+ downstreamBuffer = downstreamBuffer.subarray(newline + 1);
192
+ if (line.length === 0)
193
+ return fail({ type: "transport_error", code: "protocol_error", direction: "downstream->ws" });
194
+ if (line.length > REQUEST_FRAME_BYTES)
195
+ return fail({ type: "transport_error", code: "frame_oversize", direction: "downstream->ws" });
196
+ enqueue(toWs, "downstream->ws", line);
197
+ if (closed) return;
198
+ newline = downstreamBuffer.indexOf(0x0a);
199
+ }
200
+ if (downstreamBuffer.length > REQUEST_FRAME_BYTES)
201
+ fail({ type: "transport_error", code: "frame_oversize", direction: "downstream->ws" });
202
+ };
203
+ const onData = (chunk: Buffer | string): void => consumeLines(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
204
+ const onEnd = (): void => void close();
205
+ const onDownstreamError = (error: Error): void => void close(error);
206
+ const onMessage = (event: RelayWebSocketEvent): void => {
207
+ if (typeof event.data !== "string") {
208
+ fail({ type: "transport_error", code: "protocol_error", direction: "ws->downstream" });
209
+ return;
210
+ }
211
+ enqueue(toDownstream, "ws->downstream", Buffer.concat([Buffer.from(event.data, "utf8"), Buffer.from("\n")]));
212
+ };
213
+ const onClose = (): void => void close();
214
+ const onWebSocketError = (): void => void close(new Error("upstream_error"));
215
+ const onOpen = (): void => {
216
+ cleanupOpening();
217
+ opened.resolve();
218
+ };
219
+ const onOpenError = (): void => {
220
+ cleanupOpening();
221
+ opened.reject(new Error("upstream_error"));
222
+ };
223
+ const onAbort = (): void => {
224
+ const error = new RelayOpenAbortedError();
225
+ cleanupOpening();
226
+ opened.reject(error);
227
+ void close(error);
228
+ };
229
+ const cleanupOpening = (): void => {
230
+ ws.removeEventListener("open", onOpen);
231
+ ws.removeEventListener("error", onOpenError);
232
+ options.signal?.removeEventListener("abort", onAbort);
233
+ };
234
+ ws.addEventListener("open", onOpen, { once: true });
235
+ ws.addEventListener("error", onOpenError, { once: true });
236
+ options.signal?.addEventListener("abort", onAbort, { once: true });
237
+ try {
238
+ await opened.promise;
239
+ } catch (error) {
240
+ await close(error instanceof Error ? error : new Error(String(error)));
241
+ throw error;
242
+ }
243
+ ws.addEventListener("message", onMessage);
244
+ ws.addEventListener("close", onClose);
245
+ ws.addEventListener("error", onWebSocketError);
246
+ options.downstream.on("data", onData);
247
+ options.downstream.once("end", onEnd);
248
+ options.downstream.once("error", onDownstreamError);
249
+ if (options.initialDownstreamBytes?.length) consumeLines(options.initialDownstreamBytes);
250
+ if (!closed) options.downstream.resume();
251
+ return { close: () => close(), done: finished.promise };
252
+ }
@@ -0,0 +1,105 @@
1
+ import { CliParseError } from "@gajae-code/utils/cli";
2
+ import type { SdkSessionEndpoint } from "../client/discovery";
3
+ import { listSdkSessionEndpoints } from "../client/discovery";
4
+ import { type SdkEndpointSelectionError, selectLiveEndpoint } from "../client/liveness";
5
+ import { DEFAULT_PENDING_CEILING_BYTES, MIN_PENDING_CEILING_BYTES, startSocketServe, startStdioServe } from "./index";
6
+
7
+ type ServeMode = { kind: "stdio" } | { kind: "socket"; socketPath: string };
8
+
9
+ interface ServeArguments {
10
+ mode: ServeMode;
11
+ sessionId?: string;
12
+ pendingCeiling?: string;
13
+ }
14
+
15
+ function usageError(message: string): never {
16
+ throw new CliParseError(`gjc sdk serve: ${message}`);
17
+ }
18
+
19
+ function readFlagValue(argv: string[], index: number, flag: string): string {
20
+ const value = argv[index + 1];
21
+ if (value === undefined || value.startsWith("-")) usageError(`${flag} requires a value`);
22
+ return value;
23
+ }
24
+
25
+ function parseServeArguments(argv: string[]): ServeArguments {
26
+ let stdio = false;
27
+ let socketPath: string | undefined;
28
+ let sessionId: string | undefined;
29
+ let pendingCeiling: string | undefined;
30
+ for (let index = 0; index < argv.length; index++) {
31
+ switch (argv[index]) {
32
+ case "--stdio":
33
+ if (stdio) usageError("--stdio may only be specified once");
34
+ stdio = true;
35
+ break;
36
+ case "--socket":
37
+ if (socketPath !== undefined) usageError("--socket may only be specified once");
38
+ socketPath = readFlagValue(argv, index, "--socket");
39
+ index++;
40
+ break;
41
+ case "--session":
42
+ if (sessionId !== undefined) usageError("--session may only be specified once");
43
+ sessionId = readFlagValue(argv, index, "--session");
44
+ index++;
45
+ break;
46
+ case "--pending-ceiling":
47
+ if (pendingCeiling !== undefined) usageError("--pending-ceiling may only be specified once");
48
+ pendingCeiling = readFlagValue(argv, index, "--pending-ceiling");
49
+ index++;
50
+ break;
51
+ default:
52
+ usageError(`unknown argument: ${argv[index]}`);
53
+ }
54
+ }
55
+ if (stdio === (socketPath !== undefined)) usageError("specify exactly one of --stdio or --socket <path>");
56
+ return { mode: stdio ? { kind: "stdio" } : { kind: "socket", socketPath: socketPath! }, sessionId, pendingCeiling };
57
+ }
58
+
59
+ /** Resolves the pending ceiling with flag > env > default precedence; exported for tests. */
60
+ export function resolveServePendingCeiling(flagValue: string | undefined, envValue: string | undefined): number {
61
+ const value = flagValue ?? envValue;
62
+ if (value === undefined) return DEFAULT_PENDING_CEILING_BYTES;
63
+ if (!/^\d+$/.test(value)) usageError("--pending-ceiling must be a positive integer");
64
+ const ceiling = Number(value);
65
+ if (!Number.isSafeInteger(ceiling) || ceiling < MIN_PENDING_CEILING_BYTES)
66
+ usageError(`--pending-ceiling must be an integer of at least ${MIN_PENDING_CEILING_BYTES}`);
67
+ return ceiling;
68
+ }
69
+
70
+ function isSelectionError(value: SdkSessionEndpoint | SdkEndpointSelectionError): value is SdkEndpointSelectionError {
71
+ return "code" in value;
72
+ }
73
+
74
+ /** Attaches a stdio or Unix-socket relay to one live SDK session endpoint. */
75
+ export async function runSdkServe(argv: string[]): Promise<void> {
76
+ const parsed = parseServeArguments(argv);
77
+ if (parsed.mode.kind === "socket" && process.platform === "win32")
78
+ throw new Error("unsupported_platform: --socket is unavailable on Windows.");
79
+ const pendingCeilingBytes = resolveServePendingCeiling(
80
+ parsed.pendingCeiling,
81
+ process.env.GJC_SDK_SERVE_PENDING_CEILING_BYTES,
82
+ );
83
+ const discovered = await listSdkSessionEndpoints(process.cwd());
84
+ const selected = selectLiveEndpoint(discovered.endpoints, parsed.sessionId);
85
+ if (isSelectionError(selected)) {
86
+ const sessionHint = parsed.sessionId ? ` for session ${parsed.sessionId}` : "; specify --session <id>";
87
+ throw new Error(`${selected.code}${sessionHint}`);
88
+ }
89
+ const options = { url: selected.url, token: selected.token, pendingCeilingBytes };
90
+ const handle =
91
+ parsed.mode.kind === "stdio"
92
+ ? await startStdioServe(options)
93
+ : await startSocketServe({ ...options, socketPath: parsed.mode.socketPath });
94
+ const stop = () => {
95
+ void handle.close();
96
+ };
97
+ process.once("SIGINT", stop);
98
+ process.once("SIGTERM", stop);
99
+ try {
100
+ await handle.done;
101
+ } finally {
102
+ process.removeListener("SIGINT", stop);
103
+ process.removeListener("SIGTERM", stop);
104
+ }
105
+ }