bingocode 1.1.154 → 1.1.156

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.154",
3
+ "version": "1.1.156",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -100,6 +100,39 @@ function writeGlobalClaudeConfig(updates: Record<string, unknown>): void {
100
100
  fs.renameSync(tmp, configPath);
101
101
  }
102
102
 
103
+ function readClaudeSettings(): Record<string, unknown> {
104
+ const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
105
+ const settingsPath = path.join(configDir, 'settings.json');
106
+ try {
107
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
108
+ return JSON.parse(raw) as Record<string, unknown>;
109
+ } catch {
110
+ return {};
111
+ }
112
+ }
113
+
114
+ function writeClaudeSettings(updates: Record<string, unknown>): void {
115
+ const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
116
+ const settingsPath = path.join(configDir, 'settings.json');
117
+ const dir = path.dirname(settingsPath);
118
+ if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
119
+
120
+ let current: Record<string, unknown> = {};
121
+ try {
122
+ if (fs.existsSync(settingsPath)) {
123
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
124
+ current = JSON.parse(raw) as Record<string, unknown>;
125
+ }
126
+ } catch {}
127
+
128
+ const merged = { ...current, ...updates };
129
+
130
+ // atomic write via temp + rename
131
+ const tmp = `${settingsPath}.tmp.${Date.now()}`;
132
+ fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
133
+ fs.renameSync(tmp, settingsPath);
134
+ }
135
+
103
136
  /**
104
137
  * Determine if in "official" mode (no custom provider active).
105
138
  * Logic matches ConversationService.shouldMarkManagedOAuth().
@@ -193,6 +226,9 @@ const i18nMap = {
193
226
  autoModeLabel: 'Auto Mode',
194
227
  autoModeOn: '已开启',
195
228
  autoModeOff: '已关闭',
229
+ bypassPermsLabel: 'Bypass',
230
+ bypassPermsOn: '已开启',
231
+ bypassPermsOff: '已关闭',
196
232
  },
197
233
  en: {
198
234
  menu: {
@@ -229,6 +265,9 @@ const i18nMap = {
229
265
  autoModeLabel: 'Auto Mode',
230
266
  autoModeOn: 'Enabled',
231
267
  autoModeOff: 'Disabled',
268
+ bypassPermsLabel: 'Bypass',
269
+ bypassPermsOn: 'Enabled',
270
+ bypassPermsOff: 'Disabled',
232
271
  },
233
272
  ja: {
234
273
  menu: {
@@ -265,6 +304,9 @@ const i18nMap = {
265
304
  autoModeLabel: 'Auto Mode',
266
305
  autoModeOn: '有効',
267
306
  autoModeOff: '無効',
307
+ bypassPermsLabel: 'Bypass',
308
+ bypassPermsOn: '有効',
309
+ bypassPermsOff: '無効',
268
310
  },
269
311
  };
270
312
 
@@ -384,6 +426,9 @@ export const CliMenuManager: React.FC = () => {
384
426
  if (typeof bSettings.autoModeEnabled === 'boolean') {
385
427
  setAutoModeEnabled(bSettings.autoModeEnabled);
386
428
  }
429
+ if (typeof bSettings.bypassPermsEnabled === 'boolean') {
430
+ setBypassPermsEnabled(bSettings.bypassPermsEnabled);
431
+ }
387
432
  } catch {}
388
433
  }, []);
389
434
 
@@ -442,6 +487,7 @@ export const CliMenuManager: React.FC = () => {
442
487
  const [settingsStage, setSettingsStage] = useState<'list' | 'langPicker'>('list');
443
488
  const [settingsCursor, setSettingsCursor] = useState(0);
444
489
  const [autoModeEnabled, setAutoModeEnabled] = useState(false);
490
+ const [bypassPermsEnabled, setBypassPermsEnabled] = useState(false);
445
491
 
446
492
  // Top toolbar state
447
493
  const [animEnabled, setAnimEnabled] = useState(true);
@@ -829,7 +875,7 @@ export const CliMenuManager: React.FC = () => {
829
875
  if (!showHelp && page === 'settings') {
830
876
  if (settingsStage === 'list') {
831
877
  // +1 for the fixed Language row prepended before settingData entries
832
- const totalRows = 2 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
878
+ const totalRows = 3 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
833
879
  const visible = Math.max(1, MID_H - 2);
834
880
  if (key.downArrow || input === 'j') {
835
881
  setSettingsCursor(c => Math.min(totalRows - 1, c + 1));
@@ -862,12 +908,27 @@ export const CliMenuManager: React.FC = () => {
862
908
  }
863
909
  return next;
864
910
  });
911
+ } else if (settingsCursor === 2) {
912
+ // Row 2: toggle Bypass Permissions
913
+ setBypassPermsEnabled(prev => {
914
+ const next = !prev;
915
+ try {
916
+ writeBingoSettings({ bypassPermsEnabled: next });
917
+ const safeSettings = next
918
+ ? { permissions: { defaultMode: 'bypassPermissions', skipDangerousModePermissionPrompt: true } }
919
+ : { permissions: { defaultMode: 'default' } };
920
+ writeClaudeSettings(safeSettings);
921
+ } catch {
922
+ return prev; // write failed — keep old state
923
+ }
924
+ return next;
925
+ });
865
926
  }
866
927
  }
867
928
  }
868
929
  // langPicker stage: ESC handled above; selection via SelectInput onSelect
869
930
  }
870
- }, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme, settingsStage, settingsCursor, autoModeEnabled]);
931
+ }, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme, settingsStage, settingsCursor, autoModeEnabled, bypassPermsEnabled]);
871
932
 
872
933
  function cleanText(text: string): string {
873
934
  return String(text ?? '').replace(/[\n\r]+/g, ' ').replace(/\u001b\[[0-9;]*m/g, '').trim();
@@ -1350,6 +1411,7 @@ export const CliMenuManager: React.FC = () => {
1350
1411
  const fixedRows: SettingRow[] = [
1351
1412
  { key: '__lang', label: tS.langLabel, value: currentLangLabel, interactive: true },
1352
1413
  { key: '__autoMode', label: tS.autoModeLabel, value: autoModeEnabled ? tS.autoModeOn : tS.autoModeOff, interactive: true },
1414
+ { key: '__bypassPerms', label: tS.bypassPermsLabel, value: bypassPermsEnabled ? tS.bypassPermsOn : tS.bypassPermsOff, interactive: true },
1353
1415
  ];
1354
1416
  const dataEntries = settingData && typeof settingData === 'object' ? Object.entries(settingData) : [];
1355
1417
  const dataRows: SettingRow[] = dataEntries.map(([k, v]) => ({
@@ -59,10 +59,17 @@ export function registerGoalSkill(): void {
59
59
 
60
60
  Goal condition: "${trimmed}"
61
61
 
62
- This goal is now registered for this session. An independent evaluator model will check after each turn whether the goal is satisfied. Maximum ${maxIter} iterations.
62
+ This goal is now registered for this session. After each turn, an independent evaluator (Haiku 4.5, a weak model) will check whether the goal is satisfied. Maximum ${maxIter} iterations.
63
63
 
64
- Tell the user: Goal set you will work autonomously until "${trimmed}" is achieved (max ${maxIter} turns). Send \`/goal clear\` to cancel.
64
+ CRITICAL: The evaluator reads ONLY your text output. It cannot see code changes, tool results, or file contents only the plain text you write.
65
+
66
+ At each turn toward the goal, output a short evaluation block like:
67
+ > EVAL: [metric1]: [value] / [target] → ✓ or ✗
65
68
 
69
+ This block is the ONLY signal the evaluator can reliably process. Make it short,
70
+ unambiguous, and quantitative. Do NOT expect the evaluator to infer success from narrative discussion.
71
+
72
+ Tell the user: Goal set — you will work autonomously until "${trimmed}" is achieved (max ${maxIter} turns). Send \`/goal clear\` to cancel.
66
73
  Now begin: assess current state and take the first concrete action toward the goal.`,
67
74
  },
68
75
  ]
@@ -70,6 +70,16 @@ export function stripTrailingWhitespace(str: string): string {
70
70
  * @param searchString The string to search for
71
71
  * @returns The actual string found in the file, or null if not found
72
72
  */
