@wangyaoshen/remux 0.3.8-dev.29e114b

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 (183) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  4. package/.github/dependabot.yml +33 -0
  5. package/.github/workflows/ci.yml +65 -0
  6. package/.github/workflows/deploy.yml +65 -0
  7. package/.github/workflows/publish.yml +312 -0
  8. package/.github/workflows/release-please.yml +21 -0
  9. package/.gitmodules +3 -0
  10. package/.nvmrc +1 -0
  11. package/.release-please-manifest.json +3 -0
  12. package/CLAUDE.md +104 -0
  13. package/Dockerfile +23 -0
  14. package/LICENSE +21 -0
  15. package/README.md +120 -0
  16. package/apps/ios/Config/signing.xcconfig +4 -0
  17. package/apps/ios/Package.swift +26 -0
  18. package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
  19. package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  20. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
  21. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
  22. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
  23. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
  24. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
  25. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
  26. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
  27. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
  28. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
  29. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
  30. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
  31. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
  32. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
  33. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
  34. package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
  35. package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
  36. package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
  37. package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
  38. package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
  39. package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
  40. package/apps/ios/Sources/Remux/RootView.swift +130 -0
  41. package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
  42. package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
  43. package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
  44. package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
  45. package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
  46. package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
  47. package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
  48. package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
  49. package/apps/macos/Package.swift +37 -0
  50. package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
  51. package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
  52. package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
  53. package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
  54. package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
  55. package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
  56. package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
  57. package/apps/macos/Resources/terminfo/67/ghostty +0 -0
  58. package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
  59. package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
  60. package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
  61. package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
  62. package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
  63. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
  64. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
  65. package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
  66. package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
  67. package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
  68. package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
  69. package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
  70. package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
  71. package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
  72. package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
  73. package/apps/macos/Sources/Remux/SocketController.swift +258 -0
  74. package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
  75. package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
  76. package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
  77. package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
  78. package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
  79. package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
  80. package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
  81. package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
  82. package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
  83. package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
  84. package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
  85. package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
  86. package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
  87. package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
  88. package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
  89. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
  90. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
  91. package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
  92. package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
  93. package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
  94. package/build.mjs +33 -0
  95. package/native/android/DecodeGoldenPayloads.kt +487 -0
  96. package/native/android/ProtocolModels.kt +188 -0
  97. package/native/ios/DecodeGoldenPayloads.swift +711 -0
  98. package/native/ios/ProtocolModels.swift +200 -0
  99. package/package.json +45 -0
  100. package/packages/RemuxKit/Package.swift +27 -0
  101. package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
  102. package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
  103. package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
  104. package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
  105. package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
  106. package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
  107. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
  108. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
  109. package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
  110. package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
  111. package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
  112. package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
  113. package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
  114. package/playwright.config.ts +17 -0
  115. package/pnpm-lock.yaml +1588 -0
  116. package/pty-daemon.js +303 -0
  117. package/release-please-config.json +14 -0
  118. package/scripts/auto-deploy.sh +46 -0
  119. package/scripts/build-dmg.sh +121 -0
  120. package/scripts/build-ghostty-kit.sh +43 -0
  121. package/scripts/check-active-terminology.mjs +132 -0
  122. package/scripts/setup-ci-secrets.sh +80 -0
  123. package/scripts/sync-ghostty-web.sh +28 -0
  124. package/scripts/upload-testflight.sh +100 -0
  125. package/server.js +7074 -0
  126. package/src/adapters/agent-events.ts +246 -0
  127. package/src/adapters/claude-code.ts +158 -0
  128. package/src/adapters/codex.ts +210 -0
  129. package/src/adapters/generic-shell.ts +58 -0
  130. package/src/adapters/index.ts +15 -0
  131. package/src/adapters/registry.ts +99 -0
  132. package/src/adapters/types.ts +41 -0
  133. package/src/auth.ts +174 -0
  134. package/src/e2ee.ts +236 -0
  135. package/src/git-service.ts +168 -0
  136. package/src/message-buffer.ts +137 -0
  137. package/src/pty-daemon.ts +357 -0
  138. package/src/push.ts +127 -0
  139. package/src/renderers.ts +455 -0
  140. package/src/server.ts +2407 -0
  141. package/src/service.ts +226 -0
  142. package/src/session.ts +978 -0
  143. package/src/store.ts +1422 -0
  144. package/src/team.ts +123 -0
  145. package/src/tunnel.ts +126 -0
  146. package/src/types.d.ts +50 -0
  147. package/src/vt-tracker.ts +188 -0
  148. package/src/workspace-head.ts +144 -0
  149. package/src/workspace.ts +153 -0
  150. package/src/ws-handler.ts +1526 -0
  151. package/start.ps1 +83 -0
  152. package/tests/adapters.test.js +171 -0
  153. package/tests/auth.test.js +243 -0
  154. package/tests/codex-adapter.test.js +535 -0
  155. package/tests/durable-stream.test.js +153 -0
  156. package/tests/e2e/app.spec.js +530 -0
  157. package/tests/e2ee.test.js +325 -0
  158. package/tests/message-buffer.test.js +245 -0
  159. package/tests/message-routing.test.js +305 -0
  160. package/tests/pty-daemon.test.js +346 -0
  161. package/tests/push.test.js +281 -0
  162. package/tests/renderers.test.js +391 -0
  163. package/tests/search-shell.test.js +499 -0
  164. package/tests/server.test.js +882 -0
  165. package/tests/service.test.js +267 -0
  166. package/tests/store.test.js +369 -0
  167. package/tests/tunnel.test.js +67 -0
  168. package/tests/workspace-head.test.js +116 -0
  169. package/tests/workspace.test.js +417 -0
  170. package/tsconfig.backend.json +11 -0
  171. package/tsconfig.json +15 -0
  172. package/tui/client/client_test.go +125 -0
  173. package/tui/client/connection.go +342 -0
  174. package/tui/client/host_manager.go +141 -0
  175. package/tui/config/cache.go +81 -0
  176. package/tui/config/config.go +53 -0
  177. package/tui/config/config_test.go +89 -0
  178. package/tui/go.mod +32 -0
  179. package/tui/go.sum +50 -0
  180. package/tui/main.go +261 -0
  181. package/tui/tests/integration_test.go +283 -0
  182. package/tui/ui/model.go +310 -0
  183. package/vitest.config.js +10 -0
