claude-simple 0.2.19 → 0.2.21

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/bin/edge-ctl.js CHANGED
@@ -12,8 +12,13 @@ import os from 'os';
12
12
  import path from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
 
15
- const LAUNCHD_LABEL = 'system/com.claude-simple.edge';
16
- const LAUNCHD_PLIST = '/Library/LaunchDaemons/com.claude-simple.edge.plist';
15
+ const DAEMON_LABEL = 'system/com.claude-simple.edge';
16
+ const DAEMON_PLIST = '/Library/LaunchDaemons/com.claude-simple.edge.plist';
17
+ const AGENT_NAME = 'com.claude-simple.edge';
18
+ const USER_ID = typeof process.getuid === 'function' ? process.getuid() : 0;
19
+ const AGENT_DOMAIN = `gui/${USER_ID}`;
20
+ const AGENT_LABEL = `${AGENT_DOMAIN}/${AGENT_NAME}`;
21
+ const AGENT_PLIST = path.join(os.homedir(), 'Library', 'LaunchAgents', `${AGENT_NAME}.plist`);
17
22
  const SYSTEMD_UNIT = 'claude-simple-edge.service';
18
23
  const MAC_OUT = path.join(os.homedir(), '.claude-simple-edge', 'edge.out.log');
19
24
  const MAC_ERR = path.join(os.homedir(), '.claude-simple-edge', 'edge.err.log');
