dual-brain 0.2.0 → 0.2.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.
@@ -902,6 +902,27 @@ async function cmdStatus(args = []) {
902
902
  console.log(' unknown (could not read .claude/settings.json)');
903
903
  }
904
904
 
905
+ // Replit section
906
+ try {
907
+ const replit = await import('../src/replit.mjs');
908
+ const env = replit.detectReplitEnvironment(cwd);
909
+ if (env.isReplit) {
910
+ console.log('\nReplit:');
911
+ const tools = replit.inspectReplitTools(cwd);
912
+ const verStr = tools.version ? `v${tools.version}` : 'unknown';
913
+ const capsCount = Array.isArray(tools.capabilities) ? tools.capabilities.length : 0;
914
+ console.log(` replit-tools : ${tools.installed ? `${verStr} (${capsCount} capabilities)` : 'not installed'}`);
915
+ const authStatus = replit.getAuthStatus(cwd);
916
+ console.log(` auth : ${authStatus.authenticated ? 'authenticated' : 'not authenticated'}${authStatus.method ? ` (${authStatus.method})` : ''}`);
917
+ const archive = replit.getSessionArchive(cwd);
918
+ const archiveCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
919
+ console.log(` session archive: ${archiveCount} session${archiveCount !== 1 ? 's' : ''}`);
920
+ const openaiPresent = replit.hasSecret('OPENAI_API_KEY');
921
+ const anthropicPresent = replit.hasSecret('ANTHROPIC_API_KEY');
922
+ console.log(` secrets : OPENAI_API_KEY=${openaiPresent ? 'set' : 'unset'} ANTHROPIC_API_KEY=${anthropicPresent ? 'set' : 'unset'}`);
923
+ }
924
+ } catch { /* replit.mjs not available or not in Replit — skip silently */ }
925
+
905
926
  // Update check
906
927
  try {
907
928
  const localVer = readVersion();
@@ -1957,10 +1978,30 @@ async function mainScreen(rl, ask) {
1957
1978
  }
1958
1979
  } catch { /* non-fatal */ }
1959
1980
 
1981
+ // Replit awareness rows (shown only when running in Replit, max 2-3 lines)
1982
+ const replitAwarenessRows = [];
1983
+ try {
1984
+ const replitMod = await import('../src/replit.mjs');
1985
+ const replitEnv = replitMod.detectReplitEnvironment(cwd);
1986
+ if (replitEnv.isReplit) {
1987
+ const rtInfo = replitMod.inspectReplitTools(cwd);
1988
+ const authInfo = replitMod.getAuthStatus(cwd);
1989
+ const archive = replitMod.getSessionArchive(cwd);
1990
+ const archCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
1991
+ const secretNames = replitMod.listSecretNames();
1992
+ const secretCount = Array.isArray(secretNames) ? secretNames.length : 0;
1993
+ const verStr = rtInfo.version ? `v${rtInfo.version}` : (rtInfo.installed ? 'installed' : 'not installed');
1994
+ const authStr = authInfo.authenticated ? '\x1b[32m✓\x1b[0m auth' : '\x1b[2mno auth\x1b[0m';
1995
+ replitAwarenessRows.push(row(`\x1b[2m🔧\x1b[0m Replit replit-tools ${verStr} ${authStr}`));
1996
+ replitAwarenessRows.push(row(`\x1b[2m \x1b[0m ${archCount} archived session${archCount !== 1 ? 's' : ''} ${secretCount} secret${secretCount !== 1 ? 's' : ''}`));
1997
+ }
1998
+ } catch { /* replit.mjs not available — skip */ }
1999
+
1960
2000
  const awarenessRows = [
1961
2001
  row(awarenessLine1),
1962
2002
  row(awarenessLine2),
1963
2003
  row(awarenessLine3),
2004
+ ...replitAwarenessRows,
1964
2005
  ];
1965
2006
 
1966
2007
  // ── Sessions section ──────────────────────────────────────────────────────
@@ -5001,6 +5042,21 @@ async function main() {
5001
5042
  }
5002
5043
 