@@ -0,0 +1,246 @@
1
+ // E10-009: Unified agent event types and parsers
2
+ // Normalizes events from different AI agents (Claude Code, Codex, etc.)
3
+ // into a common format for structured rendering on clients.
4
+
5
+ export interface AgentToolCall {
6
+ tool: string; // "file_read", "file_write", "bash", "search", etc.
7
+ args: Record<string, unknown>;
8
+ status: "pending" | "running" | "completed" | "failed";
9
+ output?: string;
10
+ }
11
+
12
+ export interface AgentApproval {
13
+ id: string;
14
+ tool: string;
15
+ description: string;
16
+ status: "pending" | "approved" | "rejected";
17
+ }
18
+
19
+ export interface AgentTurn {
20
+ role: "user" | "assistant";
21
+ content: string;
22
+ toolCalls?: AgentToolCall[];
23
+ approvals?: AgentApproval[];
24
+ timestamp: string;
25
+ }
26
+
27
+ export interface AgentSessionSummary {
28
+ agentId: string; // "claude-code" | "codex" | "generic-shell"
29
+ agentName: string;
30
+ state: "idle" | "running" | "waiting_approval" | "error";
31
+ currentTurn?: AgentTurn;
32
+ recentToolCalls: AgentToolCall[];
33
+ pendingApprovals: AgentApproval[];
34
+ }
35
+
36
+ // ── Claude Code event parser ──────────────────────────────────────
37
+
38
+ /**
39
+ * Parse a Claude Code events.jsonl / conversation.jsonl entry
40
+ * into the unified AgentTurn format.
41
+ *
42
+ * Claude Code event shapes observed:
43
+ * { type: "assistant", message: { content: [...] } }
44
+ * { type: "tool_use", name: "Bash", input: { command: "..." } }
45
+ * { type: "tool_result", tool_use_id: "...", content: "..." }
46
+ * { type: "result", result: "..." }
47
+ * { type: "permission_request", tool: "...", description: "..." }
48
+ * { type: "end_turn" }
49
+ * { type: "error", error: "..." }
50
+ */
51
+ export function parseClaudeCodeEvent(
52
+ event: Record<string, unknown>,
53
+ ): Partial<AgentTurn> | null {
54
+ const type = event.type as string | undefined;
55
+ if (!type) return null;
56
+
57
+ switch (type) {
58
+ case "assistant": {
59
+ const message = event.message as Record<string, unknown> | undefined;
60
+ const contentBlocks = (message?.content ?? []) as Array<Record<string, unknown>>;
61
+ const textParts = contentBlocks
62
+ .filter((b) => b.type === "text")
63
+ .map((b) => b.text as string);
64
+ return {
65
+ role: "assistant",
66
+ content: textParts.join("\n") || "",
67
+ timestamp: new Date().toISOString(),
68
+ };
69
+ }
70
+
71
+ case "tool_use": {
72
+ const toolCall: AgentToolCall = {
73
+ tool: (event.name as string) ?? "unknown",
74
+ args: (event.input as Record<string, unknown>) ?? {},
75
+ status: "running",
76
+ };
77
+ return {
78
+ role: "assistant",
79
+ content: "",
80
+ toolCalls: [toolCall],
81
+ timestamp: new Date().toISOString(),
82
+ };
83
+ }
84
+
85
+ case "tool_result": {
86
+ const toolCall: AgentToolCall = {
87
+ tool: (event.name as string) ?? "unknown",
88
+ args: {},
89
+ status: "completed",
90
+ output:
91
+ typeof event.content === "string"
92
+ ? event.content
93
+ : JSON.stringify(event.content ?? ""),
94
+ };
95
+ return {
96
+ role: "assistant",
97
+ content: "",
98
+ toolCalls: [toolCall],
99
+ timestamp: new Date().toISOString(),
100
+ };
101
+ }
102
+
103
+ case "permission_request": {
104
+ const approval: AgentApproval = {
105
+ id: (event.id as string) ?? `perm-${Date.now()}`,
106
+ tool: (event.tool as string) ?? "unknown",
107
+ description: (event.description as string) ?? "",
108
+ status: "pending",
109
+ };
110
+ return {
111
+ role: "assistant",
112
+ content: "",
113
+ approvals: [approval],
114
+ timestamp: new Date().toISOString(),
115
+ };
116
+ }
117
+
118
+ case "result":
119
+ case "end_turn":
120
+ return {
121
+ role: "assistant",
122
+ content: typeof event.result === "string" ? event.result : "",
123
+ timestamp: new Date().toISOString(),
124
+ };
125
+
126
+ case "error":
127
+ return {
128
+ role: "assistant",
129
+ content: `Error: ${(event.error as string) ?? "unknown error"}`,
130
+ timestamp: new Date().toISOString(),
131
+ };
132
+
133
+ default:
134
+ return null;
135
+ }
136
+ }
137
+
138
+ // ── Codex event parser ────────────────────────────────────────────
139
+
140
+ /**
141
+ * Parse a Codex CLI JSONL event into the unified AgentTurn format.
142
+ *
143
+ * Codex uses a JSON-RPC style with items/turns/threads:
144
+ * { type: "item.created", item: { type: "message", role: "assistant", content: [...] } }
145
+ * { type: "turn.started" }
146
+ * { type: "turn.completed" }
147
+ * { type: "tool_use", name: "shell", input: { command: "..." } }
148
+ * { type: "tool_result", output: "..." }
149
+ * { type: "permission_request", command: "...", description: "..." }
150
+ * { type: "error", message: "..." }
151
+ */
152
+ export function parseCodexEvent(
153
+ event: Record<string, unknown>,
154
+ ): Partial<AgentTurn> | null {
155
+ const type = event.type as string | undefined;
156
+ if (!type) return null;
157
+
158
+ switch (type) {
159
+ case "item.created": {
160
+ const item = event.item as Record<string, unknown> | undefined;
161
+ if (!item) return null;
162
+ const role =
163
+ (item.role as string) === "user" ? "user" : "assistant";
164
+ const contentBlocks = (item.content ?? []) as Array<Record<string, unknown>>;
165
+ const textParts = contentBlocks
166
+ .filter((b) => b.type === "text" || b.type === "output_text")
167
+ .map((b) => (b.text ?? b.output ?? "") as string);
168
+ return {
169
+ role: role as "user" | "assistant",
170
+ content: textParts.join("\n") || "",
171
+ timestamp: new Date().toISOString(),
172
+ };
173
+ }
174
+
175
+ case "tool_use": {
176
+ const toolCall: AgentToolCall = {
177
+ tool: (event.name as string) ?? "unknown",
178
+ args: (event.input as Record<string, unknown>) ?? {},
179
+ status: "running",
180
+ };
181
+ return {
182
+ role: "assistant",
183
+ content: "",
184
+ toolCalls: [toolCall],
185
+ timestamp: new Date().toISOString(),
186
+ };
187
+ }
188
+
189
+ case "tool_result": {
190
+ const toolCall: AgentToolCall = {
191
+ tool: (event.name as string) ?? "unknown",
192
+ args: {},
193
+ status: "completed",
194
+ output:
195
+ typeof event.output === "string"
196
+ ? event.output
197
+ : JSON.stringify(event.output ?? ""),
198
+ };
199
+ return {
200
+ role: "assistant",
201
+ content: "",
202
+ toolCalls: [toolCall],
203
+ timestamp: new Date().toISOString(),
204
+ };
205
+ }
206
+
207
+ case "permission_request": {
208
+ const approval: AgentApproval = {
209
+ id: (event.id as string) ?? `perm-${Date.now()}`,
210
+ tool: (event.command as string) ?? "unknown",
211
+ description: (event.description as string) ?? "",
212
+ status: "pending",
213
+ };
214
+ return {
215
+ role: "assistant",
216
+ content: "",
217
+ approvals: [approval],
218
+ timestamp: new Date().toISOString(),
219
+ };
220
+ }
221
+
222
+ case "turn.started":
223
+ return {
224
+ role: "assistant",
225
+ content: "",
226
+ timestamp: new Date().toISOString(),
227
+ };
228
+
229
+ case "turn.completed":
230
+ return {
231
+ role: "assistant",
232
+ content: "",
233
+ timestamp: new Date().toISOString(),
234
+ };
235
+
236
+ case "error":
237
+ return {
238
+ role: "assistant",
239
+ content: `Error: ${(event.message as string) ?? "unknown error"}`,
240
+ timestamp: new Date().toISOString(),
241
+ };
242
+
243
+ default:
244
+ return null;
245
+ }
246
+ }
@@ -0,0 +1,158 @@
1
+ // E10-005: claude-code adapter — passive mode, monitors Claude Code events.jsonl
2
+ // Reports run status: idle/running/waiting_approval/error
3
+
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as os from "os";
7
+ import { SemanticAdapter, AdapterState, SemanticEvent } from "./types.js";
8
+
9
+ export class ClaudeCodeAdapter implements SemanticAdapter {
10
+ id = "claude-code";
11
+ name = "Claude Code";
12
+ mode = "passive" as const;
13
+ capabilities = ["run-status", "conversation-events", "tool-use"];
14
+
15
+ private state: AdapterState = {
16
+ adapterId: "claude-code",
17
+ name: "Claude Code",
18
+ mode: "passive",
19
+ capabilities: this.capabilities,
20
+ currentState: "idle",
21
+ };
22
+
23
+ private watcher: fs.FSWatcher | null = null;
24
+ private fileSizes = new Map<string, number>();
25
+ private eventsDir: string;
26
+ private onEmit?: (event: SemanticEvent) => void;
27
+
28
+ constructor(onEmit?: (event: SemanticEvent) => void) {
29
+ this.onEmit = onEmit;
30
+ // Claude Code stores events in ~/.claude/ or project-specific paths
31
+ this.eventsDir = path.join(os.homedir(), ".claude", "projects");
32
+ }
33
+
34
+ start(): void {
35
+ this.watchForEvents();
36
+ }
37
+
38
+ stop(): void {
39
+ this.watcher?.close();
40
+ this.watcher = null;
41
+ }
42
+
43
+ onTerminalData(sessionName: string, data: string): void {
44
+ // Detect Claude Code activity from terminal output patterns
45
+ if (data.includes("claude") || data.includes("Claude")) {
46
+ // Check for common Claude Code output patterns
47
+ if (data.includes("Thinking...") || data.includes("⏳")) {
48
+ this.updateState("running");
49
+ } else if (
50
+ data.includes("Done") ||
51
+ data.includes("✓") ||
52
+ data.includes("Complete")
53
+ ) {
54
+ this.updateState("idle");
55
+ } else if (
56
+ data.includes("Permission") ||
57
+ data.includes("approve") ||
58
+ data.includes("Allow")
59
+ ) {
60
+ this.updateState("waiting_approval");
61
+ }
62
+ }
63
+ }
64
+
65
+ getCurrentState(): AdapterState {
66
+ return { ...this.state };
67
+ }
68
+
69
+ private updateState(
70
+ newState: "idle" | "running" | "waiting_approval" | "error",
71
+ ): void {
72
+ if (this.state.currentState !== newState) {
73
+ this.state.currentState = newState;
74
+
75
+ if (this.onEmit) {
76
+ this.onEmit({
77
+ type: "state_change",
78
+ seq: Date.now(),
79
+ timestamp: new Date().toISOString(),
80
+ data: { state: newState },
81
+ adapterId: this.id,
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ private watchForEvents(): void {
88
+ // Watch for events.jsonl files in Claude Code project directories
89
+ if (!fs.existsSync(this.eventsDir)) return;
90
+
91
+ try {
92
+ this.watcher = fs.watch(
93
+ this.eventsDir,
94
+ { recursive: true },
95
+ (eventType, filename) => {
96
+ if (
97
+ filename &&
98
+ (filename.endsWith("events.jsonl") ||
99
+ filename.endsWith("conversation.jsonl"))
100
+ ) {
101
+ this.processEventFile(path.join(this.eventsDir, filename));
102
+ }
103
+ },
104
+ );
105
+ } catch {
106
+ // Directory may not exist or not be watchable
107
+ }
108
+ }
109
+
110
+ private processEventFile(filePath: string): void {
111
+ try {
112
+ const stat = fs.statSync(filePath);
113
+ const lastSize = this.fileSizes.get(filePath) ?? 0;
114
+ if (stat.size <= lastSize) return;
115
+
116
+ // Read only new content (with proper fd cleanup)
117
+ let newData: Buffer;
118
+ const fd = fs.openSync(filePath, "r");
119
+ try {
120
+ newData = Buffer.alloc(stat.size - lastSize);
121
+ fs.readSync(fd, newData, 0, newData.length, lastSize);
122
+ this.fileSizes.set(filePath, stat.size);
123
+ } finally {
124
+ fs.closeSync(fd);
125
+ }
126
+
127
+ // Parse JSONL lines
128
+ const lines = newData
129
+ .toString()
130
+ .split("\n")
131
+ .filter((l) => l.trim());
132
+ for (const line of lines) {
133
+ try {
134
+ const event = JSON.parse(line);
135
+ this.handleConversationEvent(event);
136
+ } catch {
137
+ // Skip malformed lines
138
+ }
139
+ }
140
+ } catch {
141
+ // File access error
142
+ }
143
+ }
144
+
145
+ private handleConversationEvent(event: Record<string, unknown>): void {
146
+ const type = event.type as string;
147
+
148
+ if (type === "assistant" || type === "tool_use") {
149
+ this.updateState("running");
150
+ } else if (type === "result" || type === "end_turn") {
151
+ this.updateState("idle");
152
+ } else if (type === "permission_request") {
153
+ this.updateState("waiting_approval");
154
+ } else if (type === "error") {
155
+ this.updateState("error");
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,210 @@
1
+ // E10-008: codex adapter — passive mode, monitors Codex CLI events
2
+ // Reports run status: idle/running/waiting_approval/error
3
+ // Watches ~/.codex/ for session JSONL files and detects terminal patterns.
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ import { SemanticAdapter, AdapterState, SemanticEvent } from "./types.js";
9
+
10
+ export class CodexAdapter implements SemanticAdapter {
11
+ id = "codex";
12
+ name = "OpenAI Codex";
13
+ mode = "passive" as const;
14
+ capabilities = ["run-status", "conversation-events", "tool-use"];
15
+
16
+ private state: AdapterState = {
17
+ adapterId: "codex",
18
+ name: "OpenAI Codex",
19
+ mode: "passive",
20
+ capabilities: this.capabilities,
21
+ currentState: "idle",
22
+ };
23
+
24
+ private watcher: fs.FSWatcher | null = null;
25
+ private fileSizes = new Map<string, number>();
26
+ private eventsDir: string;
27
+ private onEmit?: (event: SemanticEvent) => void;
28
+
29
+ constructor(onEmit?: (event: SemanticEvent) => void) {
30
+ this.onEmit = onEmit;
31
+ // Codex CLI stores session data in ~/.codex/
32
+ this.eventsDir = path.join(os.homedir(), ".codex");
33
+ }
34
+
35
+ start(): void {
36
+ this.watchForEvents();
37
+ }
38
+
39
+ stop(): void {
40
+ this.watcher?.close();
41
+ this.watcher = null;
42
+ }
43
+
44
+ onTerminalData(sessionName: string, data: string): void {
45
+ // Detect Codex CLI activity from terminal output patterns.
46
+ // Only process data that looks like Codex output to avoid false positives.
47
+
48
+ // Running indicators: thinking/working state
49
+ if (
50
+ data.includes("Thinking...") ||
51
+ data.includes("Working...") ||
52
+ data.includes("⠋") ||
53
+ data.includes("⠙") ||
54
+ data.includes("⠹") ||
55
+ data.includes("⠸") ||
56
+ data.includes("⠼") ||
57
+ data.includes("⠴") ||
58
+ data.includes("⠦") ||
59
+ data.includes("⠧") ||
60
+ data.includes("⠇") ||
61
+ data.includes("⠏")
62
+ ) {
63
+ this.updateState("running");
64
+ return;
65
+ }
66
+
67
+ // Tool use indicators
68
+ if (
69
+ data.includes("Reading file:") ||
70
+ data.includes("Writing file:") ||
71
+ data.includes("Editing file:") ||
72
+ data.includes("Running:") ||
73
+ data.includes("Patch:") ||
74
+ data.includes("[tool_use]")
75
+ ) {
76
+ this.updateState("running");
77
+ return;
78
+ }
79
+
80
+ // Approval / permission request indicators
81
+ if (
82
+ data.includes("Approve?") ||
83
+ data.includes("[y/N]") ||
84
+ data.includes("[y/n]") ||
85
+ data.includes("Allow this?") ||
86
+ data.includes("Run command?")
87
+ ) {
88
+ this.updateState("waiting_approval");
89
+ return;
90
+ }
91
+
92
+ // Error indicators
93
+ if (data.includes("Error:") || data.includes("Failed:")) {
94
+ // Only count as error if it looks like Codex produced it,
95
+ // not arbitrary program output. Check for codex context.
96
+ if (
97
+ data.includes("codex") ||
98
+ data.includes("Codex") ||
99
+ data.includes("codex>")
100
+ ) {
101
+ this.updateState("error");
102
+ return;
103
+ }
104
+ }
105
+
106
+ // Completion: return to codex prompt or explicit "Done"
107
+ if (data.includes("codex>") || data.includes("Done")) {
108
+ this.updateState("idle");
109
+ return;
110
+ }
111
+ }
112
+
113
+ getCurrentState(): AdapterState {
114
+ return { ...this.state };
115
+ }
116
+
117
+ private updateState(
118
+ newState: "idle" | "running" | "waiting_approval" | "error",
119
+ ): void {
120
+ if (this.state.currentState !== newState) {
121
+ this.state.currentState = newState;
122
+
123
+ if (this.onEmit) {
124
+ this.onEmit({
125
+ type: "state_change",
126
+ seq: Date.now(),
127
+ timestamp: new Date().toISOString(),
128
+ data: { state: newState },
129
+ adapterId: this.id,
130
+ });
131
+ }
132
+ }
133
+ }
134
+
135
+ private watchForEvents(): void {
136
+ // Watch for JSONL session files in ~/.codex/
137
+ if (!fs.existsSync(this.eventsDir)) return;
138
+
139
+ try {
140
+ this.watcher = fs.watch(
141
+ this.eventsDir,
142
+ { recursive: true },
143
+ (eventType, filename) => {
144
+ if (
145
+ filename &&
146
+ (filename.endsWith(".jsonl") || filename.endsWith(".json"))
147
+ ) {
148
+ this.processEventFile(path.join(this.eventsDir, filename));
149
+ }
150
+ },
151
+ );
152
+ } catch {
153
+ // Directory may not exist or not be watchable
154
+ }
155
+ }
156
+
157
+ private processEventFile(filePath: string): void {
158
+ try {
159
+ const stat = fs.statSync(filePath);
160
+ const lastSize = this.fileSizes.get(filePath) ?? 0;
161
+ if (stat.size <= lastSize) return;
162
+
163
+ // Read only new content (with proper fd cleanup)
164
+ let newData: Buffer;
165
+ const fd = fs.openSync(filePath, "r");
166
+ try {
167
+ newData = Buffer.alloc(stat.size - lastSize);
168
+ fs.readSync(fd, newData, 0, newData.length, lastSize);
169
+ this.fileSizes.set(filePath, stat.size);
170
+ } finally {
171
+ fs.closeSync(fd);
172
+ }
173
+
174
+ // Parse JSONL lines
175
+ const lines = newData
176
+ .toString()
177
+ .split("\n")
178
+ .filter((l) => l.trim());
179
+ for (const line of lines) {
180
+ try {
181
+ const event = JSON.parse(line);
182
+ this.handleSessionEvent(event);
183
+ } catch {
184
+ // Skip malformed lines
185
+ }
186
+ }
187
+ } catch {
188
+ // File access error — file may have been deleted between watch and read
189
+ }
190
+ }
191
+
192
+ private handleSessionEvent(event: Record<string, unknown>): void {
193
+ const type = event.type as string;
194
+
195
+ // Codex JSON-RPC style events (items/turns/threads)
196
+ if (
197
+ type === "item.created" ||
198
+ type === "tool_use" ||
199
+ type === "turn.started"
200
+ ) {
201
+ this.updateState("running");
202
+ } else if (type === "turn.completed" || type === "done") {
203
+ this.updateState("idle");
204
+ } else if (type === "permission_request") {
205
+ this.updateState("waiting_approval");
206
+ } else if (type === "error") {
207
+ this.updateState("error");
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,58 @@
1
+ // E10-004: generic-shell adapter — baseline adapter, always available as fallback
2
+ // Reports basic shell info from OSC 133 shell integration data
3
+
4
+ import { SemanticAdapter, AdapterState } from "./types.js";
5
+
6
+ export class GenericShellAdapter implements SemanticAdapter {
7
+ id = "generic-shell";
8
+ name = "Shell";
9
+ mode = "passive" as const;
10
+ capabilities = ["cwd", "last-command", "exit-code"];
11
+
12
+ private state: AdapterState = {
13
+ adapterId: "generic-shell",
14
+ name: "Shell",
15
+ mode: "passive",
16
+ capabilities: this.capabilities,
17
+ currentState: "idle",
18
+ };
19
+
20
+ private lastCwd: string | null = null;
21
+ private lastCommand: string | null = null;
22
+
23
+ onTerminalData(sessionName: string, data: string): void {
24
+ // Detect command prompts and working directory from OSC sequences
25
+ // OSC 7: working directory
26
+ const osc7Match = data.match(/\x1b\]7;file:\/\/[^/]*([^\x07\x1b]+)/);
27
+ if (osc7Match) {
28
+ this.lastCwd = decodeURIComponent(osc7Match[1]);
29
+ }
30
+
31
+ // OSC 133;B: command start — the command text follows
32
+ const osc133B = data.match(/\x1b\]133;B\x07/);
33
+ if (osc133B) {
34
+ this.state.currentState = "running";
35
+ }
36
+
37
+ // OSC 133;D: command finished
38
+ const osc133D = data.match(/\x1b\]133;D;?(\d*)\x07/);
39
+ if (osc133D) {
40
+ this.state.currentState = "idle";
41
+ }
42
+ }
43
+
44
+ getCurrentState(): AdapterState {
45
+ return {
46
+ ...this.state,
47
+ lastEvent: this.lastCwd
48
+ ? {
49
+ type: "cwd",
50
+ seq: 0,
51
+ timestamp: new Date().toISOString(),
52
+ data: { cwd: this.lastCwd, lastCommand: this.lastCommand },
53
+ adapterId: this.id,
54
+ }
55
+ : undefined,
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,15 @@
1
+ // E10 Adapter Platform — entry point
2
+
3
+ export { SemanticEvent, AdapterState, SemanticAdapter } from "./types.js";
4
+ export { AdapterRegistry } from "./registry.js";
5
+ export { GenericShellAdapter } from "./generic-shell.js";
6
+ export { ClaudeCodeAdapter } from "./claude-code.js";
7
+ export { CodexAdapter } from "./codex.js";
8
+ export {
9
+ AgentToolCall,
10
+ AgentApproval,
11
+ AgentTurn,
12
+ AgentSessionSummary,
13
+ parseClaudeCodeEvent,
14
+ parseCodexEvent,
15
+ } from "./agent-events.js";