@oar-sh/oar 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ // Same resolution as server-runtime.mjs: an isolated server (tests, secondary
9
+ // installs) points COPILOT_WEB_RELAY_CONFIG elsewhere and must not inherit the
10
+ // checkout's live feature flags.
11
+ const CONFIG_PATH = process.env.COPILOT_WEB_RELAY_CONFIG
12
+ ? path.resolve(String(process.env.COPILOT_WEB_RELAY_CONFIG))
13
+ : path.join(__dirname, 'config.json');
14
+
15
+ const DEFAULT_FEATURES = Object.freeze({
16
+ SESSION_WORKER_ROUTING_ENABLED: false,
17
+ SESSION_WORKER_CONTINUATION_ROUTING_ENABLED: false,
18
+ SESSION_WORKER_FALLBACK_RESTART_ENABLED: false,
19
+ IMAGE_CONVERSATION_CONTINUITY_ENABLED: true,
20
+ });
21
+
22
+ const ENV_PREFIX = 'COPILOT_REMOTE_';
23
+ const FEATURE_NAMES = Object.freeze(Object.keys(DEFAULT_FEATURES));
24
+
25
+ function parseBoolean(value) {
26
+ if (typeof value === 'boolean') return value;
27
+ if (typeof value === 'number') return value !== 0;
28
+ const normalized = String(value || '').trim().toLowerCase();
29
+ if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') return true;
30
+ if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') return false;
31
+ return null;
32
+ }
33
+
34
+ function normalizeKnownFeatureFlags(input) {
35
+ const source = input && typeof input === 'object' ? input : {};
36
+ const normalized = {};
37
+ for (const key of FEATURE_NAMES) {
38
+ const parsed = parseBoolean(source[key]);
39
+ if (parsed === null) continue;
40
+ normalized[key] = parsed;
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ function readFeaturesFromConfig() {
46
+ if (!fs.existsSync(CONFIG_PATH)) {
47
+ return {};
48
+ }
49
+
50
+ try {
51
+ const parsed = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
52
+ const configured = parsed && typeof parsed === 'object' ? parsed.features : null;
53
+ return normalizeKnownFeatureFlags(configured);
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ function readFeaturesFromEnv(env = process.env) {
60
+ const entries = [];
61
+ for (const key of FEATURE_NAMES) {
62
+ const envKey = `${ENV_PREFIX}${key}`;
63
+ if (!(envKey in env)) continue;
64
+ const normalized = parseBoolean(env[envKey]);
65
+ if (normalized === null) continue;
66
+ entries.push([key, normalized]);
67
+ }
68
+ return Object.fromEntries(entries);
69
+ }
70
+
71
+ export function normalizeFeatureFlags(featureFlags = null) {
72
+ return Object.freeze({
73
+ ...DEFAULT_FEATURES,
74
+ ...normalizeKnownFeatureFlags(featureFlags),
75
+ });
76
+ }
77
+
78
+ export function resolveFeatureFlags({
79
+ configFeatures = null,
80
+ env = process.env,
81
+ } = {}) {
82
+ return normalizeFeatureFlags({
83
+ ...normalizeKnownFeatureFlags(configFeatures),
84
+ ...readFeaturesFromEnv(env),
85
+ });
86
+ }
87
+
88
+ export const FEATURES = resolveFeatureFlags({
89
+ configFeatures: readFeaturesFromConfig(),
90
+ });
91
+
92
+ export function getSessionWorkerFeatureFlags(featureFlags = FEATURES) {
93
+ const normalized = normalizeFeatureFlags(featureFlags);
94
+ return {
95
+ enabled: normalized.SESSION_WORKER_ROUTING_ENABLED === true,
96
+ continuationRoutingEnabled: normalized.SESSION_WORKER_CONTINUATION_ROUTING_ENABLED === true,
97
+ fallbackRestartEnabled: normalized.SESSION_WORKER_FALLBACK_RESTART_ENABLED === true,
98
+ };
99
+ }
100
+
101
+ export function isFeatureEnabled(featureName, featureFlags = FEATURES) {
102
+ const name = String(featureName || '').trim();
103
+ if (!name) return false;
104
+ const normalized = normalizeFeatureFlags(featureFlags);
105
+ return normalized[name] === true;
106
+ }
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Minimal ACP JSON-RPC client over a child-process stdio transport.
3
+ * Adapted from grok-remote research (read-only reference); lives in
4
+ * copilot-remote so the Grok provider does not depend on that tree.
5
+ */
6
+ import { EventEmitter } from 'node:events';
7
+ import { spawn } from 'node:child_process';
8
+ import readline from 'node:readline';
9
+
10
+ function whichCommand(command = 'grok') {
11
+ const value = String(command || 'grok').trim() || 'grok';
12
+ return value;
13
+ }
14
+
15
+ // Prompt watchdog defaults: a turn that produces no ACP traffic for the
16
+ // inactivity window (while no client-side work is pending) is stalled; the
17
+ // ceiling bounds a single prompt absolutely. Both are overridable per call.
18
+ export const ACP_PROMPT_INACTIVITY_MS = 120_000;
19
+ export const ACP_PROMPT_MAX_TURN_MS = 1_800_000;
20
+
21
+ /**
22
+ * Pick the one-shot allow option from an ACP permission request. A blanket
23
+ * "allow always" would grant more than the relay's per-request approval
24
+ * parity intends; a missing/unrecognizable option list falls back to the
25
+ * conventional id.
26
+ */
27
+ export function pickAllowOnceOptionId(rawOptions) {
28
+ const options = Array.isArray(rawOptions) ? rawOptions : [];
29
+ const idOf = (opt) => String(opt?.optionId || opt?.id || '').toLowerCase();
30
+ const allowOnce = options.find((opt) => {
31
+ const id = idOf(opt);
32
+ return id.includes('allow') && !id.includes('always') && !id.includes('reject') && !id.includes('deny');
33
+ });
34
+ const allowAny = options.find((opt) => idOf(opt).includes('allow'));
35
+ return String(
36
+ allowOnce?.optionId || allowOnce?.id
37
+ || allowAny?.optionId || allowAny?.id
38
+ || 'allow-once',
39
+ );
40
+ }
41
+
42
+ export class AcpClient extends EventEmitter {
43
+ /**
44
+ * @param {object} opts
45
+ * @param {string} [opts.command]
46
+ * @param {string[]} [opts.args]
47
+ * @param {string} [opts.cwd]
48
+ * @param {NodeJS.ProcessEnv} [opts.env]
49
+ * @param {boolean} [opts.alwaysApprove]
50
+ * @param {typeof spawn} [opts.spawnImpl]
51
+ */
52
+ constructor(opts = {}) {
53
+ super();
54
+ this.command = whichCommand(opts.command || 'grok');
55
+ this.args = Array.isArray(opts.args) ? [...opts.args] : ['agent', '--no-leader', 'stdio'];
56
+ if (opts.alwaysApprove && !this.args.includes('--always-approve') && !this.args.includes('--yolo')) {
57
+ const i = this.args.indexOf('agent');
58
+ if (i >= 0) this.args = [...this.args.slice(0, i + 1), '--always-approve', ...this.args.slice(i + 1)];
59
+ else this.args = ['--always-approve', ...this.args];
60
+ }
61
+ this.spawnCwd = opts.cwd || process.cwd();
62
+ this.env = opts.env || process.env;
63
+ this.spawnImpl = opts.spawnImpl || spawn;
64
+ this.proc = null;
65
+ this.rl = null;
66
+ this.nextId = 1;
67
+ this.pending = new Map();
68
+ this.requestHandlers = new Map();
69
+ this._bufferErr = '';
70
+ this.dead = false;
71
+ this.initializeResult = null;
72
+ this.lastActivityAt = Date.now();
73
+ }
74
+
75
+ _touchActivity() {
76
+ this.lastActivityAt = Date.now();
77
+ }
78
+
79
+ /**
80
+ * Register a handler for an agent→client request method (terminal/*, fs/*).
81
+ * The handler receives the request params and its resolved value is sent as
82
+ * the JSON-RPC result (undefined becomes null); a rejection is sent as a
83
+ * -32603 error so the agent's tool call fails instead of hanging.
84
+ */
85
+ setRequestHandler(method, handler) {
86
+ this.requestHandlers.set(String(method || ''), handler);
87
+ }
88
+
89
+ start() {
90
+ if (this.proc) return;
91
+
92
+ this.proc = this.spawnImpl(this.command, this.args, {
93
+ cwd: this.spawnCwd,
94
+ env: {
95
+ ...this.env,
96
+ GROK_DISABLE_AUTOUPDATER: this.env.GROK_DISABLE_AUTOUPDATER || '1',
97
+ },
98
+ stdio: ['pipe', 'pipe', 'pipe'],
99
+ windowsHide: true,
100
+ shell: false,
101
+ });
102
+
103
+ this.rl = readline.createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
104
+ this.rl.on('line', (line) => this._onLine(line));
105
+
106
+ this.proc.stderr.on('data', (chunk) => {
107
+ const text = chunk.toString('utf8');
108
+ this._bufferErr += text;
109
+ this.emit('stderr', text);
110
+ });
111
+
112
+ this.proc.on('error', (err) => {
113
+ this.dead = true;
114
+ // Reject pending requests first: an EventEmitter 'error' emit with no
115
+ // listener throws ERR_UNHANDLED_ERROR synchronously, which would
116
+ // otherwise escape the spawn callback and crash the host process
117
+ // (e.g. the relay server during model discovery with no CLI on PATH).
118
+ this._rejectAll(err);
119
+ if (this.listenerCount('error') > 0) this.emit('error', err);
120
+ });
121
+
122
+ this.proc.on('exit', (code, signal) => {
123
+ this.dead = true;
124
+ this.emit('exit', { code, signal });
125
+ this._rejectAll(new Error(`agent exited code=${code} signal=${signal}`));
126
+ });
127
+ }
128
+
129
+ _onLine(line) {
130
+ const trimmed = line.trim();
131
+ if (!trimmed) return;
132
+ this._touchActivity();
133
+ let msg;
134
+ try {
135
+ msg = JSON.parse(trimmed);
136
+ } catch {
137
+ this.emit('parseError', trimmed);
138
+ return;
139
+ }
140
+
141
+ if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
142
+ const p = this.pending.get(msg.id);
143
+ if (p) {
144
+ this.pending.delete(msg.id);
145
+ if (msg.error) {
146
+ p.reject(Object.assign(new Error(msg.error.message || 'ACP error'), { acp: msg.error }));
147
+ } else {
148
+ p.resolve(msg.result);
149
+ }
150
+ }
151
+ return;
152
+ }
153
+
154
+ if (msg.method) {
155
+ this.emit('notification', msg);
156
+ // Method-named convenience events — except 'error', which EventEmitter
157
+ // treats specially: an agent sending {"method":"error"} would otherwise
158
+ // throw ERR_UNHANDLED_ERROR and kill the worker.
159
+ if (msg.method !== 'error') this.emit(msg.method, msg);
160
+ if (msg.id == null) return;
161
+ if (msg.method === 'session/request_permission') {
162
+ // The turn runner attaches its listener only while a prompt is in
163
+ // flight. A permission request arriving outside that window (a
164
+ // session/load replay, a request racing prompt settlement) must still
165
+ // get a reply — an unanswered agent→client request deadlocks the
166
+ // agent, the exact failure the -32601 fallback below exists for.
167
+ if (this.listenerCount('permission') === 0) {
168
+ const optionId = pickAllowOnceOptionId(msg?.params?.options);
169
+ this.respond(msg.id, { outcome: { outcome: 'selected', optionId } });
170
+ return;
171
+ }
172
+ this.emit('permission', msg);
173
+ return;
174
+ }
175
+ const handler = this.requestHandlers.get(msg.method);
176
+ if (!handler) {
177
+ // Fail fast: an agent→client request that never gets a response
178
+ // deadlocks the agent's turn — it waits forever on the reply (the
179
+ // 0117fb12 stall: terminal/create was advertised but unanswered).
180
+ // Method-not-found turns a capability mismatch into a visible tool
181
+ // error instead of a silent hang.
182
+ this.respondError(msg.id, `Method not found: ${msg.method}`, -32601);
183
+ return;
184
+ }
185
+ Promise.resolve()
186
+ .then(() => handler(msg.params || {}))
187
+ .then(
188
+ (result) => this.respond(msg.id, result === undefined ? null : result),
189
+ (error) => this.respondError(msg.id, error?.message || 'request handler failed', -32603),
190
+ );
191
+ }
192
+ }
193
+
194
+ _rejectAll(err) {
195
+ for (const [, p] of this.pending) p.reject(err);
196
+ this.pending.clear();
197
+ }
198
+
199
+ request(method, params = {}, timeoutMs = 120000) {
200
+ if (this.dead) return Promise.reject(new Error('agent is dead'));
201
+ this.start();
202
+ const id = this.nextId++;
203
+ const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params });
204
+ return new Promise((resolve, reject) => {
205
+ // timeoutMs <= 0 = no request-level timeout (an unlimited-ceiling
206
+ // prompt); the caller's watchdog owns liveness in that case.
207
+ const timer = Number(timeoutMs) > 0
208
+ ? setTimeout(() => {
209
+ this.pending.delete(id);
210
+ reject(new Error(`ACP request timeout: ${method}`));
211
+ }, timeoutMs)
212
+ : null;
213
+ // A pending request must not keep the process alive on its own — an
214
+ // abandoned prompt (watchdog raced past it) would otherwise pin the
215
+ // event loop for the full timeout.
216
+ if (timer && typeof timer.unref === 'function') timer.unref();
217
+ this.pending.set(id, {
218
+ resolve: (v) => {
219
+ if (timer) clearTimeout(timer);
220
+ resolve(v);
221
+ },
222
+ reject: (e) => {
223
+ if (timer) clearTimeout(timer);
224
+ reject(e);
225
+ },
226
+ });
227
+ this._touchActivity();
228
+ this.proc.stdin.write(`${payload}\n`, 'utf8');
229
+ });
230
+ }
231
+
232
+ notify(method, params = {}) {
233
+ if (this.dead) return;
234
+ this.start();
235
+ const payload = JSON.stringify({ jsonrpc: '2.0', method, params });
236
+ this._touchActivity();
237
+ this.proc.stdin.write(`${payload}\n`, 'utf8');
238
+ }
239
+
240
+ respond(id, result) {
241
+ if (this.dead) return;
242
+ const payload = JSON.stringify({ jsonrpc: '2.0', id, result });
243
+ this._touchActivity();
244
+ this.proc.stdin.write(`${payload}\n`, 'utf8');
245
+ }
246
+
247
+ respondError(id, message, code = -32000) {
248
+ if (this.dead) return;
249
+ const payload = JSON.stringify({
250
+ jsonrpc: '2.0',
251
+ id,
252
+ error: { code, message },
253
+ });
254
+ this._touchActivity();
255
+ this.proc.stdin.write(`${payload}\n`, 'utf8');
256
+ }
257
+
258
+ async initialize(clientInfo = { name: 'copilot-remote-grok', version: '1.0.0' }) {
259
+ this.start();
260
+ this.initializeResult = await this.request('initialize', {
261
+ protocolVersion: 1,
262
+ // Each capability advertised here is a contract to answer the agent's
263
+ // matching requests — the handlers live in acp-host-services.mjs and
264
+ // must be attached via setRequestHandler before the first prompt. An
265
+ // advertised-but-unanswered request deadlocks the agent's turn (the
266
+ // 0117fb12 stall: terminal/create waited forever on a reply).
267
+ clientCapabilities: {
268
+ fs: { readTextFile: true, writeTextFile: true },
269
+ terminal: true,
270
+ },
271
+ clientInfo,
272
+ });
273
+ // Do not send the generic ACP `initialized` notification: current Grok CLI
274
+ // agent builds log `failed to decode … Method not found` for it and ignore
275
+ // it. Session/new works without it.
276
+ return this.initializeResult;
277
+ }
278
+
279
+ async sessionNew(cwd, extra = {}) {
280
+ return this.request('session/new', {
281
+ cwd,
282
+ mcpServers: [],
283
+ ...extra,
284
+ });
285
+ }
286
+
287
+ async sessionLoad(sessionId, cwd, extra = {}) {
288
+ return this.request('session/load', {
289
+ sessionId,
290
+ cwd,
291
+ mcpServers: [],
292
+ ...extra,
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Send a prompt and stream session/update until the prompt request resolves.
298
+ *
299
+ * A watchdog replaces the old flat request timeout: the prompt fails when no
300
+ * ACP traffic (in either direction) happens for `inactivityMs` while no
301
+ * client-side work is pending (`hasPendingWork`), or when `maxTurnMs` is
302
+ * exceeded outright. On a trip the session is cancelled best-effort and the
303
+ * promise rejects with a message classifyGrokError maps to grok.turn-stalled.
304
+ *
305
+ * @param {string} sessionId
306
+ * @param {Array} prompt content blocks
307
+ * @param {(update: object) => void} [onUpdate]
308
+ * @param {object} [extra] additional session/prompt params (e.g. `_meta`)
309
+ * @param {object} [watchdog] { inactivityMs, maxTurnMs, hasPendingWork }
310
+ */
311
+ async sessionPrompt(sessionId, prompt, onUpdate, extra = {}, watchdog = {}) {
312
+ const inactivityMs = Number(watchdog?.inactivityMs) > 0
313
+ ? Number(watchdog.inactivityMs)
314
+ : ACP_PROMPT_INACTIVITY_MS;
315
+ // An explicit 0 means NO absolute ceiling (the user's "No limit"
316
+ // setting); only an absent/invalid value falls back to the default. The
317
+ // inactivity watchdog still catches dead transports either way.
318
+ const rawMaxTurnMs = Number(watchdog?.maxTurnMs);
319
+ const maxTurnMs = Number.isFinite(rawMaxTurnMs) && rawMaxTurnMs >= 0
320
+ ? rawMaxTurnMs
321
+ : ACP_PROMPT_MAX_TURN_MS;
322
+ const hasPendingWork = typeof watchdog?.hasPendingWork === 'function'
323
+ ? watchdog.hasPendingWork
324
+ : () => false;
325
+ const handler = (msg) => {
326
+ if (msg.method !== 'session/update') return;
327
+ if (msg.params?.sessionId && msg.params.sessionId !== sessionId) return;
328
+ onUpdate?.(msg.params?.update || msg.params);
329
+ };
330
+ this.on('notification', handler);
331
+ let watchdogTimer = null;
332
+ const startedAt = Date.now();
333
+ const stallPromise = new Promise((_, reject) => {
334
+ const pollMs = Math.max(250, Math.min(5000, Math.floor(inactivityMs / 4)));
335
+ watchdogTimer = setInterval(() => {
336
+ const now = Date.now();
337
+ if (maxTurnMs > 0 && now - startedAt >= maxTurnMs) {
338
+ try { this.sessionCancel(sessionId); } catch { /* ignore */ }
339
+ reject(new Error(`grok turn exceeded the ${Math.round(maxTurnMs / 60000)}-minute turn ceiling`));
340
+ return;
341
+ }
342
+ if (hasPendingWork()) return;
343
+ if (now - this.lastActivityAt >= inactivityMs) {
344
+ try { this.sessionCancel(sessionId); } catch { /* ignore */ }
345
+ reject(new Error(`grok turn stalled: no ACP activity for ${Math.round(inactivityMs / 1000)}s`));
346
+ }
347
+ }, pollMs);
348
+ if (typeof watchdogTimer.unref === 'function') watchdogTimer.unref();
349
+ });
350
+ try {
351
+ // The race keeps both promises observed, so the late loser cannot
352
+ // become an unhandled rejection. The raw request timeout sits above
353
+ // the ceiling; the watchdog is the real limiter.
354
+ return await Promise.race([
355
+ this.request(
356
+ 'session/prompt',
357
+ { sessionId, prompt, ...extra },
358
+ maxTurnMs > 0 ? maxTurnMs + 60_000 : 0,
359
+ ),
360
+ stallPromise,
361
+ ]);
362
+ } finally {
363
+ clearInterval(watchdogTimer);
364
+ this.off('notification', handler);
365
+ }
366
+ }
367
+
368
+ sessionCancel(sessionId) {
369
+ this.notify('session/cancel', { sessionId });
370
+ }
371
+
372
+ async dispose() {
373
+ this.dead = true;
374
+ try {
375
+ this.rl?.close();
376
+ } catch {
377
+ /* ignore */
378
+ }
379
+ if (this.proc && !this.proc.killed) {
380
+ this.proc.kill();
381
+ if (process.platform === 'win32' && this.proc.pid) {
382
+ try {
383
+ this.spawnImpl('taskkill', ['/pid', String(this.proc.pid), '/t', '/f'], {
384
+ stdio: 'ignore',
385
+ windowsHide: true,
386
+ });
387
+ } catch {
388
+ /* ignore */
389
+ }
390
+ }
391
+ }
392
+ this.proc = null;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Extract model ids + reasoning efforts from an ACP initialize result.
398
+ * Pure helper for unit tests and model discovery.
399
+ */
400
+ export function extractGrokModelsFromInitialize(initResult = null) {
401
+ const meta = initResult?._meta || initResult?.agentCapabilities?._meta || {};
402
+ const modelState = meta.modelState || {};
403
+ const available = Array.isArray(modelState.availableModels) ? modelState.availableModels : [];
404
+ const models = [];
405
+ const effortsByModel = {};
406
+ const contextWindowsByModel = {};
407
+ for (const entry of available) {
408
+ const modelId = String(
409
+ typeof entry === 'string' ? entry : (entry?.modelId || entry?.id || entry?.name || ''),
410
+ ).trim();
411
+ if (!modelId) continue;
412
+ models.push(modelId);
413
+ const efforts = Array.isArray(entry?.reasoningEfforts)
414
+ ? entry.reasoningEfforts.map((v) => String(v || '').trim().toLowerCase()).filter(Boolean)
415
+ : [];
416
+ if (efforts.length) effortsByModel[modelId.toLowerCase()] = efforts;
417
+ const contextWindow = Number(
418
+ entry?.contextWindow ?? entry?.context_window ?? entry?.maxContextTokens ?? entry?.maxTokens,
419
+ );
420
+ if (Number.isFinite(contextWindow) && contextWindow > 0) {
421
+ contextWindowsByModel[modelId.toLowerCase()] = Math.round(contextWindow);
422
+ }
423
+ }
424
+ const currentModelId = String(modelState.currentModelId || '').trim();
425
+ if (currentModelId && !models.some((m) => m.toLowerCase() === currentModelId.toLowerCase())) {
426
+ models.unshift(currentModelId);
427
+ }
428
+ return {
429
+ models,
430
+ defaultModel: currentModelId || models[0] || '',
431
+ effortsByModel,
432
+ contextWindowsByModel,
433
+ };
434
+ }