codebay 0.5.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.
Files changed (110) hide show
  1. package/.mochi/manifest.json +275 -223
  2. package/.mochi/svelte-client/HydratableIsland-qyjx1d98.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-1y0r9tae.js +2 -0
  4. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-89wtkb86.js +13 -0
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-eamnbyqk.js +1 -0
  6. package/.mochi/svelte-client/{_hydrate-SettingsView_8hmarwmf8onq-r9vvc7rx.js → _hydrate-SettingsView_8hmarwmf8onq-kdra4mcb.js} +3 -3
  7. package/.mochi/svelte-client/chunk-330mrspw.js +2 -0
  8. package/.mochi/svelte-client/{chunk-dz1019cy.js → chunk-4jgkak13.js} +1 -1
  9. package/.mochi/svelte-client/chunk-fqg2qzj8.js +2 -0
  10. package/.mochi/svelte-client/chunk-k5m4kt20.js +2 -0
  11. package/.mochi/svelte-client/{chunk-nmf4dq86.js → chunk-qj7zy2d9.js} +1 -1
  12. package/.mochi/svelte-client/{chunk-ssa0ad2f.js → chunk-rpdphch7.js} +1 -1
  13. package/.mochi/svelte-compile/{App-9qjy14bc.server.js → App-v5mwmv6t.server.js} +26 -2
  14. package/.mochi/svelte-compile/{Avatars-sq8mjn6k.server.js → Avatars-pzyz0tv3.server.js} +75 -15
  15. package/.mochi/svelte-compile/{Instance-13j59daw.server.js → Instance-49ksrp11.server.js} +15 -3
  16. package/.mochi/svelte-compile/{Settings-p0s2awg1.server.js → Settings-sz5d6qj7.server.js} +3 -3
  17. package/.mochi/svelte-compile/{chunk-dfe2km6a.js → chunk-q8rbesaw.js} +324 -123
  18. package/.mochi/svelte-css/Avatar-3pi30db6ozvf0.css +1 -0
  19. package/.mochi/svelte-css/AvatarEditor-2ewwtejaml4e2.css +1 -0
  20. package/.mochi/svelte-css/{AvatarsView-3hq37u4dgtljn.css → AvatarsView-2ughbzc26luyb.css} +1 -1
  21. package/.mochi/svelte-css/InstanceCard-3vnltxnbxkvay.css +1 -0
  22. package/.mochi/svelte-css/{InstanceView-wq85bxtusont.css → InstanceView-3vzow3ttxzlh3.css} +1 -1
  23. package/README.md +2 -0
  24. package/package.json +2 -2
  25. package/src/avatars/anchor.ts +1 -0
  26. package/src/avatars/ant-head.ts +17 -0
  27. package/src/avatars/apple.ts +17 -0
  28. package/src/avatars/bear.ts +1 -0
  29. package/src/avatars/bee.ts +1 -0
  30. package/src/avatars/bodybuilder.ts +17 -0
  31. package/src/avatars/cat.ts +1 -0
  32. package/src/avatars/cherry.ts +1 -0
  33. package/src/avatars/crab.ts +1 -0
  34. package/src/avatars/crown.ts +1 -0
  35. package/src/avatars/diamond.ts +1 -0
  36. package/src/avatars/dog.ts +1 -0
  37. package/src/avatars/editor.ts +17 -0
  38. package/src/avatars/fish.ts +1 -0
  39. package/src/avatars/flower.ts +1 -0
  40. package/src/avatars/fox.ts +1 -0
  41. package/src/avatars/frog.ts +1 -0
  42. package/src/avatars/ghost.ts +1 -0
  43. package/src/avatars/heart.ts +8 -8
  44. package/src/avatars/horse.ts +17 -0
  45. package/src/avatars/index.ts +20 -0
  46. package/src/avatars/invader.ts +1 -0
  47. package/src/avatars/key.ts +1 -0
  48. package/src/avatars/lightning.ts +1 -0
  49. package/src/avatars/mushroom.ts +1 -0
  50. package/src/avatars/octopus.ts +1 -0
  51. package/src/avatars/owl.ts +1 -0
  52. package/src/avatars/penguin.ts +1 -0
  53. package/src/avatars/planet.ts +1 -0
  54. package/src/avatars/praying-mantis.ts +17 -0
  55. package/src/avatars/rabbit.ts +1 -0
  56. package/src/avatars/robot.ts +1 -0
  57. package/src/avatars/rocket.ts +1 -0
  58. package/src/avatars/serialize.ts +4 -3
  59. package/src/avatars/skull.ts +6 -6
  60. package/src/avatars/snail.ts +1 -0
  61. package/src/avatars/snowflake.ts +17 -0
  62. package/src/avatars/star.ts +1 -0
  63. package/src/avatars/strawberry.ts +17 -0
  64. package/src/avatars/target.ts +1 -0
  65. package/src/avatars/tree.ts +1 -0
  66. package/src/avatars/types.ts +6 -3
  67. package/src/avatars/vangogh-almond-blossoms.ts +17 -0
  68. package/src/avatars/wasp.ts +17 -0
  69. package/src/avatars/whale.ts +1 -0
  70. package/src/avatars/windmill.ts +17 -0
  71. package/src/components/AppShell.svelte +13 -0
  72. package/src/components/Avatar.svelte +4 -1
  73. package/src/components/AvatarEditor.svelte +83 -5
  74. package/src/components/AvatarsView.svelte +14 -1
  75. package/src/components/DashboardView.svelte +24 -2
  76. package/src/components/InstanceCard.svelte +55 -6
  77. package/src/components/InstanceView.svelte +41 -8
  78. package/src/components/SettingsView.svelte +2 -2
  79. package/src/container-injections/attention-hooks.ts +19 -38
  80. package/src/container-injections/claude-aliases.ts +7 -23
  81. package/src/container-injections/claude-code-credentials.ts +12 -8
  82. package/src/container-injections/claude-code-custom.ts +35 -48
  83. package/src/container-injections/claude-code-ide-extension.ts +39 -0
  84. package/src/container-injections/claude-code-models.ts +8 -24
  85. package/src/container-injections/claude-model.ts +5 -28
  86. package/src/container-injections/claude-no-coauthor.ts +5 -17
  87. package/src/container-injections/claude-skip-permissions.ts +7 -17
  88. package/src/container-injections/claude-statusline.ts +16 -18
  89. package/src/container-injections/claude-trust.ts +73 -0
  90. package/src/container-injections/host-env-vars.ts +13 -24
  91. package/src/container-injections/tmux.ts +2 -11
  92. package/src/index.ts +2 -0
  93. package/src/lib/claude-settings.server.ts +35 -0
  94. package/src/lib/container-files.server.ts +219 -0
  95. package/src/lib/devcontainer.server.ts +38 -1
  96. package/src/lib/exec.server.ts +2 -7
  97. package/src/lib/injections.server.ts +4 -0
  98. package/src/lib/instances.server.ts +14 -0
  99. package/src/lib/theme.server.ts +7 -3
  100. package/src/shell.html +2 -0
  101. package/.mochi/svelte-client/HydratableIsland-79z9xq4b.js +0 -1
  102. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-f5rzyjm8.js +0 -2
  103. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js +0 -13
  104. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-nv31ptdp.js +0 -1
  105. package/.mochi/svelte-client/chunk-6eexkr6p.js +0 -2
  106. package/.mochi/svelte-client/chunk-7znwaf7t.js +0 -2
  107. package/.mochi/svelte-client/chunk-zmmb1c4b.js +0 -2
  108. package/.mochi/svelte-css/Avatar-24th63awm9mna.css +0 -1
  109. package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +0 -1
  110. package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +0 -1