73
+
74
+ /** Normalizes Unicode dashes to ASCII, indent whitespace to spaces.
75
+ * Fills gaps where models emit ASCII dashes instead of Unicode dashes,
76
+ * or provide different tab/space indentation than the file has. */
77
+ export function normalizeDashes(str: string): string {
78
+ return str.replaceAll('\u2014', '-').replaceAll('\u2013', '-').replaceAll('\u2015', '-')
79
+ }
80
+ export function normalizeIndentation(str: string): string {
81
+ return str.split('\n').map(line => line.trimStart()).join('\n')
82
+ }
73
83
  export function findActualString(
74
84
  fileContent: string,
75
85
  searchString: string,
@@ -89,6 +99,14 @@ export function findActualString(
89
99
  return fileContent.substring(searchIndex, searchIndex + searchString.length)
90
100
  }
91
101
 
102
+ // Try with normalized dashes (em-dash, en-dash -> ASCII dash)
103
+ const dashedSearch = normalizeDashes(searchString)
104
+ const dashedFile = normalizeDashes(fileContent)
105
+ const dashIndex = dashedFile.indexOf(dashedSearch)
106
+ if (dashIndex !== -1) {
107
+ return fileContent.substring(dashIndex, dashIndex + searchString.length)
108
+ }
109
+
92
110
  return null
93
111
  }
