mobileclaude-agent 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/README.md +116 -0
- package/dist/agent_controller.js +260 -0
- package/dist/config.js +49 -0
- package/dist/heartbeat.js +44 -0
- package/dist/index.js +68 -0
- package/dist/platform.js +107 -0
- package/dist/session_indexer.js +119 -0
- package/dist/supabase.js +14 -0
- package/dist/tray.js +104 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# MobileClaude Agent
|
|
2
|
+
|
|
3
|
+
Desktop agent for **MobileIDE** — control Claude Code from your iPhone via Supabase Realtime.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- [Claude Code CLI](https://code.claude.ai) installed and accessible in PATH
|
|
9
|
+
- A [Supabase](https://supabase.com) project with the schema applied
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g mobileclaude-agent
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run locally from this directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
### 1. Apply Supabase Migrations
|
|
27
|
+
|
|
28
|
+
In your Supabase project → SQL Editor, run in order:
|
|
29
|
+
|
|
30
|
+
1. `../supabase/migrations/001_initial_schema.sql`
|
|
31
|
+
2. `../supabase/migrations/002_realtime.sql`
|
|
32
|
+
|
|
33
|
+
Also enable Realtime in Supabase Dashboard → Database → Replication for:
|
|
34
|
+
`prompts`, `results`, `agent_status`, `session_metas`
|
|
35
|
+
|
|
36
|
+
### 2. Configure the Agent
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mobileclaude-agent --setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
You'll be prompted for:
|
|
43
|
+
- **Supabase Project URL** — from Project Settings → API
|
|
44
|
+
- **Supabase Anon Key** — public key, safe to share
|
|
45
|
+
- **Supabase Service Role Key** — secret key, stored with `chmod 600`
|
|
46
|
+
- **Your account email + password** — for initial authentication
|
|
47
|
+
|
|
48
|
+
Config is saved to `~/.mobileclaude/config.json` (chmod 600).
|
|
49
|
+
|
|
50
|
+
### 3. Start the Agent
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
mobileclaude-agent
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The agent will:
|
|
57
|
+
1. Connect to Supabase and restore your session
|
|
58
|
+
2. Start watching `~/.claude/projects/` for Claude Code sessions
|
|
59
|
+
3. Subscribe to new prompts from your iPhone
|
|
60
|
+
4. Send heartbeats every 30s so the iOS app knows you're online
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
iPhone (iOS App)
|
|
66
|
+
→ INSERT prompt into Supabase `prompts` table (status=pending)
|
|
67
|
+
|
|
68
|
+
Desktop Agent
|
|
69
|
+
→ Receives prompt via Supabase Realtime
|
|
70
|
+
→ Checks freemium quota (1 prompt/day free)
|
|
71
|
+
→ Spawns: claude --print --dangerously-skip-permissions --output-format stream-json
|
|
72
|
+
→ Streams output to `results` table every ~500 bytes
|
|
73
|
+
→ iOS app receives live updates via Realtime
|
|
74
|
+
|
|
75
|
+
On completion:
|
|
76
|
+
→ status=done, session_id saved back to prompts table
|
|
77
|
+
→ iOS can send follow-up prompts with --resume
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Session Indexing
|
|
81
|
+
|
|
82
|
+
The agent watches `~/.claude/projects/**/*.jsonl` and syncs all Claude Code sessions (including those started in the terminal) to Supabase. This makes them visible in the iOS app's session list — tap any session to continue it from your iPhone.
|
|
83
|
+
|
|
84
|
+
## Freemium Model
|
|
85
|
+
|
|
86
|
+
- **Free tier**: 1 prompt per day
|
|
87
|
+
- **Premium**: Unlimited (set by iOS app after StoreKit purchase)
|
|
88
|
+
|
|
89
|
+
The freemium gate is enforced server-side via the `check_and_increment_daily_usage` PostgreSQL function, using the service role key. The iOS app cannot forge its own count.
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm run dev # Run with tsx (no build step)
|
|
95
|
+
npm run test:types # TypeScript type check
|
|
96
|
+
npm test # Run Jest unit tests
|
|
97
|
+
npm run build # Compile to dist/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Autostart
|
|
101
|
+
|
|
102
|
+
The agent will prompt you to enable autostart during setup. This uses:
|
|
103
|
+
- **macOS**: LaunchAgent plist (`~/Library/LaunchAgents/`)
|
|
104
|
+
- **Windows**: Registry run key (via node-auto-launch)
|
|
105
|
+
- **Linux**: XDG autostart entry (`~/.config/autostart/`)
|
|
106
|
+
|
|
107
|
+
## System Tray
|
|
108
|
+
|
|
109
|
+
On desktop systems with a GUI, a system tray icon shows agent status. On headless servers, the agent runs without a tray (no error, just a log message).
|
|
110
|
+
|
|
111
|
+
## Security Notes
|
|
112
|
+
|
|
113
|
+
- The service role key bypasses Supabase RLS — keep it secret
|
|
114
|
+
- `~/.mobileclaude/config.json` is `chmod 600` — only your user can read it
|
|
115
|
+
- `--dangerously-skip-permissions` is required for unattended claude CLI execution
|
|
116
|
+
- The freemium gate uses `SECURITY DEFINER` PostgreSQL function to prevent tampering
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent_controller.ts — Core agent logic
|
|
3
|
+
*
|
|
4
|
+
* 1. Subscribes to Supabase Realtime for new pending prompts
|
|
5
|
+
* 2. Polls on startup to catch prompts missed while offline
|
|
6
|
+
* 3. Enforces freemium gate via daily_usage DB function
|
|
7
|
+
* 4. Spawns claude CLI, streams output to results table
|
|
8
|
+
* 5. On completion: sets status=done, upserts session_id back to prompt
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { supabase } from './supabase.js';
|
|
13
|
+
import { findClaudeBinary, getEnhancedPath } from './platform.js';
|
|
14
|
+
const FREE_DAILY_LIMIT = 10;
|
|
15
|
+
const STREAM_FLUSH_BYTES = 500;
|
|
16
|
+
let isProcessing = false;
|
|
17
|
+
const pendingQueue = [];
|
|
18
|
+
let channel = null;
|
|
19
|
+
let claudeBinary = null;
|
|
20
|
+
let currentConfig;
|
|
21
|
+
export function initAgentController(config, platform) {
|
|
22
|
+
currentConfig = config;
|
|
23
|
+
claudeBinary = findClaudeBinary();
|
|
24
|
+
if (!claudeBinary) {
|
|
25
|
+
console.error('❌ Claude CLI not found. Install it with:\n' +
|
|
26
|
+
' npm install -g @anthropic-ai/claude-code\n' +
|
|
27
|
+
'Then run: mobileclaude-agent');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
console.log(`✓ Claude binary: ${claudeBinary}`);
|
|
31
|
+
}
|
|
32
|
+
/** Start listening for pending prompts via Realtime + initial poll */
|
|
33
|
+
export async function startAgentController() {
|
|
34
|
+
// 1. Poll on startup — catch anything queued while offline
|
|
35
|
+
await pollPendingPrompts();
|
|
36
|
+
// 2. Subscribe to Realtime inserts on prompts table
|
|
37
|
+
channel = supabase
|
|
38
|
+
.channel('agent-prompts')
|
|
39
|
+
.on('postgres_changes', {
|
|
40
|
+
event: 'INSERT',
|
|
41
|
+
schema: 'public',
|
|
42
|
+
table: 'prompts',
|
|
43
|
+
filter: `user_id=eq.${currentConfig.relayToken}`,
|
|
44
|
+
}, (payload) => {
|
|
45
|
+
const prompt = payload.new;
|
|
46
|
+
if (prompt.status === 'pending') {
|
|
47
|
+
enqueuePrompt(prompt);
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.subscribe((status) => {
|
|
51
|
+
if (status === 'SUBSCRIBED') {
|
|
52
|
+
console.log('✓ Subscribed to prompts channel');
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function stopAgentController() {
|
|
57
|
+
if (channel) {
|
|
58
|
+
await supabase.removeChannel(channel);
|
|
59
|
+
channel = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function pollPendingPrompts() {
|
|
63
|
+
const { data, error } = await supabase
|
|
64
|
+
.from('prompts')
|
|
65
|
+
.select('*')
|
|
66
|
+
.eq('user_id', currentConfig.relayToken)
|
|
67
|
+
.eq('status', 'pending')
|
|
68
|
+
.order('created_at', { ascending: true });
|
|
69
|
+
if (error) {
|
|
70
|
+
console.error('Failed to poll pending prompts:', error.message);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const prompt of data ?? []) {
|
|
74
|
+
enqueuePrompt(prompt);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function enqueuePrompt(prompt) {
|
|
78
|
+
if (isProcessing) {
|
|
79
|
+
pendingQueue.push(prompt);
|
|
80
|
+
console.log(`Queued prompt ${prompt.prompt_id} (queue length: ${pendingQueue.length})`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
processPrompt(prompt);
|
|
84
|
+
}
|
|
85
|
+
async function processPrompt(prompt) {
|
|
86
|
+
isProcessing = true;
|
|
87
|
+
console.log(`\n→ Processing prompt ${prompt.prompt_id}: "${prompt.prompt_text.slice(0, 60)}..."`);
|
|
88
|
+
try {
|
|
89
|
+
// 1. Freemium gate
|
|
90
|
+
const allowed = await checkFreemiumGate(currentConfig.relayToken);
|
|
91
|
+
if (!allowed) {
|
|
92
|
+
await markPromptError(prompt.prompt_id, 'Daily free limit reached. Upgrade to Premium for unlimited prompts.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// 2. Mark as processing
|
|
96
|
+
await supabase
|
|
97
|
+
.from('prompts')
|
|
98
|
+
.update({ status: 'processing' })
|
|
99
|
+
.eq('prompt_id', prompt.prompt_id);
|
|
100
|
+
// 3. Create result row upfront so iOS can subscribe before streaming starts
|
|
101
|
+
const resultId = randomUUID();
|
|
102
|
+
await supabase.from('results').insert({
|
|
103
|
+
result_id: resultId,
|
|
104
|
+
user_id: currentConfig.relayToken,
|
|
105
|
+
prompt_id: prompt.prompt_id,
|
|
106
|
+
session_id: prompt.session_id || '',
|
|
107
|
+
output_text: '',
|
|
108
|
+
is_final: false,
|
|
109
|
+
});
|
|
110
|
+
// 4. Execute claude CLI
|
|
111
|
+
const { sessionId, outputText, error: execError } = await executeClaudeCli(prompt, resultId);
|
|
112
|
+
if (execError) {
|
|
113
|
+
await markPromptError(prompt.prompt_id, execError);
|
|
114
|
+
await supabase
|
|
115
|
+
.from('results')
|
|
116
|
+
.update({ output_text: `Error: ${execError}`, is_final: true })
|
|
117
|
+
.eq('result_id', resultId);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// 5. Final updates
|
|
121
|
+
const resolvedSessionId = sessionId || prompt.session_id || '';
|
|
122
|
+
await Promise.all([
|
|
123
|
+
supabase
|
|
124
|
+
.from('prompts')
|
|
125
|
+
.update({ status: 'done', session_id: resolvedSessionId })
|
|
126
|
+
.eq('prompt_id', prompt.prompt_id),
|
|
127
|
+
supabase
|
|
128
|
+
.from('results')
|
|
129
|
+
.update({ output_text: outputText, session_id: resolvedSessionId, is_final: true })
|
|
130
|
+
.eq('result_id', resultId),
|
|
131
|
+
]);
|
|
132
|
+
console.log(`✓ Prompt ${prompt.prompt_id} completed (session: ${resolvedSessionId})`);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
136
|
+
console.error(`Error processing prompt ${prompt.prompt_id}:`, msg);
|
|
137
|
+
await markPromptError(prompt.prompt_id, msg);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
isProcessing = false;
|
|
141
|
+
const next = pendingQueue.shift();
|
|
142
|
+
if (next)
|
|
143
|
+
processPrompt(next);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Build claude CLI arguments from a prompt row */
|
|
147
|
+
export function buildClaudeArgs(prompt) {
|
|
148
|
+
const args = [
|
|
149
|
+
'--print',
|
|
150
|
+
'--dangerously-skip-permissions',
|
|
151
|
+
'--output-format', 'stream-json',
|
|
152
|
+
'--verbose',
|
|
153
|
+
];
|
|
154
|
+
if (prompt.model && prompt.model !== 'default') {
|
|
155
|
+
args.push('--model', prompt.model);
|
|
156
|
+
}
|
|
157
|
+
if (prompt.session_id && prompt.session_id !== '') {
|
|
158
|
+
args.push('--resume', prompt.session_id);
|
|
159
|
+
}
|
|
160
|
+
return args;
|
|
161
|
+
}
|
|
162
|
+
async function executeClaudeCli(prompt, resultId) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const args = buildClaudeArgs(prompt);
|
|
165
|
+
const cwd = prompt.project_path || process.cwd();
|
|
166
|
+
const child = spawn(claudeBinary, args, {
|
|
167
|
+
cwd,
|
|
168
|
+
env: {
|
|
169
|
+
...process.env,
|
|
170
|
+
PATH: getEnhancedPath(),
|
|
171
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '',
|
|
172
|
+
},
|
|
173
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
|
+
});
|
|
175
|
+
child.stdin.write(prompt.prompt_text);
|
|
176
|
+
child.stdin.end();
|
|
177
|
+
let stdoutBuffer = '';
|
|
178
|
+
let textAccumulator = '';
|
|
179
|
+
let bytesSinceFlush = 0;
|
|
180
|
+
let detectedSessionId = null;
|
|
181
|
+
child.stdout.on('data', async (chunk) => {
|
|
182
|
+
stdoutBuffer += chunk.toString('utf-8');
|
|
183
|
+
bytesSinceFlush += chunk.length;
|
|
184
|
+
const lines = stdoutBuffer.split('\n');
|
|
185
|
+
stdoutBuffer = lines.pop() ?? '';
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
if (!line.trim())
|
|
188
|
+
continue;
|
|
189
|
+
try {
|
|
190
|
+
const obj = JSON.parse(line);
|
|
191
|
+
extractTextFromStreamEvent(obj, (text) => { textAccumulator += text; });
|
|
192
|
+
if (obj.type === 'result' && typeof obj.session_id === 'string') {
|
|
193
|
+
detectedSessionId = obj.session_id;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch { /* non-JSON line — ignore */ }
|
|
197
|
+
}
|
|
198
|
+
if (bytesSinceFlush >= STREAM_FLUSH_BYTES) {
|
|
199
|
+
bytesSinceFlush = 0;
|
|
200
|
+
await supabase
|
|
201
|
+
.from('results')
|
|
202
|
+
.update({ output_text: textAccumulator })
|
|
203
|
+
.eq('result_id', resultId);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
child.stderr.on('data', (chunk) => {
|
|
207
|
+
console.error('[claude stderr]', chunk.toString('utf-8').trim());
|
|
208
|
+
});
|
|
209
|
+
child.on('close', (code) => {
|
|
210
|
+
resolve({
|
|
211
|
+
sessionId: detectedSessionId,
|
|
212
|
+
outputText: textAccumulator,
|
|
213
|
+
error: code !== 0 ? `Claude exited with code ${code}` : null,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
child.on('error', (err) => {
|
|
217
|
+
resolve({ sessionId: null, outputText: '', error: err.message });
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function extractTextFromStreamEvent(obj, append) {
|
|
222
|
+
if (obj.type === 'assistant') {
|
|
223
|
+
const content = obj.message?.content;
|
|
224
|
+
if (Array.isArray(content)) {
|
|
225
|
+
for (const block of content) {
|
|
226
|
+
const b = block;
|
|
227
|
+
if (b.type === 'text' && typeof b.text === 'string')
|
|
228
|
+
append(b.text);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (obj.type === 'content_block_delta') {
|
|
233
|
+
const delta = obj.delta;
|
|
234
|
+
if (delta?.type === 'text_delta' && typeof delta.text === 'string')
|
|
235
|
+
append(delta.text);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Atomically check and increment daily usage via SECURITY DEFINER function */
|
|
239
|
+
async function checkFreemiumGate(relayToken) {
|
|
240
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
241
|
+
const { data, error } = await supabase.rpc('check_and_increment_daily_usage', {
|
|
242
|
+
p_user_id: relayToken,
|
|
243
|
+
p_date: today,
|
|
244
|
+
p_free_limit: FREE_DAILY_LIMIT,
|
|
245
|
+
});
|
|
246
|
+
if (error) {
|
|
247
|
+
// Function not deployed — allow prompt
|
|
248
|
+
console.warn('Freemium gate unavailable (allowing prompt):', error.message);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return data === true;
|
|
252
|
+
}
|
|
253
|
+
async function markPromptError(promptId, message) {
|
|
254
|
+
await supabase
|
|
255
|
+
.from('prompts')
|
|
256
|
+
.update({ status: 'error' })
|
|
257
|
+
.eq('prompt_id', promptId);
|
|
258
|
+
console.error(`✗ Prompt ${promptId} error: ${message}`);
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=agent_controller.js.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — Agent configuration
|
|
3
|
+
*
|
|
4
|
+
* Supabase credentials are baked in at publish time — users never enter them.
|
|
5
|
+
* Identity = relay token (UUID stored in ~/.mobileclaude/token).
|
|
6
|
+
* No login, no Supabase account required from the user.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
// ─── BAKED-IN BACKEND CREDENTIALS ────────────────────────────────────────────
|
|
12
|
+
// These point to the developer's Supabase project — same for every user.
|
|
13
|
+
// Replace with your real values before `npm publish`.
|
|
14
|
+
export const SUPABASE_URL = process.env.MOBILECLAUDE_SUPABASE_URL
|
|
15
|
+
?? 'https://zxmgprkewaejozfezpwx.supabase.co';
|
|
16
|
+
export const SUPABASE_ANON_KEY = process.env.MOBILECLAUDE_SUPABASE_ANON_KEY
|
|
17
|
+
?? 'sb_publishable_C9DAeRtk0fPPuhau1Iw4OQ_k75bz8kC';
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
export const AGENT_VERSION = '0.1.0';
|
|
20
|
+
// Relay token file — permanent device identity, created once
|
|
21
|
+
const configDir = join(homedir(), '.mobileclaude');
|
|
22
|
+
const tokenPath = join(configDir, 'token');
|
|
23
|
+
export function getRelayToken() {
|
|
24
|
+
if (!existsSync(configDir)) {
|
|
25
|
+
mkdirSync(configDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
if (existsSync(tokenPath)) {
|
|
28
|
+
const saved = readFileSync(tokenPath, 'utf-8').trim();
|
|
29
|
+
if (saved.length > 0)
|
|
30
|
+
return saved;
|
|
31
|
+
}
|
|
32
|
+
// Generate new UUID token
|
|
33
|
+
const token = crypto.randomUUID();
|
|
34
|
+
writeFileSync(tokenPath, token, 'utf-8');
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
export function loadConfig() {
|
|
38
|
+
return {
|
|
39
|
+
supabaseUrl: SUPABASE_URL,
|
|
40
|
+
supabaseAnonKey: SUPABASE_ANON_KEY,
|
|
41
|
+
relayToken: getRelayToken(),
|
|
42
|
+
agentVersion: AGENT_VERSION,
|
|
43
|
+
maxConcurrentPrompts: 1,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Legacy — keep for backwards compat, but no longer used
|
|
47
|
+
export function configExists() { return true; }
|
|
48
|
+
export async function runSetupWizard() { return loadConfig(); }
|
|
49
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* heartbeat.ts — Sends periodic heartbeats to agent_status table.
|
|
3
|
+
* iOS shows "Mac offline" banner if last_heartbeat > 60s ago.
|
|
4
|
+
*/
|
|
5
|
+
import { supabase } from './supabase.js';
|
|
6
|
+
import { getPlatform } from './platform.js';
|
|
7
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
8
|
+
const AGENT_VERSION = '0.1.0';
|
|
9
|
+
let heartbeatTimer = null;
|
|
10
|
+
let relayToken;
|
|
11
|
+
let sessionCount = 0;
|
|
12
|
+
export function incrementSessionCount() {
|
|
13
|
+
sessionCount++;
|
|
14
|
+
}
|
|
15
|
+
export async function startHeartbeat(config) {
|
|
16
|
+
relayToken = config.relayToken;
|
|
17
|
+
await sendHeartbeat(true);
|
|
18
|
+
heartbeatTimer = setInterval(() => { sendHeartbeat(true); }, HEARTBEAT_INTERVAL_MS);
|
|
19
|
+
}
|
|
20
|
+
export function stopHeartbeat() {
|
|
21
|
+
if (heartbeatTimer) {
|
|
22
|
+
clearInterval(heartbeatTimer);
|
|
23
|
+
heartbeatTimer = null;
|
|
24
|
+
}
|
|
25
|
+
// Fire-and-forget offline signal
|
|
26
|
+
sendHeartbeat(false).catch(() => { });
|
|
27
|
+
}
|
|
28
|
+
async function sendHeartbeat(isOnline) {
|
|
29
|
+
const platform = getPlatform();
|
|
30
|
+
const { error } = await supabase
|
|
31
|
+
.from('agent_status')
|
|
32
|
+
.upsert({
|
|
33
|
+
user_id: relayToken,
|
|
34
|
+
is_online: isOnline,
|
|
35
|
+
platform,
|
|
36
|
+
agent_version: AGENT_VERSION,
|
|
37
|
+
last_heartbeat: new Date().toISOString(),
|
|
38
|
+
session_count: sessionCount,
|
|
39
|
+
}, { onConflict: 'user_id' });
|
|
40
|
+
if (error) {
|
|
41
|
+
console.warn('Heartbeat error:', error.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=heartbeat.js.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* index.ts — MobileClaude Agent entry point
|
|
4
|
+
*
|
|
5
|
+
* No setup required — credentials are baked in.
|
|
6
|
+
* Just run: npx mobileclaude-agent
|
|
7
|
+
*/
|
|
8
|
+
import { loadConfig } from './config.js';
|
|
9
|
+
import { initSupabase } from './supabase.js';
|
|
10
|
+
import { initAgentController, startAgentController, stopAgentController } from './agent_controller.js';
|
|
11
|
+
import { startSessionIndexer, stopSessionIndexer } from './session_indexer.js';
|
|
12
|
+
import { startHeartbeat, stopHeartbeat } from './heartbeat.js';
|
|
13
|
+
import { startTray, stopTray } from './tray.js';
|
|
14
|
+
import { getPlatform, findClaudeBinary } from './platform.js';
|
|
15
|
+
async function main() {
|
|
16
|
+
const { default: chalk } = await import('chalk');
|
|
17
|
+
console.log(chalk.bold.cyan('🤖 MobileClaude Agent v0.1.0'));
|
|
18
|
+
console.log(chalk.dim(' Remote Claude Code control from your iPhone\n'));
|
|
19
|
+
// ── Load config (baked-in credentials + relay token) ─────────────────────
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
console.log(chalk.dim(` Relay token: ${config.relayToken.slice(0, 8)}...`));
|
|
22
|
+
// ── Initialize Supabase ───────────────────────────────────────────────────
|
|
23
|
+
initSupabase(config);
|
|
24
|
+
// ── Validate claude binary ────────────────────────────────────────────────
|
|
25
|
+
const claude = findClaudeBinary();
|
|
26
|
+
if (!claude) {
|
|
27
|
+
console.error(chalk.red('❌ Claude CLI not found. Install it with:'));
|
|
28
|
+
console.error(chalk.dim(' npm install -g @anthropic-ai/claude-code'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk.green(`✓ Claude CLI: ${claude}`));
|
|
32
|
+
// ── Start subsystems ──────────────────────────────────────────────────────
|
|
33
|
+
const platform = getPlatform();
|
|
34
|
+
initAgentController(config, platform);
|
|
35
|
+
console.log('\nStarting agent subsystems...');
|
|
36
|
+
await startHeartbeat(config);
|
|
37
|
+
console.log(chalk.green('✓ Heartbeat started (30s interval)'));
|
|
38
|
+
startSessionIndexer(config);
|
|
39
|
+
console.log(chalk.green('✓ Session indexer watching ~/.claude/projects/'));
|
|
40
|
+
await startAgentController();
|
|
41
|
+
console.log(chalk.green('✓ Agent controller listening for prompts'));
|
|
42
|
+
await startTray(() => gracefulShutdown());
|
|
43
|
+
const platformName = { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }[platform] ?? platform;
|
|
44
|
+
console.log(chalk.bold.green(`\n✓ Agent running on ${platformName} — waiting for prompts from iPhone\n`));
|
|
45
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
46
|
+
// ── Graceful shutdown ─────────────────────────────────────────────────────
|
|
47
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
48
|
+
process.on('SIGINT', gracefulShutdown);
|
|
49
|
+
}
|
|
50
|
+
let shuttingDown = false;
|
|
51
|
+
async function gracefulShutdown() {
|
|
52
|
+
if (shuttingDown)
|
|
53
|
+
return;
|
|
54
|
+
shuttingDown = true;
|
|
55
|
+
const { default: chalk } = await import('chalk');
|
|
56
|
+
console.log(chalk.yellow('\nShutting down agent...'));
|
|
57
|
+
stopHeartbeat();
|
|
58
|
+
stopSessionIndexer();
|
|
59
|
+
await stopAgentController();
|
|
60
|
+
stopTray();
|
|
61
|
+
console.log(chalk.green('✓ Agent stopped cleanly'));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
main().catch((err) => {
|
|
65
|
+
console.error('Fatal error:', err);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platform.ts — OS detection and platform-specific paths/utilities
|
|
3
|
+
* Supports macOS (darwin), Windows (win32), and Linux.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
export function getPlatform() {
|
|
10
|
+
const p = process.platform;
|
|
11
|
+
if (p === 'darwin' || p === 'win32' || p === 'linux')
|
|
12
|
+
return p;
|
|
13
|
+
return 'linux'; // fallback
|
|
14
|
+
}
|
|
15
|
+
/** ~/.claude/projects/ — where Claude Code stores session JSONL files */
|
|
16
|
+
export function claudeProjectsDir() {
|
|
17
|
+
return join(homedir(), '.claude', 'projects');
|
|
18
|
+
}
|
|
19
|
+
/** ~/.mobileclaude/ — config directory for this agent */
|
|
20
|
+
export function agentConfigDir() {
|
|
21
|
+
return join(homedir(), '.mobileclaude');
|
|
22
|
+
}
|
|
23
|
+
/** Full path to the agent's config file */
|
|
24
|
+
export function agentConfigPath() {
|
|
25
|
+
return join(agentConfigDir(), 'config.json');
|
|
26
|
+
}
|
|
27
|
+
/** Full path to the agent's log file */
|
|
28
|
+
export function agentLogPath() {
|
|
29
|
+
return join(agentConfigDir(), 'agent.log');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Finds the claude CLI binary on the current system.
|
|
33
|
+
* Checks PATH first, then common install locations for nvm/fnm/mise/brew.
|
|
34
|
+
* Returns null if not found.
|
|
35
|
+
*/
|
|
36
|
+
export function findClaudeBinary() {
|
|
37
|
+
// 1. Try PATH
|
|
38
|
+
try {
|
|
39
|
+
const result = execSync('which claude 2>/dev/null || where claude 2>/dev/null', {
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
}).trim();
|
|
43
|
+
if (result && existsSync(result))
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// not on PATH
|
|
48
|
+
}
|
|
49
|
+
// 2. Try common macOS/Linux install paths
|
|
50
|
+
const commonPaths = [];
|
|
51
|
+
const home = homedir();
|
|
52
|
+
if (process.platform !== 'win32') {
|
|
53
|
+
commonPaths.push('/usr/local/bin/claude', '/usr/bin/claude', '/opt/homebrew/bin/claude', join(home, '.local/bin/claude'), join(home, '.npm-global/bin/claude'),
|
|
54
|
+
// nvm
|
|
55
|
+
join(home, '.nvm/versions/node/*/bin/claude'),
|
|
56
|
+
// fnm
|
|
57
|
+
join(home, '.fnm/node-versions/*/installation/bin/claude'),
|
|
58
|
+
// mise (formerly rtx)
|
|
59
|
+
join(home, '.local/share/mise/shims/claude'),
|
|
60
|
+
// pnpm global
|
|
61
|
+
join(home, '.local/share/pnpm/claude'));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Windows common paths
|
|
65
|
+
const appData = process.env.APPDATA ?? '';
|
|
66
|
+
const localAppData = process.env.LOCALAPPDATA ?? '';
|
|
67
|
+
commonPaths.push(join(appData, 'npm', 'claude.cmd'), join(localAppData, 'pnpm', 'claude.cmd'), 'C:\\Program Files\\nodejs\\claude.cmd');
|
|
68
|
+
}
|
|
69
|
+
for (const p of commonPaths) {
|
|
70
|
+
// Handle glob patterns (simple expansion for *)
|
|
71
|
+
if (p.includes('*')) {
|
|
72
|
+
try {
|
|
73
|
+
const globResult = execSync(`ls ${p} 2>/dev/null | head -1`, {
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
}).trim();
|
|
77
|
+
if (globResult && existsSync(globResult))
|
|
78
|
+
return globResult;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// skip
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (existsSync(p)) {
|
|
85
|
+
return p;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns the PATH string to use when spawning child processes.
|
|
92
|
+
* On macOS/Linux, sources common shell locations so claude is found
|
|
93
|
+
* even when the agent runs as a LaunchAgent/systemd service.
|
|
94
|
+
*/
|
|
95
|
+
export function getEnhancedPath() {
|
|
96
|
+
const home = homedir();
|
|
97
|
+
const extraPaths = [
|
|
98
|
+
'/usr/local/bin',
|
|
99
|
+
'/opt/homebrew/bin',
|
|
100
|
+
join(home, '.local/bin'),
|
|
101
|
+
join(home, '.npm-global/bin'),
|
|
102
|
+
join(home, '.local/share/pnpm'),
|
|
103
|
+
];
|
|
104
|
+
const currentPath = process.env.PATH ?? '';
|
|
105
|
+
return [...extraPaths, currentPath].join(':');
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session_indexer.ts — Watches ~/.claude/projects/ for JSONL files
|
|
3
|
+
* and upserts session metadata to Supabase.
|
|
4
|
+
*
|
|
5
|
+
* Makes ALL Claude Code sessions (including desktop-created ones)
|
|
6
|
+
* visible in the iOS app.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { basename, dirname } from 'path';
|
|
10
|
+
import chokidar from 'chokidar';
|
|
11
|
+
import { supabase } from './supabase.js';
|
|
12
|
+
import { claudeProjectsDir } from './platform.js';
|
|
13
|
+
let watcher = null;
|
|
14
|
+
let relayToken;
|
|
15
|
+
export function startSessionIndexer(config) {
|
|
16
|
+
relayToken = config.relayToken;
|
|
17
|
+
const watchDir = claudeProjectsDir();
|
|
18
|
+
if (!existsSync(watchDir)) {
|
|
19
|
+
console.log('Session indexer: ~/.claude/projects/ not found yet, will watch once created');
|
|
20
|
+
}
|
|
21
|
+
watcher = chokidar.watch(`${watchDir}/**/*.jsonl`, {
|
|
22
|
+
persistent: true,
|
|
23
|
+
ignoreInitial: false,
|
|
24
|
+
depth: 3,
|
|
25
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
|
|
26
|
+
});
|
|
27
|
+
watcher
|
|
28
|
+
.on('add', (filePath) => { parseAndUpsertSession(filePath, false); })
|
|
29
|
+
.on('change', (filePath) => { parseAndUpsertSession(filePath, false); })
|
|
30
|
+
.on('error', (err) => { console.error('Session indexer watcher error:', err); })
|
|
31
|
+
.on('ready', () => { console.log('✓ Session indexer watching ~/.claude/projects/'); });
|
|
32
|
+
}
|
|
33
|
+
export function stopSessionIndexer() {
|
|
34
|
+
watcher?.close();
|
|
35
|
+
watcher = null;
|
|
36
|
+
}
|
|
37
|
+
export async function parseAndUpsertSession(filePath, startedFromMobile) {
|
|
38
|
+
if (!existsSync(filePath))
|
|
39
|
+
return;
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const parsed = parseJSONLSession(raw);
|
|
48
|
+
if (!parsed)
|
|
49
|
+
return;
|
|
50
|
+
const meta = {
|
|
51
|
+
session_id: parsed.sessionId,
|
|
52
|
+
user_id: relayToken,
|
|
53
|
+
project_path: parsed.projectPath,
|
|
54
|
+
project_name: parsed.projectName,
|
|
55
|
+
first_prompt: parsed.firstPrompt,
|
|
56
|
+
last_prompt: parsed.lastPrompt,
|
|
57
|
+
message_count: parsed.messageCount,
|
|
58
|
+
model: parsed.model,
|
|
59
|
+
started_from_mobile: startedFromMobile,
|
|
60
|
+
created_at: parsed.createdAt,
|
|
61
|
+
updated_at: parsed.updatedAt,
|
|
62
|
+
};
|
|
63
|
+
const { error } = await supabase
|
|
64
|
+
.from('session_metas')
|
|
65
|
+
.upsert(meta, { onConflict: 'session_id', ignoreDuplicates: false });
|
|
66
|
+
if (error) {
|
|
67
|
+
console.error(`Session indexer upsert error for ${basename(filePath)}:`, error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function parseJSONLSession(raw) {
|
|
71
|
+
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
72
|
+
const entries = [];
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
try {
|
|
75
|
+
entries.push(JSON.parse(line));
|
|
76
|
+
}
|
|
77
|
+
catch { /* malformed line — skip */ }
|
|
78
|
+
}
|
|
79
|
+
if (entries.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const userEntries = entries.filter((e) => e.type === 'user' &&
|
|
82
|
+
e.message !== null &&
|
|
83
|
+
typeof e.message === 'object' &&
|
|
84
|
+
typeof e.message.content === 'string');
|
|
85
|
+
if (userEntries.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const assistantEntries = entries.filter((e) => e.type === 'assistant');
|
|
88
|
+
const firstEntry = userEntries[0];
|
|
89
|
+
const lastEntry = userEntries[userEntries.length - 1];
|
|
90
|
+
const sessionId = String(firstEntry.sessionId ?? '');
|
|
91
|
+
if (!sessionId)
|
|
92
|
+
return null;
|
|
93
|
+
const projectPath = String(firstEntry.cwd ?? '');
|
|
94
|
+
const projectName = projectPath
|
|
95
|
+
? basename(dirname(projectPath + '/x')) || basename(projectPath)
|
|
96
|
+
: null;
|
|
97
|
+
const firstPromptFull = String(firstEntry.message.content ?? '');
|
|
98
|
+
const lastPromptFull = String(lastEntry.message.content ?? '');
|
|
99
|
+
let model = null;
|
|
100
|
+
if (assistantEntries.length > 0) {
|
|
101
|
+
const msg = assistantEntries[0].message;
|
|
102
|
+
if (msg && typeof msg.model === 'string')
|
|
103
|
+
model = msg.model;
|
|
104
|
+
}
|
|
105
|
+
const createdAt = String(firstEntry.timestamp ?? new Date().toISOString());
|
|
106
|
+
const updatedAt = String(entries[entries.length - 1].timestamp ?? createdAt);
|
|
107
|
+
return {
|
|
108
|
+
sessionId,
|
|
109
|
+
projectPath,
|
|
110
|
+
projectName,
|
|
111
|
+
firstPrompt: firstPromptFull.slice(0, 200) || null,
|
|
112
|
+
lastPrompt: lastPromptFull.slice(0, 200) || null,
|
|
113
|
+
messageCount: userEntries.length + assistantEntries.length,
|
|
114
|
+
model,
|
|
115
|
+
createdAt,
|
|
116
|
+
updatedAt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=session_indexer.js.map
|
package/dist/supabase.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supabase.ts — Supabase client (single anon-key client, no auth)
|
|
3
|
+
*
|
|
4
|
+
* Identity = relay token (UUID in ~/.mobileclaude/token).
|
|
5
|
+
* All tables have permissive RLS for the anon role.
|
|
6
|
+
*/
|
|
7
|
+
import { createClient } from '@supabase/supabase-js';
|
|
8
|
+
export let supabase;
|
|
9
|
+
export function initSupabase(config) {
|
|
10
|
+
supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
|
|
11
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=supabase.js.map
|
package/dist/tray.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tray.ts — Optional system tray icon (macOS/Windows/Linux with GUI).
|
|
3
|
+
* Falls back to headless mode if node-systray2 is unavailable.
|
|
4
|
+
*
|
|
5
|
+
* node-systray2 is an optionalDependency — if native compilation fails
|
|
6
|
+
* (e.g., on headless servers or CI), the agent continues without a tray.
|
|
7
|
+
*/
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
let trayInstance = null;
|
|
10
|
+
export async function startTray(onQuit) {
|
|
11
|
+
try {
|
|
12
|
+
// Dynamic import of optional dep — TypeScript can't verify this statically
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const mod = await import('node-systray2');
|
|
15
|
+
const Systray = mod.default ?? mod;
|
|
16
|
+
const systray = new Systray({
|
|
17
|
+
menu: {
|
|
18
|
+
icon: getIconBase64(),
|
|
19
|
+
title: 'MobileClaude',
|
|
20
|
+
tooltip: 'MobileClaude Agent',
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
title: 'MobileClaude Agent v0.1.0',
|
|
24
|
+
tooltip: 'Agent version',
|
|
25
|
+
enabled: false,
|
|
26
|
+
click: () => { },
|
|
27
|
+
},
|
|
28
|
+
Systray.separator ?? { title: '—', enabled: false, click: () => { } },
|
|
29
|
+
{
|
|
30
|
+
title: '● Status: running',
|
|
31
|
+
tooltip: 'Agent online status',
|
|
32
|
+
enabled: false,
|
|
33
|
+
click: () => { },
|
|
34
|
+
},
|
|
35
|
+
Systray.separator ?? { title: '—', enabled: false, click: () => { } },
|
|
36
|
+
{
|
|
37
|
+
title: 'Stop Agent',
|
|
38
|
+
tooltip: 'Stop the agent',
|
|
39
|
+
enabled: true,
|
|
40
|
+
click: onQuit,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'Open Logs',
|
|
44
|
+
tooltip: 'Open agent log file',
|
|
45
|
+
enabled: true,
|
|
46
|
+
click: () => openLogs(),
|
|
47
|
+
},
|
|
48
|
+
Systray.separator ?? { title: '—', enabled: false, click: () => { } },
|
|
49
|
+
{
|
|
50
|
+
title: 'Quit',
|
|
51
|
+
tooltip: 'Quit MobileClaude Agent',
|
|
52
|
+
enabled: true,
|
|
53
|
+
click: onQuit,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
debug: false,
|
|
58
|
+
copyDir: true,
|
|
59
|
+
});
|
|
60
|
+
trayInstance = systray;
|
|
61
|
+
console.log('✓ System tray started');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.log('System tray not available — running headlessly (no GUI required)');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function stopTray() {
|
|
68
|
+
if (trayInstance && typeof trayInstance.kill === 'function') {
|
|
69
|
+
try {
|
|
70
|
+
trayInstance.kill(true);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore errors during tray shutdown
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
trayInstance = null;
|
|
77
|
+
}
|
|
78
|
+
function openLogs() {
|
|
79
|
+
import('./platform.js').then(({ agentLogPath }) => {
|
|
80
|
+
const path = agentLogPath();
|
|
81
|
+
import('child_process').then(({ execSync }) => {
|
|
82
|
+
try {
|
|
83
|
+
if (process.platform === 'darwin') {
|
|
84
|
+
execSync(`open "${path}"`);
|
|
85
|
+
}
|
|
86
|
+
else if (process.platform === 'win32') {
|
|
87
|
+
execSync(`start "" "${path}"`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
execSync(`xdg-open "${path}" 2>/dev/null || true`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
console.log(`Log file: ${path}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Minimal 16x16 transparent PNG as base64 for the tray icon placeholder */
|
|
100
|
+
function getIconBase64() {
|
|
101
|
+
// In production, replace with actual app icon (16x16 PNG, base64 encoded)
|
|
102
|
+
return 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH6AELEAgSO5GSTQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAZklEQVQ4y2NgGAWkgP9EMP8z/P//nxgNZP//E6X5H6T5PzHO+E+M5v9E2f+JMP8TY/s/Eb7/R4T9nwjf/yPC/k+E7f8RYf8nwvb/iLD/E2H7f0TY/4mw/T8i7P9E2P4fUQMANyYtJpBzSj8AAAAASUVORK5CYII=';
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=tray.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobileclaude-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Desktop agent for MobileIDE — controls Claude Code from your iPhone via Supabase",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mobileclaude-agent": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
16
|
+
"test:types": "tsc --noEmit",
|
|
17
|
+
"test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@supabase/supabase-js": "^2.103.0",
|
|
21
|
+
"chokidar": "^4.0.0",
|
|
22
|
+
"chalk": "^5.3.0"
|
|
23
|
+
},
|
|
24
|
+
"optionalDependencies": {
|
|
25
|
+
"node-systray2": "^2.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@jest/globals": "^29.7.0",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"jest": "^29.7.0",
|
|
31
|
+
"ts-jest": "^29.2.0",
|
|
32
|
+
"tsx": "^4.7.0",
|
|
33
|
+
"typescript": "^5.6.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/**/*.js",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"claude",
|
|
44
|
+
"claude-code",
|
|
45
|
+
"mobile",
|
|
46
|
+
"ios",
|
|
47
|
+
"agent",
|
|
48
|
+
"supabase"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT"
|
|
51
|
+
}
|