5003
5044
  if (cmd === 'init') {
5045
+ // init --replit: run Replit-specific integration setup
5046
+ if (args.includes('--replit')) {
5047
+ const cwd = process.cwd();
5048
+ const dryRun = args.includes('--dry-run');
5049
+ try {
5050
+ const replit = await import('../src/replit.mjs');
5051
+ const report = await replit.initReplitIntegration({ dryRun, cwd });
5052
+ console.log(replit.formatReplitReport(report));
5053
+ } catch (e) {
5054
+ console.error('replit.mjs not available yet — skipping Replit init');
5055
+ if (process.env.DEBUG) console.error(e.message);
5056
+ }
5057
+ return;
5058
+ }
5059
+
5004
5060
  if (isInteractive) {
5005
5061
  // Run onboarding wizard then main screen
5006
5062
  const cwd = process.cwd();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,8 @@
22
22
  "./redact": "./src/redact.mjs",
23
23
  "./calibration": "./src/calibration.mjs",
24
24
  "./models": "./src/models.mjs",
25
- "./prompt-intel": "./src/prompt-intel.mjs"
25
+ "./prompt-intel": "./src/prompt-intel.mjs",
26
+ "./replit": "./src/replit.mjs"
26
27
  },
27
28
  "keywords": [
28
29
  "claude-code",
@@ -83,6 +84,7 @@
83
84
  "src/install-hooks.mjs",
84
85
  "src/update-check.mjs",
85
86
  "src/prompt-intel.mjs",
87
+ "src/replit.mjs",
86
88
  "src/fx.mjs",
87
89
  "bin/*.mjs",
88
90
  "hooks/enforce-tier.mjs",
package/src/awareness.mjs CHANGED
@@ -110,6 +110,23 @@ function scanReplitTools() {
110
110
  return { installed: true, version, sessionArchivePath, capabilities };
111
111
  }
112
112
 
113
+ /**
114
+ * Basic Replit environment scan used for the awareness report.
115
+ *
116
+ * NOTE: Detailed Replit integration (auth status, replit-tools capabilities,
117
+ * session archive, secrets listing, config planning, init flow) lives in
118
+ * src/replit.mjs. When that module is available, prefer its
119
+ * detectReplitEnvironment() over duplicating detection logic here.
120
+ *
121
+ * Usage (fail-safe):
122
+ * try {
123
+ * const { detectReplitEnvironment } = await import('./replit.mjs');
124
+ * const rich = detectReplitEnvironment(cwd);
125
+ * // rich has .isReplit, .replId, .version, .authStatus, .capabilities …
126
+ * } catch {
127
+ * // fall back to this scanReplit() result from scanEnvironment()
128
+ * }
129
+ */
113
130
  function scanReplit(cwd) {
114
131
  const env = process.env;
115
132
  const isReplit = Boolean(env.REPL_ID || env.REPL_SLUG);
package/src/decide.mjs CHANGED
@@ -682,10 +682,10 @@ function applyCriticalRiskFloor(model, provider, available, risk) {
682
682
 
683
683
  /**
684
684
  * Main routing decision function.
685
- * @param {{ profile: object, detection: object, cwd?: string, thinkResult?: object }} input
685
+ * @param {{ profile: object, detection: object, cwd?: string, thinkResult?: object, sessionContext?: object }} input
686
686
  * @returns {object} Routing decision
687
687
  */
688
- export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult } = {}) {
688
+ export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult, sessionContext = null } = {}) {
689
689
  const available = getAvailableModels(profile);
690
690
 
691
691
  // Resolve active work style
@@ -831,6 +831,50 @@ export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult } =
831
831
  // 'standard', 'deep', 'ultra' — leave model unchanged; existing routing already picked correctly
832
832
  }
833
833
 
834
+ // Session context: escalate or prefer model based on cross-session history
835
+ if (sessionContext) {
836
+ const sessionAttempts = Array.isArray(sessionContext.priorAttempts) ? sessionContext.priorAttempts : [];
837
+ const sessionFailures = sessionAttempts.filter(a => a && (a.failed || a.status === 'failed'));
838
+ const sessionSuccesses = sessionAttempts.filter(a => a && !a.failed && a.status !== 'failed');
839
+
840
+ // Prior failures on similar work → escalate from sonnet to opus (Claude) or gpt-4o to o3 (OpenAI)
841
+ if (sessionFailures.length >= 2 && !isHighStakes) {
842
+ if (provider === 'claude') {
843
+ const claudeRank = ['haiku', 'sonnet', 'opus'];
844
+ const currentIdx = claudeRank.indexOf(toShortName(model, 'claude'));
845
+ if (currentIdx !== -1 && currentIdx < claudeRank.length - 1) {
846
+ const escalated = claudeRank[currentIdx + 1];
847
+ if (available.claude.includes(escalated)) model = escalated;
848
+ }
849
+ } else {
850
+ const oaiRank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
851
+ const currentIdx = oaiRank.indexOf(model);
852
+ if (currentIdx !== -1 && currentIdx < oaiRank.length - 1) {
853
+ const escalated = oaiRank[currentIdx + 1];
854
+ if (available.openai.includes(escalated)) model = escalated;
855
+ }
856
+ }
857
+ }
858
+
859
+ // Prior successful approach → prefer same provider/model that worked before
860
+ if (sessionSuccesses.length > 0) {
861
+ const lastSuccess = sessionSuccesses[sessionSuccesses.length - 1];
862
+ if (lastSuccess.provider && lastSuccess.model && !isHighStakes) {
863
+ const successProvider = lastSuccess.provider;
864
+ const successModel = lastSuccess.model;
865
+ const providerEnabled = profile?.providers?.[successProvider]?.enabled;
866
+ const providerHealthy = (healthScores[successProvider] ?? 0) > 0;
867
+ if (providerEnabled && providerHealthy) {
868
+ const shortSuccess = toShortName(successModel, successProvider);
869
+ if (available[successProvider]?.includes(shortSuccess)) {
870
+ provider = successProvider;
871
+ model = shortSuccess;
872
+ }
873
+ }
874
+ }
875
+ }
876
+ }
877
+
834
878
  // Safety floor: critical-risk tasks must never use haiku/gpt-4.1-mini even in cost-saver mode
