claude-simple 0.2.0 → 0.2.1

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/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createApp, init } from './src/app.js';
2
+ import { createApp, init, hasBusyAgent } from './src/app.js';
3
+ import { startAutoUpdate } from './src/auto-update.js';
3
4
  import { WebSocket } from 'ws';
4
5
  import os from 'os';
5
6
  import crypto from 'crypto';
@@ -18,6 +19,10 @@ const { server } = createApp();
18
19
  await new Promise(resolve => server.listen(LOCAL_PORT, '127.0.0.1', resolve));
19
20
  console.log(`[client] Local app listening on 127.0.0.1:${LOCAL_PORT}`);
20
21
 
22
+ // Self-update from npm (default every 12h). Disable with AUTO_UPDATE=0.
23
+ // Requires global install + systemd Restart= (or equivalent) for full apply.
24
+ startAutoUpdate({ role: 'edge-client', isBusy: hasBusyAgent });
25
+
21
26
  // ─── Active WS tunnels: tunnelId → local WebSocket ───────────────────────────
22
27
  const tunnels = new Map();
23
28
 
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.0.min.css">
7
+ <link rel="stylesheet" href="/style.0.2.1.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.0.min.js"></script>
14
+ <script type="module" src="/app.0.2.1.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.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-simple": "server.js",
package/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createApp, init, PORT } from './src/app.js';
2
+ import { createApp, init, PORT, hasBusyAgent } from './src/app.js';
3
+ import { startAutoUpdate } from './src/auto-update.js';
3
4
 
4
5
  await init();
5
6
 
@@ -10,3 +11,6 @@ server.listen(PORT, () => {
10
11
  if (process.env.AUTH_USERNAME) console.log(` Auth: env user "${process.env.AUTH_USERNAME}"`);
11
12
  console.log();
12
13
  });
14
+
15
+ // Optional self-update (same as edge). AUTO_UPDATE=0 to disable.
16
+ startAutoUpdate({ role: 'standalone', isBusy: hasBusyAgent });
package/src/app.js CHANGED
@@ -368,6 +368,14 @@ function isSessionBusy(sessionId) {
368
368
  return false;
369
369
  }
370
370
 
