claude-simple 0.2.0 → 0.2.2
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 +64 -3
- package/dist/{app.0.2.0.min.js → app.0.2.2.min.js} +150 -97
- package/dist/index.html +2 -2
- package/dist/{style.0.2.0.min.css → style.0.2.2.min.css} +103 -4
- package/gateway.js +7 -2
- package/package.json +1 -1
- package/public/agent-panel.js +10 -1
- package/public/chat.js +9 -1
- package/public/files.js +60 -21
- package/public/index.html +2 -2
- package/public/shell/dashboard.js +62 -4
- package/public/shell/notes.js +24 -9
- package/public/shell/session-nav.js +53 -21
- package/public/shell/templates.js +30 -6
- package/public/shell.js +25 -3
- package/public/style.css +102 -3
- package/public/sw.js +2 -2
- package/server.js +5 -1
- package/src/app.js +35 -9
- package/src/auto-update.js +215 -0
- package/src/runtime/protocol.js +2 -2
package/public/style.css
CHANGED
|
@@ -11,16 +11,66 @@
|
|
|
11
11
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
12
12
|
}
|
|
13
13
|
#sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,.4); }
|
|
14
|
+
|
|
15
|
+
/* Compact chat composer on phones — less height, less dead space under bar */
|
|
16
|
+
#composer-row {
|
|
17
|
+
gap: 0.4rem;
|
|
18
|
+
padding: 0.4rem 0.55rem 0.35rem !important;
|
|
19
|
+
}
|
|
20
|
+
#chat-input.chat-input,
|
|
21
|
+
#chat-input {
|
|
22
|
+
min-height: 36px !important;
|
|
23
|
+
max-height: 96px !important;
|
|
24
|
+
padding: 0.4rem 0.65rem !important;
|
|
25
|
+
border-radius: 0.65rem;
|
|
26
|
+
/* 16px prevents iOS Safari auto-zoom on focus */
|
|
27
|
+
font-size: 16px !important;
|
|
28
|
+
line-height: 1.35 !important;
|
|
29
|
+
}
|
|
30
|
+
#btn-opts,
|
|
31
|
+
#send-btn,
|
|
32
|
+
#stop-btn {
|
|
33
|
+
height: 36px !important;
|
|
34
|
+
min-height: 36px !important;
|
|
35
|
+
padding-left: 0.65rem;
|
|
36
|
+
padding-right: 0.65rem;
|
|
37
|
+
}
|
|
38
|
+
#chat-opts {
|
|
39
|
+
padding-top: 0.35rem !important;
|
|
40
|
+
padding-left: 0.55rem !important;
|
|
41
|
+
padding-right: 0.55rem !important;
|
|
42
|
+
gap: 0.35rem !important;
|
|
43
|
+
}
|
|
44
|
+
#scroll-nav {
|
|
45
|
+
bottom: 4.5rem !important;
|
|
46
|
+
}
|
|
47
|
+
/* File list actions always visible on touch (no hover) */
|
|
48
|
+
.file-row-action {
|
|
49
|
+
opacity: 1 !important;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* File row actions: hover-reveal on desktop only */
|
|
54
|
+
@media (min-width: 641px) {
|
|
55
|
+
.file-row-action {
|
|
56
|
+
opacity: 0;
|
|
57
|
+
}
|
|
58
|
+
.group:hover .file-row-action,
|
|
59
|
+
.group:focus-within .file-row-action {
|
|
60
|
+
opacity: 1;
|
|
61
|
+
}
|
|
14
62
|
}
|
|
15
63
|
|
|
16
64
|
/*
|
|
17
|
-
* PWA
|
|
65
|
+
* Safe areas (PWA + mobile Safari with viewport-fit=cover):
|
|
18
66
|
* Do NOT pad the whole body (that leaves a dead empty strip under the chat bar).
|
|
19
67
|
* Paint chrome into the insets; only inset interactive content.
|
|
20
68
|
*/
|
|
21
69
|
html, body {
|
|
22
70
|
height: 100%;
|
|
23
71
|
height: 100dvh;
|
|
72
|
+
max-height: 100dvh;
|
|
73
|
+
overflow: hidden;
|
|
24
74
|
}
|
|
25
75
|
html.pwa-standalone,
|
|
26
76
|
html.pwa-standalone body {
|
|
@@ -34,8 +84,12 @@ html.pwa-standalone #topbar {
|
|
|
34
84
|
padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
|
|
35
85
|
padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
|
|
36
86
|
}
|
|
37
|
-
/*
|
|
38
|
-
|
|
87
|
+
/*
|
|
88
|
+
* Chat composer: background fills home-indicator inset.
|
|
89
|
+
* Inner #composer-row already has small padding; only add safe-area beyond that
|
|
90
|
+
* so we don't stack large empty strips under the input.
|
|
91
|
+
*/
|
|
92
|
+
#chat-composer {
|
|
39
93
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
40
94
|
padding-left: env(safe-area-inset-left, 0px);
|
|
41
95
|
padding-right: env(safe-area-inset-right, 0px);
|
|
@@ -829,6 +883,12 @@ html.pwa-standalone .ma-panel {
|
|
|
829
883
|
|
|
830
884
|
/* ── Chat composer ───────────────────────────────────────────────────────── */
|
|
831
885
|
/* Single outer border only — no DaisyUI nested focus ring / inner outline. */
|
|
886
|
+
#composer-row #btn-opts,
|
|
887
|
+
#composer-row #send-btn,
|
|
888
|
+
#composer-row #stop-btn {
|
|
889
|
+
height: 40px;
|
|
890
|
+
min-height: 40px;
|
|
891
|
+
}
|
|
832
892
|
#chat-input.chat-input,
|
|
833
893
|
#chat-input {
|
|
834
894
|
width: 100%;
|
|
@@ -844,6 +904,45 @@ html.pwa-standalone .ma-panel {
|
|
|
844
904
|
--tw-ring-shadow: 0 0 #0000 !important;
|
|
845
905
|
--tw-ring-offset-shadow: 0 0 #0000 !important;
|
|
846
906
|
transition: border-color .15s ease;
|
|
907
|
+
/* Avoid iOS inherited large text scaling making the box look oversized */
|
|
908
|
+
-webkit-text-size-adjust: 100%;
|
|
909
|
+
}
|
|
910
|
+
/* Kill browser password/contact autofill widgets (side checkmarks / arrows) */
|
|
911
|
+
#chat-input::-webkit-contacts-auto-fill-button,
|
|
912
|
+
#chat-input::-webkit-credentials-auto-fill-button,
|
|
913
|
+
#chat-input::-webkit-strong-password-auto-fill-button,
|
|
914
|
+
#ma-input::-webkit-contacts-auto-fill-button,
|
|
915
|
+
#ma-input::-webkit-credentials-auto-fill-button {
|
|
916
|
+
visibility: hidden !important;
|
|
917
|
+
display: none !important;
|
|
918
|
+
pointer-events: none !important;
|
|
919
|
+
position: absolute !important;
|
|
920
|
+
right: 0 !important;
|
|
921
|
+
width: 0 !important;
|
|
922
|
+
height: 0 !important;
|
|
923
|
+
margin: 0 !important;
|
|
924
|
+
padding: 0 !important;
|
|
925
|
+
opacity: 0 !important;
|
|
926
|
+
}
|
|
927
|
+
#chat-input:-webkit-autofill,
|
|
928
|
+
#chat-input:-webkit-autofill:hover,
|
|
929
|
+
#chat-input:-webkit-autofill:focus {
|
|
930
|
+
-webkit-box-shadow: 0 0 0 1000px var(--color-base-100) inset !important;
|
|
931
|
+
transition: background-color 99999s ease-in-out 0s;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/* Recent sessions topbar menu */
|
|
935
|
+
#recent-menu {
|
|
936
|
+
min-width: 16rem;
|
|
937
|
+
}
|
|
938
|
+
@media (max-width: 640px) {
|
|
939
|
+
#recent-menu-wrap #recent-menu {
|
|
940
|
+
position: fixed;
|
|
941
|
+
left: 0.5rem;
|
|
942
|
+
right: 0.5rem;
|
|
943
|
+
width: auto;
|
|
944
|
+
max-width: none;
|
|
945
|
+
}
|
|
847
946
|
}
|
|
848
947
|
#chat-input:focus,
|
|
849
948
|
#chat-input:focus-visible {
|
package/public/sw.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* Agent UI service worker — cache shell assets only.
|
|
2
2
|
* Never intercepts WebSocket or mutates API semantics beyond offline fallback.
|
|
3
3
|
* Bump CACHE when shipping UI that must invalidate old shells. */
|
|
4
|
-
const CACHE = 'agent-ui-shell-
|
|
4
|
+
const CACHE = 'agent-ui-shell-v6';
|
|
5
5
|
const PRECACHE = [
|
|
6
6
|
'/',
|
|
7
7
|
'/index.html',
|
|
8
8
|
'/manifest.webmanifest',
|
|
9
|
-
'/style.css?v=
|
|
9
|
+
'/style.css?v=ui-8',
|
|
10
10
|
'/style.css',
|
|
11
11
|
'/app.js',
|
|
12
12
|
'/icons/icon-192.png',
|
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);
|
|
@@ -375,7 +383,10 @@ function markSessionIdle(sessionId, tempKey) {
|
|
|
375
383
|
}
|
|
376
384
|
}
|
|
377
385
|
|
|
378
|
-
/**
|
|
386
|
+
/** Max JSON chars kept per tool_use input in history payloads (traffic). */
|
|
387
|
+
const HISTORY_TOOL_INPUT_MAX = 100;
|
|
388
|
+
|
|
389
|
+
/** Shrink history payloads: tool inputs dominate mobile / hub transfers. */
|
|
379
390
|
function compactHistoryMessages(messages) {
|
|
380
391
|
if (!Array.isArray(messages)) return messages;
|
|
381
392
|
return messages.map((m) => {
|
|
@@ -384,8 +395,15 @@ function compactHistoryMessages(messages) {
|
|
|
384
395
|
let raw;
|
|
385
396
|
try { raw = typeof m.input === 'string' ? m.input : JSON.stringify(m.input); }
|
|
386
397
|
catch { raw = String(m.input); }
|
|
387
|
-
if (raw.length >
|
|
388
|
-
return {
|
|
398
|
+
if (raw.length > HISTORY_TOOL_INPUT_MAX) {
|
|
399
|
+
return {
|
|
400
|
+
...m,
|
|
401
|
+
input: {
|
|
402
|
+
_truncated: true,
|
|
403
|
+
preview: raw.slice(0, HISTORY_TOOL_INPUT_MAX),
|
|
404
|
+
bytes: raw.length,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
389
407
|
}
|
|
390
408
|
}
|
|
391
409
|
if (m.type === 'text' && typeof m.content === 'string' && m.content.length > 48_000) {
|
|
@@ -399,8 +417,12 @@ function compactHistoryMessages(messages) {
|
|
|
399
417
|
export function createApp() {
|
|
400
418
|
const app = express();
|
|
401
419
|
|
|
402
|
-
// Gzip JSON/text API responses when client accepts it (
|
|
420
|
+
// Gzip JSON/text API responses when client accepts it (mobile / direct browser).
|
|
421
|
+
// Skip for hub-proxied edge traffic: Node fetch auto-decompresses gzip while
|
|
422
|
+
// still advertising Content-Encoding, which breaks the browser after tunnel
|
|
423
|
+
// re-delivery. Control WS already uses perMessageDeflate.
|
|
403
424
|
app.use((req, res, next) => {
|
|
425
|
+
if (req.headers['x-hub-token']) return next();
|
|
404
426
|
const ae = req.headers['accept-encoding'] || '';
|
|
405
427
|
if (!/\bgzip\b/i.test(ae) || req.method === 'HEAD') return next();
|
|
406
428
|
const origJson = res.json.bind(res);
|
|
@@ -540,8 +562,12 @@ export function createApp() {
|
|
|
540
562
|
const stat = await fs.stat(filePath);
|
|
541
563
|
if (req.query.download === 'true') {
|
|
542
564
|
const name = path.basename(filePath);
|
|
543
|
-
|
|
565
|
+
// ASCII fallback + RFC 5987 so Chinese / spaces filenames download correctly.
|
|
566
|
+
const ascii = name.replace(/[^\x20-\x7E]/g, '_').replace(/["\\]/g, '_');
|
|
567
|
+
const star = encodeURIComponent(name).replace(/['()]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
568
|
+
res.setHeader('Content-Disposition', `attachment; filename="${ascii || 'download'}"; filename*=UTF-8''${star}`);
|
|
544
569
|
res.setHeader('Content-Type', 'application/octet-stream');
|
|
570
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
545
571
|
return fsSync.createReadStream(filePath).pipe(res);
|
|
546
572
|
}
|
|
547
573
|
// Office / PDF / binary previews need more headroom than plain text
|
|
@@ -873,11 +899,11 @@ export function createApp() {
|
|
|
873
899
|
const { sessionId } = req.params;
|
|
874
900
|
if (!/^[0-9a-f-]{36}$/i.test(sessionId)) return res.status(400).json({ error: 'Invalid session id' });
|
|
875
901
|
const preferred = typeof req.query.agent === 'string' ? req.query.agent : null;
|
|
876
|
-
// tail=0 → full history
|
|
877
|
-
let tail = parseInt(req.query.tail ?? '
|
|
878
|
-
if (Number.isNaN(tail)) tail =
|
|
902
|
+
// tail=0 → full history (default). Positive = last N messages only.
|
|
903
|
+
let tail = parseInt(req.query.tail ?? '0', 10);
|
|
904
|
+
if (Number.isNaN(tail)) tail = 0;
|
|
879
905
|
if (tail < 0) tail = 0;
|
|
880
|
-
if (tail >
|
|
906
|
+
if (tail > 50_000) tail = 50_000;
|
|
881
907
|
const compact = req.query.compact !== '0' && req.query.compact !== 'false';
|
|
882
908
|
const opts = {
|
|
883
909
|
claudeConfigDirs: CLAUDE_CONFIG_DIRS,
|
|
@@ -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
|
+
}
|
package/src/runtime/protocol.js
CHANGED
|
@@ -138,9 +138,9 @@ export const UI_EVENT = {
|
|
|
138
138
|
/**
|
|
139
139
|
* Compact tool_use inputs before they hit the wire (mobile tunnel cost).
|
|
140
140
|
* @param {unknown} input
|
|
141
|
-
* @param {number} [max=
|
|
141
|
+
* @param {number} [max=100]
|
|
142
142
|
*/
|
|
143
|
-
export function compactToolInput(input, max =
|
|
143
|
+
export function compactToolInput(input, max = 100) {
|
|
144
144
|
if (input == null) return input;
|
|
145
145
|
let raw;
|
|
146
146
|
try {
|