94
112
 
@@ -9,21 +9,61 @@ export type GoalEvalResult = {
9
9
  gap: string | null
10
10
  }
11
11
 
12
+ // --- EVAL Block parser for structured evaluation ---
13
+
14
+ type EvalBlock = {
15
+ metric: string
16
+ valueTarget: string
17
+ passed: boolean
18
+ }
19
+
12
20
  /**
13
- * Evaluate whether the goal condition has been met based on recent messages.
21
+ * Parse markdown text for structured statement > EVAL: lines.
14
22
  *
15
- * Runs as an independent Anthropic client call — completely decoupled from the
16
- * main query chain. Never pollutes conversation state or tool history.
23
+ * Expected actor format:
24
+ * > EVAL: <metric>: <value> / <target> or
25
+ */
26
+ function parseEvalBlocks(text: string): EvalBlock[] {
27
+ const blocks: EvalBlock[] = []
28
+ const regex = />\s*EVAL:\s*(.+?):\s*(.+?)\s*→\s*(✓|✗)/g
29
+ let match
30
+ while ((match = regex.exec(text)) !== null) {
31
+ const [, metric, valueTarget, result] = match
32
+ const passed = result === '✓' || result === '✔'
33
+ blocks.push({ metric: metric.trim(), valueTarget: valueTarget.trim(), passed })
34
+ }
35
+ return blocks
36
+ }
37
+
38
+ /** Determine if all metrics pass — enabling early termination. */
39
+ function allMetricsPassing(blocks: EvalBlock[]): boolean {
40
+ return blocks.length > 0 && blocks.every(b => b.passed)
41
+ }
42
+
43
+ /** Extract structured EVAL summary from parsed blocks for consumption by evaluator model. */
44
+ function evalSummary(blocks: EvalBlock[]): string {
45
+ if (blocks.length === 0) return '(no EVAL blocks found)'
46
+ const passed = blocks.filter(b => b.passed).length
47
+ return [
48
+ `Pre-parsed EVAL metrics (${passed}/${blocks.length} passed):`,
49
+ ...blocks.map(b => `- ${b.metric}: ${b.valueTarget} → ${b.passed ? '✓' : '✗'}`),
50
+ ].join('\n')
51
+ }
52
+
53
+ // --- Core evaluator ---
54
+
55
+ /**
56
+ * Optimized goal evaluator.
57
+ *
58
+ * Strategy:
59
+ * 1. Regex-parse EVAL blocks from recent assistant text. If all metrics
60
+ * pass → short-circuit satisfied without calling evaluator model.
61
+ * 2. Feed pre-parsed EVAL summary to Haiku-4.5 for fallback evaluation.
17
62
  */
