codebay 0.4.0 → 0.6.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 +272 -220
- package/.mochi/svelte-client/HydratableIsland-qyjx1d98.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-1y0r9tae.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-89wtkb86.js +13 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-eamnbyqk.js +1 -0
- package/.mochi/svelte-client/{_hydrate-SettingsView_8hmarwmf8onq-r9vvc7rx.js → _hydrate-SettingsView_8hmarwmf8onq-kdra4mcb.js} +3 -3
- package/.mochi/svelte-client/chunk-330mrspw.js +2 -0
- package/.mochi/svelte-client/{chunk-dz1019cy.js → chunk-4jgkak13.js} +1 -1
- package/.mochi/svelte-client/chunk-fqg2qzj8.js +2 -0
- package/.mochi/svelte-client/chunk-k5m4kt20.js +2 -0
- package/.mochi/svelte-client/{chunk-nmf4dq86.js → chunk-qj7zy2d9.js} +1 -1
- package/.mochi/svelte-client/{chunk-ssa0ad2f.js → chunk-rpdphch7.js} +1 -1
- package/.mochi/svelte-compile/{App-9qjy14bc.server.js → App-v5mwmv6t.server.js} +26 -2
- package/.mochi/svelte-compile/{Avatars-sq8mjn6k.server.js → Avatars-pzyz0tv3.server.js} +75 -15
- package/.mochi/svelte-compile/{Instance-13j59daw.server.js → Instance-49ksrp11.server.js} +15 -3
- package/.mochi/svelte-compile/{Settings-p0s2awg1.server.js → Settings-sz5d6qj7.server.js} +3 -3
- package/.mochi/svelte-compile/{chunk-dfe2km6a.js → chunk-q8rbesaw.js} +324 -123
- package/.mochi/svelte-css/Avatar-3pi30db6ozvf0.css +1 -0
- package/.mochi/svelte-css/AvatarEditor-2ewwtejaml4e2.css +1 -0
- package/.mochi/svelte-css/{AvatarsView-3hq37u4dgtljn.css → AvatarsView-2ughbzc26luyb.css} +1 -1
- package/.mochi/svelte-css/InstanceCard-3vnltxnbxkvay.css +1 -0
- package/.mochi/svelte-css/{InstanceView-wq85bxtusont.css → InstanceView-3vzow3ttxzlh3.css} +1 -1
- package/README.md +2 -0
- package/package.json +2 -2
- package/src/avatars/anchor.ts +1 -0
- package/src/avatars/ant-head.ts +17 -0
- package/src/avatars/apple.ts +17 -0
- package/src/avatars/bear.ts +1 -0
- package/src/avatars/bee.ts +1 -0
- package/src/avatars/bodybuilder.ts +17 -0
- package/src/avatars/cat.ts +1 -0
- package/src/avatars/cherry.ts +1 -0
- package/src/avatars/crab.ts +1 -0
- package/src/avatars/crown.ts +1 -0
- package/src/avatars/diamond.ts +1 -0
- package/src/avatars/dog.ts +1 -0
- package/src/avatars/editor.ts +17 -0
- package/src/avatars/fish.ts +1 -0
- package/src/avatars/flower.ts +1 -0
- package/src/avatars/fox.ts +1 -0
- package/src/avatars/frog.ts +1 -0
- package/src/avatars/ghost.ts +1 -0
- package/src/avatars/heart.ts +8 -8
- package/src/avatars/horse.ts +17 -0
- package/src/avatars/index.ts +20 -0
- package/src/avatars/invader.ts +1 -0
- package/src/avatars/key.ts +1 -0
- package/src/avatars/lightning.ts +1 -0
- package/src/avatars/mushroom.ts +1 -0
- package/src/avatars/octopus.ts +1 -0
- package/src/avatars/owl.ts +1 -0
- package/src/avatars/penguin.ts +1 -0
- package/src/avatars/planet.ts +1 -0
- package/src/avatars/praying-mantis.ts +17 -0
- package/src/avatars/rabbit.ts +1 -0
- package/src/avatars/robot.ts +1 -0
- package/src/avatars/rocket.ts +1 -0
- package/src/avatars/serialize.ts +4 -3
- package/src/avatars/skull.ts +6 -6
- package/src/avatars/snail.ts +1 -0
- package/src/avatars/snowflake.ts +17 -0
- package/src/avatars/star.ts +1 -0
- package/src/avatars/strawberry.ts +17 -0
- package/src/avatars/target.ts +1 -0
- package/src/avatars/tree.ts +1 -0
- package/src/avatars/types.ts +6 -3
- package/src/avatars/vangogh-almond-blossoms.ts +17 -0
- package/src/avatars/wasp.ts +17 -0
- package/src/avatars/whale.ts +1 -0
- package/src/avatars/windmill.ts +17 -0
- package/src/components/AppShell.svelte +13 -0
- package/src/components/Avatar.svelte +4 -1
- package/src/components/AvatarEditor.svelte +83 -5
- package/src/components/AvatarsView.svelte +14 -1
- package/src/components/DashboardView.svelte +24 -2
- package/src/components/InstanceCard.svelte +55 -6
- package/src/components/InstanceView.svelte +41 -8
- package/src/components/SettingsView.svelte +2 -2
- package/src/container-injections/attention-hooks.ts +19 -38
- package/src/container-injections/claude-aliases.ts +7 -23
- package/src/container-injections/claude-code-credentials.ts +12 -8
- package/src/container-injections/claude-code-custom.ts +35 -48
- package/src/container-injections/claude-code-ide-extension.ts +39 -0
- package/src/container-injections/claude-code-models.ts +8 -24
- package/src/container-injections/claude-model.ts +43 -0
- package/src/container-injections/claude-no-coauthor.ts +14 -16
- package/src/container-injections/claude-skip-permissions.ts +7 -17
- package/src/container-injections/claude-statusline.ts +40 -27
- package/src/container-injections/claude-trust.ts +73 -0
- package/src/container-injections/host-env-vars.ts +13 -24
- package/src/container-injections/tmux.ts +2 -11
- package/src/index.ts +2 -0
- package/src/lib/claude-settings.server.ts +35 -0
- package/src/lib/container-files.server.ts +219 -0
- package/src/lib/devcontainer.server.ts +38 -1
- package/src/lib/exec.server.ts +2 -7
- package/src/lib/injections.server.ts +6 -0
- package/src/lib/instances.server.ts +14 -0
- package/src/lib/theme.server.ts +7 -3
- package/src/shell.html +2 -0
- package/.mochi/svelte-client/HydratableIsland-79z9xq4b.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-f5rzyjm8.js +0 -2
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js +0 -13
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-nv31ptdp.js +0 -1
- package/.mochi/svelte-client/chunk-6eexkr6p.js +0 -2
- package/.mochi/svelte-client/chunk-7znwaf7t.js +0 -2
- package/.mochi/svelte-client/chunk-zmmb1c4b.js +0 -2
- package/.mochi/svelte-css/Avatar-24th63awm9mna.css +0 -1
- package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +0 -1
- package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +0 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { PORT } from '../lib/config.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
3
|
+
import {
|
|
4
|
+
claudeConfigFile,
|
|
5
|
+
mergeClaudeSettings,
|
|
6
|
+
readClaudeSettings
|
|
7
|
+
} from '../lib/claude-settings.server.ts';
|
|
3
8
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
9
|
|
|
5
10
|
function bridgeUrl(): string {
|
|
@@ -40,50 +45,32 @@ function hookFor(id: string, state: 'done' | 'waiting' | 'busy') {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/** Carries no token — that lives in the mode-600 header file `writeBridgeHeader` stages. */
|
|
43
|
-
export function attentionHookSettings(id: string): string {
|
|
44
|
-
|
|
48
|
+
export function attentionHookSettings(id: string): Record<string, unknown> {
|
|
49
|
+
return {
|
|
45
50
|
hooks: {
|
|
46
51
|
Stop: hookFor(id, 'done'),
|
|
47
52
|
Notification: hookFor(id, 'waiting'),
|
|
48
53
|
UserPromptSubmit: hookFor(id, 'busy')
|
|
49
54
|
}
|
|
50
55
|
};
|
|
51
|
-
return JSON.stringify(settings, null, 2);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** Stored as a whole header line so curl can consume the file directly with `-H @`. */
|
|
55
|
-
|
|
59
|
+
function writeBridgeHeader(
|
|
56
60
|
target: ContainerTarget,
|
|
57
61
|
token: string
|
|
58
62
|
): Promise<{ ok: boolean; error?: string }> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Merging rather than overwriting is what lets the settings.json injections compose in any order. */
|
|
67
|
-
export function mergeClaudeSettingsScript(): string {
|
|
68
|
-
return (
|
|
69
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; mkdir -p "$d"; ' +
|
|
70
|
-
'f="$d/settings.json"; new="$CODEBAY_STDIN"; ' +
|
|
71
|
-
'if command -v jq >/dev/null 2>&1 && [ -s "$f" ] && ' +
|
|
72
|
-
'merged=$(printf \'%s\' "$new" | jq -s \'.[0] * .[1]\' "$f" - 2>/dev/null); then ' +
|
|
73
|
-
'printf \'%s\' "$merged" > "$f"; else printf \'%s\' "$new" > "$f"; fi; ' +
|
|
74
|
-
'chmod 644 "$f"'
|
|
63
|
+
return writeContainerFile(
|
|
64
|
+
target,
|
|
65
|
+
claudeConfigFile('.bridge-header', '600'),
|
|
66
|
+
`X-Bridge-Token: ${token}\n`
|
|
75
67
|
);
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const res = await execInContainer(target, {
|
|
83
|
-
script: mergeClaudeSettingsScript(),
|
|
84
|
-
stdin: settingsJson
|
|
85
|
-
});
|
|
86
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
70
|
+
/** Matches the delimited `id=<id>&state=` fragment of the hook's curl URL, so no other instance id can match by substring. */
|
|
71
|
+
export function hasAttentionHook(settings: Record<string, unknown> | null, id: string): boolean {
|
|
72
|
+
if (settings === null) return false;
|
|
73
|
+
return JSON.stringify(settings.hooks ?? null).includes(`id=${encodeURIComponent(id)}&state=`);
|
|
87
74
|
}
|
|
88
75
|
|
|
89
76
|
export const attentionHooks: Injection = {
|
|
@@ -97,8 +84,7 @@ export const attentionHooks: Injection = {
|
|
|
97
84
|
log(`⚠ Claude hook injection failed: ${header.error}\n`);
|
|
98
85
|
return;
|
|
99
86
|
}
|
|
100
|
-
const
|
|
101
|
-
const hooks = await injectClaudeHooks(target, settings);
|
|
87
|
+
const hooks = await mergeClaudeSettings(target, attentionHookSettings(target.instance.id));
|
|
102
88
|
log(
|
|
103
89
|
hooks.ok
|
|
104
90
|
? '✓ Claude attention hooks installed\n'
|
|
@@ -107,11 +93,6 @@ export const attentionHooks: Injection = {
|
|
|
107
93
|
},
|
|
108
94
|
|
|
109
95
|
async check(target) {
|
|
110
|
-
return
|
|
111
|
-
target,
|
|
112
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
113
|
-
'[ -s "$d/settings.json" ] && grep -q "$1" "$d/settings.json" && echo 1 || echo 0',
|
|
114
|
-
['attention-check', target.instance.id]
|
|
115
|
-
);
|
|
96
|
+
return hasAttentionHook(await readClaudeSettings(target), target.instance.id);
|
|
116
97
|
}
|
|
117
98
|
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendLinesIfAbsent,
|
|
3
|
+
linesPresent,
|
|
4
|
+
SHELL_RC_FILES
|
|
5
|
+
} from '../lib/container-files.server.ts';
|
|
2
6
|
import type { Injection } from '../lib/injections.server.ts';
|
|
3
7
|
|
|
4
8
|
/** Both resolve `claude`, which `claude-skip-permissions` also aliases, so the two compose. */
|
|
@@ -7,37 +11,17 @@ const ALIAS_LINES = [
|
|
|
7
11
|
"alias cs='claude --model sonnet'"
|
|
8
12
|
];
|
|
9
13
|
|
|
10
|
-
/** Lines arrive as `$@` so no alias text is interpolated into the loop body. */
|
|
11
|
-
const APPLY_SCRIPT =
|
|
12
|
-
'h=$(eval echo ~$(id -un)); ' +
|
|
13
|
-
'for line in "$@"; do ' +
|
|
14
|
-
'for f in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
15
|
-
'grep -qF "$line" "$f" 2>/dev/null || printf \'%s\\n\' "$line" >> "$f"; ' +
|
|
16
|
-
'done; ' +
|
|
17
|
-
'done';
|
|
18
|
-
|
|
19
|
-
const CHECK_SCRIPT =
|
|
20
|
-
'h=$(eval echo ~$(id -un)); ' +
|
|
21
|
-
'for line in "$@"; do ' +
|
|
22
|
-
'grep -qF "$line" "$h/.bashrc" 2>/dev/null || grep -qF "$line" "$h/.zshrc" 2>/dev/null || ' +
|
|
23
|
-
'{ echo 0; exit 0; }; ' +
|
|
24
|
-
'done; ' +
|
|
25
|
-
'echo 1';
|
|
26
|
-
|
|
27
14
|
export const claudeAliases: Injection = {
|
|
28
15
|
id: 'claude-aliases',
|
|
29
16
|
label: 'claude aliases',
|
|
30
17
|
|
|
31
18
|
async apply(target, log) {
|
|
32
19
|
log('Installing c200 / cs claude aliases…\n');
|
|
33
|
-
const res = await
|
|
34
|
-
script: APPLY_SCRIPT,
|
|
35
|
-
args: ['claude-aliases', ...ALIAS_LINES]
|
|
36
|
-
});
|
|
20
|
+
const res = await appendLinesIfAbsent(target, SHELL_RC_FILES, ALIAS_LINES);
|
|
37
21
|
log(res.ok ? '✓ claude aliases installed\n' : `⚠ claude aliases setup failed: ${res.error}\n`);
|
|
38
22
|
},
|
|
39
23
|
|
|
40
24
|
async check(target) {
|
|
41
|
-
return
|
|
25
|
+
return linesPresent(target, SHELL_RC_FILES, ALIAS_LINES);
|
|
42
26
|
}
|
|
43
27
|
};
|
|
@@ -4,7 +4,9 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { CLAUDE_CODE_TOKEN, CLAUDE_KEYCHAIN_SERVICE } from '../lib/config.server.ts';
|
|
6
6
|
import { getOption } from '../lib/db.server.ts';
|
|
7
|
-
import { checkPresence
|
|
7
|
+
import { checkPresence } from '../lib/exec.server.ts';
|
|
8
|
+
import { deepMerge, editJsonFile, writeContainerFile } from '../lib/container-files.server.ts';
|
|
9
|
+
import { CLAUDE_JSON_FILE, claudeConfigFile } from '../lib/claude-settings.server.ts';
|
|
8
10
|
import { spawnCapture } from '../lib/spawn.server.ts';
|
|
9
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
10
12
|
|
|
@@ -95,13 +97,15 @@ async function injectClaudeCredentials(
|
|
|
95
97
|
target: ContainerTarget,
|
|
96
98
|
creds: string
|
|
97
99
|
): Promise<{ ok: boolean; error?: string }> {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
100
|
+
const wrote = await writeContainerFile(
|
|
101
|
+
target,
|
|
102
|
+
claudeConfigFile('.credentials.json', '600'),
|
|
103
|
+
creds
|
|
104
|
+
);
|
|
105
|
+
if (!wrote.ok) return wrote;
|
|
106
|
+
return editJsonFile(target, CLAUDE_JSON_FILE, (cur) =>
|
|
107
|
+
deepMerge(cur, { hasCompletedOnboarding: true })
|
|
108
|
+
);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/**
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { getOption } from '../lib/db.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
containerFileExists,
|
|
4
|
+
shellEnvFile,
|
|
5
|
+
deepMerge,
|
|
6
|
+
editJsonFile,
|
|
7
|
+
installShellEnvFile,
|
|
8
|
+
shellSingleQuote
|
|
9
|
+
} from '../lib/container-files.server.ts';
|
|
10
|
+
import { CLAUDE_JSON_FILE } from '../lib/claude-settings.server.ts';
|
|
3
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
12
|
|
|
5
13
|
/** Mirrors the reference launcher script (claude-code.sh); each is overridable in Settings. */
|
|
@@ -35,57 +43,36 @@ export function customEndpointConfig(): {
|
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
const
|
|
46
|
+
const ENV_FILE_NAME = '.codebay-claude-env';
|
|
47
|
+
|
|
48
|
+
/** The whole file (token included) rides `stdin` inside `installShellEnvFile`, so nothing hits argv. */
|
|
49
|
+
function envFileContent(config: NonNullable<ReturnType<typeof customEndpointConfig>>): string {
|
|
50
|
+
return (
|
|
51
|
+
[
|
|
52
|
+
'export DISABLE_AUTOUPDATER=1',
|
|
53
|
+
'export CLAUDE_CODE_USE_BEDROCK=1',
|
|
54
|
+
'export CLAUDE_CODE_SKIP_BEDROCK_AUTH=1',
|
|
55
|
+
`export ANTHROPIC_BEDROCK_BASE_URL=${shellSingleQuote(config.baseUrl)}`,
|
|
56
|
+
`export ANTHROPIC_AUTH_TOKEN=${shellSingleQuote(config.token)}`,
|
|
57
|
+
`export ANTHROPIC_DEFAULT_OPUS_MODEL=${shellSingleQuote(config.opusModel)}`,
|
|
58
|
+
`export ANTHROPIC_DEFAULT_SONNET_MODEL=${shellSingleQuote(config.sonnetModel)}`,
|
|
59
|
+
`export ANTHROPIC_DEFAULT_HAIKU_MODEL=${shellSingleQuote(config.haikuModel)}`,
|
|
60
|
+
`export ANTHROPIC_SMALL_FAST_MODEL=${shellSingleQuote(config.smallFastModel)}`,
|
|
61
|
+
`export ANTHROPIC_MODEL=${shellSingleQuote(config.defaultModel)}`
|
|
62
|
+
].join('\n') + '\n'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
39
65
|
|
|
40
|
-
/**
|
|
41
|
-
* Non-secret values ride `args`, so nothing needs hand-quoting; only the token uses `stdin`.
|
|
42
|
-
* The file mixes secret and non-secret content, hence no `writeSecretFileScript`.
|
|
43
|
-
*/
|
|
44
66
|
async function injectCustomEndpoint(
|
|
45
67
|
target: ContainerTarget,
|
|
46
68
|
config: NonNullable<ReturnType<typeof customEndpointConfig>>
|
|
47
69
|
): Promise<{ ok: boolean; error?: string }> {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"'export DISABLE_AUTOUPDATER=1' " +
|
|
55
|
-
"'export CLAUDE_CODE_USE_BEDROCK=1' " +
|
|
56
|
-
"'export CLAUDE_CODE_SKIP_BEDROCK_AUTH=1'; " +
|
|
57
|
-
'printf \'export ANTHROPIC_BEDROCK_BASE_URL=%s\\n\' "$1"; ' +
|
|
58
|
-
'printf \'export ANTHROPIC_AUTH_TOKEN=%s\\n\' "$CODEBAY_STDIN"; ' +
|
|
59
|
-
'printf \'export ANTHROPIC_DEFAULT_OPUS_MODEL=%s\\n\' "$2"; ' +
|
|
60
|
-
'printf \'export ANTHROPIC_DEFAULT_SONNET_MODEL=%s\\n\' "$3"; ' +
|
|
61
|
-
'printf \'export ANTHROPIC_DEFAULT_HAIKU_MODEL=%s\\n\' "$4"; ' +
|
|
62
|
-
'printf \'export ANTHROPIC_SMALL_FAST_MODEL=%s\\n\' "$5"; ' +
|
|
63
|
-
'printf \'export ANTHROPIC_MODEL=%s\\n\' "$6"; ' +
|
|
64
|
-
'} > "$f"; chmod 600 "$f"; ' +
|
|
65
|
-
// The grep guard keeps a re-apply from stacking duplicate source lines.
|
|
66
|
-
'h=$(eval echo ~$(id -un)); src="[ -f \\"$f\\" ] && . \\"$f\\""; ' +
|
|
67
|
-
'for rc in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
68
|
-
'grep -qF "$src" "$rc" 2>/dev/null || printf \'%s\\n\' "$src" >> "$rc"; ' +
|
|
69
|
-
'done; ' +
|
|
70
|
-
// `hasCompletedOnboarding` is what suppresses `claude`'s first-run wizard.
|
|
71
|
-
'h=$(eval echo ~$(id -un)); ' +
|
|
72
|
-
'cfg="${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}"; cfg="${cfg:-$h/.claude.json}"; ' +
|
|
73
|
-
'printf \'%s\' \'{"hasCompletedOnboarding":true}\' > "$cfg"; chmod 644 "$cfg"';
|
|
74
|
-
|
|
75
|
-
const res = await execInContainer(target, {
|
|
76
|
-
script,
|
|
77
|
-
stdin: config.token,
|
|
78
|
-
args: [
|
|
79
|
-
'claude-custom',
|
|
80
|
-
config.baseUrl,
|
|
81
|
-
config.opusModel,
|
|
82
|
-
config.sonnetModel,
|
|
83
|
-
config.haikuModel,
|
|
84
|
-
config.smallFastModel,
|
|
85
|
-
config.defaultModel
|
|
86
|
-
]
|
|
87
|
-
});
|
|
88
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
70
|
+
const env = await installShellEnvFile(target, ENV_FILE_NAME, envFileContent(config));
|
|
71
|
+
if (!env.ok) return env;
|
|
72
|
+
// `hasCompletedOnboarding` is what suppresses `claude`'s first-run wizard.
|
|
73
|
+
return editJsonFile(target, CLAUDE_JSON_FILE, (cur) =>
|
|
74
|
+
deepMerge(cur, { hasCompletedOnboarding: true })
|
|
75
|
+
);
|
|
89
76
|
}
|
|
90
77
|
|
|
91
78
|
/** Replaces `claude-code-credentials` in the registry when the custom-endpoint setting is on. */
|
|
@@ -120,6 +107,6 @@ export const claudeCodeCustom: Injection = {
|
|
|
120
107
|
},
|
|
121
108
|
|
|
122
109
|
async check(target) {
|
|
123
|
-
return
|
|
110
|
+
return containerFileExists(target, shellEnvFile(ENV_FILE_NAME));
|
|
124
111
|
}
|
|
125
112
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { checkPresence, execInContainer } from '../lib/exec.server.ts';
|
|
2
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
3
|
+
|
|
4
|
+
/** On Open VSX (code-server's default registry); the Marketplace build `/ide` targets is unreachable. */
|
|
5
|
+
export const EXTENSION_ID = 'anthropic.claude-code';
|
|
6
|
+
|
|
7
|
+
const EXT_GLOB = '~/.local/share/code-server/extensions/anthropic.claude-code-*';
|
|
8
|
+
|
|
9
|
+
/** Idempotent (skips if already present); best-effort — needs Open VSX egress. */
|
|
10
|
+
export const INSTALL_SCRIPT =
|
|
11
|
+
`if ls -d ${EXT_GLOB} >/dev/null 2>&1; then exit 0; fi; ` +
|
|
12
|
+
`command -v code-server >/dev/null 2>&1 || { echo "code-server not found" >&2; exit 1; }; ` +
|
|
13
|
+
`code-server --install-extension ${EXTENSION_ID}`;
|
|
14
|
+
|
|
15
|
+
export const CHECK_SCRIPT = `ls -d ${EXT_GLOB} >/dev/null 2>&1 && echo 1 || echo 0`;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runtime fallback to the build-time launch-line install in CODE_SERVER_LAUNCH; failure is
|
|
19
|
+
* non-fatal since the IDE integration is optional and everything else works without it.
|
|
20
|
+
*/
|
|
21
|
+
export const claudeCodeIdeExtension: Injection = {
|
|
22
|
+
id: 'claude-code-ide-extension',
|
|
23
|
+
label: 'Claude Code IDE extension',
|
|
24
|
+
|
|
25
|
+
async apply(target, log) {
|
|
26
|
+
log('Installing Claude Code IDE extension…\n');
|
|
27
|
+
// Full target (with remoteUser) so it lands in that user's code-server home, where code-server reads it.
|
|
28
|
+
const res = await execInContainer(target, { script: INSTALL_SCRIPT });
|
|
29
|
+
log(
|
|
30
|
+
res.ok
|
|
31
|
+
? '✓ Claude Code IDE extension installed\n'
|
|
32
|
+
: `⚠ Claude Code IDE extension install failed: ${res.error} — /ide integration off\n`
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async check(target) {
|
|
37
|
+
return checkPresence(target, CHECK_SCRIPT);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getOption } from '../lib/db.server.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { installShellEnvFile, shellSingleQuote } from '../lib/container-files.server.ts';
|
|
3
3
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
4
4
|
|
|
5
5
|
/** The option key → Claude env var mapping; order is the field order in Settings. */
|
|
@@ -26,33 +26,17 @@ export function manualModelConfig(): Record<string, string> | null {
|
|
|
26
26
|
return Object.keys(filled).length ? filled : null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const ENV_FILE_NAME = '.codebay-claude-models-env';
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
async function injectModels(
|
|
31
|
+
function injectModels(
|
|
33
32
|
target: ContainerTarget,
|
|
34
33
|
config: Record<string, string>
|
|
35
34
|
): Promise<{ ok: boolean; error?: string }> {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'"); ' +
|
|
42
|
-
'{ ' +
|
|
43
|
-
writes +
|
|
44
|
-
'} > "$f"; chmod 600 "$f"; ' +
|
|
45
|
-
// The grep guard keeps a re-apply from stacking duplicate source lines.
|
|
46
|
-
'h=$(eval echo ~$(id -un)); src="[ -f \\"$f\\" ] && . \\"$f\\""; ' +
|
|
47
|
-
'for rc in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
48
|
-
'grep -qF "$src" "$rc" 2>/dev/null || printf \'%s\\n\' "$src" >> "$rc"; ' +
|
|
49
|
-
'done';
|
|
50
|
-
|
|
51
|
-
const res = await execInContainer(target, {
|
|
52
|
-
script,
|
|
53
|
-
args: ['claude-models', ...entries.map(([, value]) => value)]
|
|
54
|
-
});
|
|
55
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
35
|
+
const content =
|
|
36
|
+
Object.entries(config)
|
|
37
|
+
.map(([env, value]) => `export ${env}=${shellSingleQuote(value)}`)
|
|
38
|
+
.join('\n') + '\n';
|
|
39
|
+
return installShellEnvFile(target, ENV_FILE_NAME, content);
|
|
56
40
|
}
|
|
57
41
|
|
|
58
42
|
/** Sets Claude model env vars on the standard path; skipped when LiteLLM is active. */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getOption } from '../lib/db.server.ts';
|
|
2
|
+
import { mergeClaudeSettings, readClaudeSettings } from '../lib/claude-settings.server.ts';
|
|
3
|
+
import { readHostClaudeSettings } from './claude-statusline.ts';
|
|
4
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
5
|
+
|
|
6
|
+
/** The app's own model config (manual override / LiteLLM) exports ANTHROPIC_MODEL, which wins over settings.json. */
|
|
7
|
+
function appOwnsModel(): boolean {
|
|
8
|
+
return (
|
|
9
|
+
getOption('manual_model_override_enabled') === '1' ||
|
|
10
|
+
getOption('custom_endpoint_enabled') === '1'
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The `model` default from host ~/.claude/settings.json, or null when unset or app-owned. */
|
|
15
|
+
export async function hostClaudeModel(): Promise<string | null> {
|
|
16
|
+
if (appOwnsModel()) return null;
|
|
17
|
+
const settings = await readHostClaudeSettings();
|
|
18
|
+
const model = settings?.model;
|
|
19
|
+
return typeof model === 'string' && model.trim() ? model.trim() : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Mirrors the host's default `model` into the container so subscription instances match the host. */
|
|
23
|
+
export const claudeModel: Injection = {
|
|
24
|
+
id: 'claude-model',
|
|
25
|
+
label: 'model default',
|
|
26
|
+
|
|
27
|
+
async apply(target, log) {
|
|
28
|
+
const model = await hostClaudeModel();
|
|
29
|
+
if (!model) return;
|
|
30
|
+
log(`Injecting Claude model default (${model})…\n`);
|
|
31
|
+
const result = await mergeClaudeSettings(target, { model });
|
|
32
|
+
log(
|
|
33
|
+
result.ok
|
|
34
|
+
? '✓ Claude model default configured in container\n'
|
|
35
|
+
: `⚠ Claude model default injection failed: ${result.error}\n`
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async check(target) {
|
|
40
|
+
const settings = await readClaudeSettings(target);
|
|
41
|
+
return typeof settings?.model === 'string' && settings.model.trim() !== '';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
1
|
+
import { mergeClaudeSettings, readClaudeSettings } from '../lib/claude-settings.server.ts';
|
|
2
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Both schemas on purpose: newer `claude` reads the nested `attribution` object, older builds
|
|
6
|
+
* the root-level `includeCoAuthoredBy` — setting both suppresses the byline across versions.
|
|
7
|
+
* Empty `commit`/`pr` strings drop the commit trailer and PR footer.
|
|
8
|
+
*/
|
|
9
|
+
export const NO_COAUTHOR_SETTINGS = {
|
|
10
|
+
includeCoAuthoredBy: false,
|
|
11
|
+
attribution: { commit: '', pr: '', includeCoAuthoredBy: false }
|
|
12
|
+
};
|
|
12
13
|
|
|
13
14
|
/** A Codebay-wide default rather than an opt-in setting, hence no `auth` block. */
|
|
14
15
|
export const claudeNoCoauthor: Injection = {
|
|
@@ -17,7 +18,7 @@ export const claudeNoCoauthor: Injection = {
|
|
|
17
18
|
|
|
18
19
|
async apply(target, log) {
|
|
19
20
|
log('Disabling Claude co-author commit byline…\n');
|
|
20
|
-
const result = await
|
|
21
|
+
const result = await mergeClaudeSettings(target, NO_COAUTHOR_SETTINGS);
|
|
21
22
|
log(
|
|
22
23
|
result.ok
|
|
23
24
|
? '✓ Claude co-author byline disabled\n'
|
|
@@ -26,10 +27,7 @@ export const claudeNoCoauthor: Injection = {
|
|
|
26
27
|
},
|
|
27
28
|
|
|
28
29
|
async check(target) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
32
|
-
'[ -s "$d/settings.json" ] && grep -q includeCoAuthoredBy "$d/settings.json" && echo 1 || echo 0'
|
|
33
|
-
);
|
|
30
|
+
const settings = await readClaudeSettings(target);
|
|
31
|
+
return settings !== null && 'includeCoAuthoredBy' in settings;
|
|
34
32
|
}
|
|
35
33
|
};
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendLinesIfAbsent,
|
|
3
|
+
linesPresent,
|
|
4
|
+
SHELL_RC_FILES
|
|
5
|
+
} from '../lib/container-files.server.ts';
|
|
2
6
|
import type { Injection } from '../lib/injections.server.ts';
|
|
3
7
|
|
|
4
8
|
const ALIAS_LINE = "alias claude='claude --dangerously-skip-permissions'";
|
|
5
9
|
|
|
6
|
-
/** Both rc files, since either shell may be the one code-server opens. */
|
|
7
|
-
const APPLY_SCRIPT =
|
|
8
|
-
'h=$(eval echo ~$(id -un)); ' +
|
|
9
|
-
`line="${ALIAS_LINE}"; ` +
|
|
10
|
-
'for f in "$h/.bashrc" "$h/.zshrc"; do ' +
|
|
11
|
-
'grep -qF "$line" "$f" 2>/dev/null || printf \'%s\\n\' "$line" >> "$f"; ' +
|
|
12
|
-
'done';
|
|
13
|
-
|
|
14
|
-
const CHECK_SCRIPT =
|
|
15
|
-
'h=$(eval echo ~$(id -un)); ' +
|
|
16
|
-
`line="${ALIAS_LINE}"; ` +
|
|
17
|
-
'if grep -qF "$line" "$h/.bashrc" 2>/dev/null || grep -qF "$line" "$h/.zshrc" 2>/dev/null; ' +
|
|
18
|
-
'then echo 1; else echo 0; fi';
|
|
19
|
-
|
|
20
10
|
/** Safe here only because instances are throwaway, single-tenant sandboxes. */
|
|
21
11
|
export const claudeSkipPermissions: Injection = {
|
|
22
12
|
id: 'claude-skip-permissions',
|
|
@@ -24,7 +14,7 @@ export const claudeSkipPermissions: Injection = {
|
|
|
24
14
|
|
|
25
15
|
async apply(target, log) {
|
|
26
16
|
log('Installing claude --dangerously-skip-permissions alias…\n');
|
|
27
|
-
const res = await
|
|
17
|
+
const res = await appendLinesIfAbsent(target, SHELL_RC_FILES, [ALIAS_LINE]);
|
|
28
18
|
log(
|
|
29
19
|
res.ok
|
|
30
20
|
? '✓ claude skip-permissions alias installed\n'
|
|
@@ -33,6 +23,6 @@ export const claudeSkipPermissions: Injection = {
|
|
|
33
23
|
},
|
|
34
24
|
|
|
35
25
|
async check(target) {
|
|
36
|
-
return
|
|
26
|
+
return linesPresent(target, SHELL_RC_FILES, [ALIAS_LINE]);
|
|
37
27
|
}
|
|
38
28
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
6
|
+
import {
|
|
7
|
+
claudeConfigFile,
|
|
8
|
+
mergeClaudeSettings,
|
|
9
|
+
readClaudeSettings
|
|
10
|
+
} from '../lib/claude-settings.server.ts';
|
|
7
11
|
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
8
12
|
|
|
9
13
|
/** Left unexpanded — Claude Code invokes this long after the injection has finished. */
|
|
@@ -15,7 +19,7 @@ interface StatusLineConfig {
|
|
|
15
19
|
script?: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
|
|
22
|
+
export async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
|
|
19
23
|
const file = join(homedir(), '.claude', 'settings.json');
|
|
20
24
|
if (!existsSync(file)) return null;
|
|
21
25
|
try {
|
|
@@ -25,9 +29,23 @@ async function readHostClaudeSettings(): Promise<Record<string, unknown> | null>
|
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
export const expandTilde = (p: string): string =>
|
|
33
|
+
p.startsWith('~/') ? join(homedir(), p.slice(2)) : p;
|
|
34
|
+
|
|
35
|
+
/** Only a real file counts — else jq's `//` operator and a bare `/` resolve to the root dir. */
|
|
36
|
+
const isExistingFile = (p: string): boolean => {
|
|
37
|
+
try {
|
|
38
|
+
return statSync(p).isFile();
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A heuristic: a statusLine command is either a script path or an inline snippet with no file. */
|
|
29
45
|
export function extractScriptPath(command: string): string | null {
|
|
30
|
-
const token = command
|
|
46
|
+
const token = command
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.find((t) => (t.startsWith('/') || t.startsWith('~/')) && isExistingFile(expandTilde(t)));
|
|
31
49
|
return token ?? null;
|
|
32
50
|
}
|
|
33
51
|
|
|
@@ -37,36 +55,34 @@ export async function readStatusLineConfig(): Promise<StatusLineConfig | null> {
|
|
|
37
55
|
const statusLine = settings?.statusLine as { type?: string; command?: string } | undefined;
|
|
38
56
|
if (!statusLine || statusLine.type !== 'command' || !statusLine.command) return null;
|
|
39
57
|
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
58
|
+
const token = extractScriptPath(statusLine.command);
|
|
59
|
+
if (!token) return { statusLine };
|
|
42
60
|
|
|
43
61
|
let script: string;
|
|
44
62
|
try {
|
|
45
|
-
script = await readFile(
|
|
63
|
+
script = await readFile(expandTilde(token), 'utf8');
|
|
46
64
|
} catch {
|
|
47
|
-
|
|
65
|
+
// A false-positive match must not disable the feature — inject the command verbatim.
|
|
66
|
+
return { statusLine };
|
|
48
67
|
}
|
|
49
|
-
const command = statusLine.command.replaceAll(
|
|
68
|
+
const command = statusLine.command.replaceAll(token, CONTAINER_SCRIPT_PATH);
|
|
50
69
|
return { statusLine: { ...statusLine, command }, script };
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
/**
|
|
72
|
+
/** A referenced script is staged first, then the statusLine block is merged into settings.json. */
|
|
54
73
|
async function injectStatusLine(
|
|
55
74
|
target: ContainerTarget,
|
|
56
75
|
config: StatusLineConfig
|
|
57
76
|
): Promise<{ ok: boolean; error?: string }> {
|
|
58
77
|
if (config.script) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
const wrote = await writeContainerFile(
|
|
79
|
+
target,
|
|
80
|
+
claudeConfigFile('statusline.sh', '755'),
|
|
81
|
+
config.script
|
|
82
|
+
);
|
|
83
|
+
if (!wrote.ok) return wrote;
|
|
64
84
|
}
|
|
65
|
-
|
|
66
|
-
script: mergeClaudeSettingsScript(),
|
|
67
|
-
stdin: JSON.stringify({ statusLine: config.statusLine })
|
|
68
|
-
});
|
|
69
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
85
|
+
return mergeClaudeSettings(target, { statusLine: config.statusLine });
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
export const claudeStatusline: Injection = {
|
|
@@ -100,10 +116,7 @@ export const claudeStatusline: Injection = {
|
|
|
100
116
|
},
|
|
101
117
|
|
|
102
118
|
async check(target) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
|
|
106
|
-
'[ -s "$d/settings.json" ] && grep -q statusLine "$d/settings.json" && echo 1 || echo 0'
|
|
107
|
-
);
|
|
119
|
+
const settings = await readClaudeSettings(target);
|
|
120
|
+
return !!settings?.statusLine;
|
|
108
121
|
}
|
|
109
122
|
};
|