@lifeaitools/clauth 1.30.26 → 2.0.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/.clauth-skill/references/keys-guide.md +270 -270
- package/README.md +2 -0
- package/cli/api.js +238 -238
- package/cli/commands/agent-pool.js +51 -15
- package/cli/commands/install.js +396 -396
- package/cli/commands/login.js +135 -0
- package/cli/commands/login.test.js +73 -0
- package/cli/commands/serve.js +290 -1555
- package/cli/commands/uninstall.js +164 -164
- package/cli/index.js +102 -1
- package/cli/supervisor-registry.js +106 -5
- package/cli/supervisor-registry.test.js +151 -0
- package/cli/webdav-service.js +339 -339
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +4 -5
- package/scripts/bootstrap.cjs +121 -121
- package/scripts/build.mjs +66 -0
- package/scripts/build.sh +5 -45
- package/scripts/postinstall.js +189 -189
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
package/cli/commands/serve.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
runSurfaceAction,
|
|
48
48
|
setPluginEnabled,
|
|
49
49
|
supervisorHealth,
|
|
50
|
+
operation,
|
|
50
51
|
} from "../supervisor-registry.js";
|
|
51
52
|
import {
|
|
52
53
|
AgentPool,
|
|
@@ -772,15 +773,13 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
772
773
|
.btn-ccandme{background:linear-gradient(135deg,#1a1a2e,#16213e);color:#a78bfa;border:1px solid #4c1d95;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:600;transition:all .15s;white-space:nowrap}
|
|
773
774
|
.btn-ccandme:hover{background:linear-gradient(135deg,#2d1b69,#1e1b4b);border-color:#7c3aed;color:#c4b5fd;transform:translateY(-1px)}
|
|
774
775
|
.btn-ccandme:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
775
|
-
.btn-tintin{background:linear-gradient(135deg,#103c2f,#0f2f3f);color:#86efac;border:1px solid #166534;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:700;transition:all .15s;white-space:nowrap}
|
|
776
|
-
.btn-tintin:hover{background:linear-gradient(135deg,#14532d,#155e75);border-color:#22c55e;color:#bbf7d0;transform:translateY(-1px)}
|
|
777
|
-
.btn-tintin:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
778
776
|
.btn-claude{background:linear-gradient(135deg,#d97706,#f59e0b);color:#0a0f1a;padding:8px 18px;font-size:.85rem;border-radius:7px;border:none;cursor:pointer;font-weight:700;letter-spacing:.3px;transition:all .15s;white-space:nowrap}
|
|
779
777
|
.btn-claude:hover{filter:brightness(1.1);transform:translateY(-1px)}
|
|
780
778
|
.btn-claude:disabled{opacity:.4;cursor:not-allowed;transform:none;filter:none}
|
|
781
779
|
.btn-tunnel-stop{background:#1e293b;color:#f87171;border:1px solid #334155;padding:6px 12px;font-size:.8rem;border-radius:6px;cursor:pointer;font-weight:500}
|
|
782
780
|
.btn-tunnel-stop:hover{background:#2d1f1f;border-color:#f87171}
|
|
783
781
|
.tunnel-err{font-size:.78rem;color:#f87171;width:100%;margin-top:4px}
|
|
782
|
+
.dash-panel{margin-bottom:1.25rem}
|
|
784
783
|
.build-panel{background:#0f1a2d;border:1px solid #1e3a5f;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1.25rem;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
785
784
|
.build-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
|
786
785
|
.build-dot.idle{background:#64748b}
|
|
@@ -791,25 +790,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
791
790
|
.build-label{font-size:.85rem;color:#94a3b8;flex:1}
|
|
792
791
|
.build-label strong{color:#e2e8f0}
|
|
793
792
|
.build-meta{font-family:'Courier New',monospace;font-size:.75rem;color:#64748b}
|
|
794
|
-
.tintin-tab{position:fixed;top:50%;right:0;transform:translateY(-50%);z-index:81;writing-mode:vertical-rl;text-orientation:mixed;background:linear-gradient(180deg,#103c2f,#0f2f3f);color:#86efac;border:1px solid #166534;border-right:none;border-radius:8px 0 0 8px;padding:14px 6px;font-size:.78rem;font-weight:700;cursor:pointer;letter-spacing:.5px;transition:all .2s}
|
|
795
|
-
.tintin-tab:hover{background:linear-gradient(180deg,#14532d,#155e75);color:#bbf7d0;padding-right:10px}
|
|
796
|
-
.tintin-tab.active{background:#081c1a;border-color:#22c55e;color:#bbf7d0}
|
|
797
|
-
.tintin-panel{position:fixed;top:0;right:-440px;bottom:0;z-index:80;width:min(420px,calc(100vw - 24px));background:#081c1a;border-left:1px solid #14532d;padding:0;box-shadow:-22px 0 60px rgba(0,0,0,.42);display:flex;flex-direction:column;transition:right .25s ease}
|
|
798
|
-
.tintin-panel.open{right:0}
|
|
799
|
-
.tintin-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:.8rem}
|
|
800
|
-
.tintin-title{font-size:.95rem;font-weight:700;color:#dcfce7}
|
|
801
|
-
.tintin-sub{font-size:.78rem;color:#6ee7b7;margin-top:2px}
|
|
802
|
-
.tintin-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
|
|
803
|
-
.tintin-field{display:flex;flex-direction:column;gap:4px}
|
|
804
|
-
.tintin-field label{font-size:.72rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;font-weight:700}
|
|
805
|
-
.tintin-input,.tintin-textarea{background:#031312;border:1px solid #14532d;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.85rem;padding:8px 10px;outline:none;transition:border-color .15s}
|
|
806
|
-
.tintin-input:focus,.tintin-textarea:focus{border-color:#22c55e}
|
|
807
|
-
.tintin-textarea{width:100%;min-height:92px;resize:vertical;line-height:1.45;margin-bottom:10px}
|
|
808
|
-
.tintin-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
809
|
-
.tintin-status{font-family:'Courier New',monospace;font-size:.78rem;color:#94a3b8}
|
|
810
|
-
.tintin-result{display:none;margin-top:12px;background:#020b0a;border:1px solid #134e4a;border-radius:8px;padding:10px;white-space:pre-wrap;word-break:break-word;font-family:'Courier New',monospace;font-size:.82rem;color:#bbf7d0;max-height:240px;overflow:auto}
|
|
811
|
-
.tintin-result.open{display:block}
|
|
812
|
-
@media (max-width:720px){.tintin-panel{width:100vw}.tintin-grid{grid-template-columns:1fr}}
|
|
813
793
|
.mcp-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
|
814
794
|
.mcp-label{font-size:.72rem;color:#64748b;min-width:80px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
|
|
815
795
|
.mcp-val{flex:1;font-family:'Courier New',monospace;font-size:.82rem;color:#60a5fa;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;padding:6px 10px;word-break:break-all;user-select:all}
|
|
@@ -936,10 +916,16 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
936
916
|
.supervisor-pill.warn{border-color:rgba(250,204,21,.35);color:#fde68a;background:rgba(250,204,21,.08)}
|
|
937
917
|
.supervisor-pill.bad{border-color:rgba(248,113,113,.35);color:#fecaca;background:rgba(248,113,113,.08)}
|
|
938
918
|
.supervisor-list{display:flex;flex-direction:column;gap:8px;max-height:320px;overflow:auto}
|
|
939
|
-
.supervisor-row{border:1px solid #1e293b;border-radius:8px;padding:8px;background:rgba(15,23,42,.45)}
|
|
919
|
+
.supervisor-row{border:1px solid #1e293b;border-radius:8px;padding:6px 8px;background:rgba(15,23,42,.45);cursor:pointer}
|
|
920
|
+
.supervisor-row:hover{border-color:#334155}
|
|
921
|
+
.supervisor-row.selected{border-color:#38bdf8;background:rgba(56,189,248,.08)}
|
|
922
|
+
.supervisor-row.readonly{cursor:default}
|
|
940
923
|
.supervisor-row-top{display:flex;justify-content:space-between;gap:8px;align-items:center}
|
|
941
924
|
.supervisor-name{font-size:.82rem;color:#e2e8f0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
942
925
|
.supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
|
|
926
|
+
.supervisor-cmdbar{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px;padding:8px;border:1px solid #1e293b;border-radius:8px;background:rgba(2,6,23,.4);min-height:36px}
|
|
927
|
+
.supervisor-cmdbar-empty{font-size:.74rem;color:#64748b}
|
|
928
|
+
.supervisor-cmdbar-name{font-size:.78rem;color:#e2e8f0;font-weight:600;margin-right:4px}
|
|
943
929
|
.supervisor-log{font-family:'Courier New',monospace;font-size:.7rem;color:#94a3b8;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow:auto}
|
|
944
930
|
.supervisor-feedback{margin-top:8px;padding:7px 9px;border-radius:6px;font-family:'Courier New',monospace;font-size:.72rem;color:#86efac;background:rgba(34,197,94,.08);border:1px solid rgba(74,222,128,.22);display:none}
|
|
945
931
|
.supervisor-feedback.bad{color:#fecaca;background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
|
|
@@ -982,13 +968,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
982
968
|
|
|
983
969
|
<!-- ── Main view (shown after unlock) ──────── -->
|
|
984
970
|
<div id="main-view">
|
|
985
|
-
<
|
|
986
|
-
<div class="upgrade-content">
|
|
987
|
-
<strong>⬆ clauth upgraded to v<span id="upgrade-to-version"></span></strong>
|
|
988
|
-
<span id="upgrade-details"></span>
|
|
989
|
-
<button onclick="dismissUpgrade()">Dismiss ✕</button>
|
|
990
|
-
</div>
|
|
991
|
-
</div>
|
|
971
|
+
<section class="dash-panel" data-panel="title-status">
|
|
992
972
|
<div class="header">
|
|
993
973
|
<div class="dot" id="dot"></div>
|
|
994
974
|
<h1>🔐 clauth vault <span style="font-size:0.55em;opacity:0.45;font-weight:400">v${VERSION}</span>${isStaged ? `<span style="font-size:0.5em;background:#b45309;color:#fef3c7;border-radius:4px;padding:2px 10px;margin-left:12px;font-weight:600;letter-spacing:.5px">STAGED</span>` : ""}</h1>
|
|
@@ -1007,6 +987,16 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1007
987
|
<div>Services: <span id="s-services">${whitelist ? whitelist.join(", ") : "all"}</span></div>
|
|
1008
988
|
<div>Failures: <span id="s-fails">—</span></div>
|
|
1009
989
|
</div>
|
|
990
|
+
</section>
|
|
991
|
+
|
|
992
|
+
<section class="dash-panel" data-panel="status-area">
|
|
993
|
+
<div id="upgrade-banner" style="display:none" class="upgrade-banner">
|
|
994
|
+
<div class="upgrade-content">
|
|
995
|
+
<strong>⬆ clauth upgraded to v<span id="upgrade-to-version"></span></strong>
|
|
996
|
+
<span id="upgrade-details"></span>
|
|
997
|
+
<button onclick="dismissUpgrade()">Dismiss ✕</button>
|
|
998
|
+
</div>
|
|
999
|
+
</div>
|
|
1010
1000
|
<div class="build-panel" id="build-panel">
|
|
1011
1001
|
<div class="build-dot idle" id="build-dot"></div>
|
|
1012
1002
|
<div class="build-label" id="build-label">
|
|
@@ -1015,8 +1005,9 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1015
1005
|
<span id="build-sha" style="font-family:'Courier New',monospace;font-size:.72rem;color:#60a5fa;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;padding:2px 8px;letter-spacing:.3px"></span>
|
|
1016
1006
|
<div class="build-meta" id="build-meta" style="width:100%;margin-top:4px"></div>
|
|
1017
1007
|
</div>
|
|
1008
|
+
</section>
|
|
1018
1009
|
|
|
1019
|
-
<div class="toolbar">
|
|
1010
|
+
<div class="toolbar" data-panel="menu-bar">
|
|
1020
1011
|
<button class="btn-refresh" onclick="loadServices()">↻ Refresh</button>
|
|
1021
1012
|
<button class="btn-add" onclick="toggleAddService()">+ Add Service</button>
|
|
1022
1013
|
<button class="btn-check" id="check-btn" onclick="checkAll()">⬤ Check All</button>
|
|
@@ -1108,7 +1099,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1108
1099
|
</form>
|
|
1109
1100
|
</div>
|
|
1110
1101
|
|
|
1111
|
-
<div class="tunnel-panel" id="tunnel-panel">
|
|
1102
|
+
<div class="tunnel-panel" id="tunnel-panel" data-panel="tunnel-bar">
|
|
1112
1103
|
<!-- not_configured -->
|
|
1113
1104
|
<div class="tunnel-state not-configured" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
|
|
1114
1105
|
<div class="tunnel-dot off"></div>
|
|
@@ -1149,39 +1140,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1149
1140
|
</div>
|
|
1150
1141
|
</div>
|
|
1151
1142
|
|
|
1152
|
-
<
|
|
1153
|
-
<div class="tintin-panel" id="tintin-panel">
|
|
1154
|
-
<div class="tintin-head" style="padding:12px 16px;border-bottom:1px solid #14532d;flex-shrink:0">
|
|
1155
|
-
<div style="display:flex;align-items:center;justify-content:space-between">
|
|
1156
|
-
<div class="tintin-title">TinTin</div>
|
|
1157
|
-
<button class="btn-cancel" onclick="toggleTinTinPanel()" style="font-size:16px;padding:2px 6px">x</button>
|
|
1158
|
-
</div>
|
|
1159
|
-
<div class="tintin-sub">claude-sonnet-4-6 · clauth CLI dispatch</div>
|
|
1160
|
-
<div style="margin-top:6px"><a href="/tintin/settings/ui" target="_blank" rel="noopener" style="font-size:.72rem;color:#86efac;text-decoration:none;border:1px solid #14532d;padding:2px 6px;border-radius:4px">Agent settings</a></div>
|
|
1161
|
-
</div>
|
|
1162
|
-
<div id="tintin-messages" style="flex:1;min-height:0;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px">
|
|
1163
|
-
<div style="font-size:.78rem;color:#6ee7b7;font-style:italic">Ask anything. Runs a Claude CLI agent with full clauth + repo context.</div>
|
|
1164
|
-
</div>
|
|
1165
|
-
<div style="padding:10px 16px;border-top:1px solid #14532d;flex-shrink:0">
|
|
1166
|
-
<div class="tintin-grid" style="margin-bottom:8px">
|
|
1167
|
-
<div class="tintin-field">
|
|
1168
|
-
<label>CWD</label>
|
|
1169
|
-
<input class="tintin-input" id="tintin-cwd" value="C:\\Dev\\regen-root" spellcheck="false" autocomplete="off">
|
|
1170
|
-
</div>
|
|
1171
|
-
<div class="tintin-field">
|
|
1172
|
-
<label>App</label>
|
|
1173
|
-
<input class="tintin-input" id="tintin-app" value="clauth" spellcheck="false" autocomplete="off">
|
|
1174
|
-
</div>
|
|
1175
|
-
</div>
|
|
1176
|
-
<textarea class="tintin-textarea" id="tintin-prompt" spellcheck="false" placeholder="Ask TinTin..." style="min-height:64px;margin-bottom:8px"></textarea>
|
|
1177
|
-
<div style="display:flex;gap:8px;align-items:center">
|
|
1178
|
-
<button class="btn-tintin" id="tintin-send" onclick="sendTinTinMessage()" style="flex:1">Send</button>
|
|
1179
|
-
<span class="tintin-status" id="tintin-status">idle</span>
|
|
1180
|
-
</div>
|
|
1181
|
-
</div>
|
|
1182
|
-
</div>
|
|
1183
|
-
|
|
1184
|
-
<section class="supervisor-panel" id="supervisor-panel">
|
|
1143
|
+
<section class="supervisor-panel" id="supervisor-panel" data-panel="surfaces-log">
|
|
1185
1144
|
<div class="supervisor-head">
|
|
1186
1145
|
<div>
|
|
1187
1146
|
<div class="supervisor-title">Local Software Factory Supervisor</div>
|
|
@@ -1199,6 +1158,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1199
1158
|
<h4>Surfaces</h4>
|
|
1200
1159
|
<div class="supervisor-kpi" id="supervisor-surface-count">—</div>
|
|
1201
1160
|
<div class="supervisor-meta" id="supervisor-surface-meta">destination + owner matrix</div>
|
|
1161
|
+
<div class="supervisor-cmdbar" id="supervisor-cmdbar"><span class="supervisor-cmdbar-empty">Select a surface below to act on it.</span></div>
|
|
1202
1162
|
<div class="supervisor-list" id="supervisor-surfaces" style="margin-top:8px"></div>
|
|
1203
1163
|
</div>
|
|
1204
1164
|
<div class="supervisor-card">
|
|
@@ -1211,7 +1171,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1211
1171
|
</section>
|
|
1212
1172
|
|
|
1213
1173
|
|
|
1214
|
-
<div class="tunnel-panel" id="webdav-panel" style="flex-direction:column;align-items:stretch;gap:8px">
|
|
1174
|
+
<div class="tunnel-panel" id="webdav-panel" data-panel="webdav-mounts" style="flex-direction:column;align-items:stretch;gap:8px">
|
|
1215
1175
|
<div style="display:flex;align-items:center;gap:10px;width:100%">
|
|
1216
1176
|
<div class="tunnel-dot off" id="webdav-dot"></div>
|
|
1217
1177
|
<strong style="color:#e2e8f0;font-size:.88rem">WebDAV Mounts</strong>
|
|
@@ -1247,6 +1207,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1247
1207
|
<div class="wizard-foot" id="wizard-foot"></div>
|
|
1248
1208
|
</div>
|
|
1249
1209
|
|
|
1210
|
+
<section class="dash-panel" data-panel="search-credentials">
|
|
1250
1211
|
<div id="project-tabs" class="project-tabs" style="display:none"></div>
|
|
1251
1212
|
<div id="service-search" class="service-search">
|
|
1252
1213
|
<span class="service-search-label">Search</span>
|
|
@@ -1254,7 +1215,8 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1254
1215
|
<span id="service-search-count" class="service-search-count"></span>
|
|
1255
1216
|
</div>
|
|
1256
1217
|
<div id="grid" class="grid"><p class="loading">Loading services…</p></div>
|
|
1257
|
-
|
|
1218
|
+
</section>
|
|
1219
|
+
<div class="footer" id="originFooter" data-panel="footer">checking origin… · 10-strike lockout</div>
|
|
1258
1220
|
</div>
|
|
1259
1221
|
|
|
1260
1222
|
<script>
|
|
@@ -1496,9 +1458,17 @@ function showMain(ping) {
|
|
|
1496
1458
|
loadWebdavMounts();
|
|
1497
1459
|
updateBuildStatus();
|
|
1498
1460
|
loadSupervisorCockpit();
|
|
1461
|
+
startSupervisorLogTail();
|
|
1499
1462
|
refreshWriteLockUi();
|
|
1500
1463
|
}
|
|
1501
1464
|
|
|
1465
|
+
let supervisorLogTailStarted = false;
|
|
1466
|
+
function startSupervisorLogTail() {
|
|
1467
|
+
if (supervisorLogTailStarted) return; // showMain() can run more than once per page load
|
|
1468
|
+
supervisorLogTailStarted = true;
|
|
1469
|
+
setInterval(loadSupervisorCockpit, 3000);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1502
1472
|
function supervisorBadge(text, kind) {
|
|
1503
1473
|
return '<span class="supervisor-pill ' + (kind || '') + '">' + htmlEscape(text) + '</span>';
|
|
1504
1474
|
}
|
|
@@ -1518,6 +1488,22 @@ function supervisorFeedback(text, bad) {
|
|
|
1518
1488
|
el.style.display = text ? "block" : "none";
|
|
1519
1489
|
}
|
|
1520
1490
|
|
|
1491
|
+
const SUPERVISOR_SURFACE_ACTIONS = ["start", "stop", "restart", "reconcile", "test", "promote", "rollback"];
|
|
1492
|
+
let selectedSupervisorSurface = null; // { id, name } | null
|
|
1493
|
+
let lastSupervisorSurfaceRows = [];
|
|
1494
|
+
|
|
1495
|
+
const CLAUTH_SELF_PSEUDO_SURFACE = {
|
|
1496
|
+
__pseudo: true,
|
|
1497
|
+
id: "clauth:mcp-sse",
|
|
1498
|
+
plugin_id: "clauth",
|
|
1499
|
+
name: "clauth (self)",
|
|
1500
|
+
lifecycle_owner: "external",
|
|
1501
|
+
destination: "self",
|
|
1502
|
+
state: "self_managed",
|
|
1503
|
+
port: null,
|
|
1504
|
+
health: null,
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1521
1507
|
async function loadSupervisorCockpit() {
|
|
1522
1508
|
const status = document.getElementById("supervisor-status");
|
|
1523
1509
|
if (status) status.textContent = "Loading supervisor state…";
|
|
@@ -1527,16 +1513,27 @@ async function loadSupervisorCockpit() {
|
|
|
1527
1513
|
supervisorJson("/v1/surfaces"),
|
|
1528
1514
|
supervisorJson("/v1/logs?limit=40"),
|
|
1529
1515
|
]);
|
|
1530
|
-
const surfaceRows = surfaces.surfaces || [];
|
|
1531
|
-
|
|
1516
|
+
const surfaceRows = [CLAUTH_SELF_PSEUDO_SURFACE, ...(surfaces.surfaces || [])];
|
|
1517
|
+
lastSupervisorSurfaceRows = surfaceRows;
|
|
1518
|
+
if (selectedSupervisorSurface && !surfaceRows.some(s => (s.plugin_id + ":" + s.id) === selectedSupervisorSurface.id)) {
|
|
1519
|
+
selectedSupervisorSurface = null;
|
|
1520
|
+
}
|
|
1521
|
+
document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length - 1);
|
|
1532
1522
|
document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
|
|
1533
1523
|
document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
|
|
1534
1524
|
document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
|
|
1535
|
-
document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.
|
|
1536
|
-
|
|
1525
|
+
document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.map(renderSupervisorSurface).join("");
|
|
1526
|
+
renderSupervisorCmdbar();
|
|
1527
|
+
const logEl = document.getElementById("supervisor-events");
|
|
1528
|
+
// Newest-at-bottom, like a real tail -- and only auto-scroll if the
|
|
1529
|
+
// reader was already at the bottom, so scrolling up to read history
|
|
1530
|
+
// during a poll tick doesn't get yanked back down.
|
|
1531
|
+
const wasAtBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 12;
|
|
1532
|
+
logEl.textContent = (logs.events || []).slice(-40).map(e => {
|
|
1537
1533
|
const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
|
|
1538
1534
|
return (e.ts || e.created_at || "") + " " + label;
|
|
1539
1535
|
}).join("\\n") || "No events yet.";
|
|
1536
|
+
if (wasAtBottom) logEl.scrollTop = logEl.scrollHeight;
|
|
1540
1537
|
if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status === "ok" ? "healthy" : (health.status || "unknown")), health.status === "ok" ? "ok" : "warn") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || "${VERSION}"), "");
|
|
1541
1538
|
} catch (err) {
|
|
1542
1539
|
if (status) status.innerHTML = supervisorBadge("supervisor unreachable", "bad") + " " + htmlEscape(err.message || err);
|
|
@@ -1544,13 +1541,55 @@ async function loadSupervisorCockpit() {
|
|
|
1544
1541
|
}
|
|
1545
1542
|
}
|
|
1546
1543
|
|
|
1544
|
+
// Derives an "Open UI" target for a surface without storing one on disk —
|
|
1545
|
+
// a real localhost port wins, else the first externally-routable URL.
|
|
1546
|
+
function openUiUrlForSurface(surface) {
|
|
1547
|
+
if (surface.port && surface.port !== "auto") return "http://127.0.0.1:" + surface.port + "/";
|
|
1548
|
+
const external = (surface.routes || []).find(r => r.kind === "external" && r.url);
|
|
1549
|
+
return external ? external.url : null;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function selectSupervisorSurface(compositeId, name) {
|
|
1553
|
+
selectedSupervisorSurface = selectedSupervisorSurface && selectedSupervisorSurface.id === compositeId
|
|
1554
|
+
? null // clicking the already-selected card deselects it
|
|
1555
|
+
: { id: compositeId, name };
|
|
1556
|
+
document.querySelectorAll(".supervisor-row[data-surface-id]").forEach(row => {
|
|
1557
|
+
row.classList.toggle("selected", row.dataset.surfaceId === (selectedSupervisorSurface && selectedSupervisorSurface.id));
|
|
1558
|
+
});
|
|
1559
|
+
renderSupervisorCmdbar();
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function renderSupervisorCmdbar() {
|
|
1563
|
+
const bar = document.getElementById("supervisor-cmdbar");
|
|
1564
|
+
if (!bar) return;
|
|
1565
|
+
if (!selectedSupervisorSurface) {
|
|
1566
|
+
bar.innerHTML = '<span class="supervisor-cmdbar-empty">Select a surface below to act on it.</span>';
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const { id, name } = selectedSupervisorSurface;
|
|
1570
|
+
bar.innerHTML = '<span class="supervisor-cmdbar-name">' + htmlEscape(name) + '</span>' +
|
|
1571
|
+
SUPERVISOR_SURFACE_ACTIONS.map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(id) + ',' + jsArg(a) + ')">' + a + '</button>').join("");
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1547
1574
|
function renderSupervisorSurface(surface) {
|
|
1575
|
+
const compositeId = surface.plugin_id + ":" + surface.id;
|
|
1548
1576
|
const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
|
|
1549
1577
|
const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
|
|
1550
|
-
|
|
1578
|
+
const openUrl = surface.__pseudo ? null : openUiUrlForSurface(surface);
|
|
1579
|
+
const isSelected = selectedSupervisorSurface && selectedSupervisorSurface.id === compositeId;
|
|
1580
|
+
const rowClass = "supervisor-row" + (surface.__pseudo ? " readonly" : "") + (isSelected ? " selected" : "");
|
|
1581
|
+
const onclick = surface.__pseudo ? "" : ' onclick="selectSupervisorSurface(' + jsArg(compositeId) + ',' + jsArg(surface.name || compositeId) + ')"';
|
|
1582
|
+
const openBtn = surface.__pseudo
|
|
1583
|
+
? '<button class="supervisor-action" onclick="event.stopPropagation();openClauthSelfMcp()" title="Open clauth\\'s own MCP endpoint">Open</button>'
|
|
1584
|
+
: (openUrl ? '<button class="supervisor-action" onclick="event.stopPropagation();window.open(' + jsArg(openUrl) + ',\\'_blank\\')" title="Open this surface\\'s UI">Open UI</button>' : "");
|
|
1585
|
+
return '<div class="' + rowClass + '" data-surface-id="' + htmlEscape(compositeId) + '"' + onclick + '>' +
|
|
1586
|
+
'<div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.name || compositeId) + '</span>' +
|
|
1587
|
+
supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) +
|
|
1588
|
+
openBtn +
|
|
1589
|
+
'</div>' +
|
|
1551
1590
|
'<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
|
|
1552
1591
|
'<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
|
|
1553
|
-
'
|
|
1592
|
+
'</div>';
|
|
1554
1593
|
}
|
|
1555
1594
|
|
|
1556
1595
|
async function rescanSupervisorPlugins() {
|
|
@@ -1570,6 +1609,21 @@ async function runSupervisorSurface(id, action) {
|
|
|
1570
1609
|
} catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
|
|
1571
1610
|
}
|
|
1572
1611
|
|
|
1612
|
+
// clauth doesn't manage itself as a plugin, so its own MCP endpoint has no
|
|
1613
|
+
// entry in the real surfaces registry — this reads the vault value directly
|
|
1614
|
+
// (never hardcoded) rather than storing a synthetic surfaces row.
|
|
1615
|
+
async function openClauthSelfMcp() {
|
|
1616
|
+
try {
|
|
1617
|
+
const res = await fetch(BASE + "/v/mcp-clauth-endpoint");
|
|
1618
|
+
const text = (await res.text()).trim();
|
|
1619
|
+
if (!res.ok) {
|
|
1620
|
+
supervisorFeedback("Could not read clauth's own MCP endpoint: " + text, true);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
window.open(text, "_blank");
|
|
1624
|
+
} catch (err) { supervisorFeedback("Could not read clauth's own MCP endpoint: " + (err.message || err), true); }
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1573
1627
|
// ── Unlock ──────────────────────────────────
|
|
1574
1628
|
async function unlock() {
|
|
1575
1629
|
const input = document.getElementById("lock-input");
|
|
@@ -1743,124 +1797,6 @@ async function launchCCandMe() {
|
|
|
1743
1797
|
}
|
|
1744
1798
|
}
|
|
1745
1799
|
|
|
1746
|
-
// ── TinTin local CLI dispatch sidebar ──
|
|
1747
|
-
let tintinPollTimer = null;
|
|
1748
|
-
|
|
1749
|
-
function toggleTinTinPanel() {
|
|
1750
|
-
const panel = document.getElementById("tintin-panel");
|
|
1751
|
-
const tab = document.getElementById("tintin-tab");
|
|
1752
|
-
if (!panel) return;
|
|
1753
|
-
panel.classList.toggle("open");
|
|
1754
|
-
if (tab) tab.classList.toggle("active");
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
function fillTinTinSmoke() {
|
|
1758
|
-
const prompt = document.getElementById("tintin-prompt");
|
|
1759
|
-
const app = document.getElementById("tintin-app");
|
|
1760
|
-
if (app) app.value = "clauth-dashboard";
|
|
1761
|
-
if (prompt) prompt.value = "Reply exactly: tintin-dashboard-ok";
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
function setTinTinStatus(text) {
|
|
1765
|
-
const el = document.getElementById("tintin-status");
|
|
1766
|
-
if (el) el.textContent = text || "";
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
function appendTinTinMessage(role, text) {
|
|
1770
|
-
const container = document.getElementById("tintin-messages");
|
|
1771
|
-
if (!container || !text) return;
|
|
1772
|
-
const bubble = document.createElement("div");
|
|
1773
|
-
bubble.style.cssText = role === "user"
|
|
1774
|
-
? "background:#14532d;border:1px solid #166534;border-radius:8px;padding:8px 10px;font-size:.82rem;color:#bbf7d0;white-space:pre-wrap;word-break:break-word;align-self:flex-end;max-width:90%"
|
|
1775
|
-
: "background:#0a1f1a;border:1px solid #134e4a;border-radius:8px;padding:8px 10px;font-size:.82rem;color:#86efac;font-family:'Courier New',monospace;white-space:pre-wrap;word-break:break-word;max-width:95%;max-height:300px;overflow:auto";
|
|
1776
|
-
bubble.textContent = text;
|
|
1777
|
-
container.appendChild(bubble);
|
|
1778
|
-
container.scrollTop = container.scrollHeight;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
function setTinTinResult(text, open) {
|
|
1782
|
-
if (open && text) appendTinTinMessage("assistant", text);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
async function sendTinTinMessage() {
|
|
1786
|
-
const sendBtn = document.getElementById("tintin-send");
|
|
1787
|
-
const promptEl = document.getElementById("tintin-prompt");
|
|
1788
|
-
const appEl = document.getElementById("tintin-app");
|
|
1789
|
-
const cwdEl = document.getElementById("tintin-cwd");
|
|
1790
|
-
const prompt = (promptEl && promptEl.value || "").trim();
|
|
1791
|
-
const appSlug = (appEl && appEl.value || "clauth-dashboard").trim() || "clauth-dashboard";
|
|
1792
|
-
const cwd = (cwdEl && cwdEl.value || "").trim();
|
|
1793
|
-
if (!prompt) {
|
|
1794
|
-
setTinTinStatus("prompt required");
|
|
1795
|
-
return;
|
|
1796
|
-
}
|
|
1797
|
-
if (tintinPollTimer) {
|
|
1798
|
-
clearTimeout(tintinPollTimer);
|
|
1799
|
-
tintinPollTimer = null;
|
|
1800
|
-
}
|
|
1801
|
-
const jobId = "tintin-" + Date.now();
|
|
1802
|
-
const body = {
|
|
1803
|
-
prompt,
|
|
1804
|
-
job_id: jobId,
|
|
1805
|
-
cwd,
|
|
1806
|
-
agent_context: {
|
|
1807
|
-
app: { slug: appSlug, route: "/clauth-dashboard", origin: window.location.origin },
|
|
1808
|
-
repo: { root: cwd, cwd },
|
|
1809
|
-
runtime: { agent: "clauth-cli", requested_by: "clauth-dashboard", model: "claude-sonnet-4-6" },
|
|
1810
|
-
task: { intent: "general_chat", thread_id: jobId }
|
|
1811
|
-
}
|
|
1812
|
-
};
|
|
1813
|
-
|
|
1814
|
-
appendTinTinMessage("user", prompt);
|
|
1815
|
-
if (promptEl) promptEl.value = "";
|
|
1816
|
-
if (sendBtn) { sendBtn.disabled = true; sendBtn.textContent = "Dispatching..."; }
|
|
1817
|
-
setTinTinStatus("dispatching");
|
|
1818
|
-
try {
|
|
1819
|
-
const response = await fetch(BASE + "/tintin/dispatch", {
|
|
1820
|
-
method: "POST",
|
|
1821
|
-
headers: { "Content-Type": "application/json" },
|
|
1822
|
-
body: JSON.stringify(body)
|
|
1823
|
-
});
|
|
1824
|
-
const result = await response.json();
|
|
1825
|
-
if (!response.ok || result.error) {
|
|
1826
|
-
setTinTinStatus("dispatch failed");
|
|
1827
|
-
setTinTinResult(JSON.stringify(result, null, 2), true);
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
setTinTinStatus("spawned pid " + result.pid + " - polling");
|
|
1831
|
-
pollTinTinJob(jobId, 0);
|
|
1832
|
-
} catch (err) {
|
|
1833
|
-
setTinTinStatus("dispatch error");
|
|
1834
|
-
setTinTinResult(err && err.message ? err.message : String(err), true);
|
|
1835
|
-
} finally {
|
|
1836
|
-
if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = "Dispatch"; }
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
async function pollTinTinJob(jobId, attempt) {
|
|
1841
|
-
try {
|
|
1842
|
-
const response = await fetch(BASE + "/tintin/dispatch/" + encodeURIComponent(jobId), { cache: "no-store" });
|
|
1843
|
-
const job = await response.json();
|
|
1844
|
-
if (!response.ok || job.error) {
|
|
1845
|
-
setTinTinStatus("job lookup failed");
|
|
1846
|
-
setTinTinResult(JSON.stringify(job, null, 2), true);
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
if (job.status === "running" && attempt < 60) {
|
|
1850
|
-
setTinTinStatus("running pid " + (job.pid || "?") + " - " + (attempt + 1));
|
|
1851
|
-
tintinPollTimer = setTimeout(function() { pollTinTinJob(jobId, attempt + 1); }, 1500);
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
setTinTinStatus(job.status === "completed" ? "done" : job.status);
|
|
1855
|
-
const out = (job.stdout || "").trim();
|
|
1856
|
-
if (out) appendTinTinMessage("assistant", out);
|
|
1857
|
-
if (job.stderr && job.stderr.trim()) appendTinTinMessage("assistant", "stderr: " + job.stderr.trim());
|
|
1858
|
-
} catch (err) {
|
|
1859
|
-
setTinTinStatus("poll error");
|
|
1860
|
-
setTinTinResult(err && err.message ? err.message : String(err), true);
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
1800
|
// ── Restart daemon (keeps boot.key — vault stays unlocked) ──
|
|
1865
1801
|
async function restartDaemon() {
|
|
1866
1802
|
if (!confirm("Restart the daemon?\\n\\nThe vault will stay unlocked (boot.key kept).")) return;
|
|
@@ -1932,8 +1868,16 @@ function switchProjectTab(key) {
|
|
|
1932
1868
|
renderServiceGrid(allServices);
|
|
1933
1869
|
}
|
|
1934
1870
|
|
|
1871
|
+
const MCP_SURFACE_DUPLICATE_SERVICES = new Set([
|
|
1872
|
+
"mcp-clauth-endpoint",
|
|
1873
|
+
"mcp-fs-endpoint",
|
|
1874
|
+
"mcp-regen-media-endpoint",
|
|
1875
|
+
"mcp-web-research-endpoint",
|
|
1876
|
+
]);
|
|
1877
|
+
|
|
1935
1878
|
function renderServiceGrid(services) {
|
|
1936
1879
|
const grid = document.getElementById("grid");
|
|
1880
|
+
services = services.filter(s => !MCP_SURFACE_DUPLICATE_SERVICES.has(s.name));
|
|
1937
1881
|
let filtered = services;
|
|
1938
1882
|
if (activeProjectTab === "unassigned") {
|
|
1939
1883
|
filtered = services.filter(s => !s.project);
|
|
@@ -3326,11 +3270,6 @@ setInterval(async () => {
|
|
|
3326
3270
|
}, 5000);
|
|
3327
3271
|
|
|
3328
3272
|
boot();
|
|
3329
|
-
|
|
3330
|
-
const tintinSidebarScript = document.createElement("script");
|
|
3331
|
-
tintinSidebarScript.src = "/tintin/sidebar.user.js";
|
|
3332
|
-
tintinSidebarScript.defer = true;
|
|
3333
|
-
document.body.appendChild(tintinSidebarScript);
|
|
3334
3273
|
</script>
|
|
3335
3274
|
</body>
|
|
3336
3275
|
</html>`;
|
|
@@ -3353,561 +3292,6 @@ function clauthConfigDir() {
|
|
|
3353
3292
|
return path.join(os.homedir(), ".config", "clauth");
|
|
3354
3293
|
}
|
|
3355
3294
|
|
|
3356
|
-
function tintinConfigPath() {
|
|
3357
|
-
return process.env.CLAUTH_MONKEY_CONFIG || path.join(clauthConfigDir(), "monkey.config.json");
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
function defaultTinTinConfig() {
|
|
3361
|
-
return {
|
|
3362
|
-
schema_version: 3,
|
|
3363
|
-
trust_mode: process.env.CLAUTH_MONKEY_TRUST_MODE || "open-local",
|
|
3364
|
-
agent_settings: {
|
|
3365
|
-
default_repo_root: process.env.CLAUTH_TINTIN_REPO_ROOT || "C:\\Dev\\regen-root",
|
|
3366
|
-
worktree_root: process.env.CLAUTH_TINTIN_WORKTREE_ROOT || "C:\\Dev\\regen-root.wt",
|
|
3367
|
-
base_branch: process.env.CLAUTH_TINTIN_BASE_BRANCH || "develop",
|
|
3368
|
-
isolation: process.env.CLAUTH_TINTIN_ISOLATION || "worktree",
|
|
3369
|
-
launch_mode: "setup_only",
|
|
3370
|
-
agents: {
|
|
3371
|
-
claude: {
|
|
3372
|
-
runtime: "claude",
|
|
3373
|
-
model: process.env.CLAUTH_TINTIN_CLAUDE_MODEL || "claude-sonnet-4-6",
|
|
3374
|
-
command: process.env.CLAUDE_BIN || "claude",
|
|
3375
|
-
enabled: true,
|
|
3376
|
-
},
|
|
3377
|
-
codex: {
|
|
3378
|
-
runtime: "codex",
|
|
3379
|
-
model: process.env.CLAUTH_TINTIN_CODEX_MODEL || "gpt-5-codex",
|
|
3380
|
-
command: process.env.CODEX_BIN || "codex",
|
|
3381
|
-
enabled: true,
|
|
3382
|
-
},
|
|
3383
|
-
},
|
|
3384
|
-
},
|
|
3385
|
-
sidebar_settings: {
|
|
3386
|
-
enabled: true,
|
|
3387
|
-
default_app_slug: "tintin-console",
|
|
3388
|
-
session_ttl_hours: 24,
|
|
3389
|
-
event_history_limit: 200,
|
|
3390
|
-
},
|
|
3391
|
-
dispatch_settings: {
|
|
3392
|
-
enabled: true,
|
|
3393
|
-
default_agent: "claude",
|
|
3394
|
-
default_model: process.env.CLAUTH_TINTIN_CLAUDE_MODEL || "claude-sonnet-4-6",
|
|
3395
|
-
max_concurrent_workers: 2,
|
|
3396
|
-
},
|
|
3397
|
-
codevelop_settings: {
|
|
3398
|
-
enabled: true,
|
|
3399
|
-
default_repo: process.env.CLAUTH_TINTIN_REPO_ROOT || "C:\\Dev\\regen-root",
|
|
3400
|
-
message_retention_limit: 500,
|
|
3401
|
-
require_peer_join: true,
|
|
3402
|
-
},
|
|
3403
|
-
agent_sessions: [],
|
|
3404
|
-
apps: [
|
|
3405
|
-
{
|
|
3406
|
-
slug: "rdc-marketing-engine",
|
|
3407
|
-
origins: [
|
|
3408
|
-
"http://localhost:3000",
|
|
3409
|
-
"http://127.0.0.1:3000",
|
|
3410
|
-
"https://app.regendevcorp.com",
|
|
3411
|
-
"https://rdc-marketing-engine.dev.regendevcorp.com",
|
|
3412
|
-
],
|
|
3413
|
-
manifest_url: "https://app.regendevcorp.com/.well-known/monkey.json",
|
|
3414
|
-
cwd: "C:\\Dev\\regen-root",
|
|
3415
|
-
capabilities: ["general_chat", "skill_request", "handoff"],
|
|
3416
|
-
},
|
|
3417
|
-
],
|
|
3418
|
-
};
|
|
3419
|
-
}
|
|
3420
|
-
|
|
3421
|
-
function mergeTinTinAgentSettings(input = {}) {
|
|
3422
|
-
const defaults = defaultTinTinConfig().agent_settings;
|
|
3423
|
-
const incoming = input && typeof input === "object" ? input : {};
|
|
3424
|
-
return {
|
|
3425
|
-
...defaults,
|
|
3426
|
-
...incoming,
|
|
3427
|
-
agents: {
|
|
3428
|
-
...defaults.agents,
|
|
3429
|
-
...(incoming.agents && typeof incoming.agents === "object" ? incoming.agents : {}),
|
|
3430
|
-
},
|
|
3431
|
-
};
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
|
-
function mergeTinTinSectionSettings(sectionName, input = {}) {
|
|
3435
|
-
const defaults = defaultTinTinConfig()[sectionName] || {};
|
|
3436
|
-
const incoming = input && typeof input === "object" ? input : {};
|
|
3437
|
-
return { ...defaults, ...incoming };
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
function normalizeTinTinConfig(input = {}) {
|
|
3441
|
-
const defaults = defaultTinTinConfig();
|
|
3442
|
-
return {
|
|
3443
|
-
schema_version: Math.max(Number(input.schema_version || 0), defaults.schema_version),
|
|
3444
|
-
trust_mode: typeof input.trust_mode === "string" ? input.trust_mode : defaults.trust_mode,
|
|
3445
|
-
agent_settings: mergeTinTinAgentSettings(input.agent_settings),
|
|
3446
|
-
sidebar_settings: mergeTinTinSectionSettings("sidebar_settings", input.sidebar_settings),
|
|
3447
|
-
dispatch_settings: mergeTinTinSectionSettings("dispatch_settings", input.dispatch_settings),
|
|
3448
|
-
codevelop_settings: mergeTinTinSectionSettings("codevelop_settings", input.codevelop_settings),
|
|
3449
|
-
agent_sessions: Array.isArray(input.agent_sessions) ? input.agent_sessions : defaults.agent_sessions,
|
|
3450
|
-
apps: Array.isArray(input.apps) ? input.apps : defaults.apps,
|
|
3451
|
-
};
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
function loadTinTinConfig() {
|
|
3455
|
-
const configPath = tintinConfigPath();
|
|
3456
|
-
try {
|
|
3457
|
-
if (!fs.existsSync(configPath)) return { ...normalizeTinTinConfig(), path: configPath, exists: false };
|
|
3458
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
3459
|
-
return {
|
|
3460
|
-
...normalizeTinTinConfig(parsed),
|
|
3461
|
-
path: configPath,
|
|
3462
|
-
exists: true,
|
|
3463
|
-
};
|
|
3464
|
-
} catch (err) {
|
|
3465
|
-
return {
|
|
3466
|
-
...normalizeTinTinConfig(),
|
|
3467
|
-
path: configPath,
|
|
3468
|
-
exists: false,
|
|
3469
|
-
error: err.message,
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
|
|
3474
|
-
function saveTinTinConfig(input = {}) {
|
|
3475
|
-
const configPath = tintinConfigPath();
|
|
3476
|
-
const next = normalizeTinTinConfig(input);
|
|
3477
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
3478
|
-
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
3479
|
-
return { ...next, path: configPath, exists: true };
|
|
3480
|
-
}
|
|
3481
|
-
|
|
3482
|
-
function slugPart(value, fallback = "session") {
|
|
3483
|
-
return String(value || fallback).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || fallback;
|
|
3484
|
-
}
|
|
3485
|
-
|
|
3486
|
-
function assertInside(parent, child) {
|
|
3487
|
-
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
3488
|
-
return rel && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
function gitTopLevel(repoRoot) {
|
|
3492
|
-
try {
|
|
3493
|
-
return execSyncTop("git rev-parse --show-toplevel", {
|
|
3494
|
-
cwd: repoRoot,
|
|
3495
|
-
encoding: "utf8",
|
|
3496
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3497
|
-
windowsHide: true,
|
|
3498
|
-
}).trim();
|
|
3499
|
-
} catch {
|
|
3500
|
-
return null;
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
|
|
3504
|
-
function createTinTinAgentSession(input = {}) {
|
|
3505
|
-
const config = loadTinTinConfig();
|
|
3506
|
-
const settings = mergeTinTinAgentSettings(config.agent_settings);
|
|
3507
|
-
const agentKey = slugPart(input.agent || input.runtime || "claude", "claude");
|
|
3508
|
-
const agent = settings.agents?.[agentKey] || { runtime: agentKey, model: input.model || "", command: agentKey, enabled: true };
|
|
3509
|
-
if (agent.enabled === false) return { ok: false, error: "agent_disabled", agent: agentKey };
|
|
3510
|
-
|
|
3511
|
-
const sessionId = slugPart(input.session_id || makeTinTinId("agent"), "agent");
|
|
3512
|
-
const repoRoot = path.resolve(String(input.repo_root || settings.default_repo_root || settings.cwd || process.cwd()));
|
|
3513
|
-
const top = gitTopLevel(repoRoot);
|
|
3514
|
-
if (!top) return { ok: false, error: "not_git_repo", repo_root: repoRoot };
|
|
3515
|
-
|
|
3516
|
-
const isolation = String(input.isolation || settings.isolation || "worktree");
|
|
3517
|
-
const baseBranch = slugPart(input.base_branch || settings.base_branch || "develop", "develop");
|
|
3518
|
-
let cwd = top;
|
|
3519
|
-
let branch = null;
|
|
3520
|
-
let worktreePath = null;
|
|
3521
|
-
let worktreeCreated = false;
|
|
3522
|
-
const commands = [];
|
|
3523
|
-
|
|
3524
|
-
if (isolation === "worktree") {
|
|
3525
|
-
const worktreeRoot = path.resolve(String(input.worktree_root || settings.worktree_root || path.join(path.dirname(top), `${path.basename(top)}.wt`)));
|
|
3526
|
-
fs.mkdirSync(worktreeRoot, { recursive: true });
|
|
3527
|
-
worktreePath = path.join(worktreeRoot, `tintin-${agentKey}-${sessionId}`);
|
|
3528
|
-
if (!assertInside(worktreeRoot, worktreePath)) return { ok: false, error: "worktree_path_escape", worktree_root: worktreeRoot, worktree_path: worktreePath };
|
|
3529
|
-
branch = `wt/tintin/${agentKey}/${sessionId}`;
|
|
3530
|
-
if (!fs.existsSync(worktreePath)) {
|
|
3531
|
-
execSyncTop(`git worktree add -b ${JSON.stringify(branch)} ${JSON.stringify(worktreePath)} ${JSON.stringify(baseBranch)}`, {
|
|
3532
|
-
cwd: top,
|
|
3533
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3534
|
-
timeout: 60_000,
|
|
3535
|
-
windowsHide: true,
|
|
3536
|
-
});
|
|
3537
|
-
worktreeCreated = true;
|
|
3538
|
-
commands.push(`git worktree add -b ${branch} ${worktreePath} ${baseBranch}`);
|
|
3539
|
-
}
|
|
3540
|
-
cwd = worktreePath;
|
|
3541
|
-
} else if (isolation !== "cwd") {
|
|
3542
|
-
return { ok: false, error: "unsupported_isolation", isolation };
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
const runtime = agent.runtime || agentKey;
|
|
3546
|
-
const command = agent.command || runtime;
|
|
3547
|
-
const model = input.model || agent.model || "";
|
|
3548
|
-
const launchCommand = runtime === "codex"
|
|
3549
|
-
? `${command} exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check -C ${JSON.stringify(cwd)} -`
|
|
3550
|
-
: `${command} -p <prompt> --dangerously-skip-permissions`;
|
|
3551
|
-
|
|
3552
|
-
const result = {
|
|
3553
|
-
ok: true,
|
|
3554
|
-
agent_session_id: sessionId,
|
|
3555
|
-
agent: agentKey,
|
|
3556
|
-
runtime,
|
|
3557
|
-
model,
|
|
3558
|
-
isolation,
|
|
3559
|
-
repo_root: top,
|
|
3560
|
-
cwd,
|
|
3561
|
-
worktree_root: isolation === "worktree" ? path.dirname(worktreePath) : null,
|
|
3562
|
-
worktree_path: worktreePath,
|
|
3563
|
-
worktree_created: worktreeCreated,
|
|
3564
|
-
branch,
|
|
3565
|
-
base_branch: baseBranch,
|
|
3566
|
-
launch_mode: settings.launch_mode || "setup_only",
|
|
3567
|
-
launch_command: launchCommand,
|
|
3568
|
-
commands,
|
|
3569
|
-
};
|
|
3570
|
-
const sessionRecord = {
|
|
3571
|
-
agent_session_id: result.agent_session_id,
|
|
3572
|
-
agent: result.agent,
|
|
3573
|
-
runtime: result.runtime,
|
|
3574
|
-
model: result.model,
|
|
3575
|
-
isolation: result.isolation,
|
|
3576
|
-
repo_root: result.repo_root,
|
|
3577
|
-
cwd: result.cwd,
|
|
3578
|
-
worktree_path: result.worktree_path,
|
|
3579
|
-
branch: result.branch,
|
|
3580
|
-
base_branch: result.base_branch,
|
|
3581
|
-
launch_mode: result.launch_mode,
|
|
3582
|
-
created_at: new Date().toISOString(),
|
|
3583
|
-
};
|
|
3584
|
-
const sessions = [sessionRecord, ...(Array.isArray(config.agent_sessions) ? config.agent_sessions : [])
|
|
3585
|
-
.filter((item) => item && item.agent_session_id !== sessionId)]
|
|
3586
|
-
.slice(0, 100);
|
|
3587
|
-
saveTinTinConfig({ ...config, agent_sessions: sessions });
|
|
3588
|
-
return result;
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
function isLoopbackOrigin(origin) {
|
|
3592
|
-
if (!origin) return false;
|
|
3593
|
-
try {
|
|
3594
|
-
const parsed = new URL(origin);
|
|
3595
|
-
return ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
|
|
3596
|
-
} catch {
|
|
3597
|
-
return false;
|
|
3598
|
-
}
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
|
-
function tintinAppForOrigin(config, origin) {
|
|
3602
|
-
if (!origin) return null;
|
|
3603
|
-
return (config.apps || []).find((app) => (
|
|
3604
|
-
Array.isArray(app.origins) && app.origins.some((allowed) => allowed === origin || allowed === "*")
|
|
3605
|
-
)) || null;
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
|
-
function checkTinTinBrowserAccess(req) {
|
|
3609
|
-
const config = loadTinTinConfig();
|
|
3610
|
-
const origin = req.headers.origin || "";
|
|
3611
|
-
const trustMode = config.trust_mode || "open-local";
|
|
3612
|
-
const matchedApp = tintinAppForOrigin(config, origin);
|
|
3613
|
-
|
|
3614
|
-
if (trustMode === "disabled") {
|
|
3615
|
-
return { allowed: false, config, reason: "monkey_disabled" };
|
|
3616
|
-
}
|
|
3617
|
-
if (!origin) {
|
|
3618
|
-
return { allowed: true, config, app: null, reason: "non_browser_local" };
|
|
3619
|
-
}
|
|
3620
|
-
if (matchedApp) {
|
|
3621
|
-
return { allowed: true, config, app: matchedApp, reason: "configured_origin" };
|
|
3622
|
-
}
|
|
3623
|
-
if (trustMode === "open-local" && isLoopbackOrigin(origin)) {
|
|
3624
|
-
return { allowed: true, config, app: null, reason: "open_local_loopback" };
|
|
3625
|
-
}
|
|
3626
|
-
return { allowed: false, config, reason: "origin_not_allowed", origin };
|
|
3627
|
-
}
|
|
3628
|
-
|
|
3629
|
-
function publicTinTinConfig(config) {
|
|
3630
|
-
return {
|
|
3631
|
-
schema_version: config.schema_version || 1,
|
|
3632
|
-
trust_mode: config.trust_mode || "open-local",
|
|
3633
|
-
config_path: config.path,
|
|
3634
|
-
config_exists: !!config.exists,
|
|
3635
|
-
config_error: config.error || null,
|
|
3636
|
-
agent_settings: mergeTinTinAgentSettings(config.agent_settings),
|
|
3637
|
-
sidebar_settings: mergeTinTinSectionSettings("sidebar_settings", config.sidebar_settings),
|
|
3638
|
-
dispatch_settings: mergeTinTinSectionSettings("dispatch_settings", config.dispatch_settings),
|
|
3639
|
-
codevelop_settings: mergeTinTinSectionSettings("codevelop_settings", config.codevelop_settings),
|
|
3640
|
-
agent_sessions: Array.isArray(config.agent_sessions) ? config.agent_sessions : [],
|
|
3641
|
-
apps: (config.apps || []).map((app) => ({
|
|
3642
|
-
slug: app.slug,
|
|
3643
|
-
origins: app.origins || [],
|
|
3644
|
-
manifest_url: app.manifest_url || null,
|
|
3645
|
-
cwd: app.cwd || null,
|
|
3646
|
-
capabilities: app.capabilities || [],
|
|
3647
|
-
})),
|
|
3648
|
-
};
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3651
|
-
function tintinSettingsHtml() {
|
|
3652
|
-
return `<!DOCTYPE html>
|
|
3653
|
-
<html lang="en">
|
|
3654
|
-
<head>
|
|
3655
|
-
<meta charset="utf-8">
|
|
3656
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3657
|
-
<title>clauth TinTin Settings</title>
|
|
3658
|
-
<style>
|
|
3659
|
-
body{margin:0;background:#07100e;color:#d9fbe8;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
|
3660
|
-
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid #14352f;background:#091814}
|
|
3661
|
-
h1{font-size:20px;margin:0}.sub{font-size:12px;color:#8dd8c2;margin-top:3px}.pill{display:inline-flex;align-items:center;border:1px solid #14532d;color:#86efac;padding:2px 7px;font-size:11px;margin-top:7px}
|
|
3662
|
-
main{display:grid;grid-template-columns:minmax(300px,430px) 1fr;gap:14px;padding:14px}
|
|
3663
|
-
.stack{display:grid;gap:14px}.wide{display:grid;gap:14px}
|
|
3664
|
-
section{border:1px solid #14352f;background:#091814;padding:14px}
|
|
3665
|
-
h2{font-size:15px;margin:0 0 10px;color:#dcfce7}
|
|
3666
|
-
label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#8dd8c2;font-weight:700;margin:10px 0 4px}
|
|
3667
|
-
input,select,textarea{box-sizing:border-box;width:100%;border:1px solid #1d4d43;background:#031312;color:#d9fbe8;padding:8px 9px;font:13px ui-monospace,SFMono-Regular,Consolas,monospace;outline:none}
|
|
3668
|
-
textarea{min-height:170px;resize:vertical}.short{min-height:92px}.tiny{min-height:58px}
|
|
3669
|
-
.row{display:grid;grid-template-columns:1fr 1fr;gap:8px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
|
3670
|
-
button,a.btn{border:1px solid #2d6a5e;background:#08231f;color:#b8ffe1;padding:7px 10px;font-size:12px;font-weight:700;text-decoration:none;cursor:pointer}
|
|
3671
|
-
button:hover,a.btn:hover{background:#0f2a24}.danger{border-color:#5f3b3b;color:#fecaca;background:#241010}
|
|
3672
|
-
pre{white-space:pre-wrap;word-break:break-word;border:1px solid #0d2a25;background:#020b09;color:#c8f9df;padding:10px;max-height:460px;overflow:auto;font-size:12px}
|
|
3673
|
-
.status{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:#86efac}.hint{font-size:12px;color:#7dd3c0;line-height:1.4}.check{display:flex;align-items:center;gap:8px;margin-top:10px;color:#d9fbe8;font-size:13px}.check input{width:auto}
|
|
3674
|
-
@media(max-width:900px){main{grid-template-columns:1fr}.row{grid-template-columns:1fr}}
|
|
3675
|
-
</style>
|
|
3676
|
-
</head>
|
|
3677
|
-
<body>
|
|
3678
|
-
<header>
|
|
3679
|
-
<div><h1>TinTin Settings</h1><div class="sub">Persistent clauth control plane for agent setup, app sidebars, blackboard dispatch, and co-develop relay.</div><div class="pill">GET/PUT /tintin/settings</div></div>
|
|
3680
|
-
<a class="btn" href="/">Vault Dashboard</a>
|
|
3681
|
-
</header>
|
|
3682
|
-
<main>
|
|
3683
|
-
<div class="stack">
|
|
3684
|
-
<section>
|
|
3685
|
-
<h2>Agent Setup</h2>
|
|
3686
|
-
<div class="hint">Creates a proper isolated cwd for Claude or Codex. Worktree setup is explicit and returned before any supervisor launches a process.</div>
|
|
3687
|
-
<div class="row">
|
|
3688
|
-
<div><label>agent</label><select id="agent"><option value="claude">claude</option><option value="codex">codex</option></select></div>
|
|
3689
|
-
<div><label>isolation</label><select id="isolation"><option value="worktree">worktree</option><option value="cwd">cwd</option></select></div>
|
|
3690
|
-
</div>
|
|
3691
|
-
<label>repo root</label><input id="repoRoot">
|
|
3692
|
-
<label>worktree root</label><input id="worktreeRoot">
|
|
3693
|
-
<div class="row">
|
|
3694
|
-
<div><label>base branch</label><input id="baseBranch"></div>
|
|
3695
|
-
<div><label>session id</label><input id="sessionId" placeholder="auto if blank"></div>
|
|
3696
|
-
</div>
|
|
3697
|
-
<div class="actions">
|
|
3698
|
-
<button onclick="loadSettings()" title="GET /tintin/settings">Load</button>
|
|
3699
|
-
<button onclick="saveSettings()" title="PUT /tintin/settings">Save</button>
|
|
3700
|
-
<button onclick="setupAgent()" title="POST /tintin/agent-sessions">Setup Agent</button>
|
|
3701
|
-
</div>
|
|
3702
|
-
<p class="status" id="status">idle</p>
|
|
3703
|
-
</section>
|
|
3704
|
-
<section>
|
|
3705
|
-
<h2>Stored Agent Sessions</h2>
|
|
3706
|
-
<pre id="agentSessions">No sessions loaded.</pre>
|
|
3707
|
-
</section>
|
|
3708
|
-
<section>
|
|
3709
|
-
<h2>Return</h2>
|
|
3710
|
-
<pre id="result">No return yet.</pre>
|
|
3711
|
-
</section>
|
|
3712
|
-
</div>
|
|
3713
|
-
<div class="wide">
|
|
3714
|
-
<section>
|
|
3715
|
-
<h2>Agent Defaults</h2>
|
|
3716
|
-
<div class="row">
|
|
3717
|
-
<div><label>Claude model</label><input id="claudeModel"></div>
|
|
3718
|
-
<div><label>Codex model</label><input id="codexModel"></div>
|
|
3719
|
-
</div>
|
|
3720
|
-
<div class="row">
|
|
3721
|
-
<div><label>Claude command</label><input id="claudeCommand"></div>
|
|
3722
|
-
<div><label>Codex command</label><input id="codexCommand"></div>
|
|
3723
|
-
</div>
|
|
3724
|
-
<div class="row">
|
|
3725
|
-
<label class="check"><input type="checkbox" id="claudeEnabled"> Claude enabled</label>
|
|
3726
|
-
<label class="check"><input type="checkbox" id="codexEnabled"> Codex enabled</label>
|
|
3727
|
-
</div>
|
|
3728
|
-
</section>
|
|
3729
|
-
<section>
|
|
3730
|
-
<h2>Sidebar Apps</h2>
|
|
3731
|
-
<div class="row">
|
|
3732
|
-
<div><label>trust mode</label><select id="trustMode"><option value="open-local">open-local</option><option value="configured-origins">configured-origins</option><option value="disabled">disabled</option></select></div>
|
|
3733
|
-
<div><label>default app slug</label><input id="defaultAppSlug"></div>
|
|
3734
|
-
</div>
|
|
3735
|
-
<div class="row">
|
|
3736
|
-
<div><label>session ttl hours</label><input id="sessionTtlHours" type="number" min="1" step="1"></div>
|
|
3737
|
-
<div><label>event history limit</label><input id="eventHistoryLimit" type="number" min="1" step="1"></div>
|
|
3738
|
-
</div>
|
|
3739
|
-
<label class="check"><input type="checkbox" id="sidebarEnabled"> Sidebar sessions enabled</label>
|
|
3740
|
-
<label>apps JSON</label><textarea class="short" id="appsJson" spellcheck="false"></textarea>
|
|
3741
|
-
</section>
|
|
3742
|
-
<section>
|
|
3743
|
-
<h2>Blackboard Dispatch</h2>
|
|
3744
|
-
<div class="row">
|
|
3745
|
-
<div><label>default agent</label><select id="dispatchDefaultAgent"><option value="claude">claude</option><option value="codex">codex</option></select></div>
|
|
3746
|
-
<div><label>default model</label><input id="dispatchDefaultModel"></div>
|
|
3747
|
-
</div>
|
|
3748
|
-
<div class="row">
|
|
3749
|
-
<div><label>max workers</label><input id="dispatchMaxWorkers" type="number" min="1" step="1"></div>
|
|
3750
|
-
<label class="check"><input type="checkbox" id="dispatchEnabled"> Dispatch enabled</label>
|
|
3751
|
-
</div>
|
|
3752
|
-
</section>
|
|
3753
|
-
<section>
|
|
3754
|
-
<h2>Co-develop Relay</h2>
|
|
3755
|
-
<div class="row">
|
|
3756
|
-
<div><label>default repo</label><input id="codevelopDefaultRepo"></div>
|
|
3757
|
-
<div><label>message retention</label><input id="codevelopRetention" type="number" min="1" step="1"></div>
|
|
3758
|
-
</div>
|
|
3759
|
-
<div class="row">
|
|
3760
|
-
<label class="check"><input type="checkbox" id="codevelopEnabled"> Co-develop enabled</label>
|
|
3761
|
-
<label class="check"><input type="checkbox" id="codevelopRequirePeer"> Require peer join</label>
|
|
3762
|
-
</div>
|
|
3763
|
-
</section>
|
|
3764
|
-
<section>
|
|
3765
|
-
<h2>Settings JSON</h2>
|
|
3766
|
-
<textarea id="settingsJson" spellcheck="false"></textarea>
|
|
3767
|
-
</section>
|
|
3768
|
-
</div>
|
|
3769
|
-
</main>
|
|
3770
|
-
<script>
|
|
3771
|
-
const BASE = location.origin;
|
|
3772
|
-
let currentSettings = {};
|
|
3773
|
-
function setStatus(text){ document.getElementById("status").textContent = text; }
|
|
3774
|
-
function show(value){ document.getElementById("result").textContent = typeof value === "string" ? value : JSON.stringify(value,null,2); }
|
|
3775
|
-
function num(id, fallback){ const n = Number(document.getElementById(id).value); return Number.isFinite(n) ? n : fallback; }
|
|
3776
|
-
function applySettings(data){
|
|
3777
|
-
const cfg = data.agent_settings ? data : data.config || data;
|
|
3778
|
-
currentSettings = cfg;
|
|
3779
|
-
const s = cfg.agent_settings || {};
|
|
3780
|
-
const side = cfg.sidebar_settings || {};
|
|
3781
|
-
const dispatch = cfg.dispatch_settings || {};
|
|
3782
|
-
const codevelop = cfg.codevelop_settings || {};
|
|
3783
|
-
const agents = s.agents || {};
|
|
3784
|
-
const claude = agents.claude || {};
|
|
3785
|
-
const codex = agents.codex || {};
|
|
3786
|
-
document.getElementById("trustMode").value = cfg.trust_mode || "open-local";
|
|
3787
|
-
document.getElementById("repoRoot").value = s.default_repo_root || "";
|
|
3788
|
-
document.getElementById("worktreeRoot").value = s.worktree_root || "";
|
|
3789
|
-
document.getElementById("baseBranch").value = s.base_branch || "develop";
|
|
3790
|
-
document.getElementById("isolation").value = s.isolation || "worktree";
|
|
3791
|
-
document.getElementById("claudeModel").value = claude.model || "";
|
|
3792
|
-
document.getElementById("codexModel").value = codex.model || "";
|
|
3793
|
-
document.getElementById("claudeCommand").value = claude.command || "claude";
|
|
3794
|
-
document.getElementById("codexCommand").value = codex.command || "codex";
|
|
3795
|
-
document.getElementById("claudeEnabled").checked = claude.enabled !== false;
|
|
3796
|
-
document.getElementById("codexEnabled").checked = codex.enabled !== false;
|
|
3797
|
-
document.getElementById("sidebarEnabled").checked = side.enabled !== false;
|
|
3798
|
-
document.getElementById("defaultAppSlug").value = side.default_app_slug || "";
|
|
3799
|
-
document.getElementById("sessionTtlHours").value = side.session_ttl_hours || 24;
|
|
3800
|
-
document.getElementById("eventHistoryLimit").value = side.event_history_limit || 200;
|
|
3801
|
-
document.getElementById("appsJson").value = JSON.stringify(cfg.apps || [],null,2);
|
|
3802
|
-
document.getElementById("dispatchEnabled").checked = dispatch.enabled !== false;
|
|
3803
|
-
document.getElementById("dispatchDefaultAgent").value = dispatch.default_agent || "claude";
|
|
3804
|
-
document.getElementById("dispatchDefaultModel").value = dispatch.default_model || "";
|
|
3805
|
-
document.getElementById("dispatchMaxWorkers").value = dispatch.max_concurrent_workers || 2;
|
|
3806
|
-
document.getElementById("codevelopEnabled").checked = codevelop.enabled !== false;
|
|
3807
|
-
document.getElementById("codevelopDefaultRepo").value = codevelop.default_repo || "";
|
|
3808
|
-
document.getElementById("codevelopRetention").value = codevelop.message_retention_limit || 500;
|
|
3809
|
-
document.getElementById("codevelopRequirePeer").checked = codevelop.require_peer_join !== false;
|
|
3810
|
-
document.getElementById("agentSessions").textContent = JSON.stringify(cfg.agent_sessions || [],null,2);
|
|
3811
|
-
document.getElementById("settingsJson").value = JSON.stringify(cfg,null,2);
|
|
3812
|
-
}
|
|
3813
|
-
function collectSettings(){
|
|
3814
|
-
let apps;
|
|
3815
|
-
try { apps = JSON.parse(document.getElementById("appsJson").value || "[]"); }
|
|
3816
|
-
catch(e){ throw new Error("apps JSON: " + e.message); }
|
|
3817
|
-
const parsed = JSON.parse(document.getElementById("settingsJson").value || "{}");
|
|
3818
|
-
return {
|
|
3819
|
-
...parsed,
|
|
3820
|
-
trust_mode: document.getElementById("trustMode").value,
|
|
3821
|
-
agent_settings: {
|
|
3822
|
-
...(parsed.agent_settings || {}),
|
|
3823
|
-
default_repo_root: document.getElementById("repoRoot").value,
|
|
3824
|
-
worktree_root: document.getElementById("worktreeRoot").value,
|
|
3825
|
-
base_branch: document.getElementById("baseBranch").value,
|
|
3826
|
-
isolation: document.getElementById("isolation").value,
|
|
3827
|
-
agents: {
|
|
3828
|
-
...((parsed.agent_settings || {}).agents || {}),
|
|
3829
|
-
claude: {
|
|
3830
|
-
...(((parsed.agent_settings || {}).agents || {}).claude || {}),
|
|
3831
|
-
runtime: "claude",
|
|
3832
|
-
model: document.getElementById("claudeModel").value,
|
|
3833
|
-
command: document.getElementById("claudeCommand").value,
|
|
3834
|
-
enabled: document.getElementById("claudeEnabled").checked
|
|
3835
|
-
},
|
|
3836
|
-
codex: {
|
|
3837
|
-
...(((parsed.agent_settings || {}).agents || {}).codex || {}),
|
|
3838
|
-
runtime: "codex",
|
|
3839
|
-
model: document.getElementById("codexModel").value,
|
|
3840
|
-
command: document.getElementById("codexCommand").value,
|
|
3841
|
-
enabled: document.getElementById("codexEnabled").checked
|
|
3842
|
-
}
|
|
3843
|
-
}
|
|
3844
|
-
},
|
|
3845
|
-
sidebar_settings: {
|
|
3846
|
-
...(parsed.sidebar_settings || {}),
|
|
3847
|
-
enabled: document.getElementById("sidebarEnabled").checked,
|
|
3848
|
-
default_app_slug: document.getElementById("defaultAppSlug").value,
|
|
3849
|
-
session_ttl_hours: num("sessionTtlHours", 24),
|
|
3850
|
-
event_history_limit: num("eventHistoryLimit", 200)
|
|
3851
|
-
},
|
|
3852
|
-
dispatch_settings: {
|
|
3853
|
-
...(parsed.dispatch_settings || {}),
|
|
3854
|
-
enabled: document.getElementById("dispatchEnabled").checked,
|
|
3855
|
-
default_agent: document.getElementById("dispatchDefaultAgent").value,
|
|
3856
|
-
default_model: document.getElementById("dispatchDefaultModel").value,
|
|
3857
|
-
max_concurrent_workers: num("dispatchMaxWorkers", 2)
|
|
3858
|
-
},
|
|
3859
|
-
codevelop_settings: {
|
|
3860
|
-
...(parsed.codevelop_settings || {}),
|
|
3861
|
-
enabled: document.getElementById("codevelopEnabled").checked,
|
|
3862
|
-
default_repo: document.getElementById("codevelopDefaultRepo").value,
|
|
3863
|
-
message_retention_limit: num("codevelopRetention", 500),
|
|
3864
|
-
require_peer_join: document.getElementById("codevelopRequirePeer").checked
|
|
3865
|
-
},
|
|
3866
|
-
agent_sessions: Array.isArray(parsed.agent_sessions) ? parsed.agent_sessions : (currentSettings.agent_sessions || []),
|
|
3867
|
-
apps
|
|
3868
|
-
};
|
|
3869
|
-
}
|
|
3870
|
-
async function loadSettings(){
|
|
3871
|
-
setStatus("GET /tintin/settings");
|
|
3872
|
-
const r = await fetch(BASE + "/tintin/settings", { cache:"no-store" });
|
|
3873
|
-
const data = await r.json();
|
|
3874
|
-
applySettings(data);
|
|
3875
|
-
show(data);
|
|
3876
|
-
setStatus(r.ok ? "loaded" : "load failed");
|
|
3877
|
-
}
|
|
3878
|
-
async function saveSettings(){
|
|
3879
|
-
setStatus("PUT /tintin/settings");
|
|
3880
|
-
let body;
|
|
3881
|
-
try { body = collectSettings(); }
|
|
3882
|
-
catch(e){ setStatus("invalid JSON"); show(e.message); return; }
|
|
3883
|
-
const r = await fetch(BASE + "/tintin/settings", { method:"PUT", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(body) });
|
|
3884
|
-
const data = await r.json();
|
|
3885
|
-
applySettings(data);
|
|
3886
|
-
show(data);
|
|
3887
|
-
setStatus(r.ok ? "saved" : "save failed");
|
|
3888
|
-
}
|
|
3889
|
-
async function setupAgent(){
|
|
3890
|
-
const body = {
|
|
3891
|
-
agent: document.getElementById("agent").value,
|
|
3892
|
-
isolation: document.getElementById("isolation").value,
|
|
3893
|
-
repo_root: document.getElementById("repoRoot").value,
|
|
3894
|
-
worktree_root: document.getElementById("worktreeRoot").value,
|
|
3895
|
-
base_branch: document.getElementById("baseBranch").value,
|
|
3896
|
-
session_id: document.getElementById("sessionId").value || undefined
|
|
3897
|
-
};
|
|
3898
|
-
setStatus("POST /tintin/agent-sessions");
|
|
3899
|
-
const r = await fetch(BASE + "/tintin/agent-sessions", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(body) });
|
|
3900
|
-
const data = await r.json();
|
|
3901
|
-
show(data);
|
|
3902
|
-
setStatus(r.ok && data.ok ? "agent setup ready" : "agent setup failed");
|
|
3903
|
-
if (r.ok && data.ok) loadSettings().catch(function(){});
|
|
3904
|
-
}
|
|
3905
|
-
loadSettings().catch(err => { setStatus("load failed"); show(err.message || String(err)); });
|
|
3906
|
-
</script>
|
|
3907
|
-
</body>
|
|
3908
|
-
</html>`;
|
|
3909
|
-
}
|
|
3910
|
-
|
|
3911
3295
|
// ── call_agent noauth-host guard (Gate B, bearer-gated tunnel) ───────────────
|
|
3912
3296
|
// Pure decision function so the policy is testable without a daemon. Decides
|
|
3913
3297
|
// whether a /call-agent request may proceed.
|
|
@@ -5091,6 +4475,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5091
4475
|
saveClients(oauthClients);
|
|
5092
4476
|
const logMsg = `[${new Date().toISOString()}] OAuth: registered public client ${clientId} (${client.client_name})\n`;
|
|
5093
4477
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
4478
|
+
operation("oauth.client_register", { client_id: clientId, client_name: client.client_name }, null, { ok: true });
|
|
5094
4479
|
res.writeHead(201, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
5095
4480
|
return res.end(JSON.stringify(client));
|
|
5096
4481
|
}
|
|
@@ -5214,6 +4599,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5214
4599
|
|
|
5215
4600
|
const logMsg = `[${new Date().toISOString()}] OAuth: token issued for ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
|
|
5216
4601
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
4602
|
+
operation("oauth.token_issue", { client_id: stored.client_id }, null, { ok: true });
|
|
5217
4603
|
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
5218
4604
|
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", scope: "mcp:tools", expires_in: 86400 }));
|
|
5219
4605
|
}
|
|
@@ -5224,7 +4610,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5224
4610
|
function toolsForPath(p) {
|
|
5225
4611
|
const tools = filterMcpToolsForWriteMode(MCP_TOOLS);
|
|
5226
4612
|
if (p === "/gws") return tools.filter(t => t.name.startsWith("gws_"));
|
|
5227
|
-
if (p === "/clauth") return tools.filter(t => t.name.startsWith("clauth_") || t.name === "
|
|
4613
|
+
if (p === "/clauth") return tools.filter(t => t.name.startsWith("clauth_") || t.name === "call_agent" || t.name.startsWith("terminal_") || t.name.startsWith("channel_") || t.name.startsWith("handoff_"));
|
|
5228
4614
|
if (p === "/fs") return tools.filter(t => t.name.startsWith("fs_"));
|
|
5229
4615
|
if (p === "/chitchat") return tools.filter(t => t.name.startsWith("chitchat_"));
|
|
5230
4616
|
if (p === "/codevelop") return tools.filter(t => t.name.startsWith("codevelop_"));
|
|
@@ -5530,8 +4916,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5530
4916
|
return res.end(JSON.stringify({ error: "name and path are required" }));
|
|
5531
4917
|
}
|
|
5532
4918
|
const result = webdavService.addMount(name.trim(), mountPath.trim());
|
|
5533
|
-
if (result.error) {
|
|
4919
|
+
if (result.error) {
|
|
4920
|
+
operation("webdav.mount_add", { name: name.trim() }, null, { ok: false, error: result.error });
|
|
4921
|
+
res.writeHead(409, { "Content-Type": "application/json", ...CORS }); return res.end(JSON.stringify(result));
|
|
4922
|
+
}
|
|
5534
4923
|
webdavService.shutdown();
|
|
4924
|
+
operation("webdav.mount_add", { name: name.trim(), path: mountPath.trim() }, null, { ok: true });
|
|
5535
4925
|
return ok(res, result);
|
|
5536
4926
|
}
|
|
5537
4927
|
|
|
@@ -5539,8 +4929,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5539
4929
|
if (writeGuard(req, res)) return;
|
|
5540
4930
|
const mountName = decodeURIComponent(reqPath.slice("/webdav/mounts/".length));
|
|
5541
4931
|
const result = webdavService.removeMount(mountName);
|
|
5542
|
-
if (result.error) {
|
|
4932
|
+
if (result.error) {
|
|
4933
|
+
operation("webdav.mount_remove", { name: mountName }, null, { ok: false, error: result.error });
|
|
4934
|
+
res.writeHead(404, { "Content-Type": "application/json", ...CORS }); return res.end(JSON.stringify(result));
|
|
4935
|
+
}
|
|
5543
4936
|
webdavService.shutdown();
|
|
4937
|
+
operation("webdav.mount_remove", { name: mountName }, null, { ok: true });
|
|
5544
4938
|
return ok(res, result);
|
|
5545
4939
|
}
|
|
5546
4940
|
|
|
@@ -5554,6 +4948,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5554
4948
|
return res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
5555
4949
|
}
|
|
5556
4950
|
const result = startCodevelopSession(body || {});
|
|
4951
|
+
operation("codevelop.start", { session_id: result?.session_id }, null, { ok: !result?.error, error: result?.error });
|
|
5557
4952
|
return ok(res, result);
|
|
5558
4953
|
}
|
|
5559
4954
|
|
|
@@ -5565,12 +4960,19 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5565
4960
|
}
|
|
5566
4961
|
const result = joinCodevelopSession(body || {});
|
|
5567
4962
|
if (result.error) {
|
|
4963
|
+
operation("codevelop.join", { session_id: body?.session_id }, null, { ok: false, error: result.error });
|
|
5568
4964
|
res.writeHead(result.error === "not_found" ? 404 : 400, { "Content-Type": "application/json", ...CORS });
|
|
5569
4965
|
return res.end(JSON.stringify(result));
|
|
5570
4966
|
}
|
|
4967
|
+
operation("codevelop.join", { session_id: body?.session_id }, null, { ok: true });
|
|
5571
4968
|
return ok(res, result);
|
|
5572
4969
|
}
|
|
5573
4970
|
|
|
4971
|
+
// codevelop/send is a genuine mutation (adds a message) but excluded from
|
|
4972
|
+
// per-route logging by design — a live peer-to-peer relay can send at a
|
|
4973
|
+
// frequency that would bloat events.jsonl (same risk noted in the audit-
|
|
4974
|
+
// logging retrofit plan for codevelop/poll). Session lifecycle
|
|
4975
|
+
// (start/join/stop) is logged; message throughput is not.
|
|
5574
4976
|
if (method === "POST" && reqPath === "/codevelop/send") {
|
|
5575
4977
|
let body;
|
|
5576
4978
|
try { body = await readBody(req); } catch {
|
|
@@ -5654,9 +5056,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5654
5056
|
}
|
|
5655
5057
|
const result = stopCodevelopSession(body?.session_id);
|
|
5656
5058
|
if (result.error) {
|
|
5059
|
+
operation("codevelop.stop", { session_id: body?.session_id }, null, { ok: false, error: result.error });
|
|
5657
5060
|
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
5658
5061
|
return res.end(JSON.stringify(result));
|
|
5659
5062
|
}
|
|
5063
|
+
operation("codevelop.stop", { session_id: body?.session_id }, null, { ok: true });
|
|
5660
5064
|
return ok(res, result);
|
|
5661
5065
|
}
|
|
5662
5066
|
|
|
@@ -5844,6 +5248,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5844
5248
|
saveTokens(oauthTokens);
|
|
5845
5249
|
const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials — all clients and tokens invalidated\n`;
|
|
5846
5250
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
5251
|
+
operation("oauth.roll_creds", {}, null, { ok: true });
|
|
5847
5252
|
return ok(res, { clients_cleared: true, tokens_invalidated: true });
|
|
5848
5253
|
}
|
|
5849
5254
|
|
|
@@ -5857,10 +5262,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5857
5262
|
}
|
|
5858
5263
|
if (body.action === "stop") {
|
|
5859
5264
|
stopTunnel();
|
|
5265
|
+
operation("tunnel.stop", {}, null, { ok: true });
|
|
5860
5266
|
return ok(res, { status: tunnelStatus, running: false });
|
|
5861
5267
|
}
|
|
5862
5268
|
// start
|
|
5863
5269
|
await startTunnel();
|
|
5270
|
+
operation("tunnel.start", {}, null, { ok: !!tunnelProc, url: tunnelUrl, error: tunnelError });
|
|
5864
5271
|
return ok(res, { status: tunnelStatus, running: !!tunnelProc, url: tunnelUrl, error: tunnelError });
|
|
5865
5272
|
}
|
|
5866
5273
|
|
|
@@ -5870,6 +5277,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5870
5277
|
if (tunnelProc) return ok(res, { status: tunnelStatus, message: "already running" });
|
|
5871
5278
|
tunnelStatus = "starting";
|
|
5872
5279
|
startTunnel().catch(() => {});
|
|
5280
|
+
operation("tunnel.start", {}, null, { ok: true, status: "starting" });
|
|
5873
5281
|
return ok(res, { status: "starting" });
|
|
5874
5282
|
}
|
|
5875
5283
|
|
|
@@ -5877,6 +5285,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5877
5285
|
if (method === "POST" && reqPath === "/tunnel/stop") {
|
|
5878
5286
|
if (lockedGuard(res)) return;
|
|
5879
5287
|
stopTunnel();
|
|
5288
|
+
operation("tunnel.stop", {}, null, { ok: true });
|
|
5880
5289
|
return ok(res, { status: tunnelStatus });
|
|
5881
5290
|
}
|
|
5882
5291
|
|
|
@@ -5919,8 +5328,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5919
5328
|
stdio: "ignore",
|
|
5920
5329
|
});
|
|
5921
5330
|
child.unref();
|
|
5331
|
+
operation("ccandme.launch", {}, null, { ok: true });
|
|
5922
5332
|
return ok(res, { ok: true, message: "CCandMe launched" });
|
|
5923
5333
|
} catch (err) {
|
|
5334
|
+
operation("ccandme.launch", {}, null, { ok: false, error: err.message });
|
|
5924
5335
|
res.writeHead(500, { "Content-Type": "application/json", ...CORS });
|
|
5925
5336
|
return res.end(JSON.stringify({ error: err.message }));
|
|
5926
5337
|
}
|
|
@@ -5928,6 +5339,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5928
5339
|
|
|
5929
5340
|
// POST /restart — spawn fresh process then exit (keeps boot.key, vault stays unlocked)
|
|
5930
5341
|
if (method === "POST" && reqPath === "/restart") {
|
|
5342
|
+
operation("daemon.restart_requested", { port }, null, { ok: true });
|
|
5931
5343
|
ok(res, { ok: true, message: "restarting" });
|
|
5932
5344
|
const { spawn } = await import("child_process");
|
|
5933
5345
|
const cliEntry = path.resolve(__dirname, "../index.js");
|
|
@@ -5949,454 +5361,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5949
5361
|
return;
|
|
5950
5362
|
}
|
|
5951
5363
|
|
|
5952
|
-
// GET /tintin/manifest — app manifest for the TinTin sidebar loader.
|
|
5953
|
-
// GET /monkey/manifest — backward-compat alias.
|
|
5954
|
-
if (method === "GET" && (reqPath === "/tintin/manifest" || reqPath === "/monkey/manifest")) {
|
|
5955
|
-
return ok(res, {
|
|
5956
|
-
schema_version: 1,
|
|
5957
|
-
app: {
|
|
5958
|
-
slug: "clauth-dashboard",
|
|
5959
|
-
name: "clauth Dashboard",
|
|
5960
|
-
},
|
|
5961
|
-
load: {
|
|
5962
|
-
mode: "external-sidebar",
|
|
5963
|
-
requires_compile_in: false,
|
|
5964
|
-
recommended_loader: "chrome-extension-or-tampermonkey",
|
|
5965
|
-
userscript_url: "/tintin/sidebar.user.js",
|
|
5966
|
-
},
|
|
5967
|
-
local_clauth: {
|
|
5968
|
-
base_url: `http://127.0.0.1:${port}`,
|
|
5969
|
-
capabilities_endpoint: "/tintin/capabilities",
|
|
5970
|
-
session_endpoint: "/tintin/sessions",
|
|
5971
|
-
transport: "tintin",
|
|
5972
|
-
},
|
|
5973
|
-
repo: {
|
|
5974
|
-
root_hint: "C:\\Dev\\regen-root",
|
|
5975
|
-
cwd_hint: "C:\\Dev\\regen-root",
|
|
5976
|
-
},
|
|
5977
|
-
capabilities: ["general_chat", "skill_request", "handoff", "dashboard_test"],
|
|
5978
|
-
agent_context: {
|
|
5979
|
-
app: { slug: "clauth-dashboard" },
|
|
5980
|
-
runtime: { requested_by: "clauth-dashboard-monkey" },
|
|
5981
|
-
},
|
|
5982
|
-
});
|
|
5983
|
-
}
|
|
5984
|
-
|
|
5985
|
-
// GET /tintin/sidebar.user.js — shared app-side TinTin sidebar loader.
|
|
5986
|
-
// GET /monkey/sidebar.user.js — backward-compat alias.
|
|
5987
|
-
if (method === "GET" && (reqPath === "/tintin/sidebar.user.js" || reqPath === "/monkey/sidebar.user.js")) {
|
|
5988
|
-
const candidates = [
|
|
5989
|
-
process.env.CLAUTH_MONKEY_SIDEBAR_SCRIPT,
|
|
5990
|
-
"C:\\Dev\\regen-root\\scripts\\monkey-sidebar.user.js",
|
|
5991
|
-
path.resolve(process.cwd(), "..", "regen-root", "scripts", "monkey-sidebar.user.js"),
|
|
5992
|
-
].filter(Boolean);
|
|
5993
|
-
const scriptPath = candidates.find((candidate) => {
|
|
5994
|
-
try { return fs.existsSync(candidate); } catch { return false; }
|
|
5995
|
-
});
|
|
5996
|
-
if (!scriptPath) {
|
|
5997
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
5998
|
-
return res.end(JSON.stringify({ error: "monkey_sidebar_script_not_found" }));
|
|
5999
|
-
}
|
|
6000
|
-
res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache, no-store, must-revalidate", ...CORS });
|
|
6001
|
-
return res.end(fs.readFileSync(scriptPath, "utf8"));
|
|
6002
|
-
}
|
|
6003
|
-
|
|
6004
|
-
// GET /tintin/capabilities — browser-safe local TinTin sidebar capability probe.
|
|
6005
|
-
// GET /monkey/capabilities — backward-compat alias.
|
|
6006
|
-
if (method === "GET" && (reqPath === "/tintin/capabilities" || reqPath === "/monkey/capabilities")) {
|
|
6007
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6008
|
-
if (!access.allowed) {
|
|
6009
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6010
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6011
|
-
}
|
|
6012
|
-
return ok(res, {
|
|
6013
|
-
ok: true,
|
|
6014
|
-
status: password ? "available" : "locked",
|
|
6015
|
-
locked: !password,
|
|
6016
|
-
trust_mode: access.config.trust_mode || "open-local",
|
|
6017
|
-
app: access.app?.slug || null,
|
|
6018
|
-
config: publicTinTinConfig(access.config),
|
|
6019
|
-
transports: ["dispatch", "sse", "polling", "result"],
|
|
6020
|
-
endpoints: {
|
|
6021
|
-
sessions: "/tintin/sessions",
|
|
6022
|
-
events: "/tintin/sessions/:session_id/events",
|
|
6023
|
-
messages: "/tintin/sessions/:session_id/messages",
|
|
6024
|
-
result: "/tintin/messages/:message_id/result",
|
|
6025
|
-
},
|
|
6026
|
-
});
|
|
6027
|
-
}
|
|
6028
|
-
|
|
6029
|
-
// GET /tintin/config — local operator view of the app allowlist/config.
|
|
6030
|
-
// GET /monkey/config — backward-compat alias.
|
|
6031
|
-
if (method === "GET" && (reqPath === "/tintin/config" || reqPath === "/monkey/config")) {
|
|
6032
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6033
|
-
if (!access.allowed) {
|
|
6034
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6035
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6036
|
-
}
|
|
6037
|
-
return ok(res, { ok: true, ...publicTinTinConfig(access.config) });
|
|
6038
|
-
}
|
|
6039
|
-
|
|
6040
|
-
if (method === "GET" && reqPath === "/tintin/settings/ui") {
|
|
6041
|
-
res.writeHead(200, { "Content-Type": "text/html", ...CORS });
|
|
6042
|
-
return res.end(tintinSettingsHtml());
|
|
6043
|
-
}
|
|
6044
|
-
|
|
6045
|
-
if (method === "GET" && (reqPath === "/tintin/settings" || reqPath === "/monkey/settings")) {
|
|
6046
|
-
const config = loadTinTinConfig();
|
|
6047
|
-
return ok(res, { ok: true, ...publicTinTinConfig(config) });
|
|
6048
|
-
}
|
|
6049
|
-
|
|
6050
|
-
if ((method === "PUT" || method === "POST") && (reqPath === "/tintin/settings" || reqPath === "/monkey/settings")) {
|
|
6051
|
-
let body;
|
|
6052
|
-
try { body = await readBody(req); } catch {
|
|
6053
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6054
|
-
return res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
6055
|
-
}
|
|
6056
|
-
try {
|
|
6057
|
-
const saved = saveTinTinConfig(body || {});
|
|
6058
|
-
return ok(res, { ok: true, ...publicTinTinConfig(saved) });
|
|
6059
|
-
} catch (err) {
|
|
6060
|
-
res.writeHead(500, { "Content-Type": "application/json", ...CORS });
|
|
6061
|
-
return res.end(JSON.stringify({ ok: false, error: "settings_save_failed", message: err.message }));
|
|
6062
|
-
}
|
|
6063
|
-
}
|
|
6064
|
-
|
|
6065
|
-
if (method === "POST" && (reqPath === "/tintin/agent-sessions" || reqPath === "/monkey/agent-sessions")) {
|
|
6066
|
-
let body;
|
|
6067
|
-
try { body = await readBody(req); } catch {
|
|
6068
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6069
|
-
return res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
6070
|
-
}
|
|
6071
|
-
try {
|
|
6072
|
-
const result = createTinTinAgentSession(body || {});
|
|
6073
|
-
if (!result.ok) {
|
|
6074
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6075
|
-
return res.end(JSON.stringify(result));
|
|
6076
|
-
}
|
|
6077
|
-
return ok(res, result);
|
|
6078
|
-
} catch (err) {
|
|
6079
|
-
res.writeHead(500, { "Content-Type": "application/json", ...CORS });
|
|
6080
|
-
return res.end(JSON.stringify({ ok: false, error: "agent_session_setup_failed", message: err.message }));
|
|
6081
|
-
}
|
|
6082
|
-
}
|
|
6083
|
-
|
|
6084
|
-
// POST /tintin/sessions — create or attach to a local CLI-backed TinTin session.
|
|
6085
|
-
// POST /monkey/sessions — backward-compat alias.
|
|
6086
|
-
if (method === "POST" && (reqPath === "/tintin/sessions" || reqPath === "/monkey/sessions")) {
|
|
6087
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6088
|
-
if (!access.allowed) {
|
|
6089
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6090
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6091
|
-
}
|
|
6092
|
-
let body;
|
|
6093
|
-
try { body = await readBody(req); } catch {
|
|
6094
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6095
|
-
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
6096
|
-
}
|
|
6097
|
-
const session = createTinTinSession(body || {});
|
|
6098
|
-
pushTinTinEvent(session.id, {
|
|
6099
|
-
source: "clauth",
|
|
6100
|
-
type: "status",
|
|
6101
|
-
content: "session ready",
|
|
6102
|
-
payload: { status: "ready" },
|
|
6103
|
-
});
|
|
6104
|
-
return ok(res, {
|
|
6105
|
-
ok: true,
|
|
6106
|
-
session_id: session.id,
|
|
6107
|
-
status: session.status,
|
|
6108
|
-
agent_context: session.agent_context,
|
|
6109
|
-
last_seq: session.seq,
|
|
6110
|
-
});
|
|
6111
|
-
}
|
|
6112
|
-
|
|
6113
|
-
// GET /tintin/sessions — list in-memory local TinTin sessions.
|
|
6114
|
-
// GET /monkey/sessions — backward-compat alias.
|
|
6115
|
-
if (method === "GET" && (reqPath === "/tintin/sessions" || reqPath === "/monkey/sessions")) {
|
|
6116
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6117
|
-
if (!access.allowed) {
|
|
6118
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6119
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6120
|
-
}
|
|
6121
|
-
return ok(res, {
|
|
6122
|
-
ok: true,
|
|
6123
|
-
sessions: [...tintinSessions.values()].map((session) => ({
|
|
6124
|
-
session_id: session.id,
|
|
6125
|
-
status: session.status,
|
|
6126
|
-
created_at: session.created_at,
|
|
6127
|
-
updated_at: session.updated_at,
|
|
6128
|
-
last_seq: session.seq,
|
|
6129
|
-
agent_context: session.agent_context,
|
|
6130
|
-
})),
|
|
6131
|
-
});
|
|
6132
|
-
}
|
|
6133
|
-
|
|
6134
|
-
const tintinSessionMatch = reqPath.match(/^\/(?:tintin|monkey)\/sessions\/([^/]+)$/);
|
|
6135
|
-
if (method === "GET" && tintinSessionMatch) {
|
|
6136
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6137
|
-
if (!access.allowed) {
|
|
6138
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6139
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6140
|
-
}
|
|
6141
|
-
const sessionId = decodeURIComponent(tintinSessionMatch[1]);
|
|
6142
|
-
const session = getTinTinSession(sessionId);
|
|
6143
|
-
if (!session) {
|
|
6144
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
6145
|
-
return res.end(JSON.stringify({ error: "not_found", session_id: sessionId }));
|
|
6146
|
-
}
|
|
6147
|
-
return ok(res, {
|
|
6148
|
-
ok: true,
|
|
6149
|
-
session_id: session.id,
|
|
6150
|
-
status: session.status,
|
|
6151
|
-
created_at: session.created_at,
|
|
6152
|
-
updated_at: session.updated_at,
|
|
6153
|
-
last_seq: session.seq,
|
|
6154
|
-
agent_context: session.agent_context,
|
|
6155
|
-
});
|
|
6156
|
-
}
|
|
6157
|
-
|
|
6158
|
-
const tintinMessagesMatch = reqPath.match(/^\/(?:tintin|monkey)\/sessions\/([^/]+)\/messages$/);
|
|
6159
|
-
if (method === "GET" && tintinMessagesMatch) {
|
|
6160
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6161
|
-
if (!access.allowed) {
|
|
6162
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6163
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6164
|
-
}
|
|
6165
|
-
const sessionId = decodeURIComponent(tintinMessagesMatch[1]);
|
|
6166
|
-
const afterSeq = Number(url.searchParams.get("after_seq") || url.searchParams.get("after") || 0);
|
|
6167
|
-
const events = listTinTinEvents(sessionId, Number.isFinite(afterSeq) ? afterSeq : 0);
|
|
6168
|
-
if (!events) {
|
|
6169
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
6170
|
-
return res.end(JSON.stringify({ error: "not_found", session_id: sessionId }));
|
|
6171
|
-
}
|
|
6172
|
-
return ok(res, { ok: true, session_id: sessionId, events });
|
|
6173
|
-
}
|
|
6174
|
-
|
|
6175
|
-
if (method === "POST" && tintinMessagesMatch) {
|
|
6176
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6177
|
-
if (!access.allowed) {
|
|
6178
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6179
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6180
|
-
}
|
|
6181
|
-
if (!password) {
|
|
6182
|
-
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
6183
|
-
return res.end(JSON.stringify({ error: "Vault is locked", locked: true }));
|
|
6184
|
-
}
|
|
6185
|
-
const sessionId = decodeURIComponent(tintinMessagesMatch[1]);
|
|
6186
|
-
let session = getTinTinSession(sessionId);
|
|
6187
|
-
let body;
|
|
6188
|
-
try { body = await readBody(req); } catch {
|
|
6189
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6190
|
-
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
6191
|
-
}
|
|
6192
|
-
if (!session) session = createTinTinSession({ ...(body || {}), session_id: sessionId });
|
|
6193
|
-
const content = String(body?.content || body?.prompt || "").trim();
|
|
6194
|
-
if (!content) {
|
|
6195
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6196
|
-
return res.end(JSON.stringify({ error: "content required" }));
|
|
6197
|
-
}
|
|
6198
|
-
|
|
6199
|
-
const messageId = body.message_id || makeTinTinId("msg");
|
|
6200
|
-
const jobId = body.job_id || messageId;
|
|
6201
|
-
const agentContext = normalizeAgentContext(body.agent_context || {
|
|
6202
|
-
...session.agent_context,
|
|
6203
|
-
task: {
|
|
6204
|
-
...(session.agent_context?.task || {}),
|
|
6205
|
-
intent: body.skill ? "skill_request" : "general_chat",
|
|
6206
|
-
thread_id: sessionId,
|
|
6207
|
-
},
|
|
6208
|
-
});
|
|
6209
|
-
pushTinTinEvent(sessionId, {
|
|
6210
|
-
source: "sidebar",
|
|
6211
|
-
type: "message",
|
|
6212
|
-
role: "user",
|
|
6213
|
-
message_id: messageId,
|
|
6214
|
-
job_id: jobId,
|
|
6215
|
-
content,
|
|
6216
|
-
payload: { skill: body.skill || null },
|
|
6217
|
-
});
|
|
6218
|
-
|
|
6219
|
-
const prompt = buildTinTinPrompt({
|
|
6220
|
-
prompt: body.skill ? `Skill: ${body.skill}\n\n${content}` : content,
|
|
6221
|
-
job_id: jobId,
|
|
6222
|
-
agent_context: agentContext,
|
|
6223
|
-
});
|
|
6224
|
-
const dispatchCwd = resolveDispatchCwd(body.cwd || session.cwd, agentContext);
|
|
6225
|
-
const result = spawnClaudeTask(prompt, jobId, dispatchCwd, agentContext);
|
|
6226
|
-
if (result.error) {
|
|
6227
|
-
pushTinTinEvent(sessionId, {
|
|
6228
|
-
source: "clauth",
|
|
6229
|
-
type: "error",
|
|
6230
|
-
role: "assistant",
|
|
6231
|
-
message_id: messageId,
|
|
6232
|
-
job_id: jobId,
|
|
6233
|
-
content: result.message || result.error,
|
|
6234
|
-
payload: { code: result.error, retryable: result.error === "concurrency_limit" },
|
|
6235
|
-
});
|
|
6236
|
-
res.writeHead(503, { "Content-Type": "application/json", ...CORS });
|
|
6237
|
-
return res.end(JSON.stringify(result));
|
|
6238
|
-
}
|
|
6239
|
-
|
|
6240
|
-
tintinMessageIndex.set(messageId, { session_id: sessionId, job_id: jobId });
|
|
6241
|
-
pushTinTinEvent(sessionId, {
|
|
6242
|
-
source: "clauth",
|
|
6243
|
-
type: "status",
|
|
6244
|
-
role: "assistant",
|
|
6245
|
-
message_id: messageId,
|
|
6246
|
-
job_id: jobId,
|
|
6247
|
-
content: "spawned",
|
|
6248
|
-
payload: result,
|
|
6249
|
-
});
|
|
6250
|
-
startTinTinMessageMonitor(sessionId, messageId, jobId);
|
|
6251
|
-
return ok(res, {
|
|
6252
|
-
ok: true,
|
|
6253
|
-
session_id: sessionId,
|
|
6254
|
-
message_id: messageId,
|
|
6255
|
-
job_id: jobId,
|
|
6256
|
-
status: "spawned",
|
|
6257
|
-
dispatch: result,
|
|
6258
|
-
});
|
|
6259
|
-
}
|
|
6260
|
-
|
|
6261
|
-
const tintinEventsMatch = reqPath.match(/^\/(?:tintin|monkey)\/sessions\/([^/]+)\/events$/);
|
|
6262
|
-
if (method === "GET" && tintinEventsMatch) {
|
|
6263
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6264
|
-
if (!access.allowed) {
|
|
6265
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6266
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6267
|
-
}
|
|
6268
|
-
const sessionId = decodeURIComponent(tintinEventsMatch[1]);
|
|
6269
|
-
const afterSeq = Number(url.searchParams.get("after_seq") || url.searchParams.get("after") || 0);
|
|
6270
|
-
const session = getTinTinSession(sessionId);
|
|
6271
|
-
if (!session) {
|
|
6272
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
6273
|
-
return res.end(JSON.stringify({ error: "not_found", session_id: sessionId }));
|
|
6274
|
-
}
|
|
6275
|
-
|
|
6276
|
-
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", "Connection": "keep-alive", ...CORS });
|
|
6277
|
-
const send = (event) => {
|
|
6278
|
-
const eventType = event.type || "message";
|
|
6279
|
-
res.write(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
6280
|
-
};
|
|
6281
|
-
res.write(`event: ready\ndata: ${JSON.stringify({ ok: true, session_id: sessionId, last_seq: session.seq })}\n\n`);
|
|
6282
|
-
for (const event of listTinTinEvents(sessionId, Number.isFinite(afterSeq) ? afterSeq : 0) || []) send(event);
|
|
6283
|
-
const unsubscribe = subscribeTinTinEvents(sessionId, send);
|
|
6284
|
-
const heartbeat = setInterval(() => {
|
|
6285
|
-
try { res.write(`event: heartbeat\ndata: ${JSON.stringify({ session_id: sessionId, at: new Date().toISOString() })}\n\n`); }
|
|
6286
|
-
catch { clearInterval(heartbeat); unsubscribe(); }
|
|
6287
|
-
}, 15000);
|
|
6288
|
-
req.on("close", () => {
|
|
6289
|
-
clearInterval(heartbeat);
|
|
6290
|
-
unsubscribe();
|
|
6291
|
-
});
|
|
6292
|
-
return;
|
|
6293
|
-
}
|
|
6294
|
-
|
|
6295
|
-
const tintinResultMatch = reqPath.match(/^\/(?:tintin|monkey)\/messages\/([^/]+)\/result$/);
|
|
6296
|
-
if (method === "GET" && tintinResultMatch) {
|
|
6297
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6298
|
-
if (!access.allowed) {
|
|
6299
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6300
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6301
|
-
}
|
|
6302
|
-
const messageId = decodeURIComponent(tintinResultMatch[1]);
|
|
6303
|
-
const ref = tintinMessageIndex.get(messageId);
|
|
6304
|
-
if (!ref) {
|
|
6305
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
6306
|
-
return res.end(JSON.stringify({ error: "not_found", message_id: messageId }));
|
|
6307
|
-
}
|
|
6308
|
-
const job = tintinJobs.get(ref.job_id);
|
|
6309
|
-
const session = getTinTinSession(ref.session_id);
|
|
6310
|
-
return ok(res, {
|
|
6311
|
-
ok: true,
|
|
6312
|
-
message_id: messageId,
|
|
6313
|
-
session_id: ref.session_id,
|
|
6314
|
-
job_id: ref.job_id,
|
|
6315
|
-
status: job?.status || "unknown",
|
|
6316
|
-
stdout: job?.stdout || "",
|
|
6317
|
-
stderr: job?.stderr || "",
|
|
6318
|
-
events: session?.events.filter((event) => event.message_id === messageId) || [],
|
|
6319
|
-
});
|
|
6320
|
-
}
|
|
6321
|
-
|
|
6322
|
-
// POST /tintin/dispatch — browser-safe, config-checked CLI worker spawn.
|
|
6323
|
-
// POST /monkey/dispatch, /dispatch, and /monkey-dispatch remain legacy backward-compat aliases.
|
|
6324
|
-
if (method === "POST" && (reqPath === "/tintin/dispatch" || reqPath === "/dispatch" || reqPath === "/monkey-dispatch" || reqPath === "/monkey/dispatch")) {
|
|
6325
|
-
if (reqPath === "/tintin/dispatch" || reqPath === "/monkey/dispatch") {
|
|
6326
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6327
|
-
if (!access.allowed) {
|
|
6328
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6329
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6330
|
-
}
|
|
6331
|
-
} else if (req.headers.origin) {
|
|
6332
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6333
|
-
return res.end(JSON.stringify({ ok: false, error: "legacy_dispatch_rejects_browser_origin", use: "/tintin/dispatch" }));
|
|
6334
|
-
}
|
|
6335
|
-
let body = "";
|
|
6336
|
-
req.on("data", d => body += d);
|
|
6337
|
-
req.on("end", () => {
|
|
6338
|
-
try {
|
|
6339
|
-
const { prompt, job_id, cwd, agent_context } = JSON.parse(body || "{}");
|
|
6340
|
-
if (!prompt && !job_id) {
|
|
6341
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6342
|
-
return res.end(JSON.stringify({ error: "prompt required" }));
|
|
6343
|
-
}
|
|
6344
|
-
const normalizedContext = normalizeAgentContext(agent_context);
|
|
6345
|
-
const wrappedPrompt = buildTinTinPrompt({ prompt, job_id, agent_context: normalizedContext });
|
|
6346
|
-
const dispatchCwd = resolveDispatchCwd(cwd, normalizedContext);
|
|
6347
|
-
const result = spawnClaudeTask(wrappedPrompt, job_id || "untracked", dispatchCwd, normalizedContext);
|
|
6348
|
-
const status = result.error ? 503 : 200;
|
|
6349
|
-
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
6350
|
-
res.end(JSON.stringify({ ...result, context: normalizedContext }));
|
|
6351
|
-
} catch {
|
|
6352
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6353
|
-
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
6354
|
-
}
|
|
6355
|
-
});
|
|
6356
|
-
return;
|
|
6357
|
-
}
|
|
6358
|
-
|
|
6359
|
-
// POST /tintin/dispatch/:jobId/kill — kill a running dispatch job.
|
|
6360
|
-
// POST /monkey/dispatch/:jobId/kill — backward-compat alias.
|
|
6361
|
-
const dispatchKillMatch = reqPath.match(/^\/(?:tintin|monkey)\/dispatch\/([^/]+)\/kill$/);
|
|
6362
|
-
if (method === "POST" && dispatchKillMatch) {
|
|
6363
|
-
const jobId = decodeURIComponent(dispatchKillMatch[1]);
|
|
6364
|
-
const job = tintinJobs.get(jobId);
|
|
6365
|
-
if (!job) {
|
|
6366
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
6367
|
-
return res.end(JSON.stringify({ ok: false, error: "not_found" }));
|
|
6368
|
-
}
|
|
6369
|
-
if (job.pid && job.status === "running") {
|
|
6370
|
-
try { process.kill(job.pid); } catch {}
|
|
6371
|
-
job.status = "killed";
|
|
6372
|
-
job.completed_at = new Date().toISOString();
|
|
6373
|
-
}
|
|
6374
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
6375
|
-
return res.end(JSON.stringify({ ok: true, status: job.status, jobId }));
|
|
6376
|
-
}
|
|
6377
|
-
|
|
6378
|
-
const dispatchStatusMatch = reqPath.match(/^\/(?:tintin|monkey)\/dispatch\/([^/]+)$/) || reqPath.match(/^\/dispatch\/([^/]+)$/);
|
|
6379
|
-
if (method === "GET" && dispatchStatusMatch) {
|
|
6380
|
-
if (reqPath.startsWith("/tintin/dispatch/") || reqPath.startsWith("/monkey/dispatch/")) {
|
|
6381
|
-
const access = checkTinTinBrowserAccess(req);
|
|
6382
|
-
if (!access.allowed) {
|
|
6383
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6384
|
-
return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
|
|
6385
|
-
}
|
|
6386
|
-
} else if (req.headers.origin) {
|
|
6387
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
6388
|
-
return res.end(JSON.stringify({ ok: false, error: "legacy_dispatch_rejects_browser_origin", use: "/tintin/dispatch/:job_id" }));
|
|
6389
|
-
}
|
|
6390
|
-
const jobId = decodeURIComponent(dispatchStatusMatch[1]);
|
|
6391
|
-
const job = tintinJobs.get(jobId);
|
|
6392
|
-
if (!job) {
|
|
6393
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
6394
|
-
return res.end(JSON.stringify({ error: "not_found", job_id: jobId }));
|
|
6395
|
-
}
|
|
6396
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
6397
|
-
return res.end(JSON.stringify(job));
|
|
6398
|
-
}
|
|
6399
|
-
|
|
6400
5364
|
// ── call_agent (Gate B) ───────────────────────────────────────────────────
|
|
6401
5365
|
// POST /call-agent — lean one-shot Haiku dispatch through the warm pool.
|
|
6402
5366
|
// sync (default): blocks, returns { ok, package, jobId, model, ms }.
|
|
@@ -6535,6 +5499,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6535
5499
|
try {
|
|
6536
5500
|
const { name } = JSON.parse(body || "{}");
|
|
6537
5501
|
const result = await startChitchatSession(name || "collab");
|
|
5502
|
+
operation("chitchat.start", { name: name || "collab" }, null, { ok: !result?.error, session_id: result?.session_id, error: result?.error });
|
|
6538
5503
|
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
6539
5504
|
res.end(JSON.stringify(result));
|
|
6540
5505
|
} catch (e) {
|
|
@@ -6545,14 +5510,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6545
5510
|
return;
|
|
6546
5511
|
}
|
|
6547
5512
|
|
|
6548
|
-
// POST /handoff
|
|
6549
|
-
if (method === "POST" &&
|
|
5513
|
+
// POST /handoff — visible local handoff from claude.ai or a sidebar.
|
|
5514
|
+
if (method === "POST" && reqPath === "/handoff") {
|
|
6550
5515
|
let body;
|
|
6551
5516
|
try { body = await readBody(req); } catch {
|
|
6552
5517
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
6553
5518
|
return res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
6554
5519
|
}
|
|
6555
5520
|
const result = await startHandoffSession(body || {});
|
|
5521
|
+
operation("handoff.start", {}, null, { ok: !result.error, error: result.error });
|
|
6556
5522
|
const status = result.error ? 503 : 200;
|
|
6557
5523
|
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
6558
5524
|
return res.end(JSON.stringify(result));
|
|
@@ -6570,6 +5536,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6570
5536
|
return res.end(JSON.stringify({ error: "session_id and message required" }));
|
|
6571
5537
|
}
|
|
6572
5538
|
const result = sendChitchatMessage(session_id, message);
|
|
5539
|
+
operation("chitchat.send", { session_id }, null, { ok: !result.error, error: result.error });
|
|
6573
5540
|
const status = result.error === 'not_found' ? 404 : result.error ? 400 : 200;
|
|
6574
5541
|
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
6575
5542
|
res.end(JSON.stringify(result));
|
|
@@ -6594,6 +5561,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6594
5561
|
}
|
|
6595
5562
|
const tier = knowledge_tier || 'db_only';
|
|
6596
5563
|
const result = startTerminalSession(name, tier, context_md || null);
|
|
5564
|
+
operation("terminal.start", { name, tier }, null, { ok: !result.error, error: result.error });
|
|
6597
5565
|
const status = result.error ? 503 : 200;
|
|
6598
5566
|
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
6599
5567
|
res.end(JSON.stringify(result));
|
|
@@ -6617,6 +5585,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6617
5585
|
return res.end(JSON.stringify({ error: "session_id and message required" }));
|
|
6618
5586
|
}
|
|
6619
5587
|
const result = sendTerminalMessage(session_id, message);
|
|
5588
|
+
operation("terminal.send", { session_id }, null, { ok: !result.error, error: result.error });
|
|
6620
5589
|
const status = result.error === 'session_busy' ? 409 : result.error ? 404 : 200;
|
|
6621
5590
|
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
6622
5591
|
res.end(JSON.stringify(result));
|
|
@@ -6670,6 +5639,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6670
5639
|
return res.end(JSON.stringify({ error: "session_id required" }));
|
|
6671
5640
|
}
|
|
6672
5641
|
const result = stopTerminalSession(session_id);
|
|
5642
|
+
operation("terminal.stop", { session_id }, null, { ok: !result.error, error: result.error });
|
|
6673
5643
|
const status = result.error ? 404 : 200;
|
|
6674
5644
|
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
6675
5645
|
res.end(JSON.stringify(result));
|
|
@@ -6729,6 +5699,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6729
5699
|
channelEvents.push(entry);
|
|
6730
5700
|
if (channelEvents.length > MAX_CHANNEL_EVENTS) channelEvents.shift();
|
|
6731
5701
|
console.log(`[channel] queued event ${eventId} type=${event}`);
|
|
5702
|
+
operation("channel.event", { event, resource, repository }, null, { ok: true, event_id: eventId });
|
|
6732
5703
|
return ok(res, { received: true, event_id: eventId });
|
|
6733
5704
|
} catch {
|
|
6734
5705
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
@@ -6741,6 +5712,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6741
5712
|
// GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
|
|
6742
5713
|
// Accept POST as well — older scripts and curl default to POST
|
|
6743
5714
|
if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
|
|
5715
|
+
operation("daemon.shutdown", { port }, null, { ok: true });
|
|
6744
5716
|
stopTunnel();
|
|
6745
5717
|
ok(res, { ok: true, message: "shutting down" });
|
|
6746
5718
|
removePid();
|
|
@@ -6750,6 +5722,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6750
5722
|
|
|
6751
5723
|
// POST /shutdown-ui (user-initiated stop — clears boot.key so password is required on restart)
|
|
6752
5724
|
if (method === "POST" && reqPath === "/shutdown-ui") {
|
|
5725
|
+
operation("daemon.shutdown_ui", { port }, null, { ok: true });
|
|
6753
5726
|
stopTunnel();
|
|
6754
5727
|
// Clear boot.key so watchdog can't auto-unlock on restart
|
|
6755
5728
|
const bootKeyPath = getBootKeyPath();
|
|
@@ -6831,10 +5804,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6831
5804
|
if (promoted) {
|
|
6832
5805
|
const okLog = `[${new Date().toISOString()}] Make-live: promoted to live on port ${LIVE_PORT}\n`;
|
|
6833
5806
|
try { fs.appendFileSync(LOG_FILE, okLog); } catch {}
|
|
5807
|
+
operation("daemon.make_live", { from_port: port, live_port: LIVE_PORT }, null, { ok: true });
|
|
6834
5808
|
ok(res, { ok: true, message: "promoted to live", live_port: LIVE_PORT });
|
|
6835
5809
|
} else {
|
|
6836
5810
|
const failLog = `[${new Date().toISOString()}] Make-live: new daemon failed to start on port ${LIVE_PORT}\n`;
|
|
6837
5811
|
try { fs.appendFileSync(LOG_FILE, failLog); } catch {}
|
|
5812
|
+
operation("daemon.make_live", { from_port: port, live_port: LIVE_PORT }, null, { ok: false, error: "new daemon failed to start" });
|
|
6838
5813
|
ok(res, { ok: false, error: "New daemon failed to start on live port — check log" });
|
|
6839
5814
|
}
|
|
6840
5815
|
|
|
@@ -7173,6 +6148,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7173
6148
|
tunnelStatus = "starting";
|
|
7174
6149
|
startTunnel().catch(() => {});
|
|
7175
6150
|
}
|
|
6151
|
+
operation("vault.unlock", {}, null, { ok: true });
|
|
7176
6152
|
return ok(res, { ok: true, locked: false, write_token: writeSession.token, write_expires_at: new Date(writeSession.expiresAt).toISOString() });
|
|
7177
6153
|
} catch (authErr) {
|
|
7178
6154
|
const msg = authErr.message || "";
|
|
@@ -7198,6 +6174,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7198
6174
|
// No strike, no hard-lock — the verdict was never rendered.
|
|
7199
6175
|
const failLog = `[${new Date().toISOString()}] [BACKEND ${backendKind}] vault backend unreachable, no auth strike — ${detail}\n`;
|
|
7200
6176
|
try { fs.appendFileSync(LOG_FILE, failLog); } catch {}
|
|
6177
|
+
operation("vault.unlock", {}, null, { ok: false, backend_error: true, kind: backendKind });
|
|
7201
6178
|
res.writeHead(503, { "Content-Type": "application/json", ...CORS });
|
|
7202
6179
|
return res.end(JSON.stringify({
|
|
7203
6180
|
error: friendly,
|
|
@@ -7222,6 +6199,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7222
6199
|
authHardLocked = true;
|
|
7223
6200
|
const lockLog = `[${new Date().toISOString()}] Server rejected with terminal verdict${reasonSuffix} — hard-locking locally to stop strike accrual; recover via the runbook (unlock machine + re-seal boot.key)\n`;
|
|
7224
6201
|
try { fs.appendFileSync(LOG_FILE, lockLog); } catch {}
|
|
6202
|
+
operation("vault.unlock", {}, null, { ok: false, terminal: true, reason: serverReason });
|
|
7225
6203
|
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
7226
6204
|
return res.end(JSON.stringify({ error: "Vault rejected credentials — recovery required", reason: serverReason, hard_locked: true, terminal: true }));
|
|
7227
6205
|
}
|
|
@@ -7229,9 +6207,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7229
6207
|
authHardLocked = true;
|
|
7230
6208
|
const lockLog = `[${new Date().toISOString()}] Auth failure limit reached — vault hard-locked\n`;
|
|
7231
6209
|
try { fs.appendFileSync(LOG_FILE, lockLog); } catch {}
|
|
6210
|
+
operation("vault.unlock", {}, null, { ok: false, hard_locked: true, reason: serverReason });
|
|
7232
6211
|
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
7233
6212
|
return res.end(JSON.stringify({ error: "Too many failed attempts — restart daemon to try again", reason: serverReason, hard_locked: true }));
|
|
7234
6213
|
}
|
|
6214
|
+
operation("vault.unlock", {}, null, { ok: false, reason: serverReason, failures_remaining: authRemaining });
|
|
7235
6215
|
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
7236
6216
|
return res.end(JSON.stringify({ error: "Invalid password", reason: serverReason, failures_remaining: authRemaining }));
|
|
7237
6217
|
}
|
|
@@ -7245,6 +6225,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7245
6225
|
stopTunnel();
|
|
7246
6226
|
const logLine = `[${new Date().toISOString()}] Vault locked\n`;
|
|
7247
6227
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
6228
|
+
operation("vault.lock", {}, null, { ok: true });
|
|
7248
6229
|
return ok(res, { ok: true, locked: true, hard_locked: authHardLocked });
|
|
7249
6230
|
}
|
|
7250
6231
|
|
|
@@ -7266,10 +6247,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7266
6247
|
try {
|
|
7267
6248
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
7268
6249
|
const result = await api.updateService(password, machineHash, token, timestamp, service, { name: newName, label: newName });
|
|
7269
|
-
if (result.error)
|
|
6250
|
+
if (result.error) {
|
|
6251
|
+
operation("service.rename", { service, new_name: newName }, null, { ok: false, error: result.error });
|
|
6252
|
+
return strike(res, 502, result.error);
|
|
6253
|
+
}
|
|
7270
6254
|
invalidateServiceStatusCache(machineHash);
|
|
6255
|
+
operation("service.rename", { service, new_name: newName }, null, { ok: true });
|
|
7271
6256
|
return ok(res, { ok: true, old_name: service, new_name: newName });
|
|
7272
6257
|
} catch (err) {
|
|
6258
|
+
operation("service.rename", { service, new_name: newName }, null, { ok: false, error: err.message });
|
|
7273
6259
|
return strike(res, 502, err.message);
|
|
7274
6260
|
}
|
|
7275
6261
|
}
|
|
@@ -7282,10 +6268,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7282
6268
|
try {
|
|
7283
6269
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
7284
6270
|
const result = await api.removeService(password, machineHash, token, timestamp, service, `CONFIRM REMOVE ${service.toUpperCase()}`);
|
|
7285
|
-
if (result.error)
|
|
6271
|
+
if (result.error) {
|
|
6272
|
+
operation("service.delete", { service }, null, { ok: false, error: result.error });
|
|
6273
|
+
return strike(res, 502, result.error);
|
|
6274
|
+
}
|
|
7286
6275
|
invalidateServiceStatusCache(machineHash);
|
|
6276
|
+
operation("service.delete", { service }, null, { ok: true });
|
|
7287
6277
|
return ok(res, { ok: true, deleted: service });
|
|
7288
6278
|
} catch (err) {
|
|
6279
|
+
operation("service.delete", { service }, null, { ok: false, error: err.message });
|
|
7289
6280
|
return strike(res, 502, err.message);
|
|
7290
6281
|
}
|
|
7291
6282
|
}
|
|
@@ -7311,10 +6302,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7311
6302
|
try {
|
|
7312
6303
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
7313
6304
|
const result = await api.enable(password, machineHash, token, timestamp, service, enabled);
|
|
7314
|
-
if (result.error)
|
|
6305
|
+
if (result.error) {
|
|
6306
|
+
operation("service.toggle", { service, enabled }, null, { ok: false, error: result.error });
|
|
6307
|
+
return strike(res, 502, result.error);
|
|
6308
|
+
}
|
|
7315
6309
|
invalidateServiceStatusCache(machineHash);
|
|
6310
|
+
operation("service.toggle", { service, enabled }, null, { ok: true });
|
|
7316
6311
|
return ok(res, { ok: true, service, enabled });
|
|
7317
6312
|
} catch (err) {
|
|
6313
|
+
operation("service.toggle", { service, enabled }, null, { ok: false, error: err.message });
|
|
7318
6314
|
return strike(res, 502, err.message);
|
|
7319
6315
|
}
|
|
7320
6316
|
}
|
|
@@ -7401,6 +6397,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7401
6397
|
return res.end(JSON.stringify({ error: "Service name required" }));
|
|
7402
6398
|
}
|
|
7403
6399
|
const result = await rotationEngine.rotateService(service);
|
|
6400
|
+
operation("service.rotate", { service }, null, { ok: !result?.error, error: result?.error });
|
|
7404
6401
|
return ok(res, result);
|
|
7405
6402
|
}
|
|
7406
6403
|
|
|
@@ -7414,6 +6411,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7414
6411
|
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
7415
6412
|
}
|
|
7416
6413
|
rotationEngine.setExpiry(service, body.expires_at, body.rotation_days);
|
|
6414
|
+
operation("service.set_expiry", { service }, null, { ok: true, expires_at: body.expires_at, rotation_days: body.rotation_days });
|
|
7417
6415
|
return ok(res, { ok: true, service, expires_at: body.expires_at });
|
|
7418
6416
|
}
|
|
7419
6417
|
|
|
@@ -7597,8 +6595,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7597
6595
|
});
|
|
7598
6596
|
}
|
|
7599
6597
|
|
|
6598
|
+
operation("tunnel.setup.cf_token", {}, null, { ok: true, accountId, accountName });
|
|
7600
6599
|
return ok(res, { ok: true, accountId, accountName });
|
|
7601
6600
|
} catch (err) {
|
|
6601
|
+
operation("tunnel.setup.cf_token", {}, null, { ok: false, error: err.message });
|
|
7602
6602
|
return strike(res, 502, err.message);
|
|
7603
6603
|
}
|
|
7604
6604
|
}
|
|
@@ -7669,8 +6669,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7669
6669
|
tunnelHostname = hostname;
|
|
7670
6670
|
tunnelUrl = `https://${hostname}`;
|
|
7671
6671
|
|
|
6672
|
+
operation("tunnel.setup.cf_save", { hostname, tunnelId }, null, { ok: true });
|
|
7672
6673
|
return ok(res, { ok: true, hostname });
|
|
7673
6674
|
} catch (err) {
|
|
6675
|
+
operation("tunnel.setup.cf_save", { hostname }, null, { ok: false, error: err.message });
|
|
7674
6676
|
return strike(res, 502, err.message);
|
|
7675
6677
|
}
|
|
7676
6678
|
}
|
|
@@ -7750,8 +6752,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7750
6752
|
tunnelHostname = hostname;
|
|
7751
6753
|
tunnelUrl = `https://${hostname}`;
|
|
7752
6754
|
|
|
6755
|
+
operation("tunnel.setup.cf_create_api", { hostname, tunnelId }, null, { ok: true });
|
|
7753
6756
|
return ok(res, { ok: true, tunnelId, hostname });
|
|
7754
6757
|
} catch (err) {
|
|
6758
|
+
operation("tunnel.setup.cf_create_api", { hostname }, null, { ok: false, error: err.message });
|
|
7755
6759
|
return strike(res, 502, err.message);
|
|
7756
6760
|
}
|
|
7757
6761
|
}
|
|
@@ -7792,9 +6796,13 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7792
6796
|
const proc = spawn("cloudflared", ["tunnel", "login"], { stdio: ["ignore","pipe","pipe"], windowsHide: true });
|
|
7793
6797
|
proc.stdout.on("data", d => d.toString().split("\n").forEach(l => l.trim() && sendEvt({ line: l })));
|
|
7794
6798
|
proc.stderr.on("data", d => d.toString().split("\n").forEach(l => l.trim() && sendEvt({ line: l })));
|
|
7795
|
-
proc.on("close", code => {
|
|
6799
|
+
proc.on("close", code => {
|
|
6800
|
+
operation("tunnel.setup.cf_login", {}, null, { ok: code === 0, exit_code: code });
|
|
6801
|
+
sendEvt({ done: true, code }); res.end();
|
|
6802
|
+
});
|
|
7796
6803
|
req.on("close", () => { try { proc.kill(); } catch {} });
|
|
7797
6804
|
} catch (err) {
|
|
6805
|
+
operation("tunnel.setup.cf_login", {}, null, { ok: false, error: err.message });
|
|
7798
6806
|
sendEvt({ done: true, code: 1, error: err.message });
|
|
7799
6807
|
res.end();
|
|
7800
6808
|
}
|
|
@@ -7866,9 +6874,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7866
6874
|
tunnelHostname = hostname;
|
|
7867
6875
|
tunnelUrl = `https://${hostname}`;
|
|
7868
6876
|
|
|
6877
|
+
operation("tunnel.setup.cf_create", { name, hostname, tunnelId }, null, { ok: true });
|
|
7869
6878
|
sendEvt({ done: true, tunnelId, hostname });
|
|
7870
6879
|
res.end();
|
|
7871
6880
|
} catch (err) {
|
|
6881
|
+
operation("tunnel.setup.cf_create", { name, hostname }, null, { ok: false, error: err.message });
|
|
7872
6882
|
sendEvt({ error: err.message, done: true });
|
|
7873
6883
|
res.end();
|
|
7874
6884
|
}
|
|
@@ -7900,8 +6910,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7900
6910
|
writeSession = makeWriteToken();
|
|
7901
6911
|
const logLine = `[${new Date().toISOString()}] Password changed\n`;
|
|
7902
6912
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
6913
|
+
operation("vault.change_password", {}, null, { ok: true });
|
|
7903
6914
|
return ok(res, { ok: true, write_token: writeSession.token, write_expires_at: new Date(writeSession.expiresAt).toISOString() });
|
|
7904
6915
|
} catch (err) {
|
|
6916
|
+
operation("vault.change_password", {}, null, { ok: false, error: err.message });
|
|
7905
6917
|
res.writeHead(502, { "Content-Type": "application/json", ...CORS });
|
|
7906
6918
|
return res.end(JSON.stringify({ error: err.message }));
|
|
7907
6919
|
}
|
|
@@ -7929,11 +6941,17 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7929
6941
|
}
|
|
7930
6942
|
|
|
7931
6943
|
try {
|
|
6944
|
+
// NEVER log `value` — it is the secret being written.
|
|
7932
6945
|
const { result, snapshot, normalized } = await writeCredentialWithRecovery({ password, machineHash, service, value, logFile: LOG_FILE });
|
|
7933
|
-
if (result.error)
|
|
6946
|
+
if (result.error) {
|
|
6947
|
+
operation("service.set", { service }, null, { ok: false, error: result.error });
|
|
6948
|
+
return strike(res, 502, result.error);
|
|
6949
|
+
}
|
|
7934
6950
|
invalidateServiceStatusCache(machineHash);
|
|
6951
|
+
operation("service.set", { service }, null, { ok: true, normalized });
|
|
7935
6952
|
return ok(res, { ok: true, service, recovery_snapshot: snapshot?.ok ? true : false, normalized });
|
|
7936
6953
|
} catch (err) {
|
|
6954
|
+
operation("service.set", { service }, null, { ok: false, error: err.message });
|
|
7937
6955
|
return strike(res, 502, err.message);
|
|
7938
6956
|
}
|
|
7939
6957
|
}
|
|
@@ -7962,11 +6980,17 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7962
6980
|
try {
|
|
7963
6981
|
const randomHex = crypto.randomBytes(32).toString("hex");
|
|
7964
6982
|
const token = `${prefix}${randomHex}`;
|
|
6983
|
+
// NEVER log `token` — it is the secret being generated/stored.
|
|
7965
6984
|
const { result, snapshot } = await writeCredentialWithRecovery({ password, machineHash, service, value: token, logFile: LOG_FILE, normalize: false });
|
|
7966
|
-
if (result.error)
|
|
6985
|
+
if (result.error) {
|
|
6986
|
+
operation("service.generate_token", { service }, null, { ok: false, error: result.error });
|
|
6987
|
+
return strike(res, 502, result.error);
|
|
6988
|
+
}
|
|
7967
6989
|
invalidateServiceStatusCache(machineHash);
|
|
6990
|
+
operation("service.generate_token", { service }, null, { ok: true });
|
|
7968
6991
|
return ok(res, { token, service, stored: true, recovery_snapshot: snapshot?.ok ? true : false });
|
|
7969
6992
|
} catch (err) {
|
|
6993
|
+
operation("service.generate_token", { service }, null, { ok: false, error: err.message });
|
|
7970
6994
|
return strike(res, 502, err.message);
|
|
7971
6995
|
}
|
|
7972
6996
|
}
|
|
@@ -7996,10 +7020,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7996
7020
|
try {
|
|
7997
7021
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
7998
7022
|
const result = await api.addService(password, machineHash, token, timestamp, name.trim().toLowerCase(), label || name.trim(), type, description || "", project || undefined);
|
|
7999
|
-
if (result.error)
|
|
7023
|
+
if (result.error) {
|
|
7024
|
+
operation("service.add", { name: name.trim().toLowerCase() }, null, { ok: false, error: result.error });
|
|
7025
|
+
return strike(res, 502, result.error);
|
|
7026
|
+
}
|
|
8000
7027
|
invalidateServiceStatusCache(machineHash);
|
|
7028
|
+
operation("service.add", { name: name.trim().toLowerCase() }, null, { ok: true, key_type: type });
|
|
8001
7029
|
return ok(res, { ok: true, service: name.trim().toLowerCase() });
|
|
8002
7030
|
} catch (err) {
|
|
7031
|
+
operation("service.add", { name: name.trim().toLowerCase() }, null, { ok: false, error: err.message });
|
|
8003
7032
|
return strike(res, 502, err.message);
|
|
8004
7033
|
}
|
|
8005
7034
|
}
|
|
@@ -8033,10 +7062,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
8033
7062
|
try {
|
|
8034
7063
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
8035
7064
|
const result = await api.updateService(password, machineHash, token, timestamp, service.toLowerCase(), updates);
|
|
8036
|
-
if (result.error)
|
|
7065
|
+
if (result.error) {
|
|
7066
|
+
operation("service.update", { name: service.toLowerCase() }, null, { ok: false, error: result.error });
|
|
7067
|
+
return strike(res, 502, result.error);
|
|
7068
|
+
}
|
|
8037
7069
|
invalidateServiceStatusCache(machineHash);
|
|
7070
|
+
operation("service.update", { name: service.toLowerCase() }, null, { ok: true, fields: Object.keys(updates) });
|
|
8038
7071
|
return ok(res, { ok: true, service: service.toLowerCase(), ...updates });
|
|
8039
7072
|
} catch (err) {
|
|
7073
|
+
operation("service.update", { name: service.toLowerCase() }, null, { ok: false, error: err.message });
|
|
8040
7074
|
return strike(res, 502, err.message);
|
|
8041
7075
|
}
|
|
8042
7076
|
}
|
|
@@ -8087,11 +7121,16 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
8087
7121
|
try {
|
|
8088
7122
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
8089
7123
|
const result = await api.createEnrollment(password, machineHash, token, timestamp, label, ttlMinutes);
|
|
8090
|
-
if (result.error)
|
|
7124
|
+
if (result.error) {
|
|
7125
|
+
operation("machine.enroll", { label, target }, null, { ok: false, error: result.error });
|
|
7126
|
+
return strike(res, 502, result.error);
|
|
7127
|
+
}
|
|
8091
7128
|
const localConfig = new Conf(getConfOptions());
|
|
8092
7129
|
const supabaseUrl = localConfig.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL || "";
|
|
8093
7130
|
const anonKey = localConfig.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY || "";
|
|
7131
|
+
// NEVER log `enrollment_code` — it is a one-time credential.
|
|
8094
7132
|
const scriptPath = writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode: result.enrollment_code, label, target });
|
|
7133
|
+
operation("machine.enroll", { label, target }, null, { ok: true, expires_at: result.expires_at });
|
|
8095
7134
|
return ok(res, {
|
|
8096
7135
|
ok: true,
|
|
8097
7136
|
enrollment_code: result.enrollment_code,
|
|
@@ -8099,6 +7138,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
8099
7138
|
script_path: scriptPath,
|
|
8100
7139
|
});
|
|
8101
7140
|
} catch (err) {
|
|
7141
|
+
operation("machine.enroll", { label, target }, null, { ok: false, error: err.message });
|
|
8102
7142
|
return strike(res, 502, err.message);
|
|
8103
7143
|
}
|
|
8104
7144
|
}
|
|
@@ -8561,9 +7601,35 @@ async function actionForeground(opts) {
|
|
|
8561
7601
|
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
8562
7602
|
console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
|
|
8563
7603
|
|
|
7604
|
+
// Plugin discovery previously only ran under `serve supervisor` or an
|
|
7605
|
+
// on-demand rescan — normal `serve start`/`foreground` boot never ran it,
|
|
7606
|
+
// so a stale/never-discovered plugin list could sit unnoticed until
|
|
7607
|
+
// someone happened to hit rescan. Discovery touching disk must never be
|
|
7608
|
+
// allowed to crash daemon boot, hence the try/catch.
|
|
7609
|
+
let discoveryResult = null;
|
|
7610
|
+
let discoveryError = null;
|
|
7611
|
+
try {
|
|
7612
|
+
discoveryResult = discoverPlugins();
|
|
7613
|
+
} catch (err) {
|
|
7614
|
+
discoveryError = err;
|
|
7615
|
+
console.log(chalk.yellow(` ⚠ plugin discovery failed at boot: ${err.message}`));
|
|
7616
|
+
}
|
|
7617
|
+
|
|
8564
7618
|
const server = createServer(password, whitelist, port, tunnelHostname);
|
|
8565
7619
|
server.listen(port, bindHost, () => {
|
|
8566
7620
|
if (!isolated) writePid(process.pid, port);
|
|
7621
|
+
try {
|
|
7622
|
+
operation("daemon.started", { port, isolated }, null, { ok: true, version: VERSION }, "system");
|
|
7623
|
+
operation(
|
|
7624
|
+
"daemon.plugin_discovery",
|
|
7625
|
+
{ port },
|
|
7626
|
+
null,
|
|
7627
|
+
discoveryError
|
|
7628
|
+
? { ok: false, error: discoveryError.message }
|
|
7629
|
+
: { ok: true, plugin_count: (discoveryResult?.plugins || discoveryResult || []).length },
|
|
7630
|
+
"system",
|
|
7631
|
+
);
|
|
7632
|
+
} catch { /* startup logging must never block the daemon from serving */ }
|
|
8567
7633
|
console.log(chalk.green(` clauth serve → http://${bindHost}:${port}`));
|
|
8568
7634
|
if (tunnelHostname) {
|
|
8569
7635
|
console.log(chalk.cyan(` Tunnel: https://${tunnelHostname}/sse`));
|
|
@@ -8604,7 +7670,7 @@ async function actionForeground(opts) {
|
|
|
8604
7670
|
import { createInterface } from "readline";
|
|
8605
7671
|
import { execSync, spawn as spawnProc, spawnSync } from "child_process";
|
|
8606
7672
|
|
|
8607
|
-
// ──
|
|
7673
|
+
// ── Shared headless Claude CLI worker helpers (used by call-agent, terminal, codevelop) ───
|
|
8608
7674
|
function findClaudeBinary() {
|
|
8609
7675
|
const candidates = [
|
|
8610
7676
|
process.env.CLAUDE_BIN,
|
|
@@ -9095,7 +8161,7 @@ async function runCallAgent(args = {}) {
|
|
|
9095
8161
|
const persistMeta = { prompt, skill_slug: args.skill_slug, model, agent_context: args.agent_context };
|
|
9096
8162
|
|
|
9097
8163
|
if (mode === "async") {
|
|
9098
|
-
const jobId =
|
|
8164
|
+
const jobId = `call-${crypto.randomUUID()}`;
|
|
9099
8165
|
// Persist the queued row FIRST so the { jobId } we return is recoverable
|
|
9100
8166
|
// even if the daemon restarts before the worker settles.
|
|
9101
8167
|
await persistCallAgentDispatch({ jobId, ...persistMeta });
|
|
@@ -9119,307 +8185,6 @@ async function runCallAgent(args = {}) {
|
|
|
9119
8185
|
return { ok: true, package: rec.package, jobId: rec.jobId, model: rec.model, ms: rec.ms };
|
|
9120
8186
|
}
|
|
9121
8187
|
|
|
9122
|
-
const tintinJobs = new Map();
|
|
9123
|
-
const MAX_TINTIN_JOBS = 100;
|
|
9124
|
-
const tintinSessions = new Map();
|
|
9125
|
-
const tintinMessageIndex = new Map();
|
|
9126
|
-
const tintinEventStreams = new Map();
|
|
9127
|
-
const tintinMessageMonitors = new Map();
|
|
9128
|
-
const MAX_TINTIN_SESSION_EVENTS = 500;
|
|
9129
|
-
|
|
9130
|
-
function normalizeAgentContext(input = {}) {
|
|
9131
|
-
const context = input && typeof input === "object" ? input : {};
|
|
9132
|
-
const app = context.app && typeof context.app === "object" ? context.app : {};
|
|
9133
|
-
const repo = context.repo && typeof context.repo === "object" ? context.repo : {};
|
|
9134
|
-
const runtime = context.runtime && typeof context.runtime === "object" ? context.runtime : {};
|
|
9135
|
-
const task = context.task && typeof context.task === "object" ? context.task : {};
|
|
9136
|
-
|
|
9137
|
-
return {
|
|
9138
|
-
app: {
|
|
9139
|
-
slug: String(app.slug || context.app_slug || "unknown-app"),
|
|
9140
|
-
route: String(app.route || context.route || "/"),
|
|
9141
|
-
origin: String(app.origin || context.origin || "unknown-origin"),
|
|
9142
|
-
},
|
|
9143
|
-
repo: {
|
|
9144
|
-
root: String(repo.root || context.repo_root || ""),
|
|
9145
|
-
cwd: String(repo.cwd || context.cwd || ""),
|
|
9146
|
-
},
|
|
9147
|
-
runtime: {
|
|
9148
|
-
agent: String(runtime.agent || context.agent || "clauth-cli"),
|
|
9149
|
-
requested_by: String(runtime.requested_by || context.requested_by || "tintin"),
|
|
9150
|
-
},
|
|
9151
|
-
task: {
|
|
9152
|
-
intent: String(task.intent || context.intent || "tintin-dispatch"),
|
|
9153
|
-
thread_id: task.thread_id || context.thread_id || null,
|
|
9154
|
-
},
|
|
9155
|
-
};
|
|
9156
|
-
}
|
|
9157
|
-
|
|
9158
|
-
function makeTinTinId(prefix) {
|
|
9159
|
-
return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
9160
|
-
}
|
|
9161
|
-
|
|
9162
|
-
function createTinTinSession(input = {}) {
|
|
9163
|
-
const sessionId = input.session_id || input.thread_id || makeTinTinId("ms");
|
|
9164
|
-
const agentContext = normalizeAgentContext(input.agent_context || {
|
|
9165
|
-
app: {
|
|
9166
|
-
slug: input.app_id || input.app_slug || "unknown-app",
|
|
9167
|
-
route: input.route || "/",
|
|
9168
|
-
origin: input.origin || "unknown-origin",
|
|
9169
|
-
},
|
|
9170
|
-
repo: {
|
|
9171
|
-
root: input.repo_root || "",
|
|
9172
|
-
cwd: input.cwd || "",
|
|
9173
|
-
},
|
|
9174
|
-
runtime: {
|
|
9175
|
-
agent: "clauth-cli",
|
|
9176
|
-
requested_by: "tintin-sidebar",
|
|
9177
|
-
},
|
|
9178
|
-
task: {
|
|
9179
|
-
intent: input.intent || "general_chat",
|
|
9180
|
-
thread_id: sessionId,
|
|
9181
|
-
},
|
|
9182
|
-
});
|
|
9183
|
-
const existing = tintinSessions.get(sessionId);
|
|
9184
|
-
if (existing) {
|
|
9185
|
-
existing.agent_context = agentContext;
|
|
9186
|
-
existing.updated_at = new Date().toISOString();
|
|
9187
|
-
tintinSessions.set(sessionId, existing);
|
|
9188
|
-
return existing;
|
|
9189
|
-
}
|
|
9190
|
-
const session = {
|
|
9191
|
-
id: sessionId,
|
|
9192
|
-
status: "ready",
|
|
9193
|
-
created_at: new Date().toISOString(),
|
|
9194
|
-
updated_at: new Date().toISOString(),
|
|
9195
|
-
seq: 0,
|
|
9196
|
-
agent_context: agentContext,
|
|
9197
|
-
cwd: input.cwd || agentContext.repo.cwd || agentContext.repo.root || "",
|
|
9198
|
-
events: [],
|
|
9199
|
-
};
|
|
9200
|
-
tintinSessions.set(sessionId, session);
|
|
9201
|
-
return session;
|
|
9202
|
-
}
|
|
9203
|
-
|
|
9204
|
-
function getTinTinSession(sessionId) {
|
|
9205
|
-
return tintinSessions.get(sessionId);
|
|
9206
|
-
}
|
|
9207
|
-
|
|
9208
|
-
function pushTinTinEvent(sessionId, event) {
|
|
9209
|
-
const session = getTinTinSession(sessionId);
|
|
9210
|
-
if (!session) return null;
|
|
9211
|
-
session.seq += 1;
|
|
9212
|
-
session.updated_at = new Date().toISOString();
|
|
9213
|
-
const fullEvent = {
|
|
9214
|
-
id: event.id || makeTinTinId("mev"),
|
|
9215
|
-
session_id: sessionId,
|
|
9216
|
-
seq: session.seq,
|
|
9217
|
-
source: event.source || "clauth",
|
|
9218
|
-
type: event.type || "status",
|
|
9219
|
-
role: event.role,
|
|
9220
|
-
message_id: event.message_id,
|
|
9221
|
-
job_id: event.job_id,
|
|
9222
|
-
content: event.content,
|
|
9223
|
-
payload: event.payload || {},
|
|
9224
|
-
created_at: new Date().toISOString(),
|
|
9225
|
-
};
|
|
9226
|
-
session.events.push(fullEvent);
|
|
9227
|
-
session.events = session.events.slice(-MAX_TINTIN_SESSION_EVENTS);
|
|
9228
|
-
tintinSessions.set(sessionId, session);
|
|
9229
|
-
const streams = tintinEventStreams.get(sessionId);
|
|
9230
|
-
if (streams) {
|
|
9231
|
-
for (const send of [...streams]) {
|
|
9232
|
-
try {
|
|
9233
|
-
send(fullEvent);
|
|
9234
|
-
} catch {
|
|
9235
|
-
streams.delete(send);
|
|
9236
|
-
}
|
|
9237
|
-
}
|
|
9238
|
-
}
|
|
9239
|
-
return fullEvent;
|
|
9240
|
-
}
|
|
9241
|
-
|
|
9242
|
-
function listTinTinEvents(sessionId, afterSeq = 0) {
|
|
9243
|
-
const session = getTinTinSession(sessionId);
|
|
9244
|
-
if (!session) return null;
|
|
9245
|
-
return session.events.filter((event) => event.seq > afterSeq);
|
|
9246
|
-
}
|
|
9247
|
-
|
|
9248
|
-
function subscribeTinTinEvents(sessionId, send) {
|
|
9249
|
-
const streams = tintinEventStreams.get(sessionId) || new Set();
|
|
9250
|
-
streams.add(send);
|
|
9251
|
-
tintinEventStreams.set(sessionId, streams);
|
|
9252
|
-
return () => {
|
|
9253
|
-
const current = tintinEventStreams.get(sessionId);
|
|
9254
|
-
if (!current) return;
|
|
9255
|
-
current.delete(send);
|
|
9256
|
-
if (current.size === 0) tintinEventStreams.delete(sessionId);
|
|
9257
|
-
};
|
|
9258
|
-
}
|
|
9259
|
-
|
|
9260
|
-
function startTinTinMessageMonitor(sessionId, messageId, jobId) {
|
|
9261
|
-
const key = `${sessionId}:${messageId}`;
|
|
9262
|
-
if (tintinMessageMonitors.has(key)) return;
|
|
9263
|
-
let lastStdoutLength = 0;
|
|
9264
|
-
const timer = setInterval(() => {
|
|
9265
|
-
const job = tintinJobs.get(jobId);
|
|
9266
|
-
if (!job) return;
|
|
9267
|
-
const stdout = job.stdout || "";
|
|
9268
|
-
if (stdout.length > lastStdoutLength) {
|
|
9269
|
-
const chunk = stdout.slice(lastStdoutLength);
|
|
9270
|
-
lastStdoutLength = stdout.length;
|
|
9271
|
-
pushTinTinEvent(sessionId, {
|
|
9272
|
-
source: "claude",
|
|
9273
|
-
type: "delta",
|
|
9274
|
-
role: "assistant",
|
|
9275
|
-
message_id: messageId,
|
|
9276
|
-
job_id: jobId,
|
|
9277
|
-
content: chunk,
|
|
9278
|
-
});
|
|
9279
|
-
}
|
|
9280
|
-
if (job.status !== "running") {
|
|
9281
|
-
clearInterval(timer);
|
|
9282
|
-
tintinMessageMonitors.delete(key);
|
|
9283
|
-
pushTinTinEvent(sessionId, {
|
|
9284
|
-
source: "clauth",
|
|
9285
|
-
type: job.status === "completed" ? "done" : "error",
|
|
9286
|
-
role: "assistant",
|
|
9287
|
-
message_id: messageId,
|
|
9288
|
-
job_id: jobId,
|
|
9289
|
-
content: job.status === "completed" ? "" : (job.stderr || job.stdout || "TinTin worker failed"),
|
|
9290
|
-
payload: {
|
|
9291
|
-
status: job.status,
|
|
9292
|
-
exit_code: job.exit_code,
|
|
9293
|
-
completed_at: job.completed_at,
|
|
9294
|
-
},
|
|
9295
|
-
});
|
|
9296
|
-
}
|
|
9297
|
-
}, 500);
|
|
9298
|
-
tintinMessageMonitors.set(key, timer);
|
|
9299
|
-
}
|
|
9300
|
-
|
|
9301
|
-
function resolveDispatchCwd(requestedCwd, agentContext = {}) {
|
|
9302
|
-
const raw = requestedCwd || agentContext?.repo?.cwd || agentContext?.repo?.root || CHITCHAT_FALLBACK_CWD;
|
|
9303
|
-
if (!raw || typeof raw !== "string") return CHITCHAT_FALLBACK_CWD;
|
|
9304
|
-
const resolved = path.resolve(raw);
|
|
9305
|
-
try {
|
|
9306
|
-
const st = fs.statSync(resolved);
|
|
9307
|
-
if (!st.isDirectory()) return CHITCHAT_FALLBACK_CWD;
|
|
9308
|
-
return resolved;
|
|
9309
|
-
} catch {
|
|
9310
|
-
return CHITCHAT_FALLBACK_CWD;
|
|
9311
|
-
}
|
|
9312
|
-
}
|
|
9313
|
-
|
|
9314
|
-
function buildTinTinPrompt({ prompt, job_id, agent_context }) {
|
|
9315
|
-
const normalized = normalizeAgentContext(agent_context);
|
|
9316
|
-
const jobInstruction = job_id
|
|
9317
|
-
? [
|
|
9318
|
-
`TinTin job id: ${job_id}`,
|
|
9319
|
-
"If the prompt does not contain all job details, use clauth at http://127.0.0.1:52437 to fetch exact credentials and query the app database/API directly.",
|
|
9320
|
-
"Do not assume agent MCP/plugin tools are available to this spawned CLI process.",
|
|
9321
|
-
].join("\n")
|
|
9322
|
-
: "";
|
|
9323
|
-
|
|
9324
|
-
const body = prompt || [
|
|
9325
|
-
"Process this TinTin job using the provided agent context.",
|
|
9326
|
-
jobInstruction,
|
|
9327
|
-
"Find the job payload from the consuming app's declared data path before making changes.",
|
|
9328
|
-
].filter(Boolean).join("\n\n");
|
|
9329
|
-
|
|
9330
|
-
return [
|
|
9331
|
-
"You are a clauth-spawned TinTin CLI agent (model: claude-sonnet-4-6).",
|
|
9332
|
-
"Agent context setup follows. Treat it as the runtime contract for this task.",
|
|
9333
|
-
"```json",
|
|
9334
|
-
JSON.stringify(normalized, null, 2),
|
|
9335
|
-
"```",
|
|
9336
|
-
"",
|
|
9337
|
-
"Execution rules:",
|
|
9338
|
-
"- You are a local CLI process, not the parent agent runtime.",
|
|
9339
|
-
"- You do not inherit Codex/Claude MCP or plugin tools.",
|
|
9340
|
-
"- Use clauth HTTP, direct HTTP APIs, repo files, or app endpoints for access.",
|
|
9341
|
-
"- Keep results tied to the supplied app, route, cwd, thread, and job id.",
|
|
9342
|
-
"",
|
|
9343
|
-
jobInstruction,
|
|
9344
|
-
"",
|
|
9345
|
-
body,
|
|
9346
|
-
].filter(Boolean).join("\n");
|
|
9347
|
-
}
|
|
9348
|
-
|
|
9349
|
-
function spawnClaudeTask(prompt, jobId, cwd, agentContext) {
|
|
9350
|
-
if (activeCliWorkers >= MAX_CLI_WORKERS) {
|
|
9351
|
-
return { error: 'concurrency_limit', message: `Max ${MAX_CLI_WORKERS} CLI workers active` };
|
|
9352
|
-
}
|
|
9353
|
-
const binary = findClaudeBinary();
|
|
9354
|
-
if (!binary) {
|
|
9355
|
-
return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
|
|
9356
|
-
}
|
|
9357
|
-
|
|
9358
|
-
const resolvedCwd = resolveDispatchCwd(cwd, agentContext);
|
|
9359
|
-
activeCliWorkers++;
|
|
9360
|
-
const startedAtIso = new Date().toISOString();
|
|
9361
|
-
tintinJobs.set(jobId, {
|
|
9362
|
-
jobId,
|
|
9363
|
-
status: "running",
|
|
9364
|
-
cwd: resolvedCwd,
|
|
9365
|
-
pid: null,
|
|
9366
|
-
started_at: startedAtIso,
|
|
9367
|
-
completed_at: null,
|
|
9368
|
-
exit_code: null,
|
|
9369
|
-
stdout: "",
|
|
9370
|
-
stderr: "",
|
|
9371
|
-
context: normalizeAgentContext(agentContext),
|
|
9372
|
-
});
|
|
9373
|
-
while (tintinJobs.size > MAX_TINTIN_JOBS) {
|
|
9374
|
-
const firstKey = tintinJobs.keys().next().value;
|
|
9375
|
-
tintinJobs.delete(firstKey);
|
|
9376
|
-
}
|
|
9377
|
-
const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(binary);
|
|
9378
|
-
const command = isCmdShim ? "cmd" : binary;
|
|
9379
|
-
const args = isCmdShim
|
|
9380
|
-
? ["/d", "/s", "/c", `"${binary}"`, "-p", prompt, "--dangerously-skip-permissions"]
|
|
9381
|
-
: ["-p", prompt, "--dangerously-skip-permissions"];
|
|
9382
|
-
const proc = spawnProc(command, args, {
|
|
9383
|
-
cwd: resolvedCwd,
|
|
9384
|
-
env: process.env,
|
|
9385
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9386
|
-
shell: false,
|
|
9387
|
-
windowsHide: true,
|
|
9388
|
-
});
|
|
9389
|
-
|
|
9390
|
-
const tracked = tintinJobs.get(jobId);
|
|
9391
|
-
if (tracked) {
|
|
9392
|
-
tracked.pid = proc.pid;
|
|
9393
|
-
tintinJobs.set(jobId, tracked);
|
|
9394
|
-
}
|
|
9395
|
-
|
|
9396
|
-
const startedAt = Date.now();
|
|
9397
|
-
proc.stdout?.on("data", (chunk) => {
|
|
9398
|
-
const trackedJob = tintinJobs.get(jobId);
|
|
9399
|
-
if (!trackedJob) return;
|
|
9400
|
-
trackedJob.stdout = `${trackedJob.stdout}${chunk.toString()}`.slice(-20000);
|
|
9401
|
-
tintinJobs.set(jobId, trackedJob);
|
|
9402
|
-
});
|
|
9403
|
-
proc.stderr?.on("data", (chunk) => {
|
|
9404
|
-
const trackedJob = tintinJobs.get(jobId);
|
|
9405
|
-
if (!trackedJob) return;
|
|
9406
|
-
trackedJob.stderr = `${trackedJob.stderr}${chunk.toString()}`.slice(-20000);
|
|
9407
|
-
tintinJobs.set(jobId, trackedJob);
|
|
9408
|
-
});
|
|
9409
|
-
proc.on('close', (code) => {
|
|
9410
|
-
activeCliWorkers--;
|
|
9411
|
-
const trackedJob = tintinJobs.get(jobId);
|
|
9412
|
-
if (trackedJob) {
|
|
9413
|
-
trackedJob.status = code === 0 ? "completed" : "failed";
|
|
9414
|
-
trackedJob.exit_code = code;
|
|
9415
|
-
trackedJob.completed_at = new Date().toISOString();
|
|
9416
|
-
tintinJobs.set(jobId, trackedJob);
|
|
9417
|
-
}
|
|
9418
|
-
console.log(`[tintin] job ${jobId} exited code=${code} in ${Date.now() - startedAt}ms`);
|
|
9419
|
-
});
|
|
9420
|
-
|
|
9421
|
-
return { status: 'spawned', pid: proc.pid, jobId, cwd: resolvedCwd, activeWorkers: activeCliWorkers };
|
|
9422
|
-
}
|
|
9423
8188
|
|
|
9424
8189
|
// ── Terminal session manager ─────────────────────────────────────
|
|
9425
8190
|
// Rolling-context approach: each session stores a context string.
|
|
@@ -9437,7 +8202,7 @@ function generateSessionId() {
|
|
|
9437
8202
|
}
|
|
9438
8203
|
|
|
9439
8204
|
function defaultTerminalCwd() {
|
|
9440
|
-
const dir = process.env.CLAUTH_TERMINAL_CWD || path.join(os.tmpdir(), "clauth-
|
|
8205
|
+
const dir = process.env.CLAUTH_TERMINAL_CWD || path.join(os.tmpdir(), "clauth-terminal");
|
|
9441
8206
|
try {
|
|
9442
8207
|
fs.mkdirSync(dir, { recursive: true });
|
|
9443
8208
|
} catch {
|
|
@@ -9471,7 +8236,7 @@ function startTerminalSession(name, knowledge_tier, context_md, cwd, use_warm =
|
|
|
9471
8236
|
};
|
|
9472
8237
|
|
|
9473
8238
|
// Try to acquire a warm worker only when explicitly requested. The warm pool
|
|
9474
|
-
// is optimized for call_agent one-shots;
|
|
8239
|
+
// is optimized for call_agent one-shots; a terminal session needs reliable
|
|
9475
8240
|
// repeated turns more than experimental low-latency pinning.
|
|
9476
8241
|
if (use_warm) {
|
|
9477
8242
|
try {
|
|
@@ -9679,12 +8444,12 @@ function showTerminalSession(session_id) {
|
|
|
9679
8444
|
|
|
9680
8445
|
if (process.platform === "win32") {
|
|
9681
8446
|
try {
|
|
9682
|
-
const title = `
|
|
8447
|
+
const title = `clauth terminal ${session.name || session_id}`;
|
|
9683
8448
|
const lines = [
|
|
9684
8449
|
`$Host.UI.RawUI.WindowTitle = ${JSON.stringify(title)}`,
|
|
9685
8450
|
`Set-Location -LiteralPath ${JSON.stringify(session.cwd || os.homedir())}`,
|
|
9686
8451
|
"Clear-Host",
|
|
9687
|
-
`Write-Host ${JSON.stringify(`
|
|
8452
|
+
`Write-Host ${JSON.stringify(`clauth terminal session ${session_id}`)} -ForegroundColor Cyan`,
|
|
9688
8453
|
`Write-Host ${JSON.stringify(`Name: ${session.name || ""}`)}`,
|
|
9689
8454
|
`Write-Host ${JSON.stringify(`Status: ${session.status}`)}`,
|
|
9690
8455
|
`Write-Host ${JSON.stringify(`CWD: ${session.cwd || ""}`)}`,
|
|
@@ -10653,7 +9418,7 @@ function isAllowedGitImportPath(p, allowedPrefixes = FS_GIT_IMPORT_ALLOWED_PREFI
|
|
|
10653
9418
|
return allowedPrefixes.some((prefix) => p === prefix.replace(/\/$/, "") || p.startsWith(prefix));
|
|
10654
9419
|
}
|
|
10655
9420
|
|
|
10656
|
-
const MCP_TOOLS = [
|
|
9421
|
+
export const MCP_TOOLS = [
|
|
10657
9422
|
{
|
|
10658
9423
|
name: "clauth_ping",
|
|
10659
9424
|
description: "Check if the vault is locked or unlocked, show failure count",
|
|
@@ -10803,7 +9568,7 @@ const MCP_TOOLS = [
|
|
|
10803
9568
|
},
|
|
10804
9569
|
{
|
|
10805
9570
|
name: "call_agent",
|
|
10806
|
-
description: "Call a headless Haiku agent for a single prompt or skill and get the answer back. Lean one-shot through a warm pool (model default claude-haiku-4-5, ~4-5s warm). sync mode (default) blocks and returns { ok, package, jobId, model, ms }; async returns { ok, jobId } for polling.
|
|
9571
|
+
description: "Call a headless Haiku agent for a single prompt or skill and get the answer back. Lean one-shot through a warm pool (model default claude-haiku-4-5, ~4-5s warm). sync mode (default) blocks and returns { ok, package, jobId, model, ms }; async returns { ok, jobId } for polling.",
|
|
10807
9572
|
inputSchema: {
|
|
10808
9573
|
type: "object",
|
|
10809
9574
|
properties: {
|
|
@@ -10844,24 +9609,6 @@ const MCP_TOOLS = [
|
|
|
10844
9609
|
additionalProperties: false,
|
|
10845
9610
|
},
|
|
10846
9611
|
},
|
|
10847
|
-
{
|
|
10848
|
-
name: "tintin_dispatch", // formerly monkey_dispatch
|
|
10849
|
-
description: "Dispatch a TinTin job to a headless Claude Code CLI worker with an app/repo/runtime/task agent_context envelope. Max 2 concurrent workers.",
|
|
10850
|
-
inputSchema: {
|
|
10851
|
-
type: "object",
|
|
10852
|
-
properties: {
|
|
10853
|
-
prompt: { type: "string", description: "Full prompt for the CLI worker to execute. Optional when job_id is supplied." },
|
|
10854
|
-
job_id: { type: "string", description: "tintin job UUID or app job id for tracking" },
|
|
10855
|
-
cwd: { type: "string", description: "Existing local directory where the CLI worker should run" },
|
|
10856
|
-
agent_context: {
|
|
10857
|
-
type: "object",
|
|
10858
|
-
description: "App-neutral context envelope with app, repo, runtime, and task fields",
|
|
10859
|
-
additionalProperties: true,
|
|
10860
|
-
},
|
|
10861
|
-
},
|
|
10862
|
-
additionalProperties: false,
|
|
10863
|
-
},
|
|
10864
|
-
},
|
|
10865
9612
|
{
|
|
10866
9613
|
name: "handoff_start",
|
|
10867
9614
|
description: "Start a visible local Claude Code handoff session from a /handoff command and return immediately with session metadata.",
|
|
@@ -10913,7 +9660,7 @@ const MCP_TOOLS = [
|
|
|
10913
9660
|
},
|
|
10914
9661
|
{
|
|
10915
9662
|
name: "terminal_show",
|
|
10916
|
-
description: "Reveal/focus a
|
|
9663
|
+
description: "Reveal/focus a clauth terminal session on this Windows machine. If no live Claude process window exists, opens a visible session inspector at the session cwd.",
|
|
10917
9664
|
inputSchema: {
|
|
10918
9665
|
type: "object",
|
|
10919
9666
|
properties: {
|
|
@@ -12819,19 +11566,6 @@ async function handleMcpTool(vault, name, args) {
|
|
|
12819
11566
|
return mcpResult(JSON.stringify(result));
|
|
12820
11567
|
}
|
|
12821
11568
|
|
|
12822
|
-
case "tintin_dispatch": // formerly monkey_dispatch
|
|
12823
|
-
case "monkey_dispatch": {
|
|
12824
|
-
const { prompt, job_id, cwd: requestedCwd, agent_context } = args;
|
|
12825
|
-
if (!prompt && !job_id) return mcpError("prompt required");
|
|
12826
|
-
const fallbackCwd = await resolveChitchatRoot(vault);
|
|
12827
|
-
const normalizedContext = normalizeAgentContext(agent_context);
|
|
12828
|
-
const dispatchCwd = resolveDispatchCwd(requestedCwd || fallbackCwd, normalizedContext);
|
|
12829
|
-
const wrappedPrompt = buildTinTinPrompt({ prompt, job_id, agent_context: normalizedContext });
|
|
12830
|
-
const result = spawnClaudeTask(wrappedPrompt, job_id || "untracked", dispatchCwd, normalizedContext);
|
|
12831
|
-
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
12832
|
-
return mcpResult(JSON.stringify({ ...result, context: normalizedContext }));
|
|
12833
|
-
}
|
|
12834
|
-
|
|
12835
11569
|
case "handoff_start": {
|
|
12836
11570
|
const result = await startHandoffSession(args || {}, vault);
|
|
12837
11571
|
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
@@ -13957,7 +12691,8 @@ async function actionUpgrade(opts) {
|
|
|
13957
12691
|
async function actionSupervisor(opts) {
|
|
13958
12692
|
opts.isolated = true;
|
|
13959
12693
|
opts.port = String(opts.port || getSupervisorPort());
|
|
13960
|
-
discoverPlugins()
|
|
12694
|
+
// discoverPlugins() now runs unconditionally inside actionForeground at
|
|
12695
|
+
// every boot — calling it here too would double-run discovery.
|
|
13961
12696
|
return actionForeground(opts);
|
|
13962
12697
|
}
|
|
13963
12698
|
|