835
879
  model = applyCriticalRiskFloor(model, provider, available[provider], detection.risk);
836
880
 
package/src/detect.mjs CHANGED
@@ -280,9 +280,112 @@ function classifyReasoningDepth(prompt, files = [], priorOutcomes = []) {
280
280
  return { depth: 'low', signals: lowSignals.length > 0 ? lowSignals : ['no elevated signals detected'] };
281
281
  }
282
282
 
283
- /** Main detection function. Input: { prompt, files?, priorFailures? } */
283
+ // ─── Plugin-aware detection helpers ───────────────────────────────────────────
284
+
285
+ /**
286
+ * Known plugin service keywords → plugin IDs.
287
+ * Maps common service names and their aliases to Codex plugin directory names.
288
+ * Static map so detect.mjs stays self-contained (no I/O at classify time).
289
+ */
290
+ const PLUGIN_KEYWORD_MAP = {
291
+ // Payments
292
+ stripe: 'stripe',
293
+ payment: 'stripe',
294
+ checkout: 'stripe',
295
+ subscription: 'stripe',
296
+ webhook: 'stripe',
297
+ // Collaboration / messaging
298
+ slack: 'slack',
299
+ teams: 'teams',
300
+ // Data / backend
301
+ supabase: 'supabase',
302
+ neondb: 'neon-postgres',
303
+ // Dev tools
304
+ github: 'github',
305
+ 'pull request': 'github',
306
+ linear: 'linear',
307
+ jira: 'atlassian-rovo',
308
+ atlassian: 'atlassian-rovo',
309
+ // Comms / productivity
310
+ gmail: 'gmail',
311
+ outlook: 'outlook-email',
312
+ notion: 'notion',
313
+ 'google calendar': 'google-calendar',
314
+ 'google drive': 'google-drive',
315
+ // Monitoring / infra
316
+ sentry: 'sentry',
317
+ vercel: 'vercel',
318
+ netlify: 'netlify',
319
+ cloudflare: 'cloudflare',
320
+ // Analytics
321
+ amplitude: 'amplitude',
322
+ // Design
323
+ figma: 'figma',
324
+ canva: 'canva',
325
+ // CRM / sales
326
+ hubspot: 'hubspot',
327
+ pipedrive: 'pipedrive',
328
+ // Communication
329
+ sendgrid: 'sendgrid',
330
+ twilio: 'twilio-developer-kit',
331
+ // Storage
332
+ sharepoint: 'sharepoint',
333
+ box: 'box',
334
+ // AI / ML
335
+ openai: 'openai-developers',
336
+ 'hugging face': 'hugging-face',
337
+ // Other
338
+ razorpay: 'razorpay',
339
+ render: 'render',
340
+ monday: 'monday-com',
341
+ asana: 'asana',
342
+ clickup: 'clickup',
343
+ };
344
+
345
+ /**
346
+ * Detect Codex plugin IDs that match keywords in the prompt.
347
+ * Returns an array of matched plugin IDs (deduplicated, max 5).
348
+ * @param {string} prompt
349
+ * @returns {string[]}
350
+ */
351
+ function detectSuggestedPlugins(prompt) {
352
+ if (!prompt) return [];
353
+ const lower = prompt.toLowerCase();
354
+ const matched = new Set();
355
+
356
+ // Check multi-word phrases first (longer matches take priority)
357
+ const sortedEntries = Object.entries(PLUGIN_KEYWORD_MAP).sort((a, b) => b[0].length - a[0].length);
358
+ for (const [keyword, pluginId] of sortedEntries) {
359
+ if (lower.includes(keyword)) {
360
+ matched.add(pluginId);
361
+ if (matched.size >= 5) break;
362
+ }
363
+ }
364
+
365
+ return [...matched];
366
+ }
367
+
368
+ /** Main detection function. Input: { prompt, files?, priorFailures?, sessionContext? } */
284
369
  function detectTask(input) {
285
- const { prompt = '', files = [], priorFailures = 0 } = input;
370
+ const { prompt = '', files = [], sessionContext = null } = input;
371
+ let { priorFailures = 0 } = input;
372
+
373
+ // Session context: bump priorFailures if session history shows failures on similar tasks
374
+ let repeatedFailure = false;
375
+ if (sessionContext) {
376
+ const sessionFailures = Array.isArray(sessionContext.priorAttempts)
377
+ ? sessionContext.priorAttempts.filter(a => a && (a.failed || a.status === 'failed')).length
378
+ : 0;
379
+ if (sessionFailures > 0) {
380
+ priorFailures = Math.max(priorFailures, sessionFailures);
381
+ }
382
+ // Flag repeated_failure if riskSignals contains failure indicators
383
+ const riskSignals = sessionContext.riskSignals ?? [];
384
+ if (riskSignals.some(s => s && (s.type === 'failure' || s.failed || /fail/i.test(String(s))))) {
385
+ repeatedFailure = true;
386
+ }
387
+ if (sessionFailures >= 2) repeatedFailure = true;
388
+ }
286
389
 
287
390
  // 1. Intent
288
391
  const intent = classifyIntent(prompt);
@@ -299,7 +402,14 @@ function detectTask(input) {
299
402
  if (regex.test(prompt)) { keywordRisk = level; break; }
300
403
  }
301
404
 
302
- const risk = higherRisk(pathRiskLevel, keywordRisk);
405
+ let risk = higherRisk(pathRiskLevel, keywordRisk);
406
+
407
+ // Session context: bump risk one level if prior session attempts failed on similar tasks
408
+ if (repeatedFailure && LEVEL_ORDER[risk] < LEVEL_ORDER['high']) {
409
+ const riskLevels = ['low', 'medium', 'high', 'critical'];
410
+ const currentIdx = riskLevels.indexOf(risk);
411
+ risk = riskLevels[Math.min(currentIdx + 1, riskLevels.length - 1)];
412
+ }
303
413
  const fileCount = files.length;
304
414
 
305
415
  // 4. Complexity
@@ -342,6 +452,9 @@ function detectTask(input) {
342
452
  : [];
343
453
  const { depth: reasoningDepth, signals: reasoningSignals } = classifyReasoningDepth(prompt, files, priorOutcomes);
344
454
 
455
+ // 10. Suggested Codex plugins (keyword-based, static map — no I/O)
456
+ const suggestedPlugins = detectSuggestedPlugins(prompt);
457
+
345
458
  return {
346
459
  intent,
347
460
  risk,
@@ -356,6 +469,8 @@ function detectTask(input) {
356
469
  specialist: specialistResult,
357
470
  reasoningDepth,
358
471
  reasoningSignals,
472
+ suggestedPlugins,
473
+ ...(repeatedFailure && { repeatedFailure: true }),
359
474
  };
360
475
  }
361
476
 
@@ -474,4 +589,4 @@ if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
474
589
  console.log(JSON.stringify(result, null, 2));
475
590
  }
476
591
 
477
- export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths, classifySpecialist, classifyReasoningDepth };
592
+ export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths, classifySpecialist, classifyReasoningDepth, detectSuggestedPlugins };
package/src/dispatch.mjs CHANGED
@@ -737,6 +737,25 @@ async function dispatch(input = {}) {
737
737
  }