@@ -1,5 +1,10 @@
1
1
  import { PORT } from '../lib/config.server.ts';
2
- import { checkPresence, execInContainer } from '../lib/exec.server.ts';
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
- const settings = {
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
- async function writeBridgeHeader(
59
+ function writeBridgeHeader(
56
60
  target: ContainerTarget,
57
61
  token: string
58
62
  ): Promise<{ ok: boolean; error?: string }> {
59
- const script =
60
- 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; mkdir -p "$d"; ' +
61
- 'f="$d/.bridge-header"; printf \'X-Bridge-Token: %s\\n\' "$CODEBAY_STDIN" > "$f"; chmod 600 "$f"';
62
- const res = await execInContainer(target, { script, stdin: token });
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
- async function injectClaudeHooks(
79
- target: ContainerTarget,
80
- settingsJson: string
81
- ): Promise<{ ok: boolean; error?: string }> {
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 settings = attentionHookSettings(target.instance.id);
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 checkPresence(
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 { checkPresence, execInContainer } from '../lib/exec.server.ts';
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 execInContainer(target, {
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 checkPresence(target, CHECK_SCRIPT, ['claude-aliases', ...ALIAS_LINES]);
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, execInContainer, writeSecretFileScript } from '../lib/exec.server.ts';
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 script =
99
- 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
100
- writeSecretFileScript('$d', '.credentials.json', '600') +
101
- ' cfg="${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}"; cfg="${cfg:-$h/.claude.json}"; ' +
102
- 'printf \'%s\' \'{"hasCompletedOnboarding":true}\' > "$cfg"; chmod 644 "$cfg"';
103
- const res = await execInContainer(target, { script, stdin: creds });
104
- return res.ok ? { ok: true } : { ok: false, error: res.error };
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 { checkPresence, execInContainer } from '../lib/exec.server.ts';
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 ENV_FILE = '~/.codebay-claude-env';
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 script =
49
- 'set -e; f=$(eval echo "' +
50
- ENV_FILE +
51
- '"); ' +
52
- '{ ' +
53
- "printf '%s\\n' " +
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 checkPresence(target, `f=$(eval echo "${ENV_FILE}"); [ -s "$f" ] && echo 1 || echo 0`);
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 { execInContainer } from '../lib/exec.server.ts';
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 ENV_FILE = '~/.codebay-claude-models-env';
29
+ const ENV_FILE_NAME = '.codebay-claude-models-env';
30
30
 
31
- /** Model IDs are non-secret, so every value rides `args` ($1, $2, …) — no stdin. */
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 entries = Object.entries(config);
37
- const writes = entries.map(([env], i) => `printf 'export ${env}=%s\\n' "$${i + 1}"; `).join('');
38
- const script =
39
- 'set -e; f=$(eval echo "' +
40
- ENV_FILE +
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. */
@@ -1,8 +1,7 @@
1
1
  import { getOption } from '../lib/db.server.ts';
2
- import { checkPresence, execInContainer } from '../lib/exec.server.ts';
3
- import { mergeClaudeSettingsScript } from './attention-hooks.ts';
2
+ import { mergeClaudeSettings, readClaudeSettings } from '../lib/claude-settings.server.ts';
4
3
  import { readHostClaudeSettings } from './claude-statusline.ts';
5
- import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
4
+ import type { Injection } from '../lib/injections.server.ts';
6
5
 
7
6
  /** The app's own model config (manual override / LiteLLM) exports ANTHROPIC_MODEL, which wins over settings.json. */
8
7
  function appOwnsModel(): boolean {
@@ -20,35 +19,16 @@ export async function hostClaudeModel(): Promise<string | null> {
20
19
  return typeof model === 'string' && model.trim() ? model.trim() : null;
21
20
  }
22
21
 
23
- async function injectModel(
24
- target: ContainerTarget,
25
- model: string
26
- ): Promise<{ ok: boolean; error?: string }> {
27
- const res = await execInContainer(target, {
28
- script: mergeClaudeSettingsScript(),
29
- stdin: JSON.stringify({ model })
30
- });
31
- return res.ok ? { ok: true } : { ok: false, error: res.error };
32
- }
33
-
34
22
  /** Mirrors the host's default `model` into the container so subscription instances match the host. */
35
23
  export const claudeModel: Injection = {
36
24
  id: 'claude-model',
37
25
  label: 'model default',
38
26
 
39
- auth: {
40
- hint: 'set `model` in ~/.claude/settings.json',
41
- async status() {
42
- const model = await hostClaudeModel();
43
- return { available: model !== null, source: model ? '~/.claude/settings.json' : null };
44
- }
45
- },
46
-
47
27
  async apply(target, log) {
48
28
  const model = await hostClaudeModel();
49
29
  if (!model) return;
50
30
  log(`Injecting Claude model default (${model})…\n`);
51
- const result = await injectModel(target, model);
31
+ const result = await mergeClaudeSettings(target, { model });
52
32
  log(
53
33
  result.ok
54
34
  ? '✓ Claude model default configured in container\n'
@@ -57,10 +37,7 @@ export const claudeModel: Injection = {
57
37
  },
58
38
 
59
39
  async check(target) {
60
- return checkPresence(
61
- target,
62
- 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
63
- '[ -s "$d/settings.json" ] && grep -q \'"model"\' "$d/settings.json" && echo 1 || echo 0'
64
- );
40
+ const settings = await readClaudeSettings(target);
41
+ return typeof settings?.model === 'string' && settings.model.trim() !== '';
65
42
  }
66
43
  };
@@ -1,6 +1,5 @@
1
- import { checkPresence, execInContainer } from '../lib/exec.server.ts';
2
- import { mergeClaudeSettingsScript } from './attention-hooks.ts';
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
4
  /**
6
5
  * Both schemas on purpose: newer `claude` reads the nested `attribution` object, older builds
@@ -12,14 +11,6 @@ export const NO_COAUTHOR_SETTINGS = {
12
11
  attribution: { commit: '', pr: '', includeCoAuthoredBy: false }
13
12
  };
14
13
 
15
- async function injectNoCoauthor(target: ContainerTarget): Promise<{ ok: boolean; error?: string }> {
16
- const res = await execInContainer(target, {
17
- script: mergeClaudeSettingsScript(),
18
- stdin: JSON.stringify(NO_COAUTHOR_SETTINGS)
19
- });
20
- return res.ok ? { ok: true } : { ok: false, error: res.error };
21
- }
22
-
23
14
  /** A Codebay-wide default rather than an opt-in setting, hence no `auth` block. */
24
15
  export const claudeNoCoauthor: Injection = {
25
16
  id: 'claude-no-coauthor',
@@ -27,7 +18,7 @@ export const claudeNoCoauthor: Injection = {
27
18
 
28
19
  async apply(target, log) {
29
20
  log('Disabling Claude co-author commit byline…\n');
30
- const result = await injectNoCoauthor(target);
21
+ const result = await mergeClaudeSettings(target, NO_COAUTHOR_SETTINGS);
31
22
  log(
32
23
  result.ok
33
24
  ? '✓ Claude co-author byline disabled\n'
@@ -36,10 +27,7 @@ export const claudeNoCoauthor: Injection = {
36
27
  },
37
28
 
38
29
  async check(target) {
39
- return checkPresence(
40
- target,
41
- 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
42
- '[ -s "$d/settings.json" ] && grep -q includeCoAuthoredBy "$d/settings.json" && echo 1 || echo 0'
43
- );
30
+ const settings = await readClaudeSettings(target);
31
+ return settings !== null && 'includeCoAuthoredBy' in settings;
44
32
  }
45
33
  };
@@ -1,22 +1,12 @@
1
- import { checkPresence, execInContainer } from '../lib/exec.server.ts';
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 execInContainer(target, { script: APPLY_SCRIPT });
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 checkPresence(target, CHECK_SCRIPT);
26
+ return linesPresent(target, SHELL_RC_FILES, [ALIAS_LINE]);
37
27
  }
38
28
  };
@@ -2,8 +2,12 @@ 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 { checkPresence, execInContainer } from '../lib/exec.server.ts';
6
- import { mergeClaudeSettingsScript } from './attention-hooks.ts';
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. */
@@ -65,23 +69,20 @@ export async function readStatusLineConfig(): Promise<StatusLineConfig | null> {
65
69
  return { statusLine: { ...statusLine, command }, script };
66
70
  }
67
71
 
68
- /** Two execs rather than one, because `execInContainer` carries a single `stdin` payload per call. */
72
+ /** A referenced script is staged first, then the statusLine block is merged into settings.json. */
69
73
  async function injectStatusLine(
70
74
  target: ContainerTarget,
71
75
  config: StatusLineConfig
72
76
  ): Promise<{ ok: boolean; error?: string }> {
73
77
  if (config.script) {
74
- const writeScript =
75
- 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; mkdir -p "$d"; ' +
76
- 'printf \'%s\' "$CODEBAY_STDIN" > "$d/statusline.sh"; chmod 755 "$d/statusline.sh"';
77
- const res = await execInContainer(target, { script: writeScript, stdin: config.script });
78
- if (!res.ok) return { ok: false, error: res.error };
78
+ const wrote = await writeContainerFile(
79
+ target,
80
+ claudeConfigFile('statusline.sh', '755'),
81
+ config.script
82
+ );
83
+ if (!wrote.ok) return wrote;
79
84
  }
80
- const res = await execInContainer(target, {
81
- script: mergeClaudeSettingsScript(),
82
- stdin: JSON.stringify({ statusLine: config.statusLine })
83
- });
84
- return res.ok ? { ok: true } : { ok: false, error: res.error };
85
+ return mergeClaudeSettings(target, { statusLine: config.statusLine });
85
86
  }
86
87
 
87
88
  export const claudeStatusline: Injection = {
@@ -115,10 +116,7 @@ export const claudeStatusline: Injection = {
115
116
  },
116
117
 
117
118
  async check(target) {
118
- return checkPresence(
119
- target,
120
- 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
121
- '[ -s "$d/settings.json" ] && grep -q statusLine "$d/settings.json" && echo 1 || echo 0'
122
- );
119
+ const settings = await readClaudeSettings(target);
120
+ return !!settings?.statusLine;
123
121
  }
124
122
  };
@@ -0,0 +1,73 @@
1
+ import { deepMerge, editJsonFile, readJsonFile } from '../lib/container-files.server.ts';
2
+ import {
3
+ CLAUDE_JSON_FILE,
4
+ mergeClaudeSettings,
5
+ readClaudeSettings
6
+ } from '../lib/claude-settings.server.ts';
7
+ import type { Injection } from '../lib/injections.server.ts';
8
+
9
+ /**
10
+ * Pre-accepts the three prompts `claude` shows on first launch, so a throwaway instance
11
+ * opens straight into a session: the folder-trust dialog and the MCP-server approval
12
+ * (both per-project, keyed by the path `claude` runs in), and the one-time Bypass
13
+ * Permissions acknowledgement (`--dangerously-skip-permissions` still trips it without this).
14
+ * Key names verified against the shipped `claude` binary.
15
+ */
16
+ export function claudeTrustConfig(workspacePath: string | null): Record<string, unknown> {
17
+ const cfg: Record<string, unknown> = {
18
+ // Idempotent with the credentials injection; keeps this self-sufficient when auth is skipped.
19
+ hasCompletedOnboarding: true,
20
+ bypassPermissionsModeAccepted: true
21
+ };
22
+ if (workspacePath) {
23
+ cfg.projects = {
24
+ [workspacePath]: {
25
+ hasTrustDialogAccepted: true,
26
+ hasCompletedProjectOnboarding: true,
27
+ enableAllProjectMcpServers: true
28
+ }
29
+ };
30
+ }
31
+ return cfg;
32
+ }
33
+
34
+ /** claude ≥2.1.220 keeps the bypass acknowledgement here; the `.claude.json` key stays for older versions. */
35
+ export const CLAUDE_TRUST_SETTINGS: Record<string, unknown> = {
36
+ skipDangerousModePermissionPrompt: true
37
+ };
38
+
39
+ /** Safe here only because instances are throwaway, single-tenant sandboxes. */
40
+ export const claudeTrust: Injection = {
41
+ id: 'claude-trust',
42
+ label: 'Claude trust & MCP auto-accept',
43
+
44
+ async apply(target, log) {
45
+ log('Pre-accepting Claude trust, MCP, and bypass-permissions prompts…\n');
46
+ // deepMerge recurses into `projects`, so the per-project keys layer onto any existing entries.
47
+ const res = await editJsonFile(target, CLAUDE_JSON_FILE, (cur) =>
48
+ deepMerge(cur, claudeTrustConfig(target.instance.remote_workspace_folder))
49
+ );
50
+ if (!res.ok) {
51
+ log(`⚠ Claude trust injection failed: ${res.error}\n`);
52
+ return;
53
+ }
54
+ const settings = await mergeClaudeSettings(target, CLAUDE_TRUST_SETTINGS);
55
+ log(
56
+ settings.ok
57
+ ? '✓ Claude startup prompts pre-accepted\n'
58
+ : `⚠ Claude trust injection failed: ${settings.error}\n`
59
+ );
60
+ },
61
+
62
+ // Not keyed on `.claude.json`'s `bypassPermissionsModeAccepted`: claude ≥2.1.220 drops
63
+ // that key (and `enableAllProjectMcpServers`) when it rewrites the file after startup.
64
+ async check(target) {
65
+ const settings = await readClaudeSettings(target);
66
+ if (settings?.skipDangerousModePermissionPrompt !== true) return false;
67
+ const workspace = target.instance.remote_workspace_folder;
68
+ if (!workspace) return true;
69
+ const cfg = await readJsonFile(target, CLAUDE_JSON_FILE);
70
+ const projects = cfg?.projects as Record<string, Record<string, unknown>> | undefined;
71
+ return projects?.[workspace]?.hasTrustDialogAccepted === true;
72
+ }
73
+ };