18
63
  export async function evaluateGoal(
19
64
  goalCondition: string,
20
65
  messages: MessageType[],
21
66
  ): Promise<GoalEvalResult> {
22
- const client = new Anthropic({
23
- baseURL: process.env.ANTHROPIC_BASE_URL ?? undefined,
24
- apiKey: process.env.ANTHROPIC_API_KEY ?? 'dummy',
25
- })
26
-
27
67
  const recentAssistantTexts = messages
28
68
  .filter(m => m.type === 'assistant' || m.role === 'assistant')
29
69
  .slice(-5)
@@ -40,15 +80,37 @@ export async function evaluateGoal(
40
80
  .filter(Boolean)
41
81
  .join('\n---\n')
42
82
 
43
- const prompt = `You are a goal completion evaluator. Determine if the goal has been fully achieved.
83
+ // Phase 1: regex-parse EVAL blocks from recent output (fast, no model call)
84
+ const evalBlocks = parseEvalBlocks(recentAssistantTexts)
85
+
86
+ // If ALL named metrics pass, the agent itself confirms goal completion.
87
+ if (evalBlocks.length > 0 && allMetricsPassing(evalBlocks)) {
88
+ return {
89
+ satisfied: true,
90
+ reason: `all ${evalBlocks.length} EVAL metrics satisfied`,
91
+ gap: null,
92
+ }
93
+ }
94
+
95
+ // Phase 2: Fallback to Haiku evaluator with pre-parsed summary
96
+ const evalInput = [
97
+ evalSummary(evalBlocks),
98
+ '(note: EVAL blocks already pre-parsed above — use to guide your evaluation)',
99
+ '',
100
+ recentAssistantTexts.slice(-4000), // trim long messages to fit context
101
+ ].join('\n')
102
+
103
+ const client = new Anthropic({
104
+ baseURL: process.env.ANTHROPIC_BASE_URL ?? undefined,
105
+ apiKey: process.env.ANTHROPIC_API_KEY ?? 'dummy',
106
+ })
44
107
 
45
- Goal: "${goalCondition}"
108
+ const prompt = `You are a goal completion evaluator. Determine if ${goalCondition} is fulfilled.
46
109
 
47
- Recent assistant output:
48
- ${recentAssistantTexts || '(none yet)'}
110
+ ${evalInput.slice(0, 6000)}
49
111
 
50
- Respond in JSON only:
51
- {"satisfied": true|false, "reason": "<one sentence>", "gap": "<missing item or null>"}`
112
+ Evaluate and respond ONLY in valid JSON:
113
+ {"satisfied": true|false, "reason": "<one sentence>", "gap": "<specific missing item, or null if satisfied>"}`
52
114
 
53
115
  let text = ''
54
116
  try {
@@ -59,6 +121,7 @@ Respond in JSON only:
59
121
  })
60
122
  text = response.content.find((b: any) => b.type === 'text')?.text || ''
61
123
  } catch (e) {
124
+ // Short-circuit on API error — parse what we can
62
125
  return {
63
126
  satisfied: false,
64
127
  reason: 'Evaluator API error',
@@ -66,8 +129,8 @@ Respond in JSON only:
66
129
  }
67
130
  }
68
131
 
132
+ // Phase 3: Parse evaluator output back to JSON
69
133
  try {
70
- // Strip markdown code fences and find JSON object bounds
71
134
  let cleaned = text
72
135
  .replace(/```(?:json)?\s*/gi, '')
73
136
  .replace(/```/g, '')