dual-brain 7.1.6 → 7.1.7
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/bin/dual-brain.mjs +641 -299
- package/install.mjs +30 -0
- package/package.json +3 -2
- package/shell-hook.sh +26 -0
- package/src/index.mjs +2 -2
- package/src/profile.mjs +42 -244
- package/src/session.mjs +84 -0
package/install.mjs
CHANGED
|
@@ -1463,6 +1463,36 @@ async function main() {
|
|
|
1463
1463
|
}
|
|
1464
1464
|
|
|
1465
1465
|
const actions = install(env.workspace, env, mode);
|
|
1466
|
+
|
|
1467
|
+
// Write a standalone shell-hook.sh so users can source it from .bashrc.
|
|
1468
|
+
// Non-interactive installs (npm postinstall) just print the hint; interactive
|
|
1469
|
+
// installs also write the file so it's ready to source.
|
|
1470
|
+
const shellHookSrc = join(__dirname, 'shell-hook.sh');
|
|
1471
|
+
const shellHookDst = join(env.workspace, '.dualbrain', 'shell-hook.sh');
|
|
1472
|
+
try {
|
|
1473
|
+
mkdirSync(join(env.workspace, '.dualbrain'), { recursive: true });
|
|
1474
|
+
if (existsSync(shellHookSrc)) {
|
|
1475
|
+
cpSync(shellHookSrc, shellHookDst);
|
|
1476
|
+
actions.push('✓ .dualbrain/shell-hook.sh (source from .bashrc to auto-launch)');
|
|
1477
|
+
}
|
|
1478
|
+
} catch { /* non-fatal — shell hook is optional */ }
|
|
1479
|
+
|
|
1480
|
+
// On Replit, print a one-liner hint for the shell hook if .bashrc doesn't have it yet.
|
|
1481
|
+
if (env.isReplit) {
|
|
1482
|
+
let bashrcHasDualBrain = false;
|
|
1483
|
+
const bashrcPath = join(process.env.HOME || '', '.bashrc');
|
|
1484
|
+
try {
|
|
1485
|
+
bashrcHasDualBrain = readFileSync(bashrcPath, 'utf8').includes('dual-brain');
|
|
1486
|
+
} catch { /* .bashrc may not exist */ }
|
|
1487
|
+
|
|
1488
|
+
if (!bashrcHasDualBrain) {
|
|
1489
|
+
actions.push('');
|
|
1490
|
+
actions.push('Shell hook (optional — shows dual-brain on new terminal):');
|
|
1491
|
+
actions.push(' dual-brain shell-hook >> ~/.bashrc');
|
|
1492
|
+
actions.push(' # or: source .dualbrain/shell-hook.sh');
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1466
1496
|
printReport(env, mode, actions);
|
|
1467
1497
|
|
|
1468
1498
|
// After install, launch the session manager (interactive TTY only)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.7",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"playbooks/*.json",
|
|
104
104
|
"plugin.json",
|
|
105
105
|
"skills/*.md",
|
|
106
|
-
"agents/*.md"
|
|
106
|
+
"agents/*.md",
|
|
107
|
+
"shell-hook.sh"
|
|
107
108
|
]
|
|
108
109
|
}
|
package/shell-hook.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dual-brain shell integration
|
|
3
|
+
# Add to .bashrc with one command:
|
|
4
|
+
# dual-brain shell-hook >> ~/.bashrc
|
|
5
|
+
# Or source directly:
|
|
6
|
+
# source /path/to/shell-hook.sh
|
|
7
|
+
|
|
8
|
+
# Quick alias
|
|
9
|
+
alias db='dual-brain'
|
|
10
|
+
|
|
11
|
+
# Show session manager on new interactive terminal.
|
|
12
|
+
# Skipped when:
|
|
13
|
+
# - not a TTY (non-interactive shell, CI, pipes)
|
|
14
|
+
# - DUAL_BRAIN_LOADED already set (prevents double-launch in nested shells)
|
|
15
|
+
# - DUAL_BRAIN_SKIP=1 (user opt-out)
|
|
16
|
+
# - DATA_TOOLS_LOADED or CLAUDE_MENU_LOADED is set (data-tools is managing the shell)
|
|
17
|
+
if [ -t 1 ] \
|
|
18
|
+
&& [ -z "$DUAL_BRAIN_LOADED" ] \
|
|
19
|
+
&& [ -z "$DUAL_BRAIN_SKIP" ] \
|
|
20
|
+
&& [ -z "$DATA_TOOLS_LOADED" ] \
|
|
21
|
+
&& [ -z "$CLAUDE_MENU_LOADED" ]; then
|
|
22
|
+
export DUAL_BRAIN_LOADED=1
|
|
23
|
+
if command -v dual-brain &>/dev/null; then
|
|
24
|
+
dual-brain
|
|
25
|
+
fi
|
|
26
|
+
fi
|
package/src/index.mjs
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* orchestrate() convenience function for programmatic use.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment,
|
|
9
|
+
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, saveSubscription, listSubscriptions } from './profile.mjs';
|
|
10
10
|
export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths } from './detect.mjs';
|
|
11
11
|
export { decideRoute, getModelCapabilities, getAvailableModels, shouldDualBrain, explainDecision } from './decide.mjs';
|
|
12
12
|
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain } from './dispatch.mjs';
|
|
13
13
|
export { loadPlaybook, listPlaybooks, executePlaybook, createRunArtifact } from './playbook.mjs';
|
|
14
14
|
export { getHealth, markHot, markDegraded, markHealthy, checkCooldown, getProviderScore, recordDispatch, getSessionStats, resetHealth, remainingCooldownMinutes } from './health.mjs';
|
|
15
15
|
export { detectRepo, loadRepoCache, getTestCommand, getLintCommand } from './repo.mjs';
|
|
16
|
-
export { loadSession, saveSession, updateSession, clearSession, formatSessionCard } from './session.mjs';
|
|
16
|
+
export { loadSession, saveSession, updateSession, clearSession, formatSessionCard, importReplitSessions, renameSession, pinSession, unpinSession, categorizeSession, getSessionMeta, autoLabel, enrichSessions } from './session.mjs';
|
|
17
17
|
export { decompose, isSimpleTask, taskGraphToWaves } from './decompose.mjs';
|
|
18
18
|
export { generateBrief, compressPriorResults, listRoles } from './brief.mjs';
|
|
19
19
|
export { redact, redactFiles, isSecretFile } from './redact.mjs';
|
package/src/profile.mjs
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { createInterface } from 'readline';
|
|
26
|
-
import {
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
27
27
|
import { homedir } from 'os';
|
|
28
|
-
import {
|
|
28
|
+
import { join } from 'path';
|
|
29
29
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Claude Code memory integration
|
|
@@ -126,31 +126,19 @@ function detectEnvironment() {
|
|
|
126
126
|
// ---------------------------------------------------------------------------
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* @param {string} str
|
|
132
|
-
* @returns {string}
|
|
133
|
-
*/
|
|
134
|
-
function _maskCredential(str) {
|
|
135
|
-
if (!str || str.length < 8) return '***';
|
|
136
|
-
return str.slice(0, 4) + '...' + str.slice(-4);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Detect authentication credentials from all known sources.
|
|
141
|
-
* Checks in priority order: config files first, then env vars.
|
|
142
|
-
* Never makes network calls — validation is always null in v1.
|
|
129
|
+
* Detect CLI login status for Claude and Codex.
|
|
130
|
+
* Checks config files on disk — never makes network calls.
|
|
143
131
|
*
|
|
144
132
|
* @returns {{ claude: AuthEntry, openai: AuthEntry }}
|
|
145
|
-
* @typedef {{ found: boolean, source: string|null,
|
|
133
|
+
* @typedef {{ found: boolean, source: string|null, loginType: 'oauth'|'cli'|null }} AuthEntry
|
|
146
134
|
*/
|
|
147
135
|
async function detectAuth() {
|
|
148
136
|
const results = {
|
|
149
|
-
claude: { found: false, source: null,
|
|
150
|
-
openai: { found: false, source: null,
|
|
137
|
+
claude: { found: false, source: null, loginType: null },
|
|
138
|
+
openai: { found: false, source: null, loginType: null },
|
|
151
139
|
};
|
|
152
140
|
|
|
153
|
-
// --- Claude: check .claude.json for oauthAccount
|
|
141
|
+
// --- Claude: check .claude.json for oauthAccount (CLI login) ---
|
|
154
142
|
const claudePaths = [
|
|
155
143
|
'/home/runner/workspace/.replit-tools/.claude-persistent/.claude.json',
|
|
156
144
|
join(homedir(), '.claude', '.claude.json'),
|
|
@@ -159,51 +147,22 @@ async function detectAuth() {
|
|
|
159
147
|
try {
|
|
160
148
|
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
161
149
|
if (data?.oauthAccount) {
|
|
162
|
-
|
|
163
|
-
results.claude.
|
|
164
|
-
results.claude.
|
|
165
|
-
results.claude.masked = 'oauth:configured';
|
|
150
|
+
results.claude.found = true;
|
|
151
|
+
results.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
152
|
+
results.claude.loginType = 'oauth';
|
|
166
153
|
break;
|
|
167
154
|
}
|
|
155
|
+
// Legacy: apiKey field in .claude.json (set by claude CLI in some versions)
|
|
168
156
|
if (data?.apiKey && typeof data.apiKey === 'string') {
|
|
169
|
-
results.claude.found
|
|
170
|
-
results.claude.source
|
|
171
|
-
results.claude.
|
|
157
|
+
results.claude.found = true;
|
|
158
|
+
results.claude.source = p.includes('.replit-tools') ? 'claude CLI (replit-tools)' : 'claude CLI';
|
|
159
|
+
results.claude.loginType = 'cli';
|
|
172
160
|
break;
|
|
173
161
|
}
|
|
174
162
|
} catch { continue; }
|
|
175
163
|
}
|
|
176
164
|
|
|
177
|
-
// ---
|
|
178
|
-
if (!results.claude.found) {
|
|
179
|
-
const storedAuth = loadAuthKeys();
|
|
180
|
-
const claudeKeys = storedAuth.claude || [];
|
|
181
|
-
const activeClaudeKey = getActiveKey('claude');
|
|
182
|
-
if (activeClaudeKey) {
|
|
183
|
-
results.claude.found = true;
|
|
184
|
-
results.claude.source = '.dualbrain/auth.json';
|
|
185
|
-
results.claude.masked = _maskCredential(activeClaudeKey.key);
|
|
186
|
-
process.env.ANTHROPIC_API_KEY = activeClaudeKey.key;
|
|
187
|
-
// Report all keys with masked values
|
|
188
|
-
const now = new Date();
|
|
189
|
-
results.claude.keys = claudeKeys.map(k => ({
|
|
190
|
-
label: k.label || 'unlabeled',
|
|
191
|
-
masked: _maskCredential(k.key),
|
|
192
|
-
priority: k.priority || 99,
|
|
193
|
-
enabled: k.enabled !== false,
|
|
194
|
-
expired: !!(k.expiresAt && new Date(k.expiresAt) <= now),
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// --- Claude: fallback to ANTHROPIC_API_KEY env var ---
|
|
200
|
-
if (!results.claude.found && process.env.ANTHROPIC_API_KEY) {
|
|
201
|
-
results.claude.found = true;
|
|
202
|
-
results.claude.source = 'env:ANTHROPIC_API_KEY';
|
|
203
|
-
results.claude.masked = _maskCredential(process.env.ANTHROPIC_API_KEY);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// --- OpenAI/Codex: check auth.json for access_token or id_token ---
|
|
165
|
+
// --- OpenAI/Codex: check auth.json for access_token or id_token (CLI login) ---
|
|
207
166
|
const codexPaths = [
|
|
208
167
|
'/home/runner/workspace/.replit-tools/.codex-persistent/auth.json',
|
|
209
168
|
join(homedir(), '.codex', 'auth.json'),
|
|
@@ -213,208 +172,48 @@ async function detectAuth() {
|
|
|
213
172
|
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
214
173
|
const accessToken = data?.tokens?.access_token || data?.access_token;
|
|
215
174
|
const idToken = data?.tokens?.id_token || data?.id_token;
|
|
216
|
-
const apiKey = data?.apiKey ?? data?.api_key ?? null;
|
|
217
175
|
|
|
218
176
|
if (accessToken || idToken) {
|
|
219
|
-
results.openai.found
|
|
220
|
-
results.openai.source
|
|
221
|
-
results.openai.
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
if (apiKey && typeof apiKey === 'string') {
|
|
225
|
-
results.openai.found = true;
|
|
226
|
-
results.openai.source = p.includes('.replit-tools') ? 'codex auth.json (replit-tools)' : 'codex auth.json';
|
|
227
|
-
results.openai.masked = _maskCredential(apiKey);
|
|
177
|
+
results.openai.found = true;
|
|
178
|
+
results.openai.source = p.includes('.replit-tools') ? 'codex CLI (replit-tools)' : 'codex CLI';
|
|
179
|
+
results.openai.loginType = 'oauth';
|
|
228
180
|
break;
|
|
229
181
|
}
|
|
230
182
|
} catch { continue; }
|
|
231
183
|
}
|
|
232
184
|
|
|
233
|
-
// --- OpenAI: check .dualbrain/auth.json (before env var) ---
|
|
234
|
-
if (!results.openai.found) {
|
|
235
|
-
const storedAuth = loadAuthKeys();
|
|
236
|
-
const openaiKeys = storedAuth.openai || [];
|
|
237
|
-
const activeOpenaiKey = getActiveKey('openai');
|
|
238
|
-
if (activeOpenaiKey) {
|
|
239
|
-
results.openai.found = true;
|
|
240
|
-
results.openai.source = '.dualbrain/auth.json';
|
|
241
|
-
results.openai.masked = _maskCredential(activeOpenaiKey.key);
|
|
242
|
-
process.env.OPENAI_API_KEY = activeOpenaiKey.key;
|
|
243
|
-
// Report all keys with masked values
|
|
244
|
-
const now = new Date();
|
|
245
|
-
results.openai.keys = openaiKeys.map(k => ({
|
|
246
|
-
label: k.label || 'unlabeled',
|
|
247
|
-
masked: _maskCredential(k.key),
|
|
248
|
-
priority: k.priority || 99,
|
|
249
|
-
enabled: k.enabled !== false,
|
|
250
|
-
expired: !!(k.expiresAt && new Date(k.expiresAt) <= now),
|
|
251
|
-
}));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// --- OpenAI: fallback to OPENAI_API_KEY env var ---
|
|
256
|
-
if (!results.openai.found && process.env.OPENAI_API_KEY) {
|
|
257
|
-
results.openai.found = true;
|
|
258
|
-
results.openai.source = 'env:OPENAI_API_KEY';
|
|
259
|
-
results.openai.masked = _maskCredential(process.env.OPENAI_API_KEY);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
185
|
return results;
|
|
263
186
|
}
|
|
264
187
|
|
|
265
188
|
// ---------------------------------------------------------------------------
|
|
266
|
-
//
|
|
189
|
+
// Subscription management (.dualbrain/profile.json)
|
|
267
190
|
// ---------------------------------------------------------------------------
|
|
268
191
|
|
|
269
|
-
const AUTH_FILE = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'auth.json');
|
|
270
|
-
|
|
271
192
|
/**
|
|
272
|
-
*
|
|
193
|
+
* Save subscription config for a provider into .dualbrain/profile.json.
|
|
194
|
+
* @param {string} provider — 'claude' or 'openai'
|
|
195
|
+
* @param {{ plan: string, label?: string, expiresAt?: string }} config
|
|
273
196
|
* @param {string} [cwd]
|
|
274
|
-
* @returns {object} auth object with arrays per provider
|
|
275
197
|
*/
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
198
|
+
function saveSubscription(provider, config, cwd) {
|
|
199
|
+
const profile = loadProfile(cwd);
|
|
200
|
+
if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
|
|
201
|
+
profile.providers[provider].plan = config.plan;
|
|
202
|
+
profile.providers[provider].enabled = true;
|
|
203
|
+
if (config.label) profile.providers[provider].label = config.label;
|
|
204
|
+
if (config.expiresAt) profile.providers[provider].expiresAt = config.expiresAt;
|
|
205
|
+
saveProfile(profile, { cwd: cwd || process.cwd() });
|
|
206
|
+
return profile;
|
|
282
207
|
}
|
|
283
208
|
|
|
284
209
|
/**
|
|
285
|
-
*
|
|
286
|
-
* @param {string} provider
|
|
210
|
+
* Return subscription configs for all providers from the saved profile.
|
|
287
211
|
* @param {string} [cwd]
|
|
288
|
-
* @returns {{
|
|
289
|
-
*/
|
|
290
|
-
function getActiveKey(provider, cwd) {
|
|
291
|
-
const auth = loadAuthKeys(cwd);
|
|
292
|
-
const keys = auth[provider] || [];
|
|
293
|
-
const now = new Date();
|
|
294
|
-
|
|
295
|
-
const valid = keys
|
|
296
|
-
.filter(k => k.enabled)
|
|
297
|
-
.filter(k => !k.expiresAt || new Date(k.expiresAt) > now)
|
|
298
|
-
.sort((a, b) => (a.priority || 99) - (b.priority || 99));
|
|
299
|
-
|
|
300
|
-
return valid[0] || null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Append a new key to the provider's array in .dualbrain/auth.json.
|
|
305
|
-
* Injects the highest-priority valid key into process.env.
|
|
306
|
-
* @param {string} provider
|
|
307
|
-
* @param {string} key
|
|
308
|
-
* @param {object} [opts]
|
|
309
|
-
* @param {string} [opts.label]
|
|
310
|
-
* @param {string|null} [opts.expiresAt]
|
|
311
|
-
* @param {number} [opts.priority]
|
|
312
|
-
* @param {string} [opts.cwd]
|
|
313
|
-
*/
|
|
314
|
-
function saveAuthKey(provider, key, opts = {}) {
|
|
315
|
-
const cwd = opts.cwd || process.cwd();
|
|
316
|
-
const authFile = AUTH_FILE(cwd);
|
|
317
|
-
const dir = dirname(authFile);
|
|
318
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
319
|
-
|
|
320
|
-
const auth = loadAuthKeys(cwd);
|
|
321
|
-
if (!Array.isArray(auth[provider])) auth[provider] = [];
|
|
322
|
-
|
|
323
|
-
// Determine default label and priority
|
|
324
|
-
const existing = auth[provider];
|
|
325
|
-
const defaultLabel = `key-${existing.length + 1}`;
|
|
326
|
-
const defaultPriority = existing.length > 0
|
|
327
|
-
? Math.max(...existing.map(k => k.priority || 1)) + 1
|
|
328
|
-
: 1;
|
|
329
|
-
|
|
330
|
-
existing.push({
|
|
331
|
-
key,
|
|
332
|
-
label: opts.label || defaultLabel,
|
|
333
|
-
savedAt: new Date().toISOString(),
|
|
334
|
-
expiresAt: opts.expiresAt || null,
|
|
335
|
-
priority: opts.priority !== undefined ? opts.priority : defaultPriority,
|
|
336
|
-
enabled: true,
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
340
|
-
chmodSync(authFile, 0o600);
|
|
341
|
-
|
|
342
|
-
// Inject highest-priority valid key into process.env for this session
|
|
343
|
-
const active = getActiveKey(provider, cwd);
|
|
344
|
-
if (active) {
|
|
345
|
-
if (provider === 'claude') process.env.ANTHROPIC_API_KEY = active.key;
|
|
346
|
-
if (provider === 'openai') process.env.OPENAI_API_KEY = active.key;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Interactive setup flow: walks user through entering API keys for missing providers.
|
|
352
|
-
* Accepts an existing readline Interface (rl) — does NOT close it.
|
|
353
|
-
* @param {import('readline').Interface} rl
|
|
212
|
+
* @returns {{ [provider: string]: { plan: string, enabled: boolean, label?: string, expiresAt?: string } }}
|
|
354
213
|
*/
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
// Claude setup
|
|
360
|
-
if (!auth.claude.found) {
|
|
361
|
-
console.log('\n— Claude Setup —');
|
|
362
|
-
console.log('Options:');
|
|
363
|
-
console.log(' (1) Paste API key (recommended for Replit)');
|
|
364
|
-
console.log(' (2) Skip for now');
|
|
365
|
-
const choice = (await ask('> ')).trim();
|
|
366
|
-
if (choice === '1') {
|
|
367
|
-
const key = (await ask('Paste your Anthropic API key: ')).trim();
|
|
368
|
-
if (key && (key.startsWith('sk-ant-') || key.startsWith('sk-'))) {
|
|
369
|
-
const labelStr = (await ask('Label for this key (or Enter for "key-1"): ')).trim();
|
|
370
|
-
const label = labelStr || undefined;
|
|
371
|
-
const expiryStr = (await ask('Set expiry in days (or Enter to skip): ')).trim();
|
|
372
|
-
let expiresAt = null;
|
|
373
|
-
if (expiryStr && /^\d+$/.test(expiryStr)) {
|
|
374
|
-
const d = new Date();
|
|
375
|
-
d.setDate(d.getDate() + parseInt(expiryStr, 10));
|
|
376
|
-
expiresAt = d.toISOString();
|
|
377
|
-
console.log(`✓ Key expires in ${expiryStr} days (${d.toISOString().slice(0, 10)})`);
|
|
378
|
-
}
|
|
379
|
-
saveAuthKey('claude', key, { expiresAt, label });
|
|
380
|
-
console.log('✓ Claude API key saved');
|
|
381
|
-
} else {
|
|
382
|
-
console.log('Invalid key format. Expected sk-ant-... or sk-...');
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
console.log(`\n✓ Claude: already configured via ${auth.claude.source}`);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// OpenAI setup
|
|
390
|
-
if (!auth.openai.found) {
|
|
391
|
-
console.log('\n— OpenAI Setup —');
|
|
392
|
-
console.log('Options:');
|
|
393
|
-
console.log(' (1) Paste API key (recommended for Replit)');
|
|
394
|
-
console.log(' (2) Skip for now');
|
|
395
|
-
const choice = (await ask('> ')).trim();
|
|
396
|
-
if (choice === '1') {
|
|
397
|
-
const key = (await ask('Paste your OpenAI API key: ')).trim();
|
|
398
|
-
if (key && key.startsWith('sk-')) {
|
|
399
|
-
const labelStr = (await ask('Label for this key (or Enter for "key-1"): ')).trim();
|
|
400
|
-
const label = labelStr || undefined;
|
|
401
|
-
const expiryStr = (await ask('Set expiry in days (or Enter to skip): ')).trim();
|
|
402
|
-
let expiresAt = null;
|
|
403
|
-
if (expiryStr && /^\d+$/.test(expiryStr)) {
|
|
404
|
-
const d = new Date();
|
|
405
|
-
d.setDate(d.getDate() + parseInt(expiryStr, 10));
|
|
406
|
-
expiresAt = d.toISOString();
|
|
407
|
-
console.log(`✓ Key expires in ${expiryStr} days (${d.toISOString().slice(0, 10)})`);
|
|
408
|
-
}
|
|
409
|
-
saveAuthKey('openai', key, { expiresAt, label });
|
|
410
|
-
console.log('✓ OpenAI API key saved');
|
|
411
|
-
} else {
|
|
412
|
-
console.log('Invalid key format. Expected sk-...');
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
} else {
|
|
416
|
-
console.log(`\n✓ OpenAI: already configured via ${auth.openai.source}`);
|
|
417
|
-
}
|
|
214
|
+
function listSubscriptions(cwd) {
|
|
215
|
+
const profile = loadProfile(cwd);
|
|
216
|
+
return profile.providers || {};
|
|
418
217
|
}
|
|
419
218
|
|
|
420
219
|
// ---------------------------------------------------------------------------
|
|
@@ -827,10 +626,10 @@ async function autoSetup(cwd) {
|
|
|
827
626
|
'$100': 'Claude Max x5 ($100)',
|
|
828
627
|
'$200': 'Claude Max x20 ($200)',
|
|
829
628
|
}[profile.providers.claude.plan] || profile.providers.claude.plan;
|
|
830
|
-
result.actions.push(`${planLabel}
|
|
629
|
+
result.actions.push(`${planLabel} (${auth.claude.source})`);
|
|
831
630
|
} else {
|
|
832
631
|
profile.providers.claude.enabled = false;
|
|
833
|
-
result.warnings.push('Claude not
|
|
632
|
+
result.warnings.push('Claude CLI not logged in — run: claude login');
|
|
834
633
|
}
|
|
835
634
|
|
|
836
635
|
// OpenAI
|
|
@@ -842,10 +641,10 @@ async function autoSetup(cwd) {
|
|
|
842
641
|
'$100': 'ChatGPT Pro ($100)',
|
|
843
642
|
'$200': 'ChatGPT Pro ($200)',
|
|
844
643
|
}[profile.providers.openai.plan] || profile.providers.openai.plan;
|
|
845
|
-
result.actions.push(`${planLabel}
|
|
644
|
+
result.actions.push(`${planLabel} (${auth.openai.source})`);
|
|
846
645
|
} else {
|
|
847
646
|
profile.providers.openai.enabled = false;
|
|
848
|
-
result.warnings.push('
|
|
647
|
+
result.warnings.push('Codex CLI not logged in — run: codex login');
|
|
849
648
|
}
|
|
850
649
|
|
|
851
650
|
// Mode
|
|
@@ -873,7 +672,6 @@ export {
|
|
|
873
672
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
874
673
|
detectPlans, syncPreferencesToMemory,
|
|
875
674
|
detectAuth, detectEnvironment,
|
|
876
|
-
|
|
877
|
-
getActiveKey,
|
|
675
|
+
saveSubscription, listSubscriptions,
|
|
878
676
|
defaultProfile, autoSetup,
|
|
879
677
|
};
|
package/src/session.mjs
CHANGED
|
@@ -355,6 +355,90 @@ export function importReplitSessions(cwd = process.cwd()) {
|
|
|
355
355
|
return sessions;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// ─── Session metadata overlay ─────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
const SESSION_META_FILE = '.dualbrain/sessions.json';
|
|
361
|
+
|
|
362
|
+
function sessionMetaPath(cwd) {
|
|
363
|
+
return join(cwd ?? process.cwd(), SESSION_META_FILE);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function getSessionMeta(cwd = process.cwd()) {
|
|
367
|
+
const p = sessionMetaPath(cwd);
|
|
368
|
+
if (!existsSync(p)) return {};
|
|
369
|
+
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return {}; }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function saveSessionMeta(meta, cwd = process.cwd()) {
|
|
373
|
+
ensureDir(cwd);
|
|
374
|
+
const p = sessionMetaPath(cwd);
|
|
375
|
+
const tmp = p + '.tmp.' + process.pid;
|
|
376
|
+
writeFileSync(tmp, JSON.stringify(meta, null, 2) + '\n');
|
|
377
|
+
renameSync(tmp, p);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function renameSession(sessionId, name, cwd = process.cwd()) {
|
|
381
|
+
const meta = getSessionMeta(cwd);
|
|
382
|
+
meta[sessionId] = { ...meta[sessionId], name, createdAt: meta[sessionId]?.createdAt ?? new Date().toISOString() };
|
|
383
|
+
saveSessionMeta(meta, cwd);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function pinSession(sessionId, cwd = process.cwd()) {
|
|
387
|
+
const meta = getSessionMeta(cwd);
|
|
388
|
+
meta[sessionId] = { ...meta[sessionId], pinned: true, createdAt: meta[sessionId]?.createdAt ?? new Date().toISOString() };
|
|
389
|
+
saveSessionMeta(meta, cwd);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function unpinSession(sessionId, cwd = process.cwd()) {
|
|
393
|
+
const meta = getSessionMeta(cwd);
|
|
394
|
+
meta[sessionId] = { ...meta[sessionId], pinned: false };
|
|
395
|
+
saveSessionMeta(meta, cwd);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function categorizeSession(sessionId, category, cwd = process.cwd()) {
|
|
399
|
+
const meta = getSessionMeta(cwd);
|
|
400
|
+
meta[sessionId] = { ...meta[sessionId], category, createdAt: meta[sessionId]?.createdAt ?? new Date().toISOString() };
|
|
401
|
+
saveSessionMeta(meta, cwd);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const AUTO_LABEL_RULES = [
|
|
405
|
+
{ keywords: ['auth', 'login', 'credential', 'security', 'token'], label: 'security' },
|
|
406
|
+
{ keywords: ['ui', 'css', 'style', 'component', 'react', 'frontend'], label: 'ui' },
|
|
407
|
+
{ keywords: ['refactor', 'cleanup', 'rename', 'reorganize'], label: 'refactor' },
|
|
408
|
+
{ keywords: ['bug', 'fix', 'error', 'crash', 'broken'], label: 'bugfix' },
|
|
409
|
+
{ keywords: ['test', 'spec', 'coverage'], label: 'testing' },
|
|
410
|
+
{ keywords: ['deploy', 'ci', 'build', 'release'], label: 'devops' },
|
|
411
|
+
{ keywords: ['plan', 'design', 'architect', 'brainstorm'], label: 'planning' },
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
export function autoLabel(session) {
|
|
415
|
+
const text = (session.name || '').toLowerCase();
|
|
416
|
+
for (const { keywords, label } of AUTO_LABEL_RULES) {
|
|
417
|
+
if (keywords.some(kw => new RegExp(`\\b${kw}\\b`).test(text))) return label;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function enrichSessions(sessions, cwd = process.cwd()) {
|
|
423
|
+
const meta = getSessionMeta(cwd);
|
|
424
|
+
const enriched = sessions.map(sess => {
|
|
425
|
+
const overlay = meta[sess.id] ?? {};
|
|
426
|
+
const category = overlay.category ?? autoLabel({ ...sess, name: overlay.name ?? sess.name });
|
|
427
|
+
return {
|
|
428
|
+
...sess,
|
|
429
|
+
name: overlay.name ?? sess.name,
|
|
430
|
+
pinned: overlay.pinned ?? false,
|
|
431
|
+
category: category ?? null,
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
enriched.sort((a, b) => {
|
|
435
|
+
if (a.pinned && !b.pinned) return -1;
|
|
436
|
+
if (!a.pinned && b.pinned) return 1;
|
|
437
|
+
return new Date(b.lastActive) - new Date(a.lastActive);
|
|
438
|
+
});
|
|
439
|
+
return enriched;
|
|
440
|
+
}
|
|
441
|
+
|
|
358
442
|
// ─── CLI (direct invocation) ──────────────────────────────────────────────────
|
|
359
443
|
|
|
360
444
|
const isMain = process.argv[1]?.endsWith('session.mjs');
|