aicodeman 1.30.0 → 1.32.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 +7 -6
- package/dist/config/cli-registry/schema.d.ts +8 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +29 -0
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +62 -7
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +62 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/remote-wake-limits.d.ts +23 -0
- package/dist/config/remote-wake-limits.d.ts.map +1 -0
- package/dist/config/remote-wake-limits.js +23 -0
- package/dist/config/remote-wake-limits.js.map +1 -0
- package/dist/custom-model-hosts.d.ts +32 -0
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +29 -1
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +191 -5
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +35 -9
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +37 -16
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/mux-interface.d.ts +12 -0
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-hosts.d.ts +20 -0
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +33 -0
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-wake.d.ts +446 -0
- package/dist/remote-wake.d.ts.map +1 -0
- package/dist/remote-wake.js +880 -0
- package/dist/remote-wake.js.map +1 -0
- package/dist/session-env-clamp.d.ts +12 -7
- package/dist/session-env-clamp.d.ts.map +1 -1
- package/dist/session-env-clamp.js +12 -7
- package/dist/session-env-clamp.js.map +1 -1
- package/dist/session-submit-verifier.d.ts +44 -0
- package/dist/session-submit-verifier.d.ts.map +1 -0
- package/dist/session-submit-verifier.js +122 -0
- package/dist/session-submit-verifier.js.map +1 -0
- package/dist/session.d.ts +9 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +42 -1
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +9 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/session.d.ts +26 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.a792bf71.js +38 -0
- package/dist/web/public/app.a792bf71.js.br +0 -0
- package/dist/web/public/app.a792bf71.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.258b140f.js → constants.b26748e9.js} +123 -0
- package/dist/web/public/constants.b26748e9.js.br +0 -0
- package/dist/web/public/constants.b26748e9.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js +440 -0
- package/dist/web/public/host-wake-ui.js.br +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.d22be5c4.js +1 -0
- package/dist/web/public/i18n.d22be5c4.js.br +0 -0
- package/dist/web/public/i18n.d22be5c4.js.gz +0 -0
- package/dist/web/public/{image-input.cd4b97c4.js → image-input.a216cf9e.js} +8 -4
- package/dist/web/public/image-input.a216cf9e.js.br +0 -0
- package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
- package/dist/web/public/index.html +191 -12
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.2cf04f17.js → keyboard-accessory.c1e7c7b7.js} +280 -15
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/mobile-handlers.6f354a87.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.6caaa28a.css.gz +0 -0
- package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.5b07ad14.js → panels-ui.28df5053.js} +5 -5
- package/dist/web/public/panels-ui.28df5053.js.br +0 -0
- package/dist/web/public/panels-ui.28df5053.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.7df17fd3.js +88 -0
- package/dist/web/public/session-ui.7df17fd3.js.br +0 -0
- package/dist/web/public/session-ui.7df17fd3.js.gz +0 -0
- package/dist/web/public/settings-ui.df5ffb68.js +78 -0
- package/dist/web/public/settings-ui.df5ffb68.js.br +0 -0
- package/dist/web/public/settings-ui.df5ffb68.js.gz +0 -0
- package/dist/web/public/styles.74f312e5.css +1 -0
- package/dist/web/public/styles.74f312e5.css.br +0 -0
- package/dist/web/public/styles.74f312e5.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
- package/dist/web/public/terminal-split.js +804 -0
- package/dist/web/public/terminal-split.js.br +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-ui.32fbeaf6.js +2 -0
- package/dist/web/public/terminal-ui.32fbeaf6.js.br +0 -0
- package/dist/web/public/terminal-ui.32fbeaf6.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/routes/custom-model-routes.d.ts +173 -0
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +628 -11
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +1 -1
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -1
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +6 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +448 -17
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +15 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +76 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +21 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +142 -9
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +25 -2
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +30 -3
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +75 -26
- package/skills/codeman/preamble.sh +66 -19
- package/skills/codeman/reference/recipes.md +1 -1
- package/dist/web/public/app.6d2dc4e8.js +0 -38
- package/dist/web/public/app.6d2dc4e8.js.br +0 -0
- package/dist/web/public/app.6d2dc4e8.js.gz +0 -0
- package/dist/web/public/constants.258b140f.js.br +0 -0
- package/dist/web/public/constants.258b140f.js.gz +0 -0
- package/dist/web/public/i18n.5f897ed5.js +0 -1
- package/dist/web/public/i18n.5f897ed5.js.br +0 -0
- package/dist/web/public/i18n.5f897ed5.js.gz +0 -0
- package/dist/web/public/image-input.cd4b97c4.js.br +0 -0
- package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.2cf04f17.js.br +0 -0
- package/dist/web/public/keyboard-accessory.2cf04f17.js.gz +0 -0
- package/dist/web/public/panels-ui.5b07ad14.js.br +0 -0
- package/dist/web/public/panels-ui.5b07ad14.js.gz +0 -0
- package/dist/web/public/session-ui.42b81477.js +0 -77
- package/dist/web/public/session-ui.42b81477.js.br +0 -0
- package/dist/web/public/session-ui.42b81477.js.gz +0 -0
- package/dist/web/public/settings-ui.58f1d756.js +0 -67
- package/dist/web/public/settings-ui.58f1d756.js.br +0 -0
- package/dist/web/public/settings-ui.58f1d756.js.gz +0 -0
- package/dist/web/public/styles.e6edfb0b.css +0 -1
- package/dist/web/public/styles.e6edfb0b.css.br +0 -0
- package/dist/web/public/styles.e6edfb0b.css.gz +0 -0
- package/dist/web/public/terminal-ui.4f8d5820.js +0 -2
- package/dist/web/public/terminal-ui.4f8d5820.js.br +0 -0
- package/dist/web/public/terminal-ui.4f8d5820.js.gz +0 -0
package/dist/web/server.js
CHANGED
|
@@ -42,6 +42,7 @@ import { hostname as getHostname, uptime as osUptime } from 'node:os';
|
|
|
42
42
|
import { looksLikeHostReboot, newestPersistedActivity, planRebootRestore } from '../reboot-restore.js';
|
|
43
43
|
import { rebootRestoreRegistry } from './reboot-restore-registry.js';
|
|
44
44
|
import { dataPath, getDataDir, CODEMAN_INSTANCE } from '../config/instance.js';
|
|
45
|
+
import { readRemoteHosts, rehydrateRemoteHostFields } from '../remote-hosts.js';
|
|
45
46
|
import { normalizeBasePath, stripBasePath, joinBasePath } from '../config/base-path.js';
|
|
46
47
|
import { GLYPH, palette } from '../cli-style.js';
|
|
47
48
|
import { getHookSecret } from '../config/hook-secret.js';
|
|
@@ -58,7 +59,7 @@ import { subagentWatcher, } from '../subagent-watcher.js';
|
|
|
58
59
|
import { imageWatcher } from '../image-watcher.js';
|
|
59
60
|
import { workflowRunWatcher, summarizeRun } from '../workflow-run-watcher.js';
|
|
60
61
|
import { attachmentRegistry, buildFileThumbnailRoute, registerExternalAttachment } from '../attachment-registry.js';
|
|
61
|
-
import { getCli } from '../config/cli-registry/registry.js';
|
|
62
|
+
import { getCli, enabledClis } from '../config/cli-registry/registry.js';
|
|
62
63
|
import { readCustomModelHosts } from '../custom-model-hosts.js';
|
|
63
64
|
import { applyCustomModelInjection, customModelConfigDir, removeConfigDir } from '../custom-model-injection-apply.js';
|
|
64
65
|
import { registerGeneratedArtifactAttachment } from '../generated-artifact-attachments.js';
|
|
@@ -113,7 +114,7 @@ import { isMultiUserMode } from '../config/multiuser.js';
|
|
|
113
114
|
import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '../user-store.js';
|
|
114
115
|
import { installRouteErrorHandler } from './route-error-handler.js';
|
|
115
116
|
import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy } from './network-auth-policy.js';
|
|
116
|
-
import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerCustomModelRoutes, tryWebviewRefererFallback, } from './routes/index.js';
|
|
117
|
+
import { registerPushRoutes, registerTeamRoutes, registerMuxRoutes, registerFileRoutes, registerScheduledRoutes, registerHookEventRoutes, registerApprovalRoutes, registerRebootRestoreRoutes, registerReadMyMindRoutes, registerStatusTelemetryRoutes, registerSystemRoutes, registerCaseRoutes, registerSessionRoutes, registerRespawnRoutes, registerRalphRoutes, registerPlanRoutes, registerClipboardRoutes, registerSearchRoutes, registerOrchestratorRoutes, registerCronRoutes, registerMeRoutes, registerAdminRoutes, registerWsRoutes, registerVoiceRoutes, registerWebviewRoutes, registerTabLayoutRoutes, registerCustomModelRoutes, refreshAllCustomModelHosts, readCustomModelEndpointsEnabled, closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, tryWebviewRefererFallback, } from './routes/index.js';
|
|
117
118
|
import { isLostWebviewFrameNavigation } from './webview-proxy.js';
|
|
118
119
|
import { CronService } from '../cron/cron-service.js';
|
|
119
120
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -122,9 +123,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
122
123
|
// while capping growth of `sseClientsById` and blocking pathological inputs.
|
|
123
124
|
const SSE_CLIENT_ID_RE = /^[A-Za-z0-9_-]{8,64}$/;
|
|
124
125
|
const CODEX_USAGE_POLL_INTERVAL_MS = 5 * 60_000;
|
|
126
|
+
const CUSTOM_MODEL_REDISCOVER_INTERVAL_MS = 5 * 60_000;
|
|
127
|
+
// Much shorter than the model-LIST refresh above on purpose: this catches an actual
|
|
128
|
+
// eviction (a session's model no longer loaded, silently swapped out by another
|
|
129
|
+
// session's use), which the user wants to know about promptly, not once every 5
|
|
130
|
+
// minutes. Cheap either way — one /running GET per distinct endpoint with at least
|
|
131
|
+
// one live custom-model session, not per session.
|
|
132
|
+
const CUSTOM_MODEL_SWAP_CHECK_INTERVAL_MS = 20_000;
|
|
125
133
|
function escapeHtmlText(value) {
|
|
126
134
|
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
127
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Escapes a JSON string for safe embedding as the body of an inline `<script>`
|
|
138
|
+
* tag: `<` becomes the six-character sequence `<`, which both a JSON
|
|
139
|
+
* parser and a plain JS string literal decode back to `<` (both treat
|
|
140
|
+
* `\uXXXX` identically), but which can never itself form the two literal
|
|
141
|
+
* characters `<` `/` a browser's HTML tokenizer looks for to end the tag. A
|
|
142
|
+
* value containing a literal `</script>` would otherwise close the tag early
|
|
143
|
+
* and turn the rest of the document into inert script-body text. Exported so
|
|
144
|
+
* it unit-tests without constructing a WebServer (which needs a real tmux).
|
|
145
|
+
*/
|
|
146
|
+
export function escapeScriptJson(json) {
|
|
147
|
+
return json.replace(/</g, '\\u003c');
|
|
148
|
+
}
|
|
128
149
|
import { SESSIONS_LIST_CACHE_TTL, SCHEDULED_CLEANUP_INTERVAL, SCHEDULED_RUN_MAX_AGE, SSE_HEARTBEAT_INTERVAL, SESSION_LIMIT_WAIT_MS, ITERATION_PAUSE_MS, STATS_COLLECTION_INTERVAL_MS, INACTIVITY_TIMEOUT_MS, CRON_TICK_INTERVAL, } from '../config/server-timing.js';
|
|
129
150
|
/**
|
|
130
151
|
* Get or generate a self-signed TLS certificate for HTTPS.
|
|
@@ -163,8 +184,18 @@ export class WebServer extends EventEmitter {
|
|
|
163
184
|
// Store session listener references for explicit cleanup (prevents memory leaks)
|
|
164
185
|
sessionListenerRefs = new Map();
|
|
165
186
|
scheduledRuns = new Map();
|
|
187
|
+
/** De-dupe state for the swap-displacement sweep — see detectCustomModelSwapDisplacements. */
|
|
188
|
+
_customModelDisplacedNotified = new Set();
|
|
166
189
|
/** Cron service (assigned in setupRoutes). */
|
|
167
190
|
cronService;
|
|
191
|
+
/**
|
|
192
|
+
* Wake-on-LAN registry, returned by `registerSessionRoutes`. Held for its LIFETIME
|
|
193
|
+
* only — `drop()` on session cleanup, `stop()` on shutdown. Waking from here would
|
|
194
|
+
* re-wake a host on every timer tick (the invariant `remote-wake.ts` documents), so
|
|
195
|
+
* the wiring guard in `test/remote-wake.test.ts` pins that this file calls nothing
|
|
196
|
+
* but `drop`/`stop` on it.
|
|
197
|
+
*/
|
|
198
|
+
remoteWake = null;
|
|
168
199
|
sse;
|
|
169
200
|
store = getStore();
|
|
170
201
|
tabLayouts;
|
|
@@ -902,7 +933,9 @@ export class WebServer extends EventEmitter {
|
|
|
902
933
|
registerStatusTelemetryRoutes(this.app, ctx);
|
|
903
934
|
registerSystemRoutes(this.app, ctx);
|
|
904
935
|
registerCaseRoutes(this.app, ctx);
|
|
905
|
-
|
|
936
|
+
// The registry's lifetime is the server's: it drops per-session wake state on every
|
|
937
|
+
// cleanup path and resolves in-flight wakes on shutdown.
|
|
938
|
+
this.remoteWake = registerSessionRoutes(this.app, ctx);
|
|
906
939
|
registerRespawnRoutes(this.app, ctx);
|
|
907
940
|
registerRalphRoutes(this.app, ctx);
|
|
908
941
|
registerPlanRoutes(this.app, ctx);
|
|
@@ -1027,7 +1060,7 @@ export class WebServer extends EventEmitter {
|
|
|
1027
1060
|
return undefined;
|
|
1028
1061
|
}
|
|
1029
1062
|
try {
|
|
1030
|
-
return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id)?.envOverrides;
|
|
1063
|
+
return applyCustomModelInjection(entry, endpoint, saved.modelId, session.id, endpoint.modelContextLengths?.[saved.modelId])?.envOverrides;
|
|
1031
1064
|
}
|
|
1032
1065
|
catch (err) {
|
|
1033
1066
|
console.warn('[WebServer] Failed to rebuild custom-model env on recovery:', err);
|
|
@@ -1112,6 +1145,9 @@ export class WebServer extends EventEmitter {
|
|
|
1112
1145
|
if (session) {
|
|
1113
1146
|
session.ralphTracker.stopWatchingFixPlan();
|
|
1114
1147
|
}
|
|
1148
|
+
// Custom Model Endpoint Profiles: drop this session's swap-displacement notify flag
|
|
1149
|
+
// (see _checkCustomModelSwapDisplacements below) so it can't linger in that Set forever.
|
|
1150
|
+
this._customModelDisplacedNotified.delete(sessionId);
|
|
1115
1151
|
// Kill all subagents spawned by this session (scoped to sessionId to avoid cross-session kills)
|
|
1116
1152
|
if (session && killMux) {
|
|
1117
1153
|
try {
|
|
@@ -1251,6 +1287,11 @@ export class WebServer extends EventEmitter {
|
|
|
1251
1287
|
sessionWaits.notifySignal(sessionId, 'exit');
|
|
1252
1288
|
sessionWaits.cancelAll(sessionId);
|
|
1253
1289
|
approvalInbox.resolveForSession(sessionId, 'session_ended');
|
|
1290
|
+
// Wake state goes with the session on EVERY cleanup path (delete routes, the cron
|
|
1291
|
+
// and admin paths, scheduled-run teardown, error paths) — that is why it lives here
|
|
1292
|
+
// rather than in the two delete routes, where it left an entry behind, including up
|
|
1293
|
+
// to 4 KB of the user's buffered keystrokes.
|
|
1294
|
+
this.remoteWake?.drop(sessionId);
|
|
1254
1295
|
this.broadcast(SseEvent.SessionDeleted, { id: sessionId });
|
|
1255
1296
|
}
|
|
1256
1297
|
async renderIndexHtml(soloSessionId) {
|
|
@@ -1307,9 +1348,17 @@ export class WebServer extends EventEmitter {
|
|
|
1307
1348
|
// the /session/:id URL path; this global is a belt-and-suspenders fallback.
|
|
1308
1349
|
// The id is gated to JSON + <-escaped so it can't break out of the inline
|
|
1309
1350
|
// <script> (ids are UUIDs in practice, but defense-in-depth is cheap).
|
|
1351
|
+
//
|
|
1352
|
+
// Every `</head>` injection below passes a replacer FUNCTION, never a
|
|
1353
|
+
// replacement STRING: `String.replace` interprets `$&`, `$'`, `` $` `` and
|
|
1354
|
+
// `$<n>` inside a string replacement, so a payload carrying `$'` would splice
|
|
1355
|
+
// the rest of the document (the whole <body>) into the inline script, past
|
|
1356
|
+
// any escaping applied to the payload itself. The custom-model list below
|
|
1357
|
+
// carries a user-settable `label` (clis.json), which is the site that made
|
|
1358
|
+
// this real; the others follow the same rule so the class of bug stays out.
|
|
1310
1359
|
if (soloSessionId) {
|
|
1311
1360
|
const safeId = JSON.stringify(soloSessionId).replace(/</g, '\\u003c');
|
|
1312
|
-
html = html.replace('</head>', `<script>window.__CODEMAN_SOLO__=${safeId};</script>\n</head>`);
|
|
1361
|
+
html = html.replace('</head>', () => `<script>window.__CODEMAN_SOLO__=${safeId};</script>\n</head>`);
|
|
1313
1362
|
}
|
|
1314
1363
|
// Gesture-control overlay (Phase 5): dashboard only (not solo popups, which
|
|
1315
1364
|
// have no tab strip). `CODEMAN_GESTURE=1` makes the feature *available* on
|
|
@@ -1367,15 +1416,29 @@ export class WebServer extends EventEmitter {
|
|
|
1367
1416
|
// keep without git (issue #236), same reasoning as cloudflared above.
|
|
1368
1417
|
git: isGitAvailable(),
|
|
1369
1418
|
};
|
|
1370
|
-
html = html.replace('</head>', `<script>window.__codemanCliAvailable=${JSON.stringify(available)};</script>\n</head>`);
|
|
1419
|
+
html = html.replace('</head>', () => `<script>window.__codemanCliAvailable=${JSON.stringify(available)};</script>\n</head>`);
|
|
1420
|
+
// Which run modes the Run-menu picker (docs/custom-model-endpoints-plan.md) may
|
|
1421
|
+
// generate an entry for: read generically off the registry's `capabilities`
|
|
1422
|
+
// (never an id list here) so a CLI whose customModelInjection lands later shows
|
|
1423
|
+
// up in the picker with no frontend change, and one that ships `unsupported`
|
|
1424
|
+
// (antigravity, and `shell`'s `kind !== 'agent'`) never does.
|
|
1425
|
+
const customModelClis = enabledClis()
|
|
1426
|
+
.filter((entry) => entry.kind === 'agent' && entry.capabilities.customModelInjection.kind !== 'unsupported')
|
|
1427
|
+
.map((entry) => ({ id: entry.id, label: entry.label }));
|
|
1428
|
+
// Unlike the boolean-only __codemanCliAvailable above, this payload carries
|
|
1429
|
+
// `label`, a string a user's own clis.json can set (CliEntry.label, up to 60
|
|
1430
|
+
// chars) — see escapeScriptJson's own doc comment for why that needs escaping
|
|
1431
|
+
// and __codemanCliAvailable's booleans never did.
|
|
1432
|
+
const customModelClisJson = escapeScriptJson(JSON.stringify(customModelClis));
|
|
1433
|
+
html = html.replace('</head>', () => `<script>window.__codemanCustomModelClis=${customModelClisJson};</script>\n</head>`);
|
|
1371
1434
|
}
|
|
1372
1435
|
if (!soloSessionId && process.env.CODEMAN_GESTURE === '1') {
|
|
1373
|
-
html = html.replace('</head>', `<script>window.__codemanGestureAvailable=true;</script>\n</head>`);
|
|
1436
|
+
html = html.replace('</head>', () => `<script>window.__codemanGestureAvailable=true;</script>\n</head>`);
|
|
1374
1437
|
if (settings.gestureControlEnabled === true) {
|
|
1375
1438
|
const v = this.gestureBundleVersion();
|
|
1376
1439
|
// Relative src so the injected `<base href>` resolves it under the mount
|
|
1377
1440
|
// prefix (a root-absolute `/gesture/...` would escape a sub-path mount).
|
|
1378
|
-
html = html.replace('</head>', `<script type="module" src="gesture/gesture-codeman.js${v}"></script>\n</head>`);
|
|
1441
|
+
html = html.replace('</head>', () => `<script type="module" src="gesture/gesture-codeman.js${v}"></script>\n</head>`);
|
|
1379
1442
|
}
|
|
1380
1443
|
}
|
|
1381
1444
|
return html;
|
|
@@ -2019,11 +2082,20 @@ export class WebServer extends EventEmitter {
|
|
|
2019
2082
|
'scheduled:',
|
|
2020
2083
|
'team:',
|
|
2021
2084
|
'case:',
|
|
2085
|
+
'remote:',
|
|
2086
|
+
'custom-model:',
|
|
2022
2087
|
];
|
|
2023
2088
|
if (SESSION_PREFIXES.some((p) => event.startsWith(p))) {
|
|
2024
2089
|
const d = (data ?? {});
|
|
2025
2090
|
const sessionId = d.sessionId ?? d.id ?? d.session?.id;
|
|
2026
2091
|
const owner = sessionId ? this.sessions.get(sessionId)?.owner : undefined;
|
|
2092
|
+
// `remote:hostWaking` / `remote:hostWakeFailed` for a create/attach wake have no
|
|
2093
|
+
// session yet (nothing exists until the host is up), so the registry names the
|
|
2094
|
+
// requesting user instead; the payload carries `hostId`/`label`, which non-admins
|
|
2095
|
+
// are not shown elsewhere. No session and no requester: admins only (fail closed).
|
|
2096
|
+
if (!sessionId && event.startsWith('remote:') && d.username) {
|
|
2097
|
+
return { username: d.username, sessionScoped: true };
|
|
2098
|
+
}
|
|
2027
2099
|
return { owner, sessionScoped: true };
|
|
2028
2100
|
}
|
|
2029
2101
|
// #20/#38: clipboard:write writes into the receiver's OS clipboard — route it to
|
|
@@ -2343,6 +2415,55 @@ export class WebServer extends EventEmitter {
|
|
|
2343
2415
|
description: 'Codex plan-usage refresh',
|
|
2344
2416
|
});
|
|
2345
2417
|
}
|
|
2418
|
+
// Custom Model Endpoint Profiles (docs/custom-model-endpoints-plan.md): keeps
|
|
2419
|
+
// each saved endpoint's discovered model list current with no manual
|
|
2420
|
+
// "Discover" click, so a model added on the server side (or one that drops
|
|
2421
|
+
// off) shows up in the Run-menu picker within one cycle. Best-effort per
|
|
2422
|
+
// endpoint (refreshAllCustomModelHosts skips one that's unreachable rather
|
|
2423
|
+
// than failing the sweep) and off in tests for the same reason the Codex
|
|
2424
|
+
// poll above is — no real network to hit, no server instance to keep alive.
|
|
2425
|
+
if (!this.testMode) {
|
|
2426
|
+
this.cleanup.setInterval(() => {
|
|
2427
|
+
// Reads the setting fresh on every tick, same reasoning as
|
|
2428
|
+
// readPlanUsageTelemetryEnabled() beside it: a live toggle takes effect
|
|
2429
|
+
// on the very next cycle, not just at server boot, and turning the
|
|
2430
|
+
// feature off actually stops the polling instead of only hiding the UI.
|
|
2431
|
+
void readCustomModelEndpointsEnabled()
|
|
2432
|
+
.then((enabled) => {
|
|
2433
|
+
if (!enabled)
|
|
2434
|
+
return;
|
|
2435
|
+
return refreshAllCustomModelHosts();
|
|
2436
|
+
})
|
|
2437
|
+
.catch((err) => {
|
|
2438
|
+
console.error('[custom-model] periodic re-discovery failed:', getErrorMessage(err));
|
|
2439
|
+
});
|
|
2440
|
+
}, CUSTOM_MODEL_REDISCOVER_INTERVAL_MS, { description: 'custom model endpoint re-discovery' });
|
|
2441
|
+
}
|
|
2442
|
+
// Custom Model Endpoint Profiles: the swap-conflict check on the apply/create routes
|
|
2443
|
+
// only ever runs at THAT session's own launch/apply moment — it cannot catch a LATER
|
|
2444
|
+
// eviction triggered by a different session's normal use, since llama-swap has no push
|
|
2445
|
+
// notification of its own and only swaps in response to a real inference request
|
|
2446
|
+
// (confirmed live: a session created while nothing else conflicted at that instant can
|
|
2447
|
+
// still get silently displaced afterward). This periodic sweep is what catches that
|
|
2448
|
+
// case after the fact and tells the displaced session's user, rather than leaving them
|
|
2449
|
+
// to discover it only when their next prompt behaves unexpectedly.
|
|
2450
|
+
if (!this.testMode) {
|
|
2451
|
+
this.cleanup.setInterval(() => {
|
|
2452
|
+
detectCustomModelSwapDisplacements(this.sessions.values(), this._customModelDisplacedNotified)
|
|
2453
|
+
.then((displacements) => {
|
|
2454
|
+
for (const displacement of displacements) {
|
|
2455
|
+
this.broadcast(SseEvent.CustomModelSwappedOut, displacement);
|
|
2456
|
+
}
|
|
2457
|
+
})
|
|
2458
|
+
.catch((err) => {
|
|
2459
|
+
console.error('[custom-model] swap-displacement check failed:', getErrorMessage(err));
|
|
2460
|
+
});
|
|
2461
|
+
// Same cadence, unrelated concern: close any /api/events tail (see
|
|
2462
|
+
// getLatestLlamaSwapLogLine) nothing has polled in a while, so a loading banner
|
|
2463
|
+
// that finished (or was abandoned) doesn't leave a connection open forever.
|
|
2464
|
+
pruneIdleLlamaSwapLogTails();
|
|
2465
|
+
}, CUSTOM_MODEL_SWAP_CHECK_INTERVAL_MS, { description: 'custom model swap-displacement check' });
|
|
2466
|
+
}
|
|
2346
2467
|
// Start scheduled runs cleanup timer
|
|
2347
2468
|
this.cleanup.setInterval(() => {
|
|
2348
2469
|
this.cleanupScheduledRuns();
|
|
@@ -2718,6 +2839,9 @@ export class WebServer extends EventEmitter {
|
|
|
2718
2839
|
console.log(`[Server] Found ${alive.length + discovered.length} alive mux session(s) from previous run`);
|
|
2719
2840
|
// For each alive mux session, create a Session object if it doesn't exist
|
|
2720
2841
|
const muxSessions = this.mux.getSessions();
|
|
2842
|
+
// Host-level config lives in remote-hosts.json, not in the persisted session
|
|
2843
|
+
// snapshot, so refresh the fields that only exist there (see the helper).
|
|
2844
|
+
const remoteHostsById = new Map((await readRemoteHosts(getDataDir())).map((host) => [host.id, host]));
|
|
2721
2845
|
for (const muxSession of muxSessions) {
|
|
2722
2846
|
if (!this.sessions.has(muxSession.sessionId)) {
|
|
2723
2847
|
// Restore session settings from state.json (single source of truth)
|
|
@@ -2792,7 +2916,9 @@ export class WebServer extends EventEmitter {
|
|
|
2792
2916
|
// respawn rebuilds a LOCAL command, breaking the pane and silently
|
|
2793
2917
|
// erasing `remote` from state.json on the next persist. mux-sessions.json
|
|
2794
2918
|
// round-trips MuxSession.remote; state.json carries SessionState.remote.
|
|
2795
|
-
remote
|
|
2919
|
+
// Host-level fields are refreshed from remote-hosts.json on top, or a
|
|
2920
|
+
// field added to the host config after launch would never arrive.
|
|
2921
|
+
remote: rehydrateRemoteHostFields(muxSession.remote ?? savedState?.remote, remoteHostsById),
|
|
2796
2922
|
// Docker metadata round-trips the same way (mux-sessions.json carries
|
|
2797
2923
|
// MuxSession.docker; state.json carries SessionState.docker), so recovery
|
|
2798
2924
|
// rebuilds the `docker exec` launch instead of a broken local command.
|
|
@@ -3137,6 +3263,9 @@ export class WebServer extends EventEmitter {
|
|
|
3137
3263
|
// port against the next start — the exact EADDRINUSE this feature already
|
|
3138
3264
|
// got wrong once.
|
|
3139
3265
|
void stopDeepSeekWeb();
|
|
3266
|
+
// Same teardown rule: the per-endpoint llama-swap log tails are otherwise closed
|
|
3267
|
+
// only by the periodic idle sweep, whose interval is disposed just below.
|
|
3268
|
+
closeAllLlamaSwapLogTails();
|
|
3140
3269
|
// Dispose all managed timers (intervals + resettable timeouts)
|
|
3141
3270
|
this.cleanup.dispose();
|
|
3142
3271
|
// Gracefully close all SSE connections and clear batching state
|
|
@@ -3146,6 +3275,10 @@ export class WebServer extends EventEmitter {
|
|
|
3146
3275
|
// response), so without this a 10-minute wait holds shutdown open.
|
|
3147
3276
|
sessionWaits.cancelEverything();
|
|
3148
3277
|
approvalInbox.stop();
|
|
3278
|
+
// Same reason as `cancelEverything` above: an in-flight wake is awaited by a request,
|
|
3279
|
+
// and `app.close()` (the last line of this method) does not abort in-flight requests —
|
|
3280
|
+
// so without this a restart during a wake waits out the readiness poll.
|
|
3281
|
+
this.remoteWake?.stop();
|
|
3149
3282
|
this.lastRecordedTokens.clear();
|
|
3150
3283
|
// Stop multiplexer and flush pending saves
|
|
3151
3284
|
this.mux.destroy();
|