pragma-so 0.1.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 (65) hide show
  1. package/cli/index.ts +882 -0
  2. package/index.ts +3 -0
  3. package/package.json +53 -0
  4. package/server/connectorBinaries.ts +103 -0
  5. package/server/connectorRegistry.ts +158 -0
  6. package/server/conversation/adapterRegistry.ts +53 -0
  7. package/server/conversation/adapters/claudeAdapter.ts +138 -0
  8. package/server/conversation/adapters/codexAdapter.ts +142 -0
  9. package/server/conversation/adapters.ts +224 -0
  10. package/server/conversation/executeRunner.ts +1191 -0
  11. package/server/conversation/gitWorkflow.ts +1037 -0
  12. package/server/conversation/models.ts +23 -0
  13. package/server/conversation/pragmaCli.ts +34 -0
  14. package/server/conversation/prompts.ts +335 -0
  15. package/server/conversation/store.ts +805 -0
  16. package/server/conversation/titleGenerator.ts +106 -0
  17. package/server/conversation/turnRunner.ts +365 -0
  18. package/server/conversation/types.ts +134 -0
  19. package/server/db.ts +837 -0
  20. package/server/http/middleware.ts +31 -0
  21. package/server/http/schemas.ts +430 -0
  22. package/server/http/validators.ts +38 -0
  23. package/server/index.ts +6560 -0
  24. package/server/process/runCommand.ts +142 -0
  25. package/server/stores/agentStore.ts +167 -0
  26. package/server/stores/connectorStore.ts +299 -0
  27. package/server/stores/humanStore.ts +28 -0
  28. package/server/stores/skillStore.ts +127 -0
  29. package/server/stores/taskStore.ts +371 -0
  30. package/shared/net.ts +24 -0
  31. package/tsconfig.json +14 -0
  32. package/ui/index.html +14 -0
  33. package/ui/public/favicon-32.png +0 -0
  34. package/ui/public/favicon.png +0 -0
  35. package/ui/src/App.jsx +1338 -0
  36. package/ui/src/api.js +954 -0
  37. package/ui/src/components/CodeView.jsx +319 -0
  38. package/ui/src/components/ConnectionsView.jsx +1004 -0
  39. package/ui/src/components/ContextView.jsx +315 -0
  40. package/ui/src/components/ConversationDrawer.jsx +963 -0
  41. package/ui/src/components/EmptyPane.jsx +20 -0
  42. package/ui/src/components/FeedView.jsx +773 -0
  43. package/ui/src/components/FilesView.jsx +257 -0
  44. package/ui/src/components/InlineChatView.jsx +158 -0
  45. package/ui/src/components/InputBar.jsx +476 -0
  46. package/ui/src/components/OnboardingModal.jsx +112 -0
  47. package/ui/src/components/OutputPanel.jsx +658 -0
  48. package/ui/src/components/PlanProposalPanel.jsx +177 -0
  49. package/ui/src/components/RightPanel.jsx +951 -0
  50. package/ui/src/components/SettingsView.jsx +186 -0
  51. package/ui/src/components/Sidebar.jsx +247 -0
  52. package/ui/src/components/TestingPane.jsx +198 -0
  53. package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
  54. package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
  55. package/ui/src/components/testing/TerminalPanel.jsx +104 -0
  56. package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
  57. package/ui/src/hooks/useAgents.js +81 -0
  58. package/ui/src/hooks/useConversation.js +252 -0
  59. package/ui/src/hooks/useTasks.js +161 -0
  60. package/ui/src/hooks/useWorkspace.js +259 -0
  61. package/ui/src/lib/agentIcon.js +10 -0
  62. package/ui/src/lib/conversationUtils.js +575 -0
  63. package/ui/src/main.jsx +10 -0
  64. package/ui/src/styles.css +6899 -0
  65. package/ui/vite.config.mjs +6 -0
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "./cli";
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "pragma-so",
3
+ "version": "0.1.0",
4
+ "description": "Very minimal pragma CLI",
5
+ "main": "dist/cli/index.js",
6
+ "bin": {
7
+ "pragma": "dist/cli/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && npm run ui:build",
11
+ "start": "node dist/cli/index.js",
12
+ "ui:dev": "vite --config ui/vite.config.mjs ui",
13
+ "ui:build": "vite build --config ui/vite.config.mjs ui",
14
+ "ui:preview": "vite preview --config ui/vite.config.mjs ui"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "commander",
19
+ "typescript"
20
+ ],
21
+ "author": "",
22
+ "license": "ISC",
23
+ "type": "commonjs",
24
+ "dependencies": {
25
+ "@electric-sql/pglite": "^0.3.16",
26
+ "@emoji-mart/data": "^1.2.1",
27
+ "@emoji-mart/react": "^1.1.1",
28
+ "@hono/node-server": "^1.19.11",
29
+ "@hono/zod-validator": "^0.7.6",
30
+ "@types/mime-types": "^3.0.1",
31
+ "commander": "^14.0.3",
32
+ "date-fns": "^4.1.0",
33
+ "eventsource-parser": "^3.0.6",
34
+ "execa": "^5.1.1",
35
+ "framer-motion": "^12.38.0",
36
+ "hono": "^4.12.7",
37
+ "lucide-react": "^0.577.0",
38
+ "mime-types": "^3.0.2",
39
+ "open": "^8.4.2",
40
+ "papaparse": "^5.5.3",
41
+ "react": "^18.3.1",
42
+ "react-dom": "^18.3.1",
43
+ "react-markdown": "^10.1.0",
44
+ "remark-gfm": "^4.0.1",
45
+ "zod": "^4.3.6"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.4.0",
49
+ "@vitejs/plugin-react": "^4.3.1",
50
+ "typescript": "^5.9.3",
51
+ "vite": "^5.4.11"
52
+ }
53
+ }
@@ -0,0 +1,103 @@
1
+ import { join } from "node:path";
2
+ import { access, mkdir, chmod, readdir, rename, unlink } from "node:fs/promises";
3
+ import { createWriteStream } from "node:fs";
4
+ import { pipeline } from "node:stream/promises";
5
+ import { execSync } from "node:child_process";
6
+ import { getWorkspacePaths } from "./db";
7
+
8
+ export async function ensureConnectorBinary(
9
+ binaryName: string,
10
+ downloadUrl: string,
11
+ workspaceName: string,
12
+ ): Promise<string> {
13
+ const binDir = getWorkspacePaths(workspaceName).binDir;
14
+ const binPath = join(binDir, binaryName);
15
+
16
+ try {
17
+ await access(binPath);
18
+ return binPath;
19
+ } catch {
20
+ // need to download
21
+ }
22
+
23
+ await mkdir(binDir, { recursive: true });
24
+
25
+ if (downloadUrl.startsWith("npm:")) {
26
+ const pkg = downloadUrl.slice(4);
27
+ const modulesDir = join(binDir, "node_modules");
28
+ execSync(`npm install --prefix "${binDir}" ${pkg}`, { stdio: "pipe" });
29
+ // Try to find the binary in the installed package
30
+ const pkgBinDir = join(modulesDir, ".bin");
31
+ const pkgBinPath = join(pkgBinDir, binaryName);
32
+ try {
33
+ await access(pkgBinPath);
34
+ // Symlink or copy to binDir
35
+ const { symlinkSync } = await import("node:fs");
36
+ try {
37
+ symlinkSync(pkgBinPath, binPath);
38
+ } catch {
39
+ // Already exists or can't symlink — that's fine
40
+ }
41
+ } catch {
42
+ // Binary might be directly in modulesDir — best effort
43
+ }
44
+ return binPath;
45
+ }
46
+
47
+ // Download .tar.gz, extract the binary
48
+ const response = await fetch(downloadUrl);
49
+ if (!response.ok) {
50
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
51
+ }
52
+
53
+ const tmpFile = join(binDir, `_download_${binaryName}.tar.gz`);
54
+ const tmpExtractDir = join(binDir, `_extract_${binaryName}`);
55
+
56
+ try {
57
+ // Write response to temp file
58
+ const body = response.body;
59
+ if (!body) {
60
+ throw new Error("Empty response body");
61
+ }
62
+ const fileStream = createWriteStream(tmpFile);
63
+ await pipeline(body, fileStream);
64
+
65
+ // Extract tar.gz
66
+ await mkdir(tmpExtractDir, { recursive: true });
67
+ execSync(`tar xzf "${tmpFile}" -C "${tmpExtractDir}"`, { stdio: "pipe" });
68
+
69
+ // Find the binary in extracted files (search recursively)
70
+ const found = await findBinaryInDir(tmpExtractDir, binaryName);
71
+ if (found) {
72
+ await rename(found, binPath);
73
+ await chmod(binPath, 0o755);
74
+ } else {
75
+ throw new Error(`Binary '${binaryName}' not found in downloaded archive`);
76
+ }
77
+ } finally {
78
+ // Cleanup temp files
79
+ try { await unlink(tmpFile); } catch { /* ignore */ }
80
+ try { execSync(`rm -rf "${tmpExtractDir}"`, { stdio: "pipe" }); } catch { /* ignore */ }
81
+ }
82
+
83
+ return binPath;
84
+ }
85
+
86
+ async function findBinaryInDir(dir: string, name: string): Promise<string | null> {
87
+ const entries = await readdir(dir, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ const full = join(dir, entry.name);
90
+ if (entry.isFile() && entry.name === name) {
91
+ return full;
92
+ }
93
+ if (entry.isDirectory()) {
94
+ const found = await findBinaryInDir(full, name);
95
+ if (found) return found;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ export function getConnectorBinDir(workspaceName: string): string {
102
+ return getWorkspacePaths(workspaceName).binDir;
103
+ }
@@ -0,0 +1,158 @@
1
+ export interface ConnectorDef {
2
+ name: string;
3
+ displayName: string;
4
+ description: string;
5
+ content: string;
6
+ provider: string;
7
+ binaryName: string;
8
+ envVar: string;
9
+ authType: "oauth2";
10
+ oauthAuthUrl: string;
11
+ oauthTokenUrl: string;
12
+ scopes: string;
13
+ proxyProvider?: string;
14
+ getBinaryUrl(platform: string, arch: string): string;
15
+ }
16
+
17
+ export const OAUTH_PROXY_URL = process.env.PRAGMA_OAUTH_PROXY_URL || 'https://pragma-production-f107.up.railway.app';
18
+
19
+ export const CONNECTOR_REGISTRY: ConnectorDef[] = [
20
+ {
21
+ name: "google-workspace",
22
+ displayName: "Google Workspace",
23
+ description: "Google Calendar, Gmail, Drive, Sheets, Docs via gws CLI",
24
+ content: `# Google Workspace
25
+
26
+ You have access to Google Workspace services via the \`gws\` CLI.
27
+ Authentication is pre-configured — just run commands directly.
28
+
29
+ ## Calendar
30
+ \`\`\`
31
+ gws calendar events list --calendar-id primary --max-results 10
32
+ gws calendar events insert --calendar-id primary --summary "Meeting" --start-date-time "2024-03-20T10:00:00Z" --end-date-time "2024-03-20T11:00:00Z"
33
+ \`\`\`
34
+
35
+ ## Gmail
36
+ \`\`\`
37
+ gws gmail messages list --user-id me --max-results 10 --query "is:unread"
38
+ gws gmail messages get --user-id me --id <messageId>
39
+ gws gmail messages send --user-id me --to "user@example.com" --subject "Subject" --body "Body"
40
+ \`\`\`
41
+
42
+ ## Drive
43
+ \`\`\`
44
+ gws drive files list --page-size 10
45
+ gws drive files get --file-id <fileId>
46
+ \`\`\`
47
+
48
+ ## Sheets
49
+ \`\`\`
50
+ gws sheets spreadsheets-values get --spreadsheet-id <id> --range "Sheet1!A1:D10"
51
+ \`\`\`
52
+
53
+ Run \`gws <service> --help\` to discover all available commands.
54
+ `,
55
+ provider: "google",
56
+ binaryName: "gws",
57
+ envVar: "GOOGLE_WORKSPACE_CLI_TOKEN",
58
+ authType: "oauth2",
59
+ oauthAuthUrl: "https://accounts.google.com/o/oauth2/v2/auth",
60
+ oauthTokenUrl: "https://oauth2.googleapis.com/token",
61
+ scopes:
62
+ "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/spreadsheets",
63
+ proxyProvider: "google",
64
+ getBinaryUrl: (platform: string, arch: string) => {
65
+ const targets: Record<string, string> = {
66
+ "darwin-arm64": "aarch64-apple-darwin",
67
+ "darwin-x64": "x86_64-apple-darwin",
68
+ "linux-arm64": "aarch64-unknown-linux-gnu",
69
+ "linux-x64": "x86_64-unknown-linux-gnu",
70
+ };
71
+ const target =
72
+ targets[`${platform}-${arch}`] ?? "x86_64-unknown-linux-gnu";
73
+ return `https://github.com/googleworkspace/cli/releases/download/v0.17.0/gws-${target}.tar.gz`;
74
+ },
75
+ },
76
+ {
77
+ name: "slack",
78
+ displayName: "Slack",
79
+ description: "Send messages, read channels, manage Slack workspace",
80
+ content: `# Slack
81
+
82
+ You have access to Slack via the \`agent-slack\` CLI.
83
+ Authentication is pre-configured.
84
+
85
+ ## Send a message
86
+ \`\`\`
87
+ agent-slack send --channel "#general" --text "Hello from Pragma"
88
+ \`\`\`
89
+
90
+ ## Read messages
91
+ \`\`\`
92
+ agent-slack read --channel "#general" --limit 20
93
+ \`\`\`
94
+
95
+ ## List channels
96
+ \`\`\`
97
+ agent-slack channels
98
+ \`\`\`
99
+
100
+ Run \`agent-slack --help\` to see all commands.
101
+ `,
102
+ provider: "slack",
103
+ binaryName: "agent-slack",
104
+ envVar: "SLACK_TOKEN",
105
+ authType: "oauth2",
106
+ oauthAuthUrl: "https://slack.com/oauth/v2/authorize",
107
+ oauthTokenUrl: "https://slack.com/api/oauth.v2.access",
108
+ scopes: "chat:write channels:read channels:history groups:read im:read",
109
+ proxyProvider: "slack",
110
+ getBinaryUrl: () => "npm:agent-slack",
111
+ },
112
+ {
113
+ name: "notion",
114
+ displayName: "Notion",
115
+ description: "Search, read, and create Notion pages and databases",
116
+ content: `# Notion
117
+
118
+ You have access to Notion via the \`notion\` CLI.
119
+ Authentication is pre-configured.
120
+
121
+ ## Search
122
+ \`\`\`
123
+ notion search --query "project plan"
124
+ \`\`\`
125
+
126
+ ## Pages
127
+ \`\`\`
128
+ notion page get <page-id>
129
+ notion page create --parent <parent-id> --title "New Page" --content "Page content"
130
+ \`\`\`
131
+
132
+ ## Databases
133
+ \`\`\`
134
+ notion db list
135
+ notion db query <database-id> --filter '{"property":"Status","select":{"equals":"In Progress"}}'
136
+ \`\`\`
137
+
138
+ Run \`notion --help\` to see all commands.
139
+ `,
140
+ provider: "notion",
141
+ binaryName: "notion",
142
+ envVar: "NOTION_TOKEN",
143
+ authType: "oauth2",
144
+ oauthAuthUrl: "https://api.notion.com/v1/oauth/authorize",
145
+ oauthTokenUrl: "https://api.notion.com/v1/oauth/token",
146
+ scopes: "",
147
+ getBinaryUrl: (platform: string, arch: string) => {
148
+ const targets: Record<string, string> = {
149
+ "darwin-arm64": "darwin_arm64",
150
+ "darwin-x64": "darwin_amd64",
151
+ "linux-arm64": "linux_arm64",
152
+ "linux-x64": "linux_amd64",
153
+ };
154
+ const target = targets[`${platform}-${arch}`] ?? "linux_amd64";
155
+ return `https://github.com/4ier/notion-cli/releases/download/v0.3.0/notion-cli_0.3.0_${target}.tar.gz`;
156
+ },
157
+ },
158
+ ];
@@ -0,0 +1,53 @@
1
+ import type { ConversationAdapter } from "./types";
2
+
3
+ export interface AdapterDefinition {
4
+ /** Unique harness identifier, e.g. "codex", "claude_code" */
5
+ id: string;
6
+
7
+ /** CLI binary name to probe for availability, e.g. "codex", "claude" */
8
+ command: string;
9
+
10
+ /** Model label → model ID mapping, e.g. { "Opus 4.6": "opus" } */
11
+ models: Record<string, string>;
12
+
13
+ /** Optional: model ID override for cheap title generation */
14
+ titleModelId?: string;
15
+
16
+ /**
17
+ * Directories (relative to $HOME) where this harness stores global skills.
18
+ * Each entry is a { dir, label } pair, e.g. { dir: ".claude/skills", label: "Claude Code" }.
19
+ */
20
+ globalSkillsDirs?: { dir: string; label: string }[];
21
+
22
+ /**
23
+ * JSON config files (relative to $HOME) that may contain MCP server definitions.
24
+ * Each entry specifies the file path and the JSON key that holds the server map.
25
+ * E.g. { path: ".claude.json", key: "mcpServers" } for Claude Code.
26
+ */
27
+ mcpConfigFiles?: { path: string; key: string }[];
28
+
29
+ /** Build the ConversationAdapter that handles sendTurn for this harness */
30
+ createAdapter(): ConversationAdapter;
31
+ }
32
+
33
+ const registry = new Map<string, AdapterDefinition>();
34
+
35
+ export function registerAdapter(def: AdapterDefinition): void {
36
+ registry.set(def.id, def);
37
+ }
38
+
39
+ export function getAdapterDefinition(id: string): AdapterDefinition {
40
+ const def = registry.get(id);
41
+ if (!def) {
42
+ throw new Error(`Unknown adapter: ${id}`);
43
+ }
44
+ return def;
45
+ }
46
+
47
+ export function allAdapterDefinitions(): AdapterDefinition[] {
48
+ return [...registry.values()];
49
+ }
50
+
51
+ export function getRegisteredHarnessIds(): string[] {
52
+ return [...registry.keys()].sort();
53
+ }
@@ -0,0 +1,138 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { AdapterSendTurnInput, AdapterSendTurnResult, ConversationAdapter } from "../types";
5
+ import { registerAdapter } from "../adapterRegistry";
6
+ import {
7
+ runAdapterCommand,
8
+ withReasoningEffort,
9
+ readString,
10
+ readObject,
11
+ readArray,
12
+ readBoolean,
13
+ appendText,
14
+ } from "../adapters";
15
+
16
+ /**
17
+ * Tools that mutate files. Blocked in chat mode so the agent can only read.
18
+ */
19
+ const CHAT_DISALLOWED_TOOLS = ["Edit", "Write", "NotebookEdit"];
20
+
21
+ const claudeAdapter: ConversationAdapter = {
22
+ async sendTurn(input: AdapterSendTurnInput): Promise<AdapterSendTurnResult> {
23
+ const args = buildClaudeArgs(input);
24
+ return runAdapterCommand({
25
+ command: "claude",
26
+ args,
27
+ cwd: input.cwd,
28
+ env: input.env,
29
+ abortSignal: input.abortSignal,
30
+ onJsonLine: async (line, state) => {
31
+ const eventType = readString(line, "type");
32
+
33
+ if (eventType === "system") {
34
+ const subtype = readString(line, "subtype");
35
+ if (subtype === "init") {
36
+ const sessionId = readString(line, "session_id");
37
+ if (sessionId) {
38
+ state.sessionId = sessionId;
39
+ }
40
+ }
41
+ return;
42
+ }
43
+
44
+ if (eventType === "assistant") {
45
+ const message = readObject(line, "message");
46
+ const content = readArray(message, "content");
47
+
48
+ for (const block of content) {
49
+ if (typeof block !== "object" || block === null) {
50
+ continue;
51
+ }
52
+
53
+ const blockType = readString(block, "type");
54
+ if (blockType === "text") {
55
+ const text = readString(block, "text") ?? "";
56
+ if (text) {
57
+ state.finalText = appendText(state.finalText, text);
58
+ await input.onEvent({ type: "assistant_text", delta: text });
59
+ }
60
+ continue;
61
+ }
62
+
63
+ await input.onEvent({
64
+ type: "tool_event",
65
+ name: `assistant.${blockType || "block"}`,
66
+ payload: block as Record<string, unknown>,
67
+ });
68
+ }
69
+ return;
70
+ }
71
+
72
+ if (eventType === "result") {
73
+ const isError = Boolean(readBoolean(line, "is_error"));
74
+ const resultText = readString(line, "result") ?? "";
75
+ if (resultText && !state.finalText) {
76
+ state.finalText = resultText;
77
+ }
78
+ if (isError) {
79
+ state.commandError = resultText || "Claude CLI returned an error.";
80
+ }
81
+ return;
82
+ }
83
+ // Ignore non-tool lifecycle noise (user/result/system echoes).
84
+ },
85
+ });
86
+ },
87
+ };
88
+
89
+ function buildClaudeArgs(input: AdapterSendTurnInput): string[] {
90
+ const prompt = withReasoningEffort(input.prompt, input.reasoningEffort);
91
+ const resolvedCwd = resolve(input.cwd);
92
+ const args = [
93
+ "-p",
94
+ "--output-format",
95
+ "stream-json",
96
+ "--verbose",
97
+ "--allow-dangerously-skip-permissions",
98
+ "--dangerously-skip-permissions",
99
+ "--permission-mode",
100
+ "bypassPermissions",
101
+ "--add-dir",
102
+ resolvedCwd,
103
+ "--model",
104
+ input.modelId,
105
+ ];
106
+
107
+ if (input.mode === "chat") {
108
+ args.push("--disallowedTools", CHAT_DISALLOWED_TOOLS.join(","));
109
+ }
110
+
111
+ if (input.sessionId) {
112
+ const slug = resolvedCwd.replace(/\//g, "-");
113
+ const sessionFile = join(homedir(), ".claude", "projects", slug, `${input.sessionId}.jsonl`);
114
+ if (existsSync(sessionFile)) {
115
+ args.push("--resume", input.sessionId);
116
+ }
117
+ }
118
+
119
+ args.push("--", prompt);
120
+ return args;
121
+ }
122
+
123
+ registerAdapter({
124
+ id: "claude_code",
125
+ command: "claude",
126
+ models: {
127
+ "Opus 4.6": "opus",
128
+ "Sonnet 4.6": "sonnet",
129
+ "Haiku 4.5": "haiku",
130
+ },
131
+ titleModelId: "haiku",
132
+ globalSkillsDirs: [
133
+ { dir: ".claude/skills", label: "Claude Code" },
134
+ { dir: ".agents/skills", label: "Agents" },
135
+ ],
136
+ mcpConfigFiles: [{ path: ".claude.json", key: "mcpServers" }],
137
+ createAdapter: () => claudeAdapter,
138
+ });
@@ -0,0 +1,142 @@
1
+ import { resolve } from "node:path";
2
+ import type { AdapterSendTurnInput, AdapterSendTurnResult, ConversationAdapter } from "../types";
3
+ import { registerAdapter } from "../adapterRegistry";
4
+ import {
5
+ runAdapterCommand,
6
+ withReasoningEffort,
7
+ readString,
8
+ readObject,
9
+ appendText,
10
+ } from "../adapters";
11
+
12
+ const codexAdapter: ConversationAdapter = {
13
+ async sendTurn(input: AdapterSendTurnInput): Promise<AdapterSendTurnResult> {
14
+ const args = buildCodexArgs(input);
15
+ return runAdapterCommand({
16
+ command: "codex",
17
+ args,
18
+ cwd: input.cwd,
19
+ env: input.env,
20
+ abortSignal: input.abortSignal,
21
+ onJsonLine: async (line, state) => {
22
+ const eventType = readString(line, "type");
23
+ if (eventType === "thread.started") {
24
+ const threadId = readString(line, "thread_id");
25
+ if (threadId) {
26
+ state.sessionId = threadId;
27
+ }
28
+ return;
29
+ }
30
+
31
+ if (eventType === "item.completed") {
32
+ const item = readObject(line, "item");
33
+ const itemType = readString(item, "type");
34
+
35
+ if (itemType === "agent_message") {
36
+ const text = readString(item, "text") ?? "";
37
+ if (text) {
38
+ state.finalText = appendText(state.finalText, text);
39
+ await input.onEvent({ type: "assistant_text", delta: text });
40
+ }
41
+ return;
42
+ }
43
+
44
+ if (shouldEmitCodexToolEvent(itemType, item)) {
45
+ await input.onEvent({
46
+ type: "tool_event",
47
+ name: `item.${itemType || "unknown"}`,
48
+ payload: item,
49
+ });
50
+ }
51
+ return;
52
+ }
53
+
54
+ if (eventType === "error" || eventType === "turn.failed") {
55
+ state.commandError = readString(line, "message") || JSON.stringify(line);
56
+ return;
57
+ }
58
+
59
+ // Ignore non-tool lifecycle noise like turn.started/turn.completed.
60
+ },
61
+ });
62
+ },
63
+ };
64
+
65
+ function buildCodexArgs(input: AdapterSendTurnInput): string[] {
66
+ const prompt = withReasoningEffort(input.prompt, input.reasoningEffort);
67
+ const resolvedCwd = resolve(input.cwd);
68
+ const topLevelArgs =
69
+ input.mode === "chat"
70
+ ? ["-s", "read-only", "-a", "never", "-C", resolvedCwd]
71
+ : ["-C", resolvedCwd];
72
+
73
+ const execSandboxArgs =
74
+ input.mode === "chat" ? [] : ["-s", "danger-full-access"];
75
+
76
+ if (input.sessionId) {
77
+ return [
78
+ ...topLevelArgs,
79
+ "exec",
80
+ ...execSandboxArgs,
81
+ "resume",
82
+ "--json",
83
+ "--skip-git-repo-check",
84
+ "--model",
85
+ input.modelId,
86
+ input.sessionId,
87
+ prompt,
88
+ ];
89
+ }
90
+
91
+ return [
92
+ ...topLevelArgs,
93
+ "exec",
94
+ ...execSandboxArgs,
95
+ "--json",
96
+ "--skip-git-repo-check",
97
+ "--model",
98
+ input.modelId,
99
+ prompt,
100
+ ];
101
+ }
102
+
103
+ function shouldEmitCodexToolEvent(
104
+ itemType: string | null,
105
+ item: Record<string, unknown>,
106
+ ): boolean {
107
+ if (!itemType) {
108
+ return false;
109
+ }
110
+
111
+ // Hide reasoning and other verbose metadata events.
112
+ if (itemType === "reasoning" || itemType === "plan") {
113
+ return false;
114
+ }
115
+
116
+ if (itemType.includes("tool")) {
117
+ return true;
118
+ }
119
+
120
+ // Some codex event variants may include command/file ops without "tool" in type.
121
+ if (typeof item.command === "string") {
122
+ return true;
123
+ }
124
+ if (typeof item.file_path === "string") {
125
+ return true;
126
+ }
127
+
128
+ return false;
129
+ }
130
+
131
+ registerAdapter({
132
+ id: "codex",
133
+ command: "codex",
134
+ models: {
135
+ "GPT-5": "gpt-5",
136
+ "GPT-5.3-Codex": "gpt-5.3-codex",
137
+ },
138
+ globalSkillsDirs: [
139
+ { dir: ".agents/skills", label: "Codex" },
140
+ ],
141
+ createAdapter: () => codexAdapter,
142
+ });