@askalf/dario 3.37.11 → 3.37.12
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/dist/accounts.d.ts +27 -0
- package/dist/accounts.js +72 -1
- package/dist/cli.js +53 -9
- package/dist/oauth.d.ts +41 -0
- package/dist/oauth.js +140 -0
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -35,6 +35,33 @@ export declare function addAccountViaOAuth(alias: string): Promise<AccountCreden
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function addAccountViaManualOAuth(alias: string): Promise<AccountCredentials>;
|
|
37
37
|
export declare function getAccountsDir(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Error subclass for the keychain-import path so the CLI can render
|
|
40
|
+
* actionable guidance (list of candidates) without parsing message strings.
|
|
41
|
+
*/
|
|
42
|
+
export declare class KeychainImportError extends Error {
|
|
43
|
+
readonly kind: 'empty' | 'ambiguous' | 'no-match';
|
|
44
|
+
readonly candidates: string[];
|
|
45
|
+
constructor(message: string, kind: 'empty' | 'ambiguous' | 'no-match', candidates?: string[]);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Import a Claude Code keychain entry into the pool under `alias`. Skips
|
|
49
|
+
* the OAuth flow entirely — reuses tokens the user already authorised
|
|
50
|
+
* through Claude Code itself. See askalf/dario#237 for design rationale.
|
|
51
|
+
*
|
|
52
|
+
* Resolution rules:
|
|
53
|
+
* - 0 entries on this host → throws KeychainImportError(kind: 'empty')
|
|
54
|
+
* - 1 entry total → imports it; `target` argument ignored if supplied
|
|
55
|
+
* - 2+ entries + no target → throws KeychainImportError(kind: 'ambiguous',
|
|
56
|
+
* candidates: [<target1>, <target2>, ...]) so the CLI can list them
|
|
57
|
+
* - 2+ entries + target → imports the matching one, throws
|
|
58
|
+
* KeychainImportError(kind: 'no-match', candidates) if none match
|
|
59
|
+
*
|
|
60
|
+
* macOS currently only ever surfaces a single entry (see the comment in
|
|
61
|
+
* enumerateKeychainCredentials in oauth.ts). Linux + Windows enumerate
|
|
62
|
+
* all matching entries.
|
|
63
|
+
*/
|
|
64
|
+
export declare function addAccountFromKeychain(alias: string, target?: string): Promise<AccountCredentials>;
|
|
38
65
|
/**
|
|
39
66
|
* Alias reserved for credentials auto-migrated from the single-account
|
|
40
67
|
* `dario login` store. Named `login` so it's semantically obvious where
|
package/dist/accounts.js
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from 'node:os';
|
|
|
22
22
|
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
23
23
|
import { createServer } from 'node:http';
|
|
24
24
|
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
25
|
-
import { loadCredentials, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin } from './oauth.js';
|
|
25
|
+
import { loadCredentials, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
|
|
26
26
|
import { openBrowser } from './open-browser.js';
|
|
27
27
|
import { redactSecrets } from './redact.js';
|
|
28
28
|
const MANUAL_REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';
|
|
@@ -388,6 +388,77 @@ export async function addAccountViaManualOAuth(alias) {
|
|
|
388
388
|
export function getAccountsDir() {
|
|
389
389
|
return ACCOUNTS_DIR;
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Error subclass for the keychain-import path so the CLI can render
|
|
393
|
+
* actionable guidance (list of candidates) without parsing message strings.
|
|
394
|
+
*/
|
|
395
|
+
export class KeychainImportError extends Error {
|
|
396
|
+
kind;
|
|
397
|
+
candidates;
|
|
398
|
+
constructor(message, kind, candidates = []) {
|
|
399
|
+
super(message);
|
|
400
|
+
this.kind = kind;
|
|
401
|
+
this.candidates = candidates;
|
|
402
|
+
this.name = 'KeychainImportError';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Import a Claude Code keychain entry into the pool under `alias`. Skips
|
|
407
|
+
* the OAuth flow entirely — reuses tokens the user already authorised
|
|
408
|
+
* through Claude Code itself. See askalf/dario#237 for design rationale.
|
|
409
|
+
*
|
|
410
|
+
* Resolution rules:
|
|
411
|
+
* - 0 entries on this host → throws KeychainImportError(kind: 'empty')
|
|
412
|
+
* - 1 entry total → imports it; `target` argument ignored if supplied
|
|
413
|
+
* - 2+ entries + no target → throws KeychainImportError(kind: 'ambiguous',
|
|
414
|
+
* candidates: [<target1>, <target2>, ...]) so the CLI can list them
|
|
415
|
+
* - 2+ entries + target → imports the matching one, throws
|
|
416
|
+
* KeychainImportError(kind: 'no-match', candidates) if none match
|
|
417
|
+
*
|
|
418
|
+
* macOS currently only ever surfaces a single entry (see the comment in
|
|
419
|
+
* enumerateKeychainCredentials in oauth.ts). Linux + Windows enumerate
|
|
420
|
+
* all matching entries.
|
|
421
|
+
*/
|
|
422
|
+
export async function addAccountFromKeychain(alias, target) {
|
|
423
|
+
const entries = await enumerateKeychainCredentials();
|
|
424
|
+
if (entries.length === 0) {
|
|
425
|
+
throw new KeychainImportError('No Claude Code keychain entries found on this host. Run `claude` (login flow) first, or use `dario accounts add ' + alias + '` to start a fresh OAuth.', 'empty');
|
|
426
|
+
}
|
|
427
|
+
let chosen;
|
|
428
|
+
if (target) {
|
|
429
|
+
chosen = entries.find(e => e.target === target);
|
|
430
|
+
if (!chosen) {
|
|
431
|
+
throw new KeychainImportError(`No keychain entry matches target "${target}".`, 'no-match', entries.map(e => e.target));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else if (entries.length === 1) {
|
|
435
|
+
chosen = entries[0];
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
throw new KeychainImportError(`Found ${entries.length} keychain entries — pick one with --from-keychain=<target>.`, 'ambiguous', entries.map(e => e.target));
|
|
439
|
+
}
|
|
440
|
+
const oauth = chosen.credentials.claudeAiOauth;
|
|
441
|
+
if (!oauth?.accessToken || !oauth?.refreshToken) {
|
|
442
|
+
throw new KeychainImportError(`Keychain entry "${chosen.target}" is missing accessToken/refreshToken — re-authenticate Claude Code.`, 'empty');
|
|
443
|
+
}
|
|
444
|
+
// Same identity preference as addAccountViaOAuth — prefer CC identity if
|
|
445
|
+
// installed; otherwise generate fresh IDs.
|
|
446
|
+
const identity = (await detectClaudeIdentity()) ?? {
|
|
447
|
+
deviceId: randomUUID(),
|
|
448
|
+
accountUuid: randomUUID(),
|
|
449
|
+
};
|
|
450
|
+
const creds = {
|
|
451
|
+
alias,
|
|
452
|
+
accessToken: oauth.accessToken,
|
|
453
|
+
refreshToken: oauth.refreshToken,
|
|
454
|
+
expiresAt: oauth.expiresAt,
|
|
455
|
+
scopes: oauth.scopes ?? ['user:inference'],
|
|
456
|
+
deviceId: identity.deviceId,
|
|
457
|
+
accountUuid: identity.accountUuid,
|
|
458
|
+
};
|
|
459
|
+
await saveAccount(creds);
|
|
460
|
+
return creds;
|
|
461
|
+
}
|
|
391
462
|
/**
|
|
392
463
|
* Alias reserved for credentials auto-migrated from the single-account
|
|
393
464
|
* `dario login` store. Named `login` so it's semantically obvious where
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
24
24
|
import { startAutoOAuthFlow, startManualOAuthFlow, detectHeadlessEnvironment, getStatus, refreshTokens, loadCredentials } from './oauth.js';
|
|
25
25
|
import { startProxy, sanitizeError } from './proxy.js';
|
|
26
26
|
import { VALID_EFFORT_VALUES } from './cc-template.js';
|
|
27
|
-
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, addAccountViaManualOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
27
|
+
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, addAccountViaManualOAuth, addAccountFromKeychain, KeychainImportError, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
28
28
|
import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
|
|
29
29
|
import { parseOutboundProxy, installOutboundProxyWrapper } from './outbound-proxy.js';
|
|
30
30
|
// `args` / `command` at module scope — command handlers below close over
|
|
@@ -660,15 +660,30 @@ async function accounts() {
|
|
|
660
660
|
}
|
|
661
661
|
}
|
|
662
662
|
const manualAccountFlag = args.includes('--manual') || args.includes('--headless');
|
|
663
|
+
// --from-keychain[=<target>] imports an existing Claude Code keychain
|
|
664
|
+
// entry instead of running OAuth. Bare flag uses the only/first match;
|
|
665
|
+
// --from-keychain=<target> picks a specific entry by its platform-
|
|
666
|
+
// specific identifier (Linux account, Windows TargetName). See
|
|
667
|
+
// askalf/dario#237.
|
|
668
|
+
const keychainArg = args.find(a => a === '--from-keychain' || a === '--from-cc' || a.startsWith('--from-keychain=') || a.startsWith('--from-cc='));
|
|
669
|
+
const fromKeychain = keychainArg !== undefined;
|
|
670
|
+
const keychainTarget = keychainArg && keychainArg.includes('=') ? keychainArg.split('=', 2)[1] : undefined;
|
|
671
|
+
if (fromKeychain && manualAccountFlag) {
|
|
672
|
+
console.error('');
|
|
673
|
+
console.error(' --from-keychain and --manual are mutually exclusive (one skips OAuth, the other does it manually).');
|
|
674
|
+
console.error('');
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
663
677
|
console.log('');
|
|
664
|
-
console.log(` Adding account "${alias}" to the pool${manualAccountFlag ? ' (manual / headless flow)' : ''}...`);
|
|
678
|
+
console.log(` Adding account "${alias}" to the pool${manualAccountFlag ? ' (manual / headless flow)' : fromKeychain ? ' (importing from OS keychain)' : ''}...`);
|
|
665
679
|
console.log('');
|
|
666
680
|
// Mirror the heuristic that `dario login` uses: if the user didn't
|
|
667
681
|
// explicitly pick `--manual` AND we detect SSH / container / no-DISPLAY,
|
|
668
682
|
// print a hint before opening the browser. Doesn't auto-flip — false
|
|
669
683
|
// positives are more annoying than false negatives — but the hint keeps
|
|
670
|
-
// users from waiting for a browser redirect that can't land.
|
|
671
|
-
|
|
684
|
+
// users from waiting for a browser redirect that can't land. Skip the
|
|
685
|
+
// hint entirely when --from-keychain is set since no browser is opened.
|
|
686
|
+
if (!manualAccountFlag && !fromKeychain) {
|
|
672
687
|
const reason = detectHeadlessEnvironment();
|
|
673
688
|
if (reason) {
|
|
674
689
|
console.log(` Note: ${reason}. If the browser redirect doesn't land,`);
|
|
@@ -677,9 +692,11 @@ async function accounts() {
|
|
|
677
692
|
}
|
|
678
693
|
}
|
|
679
694
|
try {
|
|
680
|
-
const creds =
|
|
681
|
-
? await
|
|
682
|
-
:
|
|
695
|
+
const creds = fromKeychain
|
|
696
|
+
? await addAccountFromKeychain(alias, keychainTarget)
|
|
697
|
+
: manualAccountFlag
|
|
698
|
+
? await addAccountViaManualOAuth(alias)
|
|
699
|
+
: await addAccountViaOAuth(alias);
|
|
683
700
|
const minutes = Math.round((creds.expiresAt - Date.now()) / 60000);
|
|
684
701
|
console.log('');
|
|
685
702
|
console.log(` Account "${alias}" added.`);
|
|
@@ -697,6 +714,26 @@ async function accounts() {
|
|
|
697
714
|
console.log('');
|
|
698
715
|
}
|
|
699
716
|
catch (err) {
|
|
717
|
+
// KeychainImportError carries structured kind+candidates so we can
|
|
718
|
+
// render a targeted next step without parsing the message.
|
|
719
|
+
if (err instanceof KeychainImportError) {
|
|
720
|
+
console.error('');
|
|
721
|
+
console.error(` Failed to add account: ${err.message}`);
|
|
722
|
+
if (err.kind === 'ambiguous' && err.candidates.length > 0) {
|
|
723
|
+
console.error('');
|
|
724
|
+
console.error(' Available keychain entries:');
|
|
725
|
+
for (const t of err.candidates)
|
|
726
|
+
console.error(` --from-keychain="${t}"`);
|
|
727
|
+
}
|
|
728
|
+
else if (err.kind === 'no-match' && err.candidates.length > 0) {
|
|
729
|
+
console.error('');
|
|
730
|
+
console.error(' Available keychain entries:');
|
|
731
|
+
for (const t of err.candidates)
|
|
732
|
+
console.error(` --from-keychain="${t}"`);
|
|
733
|
+
}
|
|
734
|
+
console.error('');
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
700
737
|
const msg = sanitizeError(err);
|
|
701
738
|
console.error('');
|
|
702
739
|
console.error(` Failed to add account: ${msg}`);
|
|
@@ -704,7 +741,7 @@ async function accounts() {
|
|
|
704
741
|
// `dario login`. Auto flow can fail on EADDRINUSE (port already
|
|
705
742
|
// bound), SSH-tunnel mismatch, or the browser timing out before
|
|
706
743
|
// the user signs in. `--manual` works in all of those cases.
|
|
707
|
-
if (!manualAccountFlag && /callback server|EADDRINUSE|bind|timed out|did not receive/i.test(msg)) {
|
|
744
|
+
if (!manualAccountFlag && !fromKeychain && /callback server|EADDRINUSE|bind|timed out|did not receive/i.test(msg)) {
|
|
708
745
|
console.error(` Hint: try \`dario accounts add ${alias} --manual\` for headless / container setups.`);
|
|
709
746
|
}
|
|
710
747
|
console.error('');
|
|
@@ -849,13 +886,20 @@ async function help() {
|
|
|
849
886
|
dario refresh Force token refresh
|
|
850
887
|
dario logout Remove saved credentials
|
|
851
888
|
dario accounts list List accounts in the multi-account pool
|
|
852
|
-
dario accounts add NAME [--manual]
|
|
889
|
+
dario accounts add NAME [--manual] [--from-keychain[=<target>]]
|
|
853
890
|
Add a new account to the pool (runs OAuth flow).
|
|
854
891
|
--manual (alias: --headless) prints an authorize
|
|
855
892
|
URL and reads the code you paste back — for
|
|
856
893
|
container / SSH / no-browser-on-this-machine
|
|
857
894
|
setups, or as the on-Windows escape hatch when
|
|
858
895
|
the URL dispatch chain truncates query params.
|
|
896
|
+
--from-keychain skips OAuth and imports an
|
|
897
|
+
existing Claude Code keychain entry on this
|
|
898
|
+
host. With no value: uses the only/first match
|
|
899
|
+
(errors with the candidate list if multiple
|
|
900
|
+
entries exist). With =<target>: picks a specific
|
|
901
|
+
entry by its platform identifier (Linux account
|
|
902
|
+
attribute, Windows TargetName).
|
|
859
903
|
dario accounts remove N Remove an account from the pool
|
|
860
904
|
dario backend list List configured OpenAI-compat backends
|
|
861
905
|
dario backend add NAME --key=sk-... [--base-url=...]
|
package/dist/oauth.d.ts
CHANGED
|
@@ -22,6 +22,47 @@ export interface OAuthTokens {
|
|
|
22
22
|
export interface CredentialsFile {
|
|
23
23
|
claudeAiOauth: OAuthTokens;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Information about a keychain entry surfaced for operator disambiguation
|
|
27
|
+
* during `dario accounts add --from-keychain`.
|
|
28
|
+
*/
|
|
29
|
+
export interface KeychainEntry {
|
|
30
|
+
/**
|
|
31
|
+
* Implementation-defined identifier the operator uses to pick a specific
|
|
32
|
+
* entry. Stable per-platform but not equal across platforms:
|
|
33
|
+
* - Linux: the libsecret `account` attribute (or value when absent)
|
|
34
|
+
* - Windows: the Credential Manager TargetName (e.g.
|
|
35
|
+
* "Claude Code-credentials" or "Claude Code-credentials@<account-uuid>")
|
|
36
|
+
* - macOS: always "Claude Code-credentials" until macOS-side multi-entry
|
|
37
|
+
* enumeration is implemented (see comment in enumerateKeychainCredentials)
|
|
38
|
+
*/
|
|
39
|
+
target: string;
|
|
40
|
+
credentials: CredentialsFile;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Enumerate every Claude Code keychain entry on this host. Pool-mode
|
|
44
|
+
* counterpart to `loadKeychainCredentials` (which returns the first hit
|
|
45
|
+
* for the single-account login flow). Used by `dario accounts add
|
|
46
|
+
* --from-keychain` to import without rerunning OAuth.
|
|
47
|
+
*
|
|
48
|
+
* Per-platform coverage:
|
|
49
|
+
* - **Linux**: `secret-tool search --all service "Claude Code-credentials"`
|
|
50
|
+
* enumerates every matching attribute set. Account name comes from the
|
|
51
|
+
* `account` attribute when set, otherwise the secret hash truncated.
|
|
52
|
+
* - **Windows**: PowerShell + CredEnumerate already iterates every
|
|
53
|
+
* matching credential (existing pattern just wasn't exposing the
|
|
54
|
+
* TargetName). New script variant emits target + JSON blob per line.
|
|
55
|
+
* - **macOS**: returns at most one entry. The `security` CLI doesn't
|
|
56
|
+
* expose a clean enumeration for `find-generic-password` results; full
|
|
57
|
+
* macOS multi-account support would need either `dump-keychain` parsing
|
|
58
|
+
* or a Swift/native helper. Filed as a follow-up; the common case (one
|
|
59
|
+
* CC account in keychain) still works.
|
|
60
|
+
*
|
|
61
|
+
* Returns an empty array on any failure (keychain unavailable, no entries
|
|
62
|
+
* matching, parse errors). Callers are expected to handle empty as
|
|
63
|
+
* "nothing to import."
|
|
64
|
+
*/
|
|
65
|
+
export declare function enumerateKeychainCredentials(): Promise<KeychainEntry[]>;
|
|
25
66
|
export declare function loadCredentials(): Promise<CredentialsFile | null>;
|
|
26
67
|
/**
|
|
27
68
|
* Pick the freshest of a set of `CredentialsFile` candidates by
|
package/dist/oauth.js
CHANGED
|
@@ -147,6 +147,146 @@ if ([CM]::CredEnumerate('Claude Code-credentials*', 0, [ref]$count, [ref]$ptr))
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
`;
|
|
150
|
+
// Enumeration variant of WIN_CRED_SCRIPT — emits one line per credential
|
|
151
|
+
// formatted as `<TargetName>\t<JSON>` so the importer can label entries
|
|
152
|
+
// for the operator to disambiguate between accounts.
|
|
153
|
+
const WIN_CRED_ENUMERATE_SCRIPT = `
|
|
154
|
+
$ErrorActionPreference = 'Stop'
|
|
155
|
+
$sig = @"
|
|
156
|
+
using System;
|
|
157
|
+
using System.Runtime.InteropServices;
|
|
158
|
+
public class CM2 {
|
|
159
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
160
|
+
public struct CRED {
|
|
161
|
+
public uint Flags; public uint Type; public string TargetName;
|
|
162
|
+
public string Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LW;
|
|
163
|
+
public uint BlobSize; public IntPtr Blob;
|
|
164
|
+
public uint Persist; public uint AC; public IntPtr Attrs;
|
|
165
|
+
public string Alias; public string UN;
|
|
166
|
+
}
|
|
167
|
+
[DllImport("advapi32.dll", EntryPoint="CredEnumerateW", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
168
|
+
public static extern bool CredEnumerate(string filter, uint flag, out uint count, out IntPtr pCredentials);
|
|
169
|
+
[DllImport("advapi32.dll", EntryPoint="CredFree")]
|
|
170
|
+
public static extern void CredFree(IntPtr cred);
|
|
171
|
+
}
|
|
172
|
+
"@
|
|
173
|
+
Add-Type -TypeDefinition $sig
|
|
174
|
+
$count = 0
|
|
175
|
+
$ptr = [IntPtr]::Zero
|
|
176
|
+
if ([CM2]::CredEnumerate('Claude Code-credentials*', 0, [ref]$count, [ref]$ptr)) {
|
|
177
|
+
try {
|
|
178
|
+
for ($i = 0; $i -lt $count; $i++) {
|
|
179
|
+
$credPtr = [System.Runtime.InteropServices.Marshal]::ReadIntPtr($ptr, $i * [IntPtr]::Size)
|
|
180
|
+
$cred = [System.Runtime.InteropServices.Marshal]::PtrToStructure($credPtr, [type][CM2+CRED])
|
|
181
|
+
if ($cred.BlobSize -gt 0) {
|
|
182
|
+
$bytes = New-Object byte[] $cred.BlobSize
|
|
183
|
+
[System.Runtime.InteropServices.Marshal]::Copy($cred.Blob, $bytes, 0, $cred.BlobSize)
|
|
184
|
+
$blob = [System.Text.Encoding]::Unicode.GetString($bytes)
|
|
185
|
+
Write-Output ($cred.TargetName + "\`t" + $blob)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
[CM2]::CredFree($ptr)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
/**
|
|
194
|
+
* Enumerate every Claude Code keychain entry on this host. Pool-mode
|
|
195
|
+
* counterpart to `loadKeychainCredentials` (which returns the first hit
|
|
196
|
+
* for the single-account login flow). Used by `dario accounts add
|
|
197
|
+
* --from-keychain` to import without rerunning OAuth.
|
|
198
|
+
*
|
|
199
|
+
* Per-platform coverage:
|
|
200
|
+
* - **Linux**: `secret-tool search --all service "Claude Code-credentials"`
|
|
201
|
+
* enumerates every matching attribute set. Account name comes from the
|
|
202
|
+
* `account` attribute when set, otherwise the secret hash truncated.
|
|
203
|
+
* - **Windows**: PowerShell + CredEnumerate already iterates every
|
|
204
|
+
* matching credential (existing pattern just wasn't exposing the
|
|
205
|
+
* TargetName). New script variant emits target + JSON blob per line.
|
|
206
|
+
* - **macOS**: returns at most one entry. The `security` CLI doesn't
|
|
207
|
+
* expose a clean enumeration for `find-generic-password` results; full
|
|
208
|
+
* macOS multi-account support would need either `dump-keychain` parsing
|
|
209
|
+
* or a Swift/native helper. Filed as a follow-up; the common case (one
|
|
210
|
+
* CC account in keychain) still works.
|
|
211
|
+
*
|
|
212
|
+
* Returns an empty array on any failure (keychain unavailable, no entries
|
|
213
|
+
* matching, parse errors). Callers are expected to handle empty as
|
|
214
|
+
* "nothing to import."
|
|
215
|
+
*/
|
|
216
|
+
export async function enumerateKeychainCredentials() {
|
|
217
|
+
const out = [];
|
|
218
|
+
try {
|
|
219
|
+
if (platform() === 'darwin') {
|
|
220
|
+
// Single-entry path; multi-entry on macOS is a known limitation.
|
|
221
|
+
const single = await loadKeychainCredentials();
|
|
222
|
+
if (single)
|
|
223
|
+
out.push({ target: 'Claude Code-credentials', credentials: single });
|
|
224
|
+
}
|
|
225
|
+
else if (platform() === 'linux') {
|
|
226
|
+
const raw = await new Promise((resolve, reject) => {
|
|
227
|
+
execFile('secret-tool', ['search', '--all', 'service', 'Claude Code-credentials'], { timeout: 5000 }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
228
|
+
});
|
|
229
|
+
// secret-tool emits blocks separated by blank lines, with lines like:
|
|
230
|
+
// [/secret/Claude Code-credentials/0]
|
|
231
|
+
// label = ...
|
|
232
|
+
// secret = <json blob>
|
|
233
|
+
// attribute.account = <account name>
|
|
234
|
+
// attribute.service = Claude Code-credentials
|
|
235
|
+
let currentSecret;
|
|
236
|
+
let currentAccount;
|
|
237
|
+
const flush = () => {
|
|
238
|
+
if (!currentSecret)
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(currentSecret);
|
|
242
|
+
if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
|
|
243
|
+
out.push({
|
|
244
|
+
target: currentAccount || 'Claude Code-credentials',
|
|
245
|
+
credentials: parsed,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch { /* not a CC creds blob — skip */ }
|
|
250
|
+
currentSecret = undefined;
|
|
251
|
+
currentAccount = undefined;
|
|
252
|
+
};
|
|
253
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
254
|
+
if (line.startsWith('[/')) {
|
|
255
|
+
flush();
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (line.startsWith('secret = '))
|
|
259
|
+
currentSecret = line.slice('secret = '.length);
|
|
260
|
+
else if (line.startsWith('attribute.account = '))
|
|
261
|
+
currentAccount = line.slice('attribute.account = '.length);
|
|
262
|
+
}
|
|
263
|
+
flush();
|
|
264
|
+
}
|
|
265
|
+
else if (platform() === 'win32') {
|
|
266
|
+
const raw = await new Promise((resolve, reject) => {
|
|
267
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', WIN_CRED_ENUMERATE_SCRIPT], { timeout: 5000, windowsHide: true }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
268
|
+
});
|
|
269
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
270
|
+
const tab = line.indexOf('\t');
|
|
271
|
+
if (tab < 0)
|
|
272
|
+
continue;
|
|
273
|
+
const target = line.slice(0, tab).trim();
|
|
274
|
+
const blob = line.slice(tab + 1).trim();
|
|
275
|
+
if (!target || !blob)
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(blob);
|
|
279
|
+
if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
|
|
280
|
+
out.push({ target, credentials: parsed });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { /* skip non-credential entries */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { /* keychain unavailable / empty */ }
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
150
290
|
async function loadKeychainCredentials() {
|
|
151
291
|
try {
|
|
152
292
|
if (platform() === 'darwin') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.37.
|
|
3
|
+
"version": "3.37.12",
|
|
4
4
|
"description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|