@@ -28,6 +33,7 @@ function usage(code = 0) {
28
33
  logs Last 80 lines of stdout (or journal on Linux)
29
34
  logs -f Follow logs
30
35
  logs err Stderr only (launchd)
36
+ migrate-user-agent Move macOS Edge into the login session (required for Keychain-backed AGY auth)
31
37
 
32
38
  Also installed as: claude-simple-edge, edge-daemon
33
39
  `;
@@ -51,19 +57,79 @@ function hasCmd(cmd) {
51
57
 
52
58
  function detectBackend() {
53
59
  if (process.platform === 'darwin') {
54
- if (fs.existsSync(LAUNCHD_PLIST)) return 'launchd';
55
- const r = run('launchctl', ['print', LAUNCHD_LABEL]);
60
+ // A user LaunchAgent can access the login Keychain. Prefer it over the
61
+ // legacy system LaunchDaemon, which intentionally cannot access it.
62
+ if (run('launchctl', ['print', AGENT_LABEL]).status === 0 || fs.existsSync(AGENT_PLIST)) return 'launchd-user';
63
+ if (fs.existsSync(DAEMON_PLIST)) return 'launchd';
64
+ const r = run('launchctl', ['print', DAEMON_LABEL]);
56
65
  if (r.status === 0) return 'launchd';
57
66
  }
58
67
  if (hasCmd('systemctl')) {
59
68
  const r = run('systemctl', ['cat', SYSTEMD_UNIT]);
60
69
  if (r.status === 0) return 'systemd';
61
70
  }
62
- if (process.platform === 'darwin') return 'launchd';
71
+ if (process.platform === 'darwin') return 'launchd-user';
63
72
  if (process.platform === 'linux') return 'systemd';
64
73
  return null;
65
74
  }
66
75
 
76
+ function macTarget(backend) {
77
+ return backend === 'launchd-user'
78
+ ? { domain: AGENT_DOMAIN, label: AGENT_LABEL, plist: AGENT_PLIST, user: true }
79
+ : { domain: 'system', label: DAEMON_LABEL, plist: DAEMON_PLIST, user: false };
80
+ }
81
+
82
+ function plistEscape(value) {
83
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
84
+ }
85
+
86
+ function readDaemonEnvironment() {
87
+ const r = run('plutil', ['-extract', 'EnvironmentVariables', 'json', '-o', '-', DAEMON_PLIST]);
88
+ if (r.status !== 0) throw new Error(r.stderr || 'cannot read the existing Edge environment');
89
+ return JSON.parse(r.stdout);
90
+ }
91
+
92
+ function userAgentPlist(env) {
93
+ const client = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'client.js');
94
+ const pairs = Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) =>
95
+ `\t\t<key>${plistEscape(key)}</key>\n\t\t<string>${plistEscape(value)}</string>`).join('\n');
96
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n\t<key>Label</key>\n\t<string>${AGENT_NAME}</string>\n\t<key>EnvironmentVariables</key>\n\t<dict>\n${pairs}\n\t</dict>\n\t<key>KeepAlive</key>\n\t<true/>\n\t<key>ProcessType</key>\n\t<string>Background</string>\n\t<key>ProgramArguments</key>\n\t<array>\n\t\t<string>${plistEscape(process.execPath)}</string>\n\t\t<string>${plistEscape(client)}</string>\n\t</array>\n\t<key>RunAtLoad</key>\n\t<true/>\n\t<key>StandardErrorPath</key>\n\t<string>${plistEscape(MAC_ERR)}</string>\n\t<key>StandardOutPath</key>\n\t<string>${plistEscape(MAC_OUT)}</string>\n\t<key>ThrottleInterval</key>\n\t<integer>3</integer>\n\t<key>WorkingDirectory</key>\n\t<string>${plistEscape(path.join(os.homedir(), '.claude-simple-edge'))}</string>\n</dict>\n</plist>\n`;
97
+ }
98
+
99
+ function cmdMigrateUserAgent() {
100
+ if (process.platform !== 'darwin') fail('user LaunchAgent migration is only needed on macOS');
101
+ let inherited;
102
+ try { inherited = readDaemonEnvironment(); }
103
+ catch (e) { fail(e.message); }
104
+ const home = os.homedir();
105
+ const prefix = [path.join(home, '.local', 'bin'), path.join(home, '.grok', 'bin'), process.env.PATH, inherited.PATH]
106
+ .filter(Boolean).join(':');
107
+ const env = {
108
+ ...inherited,
109
+ HOME: home, USER: process.env.USER || os.userInfo().username, LOGNAME: process.env.LOGNAME || os.userInfo().username,
110
+ PATH: [...new Set(prefix.split(':').filter(Boolean))].join(':'),
111
+ };
112
+ const dir = path.dirname(AGENT_PLIST);
113
+ try {
114
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
115
+ const temp = `${AGENT_PLIST}.tmp-${process.pid}`;
116
+ fs.writeFileSync(temp, userAgentPlist(env), { mode: 0o600 });
117
+ // Stop the legacy daemon before the user agent claims the same loopback
118
+ // port. This calls the existing privileged edge command when available.
119
+ const stop = run(process.execPath, [fileURLToPath(import.meta.url), 'stop']);
120
+ if (stop.status !== 0) throw new Error('could not stop the legacy system Edge; run `claude-edge stop` once, then retry');
121
+ fs.renameSync(temp, AGENT_PLIST);
122
+ run('launchctl', ['bootout', AGENT_LABEL]); // harmless if it was not loaded
123
+ const boot = run('launchctl', ['bootstrap', AGENT_DOMAIN, AGENT_PLIST]);
124
+ if (boot.status !== 0) throw new Error(boot.stderr || 'could not bootstrap user Edge agent');
125
+ const start = run('launchctl', ['kickstart', '-k', AGENT_LABEL]);
126
+ if (start.status !== 0) throw new Error(start.stderr || 'could not start user Edge agent');
127
+ } catch (e) {
128
+ fail(`migration failed: ${e.message}`);
129
+ }
130
+ console.log(`migrated to user LaunchAgent: ${AGENT_LABEL}`);
131
+ }
132
+
67
133
  function isRoot() {
68
134
  return typeof process.getuid === 'function' && process.getuid() === 0;
69
135
  }
@@ -125,25 +191,26 @@ function tailFile(file, n) {
125
191
  }
126
192
 