738
738
  // ── End specialist injection ─────────────────────────────────────────────────
739
739
 
740
+ // ── Plugin hint injection (Codex path) ──────────────────────────────────────
741
+ // When dispatching to OpenAI/Codex, check if any Codex plugins match the task
742
+ // and append an advisory hint so the agent can choose to use them.
743
+ // Uses dynamic import so failure is always non-fatal.
744
+ const targetProvider = decision.provider ?? 'claude';
745
+ if (targetProvider === 'openai') {
746
+ try {
747
+ const { matchPluginsForTask } = await import('./replit.mjs');
748
+ const matched = matchPluginsForTask(prompt, undefined, cwd);
749
+ if (matched.length > 0) {
750
+ const pluginNames = matched.slice(0, 3).map(m => m.plugin.id).join(', ');
751
+ const hint = `\n\n[Available Codex plugins for this task: ${pluginNames}. Consider using the matching plugin for direct API access.]`;
752
+ prompt = prompt + hint;
753
+ if (verbose) process.stderr.write(`[dual-brain] plugin hint injected: ${pluginNames}\n`);
754
+ }
755
+ } catch { /* non-fatal — never block dispatch */ }
756
+ }
757
+ // ── End plugin hint injection ────────────────────────────────────────────────
758
+
740
759
  const tier = decision.tier ?? 'execute';
741
760
  const timeoutMs = TIER_TIMEOUT_MS[tier] ?? 120_000;
742
761