@askalf/dario 3.32.2 → 3.33.0

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.
@@ -255,6 +255,21 @@ export function detectTextToolClient(systemText) {
255
255
  // preserve-tools is the only correct routing.
256
256
  if (/\bYou are Arnie\b/.test(systemText))
257
257
  return 'arnie';
258
+ // hands (askalf) — cross-platform computer-use agent built on the
259
+ // Anthropic SDK with computer-use beta tools (computer_20251124,
260
+ // bash_20250124, text_editor_20250728). Identity line is stable
261
+ // across CLI mode ("You are a computer control agent with FULL
262
+ // access to this <os> machine ...") and SDK mode ("You are a
263
+ // computer control agent on <os> ..."). Tool name `bash` overlaps
264
+ // with TOOL_MAP, but the wire shape is Anthropic's beta computer-
265
+ // use tool (`type: 'bash_20250124'`, no `command`/`description`
266
+ // schema) — default round-robin remap would corrupt those calls
267
+ // and lose the `computer` / `text_editor` tools entirely (neither
268
+ // is in TOOL_MAP, structural fallback won't catch them at the
269
+ // 80% threshold either). Identity match → auto preserve-tools,
270
+ // like arnie.
271
+ if (/\bYou are a computer control agent\b/.test(systemText))
272
+ return 'hands';
258
273
  // Protocol-signature fallback — unique to the Cline family and its
259
274
  // forks; survives a forked system prompt that edited the identity
260
275
  // string out but kept the tool protocol intact.
package/dist/oauth.d.ts CHANGED
@@ -14,6 +14,15 @@ export interface CredentialsFile {
14
14
  claudeAiOauth: OAuthTokens;
15
15
  }
16
16
  export declare function loadCredentials(): Promise<CredentialsFile | null>;
17
+ /**
18
+ * Pick the freshest of a set of `CredentialsFile` candidates by
19
+ * `expiresAt` (unix-ms timestamp; missing/zero sorts last). Stable on
20
+ * ties — the first-pushed candidate wins when expiresAt is equal,
21
+ * which means the canonical call order
22
+ * `[darioFile, ccFile, keychain]` keeps the dario-file source as the
23
+ * tiebreaker preference. Exported for direct testing.
24
+ */
25
+ export declare function pickFreshestCredentials(candidates: CredentialsFile[]): CredentialsFile | null;
17
26
  /**
18
27
  * Automatic OAuth flow using a local callback server (same as Claude Code).
19
28
  * Opens browser, captures the authorization code automatically.
package/dist/oauth.js CHANGED
@@ -159,27 +159,60 @@ export async function loadCredentials() {
159
159
  if (credentialsCache && Date.now() - credentialsCacheTime < CACHE_TTL_MS) {
160
160
  return credentialsCache;
161
161
  }
162
- // Try dario's own credentials first, then fall back to Claude Code's file
162
+ // Read every available source (dario file, CC file, OS keychain) and
163
+ // pick the freshest. Previously this returned the first source that
164
+ // had both tokens regardless of expiry — which means a stale
165
+ // ~/.dario/credentials.json (left over from a prior `dario login`
166
+ // whose refresh_token has since been invalidated by Anthropic) would
167
+ // shadow CC's still-fresh ~/.claude/.credentials.json forever, with
168
+ // no automatic recovery. Picking the freshest makes auto-detection
169
+ // work the way it did before any `dario login` had ever run, while
170
+ // still preferring dario's own file when both sources are equivalent
171
+ // (dario file wins ties on expiresAt by being checked first).
172
+ const candidates = [];
163
173
  for (const path of [getDarioCredentialsPath(), getClaudeCodeCredentialsPath()]) {
164
174
  try {
165
175
  const raw = await readFile(path, 'utf-8');
166
176
  const parsed = JSON.parse(raw);
167
177
  if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
168
- credentialsCache = parsed;
169
- credentialsCacheTime = Date.now();
170
- return credentialsCache;
178
+ candidates.push(parsed);
171
179
  }
172
180
  }
173
181
  catch { /* try next */ }
174
182
  }
175
- // Fall back to OS keychain (modern CC stores credentials here, not on disk)
183
+ // OS keychain (modern CC stores credentials here, not on disk).
176
184
  const keychainCreds = await loadKeychainCredentials();
177
- if (keychainCreds) {
178
- credentialsCache = keychainCreds;
179
- credentialsCacheTime = Date.now();
180
- return credentialsCache;
185
+ if (keychainCreds?.claudeAiOauth?.accessToken && keychainCreds?.claudeAiOauth?.refreshToken) {
186
+ candidates.push(keychainCreds);
181
187
  }
182
- return null;
188
+ const best = pickFreshestCredentials(candidates);
189
+ if (!best)
190
+ return null;
191
+ credentialsCache = best;
192
+ credentialsCacheTime = Date.now();
193
+ return credentialsCache;
194
+ }
195
+ /**
196
+ * Pick the freshest of a set of `CredentialsFile` candidates by
197
+ * `expiresAt` (unix-ms timestamp; missing/zero sorts last). Stable on
198
+ * ties — the first-pushed candidate wins when expiresAt is equal,
199
+ * which means the canonical call order
200
+ * `[darioFile, ccFile, keychain]` keeps the dario-file source as the
201
+ * tiebreaker preference. Exported for direct testing.
202
+ */
203
+ export function pickFreshestCredentials(candidates) {
204
+ if (candidates.length === 0)
205
+ return null;
206
+ let best = candidates[0];
207
+ let bestExp = best.claudeAiOauth.expiresAt ?? 0;
208
+ for (let i = 1; i < candidates.length; i++) {
209
+ const exp = candidates[i].claudeAiOauth.expiresAt ?? 0;
210
+ if (exp > bestExp) {
211
+ best = candidates[i];
212
+ bestExp = exp;
213
+ }
214
+ }
215
+ return best;
183
216
  }
184
217
  async function saveCredentials(creds) {
185
218
  const path = getDarioCredentialsPath();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.32.2",
3
+ "version": "3.33.0",
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": {