codebay 0.13.0 → 0.15.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/.mochi/manifest.json +522 -443
- package/.mochi/svelte-client/HydratableIsland-smgsf0ma.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-9vc087ep.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-a48xq47a.js +14 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-d4zehrqz.js +3 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t5175pf5.js +44 -0
- package/.mochi/svelte-client/chunk-0mstgbtr.js +2 -0
- package/.mochi/svelte-client/chunk-0q9g0kpg.js +1 -0
- package/.mochi/svelte-client/chunk-3fwjzw1r.js +1 -0
- package/.mochi/svelte-client/{chunk-spk1b2r4.js → chunk-5pg0be5f.js} +1 -1
- package/.mochi/svelte-client/{chunk-ccfh8hxz.js → chunk-a2hty99b.js} +1 -1
- package/.mochi/svelte-client/{chunk-0mc09cyb.js → chunk-ds9yn9cq.js} +1 -1
- package/.mochi/svelte-client/{chunk-phf7v5jj.js → chunk-fn03gxer.js} +1 -1
- package/.mochi/svelte-client/chunk-jhgq7f65.js +2 -0
- package/.mochi/svelte-client/chunk-p5a07kx1.js +1 -0
- package/.mochi/svelte-client/chunk-p5wpyhsf.js +2 -0
- package/.mochi/svelte-client/chunk-t2kfvtb6.js +2 -0
- package/.mochi/svelte-client/{chunk-x0m3k0rn.js → chunk-v3wg9mdy.js} +1 -1
- package/.mochi/svelte-compile/{App-94xsdn0w.server.js → App-68f9a9fq.server.js} +64 -59
- package/.mochi/svelte-compile/{Avatars-fnccw47h.server.js → Avatars-vyh3rwxx.server.js} +14 -29
- package/.mochi/svelte-compile/Instance-yfw7s5mp.server.js +627 -0
- package/.mochi/svelte-compile/{Settings-h7gsjyz3.server.js → Settings-0b5t9kw9.server.js} +348 -151
- package/.mochi/svelte-compile/{chunk-w0hca7va.js → chunk-0y3wrae9.js} +13 -1
- package/.mochi/svelte-compile/{chunk-ed46m58e.js → chunk-2qzc07gt.js} +13 -1
- package/.mochi/svelte-compile/chunk-ef3hvegw.js +21 -0
- package/.mochi/svelte-compile/chunk-f9td8ej2.js +24 -0
- package/.mochi/svelte-compile/{chunk-pqb403fh.js → chunk-g0eravvj.js} +5 -16
- package/.mochi/svelte-css/AgentLogBox-38adby5iygny7.css +1 -0
- package/.mochi/svelte-css/{InstanceCard-25z7nas7y98zk.css → InstanceCard-3vxozrp9uoads.css} +1 -1
- package/.mochi/svelte-css/{SettingsView-bccw34agd2bj.css → SettingsView-2400myxb9ot56.css} +1 -1
- package/README.md +32 -1
- package/migrations/0009_agent_runs.sql +24 -0
- package/migrations/0010_agent_run_model.sql +2 -0
- package/package.json +6 -2
- package/public/favicon.svg +9 -0
- package/src/components/AgentLogBox.svelte +458 -0
- package/src/components/AppShell.svelte +11 -0
- package/src/components/FolderBrowser.svelte +13 -5
- package/src/components/InstanceCard.svelte +38 -0
- package/src/components/InstanceView.svelte +8 -0
- package/src/components/SettingsView.svelte +260 -0
- package/src/components/UiShowcase.svelte +4 -2
- package/src/container-injections/claude-code-credentials.ts +2 -3
- package/src/container-injections/claude-code-skills.ts +125 -0
- package/src/container-injections/claude-output-style.ts +2 -3
- package/src/container-injections/claude-statusline.ts +9 -17
- package/src/container-injections/github-credentials.ts +12 -1
- package/src/index.ts +32 -7
- package/src/lib/agent-run-stream.ts +246 -0
- package/src/lib/agent-runs.server.ts +724 -0
- package/src/lib/auth.server.ts +11 -2
- package/src/lib/build-freshness.server.ts +71 -0
- package/src/lib/config.server.ts +29 -6
- package/src/lib/container-files.server.ts +23 -1
- package/src/lib/data-dir-lock.server.ts +78 -0
- package/src/lib/db.server.ts +120 -1
- package/src/lib/devcontainer.server.ts +60 -14
- package/src/lib/docker-client.server.ts +11 -5
- package/src/lib/drives.ts +11 -0
- package/src/lib/exec.server.ts +66 -5
- package/src/lib/host-claude.server.ts +31 -0
- package/src/lib/injections.server.ts +2 -0
- package/src/lib/instances.server.ts +45 -3
- package/src/lib/log-capture.server.ts +17 -5
- package/src/lib/mcp-auth.server.ts +52 -0
- package/src/lib/picker.server.ts +23 -2
- package/src/lib/sandbox-ops.server.ts +263 -0
- package/src/mcp/routes.server.ts +25 -0
- package/src/mcp/server.server.ts +52 -0
- package/src/mcp/tools.server.ts +494 -0
- package/src/pages/Settings.svelte +15 -0
- package/src/routes.ts +72 -1
- package/src/shell.html +1 -0
- package/src/types.ts +15 -0
- package/.mochi/svelte-client/HydratableIsland-zrdsrsmg.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-v7y7dh53.js +0 -2
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-tmwd6fpb.js +0 -14
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-m7t64dbh.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-wedc9x9y.js +0 -39
- package/.mochi/svelte-client/chunk-5jwfs4p9.js +0 -2
- package/.mochi/svelte-client/chunk-6qg4q98a.js +0 -2
- package/.mochi/svelte-client/chunk-tmeh70wg.js +0 -2
- package/.mochi/svelte-client/chunk-vs1yjbqt.js +0 -2
- package/.mochi/svelte-client/chunk-y7f4jbdm.js +0 -2
- package/.mochi/svelte-compile/Instance-knnkcn2p.server.js +0 -377
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import ThemePicker from './ThemePicker.svelte';
|
|
11
11
|
import Layers from '@lucide/svelte/icons/layers';
|
|
12
12
|
import FolderMinus from '@lucide/svelte/icons/folder-minus';
|
|
13
|
+
import FolderCog from '@lucide/svelte/icons/folder-cog';
|
|
13
14
|
import Trash2 from '@lucide/svelte/icons/trash-2';
|
|
14
15
|
import Hammer from '@lucide/svelte/icons/hammer';
|
|
15
16
|
import KeyRound from '@lucide/svelte/icons/key-round';
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
import ListOrdered from '@lucide/svelte/icons/list-ordered';
|
|
24
25
|
import Boxes from '@lucide/svelte/icons/boxes';
|
|
25
26
|
import Puzzle from '@lucide/svelte/icons/puzzle';
|
|
27
|
+
import Plug from '@lucide/svelte/icons/plug';
|
|
26
28
|
import { Toaster } from 'svelte-french-toast';
|
|
27
29
|
import { TOAST_OPTIONS } from '../toast.ts';
|
|
28
30
|
import { flushSync } from 'svelte';
|
|
@@ -69,6 +71,11 @@
|
|
|
69
71
|
disableBuildCache,
|
|
70
72
|
copyIgnorePatterns,
|
|
71
73
|
builtinCopyIgnore,
|
|
74
|
+
claudeConfigDir,
|
|
75
|
+
mcpEnabled,
|
|
76
|
+
mcpToken,
|
|
77
|
+
mcpUrl,
|
|
78
|
+
mcpPrAttribution,
|
|
72
79
|
gitIdentityEnabled,
|
|
73
80
|
gitIdentityName,
|
|
74
81
|
gitIdentityEmail,
|
|
@@ -110,6 +117,11 @@
|
|
|
110
117
|
disableBuildCache: boolean;
|
|
111
118
|
copyIgnorePatterns: string;
|
|
112
119
|
builtinCopyIgnore: string;
|
|
120
|
+
claudeConfigDir: string;
|
|
121
|
+
mcpEnabled: boolean;
|
|
122
|
+
mcpToken: string;
|
|
123
|
+
mcpUrl: string;
|
|
124
|
+
mcpPrAttribution: boolean;
|
|
113
125
|
gitIdentityEnabled: boolean;
|
|
114
126
|
gitIdentityName: string;
|
|
115
127
|
gitIdentityEmail: string;
|
|
@@ -372,6 +384,80 @@
|
|
|
372
384
|
copyIgnoreFormEl?.requestSubmit();
|
|
373
385
|
}
|
|
374
386
|
|
|
387
|
+
// Blank means "use ~/.claude" — the host dir the Claude config injections read from.
|
|
388
|
+
// svelte-ignore state_referenced_locally
|
|
389
|
+
let claudeDir = $state(claudeConfigDir);
|
|
390
|
+
let savingClaudeDir = $state(false);
|
|
391
|
+
let claudeDirError = $state<string | null>(null);
|
|
392
|
+
let claudeDirSaved = $state(false);
|
|
393
|
+
let claudeDirFormEl: HTMLFormElement | undefined;
|
|
394
|
+
|
|
395
|
+
const claudeDirOpts = saveOpts<{ dir: string }>({
|
|
396
|
+
setSaving: (v) => (savingClaudeDir = v),
|
|
397
|
+
setError: (v) => (claudeDirError = v),
|
|
398
|
+
setMsg: (v) => (claudeDirSaved = !!v),
|
|
399
|
+
onSuccess: (data) => {
|
|
400
|
+
claudeDir = data?.dir ?? claudeDir;
|
|
401
|
+
claudeDirSaved = true;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
function resetClaudeDir() {
|
|
406
|
+
flushSync(() => (claudeDir = ''));
|
|
407
|
+
claudeDirFormEl?.requestSubmit();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// svelte-ignore state_referenced_locally
|
|
411
|
+
let mcp = $state(mcpEnabled);
|
|
412
|
+
// Unlike every other secret here this one round-trips to the client — a human has to copy it.
|
|
413
|
+
// svelte-ignore state_referenced_locally
|
|
414
|
+
let mcpTokenValue = $state(mcpToken);
|
|
415
|
+
let savingMcpToggle = $state(false);
|
|
416
|
+
let mcpToggleError = $state<string | null>(null);
|
|
417
|
+
let savingMcpToken = $state(false);
|
|
418
|
+
let mcpTokenError = $state<string | null>(null);
|
|
419
|
+
let mcpCopied = $state<string | null>(null);
|
|
420
|
+
|
|
421
|
+
const mcpToggleOpts = toggleOpts<{ enabled: boolean; token: string }>({
|
|
422
|
+
set: (v) => (mcp = v),
|
|
423
|
+
setSaving: (v) => (savingMcpToggle = v),
|
|
424
|
+
setError: (v) => (mcpToggleError = v),
|
|
425
|
+
onSuccess: (data) => (mcpTokenValue = data?.token ?? '')
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const mcpTokenOpts = saveOpts<{ token: string }>({
|
|
429
|
+
setSaving: (v) => (savingMcpToken = v),
|
|
430
|
+
setError: (v) => (mcpTokenError = v),
|
|
431
|
+
onSuccess: (data) => (mcpTokenValue = data?.token ?? ''),
|
|
432
|
+
confirmMessage:
|
|
433
|
+
'Regenerate the MCP token? Any agent already configured with the current token will stop working.'
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// svelte-ignore state_referenced_locally
|
|
437
|
+
let mcpAttribution = $state(mcpPrAttribution);
|
|
438
|
+
let savingMcpAttribution = $state(false);
|
|
439
|
+
let mcpAttributionError = $state<string | null>(null);
|
|
440
|
+
|
|
441
|
+
const mcpAttributionOpts = toggleOpts({
|
|
442
|
+
set: (v) => (mcpAttribution = v),
|
|
443
|
+
setSaving: (v) => (savingMcpAttribution = v),
|
|
444
|
+
setError: (v) => (mcpAttributionError = v)
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const mcpCommand = $derived(
|
|
448
|
+
`claude mcp add --transport http codebay ${mcpUrl} --header "Authorization: Bearer ${mcpTokenValue}"`
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
async function copyMcp(label: string, text: string) {
|
|
452
|
+
try {
|
|
453
|
+
await navigator.clipboard.writeText(text);
|
|
454
|
+
mcpCopied = label;
|
|
455
|
+
setTimeout(() => (mcpCopied = null), 1500);
|
|
456
|
+
} catch {
|
|
457
|
+
mcpTokenError = 'Could not copy — select the text and copy it manually.';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
375
461
|
// svelte-ignore state_referenced_locally
|
|
376
462
|
let gitIdentity = $state(gitIdentityEnabled);
|
|
377
463
|
let savingGitToggle = $state(false);
|
|
@@ -1156,6 +1242,164 @@
|
|
|
1156
1242
|
</form>
|
|
1157
1243
|
</section>
|
|
1158
1244
|
|
|
1245
|
+
<section class="card">
|
|
1246
|
+
<form
|
|
1247
|
+
class="row image-row"
|
|
1248
|
+
method="POST"
|
|
1249
|
+
action="?/claudeConfigDir"
|
|
1250
|
+
bind:this={claudeDirFormEl}
|
|
1251
|
+
{@attach enhance(claudeDirOpts)}
|
|
1252
|
+
>
|
|
1253
|
+
<div class="label">
|
|
1254
|
+
<FolderCog size={20} />
|
|
1255
|
+
<div class="text">
|
|
1256
|
+
<div class="name">Claude config directory</div>
|
|
1257
|
+
<div class="desc">
|
|
1258
|
+
Host directory the Claude Code injections read from — credentials, statusLine, output
|
|
1259
|
+
styles, and global skills / <code>CLAUDE.md</code>. Leave blank to use
|
|
1260
|
+
<code>~/.claude</code>. Takes effect for instances created from now on.
|
|
1261
|
+
</div>
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
<div class="image-controls">
|
|
1265
|
+
<input
|
|
1266
|
+
type="text"
|
|
1267
|
+
name="dir"
|
|
1268
|
+
class="image-input"
|
|
1269
|
+
bind:value={claudeDir}
|
|
1270
|
+
spellcheck="false"
|
|
1271
|
+
autocapitalize="off"
|
|
1272
|
+
autocorrect="off"
|
|
1273
|
+
placeholder="~/.claude"
|
|
1274
|
+
/>
|
|
1275
|
+
<Button type="submit" disabled={savingClaudeDir}>Save</Button>
|
|
1276
|
+
<Button
|
|
1277
|
+
type="button"
|
|
1278
|
+
icon={RotateCcw}
|
|
1279
|
+
disabled={savingClaudeDir}
|
|
1280
|
+
onclick={resetClaudeDir}
|
|
1281
|
+
title="Reset to default (~/.claude)"
|
|
1282
|
+
aria-label="Reset to default Claude config directory"
|
|
1283
|
+
/>
|
|
1284
|
+
</div>
|
|
1285
|
+
{#if claudeDirError}
|
|
1286
|
+
<div class="msg error">{claudeDirError}</div>
|
|
1287
|
+
{:else if claudeDirSaved}
|
|
1288
|
+
<div class="msg ok">Saved.</div>
|
|
1289
|
+
{/if}
|
|
1290
|
+
</form>
|
|
1291
|
+
</section>
|
|
1292
|
+
|
|
1293
|
+
<section class="card">
|
|
1294
|
+
<form class="row" method="POST" action="?/mcpToggle" {@attach enhance(mcpToggleOpts)}>
|
|
1295
|
+
<div class="label">
|
|
1296
|
+
<Plug size={20} />
|
|
1297
|
+
<div class="text">
|
|
1298
|
+
<div class="name">MCP server</div>
|
|
1299
|
+
<div class="desc">
|
|
1300
|
+
Expose Codebay to other AI agents over MCP, so they can create sandboxes, run Claude
|
|
1301
|
+
Code in them non-interactively and read back the result. Off by default; the endpoint
|
|
1302
|
+
404s until you turn it on, then authenticates with the bearer token below.
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>
|
|
1306
|
+
<label class="switch">
|
|
1307
|
+
<input
|
|
1308
|
+
type="checkbox"
|
|
1309
|
+
name="enabled"
|
|
1310
|
+
checked={mcp}
|
|
1311
|
+
disabled={savingMcpToggle}
|
|
1312
|
+
onchange={(e) => {
|
|
1313
|
+
mcp = e.currentTarget.checked;
|
|
1314
|
+
e.currentTarget.form?.requestSubmit();
|
|
1315
|
+
}}
|
|
1316
|
+
/>
|
|
1317
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1318
|
+
</label>
|
|
1319
|
+
</form>
|
|
1320
|
+
{#if mcpToggleError}
|
|
1321
|
+
<div class="sub"><div class="msg error">{mcpToggleError}</div></div>
|
|
1322
|
+
{/if}
|
|
1323
|
+
|
|
1324
|
+
{#if mcp}
|
|
1325
|
+
<div class="row divided">
|
|
1326
|
+
<div class="label">
|
|
1327
|
+
<div class="text">
|
|
1328
|
+
<div class="name">Token</div>
|
|
1329
|
+
<div class="desc">
|
|
1330
|
+
Anything holding this token can create containers and run agents with your GitHub
|
|
1331
|
+
and Claude credentials. Treat it like a password.
|
|
1332
|
+
</div>
|
|
1333
|
+
</div>
|
|
1334
|
+
</div>
|
|
1335
|
+
<div class="model-fields">
|
|
1336
|
+
<label class="model-row">
|
|
1337
|
+
<span class="model-label">Bearer</span>
|
|
1338
|
+
<input
|
|
1339
|
+
type="text"
|
|
1340
|
+
class="image-input"
|
|
1341
|
+
value={mcpTokenValue}
|
|
1342
|
+
readonly
|
|
1343
|
+
spellcheck="false"
|
|
1344
|
+
onfocus={(e) => e.currentTarget.select()}
|
|
1345
|
+
/>
|
|
1346
|
+
</label>
|
|
1347
|
+
<div class="mcp-actions">
|
|
1348
|
+
<Button type="button" onclick={() => copyMcp('token', mcpTokenValue)}>
|
|
1349
|
+
{mcpCopied === 'token' ? 'Copied' : 'Copy token'}
|
|
1350
|
+
</Button>
|
|
1351
|
+
<Button type="button" onclick={() => copyMcp('command', mcpCommand)}>
|
|
1352
|
+
{mcpCopied === 'command' ? 'Copied' : 'Copy claude mcp add'}
|
|
1353
|
+
</Button>
|
|
1354
|
+
<form method="POST" action="?/mcpRegenerateToken" {@attach enhance(mcpTokenOpts)}>
|
|
1355
|
+
<Button type="submit" disabled={savingMcpToken}>
|
|
1356
|
+
{savingMcpToken ? 'Regenerating…' : 'Regenerate'}
|
|
1357
|
+
</Button>
|
|
1358
|
+
</form>
|
|
1359
|
+
</div>
|
|
1360
|
+
<code class="mcp-command">{mcpCommand}</code>
|
|
1361
|
+
{#if mcpTokenError}
|
|
1362
|
+
<div class="msg error">{mcpTokenError}</div>
|
|
1363
|
+
{/if}
|
|
1364
|
+
</div>
|
|
1365
|
+
</div>
|
|
1366
|
+
|
|
1367
|
+
<form
|
|
1368
|
+
class="row divided"
|
|
1369
|
+
method="POST"
|
|
1370
|
+
action="?/mcpPrAttributionToggle"
|
|
1371
|
+
{@attach enhance(mcpAttributionOpts)}
|
|
1372
|
+
>
|
|
1373
|
+
<div class="label">
|
|
1374
|
+
<div class="text">
|
|
1375
|
+
<div class="name">Credit Codebay on pull requests</div>
|
|
1376
|
+
<div class="desc">
|
|
1377
|
+
Append a one-line footer to the body of every pull request an agent opens through
|
|
1378
|
+
<code>create_pr</code>. Off by default — an attribution line on your PRs is yours to
|
|
1379
|
+
opt into.
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
<label class="switch">
|
|
1384
|
+
<input
|
|
1385
|
+
type="checkbox"
|
|
1386
|
+
name="enabled"
|
|
1387
|
+
checked={mcpAttribution}
|
|
1388
|
+
disabled={savingMcpAttribution}
|
|
1389
|
+
onchange={(e) => {
|
|
1390
|
+
mcpAttribution = e.currentTarget.checked;
|
|
1391
|
+
e.currentTarget.form?.requestSubmit();
|
|
1392
|
+
}}
|
|
1393
|
+
/>
|
|
1394
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1395
|
+
</label>
|
|
1396
|
+
</form>
|
|
1397
|
+
{#if mcpAttributionError}
|
|
1398
|
+
<div class="sub"><div class="msg error">{mcpAttributionError}</div></div>
|
|
1399
|
+
{/if}
|
|
1400
|
+
{/if}
|
|
1401
|
+
</section>
|
|
1402
|
+
|
|
1159
1403
|
<section class="card">
|
|
1160
1404
|
<form
|
|
1161
1405
|
class="row"
|
|
@@ -2619,6 +2863,22 @@
|
|
|
2619
2863
|
flex: 1;
|
|
2620
2864
|
min-width: 220px;
|
|
2621
2865
|
}
|
|
2866
|
+
.mcp-actions {
|
|
2867
|
+
display: flex;
|
|
2868
|
+
flex-wrap: wrap;
|
|
2869
|
+
gap: 6px;
|
|
2870
|
+
}
|
|
2871
|
+
/* The registration line is long and unbreakable, so it scrolls rather than widening the card. */
|
|
2872
|
+
.mcp-command {
|
|
2873
|
+
display: block;
|
|
2874
|
+
overflow-x: auto;
|
|
2875
|
+
white-space: pre;
|
|
2876
|
+
padding: 6px 8px;
|
|
2877
|
+
border: 1px solid var(--edge);
|
|
2878
|
+
background: var(--panel);
|
|
2879
|
+
font-size: 11px;
|
|
2880
|
+
line-height: 1.5;
|
|
2881
|
+
}
|
|
2622
2882
|
.model-row {
|
|
2623
2883
|
display: flex;
|
|
2624
2884
|
align-items: center;
|
|
@@ -183,7 +183,8 @@
|
|
|
183
183
|
terminal_split: 0,
|
|
184
184
|
git_branch: cardBranch ? branchName : null,
|
|
185
185
|
attention: cardAttention,
|
|
186
|
-
forwarded_ports: demoPorts
|
|
186
|
+
forwarded_ports: demoPorts,
|
|
187
|
+
active_run: null
|
|
187
188
|
});
|
|
188
189
|
|
|
189
190
|
const statuses: Instance['status'][] = ['creating', 'running', 'stopped', 'error'];
|
|
@@ -234,7 +235,8 @@
|
|
|
234
235
|
terminal_split: 0,
|
|
235
236
|
git_branch: 'main',
|
|
236
237
|
attention: null,
|
|
237
|
-
forwarded_ports: []
|
|
238
|
+
forwarded_ports: [],
|
|
239
|
+
active_run: null
|
|
238
240
|
};
|
|
239
241
|
})
|
|
240
242
|
);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { hostClaudeFile } from '../lib/host-claude.server.ts';
|
|
5
4
|
import { CLAUDE_CODE_TOKEN, CLAUDE_KEYCHAIN_SERVICE } from '../lib/config.server.ts';
|
|
6
5
|
import { getOption } from '../lib/db.server.ts';
|
|
7
6
|
import { checkPresence } from '../lib/exec.server.ts';
|
|
@@ -84,7 +83,7 @@ async function locateClaudeCredentials(): Promise<{ creds: string; source: strin
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
const file =
|
|
86
|
+
const file = hostClaudeFile('.credentials.json');
|
|
88
87
|
if (existsSync(file)) {
|
|
89
88
|
const raw = (await readFile(file, 'utf8')).trim();
|
|
90
89
|
if (isValid(raw)) return { creds: raw, source: '~/.claude/.credentials.json' };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
import { checkPresence } from '../lib/exec.server.ts';
|
|
5
|
+
import { writeContainerFileBytes } from '../lib/container-files.server.ts';
|
|
6
|
+
import { claudeConfigFile } from '../lib/claude-settings.server.ts';
|
|
7
|
+
import { hostClaudeFile } from '../lib/host-claude.server.ts';
|
|
8
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The env-var stdin carrier caps a single write near 128 KB (Linux `MAX_ARG_STRLEN`); base64
|
|
12
|
+
* inflates ~4/3, so a raw file over this is skipped rather than failing the whole injection.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_FILE_BYTES = 90_000;
|
|
15
|
+
|
|
16
|
+
interface HostFile {
|
|
17
|
+
/** Path relative to the host Claude dir, e.g. `CLAUDE.md` or `skills/foo/SKILL.md`. */
|
|
18
|
+
rel: string;
|
|
19
|
+
bytes: Buffer;
|
|
20
|
+
/** True when any execute bit is set on the host, so container scripts stay runnable. */
|
|
21
|
+
exec: boolean;
|
|
22
|
+
oversized: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Recursively lists every file under a host dir, as `skills/<subpath>` entries. */
|
|
26
|
+
async function walkSkills(dir: string, rel: string, out: HostFile[]): Promise<void> {
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const abs = posix.join(dir, entry.name);
|
|
35
|
+
const childRel = posix.join(rel, entry.name);
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
await walkSkills(abs, childRel, out);
|
|
38
|
+
} else if (entry.isFile()) {
|
|
39
|
+
const info = await stat(abs);
|
|
40
|
+
out.push({
|
|
41
|
+
rel: childRel,
|
|
42
|
+
bytes: info.size > MAX_FILE_BYTES ? Buffer.alloc(0) : await readFile(abs),
|
|
43
|
+
exec: (info.mode & 0o111) !== 0,
|
|
44
|
+
oversized: info.size > MAX_FILE_BYTES
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cheap presence probe for the setup-UI chip — never reads file contents. */
|
|
51
|
+
export async function hasHostSkillFiles(): Promise<boolean> {
|
|
52
|
+
if (existsSync(hostClaudeFile('CLAUDE.md'))) return true;
|
|
53
|
+
try {
|
|
54
|
+
return (await readdir(hostClaudeFile('skills'))).length > 0;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The host global CLAUDE.md plus every file under the host skills dir. */
|
|
61
|
+
export async function collectHostSkillFiles(): Promise<HostFile[]> {
|
|
62
|
+
const files: HostFile[] = [];
|
|
63
|
+
const claudeMd = hostClaudeFile('CLAUDE.md');
|
|
64
|
+
if (existsSync(claudeMd)) {
|
|
65
|
+
const info = await stat(claudeMd);
|
|
66
|
+
if (info.isFile()) {
|
|
67
|
+
files.push({
|
|
68
|
+
rel: 'CLAUDE.md',
|
|
69
|
+
bytes: info.size > MAX_FILE_BYTES ? Buffer.alloc(0) : await readFile(claudeMd),
|
|
70
|
+
exec: false,
|
|
71
|
+
oversized: info.size > MAX_FILE_BYTES
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await walkSkills(hostClaudeFile('skills'), 'skills', files);
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const claudeCodeSkills: Injection = {
|
|
80
|
+
id: 'claude-code-skills',
|
|
81
|
+
label: 'skills & CLAUDE.md',
|
|
82
|
+
|
|
83
|
+
auth: {
|
|
84
|
+
hint: 'add skills or a CLAUDE.md to your host ~/.claude',
|
|
85
|
+
async status() {
|
|
86
|
+
const available = await hasHostSkillFiles();
|
|
87
|
+
return { available, source: available ? '~/.claude' : null };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async apply(target, log) {
|
|
92
|
+
const files = await collectHostSkillFiles();
|
|
93
|
+
if (!files.length) {
|
|
94
|
+
log('⚠ No global skills or CLAUDE.md found on host; skipped\n');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
log(`Injecting ${files.length} global skill/CLAUDE.md file(s)…\n`);
|
|
98
|
+
let failed = 0;
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
if (file.oversized) {
|
|
101
|
+
log(`⚠ Skipped ${file.rel} (larger than ${MAX_FILE_BYTES} bytes)\n`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const dest = claudeConfigFile(file.rel, file.exec ? '755' : '644');
|
|
105
|
+
const wrote = await writeContainerFileBytes(target, dest, file.bytes);
|
|
106
|
+
if (!wrote.ok) {
|
|
107
|
+
failed++;
|
|
108
|
+
log(`⚠ Failed to write ${file.rel}: ${wrote.error}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
log(
|
|
112
|
+
failed
|
|
113
|
+
? `⚠ ${failed} skill file(s) failed to inject\n`
|
|
114
|
+
: '✓ Global skills & CLAUDE.md injected\n'
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async check(target) {
|
|
119
|
+
return checkPresence(
|
|
120
|
+
target,
|
|
121
|
+
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
122
|
+
'if [ -s "$d/CLAUDE.md" ] || [ -n "$(ls -A "$d/skills" 2>/dev/null)" ]; then echo 1; else echo 0; fi'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { getOption } from '../lib/db.server.ts';
|
|
5
4
|
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
@@ -8,7 +7,7 @@ import {
|
|
|
8
7
|
mergeClaudeSettings,
|
|
9
8
|
readClaudeSettings
|
|
10
9
|
} from '../lib/claude-settings.server.ts';
|
|
11
|
-
import { readHostClaudeSettings } from '
|
|
10
|
+
import { hostClaudeFile, readHostClaudeSettings } from '../lib/host-claude.server.ts';
|
|
12
11
|
import { normalizeOutputStyle } from '../types.ts';
|
|
13
12
|
import type { ClaudeOutputStyle } from '../types.ts';
|
|
14
13
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
@@ -29,7 +28,7 @@ export async function resolveEffectiveOutputStyle(): Promise<string | null> {
|
|
|
29
28
|
|
|
30
29
|
/** The host's custom output-style definitions, so a non-built-in name resolves inside the container. */
|
|
31
30
|
export async function readHostOutputStyleFiles(): Promise<{ name: string; content: string }[]> {
|
|
32
|
-
const dir =
|
|
31
|
+
const dir = hostClaudeFile('output-styles');
|
|
33
32
|
let names: string[];
|
|
34
33
|
try {
|
|
35
34
|
names = (await readdir(dir)).filter((n) => n.endsWith('.md'));
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
5
4
|
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
6
5
|
import {
|
|
7
6
|
claudeConfigFile,
|
|
8
7
|
mergeClaudeSettings,
|
|
9
8
|
readClaudeSettings
|
|
10
9
|
} from '../lib/claude-settings.server.ts';
|
|
10
|
+
import { expandTilde, readHostClaudeSettings } from '../lib/host-claude.server.ts';
|
|
11
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
12
12
|
|
|
13
|
+
// Re-exported so the modules and tests that historically imported these from here keep working.
|
|
14
|
+
export { expandTilde, readHostClaudeSettings };
|
|
15
|
+
|
|
13
16
|
/** Left unexpanded — Claude Code invokes this long after the injection has finished. */
|
|
14
17
|
const CONTAINER_SCRIPT_PATH = '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline.sh';
|
|
15
18
|
|
|
@@ -19,19 +22,6 @@ interface StatusLineConfig {
|
|
|
19
22
|
script?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
export async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
|
|
23
|
-
const file = join(homedir(), '.claude', 'settings.json');
|
|
24
|
-
if (!existsSync(file)) return null;
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(await readFile(file, 'utf8')) as Record<string, unknown>;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const expandTilde = (p: string): string =>
|
|
33
|
-
p.startsWith('~/') ? join(homedir(), p.slice(2)) : p;
|
|
34
|
-
|
|
35
25
|
/** Only a real file counts — else jq's `//` operator and a bare `/` resolve to the root dir. */
|
|
36
26
|
const isExistingFile = (p: string): boolean => {
|
|
37
27
|
try {
|
|
@@ -45,7 +35,9 @@ const isExistingFile = (p: string): boolean => {
|
|
|
45
35
|
export function extractScriptPath(command: string): string | null {
|
|
46
36
|
const token = command
|
|
47
37
|
.split(/\s+/)
|
|
48
|
-
|
|
38
|
+
// isAbsolute rather than a leading slash, so a Windows `C:…` command is recognised too;
|
|
39
|
+
// on POSIX the two are equivalent. The file check still rejects a bare root either way.
|
|
40
|
+
.find((t) => (isAbsolute(t) || t.startsWith('~/')) && isExistingFile(expandTilde(t)));
|
|
49
41
|
return token ?? null;
|
|
50
42
|
}
|
|
51
43
|
|
|
@@ -51,8 +51,19 @@ export function ghHostBlock(raw: string, host: string): string | null {
|
|
|
51
51
|
return rest.slice(0, end === -1 ? rest.length : end).join('\n');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Mirrors `gh`'s own resolution order, which is not XDG on Windows. */
|
|
55
|
+
function ghConfigDir(): string {
|
|
56
|
+
const explicit = process.env.GH_CONFIG_DIR?.trim();
|
|
57
|
+
if (explicit) return explicit;
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
return join(process.env.AppData || join(homedir(), 'AppData', 'Roaming'), 'GitHub CLI');
|
|
60
|
+
}
|
|
61
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
62
|
+
return xdg ? join(xdg, 'gh') : join(homedir(), '.config', 'gh');
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
async function readGhHostsFile(): Promise<string | null> {
|
|
55
|
-
const file = join(
|
|
66
|
+
const file = join(ghConfigDir(), 'hosts.yml');
|
|
56
67
|
if (!existsSync(file)) return null;
|
|
57
68
|
try {
|
|
58
69
|
return await readFile(file, 'utf8');
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Mochi, sequence, silenceInternalRoutes } from 'mochi-framework';
|
|
2
2
|
import { routes } from './routes.ts';
|
|
3
|
-
import { basicAuth } from './lib/auth.server.ts';
|
|
3
|
+
import { basicAuth, tokenAuthenticated } from './lib/auth.server.ts';
|
|
4
4
|
import { themeHandle } from './lib/theme.server.ts';
|
|
5
5
|
import { PROXY_PREFIX } from './lib/proxy.server.ts';
|
|
6
|
+
import { resumeRuns } from './lib/agent-runs.server.ts';
|
|
7
|
+
import { warnIfBuildStale } from './lib/build-freshness.server.ts';
|
|
8
|
+
import { LOCK_STALE_MS, acquireDataDirLock } from './lib/data-dir-lock.server.ts';
|
|
6
9
|
import {
|
|
7
10
|
APP_VERSION,
|
|
8
11
|
BASIC_AUTH_PASSWORD,
|
|
12
|
+
DATA_DIR,
|
|
9
13
|
HOST,
|
|
10
14
|
PORT,
|
|
11
15
|
PUBLIC_ORIGIN,
|
|
@@ -21,10 +25,32 @@ if (HOST !== '127.0.0.1' && HOST !== 'localhost' && !BASIC_AUTH_PASSWORD) {
|
|
|
21
25
|
`⚠ Binding to ${HOST} (non-loopback) without a password — anyone on the network can reach this server.`
|
|
22
26
|
);
|
|
23
27
|
}
|
|
28
|
+
warnIfBuildStale();
|
|
29
|
+
|
|
30
|
+
const lock = acquireDataDirLock(DATA_DIR);
|
|
31
|
+
if (!lock.ok) {
|
|
32
|
+
const who = lock.holder
|
|
33
|
+
? `pid ${lock.holder.pid} on ${lock.holder.host}, started ${new Date(lock.holder.startedAt).toLocaleString()}`
|
|
34
|
+
: 'an unknown process';
|
|
35
|
+
console.error(
|
|
36
|
+
`✖ Another codebay manager (${who}) is already using ${DATA_DIR}. Two managers on one data dir ` +
|
|
37
|
+
"poll the same runs and overwrite each other's rows — stop it, or start this one with a " +
|
|
38
|
+
`different DATA_DIR. A crashed manager's lock (${lock.path}) clears itself after ${LOCK_STALE_MS / 1000}s.`
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// A default Ctrl-C skips the exit hooks, and an unreleased lock refuses the restart that follows.
|
|
43
|
+
process.on('exit', () => lock.release());
|
|
44
|
+
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
45
|
+
process.on(signal, () => process.exit(signal === 'SIGINT' ? 130 : 143));
|
|
46
|
+
}
|
|
24
47
|
|
|
25
48
|
// Must precede Mochi.serve(), which is where MOCHI_KEY is read.
|
|
26
49
|
ensureMochiKey();
|
|
27
50
|
|
|
51
|
+
// Agent runs outlive the process that started them, so re-attach pollers to anything still in flight.
|
|
52
|
+
resumeRuns();
|
|
53
|
+
|
|
28
54
|
await Mochi.serve({
|
|
29
55
|
port: PORT,
|
|
30
56
|
hostname: HOST,
|
|
@@ -41,10 +67,9 @@ await Mochi.serve({
|
|
|
41
67
|
trustedOrigins: TRUSTED_ORIGINS
|
|
42
68
|
},
|
|
43
69
|
filters: {
|
|
44
|
-
// Containers
|
|
45
|
-
//
|
|
46
|
-
'csrf:check': (decision, { url }) =>
|
|
47
|
-
url.pathname.startsWith('/api/bridge/') ? null : decision,
|
|
70
|
+
// Containers and MCP clients send no Origin header, which the check would 403; safe because
|
|
71
|
+
// those routes authenticate by token, not ambient browser credentials.
|
|
72
|
+
'csrf:check': (decision, { url }) => (tokenAuthenticated(url.pathname) ? null : decision),
|
|
48
73
|
'consoleLogger:line': (line, ctx) => {
|
|
49
74
|
const kept = silenceInternalRoutes(line, ctx);
|
|
50
75
|
if (kept == null) {
|
|
@@ -53,8 +78,8 @@ await Mochi.serve({
|
|
|
53
78
|
if (ctx.source.name === 'ws:message' && ctx.path.startsWith(PROXY_PREFIX + '/')) {
|
|
54
79
|
return null;
|
|
55
80
|
}
|
|
56
|
-
//
|
|
57
|
-
if (ctx.path
|
|
81
|
+
// A line per Claude hook event or JSON-RPC frame is noise, and each would print its token.
|
|
82
|
+
if (tokenAuthenticated(ctx.path)) {
|
|
58
83
|
return null;
|
|
59
84
|
}
|
|
60
85
|
return kept;
|