371
+ /** True if any agent turn is in flight (used by auto-update before restart). */
372
+ export function hasBusyAgent() {
373
+ for (const s of activeSessions.values()) {
374
+ if (s && s.busy !== false) return true;
375
+ }
376
+ return false;
377
+ }
378
+
371
379
  function markSessionIdle(sessionId, tempKey) {
372
380
  for (const key of [sessionId, tempKey].filter(Boolean)) {
373
381
  const s = activeSessions.get(key);
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Optional self-update against the public npm registry.
3
+ *
4
+ * Env:
5
+ * AUTO_UPDATE=0|false|off disable (default: enabled for edge client)
6
+ * AUTO_UPDATE_INTERVAL_HOURS check interval, default 12 (half day)
7
+ * AUTO_UPDATE_PACKAGE package name, default claude-simple
8
+ * AUTO_UPDATE_CHANNEL dist-tag, default latest
9
+ * AUTO_UPDATE_IDLE_WAIT_MS wait for agents to finish before applying, default 30min
10
+ * AUTO_UPDATE_APPLY=0 only log when newer; do not install/restart
11
+ */
12
+ import { spawn } from 'child_process';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const PKG_ROOT = path.join(__dirname, '..');
19
+
20
+ function envBool(name, defaultOn) {
21
+ const v = process.env[name];
22
+ if (v == null || v === '') return defaultOn;
23
+ return !/^(0|false|off|no)$/i.test(String(v));
24
+ }
25
+
26
+ function readLocalVersion() {
27
+ try {
28
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
29
+ return String(pkg.version || '0.0.0');
30
+ } catch {
31
+ return '0.0.0';
32
+ }
33
+ }
34
+
35
+ /** semver-ish compare: a > b → 1, a < b → -1, eq → 0 */
36
+ export function cmpSemver(a, b) {
37
+ const pa = String(a).replace(/^v/, '').split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
38
+ const pb = String(b).replace(/^v/, '').split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
39
+ const n = Math.max(pa.length, pb.length);
40
+ for (let i = 0; i < n; i++) {
41
+ const d = (pa[i] || 0) - (pb[i] || 0);
42
+ if (d !== 0) return d > 0 ? 1 : -1;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ async function fetchLatestVersion(pkgName, tag) {
48
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
49
+ const res = await fetch(url, {
50
+ headers: { Accept: 'application/json', 'User-Agent': 'claude-simple-auto-update' },
51
+ signal: AbortSignal.timeout(20_000),
52
+ });
53
+ if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
54
+ const data = await res.json();
55
+ const ver = data?.['dist-tags']?.[tag] || data?.['dist-tags']?.latest;
56
+ if (!ver) throw new Error('no dist-tag on registry');
57
+ return String(ver);
58
+ }
59
+
60
+ /** Detect install layout: global npm vs local/source tree. */
61
+ function detectInstallMode() {
62
+ const entry = process.argv[1] ? path.resolve(process.argv[1]) : '';
63
+ const inGlobal = /[/\\]node_modules[/\\]claude-simple[/\\]/.test(entry)
64
+ || /[/\\]lib[/\\]node_modules[/\\]claude-simple[/\\]/.test(PKG_ROOT);
65
+ // Prefer global update when package lives under a node_modules tree named claude-simple
66
+ if (inGlobal || /[/\\]node_modules[/\\]claude-simple$/.test(PKG_ROOT)) {
67
+ return { mode: 'global', cwd: process.cwd() };
68
+ }
69
+ return { mode: 'local', cwd: PKG_ROOT };
70
+ }
71
+
72
+ function runNpmInstall(mode, pkgName, version, cwd) {
73
+ const spec = `${pkgName}@${version}`;
74
+ const args = mode === 'global'
75
+ ? ['install', '-g', spec, '--no-fund', '--no-audit']
76
+ : ['install', spec, '--no-fund', '--no-audit', '--prefix', cwd];
77
+ return new Promise((resolve, reject) => {
78
+ console.log(`[auto-update] running: npm ${args.join(' ')}`);
79
+ const child = spawn('npm', args, {
80
+ cwd: mode === 'global' ? osHomedir() : cwd,
81
+ env: process.env,
82
+ stdio: ['ignore', 'pipe', 'pipe'],
83
+ });
84
+ let out = '';
85
+ let err = '';
86
+ child.stdout.on('data', (d) => { out += d; process.stdout.write(d); });
87
+ child.stderr.on('data', (d) => { err += d; process.stderr.write(d); });
88
+ child.on('error', reject);
89
+ child.on('close', (code) => {
90
+ if (code === 0) resolve({ out, err });
91
+ else reject(new Error(`npm exited ${code}: ${(err || out).slice(-500)}`));
92
+ });
93
+ });
94
+ }
95
+
96
+ function osHomedir() {
97
+ return process.env.HOME || process.env.USERPROFILE || PKG_ROOT;
98
+ }
99
+
100
+ /**
101
+ * @param {object} opts
102
+ * @param {() => boolean} [opts.isBusy] return true while agent turn is running
103
+ * @param {boolean} [opts.enabled]
104
+ * @param {string} [opts.role] log label
105
+ */
106
+ export function startAutoUpdate(opts = {}) {
107
+ const enabled = opts.enabled != null ? opts.enabled : envBool('AUTO_UPDATE', true);
108
+ if (!enabled) {
109
+ console.log('[auto-update] disabled (AUTO_UPDATE=0)');
110
+ return { stop() {} };
111
+ }
112
+
113
+ const intervalHours = Math.max(0.25, parseFloat(process.env.AUTO_UPDATE_INTERVAL_HOURS || '12') || 12);
114
+ const intervalMs = intervalHours * 3600 * 1000;
115
+ const pkgName = process.env.AUTO_UPDATE_PACKAGE || 'claude-simple';
116
+ const channel = process.env.AUTO_UPDATE_CHANNEL || 'latest';
117
+ const apply = envBool('AUTO_UPDATE_APPLY', true);
118
+ const idleWaitMs = Math.max(60_000, parseInt(process.env.AUTO_UPDATE_IDLE_WAIT_MS || String(30 * 60 * 1000), 10) || 30 * 60 * 1000);
119
+ const isBusy = typeof opts.isBusy === 'function' ? opts.isBusy : () => false;
120
+ const role = opts.role || 'process';
121
+
122
+ let stopped = false;
123
+ let timer = null;
124
+ let applying = false;
125
+ const localVersion = readLocalVersion();
126
+
127
+ console.log(
128
+ `[auto-update] enabled · local=${localVersion} · check every ${intervalHours}h · package=${pkgName}@${channel} · apply=${apply}`,
129
+ );
130
+
131
+ async function waitUntilIdle() {
132
+ const deadline = Date.now() + idleWaitMs;
133
+ while (Date.now() < deadline) {
134
+ if (stopped) return false;
135
+ if (!isBusy()) return true;
136
+ console.log('[auto-update] agent busy — waiting before apply…');
137
+ await new Promise((r) => setTimeout(r, 15_000));
138
+ }
139
+ console.warn('[auto-update] idle wait timed out; applying anyway');
140
+ return true;
141
+ }
142
+
143
+ async function checkOnce() {
144
+ if (stopped || applying) return;
145
+ try {
146
+ const latest = await fetchLatestVersion(pkgName, channel);
147
+ const cmp = cmpSemver(latest, localVersion);
148
+ if (cmp <= 0) {
149
+ console.log(`[auto-update] up to date (${localVersion}, registry ${latest})`);
150
+ return;
151
+ }
152
+ console.log(`[auto-update] new version available: ${localVersion} → ${latest}`);
153
+ if (!apply) {
154
+ console.log('[auto-update] AUTO_UPDATE_APPLY=0 — skip install');
155
+ return;
156
+ }
157
+
158
+ applying = true;
159
+ await waitUntilIdle();
160
+ if (stopped) return;
161
+
162
+ const { mode, cwd } = detectInstallMode();
163
+ console.log(`[auto-update] install mode=${mode} root=${PKG_ROOT}`);
164
+
165
+ // Global install when package is under node_modules; else npm install in package root
166
+ if (mode === 'global') {
167
+ await runNpmInstall('global', pkgName, latest, cwd);
168
+ } else {
169
+ // Source / linked tree: pull package.json version bump via npm update in place
170
+ await runNpmInstall('local', pkgName, latest, cwd);
171
+ // If we're running from a git checkout that *is* the package (not a dependency),
172
+ // local `npm install claude-simple@x` only updates node_modules — won't replace this tree.
173
+ // For git checkouts, pull via npm pack is wrong; log and skip restart unless global.
174
+ const pkgJsonName = (() => {
175
+ try { return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).name; }
176
+ catch { return null; }
177
+ })();
178
+ if (pkgJsonName === pkgName && !/node_modules/.test(PKG_ROOT)) {
179
+ console.warn(
180
+ '[auto-update] running from source checkout — npm cannot overwrite this tree. '
181
+ + 'Use `npm install -g claude-simple` for auto-apply, or pull git + restart manually.',
182
+ );
183
+ applying = false;
184
+ return;
185
+ }
186
+ }
187
+
188
+ console.log(`[auto-update] installed ${latest}; restarting ${role} (exit 0 — supervisor should relaunch)`);
189
+ // Give logs a moment to flush; systemd Restart= will bring us back on new code
190
+ setTimeout(() => process.exit(0), 500);
191
+ } catch (e) {
192
+ console.warn(`[auto-update] check failed: ${e.message || e}`);
193
+ applying = false;
194
+ }
195
+ }
196
+
197
+ // First check after a short delay (don't compete with boot / hub connect)
198
+ const firstDelay = Math.min(60_000, Math.max(15_000, intervalMs / 100));
199
+ timer = setTimeout(function tick() {
200
+ checkOnce().finally(() => {
201
+ if (stopped) return;
202
+ timer = setTimeout(tick, intervalMs);
203
+ });
204
+ }, firstDelay);
205
+ if (typeof timer.unref === 'function') timer.unref();
206
+
207
+ return {
208
+ stop() {
209
+ stopped = true;
210
+ if (timer) clearTimeout(timer);
211
+ },
212
+ checkNow: checkOnce,
213
+ localVersion,
214
+ };
215
+ }
File without changes