bingocode 1.1.199 → 1.1.200-beta.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.1.199",
3
+ "version": "1.1.200-beta.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -51,8 +51,7 @@ type Stage =
51
51
  | 'editing'
52
52
  | 'slot_config'
53
53
  | 'slot_loading'
54
- | 'slot_select_model'
55
- | 'slot_input_label';
54
+ | 'slot_select_model';
56
55
 
57
56
  export const ProviderPanel: React.FC<{
58
57
  apiUrl: string;
@@ -97,9 +96,8 @@ export const ProviderPanel: React.FC<{
97
96
  const [slotProviderModels, setSlotProviderModels] = useState<Record<string, string[]>>({});
98
97
  const [currentSlotName, setCurrentSlotName] = useState<string>('main');
99
98
  const [slotLoadingMsg, setSlotLoadingMsg] = useState<string>('');
100
- const [tempSlotProviderId, setTempSlotProviderId] = useState<string>('');
101
- const [tempSlotModelId, setTempSlotModelId] = useState<string>('');
102
- const [slotLabelInput, setSlotLabelInput] = useState<string>('');
99
+
100
+
103
101
 
104
102
  const base = apiUrl.replace(/\/+$/, '');
105
103
 
@@ -348,7 +346,7 @@ export const ProviderPanel: React.FC<{
348
346
  setStage('slot_config');
349
347
  setListOffset(0);
350
348
  break;
351
- case 'test':
349
+ case 'test':
352
350
  setStage('test_select');
353
351
  setListOffset(0);
354
352
  break;
@@ -780,10 +778,25 @@ export const ProviderPanel: React.FC<{
780
778
  const sepIdx = val.indexOf('::');
781
779
  const providerId = val.slice(0, sepIdx);
782
780
  const modelId = val.slice(sepIdx + 2);
783
- setTempSlotProviderId(providerId);
784
- setTempSlotModelId(modelId);
785
- setSlotLabelInput(modelId);
786
- setStage('slot_input_label');
781
+ axios.put(`${base}/api/providers/slots/${currentSlotName}`, {
782
+ providerId,
783
+ modelId,
784
+ label: null,
785
+ })
786
+ .then(() => {
787
+ setSlotTable(prev => ({
788
+ ...prev,
789
+ [currentSlotName]: { providerId, modelId, label: null }
790
+ }));
791
+ setOpMsg(`Configured [${currentSlotName}] -> ${modelId}`);
792
+ setErr(null);
793
+ setListOffset(0);
794
+ setStage('slot_config');
795
+ })
796
+ .catch(e => {
797
+ setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Save failed');
798
+ setStage('slot_config');
799
+ });
787
800
  }}
788
801
  />
789
802
  </Box>
@@ -794,47 +807,6 @@ export const ProviderPanel: React.FC<{
794
807
  );
795
808
  }
796
809
 
797
- if (stage === 'slot_input_label') {
798
- return (
799
- <Box flexDirection="column" flexGrow={1}>
800
- <Title color="cyan">Configure Slot [{currentSlotName}] — Set Display Name</Title>
801
- <Text>
802
- Model: {providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
803
- </Text>
804
- <Box marginTop={1}>
805
- <Text>Display Name (Label): </Text>
806
- <TextInput
807
- value={slotLabelInput}
808
- onChange={setSlotLabelInput}
809
- onSubmit={() => {
810
- const label = slotLabelInput.trim() || tempSlotModelId;
811
- axios.put(`${base}/api/providers/slots/${currentSlotName}`, {
812
- providerId: tempSlotProviderId,
813
- modelId: tempSlotModelId,
814
- label,
815
- })
816
- .then(() => {
817
- setSlotTable(prev => ({
818
- ...prev,
819
- [currentSlotName]: { providerId: tempSlotProviderId, modelId: tempSlotModelId, label }
820
- }));
821
- setOpMsg(`Configured [${currentSlotName}] -> ${label}`);
822
- setErr(null);
823
- setListOffset(0);
824
- setStage('slot_config');
825
- })
826
- .catch(e => {
827
- setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Save failed');
828
- setStage('slot_config');
829
- });
830
- }}
831
- />
832
- </Box>
833
- <Hint>Enter: Save (Display name in UI) · ESC: Back to Models</Hint>
834
- </Box>
835
- );
836
- }
837
-
838
810
  return null;
839
811
  };
840
812
 
@@ -229,6 +229,9 @@ const i18nMap = {
229
229
  bypassPermsLabel: 'Bypass',
230
230
  bypassPermsOn: '已开启',
231
231
  bypassPermsOff: '已关闭',
232
+ vscodeLabel: 'Connect to VS Code',
233
+ vscodeOn: '已连接',
234
+ vscodeOff: '未连接',
232
235
  },
233
236
  en: {
234
237
  menu: {
@@ -268,6 +271,9 @@ const i18nMap = {
268
271
  bypassPermsLabel: 'Bypass',
269
272
  bypassPermsOn: 'Enabled',
270
273
  bypassPermsOff: 'Disabled',
274
+ vscodeLabel: 'Connect to VS Code',
275
+ vscodeOn: 'Connected',
276
+ vscodeOff: 'Disconnected',
271
277
  },
272
278
  ja: {
273
279
  menu: {
@@ -307,6 +313,9 @@ const i18nMap = {
307
313
  bypassPermsLabel: 'Bypass',
308
314
  bypassPermsOn: '有効',
309
315
  bypassPermsOff: '無効',
316
+ vscodeLabel: 'Connect to VS Code',
317
+ vscodeOn: '接続済',
318
+ vscodeOff: '未接続',
310
319
  },
311
320
  };
312
321
 
@@ -429,6 +438,9 @@ export const CliMenuManager: React.FC = () => {
429
438
  if (typeof bSettings.bypassPermsEnabled === 'boolean') {
430
439
  setBypassPermsEnabled(bSettings.bypassPermsEnabled);
431
440
  }
441
+ if (typeof bSettings.vscodeLinked === 'boolean') {
442
+ setVscodeLinked(bSettings.vscodeLinked);
443
+ }
432
444
  } catch {}
433
445
  }, []);
434
446
 
@@ -488,6 +500,7 @@ export const CliMenuManager: React.FC = () => {
488
500
  const [settingsCursor, setSettingsCursor] = useState(0);
489
501
  const [autoModeEnabled, setAutoModeEnabled] = useState(false);
490
502
  const [bypassPermsEnabled, setBypassPermsEnabled] = useState(false);
503
+ const [vscodeLinked, setVscodeLinked] = useState(false);
491
504
 
492
505
  // Top toolbar state
493
506
  const [animEnabled, setAnimEnabled] = useState(true);
@@ -875,7 +888,7 @@ export const CliMenuManager: React.FC = () => {
875
888
  if (!showHelp && page === 'settings') {
876
889
  if (settingsStage === 'list') {
877
890
  // +1 for the fixed Language row prepended before settingData entries
878
- const totalRows = 3 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
891
+ const totalRows = 4 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
879
892
  const visible = Math.max(1, MID_H - 2);
880
893
  if (key.downArrow || input === 'j') {
881
894
  setSettingsCursor(c => Math.min(totalRows - 1, c + 1));
@@ -923,12 +936,33 @@ export const CliMenuManager: React.FC = () => {
923
936
  }
924
937
  return next;
925
938
  });
939
+ } else if (settingsCursor === 3) {
940
+ // Row 3: toggle VS Code link
941
+ const port = (process.env.BINGO_PORT ? parseInt(process.env.BINGO_PORT) : 3456) || 3456;
942
+ const apiBase = process.env.BASE_API_URL || `http://127.0.0.1:${port}`;
943
+ setVscodeLinked(prev => {
944
+ const next = !prev;
945
+ try {
946
+ writeBingoSettings({ vscodeLinked: next });
947
+ } catch {
948
+ return prev;
949
+ }
950
+ const http = require('http');
951
+ if (next) {
952
+ http.request(`${apiBase}/api/providers/link-vscode`, { method: 'POST' })
953
+ .on('error', () => {}).end();
954
+ } else {
955
+ http.request(`${apiBase}/api/providers/link-vscode`, { method: 'DELETE' })
956
+ .on('error', () => {}).end();
957
+ }
958
+ return next;
959
+ });
926
960
  }
927
961
  }
928
962
  }
929
963
  // langPicker stage: ESC handled above; selection via SelectInput onSelect
930
964
  }
931
- }, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme, settingsStage, settingsCursor, autoModeEnabled, bypassPermsEnabled]);
965
+ }, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme, settingsStage, settingsCursor, autoModeEnabled, bypassPermsEnabled, vscodeLinked]);
932
966
 
