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
|
@@ -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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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 =
|
|
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
|
|