bingocode 1.1.155 → 1.1.157

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.155",
3
+ "version": "1.1.157",
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]) => ({
@@ -71,11 +71,14 @@ export function stripTrailingWhitespace(str: string): string {
71
71
  * @returns The actual string found in the file, or null if not found
72
72
  */
73
73
 
74
- /** Normalizes Unicode dashes (em-dash, en-dash, horizontal bar) to standard ASCII dashes.
75
- * Handles model-output ASCII dashes when file content contains Unicode dash variants.
76
- * Fixes Edit tool matching failures from encoding discrepancies. */
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
77
  export function normalizeDashes(str: string): string {
78
- return str.replaceAll('', '-').replaceAll('', '-').replaceAll('', '-')
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')
79
82
  }
80
83
  export function findActualString(
81
84
  fileContent: string,
@@ -9,21 +9,77 @@ 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 > 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
+ * Accepted actor formats:
24
+ * > EVAL: <metric>: <value> / <target> or
25
+ * > EVAL: <metric>: <value> / <target> -> PASS
26
+ * > EVAL: <metric>: <value> / <target> => true
27
+ *
28
+ * Supports ASCII and Unicode arrow/check/cross variants for maximum compatibility.
29
+ */
30
+ function parseEvalBlocks(text: string): EvalBlock[] {
31
+ const blocks: EvalBlock[] = []
32
+
33
+ // Build one combined pattern: capture metric + valuetarget + pass/fail signal.
34
+ // Arrow variants: → (U+2192), -> (ASCII), => (ASCII)
35
+ // Pass variants: ✓ (U+2713), ✔ (U+2714), PASS (case-insensitive), Y, true, yes, 1
36
+ // Fail variants: ✗ (U+2717), ✘ (U+2718), FAIL (case-insensitive), N, false, no, 0
37
+ const arrow = /(?:→|->|=>)/g.source
38
+ const pass = /(?:✓|✔|PASS|pass|Y\b|true|yes|1)/g.source
39
+ const fail = /(?:✗|✘|FAIL|fail|N\b|false|no|0)/g.source
40
+ const full = new RegExp(
41
+ `>\\s*EVAL:\\s*(.+?):\\s*(.+?)\\s*(?:${arrow}|)\\s*(${pass}|${fail})`,
42
+ 'g',
43
+ )
44
+
45
+ let match: RegExpExecArray | null
46
+ while ((match = full.exec(text)) !== null) {
47
+ const [, metric, valueTarget, signal] = match
48
+ const passed = /^(✓|✔|PASS|pass|Y\b|true|yes|1)$/.test(signal.trim())
49
+ blocks.push({ metric: metric.trim(), valueTarget: valueTarget.trim(), passed })
50
+ }
51
+ return blocks
52
+ }
53
+
54
+ /** Determine if all metrics pass — enabling early termination. */
55
+ function allMetricsPassing(blocks: EvalBlock[]): boolean {
56
+ return blocks.length > 0 && blocks.every(b => b.passed)
57
+ }
58
+
59
+ /** Extract structured EVAL summary from parsed blocks for consumption by evaluator model. */
60
+ function evalSummary(blocks: EvalBlock[]): string {
61
+ if (blocks.length === 0) return '(no EVAL blocks found)'
62
+ const passed = blocks.filter(b => b.passed).length
63
+ return [
64
+ `Pre-parsed EVAL metrics (${passed}/${blocks.length} passed):`,
65
+ ...blocks.map(b => `- ${b.metric}: ${b.valueTarget} → ${b.passed ? '✓' : '✗'}`),
66
+ ].join('\n')
67
+ }
68
+
69
+ // --- Core evaluator ---
70
+
71
+ /**
72
+ * Optimized goal evaluator.
73
+ *
74
+ * Strategy:
75
+ * 1. Regex-parse EVAL blocks from recent assistant text. If all metrics
76
+ * pass → short-circuit satisfied without calling evaluator model.
77
+ * 2. Feed pre-parsed EVAL summary to Haiku-4.5 for fallback evaluation.
17
78
  */
18
79
  export async function evaluateGoal(
19
80
  goalCondition: string,
20
81
  messages: MessageType[],
21
82
  ): 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
83
  const recentAssistantTexts = messages
28
84
  .filter(m => m.type === 'assistant' || m.role === 'assistant')
29
85
  .slice(-5)
@@ -40,22 +96,39 @@ export async function evaluateGoal(
40
96
  .filter(Boolean)
41
97
  .join('\n---\n')
42
98
 
43
- const prompt = `You are a goal completion evaluator. Determine if the goal has been fully achieved.
99
+ // Phase 1: regex-parse EVAL blocks from recent output (fast, no model call)
100
+ const evalBlocks = parseEvalBlocks(recentAssistantTexts)
44
101
 
45
- IMPORTANT: The agent may produce EVAL blocks intended for you. Parse them first.
102
+ // If ALL named metrics pass, the agent itself confirms goal completion.
103
+ if (evalBlocks.length > 0 && allMetricsPassing(evalBlocks)) {
104
+ return {
105
+ satisfied: true,
106
+ reason: `all ${evalBlocks.length} EVAL metrics satisfied`,
107
+ gap: null,
108
+ }
109
+ }
46
110
 
47
- Goal: "${goalCondition}"
111
+ // Phase 2: Fallback to Haiku evaluator with pre-parsed summary
112
+ const evalInput = [
113
+ evalSummary(evalBlocks),
114
+ '(note: EVAL blocks already pre-parsed above — use to guide your evaluation)',
115
+ '',
116
+ recentAssistantTexts.slice(-4000), // trim long messages to fit context
117
+ ].join('\n')
48
118
 
49
- Recent assistant output:
50
- ${recentAssistantTexts || '(none yet)'}
119
+ const client = new Anthropic({
120
+ baseURL: process.env.ANTHROPIC_BASE_URL ?? undefined,
121
+ apiKey: process.env.ANTHROPIC_API_KEY ?? 'dummy',
122
+ })
51
123
 
52
- Evaluate:
53
- 1. Did the agent produce a final EVAL block? If so, use those values directly.
54
- 2. If no EVAL blocks found, infer based on any explicit declarations (e.g. "✓", "100%", "fixed", "complete").
55
- 3. Output ONLY valid JSON — no explanation or markdown.
124
+ const prompt = `Goal condition to evaluate: "${goalCondition}"
56
125
 
57
- Respond in:
58
- {"satisfied": true|false, "reason": "<one sentence>", "gap": "<missing item or null>"}`
126
+ The assistant's recent output is below. Based ONLY on it, determine if the goal is satisfied.
127
+
128
+ RESPOND WITH ONLY VALID JSON — no markdown, no explanation:
129
+ {"satisfied": true|false, "reason": "<one sentence why>", "gap": "<what's still missing, or null if satisfied>"}
130
+
131
+ ${evalInput.slice(0, 5000)}`
59
132
 
60
133
  let text = ''
61
134
  try {
@@ -66,6 +139,7 @@ Respond in:
66
139
  })
67
140
  text = response.content.find((b: any) => b.type === 'text')?.text || ''
68
141
  } catch (e) {
142
+ // Short-circuit on API error — parse what we can
69
143
  return {
70
144
  satisfied: false,
71
145
  reason: 'Evaluator API error',
@@ -73,24 +147,62 @@ Respond in:
73
147
  }
74
148
  }
75
149
 
76
- try {
77
- // Strip markdown code fences and find JSON object bounds
78
- let cleaned = text
79
- .replace(/```(?:json)?\s*/gi, '')
80
- .replace(/```/g, '')
81
- .trim()
82
- const start = cleaned.indexOf('{')
83
- const end = cleaned.lastIndexOf('}')
84
- if (start === -1 || end === -1 || end <= start) {
85
- throw new Error('No JSON object found')
86
- }
87
- cleaned = cleaned.slice(start, end + 1)
88
- return JSON.parse(cleaned) as GoalEvalResult
89
- } catch (e) {
90
- return {
91
- satisfied: false,
92
- reason: 'Evaluator parse error',
93
- gap: `${e instanceof Error && e.message !== 'No JSON object found' ? e.message : 'raw output'}: ${text.slice(0, 200)}`,
150
+ // Phase 3: Parse evaluator output back to JSON.
151
+ // Try strict JSON first, then fuzzy extraction, then interpret heuristics.
152
+ const parseError = (detail: string): GoalEvalResult => ({
153
+ satisfied: false,
154
+ reason: 'Evaluator parse error',
155
+ gap: `Failed to parse evaluator output. Detail: ${detail}. First 120 chars of raw response: ${text.slice(0, 120)}`,
156
+ })
157
+
158
+ const tryJsonParse = (raw: string): { ok: true; value: GoalEvalResult } | { ok: false } => {
159
+ try {
160
+ let cleaned = raw
161
+ .replace(/```(?:json)?\s*/gi, '')
162
+ .replace(/```/g, '')
163
+ .trim()
164
+ const start = cleaned.indexOf('{')
165
+ const end = cleaned.lastIndexOf('}')
166
+ if (start === -1 || end === -1 || end <= start) return { ok: false }
167
+ cleaned = cleaned.slice(start, end + 1)
168
+ const parsed = JSON.parse(cleaned)
169
+ if (typeof parsed.satisfied === 'boolean') {
170
+ return {
171
+ ok: true,
172
+ value: {
173
+ satisfied: parsed.satisfied,
174
+ reason: parsed.reason || '',
175
+ gap: parsed.gap || null,
176
+ },
177
+ }
178
+ }
179
+ return { ok: false }
180
+ } catch {
181
+ return { ok: false }
94
182
  }
95
183
  }
184
+
185
+ // Attempt 1 — strict JSON parse of the raw text
186
+ const result = tryJsonParse(text)
187
+ if (result.ok) return result.value
188
+
189
+ // Attempt 2 — heuristic extraction from text response
190
+ const lower = text.toLowerCase()
191
+ const looksSatisfied =
192
+ lower.includes('"satisfied": true') ||
193
+ (lower.includes('satisfied') && lower.includes('true')) ||
194
+ /goal\s+is\s+(?:met|satisfied|achieved)/.test(lower) ||
195
+ /condition\s+is\s+(?:met|satisfied|fulfilled)/.test(lower)
196
+
197
+ const extractString = (field: string): string => {
198
+ const regex = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, 'i')
199
+ const match = text.match(regex)
200
+ return match ? match[1] : 'unknown'
201
+ }
202
+
203
+ return {
204
+ satisfied: looksSatisfied,
205
+ reason: extractString('reason') || (looksSatisfied ? 'condition matched' : 'condition not met'),
206
+ gap: extractString('gap') || null,
207
+ }
96
208
  }