933
967
  function cleanText(text: string): string {
934
968
  return String(text ?? '').replace(/[\n\r]+/g, ' ').replace(/\u001b\[[0-9;]*m/g, '').trim();
@@ -1412,6 +1446,7 @@ export const CliMenuManager: React.FC = () => {
1412
1446
  { key: '__lang', label: tS.langLabel, value: currentLangLabel, interactive: true },
1413
1447
  { key: '__autoMode', label: tS.autoModeLabel, value: autoModeEnabled ? tS.autoModeOn : tS.autoModeOff, interactive: true },
1414
1448
  { key: '__bypassPerms', label: tS.bypassPermsLabel, value: bypassPermsEnabled ? tS.bypassPermsOn : tS.bypassPermsOff, interactive: true },
1449
+ { key: '__vscode', label: tS.vscodeLabel, value: vscodeLinked ? tS.vscodeOn : tS.vscodeOff, interactive: true },
1415
1450
  ];
1416
1451
  const dataEntries = settingData && typeof settingData === 'object' ? Object.entries(settingData) : [];
1417
1452
  const dataRows: SettingRow[] = dataEntries.map(([k, v]) => ({
@@ -26,6 +26,9 @@ import type { SlotName } from '../types/provider.js'
26
26
  import { ApiError, errorResponse } from '../middleware/errorHandler.js'
27
27
 
28
28
  const providerService = new ProviderService()
29
+ providerService.init().catch((err) =>
30
+ console.error('[ProviderService] init failed:', err),
31
+ )
29
32
 
30
33
  function maskApiKey(key: string): string {
31
34
  if (key.length <= 8) return '****'
@@ -76,6 +79,23 @@ export async function handleProvidersApi(
76
79
  return Response.json(slots)
77
80
  }
78
81
 
82
+ // /api/providers/link-vscode — manage VS Code linking
83
+ if (id === 'link-vscode' && !action) {
84
+ if (req.method === 'GET') {
85
+ const status = await providerService.getVscodeStatus()
86
+ return Response.json(status)
87
+ }
88
+ if (req.method === 'POST') {
89
+ const result = await providerService.linkVscode()
90
+ return Response.json(result)
91
+ }
92
+ if (req.method === 'DELETE') {
93
+ const result = await providerService.unlinkVscode()
94
+ return Response.json(result)
95
+ }
96
+ throw methodNotAllowed(req.method)
97
+ }
98
+
79
99
  // PUT /api/providers/slots/:slotName — set one slot
80
100
  if (id === 'slots' && action && req.method === 'PUT') {
81
101
  const parsed = SlotNameSchema.safeParse(action)
@@ -41,6 +41,44 @@ const MANAGED_ENV_KEYS = [
41
41
  'ANTHROPIC_DEFAULT_OPUS_MODEL',
42
42
  ] as const
43
43
 
44
+ const CLAUDE_DEFAULT_LABELS: Record<SlotName, string> = {
45
+ main: 'claude-sonnet-4-5',
46
+ haiku: 'claude-haiku-4-5',
47
+ sonnet: 'claude-sonnet-4-5',
48
+ opus: 'claude-opus-4-7',
49
+ }
50
+
51
+ const VSCODE_EDITOR_NAMES = ['Code', 'Code - Insiders', 'Cursor'] as const
52
+
53
+ /**
54
+ * Return candidate VS Code settings.json paths for the current platform.
55
+ * These are checked for existence (editor directory must exist) before writing.
56
+ */
57
+ function getVscodeSettingsPaths(): string[] {
58
+ const home = os.homedir()
59
+ const paths: string[] = []
60
+
61
+ if (process.platform === 'win32') {
62
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming')
63
+ for (const name of VSCODE_EDITOR_NAMES) {
64
+ paths.push(path.join(appData, name, 'User', 'settings.json'))
65
+ }
66
+ } else if (process.platform === 'darwin') {
67
+ const lib = path.join(home, 'Library', 'Application Support')
68
+ for (const name of VSCODE_EDITOR_NAMES) {
69
+ paths.push(path.join(lib, name, 'User', 'settings.json'))
70
+ }
71
+ } else {
72
+ // Linux / other Unix
73
+ const config = path.join(home, '.config')
74
+ for (const name of VSCODE_EDITOR_NAMES) {
75
+ paths.push(path.join(config, name, 'User', 'settings.json'))
76
+ }
77
+ }
78
+
79
+ return paths
80
+ }
81
+
44
82
  const DEFAULT_INDEX: ProvidersIndex = { activeId: null, providers: [] }
45
83
 
46
84
  export class ProviderService {
@@ -476,12 +514,172 @@ export class ProviderService {
476
514
  const index = await this.readIndex()
477
515
  const provider = index.providers.find((p) => p.id === entry.providerId)
478
516
  if (!provider) return null
517
+
518
+ // Auto-fill Claude model names as label when user hasn't set one.
519
+ // This lets closed-source clients (e.g. VS Code extension) see Claude
520
+ // model names while the CLI continues to show the real upstream modelId
521
+ // (syncSettingsForSlots reads raw entry.label, which stays null).
479
522
  return {
480
523
  baseUrl: provider.baseUrl,
481
524
  apiKey: provider.apiKey,
482
525
  apiFormat: provider.apiFormat ?? 'anthropic',
483
526
  modelId: entry.modelId,
484
- label: entry.label,
527
+ label: entry.label || CLAUDE_DEFAULT_LABELS[slotName],
528
+ }
529
+ }
530
+
531
+ // --- VS Code linking ---
532
+
533
+ /**
534
+ * Build the environmentVariables array for VS Code claudeCode settings
535
+ * based on current slot configuration. Uses user-set labels or falls back
536
+ * to Claude default model names so the VS Code extension shows the right labels.
537
+ */
538
+ private async buildVscodeEnvVars(): Promise<Array<{ name: string; value: string }>> {
539
+ const slots = await this.readSlots()
540
+ const index = await this.readIndex()
541
+
542
+ function getSlotLabel(slotName: SlotName): string | null {
543
+ const entry = slots[slotName]
544
+ if (!entry) return null
545
+ const provider = index.providers.find((p) => p.id === entry.providerId)
546
+ if (!provider) return null
547
+ return entry.label || CLAUDE_DEFAULT_LABELS[slotName]
548
+ }
549
+
550
+ const envVars: Array<{ name: string; value: string }> = [
551
+ { name: 'ANTHROPIC_BASE_URL', value: `http://127.0.0.1:${ProviderService.serverPort}/proxy` },
552
+ { name: 'ANTHROPIC_AUTH_TOKEN', value: 'proxy-managed' },
553
+ ]
554
+
555
+ const mainLabel = getSlotLabel("main")
556
+ if (mainLabel) {
557
+ envVars.push({ name: "ANTHROPIC_MODEL", value: mainLabel })
558
+ }
559
+ const haikuLabel = getSlotLabel("haiku")
560
+ if (haikuLabel) {
561
+ envVars.push({ name: "ANTHROPIC_DEFAULT_HAIKU_MODEL", value: haikuLabel })
562
+ }
563
+ const sonnetLabel = getSlotLabel("sonnet")
564
+ if (sonnetLabel) {
565
+ envVars.push({ name: "ANTHROPIC_DEFAULT_SONNET_MODEL", value: sonnetLabel })
566
+ }
567
+ const opusLabel = getSlotLabel("opus")
568
+ if (opusLabel) {
569
+ envVars.push({ name: "ANTHROPIC_DEFAULT_OPUS_MODEL", value: opusLabel })
570
+ }
571
+
572
+ return envVars
573
+ }
574
+
575
+ /**
576
+ * Write claudeCode settings into ALL detected VS Code editor instances
577
+ * (stable, Insiders, Cursor). Persists linked state in bingo settings so
578
+ * it survives restarts.
579
+ */
580
+ async linkVscode(): Promise<{ paths: string[]; linked: boolean }> {
581
+ const envVars = await this.buildVscodeEnvVars()
582
+ const candidatePaths = getVscodeSettingsPaths()
583
+ const writtenPaths: string[] = []
584
+
585
+ for (const settingsPath of candidatePaths) {
586
+ // Only write if the editor config directory exists (editor is installed)
587
+ const editorDir = path.dirname(path.dirname(settingsPath))
588
+ try {
589
+ await fs.access(editorDir)
590
+ } catch {
591
+ continue
592
+ }
593
+
594
+ let settings: Record<string, unknown> = {}
595
+ try {
596
+ const raw = await fs.readFile(settingsPath, "utf-8")
597
+ settings = JSON.parse(raw)
598
+ } catch {
599
+ // File does not exist yet — start fresh
600
+ }
601
+
602
+ settings.claudeCode = {
603
+ ...(settings.claudeCode as Record<string, unknown> || {}),
604
+ preferredLocation: "panel",
605
+ disableLoginPrompt: true,
606
+ environmentVariables: envVars,
607
+ }
608
+
609
+ await fs.mkdir(path.dirname(settingsPath), { recursive: true })
610
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
611
+ writtenPaths.push(settingsPath)
612
+ }
613
+
614
+ if (writtenPaths.length === 0) {
615
+ throw ApiError.badRequest(
616
+ 'No VS Code installation detected. Please install VS Code (stable, Insiders, or Cursor) first.',
617
+ )
618
+ }
619
+
620
+ // Persist so re-sync on restart and unlink work correctly
621
+ const bingoSettings = await this.readSettings()
622
+ bingoSettings.vscodeLinked = true
623
+ bingoSettings.vscodeLinkedPaths = writtenPaths
624
+ await this.writeSettings(bingoSettings)
625
+
626
+ return { paths: writtenPaths, linked: true }
627
+ }
628
+
629
+ /**
630
+ * Remove claudeCode keys from all previously-linked VS Code settings files.
631
+ * Does NOT delete the entire file — it only removes the claudeCode key.
632
+ */
633
+ async unlinkVscode(): Promise<{ paths: string[]; linked: boolean }> {
634
+ const bingoSettings = await this.readSettings()
635
+ const linkedPaths = (bingoSettings.vscodeLinkedPaths as string[]) || []
636
+
637
+ for (const settingsPath of linkedPaths) {
638
+ try {
639
+ const raw = await fs.readFile(settingsPath, "utf-8")
640
+ const settings = JSON.parse(raw)
641
+ delete settings.claudeCode
642
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
643
+ } catch {
644
+ // File may have been moved or deleted since linking — harmless
645
+ }
646
+ }
647
+
648
+ bingoSettings.vscodeLinked = false
649
+ delete bingoSettings.vscodeLinkedPaths
650
+ await this.writeSettings(bingoSettings)
651
+
652
+ return { paths: linkedPaths, linked: false }
653
+ }
654
+
655
+ /**
656
+ * Return current VS Code link status from persisted bingo settings.
657
+ */
658
+ async getVscodeStatus(): Promise<{ linked: boolean; paths: string[] }> {
659
+ const bingoSettings = await this.readSettings()
660
+ return {
661
+ linked: Boolean(bingoSettings.vscodeLinked),
662
+ paths: (bingoSettings.vscodeLinkedPaths as string[]) || [],
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Initialise the service. If vscodeLinked is true in bingo settings,
668
+ * re-sync VS Code settings so they stay current with the slot config.
669
+ * Should be called once after server startup.
670
+ */
671
+ async init(): Promise<void> {
672
+ const status = await this.getVscodeStatus()
673
+ if (status.linked) {
674
+ try {
675
+ await this.linkVscode()
676
+ } catch {
677
+ // VS Code may have been uninstalled — clear stale state
678
+ const bingoSettings = await this.readSettings()
679
+ bingoSettings.vscodeLinked = false
680
+ delete bingoSettings.vscodeLinkedPaths
681
+ await this.writeSettings(bingoSettings)
682
+ }
485
683
  }
486
684
  }
487
685