taskdex 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dhruval Golakiya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # Codex Mobile
2
+
3
+ Control [OpenAI Codex](https://openai.com/index/openai-codex/) coding agents from your phone. Run multiple agents simultaneously, get push notifications when tasks complete, and reply directly from your lock screen.
4
+
5
+ ```
6
+ ┌─────────────────┐ WebSocket ┌──────────────────┐
7
+ │ Mobile App │◄──────────────────►│ Bridge Server │
8
+ │ (React Native) │ ws://<ip>:3001 │ (Node.js) │
9
+ └─────────────────┘ │ │
10
+ │ Spawns & manages │
11
+ │ codex app-server │
12
+ │ processes (stdio)│
13
+ └───┬───┬───┬───────┘
14
+ │ │ │
15
+ codex1 codex2 codex3
16
+ ```
17
+
18
+ The **bridge server** runs on your Mac/PC alongside the Codex CLI. The **mobile app** connects to it over your local network (or remotely via Tailscale) and gives you a chat UI to interact with each agent.
19
+
20
+ ## Prerequisites
21
+
22
+ - **Node.js** >= 18
23
+ - **OpenAI Codex CLI** installed and authenticated (`codex` command available in your terminal)
24
+ - **Expo CLI**: `npm install -g expo-cli`
25
+ - **iOS device** or Android device (for push notifications and Live Activity, a physical device is required)
26
+ - Both your computer and phone on the **same network** (or connected via [Tailscale](https://tailscale.com) for remote access)
27
+
28
+ ## Install via npm (Easiest)
29
+
30
+ After publishing this package to npm, users can install and launch with:
31
+
32
+ ```bash
33
+ npm i -g taskdex
34
+ taskdex init
35
+ ```
36
+
37
+ This will clone the repo and immediately run interactive terminal setup.
38
+
39
+ For an already-cloned repo:
40
+
41
+ ```bash
42
+ npx taskdex setup
43
+ ```
44
+
45
+ ## One-Command Terminal Setup (Recommended)
46
+
47
+ Use this flow for first-time setup with terminal prompts for bridge values, automatic dependency install, and an Expo QR code at the end:
48
+
49
+ ```bash
50
+ git clone <your-repo-url> codex-mobile
51
+ cd codex-mobile
52
+ node scripts/setup-and-start.mjs
53
+ ```
54
+
55
+ What this script does:
56
+
57
+ - prompts for `PORT`, `API key`, and Expo mode (`lan` or `tunnel`)
58
+ - installs dependencies in `bridge-server` and `mobile`
59
+ - starts the bridge server with your chosen values
60
+ - starts Expo and prints the QR code to open the app
61
+ - injects bridge URL and API key into Expo so the app opens pre-configured
62
+
63
+ ## Bridge Server
64
+
65
+ The bridge server spawns and manages `codex app-server` child processes, exposing them over a WebSocket API.
66
+
67
+ ### Setup
68
+
69
+ ```bash
70
+ cd bridge-server
71
+ npm install
72
+ ```
73
+
74
+ ### Run
75
+
76
+ ```bash
77
+ npm run dev
78
+ ```
79
+
80
+ This starts the server on port **3001**. You'll see output like:
81
+
82
+ ```
83
+ Codex Bridge Server running
84
+ Local: ws://localhost:3001
85
+ Network: ws://192.168.1.42:3001
86
+ API key: <generated-key>
87
+ [terminal QR code]
88
+ ```
89
+
90
+ Note the **Network** URL and **API key** — or scan the terminal QR from mobile settings (`Scan QR`) to auto-fill both.
91
+
92
+ ### Production
93
+
94
+ ```bash
95
+ npm run build
96
+ npm start
97
+ ```
98
+
99
+ ### Health Check (authenticated)
100
+
101
+ ```
102
+ GET http://localhost:3001/health?key=<api-key>
103
+ # or
104
+ Authorization: Bearer <api-key>
105
+ ```
106
+
107
+ Returns uptime, active agent count, connected client count, push counts, and system info.
108
+
109
+ ### Docker
110
+
111
+ Run the bridge in Docker with a mounted code workspace:
112
+
113
+ ```bash
114
+ docker compose up -d --build
115
+ ```
116
+
117
+ Environment variables used by `docker-compose.yml`:
118
+
119
+ - `PORT` (default `3001`)
120
+ - `API_KEY` (required in production)
121
+ - `CODEX_CWD` (container path to workspace, default `/workspace`)
122
+ - `HOST_CODE_DIR` (host path mounted into `CODEX_CWD`)
123
+ - `REPOS_DIR` (path where remote-managed repos are cloned)
124
+ - `AUTO_PULL_REPOS` (`true`/`false`, optional: pull before `create_agent` when cwd is under `REPOS_DIR`)
125
+ - `OPENAI_API_KEY` (required for Codex CLI)
126
+
127
+ ### VPS Deployment (Hetzner + Caddy)
128
+
129
+ Use the full step-by-step guide in `docs/vps-hetzner.md`:
130
+
131
+ - Hetzner server provisioning
132
+ - Docker installation and bridge deployment
133
+ - SSH deploy key setup for private repo cloning
134
+ - Caddy reverse proxy for `wss://...`
135
+ - systemd/PM2 service persistence
136
+ - Optional auto-pull before agent start
137
+
138
+ ### API
139
+
140
+ The bridge accepts JSON messages over WebSocket:
141
+
142
+ | Action | Params | Description |
143
+ |---|---|---|
144
+ | `create_agent` | `{ name, model, cwd, approvalPolicy?, systemPrompt? }` | Spawn a new Codex agent |
145
+ | `list_agents` | — | List all running agents |
146
+ | `send_message` | `{ agentId, text }` | Send a message to an agent |
147
+ | `interrupt` | `{ agentId }` | Interrupt an agent's current turn |
148
+ | `stop_agent` | `{ agentId }` | Stop and kill an agent process |
149
+ | `update_agent_model` | `{ agentId, model }` | Change an agent's model |
150
+ | `update_agent_config` | `{ agentId, model?, approvalPolicy?, systemPrompt? }` | Update agent runtime config |
151
+ | `get_agent` | `{ agentId }` | Get details for a specific agent |
152
+ | `register_push_token` | `{ token }` | Register an Expo push token for notifications |
153
+ | `update_notification_prefs` | `{ agentId, level }` | Set per-agent notifications (`all`, `errors`, `muted`) |
154
+ | `get_notification_prefs` | — | Get current per-agent notification preferences |
155
+ | `list_notification_history` | `{ limit? }` | List recent bridge notification send history |
156
+ | `list_files` | `{ cwd, path }` | List files/directories in workspace |
157
+ | `list_directories` | `{ cwd, path }` | List only directories for cwd browsing |
158
+ | `read_file` | `{ cwd, path }` | Read file contents |
159
+ | `git_status` | `{ cwd }` | Get git branch/dirty state |
160
+ | `git_log` | `{ cwd, limit? }` | Get commit history |
161
+ | `git_diff` | `{ cwd, file? }` | Get current diff |
162
+ | `git_commit` | `{ cwd, message }` | Commit all current changes |
163
+ | `git_branches` | `{ cwd }` | List local branches |
164
+ | `git_checkout` | `{ cwd, branch }` | Switch to local branch |
165
+ | `clone_repo` | `{ url }` | Clone repository to bridge `REPOS_DIR` |
166
+ | `list_repos` | — | List cloned repositories with paths/remotes |
167
+ | `pull_repo` | `{ path }` | Pull latest changes for a cloned repo |
168
+
169
+ The bridge streams events back to the mobile app (e.g. `turn/started`, `item/agentMessage/delta`, `turn/completed`).
170
+
171
+ ## Mobile App
172
+
173
+ React Native app built with Expo.
174
+
175
+ ### Setup
176
+
177
+ ```bash
178
+ cd mobile
179
+ npm install
180
+ ```
181
+
182
+ ### Run (Expo Go)
183
+
184
+ ```bash
185
+ npx expo start
186
+ ```
187
+
188
+ Scan the QR code with your phone. On first launch, enter the bridge server's **Network URL** (e.g. `ws://192.168.1.42:3001`).
189
+
190
+ ### Development Build (recommended)
191
+
192
+ A dev build is required for push notifications and Live Activity:
193
+
194
+ ```bash
195
+ npx expo prebuild --clean
196
+ npx expo run:ios
197
+ # or
198
+ npx expo run:android
199
+ ```
200
+
201
+ ### Features
202
+
203
+ - **Multiple agents** — Create and manage several Codex agents at once
204
+ - **Streaming responses** — See agent output in real time as it types
205
+ - **Message queue** — Send messages while agent is busy; they'll be delivered when it's ready
206
+ - **Agent persistence** — Agents survive app restarts; stopped agents auto-reconnect when you send a new message
207
+ - **Push notifications** — Get notified when agents finish tasks, even with the app closed
208
+ - **Interactive replies** — Reply to agents directly from notification (hold/long-press the notification)
209
+ - **Action buttons** — Stop Agent and Open Thread buttons on notifications
210
+ - **Live Activity (iOS)** — Real-time agent status in Dynamic Island and lock screen
211
+ - **QR connect** — Scan bridge terminal QR to auto-fill URL + API key
212
+ - **Dark mode** — Light and dark theme support
213
+
214
+ ## Remote Access with Tailscale
215
+
216
+ To use the app outside your local network:
217
+
218
+ 1. Install [Tailscale](https://tailscale.com) on both your computer and phone
219
+ 2. Sign in with the same account on both devices
220
+ 3. Use your computer's **Tailscale IP** (e.g. `ws://100.x.x.x:3001`) as the bridge URL in the mobile app
221
+
222
+ ## Project Structure
223
+
224
+ ```
225
+ codex-mobile/
226
+ ├── bridge-server/
227
+ │ ├── src/
228
+ │ │ ├── index.ts # Express + WebSocket server
229
+ │ │ ├── agent-manager.ts # Spawns/manages codex app-server processes
230
+ │ │ ├── protocol.ts # JSON-RPC message helpers
231
+ │ │ └── push.ts # Expo Push Notification sender
232
+ │ ├── package.json
233
+ │ └── tsconfig.json
234
+ ├── mobile/
235
+ │ ├── App.tsx # Main app entry (navigation, notifications, UI)
236
+ │ ├── components/
237
+ │ │ ├── AgentCard.tsx # Agent card for list view
238
+ │ │ ├── ChatBubble.tsx # Message bubble with markdown
239
+ │ │ ├── MessageInput.tsx # Text input + send button
240
+ │ │ ├── QueuePanel.tsx # Queued messages panel
241
+ │ │ └── TypingIndicator.tsx
242
+ │ ├── hooks/
243
+ │ │ ├── useWebSocket.ts # WebSocket connection & stream handling
244
+ │ │ └── useLiveActivity.ts # iOS Live Activity integration
245
+ │ ├── stores/
246
+ │ │ ├── agentStore.ts # Agent state (Zustand + AsyncStorage)
247
+ │ │ ├── workspaceStore.ts # Workspace management
248
+ │ │ └── themeStore.ts # Theme preferences
249
+ │ ├── types/index.ts # TypeScript types
250
+ │ ├── theme.ts # Colors, typography, palettes
251
+ │ ├── app.json
252
+ │ └── package.json
253
+ └── README.md
254
+ ```
255
+
256
+ ## License
257
+
258
+ MIT
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "taskdex",
3
+ "version": "0.1.0",
4
+ "description": "Terminal-first installer and launcher for Taskdex mobile + bridge",
5
+ "type": "module",
6
+ "bin": {
7
+ "taskdex": "./scripts/taskdex.mjs"
8
+ },
9
+ "files": [
10
+ "scripts/taskdex.mjs"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "MIT"
16
+ }
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from 'node:crypto';
4
+ import { spawn } from 'node:child_process';
5
+ import { existsSync, readdirSync } from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import process from 'node:process';
9
+ import { createInterface } from 'node:readline/promises';
10
+
11
+ const DEFAULT_REPO_URL = process.env.TASKDEX_REPO_URL || 'https://github.com/DhruvalGolakiya/pylon.git';
12
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
13
+ const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
14
+
15
+ function printHelp() {
16
+ console.log(`
17
+ Taskdex CLI
18
+
19
+ Usage:
20
+ taskdex init [directory] [--repo <url>] [--no-start]
21
+ taskdex setup [directory]
22
+ taskdex --help
23
+
24
+ Examples:
25
+ taskdex init
26
+ taskdex init my-taskdex
27
+ taskdex init my-taskdex --repo https://github.com/your-org/your-repo.git
28
+ taskdex setup
29
+ `);
30
+ }
31
+
32
+ function runCommand(command, args, options = {}) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, { stdio: 'inherit', ...options });
35
+ child.on('error', reject);
36
+ child.on('close', (code) => {
37
+ if (code === 0) {
38
+ resolve();
39
+ return;
40
+ }
41
+ reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`));
42
+ });
43
+ });
44
+ }
45
+
46
+ function isTaskdexRepo(dir) {
47
+ return (
48
+ existsSync(path.join(dir, 'bridge-server')) &&
49
+ existsSync(path.join(dir, 'mobile'))
50
+ );
51
+ }
52
+
53
+ function findTaskdexRoot(baseDir) {
54
+ const resolved = path.resolve(baseDir);
55
+ if (isTaskdexRepo(resolved)) return resolved;
56
+
57
+ let children = [];
58
+ try {
59
+ children = readdirSync(resolved, { withFileTypes: true });
60
+ } catch {
61
+ return null;
62
+ }
63
+
64
+ for (const child of children) {
65
+ if (!child.isDirectory()) continue;
66
+ const candidate = path.join(resolved, child.name);
67
+ if (isTaskdexRepo(candidate)) return candidate;
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ function parseInitArgs(argv) {
74
+ let directory = 'codex-mobile';
75
+ let repo = DEFAULT_REPO_URL;
76
+ let noStart = false;
77
+ const positional = [];
78
+
79
+ for (let i = 0; i < argv.length; i += 1) {
80
+ const arg = argv[i];
81
+ if (arg === '--repo') {
82
+ const next = argv[i + 1];
83
+ if (!next) throw new Error('--repo requires a value');
84
+ repo = next;
85
+ i += 1;
86
+ continue;
87
+ }
88
+ if (arg === '--no-start') {
89
+ noStart = true;
90
+ continue;
91
+ }
92
+ if (arg.startsWith('-')) {
93
+ throw new Error(`Unknown option: ${arg}`);
94
+ }
95
+ positional.push(arg);
96
+ }
97
+
98
+ if (positional[0]) directory = positional[0];
99
+ return { directory, repo, noStart };
100
+ }
101
+
102
+ function resolveTaskdexRoot(baseDir) {
103
+ const rootDir = findTaskdexRoot(baseDir);
104
+ if (!rootDir) {
105
+ const attempted = path.resolve(baseDir);
106
+ throw new Error(`Could not find Taskdex repo at ${attempted}. Expected bridge-server and mobile at root or one level below.`);
107
+ }
108
+ return rootDir;
109
+ }
110
+
111
+ function getLocalIPv4() {
112
+ const interfaces = os.networkInterfaces();
113
+ for (const name of Object.keys(interfaces)) {
114
+ for (const iface of interfaces[name] || []) {
115
+ if (iface.family === 'IPv4' && !iface.internal) return iface.address;
116
+ }
117
+ }
118
+ return '127.0.0.1';
119
+ }
120
+
121
+ function parsePort(input) {
122
+ const value = Number(input);
123
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
124
+ throw new Error('Port must be an integer between 1 and 65535.');
125
+ }
126
+ return value;
127
+ }
128
+
129
+ function parseExpoMode(input) {
130
+ const normalized = input.trim().toLowerCase() || 'lan';
131
+ if (!['lan', 'tunnel'].includes(normalized)) {
132
+ throw new Error('Expo mode must be either "lan" or "tunnel".');
133
+ }
134
+ return normalized;
135
+ }
136
+
137
+ function parseInstallChoice(input) {
138
+ const normalized = input.trim().toLowerCase();
139
+ if (!normalized || normalized === 'y' || normalized === 'yes') return true;
140
+ if (normalized === 'n' || normalized === 'no') return false;
141
+ throw new Error('Install choice must be Y or N.');
142
+ }
143
+
144
+ function wait(ms) {
145
+ return new Promise((resolve) => setTimeout(resolve, ms));
146
+ }
147
+
148
+ async function readSetupConfig() {
149
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
150
+ try {
151
+ console.log('\nTaskdex terminal setup\n');
152
+ const portInput = await rl.question('Bridge port [3001]: ');
153
+ const apiKeyInput = await rl.question('Bridge API key [auto-generate]: ');
154
+ const expoModeInput = await rl.question('Expo mode (lan/tunnel) [lan]: ');
155
+ const installInput = await rl.question('Install npm dependencies first? [Y/n]: ');
156
+
157
+ return {
158
+ port: parsePort((portInput || '3001').trim()),
159
+ apiKey: apiKeyInput.trim() || crypto.randomBytes(24).toString('hex'),
160
+ expoMode: parseExpoMode(expoModeInput),
161
+ installDeps: parseInstallChoice(installInput),
162
+ };
163
+ } finally {
164
+ rl.close();
165
+ }
166
+ }
167
+
168
+ async function runInteractiveSetup(rootDir) {
169
+ const bridgeDir = path.join(rootDir, 'bridge-server');
170
+ const mobileDir = path.join(rootDir, 'mobile');
171
+ if (!existsSync(bridgeDir) || !existsSync(mobileDir)) {
172
+ throw new Error(`Invalid Taskdex repository at ${rootDir}`);
173
+ }
174
+
175
+ const { port, apiKey, expoMode, installDeps } = await readSetupConfig();
176
+ const bridgeUrl = `ws://${getLocalIPv4()}:${port}`;
177
+
178
+ console.log(`\nBridge URL: ${bridgeUrl}`);
179
+ console.log(`Bridge API key: ${apiKey}`);
180
+ console.log(`Expo mode: ${expoMode}\n`);
181
+
182
+ if (installDeps) {
183
+ console.log('Installing bridge dependencies...\n');
184
+ await runCommand(npmCmd, ['install'], { cwd: bridgeDir });
185
+ console.log('\nInstalling mobile dependencies...\n');
186
+ await runCommand(npmCmd, ['install'], { cwd: mobileDir });
187
+ }
188
+
189
+ console.log('\nStarting bridge server...\n');
190
+ const bridgeProcess = spawn(npmCmd, ['run', 'dev'], {
191
+ cwd: bridgeDir,
192
+ stdio: 'inherit',
193
+ env: {
194
+ ...process.env,
195
+ PORT: String(port),
196
+ API_KEY: apiKey,
197
+ },
198
+ });
199
+
200
+ bridgeProcess.on('error', (error) => {
201
+ console.error(`Bridge failed to start: ${error.message}`);
202
+ process.exit(1);
203
+ });
204
+
205
+ await wait(1200);
206
+
207
+ console.log('\nStarting Expo. Scan the QR code to open the app.');
208
+ console.log('Bridge URL and API key are prefilled from this terminal setup.\n');
209
+
210
+ try {
211
+ await runCommand(npxCmd, ['expo', 'start', `--${expoMode}`], {
212
+ cwd: mobileDir,
213
+ env: {
214
+ ...process.env,
215
+ EXPO_PUBLIC_BRIDGE_URL: bridgeUrl,
216
+ EXPO_PUBLIC_BRIDGE_API_KEY: apiKey,
217
+ },
218
+ });
219
+ } finally {
220
+ if (!bridgeProcess.killed) {
221
+ bridgeProcess.kill('SIGTERM');
222
+ }
223
+ }
224
+ }
225
+
226
+ async function handleInit(argv) {
227
+ const { directory, repo, noStart } = parseInitArgs(argv);
228
+ const targetDir = path.resolve(process.cwd(), directory);
229
+
230
+ if (existsSync(targetDir)) {
231
+ throw new Error(`Target directory already exists: ${targetDir}`);
232
+ }
233
+
234
+ console.log(`\nCloning Taskdex into ${targetDir}`);
235
+ await runCommand('git', ['clone', repo, targetDir]);
236
+
237
+ if (noStart) {
238
+ console.log('\nRepository installed.');
239
+ console.log(`Next: taskdex setup ${directory}`);
240
+ return;
241
+ }
242
+
243
+ const rootDir = resolveTaskdexRoot(targetDir);
244
+ console.log('\nStarting interactive setup...\n');
245
+ await runInteractiveSetup(rootDir);
246
+ }
247
+
248
+ async function handleSetup(argv) {
249
+ const baseDir = argv[0] ? path.resolve(process.cwd(), argv[0]) : process.cwd();
250
+ const rootDir = resolveTaskdexRoot(baseDir);
251
+ await runInteractiveSetup(rootDir);
252
+ }
253
+
254
+ async function main() {
255
+ const argv = process.argv.slice(2);
256
+ const command = argv[0];
257
+
258
+ if (!command || command === 'init') {
259
+ await handleInit(command === 'init' ? argv.slice(1) : argv);
260
+ return;
261
+ }
262
+
263
+ if (command === 'setup') {
264
+ await handleSetup(argv.slice(1));
265
+ return;
266
+ }
267
+
268
+ if (command === '--help' || command === '-h' || command === 'help') {
269
+ printHelp();
270
+ return;
271
+ }
272
+
273
+ throw new Error(`Unknown command: ${command}`);
274
+ }
275
+
276
+ main().catch((error) => {
277
+ console.error(`\nTaskdex CLI error: ${error.message}`);
278
+ process.exit(1);
279
+ });