127
193
  function cmdStatus(backend) {
128
- ensureRoot();
129
- if (backend === 'launchd') {
130
- const r = runRoot('launchctl', ['print', LAUNCHD_LABEL]);
194
+ if (backend === 'launchd') ensureRoot();
195
+ if (backend === 'launchd' || backend === 'launchd-user') {
196
+ const target = macTarget(backend);
197
+ const r = runRoot('launchctl', ['print', target.label]);
131
198
  if (r.status !== 0) {
132
- console.log(`service: ${LAUNCHD_LABEL}`);
199
+ console.log(`service: ${target.label}`);
133
200
  console.log('state: not-loaded');
134
- console.log(`plist: ${LAUNCHD_PLIST}`);
201
+ console.log(`plist: ${target.plist}`);
135
202
  if (r.stderr) console.log(r.stderr.trim());
136
203
  return;
137
204
  }
138
205
  const info = parseLaunchdPrint(r.stdout);
139
206
  const machine = r.stdout.match(/MACHINE_ID => (\S+)/)?.[1] || process.env.MACHINE_ID || '-';
140
- console.log(`service: ${LAUNCHD_LABEL}`);
207
+ console.log(`service: ${target.label}`);
141
208
  console.log(`machine: ${machine}`);
142
209
  console.log(`state: ${info.state}`);
143
210
  console.log(`pid: ${info.pid}`);
144
211
  console.log(`runs: ${info.runs}`);
145
212
  console.log(`listen: ${listenPort()}`);
146
- console.log(`plist: ${LAUNCHD_PLIST}`);
213
+ console.log(`plist: ${target.plist}`);
147
214
  console.log(`stdout: ${MAC_OUT}`);
148
215
  console.log(`stderr: ${MAC_ERR}`);
149
216
  const last = tailFile(MAC_OUT, 8);
@@ -174,15 +241,16 @@ function cmdStatus(backend) {
174
241
  }
175
242
 
176
243
  function cmdStart(backend) {
177
- ensureRoot();
178
- if (backend === 'launchd') {
179
- runRoot('launchctl', ['enable', LAUNCHD_LABEL]);
180
- const loaded = runRoot('launchctl', ['print', LAUNCHD_LABEL]);
244
+ if (backend === 'launchd') ensureRoot();
245
+ if (backend === 'launchd' || backend === 'launchd-user') {
246
+ const target = macTarget(backend);
247
+ runRoot('launchctl', ['enable', target.label]);
248
+ const loaded = runRoot('launchctl', ['print', target.label]);
181
249
  if (loaded.status !== 0) {
182
- const b = runRoot('launchctl', ['bootstrap', 'system', LAUNCHD_PLIST]);
250
+ const b = runRoot('launchctl', ['bootstrap', target.domain, target.plist]);
183
251
  if (b.status !== 0) fail('bootstrap failed', b.stderr || b.stdout);
184
252
  }
185
- const k = runRoot('launchctl', ['kickstart', '-k', LAUNCHD_LABEL]);
253
+ const k = runRoot('launchctl', ['kickstart', '-k', target.label]);
186
254
  if (k.status !== 0) fail('start failed', k.stderr || k.stdout);
187
255
  console.log('started');
188
256
  return;
@@ -193,10 +261,11 @@ function cmdStart(backend) {
193
261
  }
194
262
 
195
263
  function cmdStop(backend) {
196
- ensureRoot();
197
- if (backend === 'launchd') {
198
- runRoot('launchctl', ['disable', LAUNCHD_LABEL]);
199
- runRoot('launchctl', ['bootout', LAUNCHD_LABEL]);
264
+ if (backend === 'launchd') ensureRoot();
265
+ if (backend === 'launchd' || backend === 'launchd-user') {
266
+ const target = macTarget(backend);
267
+ runRoot('launchctl', ['disable', target.label]);
268
+ runRoot('launchctl', ['bootout', target.label]);
200
269
  console.log('stopped');
201
270
  return;
202
271
  }
@@ -206,15 +275,16 @@ function cmdStop(backend) {
206
275
  }
207
276
 
208
277
  function cmdRestart(backend) {
209
- ensureRoot();
210
- if (backend === 'launchd') {
211
- runRoot('launchctl', ['enable', LAUNCHD_LABEL]);
212
- const loaded = runRoot('launchctl', ['print', LAUNCHD_LABEL]);
278
+ if (backend === 'launchd') ensureRoot();
279
+ if (backend === 'launchd' || backend === 'launchd-user') {
280
+ const target = macTarget(backend);
281
+ runRoot('launchctl', ['enable', target.label]);
282
+ const loaded = runRoot('launchctl', ['print', target.label]);
213
283
  if (loaded.status !== 0) {
214
- const b = runRoot('launchctl', ['bootstrap', 'system', LAUNCHD_PLIST]);
284
+ const b = runRoot('launchctl', ['bootstrap', target.domain, target.plist]);
215
285
  if (b.status !== 0) fail('bootstrap failed', b.stderr || b.stdout);
216
286
  }
217
- const k = runRoot('launchctl', ['kickstart', '-k', LAUNCHD_LABEL]);
287
+ const k = runRoot('launchctl', ['kickstart', '-k', target.label]);
218
288
  if (k.status !== 0) fail('restart failed', k.stderr || k.stdout);
219
289
  console.log('restarted');
220
290
  return;
@@ -226,7 +296,7 @@ function cmdRestart(backend) {
226
296
 
227
297
  function cmdLogs(backend, mode) {
228
298
  if (backend === 'systemd') ensureRoot();
229
- if (backend === 'launchd') {
299
+ if (backend === 'launchd' || backend === 'launchd-user') {
230
300
  if (mode === '-f' || mode === '--follow') {
231
301
  const child = spawn('tail', ['-n', '50', '-F', MAC_OUT, MAC_ERR], { stdio: 'inherit' });
232
302
  child.on('exit', (c) => process.exit(c || 0));
@@ -261,6 +331,10 @@ const backend = detectBackend();
261
331
  if (!backend) fail('no launchd/systemd edge service found');
262
332
 
263
333
  switch (cmd) {
334
+ case 'migrate-user-agent':
335
+ case 'migrate':
336
+ cmdMigrateUserAgent();
337
+ break;
264
338
  case 'status':
265
339
  cmdStatus(backend);
266
340
  break;
package/dist/index.html CHANGED
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
6
  <title>Claude Code</title>
7
- <link rel="stylesheet" href="/style.0.2.19.min.css">
7
+ <link rel="stylesheet" href="/style.0.2.21.min.css">
8
8
  <link rel="stylesheet" href="/highlight.min.css">
9
9
  <script src="/marked.min.js"></script>
10
10
  <script src="/highlight.min.js"></script>
11
11
  </head>
12
12
  <body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
13
13
  <div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
14
- <script type="module" src="/app.0.2.19.min.js"></script>
14
+ <script type="module" src="/app.0.2.21.min.js"></script>
15
15
  </body>
16
16
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-simple",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
package/src/app.js CHANGED
@@ -44,10 +44,22 @@ const CREDENTIALS_FILE = process.env.CREDENTIALS_FILE || path.join(rootDir, '.
44
44
  const WORKSPACES_FILE = process.env.WORKSPACES_FILE || path.join(rootDir, '.workspaces.json');
45
45
  const PROJECT_NOTES_FILE = process.env.PROJECT_NOTES_FILE || path.join(rootDir, '.project_notes.json');
46
46
  const SESSION_META_FILE = process.env.SESSION_META_FILE || path.join(rootDir, '.session_meta.json');
47
+ // launchd/systemd have a deliberately minimal PATH. Prefer a user-local binary
48
+ // for bare command names so agents installed by their own updaters keep working
49
+ // without requiring the service unit to mirror an interactive shell PATH.
50
+ function resolveUserLocalCommand(configured, fallback) {
51
+ const command = configured || fallback;
52
+ if (command.includes('/') || command.includes('\\')) return command.replace(/^~/, os.homedir());
53
+ for (const dir of [path.join(os.homedir(), '.local', 'bin'), path.join(os.homedir(), 'bin')]) {
54
+ const candidate = path.join(dir, command);
55
+ if (fsSync.existsSync(candidate)) return candidate;
56
+ }
57
+ return command;
58
+ }
47
59
  const CLAUDE_CLI_PATH = process.env.CLAUDE_CLI_PATH || 'claude';
48
60
  const CODEX_CLI_PATH = process.env.CODEX_CLI_PATH || 'codex';
49
61
  const GROK_CLI_PATH = process.env.GROK_CLI_PATH || 'grok';
50
- const GEMINI_CLI_PATH = process.env.GEMINI_CLI_PATH || process.env.AGY_PATH || 'agy';
62
+ const GEMINI_CLI_PATH = resolveUserLocalCommand(process.env.GEMINI_CLI_PATH || process.env.AGY_PATH, 'agy');
51
63
  const CURSOR_AGENT_PATH = process.env.CURSOR_AGENT_PATH || 'cursor-agent';
52
64
  const CODEX_HOME = (process.env.CODEX_HOME || path.join(os.homedir(), '.codex')).replace(/^~/, os.homedir());
53
65
  const GROK_HOME = (process.env.GROK_HOME || path.join(os.homedir(), '.grok')).replace(/^~/, os.homedir());
File without changes