@scrylog/cli 0.3.0 → 0.3.2
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/assets/highlights-eq9cgrbb.scm +604 -0
- package/dist/assets/highlights-ghv9g403.scm +205 -0
- package/dist/assets/highlights-hk7bwhj4.scm +284 -0
- package/dist/assets/highlights-r812a2qc.scm +150 -0
- package/dist/assets/highlights-x6tmsnaa.scm +115 -0
- package/dist/assets/injections-73j83es3.scm +27 -0
- package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
- package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
- package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/cli/index.js +499 -0
- package/dist/cli/index.js.map +61 -0
- package/dist/daemon/index.js +670 -0
- package/dist/daemon/index.js.map +1475 -0
- package/dist/hooks/claude.js +6 -0
- package/dist/hooks/claude.js.map +11 -0
- package/dist/hooks/codex.js +10 -0
- package/dist/hooks/codex.js.map +11 -0
- package/dist/mcp/index.js +100 -0
- package/dist/mcp/index.js.map +231 -0
- package/dist/oc/index.js +21 -0
- package/dist/oc/index.js.map +10 -0
- package/package.json +16 -13
- package/src/api/docs/sessions.keys.ts +0 -6
- package/src/api/docs/sessions.ts +0 -82
- package/src/api/docs/system.keys.ts +0 -5
- package/src/api/docs/system.ts +0 -69
- package/src/api/docs/usage.keys.ts +0 -3
- package/src/api/docs/usage.ts +0 -24
- package/src/api/docs/webhooks.keys.ts +0 -5
- package/src/api/docs/webhooks.ts +0 -78
- package/src/api/index.ts +0 -18
- package/src/api/lib/auth.ts +0 -43
- package/src/api/lib/errors.ts +0 -28
- package/src/api/lib/route.ts +0 -18
- package/src/api/models/claude.ts +0 -4
- package/src/api/models/codex.ts +0 -4
- package/src/api/models/opencode.ts +0 -11
- package/src/api/routes/sessions.ts +0 -51
- package/src/api/routes/system.ts +0 -106
- package/src/api/routes/usage.ts +0 -15
- package/src/api/routes/webhooks/claude.ts +0 -149
- package/src/api/routes/webhooks/codex.ts +0 -175
- package/src/api/routes/webhooks/opencode.ts +0 -112
- package/src/api/routes.ts +0 -93
- package/src/api/schemas/common.ts +0 -29
- package/src/api/schemas/sessions.ts +0 -86
- package/src/api/schemas/usage.ts +0 -51
- package/src/api/schemas/webhooks/claude.ts +0 -32
- package/src/api/schemas/webhooks/codex.ts +0 -34
- package/src/api/schemas/webhooks/opencode.ts +0 -29
- package/src/api/services/claude-session.ts +0 -9
- package/src/api/services/codex-session.ts +0 -8
- package/src/api/services/opencode-session.ts +0 -73
- package/src/classifier.ts +0 -206
- package/src/cli/commands.ts +0 -714
- package/src/cli/index.ts +0 -18
- package/src/cli/program.ts +0 -175
- package/src/cli/prompt.ts +0 -44
- package/src/daemon/cli.ts +0 -27
- package/src/daemon/collectors/claude/README.md +0 -108
- package/src/daemon/collectors/claude/collector.ts +0 -423
- package/src/daemon/collectors/claude/discovery.ts +0 -127
- package/src/daemon/collectors/claude/hooks.ts +0 -181
- package/src/daemon/collectors/claude/index.ts +0 -8
- package/src/daemon/collectors/claude/jsonl.ts +0 -85
- package/src/daemon/collectors/claude/transcript.ts +0 -246
- package/src/daemon/collectors/codex/README.md +0 -147
- package/src/daemon/collectors/codex/collector.ts +0 -661
- package/src/daemon/collectors/codex/discovery.ts +0 -104
- package/src/daemon/collectors/codex/hooks.ts +0 -179
- package/src/daemon/collectors/codex/index.ts +0 -7
- package/src/daemon/collectors/codex/jsonl.ts +0 -108
- package/src/daemon/collectors/codex/transcript.ts +0 -299
- package/src/daemon/collectors/git.ts +0 -353
- package/src/daemon/collectors/herdr/collector.ts +0 -128
- package/src/daemon/collectors/herdr/index.ts +0 -2
- package/src/daemon/collectors/herdr/normalization.ts +0 -111
- package/src/daemon/collectors/opencode/README.md +0 -106
- package/src/daemon/collectors/opencode/collector.ts +0 -545
- package/src/daemon/collectors/opencode/discovery.ts +0 -257
- package/src/daemon/collectors/opencode/index.ts +0 -5
- package/src/daemon/collectors/opencode/session-builder.ts +0 -59
- package/src/daemon/collectors/opencode-db.ts +0 -294
- package/src/daemon/collectors/process.ts +0 -201
- package/src/daemon/collectors/tmux/README.md +0 -128
- package/src/daemon/collectors/tmux/collector.ts +0 -492
- package/src/daemon/collectors/tmux/enrichment.ts +0 -242
- package/src/daemon/collectors/tmux/index.ts +0 -4
- package/src/daemon/collectors/tmux/parse.ts +0 -128
- package/src/daemon/collectors/usage.ts +0 -259
- package/src/daemon/index.ts +0 -209
- package/src/daemon/scheduler.ts +0 -86
- package/src/daemon/store.ts +0 -630
- package/src/detect.ts +0 -143
- package/src/hooks/claude.ts +0 -596
- package/src/hooks/codex.ts +0 -517
- package/src/legacy/classifier.ts +0 -215
- package/src/legacy/detect.ts +0 -143
- package/src/mcp/client-server.ts +0 -21
- package/src/mcp/http-handler.ts +0 -126
- package/src/mcp/index.ts +0 -61
- package/src/mcp/resources/config-current.ts +0 -19
- package/src/mcp/resources/index.ts +0 -11
- package/src/mcp/resources/sessions-active.ts +0 -19
- package/src/mcp/resources/sessions-all.ts +0 -19
- package/src/mcp/schemas/sessions.ts +0 -3
- package/src/mcp/server.ts +0 -17
- package/src/mcp/services/api-adapter.ts +0 -48
- package/src/mcp/services/data-source.ts +0 -22
- package/src/mcp/services/store-adapter.ts +0 -40
- package/src/mcp/tools/get-active-sessions.ts +0 -16
- package/src/mcp/tools/get-session-children.ts +0 -24
- package/src/mcp/tools/get-session-messages.ts +0 -24
- package/src/mcp/tools/get-session.ts +0 -23
- package/src/mcp/tools/index.ts +0 -15
- package/src/mcp/tools/list-sessions.ts +0 -16
- package/src/oc/index.ts +0 -302
- package/src/service/manager.ts +0 -284
- package/src/shared/config.ts +0 -511
- package/src/shared/ipc.ts +0 -80
- package/src/shared/logger.ts +0 -248
- package/src/shared/paths.ts +0 -73
- package/src/shared/platform.ts +0 -110
- package/src/shared/session-exclusions.ts +0 -5
- package/src/shared/session-view.ts +0 -302
- package/src/shared/terminal-capabilities.ts +0 -130
- package/src/shared/types.ts +0 -330
- package/src/shared/utils.ts +0 -339
- package/src/shared/version.ts +0 -44
- package/src/tui/App.tsx +0 -240
- package/src/tui/client.ts +0 -163
- package/src/tui/components/BindingsModal.tsx +0 -82
- package/src/tui/components/DashboardHeader.tsx +0 -256
- package/src/tui/components/KeyBar.tsx +0 -67
- package/src/tui/components/SessionDetailModal.tsx +0 -169
- package/src/tui/components/SessionsView.tsx +0 -184
- package/src/tui/components/TabBar.tsx +0 -40
- package/src/tui/components/UsageSummaryPane.tsx +0 -272
- package/src/tui/components/UsageView.tsx +0 -242
- package/src/tui/components/tool-icons.ts +0 -24
- package/src/tui/index.tsx +0 -106
- package/src/tui/keybindings.ts +0 -49
- package/src/tui/model.ts +0 -295
- package/src/tui/state.ts +0 -77
- package/src/tui/theme.ts +0 -110
- package/src/types.ts +0 -113
- package/src/usage/README.md +0 -323
- package/src/usage/api.ts +0 -99
- package/src/usage/client.ts +0 -3
- package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
- package/src/usage/clients/anthropic-web-usage.ts +0 -329
- package/src/usage/clients/claude-cli-probe.ts +0 -513
- package/src/usage/clients/codex-app-server.ts +0 -354
- package/src/usage/clients/codex-oauth-usage.ts +0 -240
- package/src/usage/clients/opencode-go-api.ts +0 -100
- package/src/usage/clients/opencode-go-usage.ts +0 -577
- package/src/usage/errors.ts +0 -33
- package/src/usage/index.ts +0 -20
- package/src/usage/local/opencode-go-local.ts +0 -194
- package/src/usage/mappers.ts +0 -282
- package/src/usage/planner.ts +0 -118
- package/src/usage/providers/claude.ts +0 -233
- package/src/usage/providers/codex.ts +0 -115
- package/src/usage/providers/opencode-go.ts +0 -115
- package/src/usage/resolver.ts +0 -4
- package/src/usage/resolvers/browser-cookies.ts +0 -808
- package/src/usage/resolvers/claude-auth.ts +0 -208
- package/src/usage/resolvers/claude-cli.ts +0 -24
- package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
- package/src/usage/resolvers/claude-web.ts +0 -82
- package/src/usage/resolvers/codex-auth.ts +0 -174
- package/src/usage/resolvers/keychain-coordinator.ts +0 -397
- package/src/usage/resolvers/opencode-go-web.ts +0 -55
- package/src/usage/types.ts +0 -225
- package/src/usage/web-session-cache.ts +0 -37
- /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { UsageError } from "../errors";
|
|
3
|
-
|
|
4
|
-
export interface KeychainProcessResult {
|
|
5
|
-
stdout: string;
|
|
6
|
-
code: number | null;
|
|
7
|
-
killed: boolean;
|
|
8
|
-
signal: string | null;
|
|
9
|
-
spawnErrorCode?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type KeychainRun = (
|
|
13
|
-
file: string,
|
|
14
|
-
args: string[],
|
|
15
|
-
options: { timeoutMs: number; signal?: AbortSignal },
|
|
16
|
-
) => Promise<KeychainProcessResult>;
|
|
17
|
-
|
|
18
|
-
export interface KeychainCoordinatorOptions {
|
|
19
|
-
timeoutMs?: number;
|
|
20
|
-
cooldownMs?: number;
|
|
21
|
-
timeoutCooldownMs?: number;
|
|
22
|
-
now?: () => number;
|
|
23
|
-
run?: KeychainRun;
|
|
24
|
-
allowPrompts?: () => boolean;
|
|
25
|
-
onFailure?: (failure: KeychainFailure) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const USER_CANCELED_EXIT = 51;
|
|
29
|
-
const ACCESS_DENIED_EXIT = 45;
|
|
30
|
-
const ITEM_NOT_FOUND_EXIT = 44;
|
|
31
|
-
/** errSecInteractionNotAllowed: the process cannot prompt for authorization. */
|
|
32
|
-
const INTERACTION_NOT_ALLOWED_EXIT = 36;
|
|
33
|
-
|
|
34
|
-
export type KeychainFailureCode =
|
|
35
|
-
| "interaction-not-allowed"
|
|
36
|
-
| "access-denied"
|
|
37
|
-
| "user-canceled"
|
|
38
|
-
| "prompt-unanswered"
|
|
39
|
-
| "cooldown"
|
|
40
|
-
| "item-not-found"
|
|
41
|
-
| "spawn-failure"
|
|
42
|
-
| "read-failure";
|
|
43
|
-
|
|
44
|
-
export interface KeychainFailure {
|
|
45
|
-
code: KeychainFailureCode;
|
|
46
|
-
service: string;
|
|
47
|
-
account?: string;
|
|
48
|
-
at: number;
|
|
49
|
-
cooldownUntil?: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Safe Storage items whose cooperation decrypts Claude-side credential caches. */
|
|
53
|
-
export const CLAUDE_SAFE_STORAGE_ITEMS = [{ service: "Claude Safe Storage", account: "Claude" }] as const;
|
|
54
|
-
|
|
55
|
-
const FAILURE_READABLE: Record<KeychainFailureCode, string> = {
|
|
56
|
-
"interaction-not-allowed": "requires user interaction which is not allowed in this session",
|
|
57
|
-
"access-denied": "access was denied",
|
|
58
|
-
"user-canceled": "was canceled by the user",
|
|
59
|
-
"prompt-unanswered": "authorization prompt went unanswered",
|
|
60
|
-
cooldown: "is cooling down after a failed read",
|
|
61
|
-
"item-not-found": "does not exist",
|
|
62
|
-
"spawn-failure": "could not be launched",
|
|
63
|
-
"read-failure": "read failed",
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/** Long enough for a human to read and answer the macOS authorization dialog. */
|
|
67
|
-
const DEFAULT_PROMPT_TIMEOUT_MS = 90_000;
|
|
68
|
-
/** Suppresses prompt re-issuing after an unanswered dialog (e.g. user is away). */
|
|
69
|
-
const DEFAULT_TIMEOUT_COOLDOWN_MS = 10 * 60 * 1_000;
|
|
70
|
-
|
|
71
|
-
export const defaultRun: KeychainRun = (file, args, options) =>
|
|
72
|
-
new Promise((resolve) => {
|
|
73
|
-
execFile(
|
|
74
|
-
file,
|
|
75
|
-
args,
|
|
76
|
-
{
|
|
77
|
-
timeout: options.timeoutMs,
|
|
78
|
-
killSignal: "SIGKILL",
|
|
79
|
-
signal: options.signal,
|
|
80
|
-
encoding: "utf8",
|
|
81
|
-
},
|
|
82
|
-
(error, stdout) => {
|
|
83
|
-
// execFile reports success as a null error, so the exit code must be
|
|
84
|
-
// derived from the error only for non-zero exits; a null error means
|
|
85
|
-
// the child exited 0 with the secret on stdout.
|
|
86
|
-
const err = error as (Error & { code?: string | number; killed?: boolean; signal?: string }) | null;
|
|
87
|
-
if (!err) {
|
|
88
|
-
resolve({
|
|
89
|
-
stdout: typeof stdout === "string" ? stdout : "",
|
|
90
|
-
code: 0,
|
|
91
|
-
killed: false,
|
|
92
|
-
signal: null,
|
|
93
|
-
});
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
resolve({
|
|
97
|
-
stdout: typeof stdout === "string" ? stdout : "",
|
|
98
|
-
code: typeof err.code === "number" ? err.code : null,
|
|
99
|
-
killed: Boolean(err.killed),
|
|
100
|
-
signal: err.signal ?? null,
|
|
101
|
-
spawnErrorCode: typeof err.code === "string" ? err.code : undefined,
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
export class KeychainCoordinator {
|
|
108
|
-
private readonly timeoutMs: number;
|
|
109
|
-
private readonly cooldownMs: number;
|
|
110
|
-
private readonly timeoutCooldownMs: number;
|
|
111
|
-
private readonly now: () => number;
|
|
112
|
-
private readonly run: KeychainRun;
|
|
113
|
-
private allowPrompts: () => boolean;
|
|
114
|
-
private onFailure?: (failure: KeychainFailure) => void;
|
|
115
|
-
private readonly inFlight = new Map<string, Promise<string | null>>();
|
|
116
|
-
private readonly cooldownUntil = new Map<string, number>();
|
|
117
|
-
private readonly values = new Map<string, string>();
|
|
118
|
-
/** Most recent blocking failure per item (cleared on success). */
|
|
119
|
-
private readonly latestFailures = new Map<string, KeychainFailure>();
|
|
120
|
-
|
|
121
|
-
constructor(options: KeychainCoordinatorOptions = {}) {
|
|
122
|
-
this.timeoutMs = options.timeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
|
|
123
|
-
this.cooldownMs = options.cooldownMs ?? 6 * 60 * 60 * 1_000;
|
|
124
|
-
this.timeoutCooldownMs = options.timeoutCooldownMs ?? DEFAULT_TIMEOUT_COOLDOWN_MS;
|
|
125
|
-
this.now = options.now ?? Date.now;
|
|
126
|
-
this.run = options.run ?? defaultRun;
|
|
127
|
-
this.allowPrompts = options.allowPrompts ?? (() => true);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
configure(configureOptions: Pick<KeychainCoordinatorOptions, "allowPrompts" | "onFailure">): void {
|
|
131
|
-
if (configureOptions.allowPrompts) this.allowPrompts = configureOptions.allowPrompts;
|
|
132
|
-
if (configureOptions.onFailure !== undefined) this.onFailure = configureOptions.onFailure;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
isCoolingDown(service: string, account?: string): boolean {
|
|
136
|
-
const until = this.cooldownUntil.get(this.keyFor(service, account));
|
|
137
|
-
return until !== undefined && this.now() < until;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
clearCooldown(service: string, account?: string): void {
|
|
141
|
-
this.cooldownUntil.delete(this.keyFor(service, account));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Clears the memoized value for one item so the next read spawns a query again. */
|
|
145
|
-
clearMemo(service: string, account?: string): void {
|
|
146
|
-
const key = this.keyFor(service, account);
|
|
147
|
-
if (this.values.delete(key)) this.debugMemo("cleared", key);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Clears every memoized keychain value (daemon restart semantics, tests). */
|
|
151
|
-
clearAllMemo(): void {
|
|
152
|
-
this.values.clear();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
isMemoized(service: string, account?: string): boolean {
|
|
156
|
-
return this.values.has(this.keyFor(service, account));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
resetForTesting(): void {
|
|
160
|
-
this.inFlight.clear();
|
|
161
|
-
this.cooldownUntil.clear();
|
|
162
|
-
this.values.clear();
|
|
163
|
-
this.latestFailures.clear();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async read(service: string, account?: string, options?: { signal?: AbortSignal }): Promise<string | null> {
|
|
167
|
-
const key = this.keyFor(service, account);
|
|
168
|
-
// A cooldown established by a denied read wins over a memo from an earlier
|
|
169
|
-
// success: the item was later rejected by the user, so its value must not
|
|
170
|
-
// be served until the cooldown expires.
|
|
171
|
-
if (this.isCoolingDown(service, account)) {
|
|
172
|
-
const original = this.latestFailures.get(key);
|
|
173
|
-
this.recordFailure(
|
|
174
|
-
{
|
|
175
|
-
code: "cooldown",
|
|
176
|
-
service,
|
|
177
|
-
account,
|
|
178
|
-
at: this.now(),
|
|
179
|
-
cooldownUntil: this.cooldownUntil.get(key),
|
|
180
|
-
},
|
|
181
|
-
original,
|
|
182
|
-
);
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
const memoized = this.values.get(key);
|
|
186
|
-
if (memoized !== undefined) {
|
|
187
|
-
this.debugMemo("hit", key);
|
|
188
|
-
return memoized;
|
|
189
|
-
}
|
|
190
|
-
if (!this.allowPrompts()) return null;
|
|
191
|
-
|
|
192
|
-
const existing = this.inFlight.get(key);
|
|
193
|
-
if (existing) return this.joinWithSignal(existing, options?.signal);
|
|
194
|
-
|
|
195
|
-
const attempt = this.readOnce(service, account, key, options).finally(() => {
|
|
196
|
-
if (this.inFlight.get(key) === attempt) this.inFlight.delete(key);
|
|
197
|
-
});
|
|
198
|
-
this.inFlight.set(key, attempt);
|
|
199
|
-
return this.joinWithSignal(attempt, options?.signal);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private async readOnce(
|
|
203
|
-
service: string,
|
|
204
|
-
account: string | undefined,
|
|
205
|
-
key: string,
|
|
206
|
-
options?: { signal?: AbortSignal },
|
|
207
|
-
): Promise<string | null> {
|
|
208
|
-
if (options?.signal?.aborted) throw new UsageError("cancelled", "Keychain read cancelled");
|
|
209
|
-
|
|
210
|
-
const args = ["find-generic-password", "-s", service];
|
|
211
|
-
if (account) args.push("-a", account);
|
|
212
|
-
args.push("-w");
|
|
213
|
-
const result = await this.run("/usr/bin/security", args, {
|
|
214
|
-
timeoutMs: this.timeoutMs,
|
|
215
|
-
signal: options?.signal,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (options?.signal?.aborted) throw new UsageError("cancelled", "Keychain read cancelled");
|
|
219
|
-
|
|
220
|
-
if (result.code === 0 && result.stdout.trim()) {
|
|
221
|
-
this.cooldownUntil.delete(key);
|
|
222
|
-
this.latestFailures.delete(key);
|
|
223
|
-
const value = result.stdout.trim();
|
|
224
|
-
this.values.set(key, value);
|
|
225
|
-
this.debugMemo("stored", key);
|
|
226
|
-
return value;
|
|
227
|
-
}
|
|
228
|
-
if (result.code === USER_CANCELED_EXIT || result.code === ACCESS_DENIED_EXIT) {
|
|
229
|
-
this.cooldownUntil.set(key, this.now() + this.cooldownMs);
|
|
230
|
-
this.values.delete(key);
|
|
231
|
-
this.debugMemo("cleared", key);
|
|
232
|
-
this.recordFailure(
|
|
233
|
-
{
|
|
234
|
-
code: result.code === USER_CANCELED_EXIT ? "user-canceled" : "access-denied",
|
|
235
|
-
service,
|
|
236
|
-
account,
|
|
237
|
-
at: this.now(),
|
|
238
|
-
cooldownUntil: this.cooldownUntil.get(key),
|
|
239
|
-
},
|
|
240
|
-
);
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
// A timed-out (killed) read means the authorization dialog went unanswered:
|
|
244
|
-
// re-prompting every poll would hammer the user, so apply the short
|
|
245
|
-
// timeout cooldown instead of leaving the item unbounded.
|
|
246
|
-
if (result.killed) {
|
|
247
|
-
this.cooldownUntil.set(key, this.now() + this.timeoutCooldownMs);
|
|
248
|
-
this.values.delete(key);
|
|
249
|
-
this.recordFailure(
|
|
250
|
-
{
|
|
251
|
-
code: "prompt-unanswered",
|
|
252
|
-
service,
|
|
253
|
-
account,
|
|
254
|
-
at: this.now(),
|
|
255
|
-
cooldownUntil: this.cooldownUntil.get(key),
|
|
256
|
-
},
|
|
257
|
-
);
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
if (result.code === ITEM_NOT_FOUND_EXIT || result.spawnErrorCode !== undefined) {
|
|
261
|
-
// Item-not-found and spawn failures leave no user-visible trace, so
|
|
262
|
-
// without a cooldown the daemon would re-spawn security on every poll
|
|
263
|
-
// for an item that does not exist yet (created e.g. after a later login).
|
|
264
|
-
this.cooldownUntil.set(key, this.now() + this.timeoutCooldownMs);
|
|
265
|
-
this.values.delete(key);
|
|
266
|
-
this.debugReadFailure(key, result);
|
|
267
|
-
this.recordFailure(
|
|
268
|
-
{
|
|
269
|
-
code: result.spawnErrorCode !== undefined ? "spawn-failure" : "item-not-found",
|
|
270
|
-
service,
|
|
271
|
-
account,
|
|
272
|
-
at: this.now(),
|
|
273
|
-
cooldownUntil: this.cooldownUntil.get(key),
|
|
274
|
-
},
|
|
275
|
-
);
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
if (result.code === INTERACTION_NOT_ALLOWED_EXIT) {
|
|
279
|
-
this.recordFailure(
|
|
280
|
-
{
|
|
281
|
-
code: "interaction-not-allowed",
|
|
282
|
-
service,
|
|
283
|
-
account,
|
|
284
|
-
at: this.now(),
|
|
285
|
-
},
|
|
286
|
-
);
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
this.debugReadFailure(key, result);
|
|
290
|
-
this.recordFailure(
|
|
291
|
-
{
|
|
292
|
-
code: "read-failure",
|
|
293
|
-
service,
|
|
294
|
-
account,
|
|
295
|
-
at: this.now(),
|
|
296
|
-
},
|
|
297
|
-
);
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Describes the latest blocking failure of one keychain item (cleared after
|
|
303
|
-
* a successful read), if that failure is actionable. Benign states — the
|
|
304
|
-
* item does not exist at all — are not surfaced as diagnostics.
|
|
305
|
-
*/
|
|
306
|
-
describeService(service: string, account?: string): string | null {
|
|
307
|
-
const failure = this.latestFailures.get(this.keyFor(service, account));
|
|
308
|
-
if (!failure || failure.code === "item-not-found") return null;
|
|
309
|
-
return this.renderFailure(failure);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* First actionable failure among the given items, or null when none of them
|
|
314
|
-
* has a blocking failure (missing items and other benign states are ignored).
|
|
315
|
-
*/
|
|
316
|
-
describeBlockingFailure(
|
|
317
|
-
items: ReadonlyArray<{ service: string; account?: string }>,
|
|
318
|
-
): string | null {
|
|
319
|
-
for (const { service, account } of items) {
|
|
320
|
-
const rendered = this.describeService(service, account);
|
|
321
|
-
if (rendered) return rendered;
|
|
322
|
-
}
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** Renders a failure without any credential material. */
|
|
327
|
-
private renderFailure(failure: KeychainFailure): string {
|
|
328
|
-
let text = `Keychain item "${failure.service}" ${FAILURE_READABLE[failure.code]}`;
|
|
329
|
-
if (failure.code === "interaction-not-allowed") {
|
|
330
|
-
text +=
|
|
331
|
-
" — the daemon is running in a non-interactive launch session (background session, pane multiplexer, or service). Start the daemon from a GUI terminal session, or add the daemon binary to the item's access list in Keychain Access";
|
|
332
|
-
}
|
|
333
|
-
if (failure.cooldownUntil) {
|
|
334
|
-
text += ` (cooling down until ${new Date(failure.cooldownUntil).toISOString()})`;
|
|
335
|
-
}
|
|
336
|
-
return text;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private recordFailure(failure: KeychainFailure, baseForCooldown?: KeychainFailure): void {
|
|
340
|
-
const key = this.keyFor(failure.service, failure.account);
|
|
341
|
-
// When a cooldown gate re-reports, carry the original blocking cause so
|
|
342
|
-
// diagnostics name the root failure, not just the cooldown.
|
|
343
|
-
const recorded: KeychainFailure = baseForCooldown && failure.code === "cooldown"
|
|
344
|
-
? { ...baseForCooldown, at: failure.at, cooldownUntil: failure.cooldownUntil }
|
|
345
|
-
: failure;
|
|
346
|
-
if (recorded.code === "cooldown") {
|
|
347
|
-
recorded.code = baseForCooldown?.code ?? "cooldown";
|
|
348
|
-
}
|
|
349
|
-
this.latestFailures.set(key, recorded);
|
|
350
|
-
this.onFailure?.(recorded);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private debugReadFailure(key: string, result: KeychainProcessResult): void {
|
|
354
|
-
if (process.env.SCRYLOG_DEBUG_AUTH) {
|
|
355
|
-
const [service, account] = key.split("\u0000");
|
|
356
|
-
process.stderr.write(
|
|
357
|
-
`[scrylog:keychain] read failed: ${service}${account ? ` (account: ${account})` : ""}` +
|
|
358
|
-
` code=${result.code} killed=${result.killed}` +
|
|
359
|
-
`${result.spawnErrorCode ? ` spawnError=${result.spawnErrorCode}` : ""}\n`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
private debugMemo(action: "stored" | "hit" | "cleared", key: string): void {
|
|
365
|
-
if (process.env.SCRYLOG_DEBUG_AUTH) {
|
|
366
|
-
const [service, account] = key.split("\u0000");
|
|
367
|
-
process.stderr.write(
|
|
368
|
-
`[scrylog:keychain] memo ${action}: ${service}${account ? ` (account: ${account})` : ""}\n`,
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
private joinWithSignal(promise: Promise<string | null>, signal?: AbortSignal): Promise<string | null> {
|
|
374
|
-
if (!signal) return promise;
|
|
375
|
-
if (signal.aborted) return Promise.reject(new UsageError("cancelled", "Keychain read cancelled"));
|
|
376
|
-
return new Promise<string | null>((resolve, reject) => {
|
|
377
|
-
const onAbort = () => reject(new UsageError("cancelled", "Keychain read cancelled"));
|
|
378
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
379
|
-
promise.then(
|
|
380
|
-
(value) => {
|
|
381
|
-
signal.removeEventListener("abort", onAbort);
|
|
382
|
-
resolve(value);
|
|
383
|
-
},
|
|
384
|
-
(error) => {
|
|
385
|
-
signal.removeEventListener("abort", onAbort);
|
|
386
|
-
reject(error);
|
|
387
|
-
},
|
|
388
|
-
);
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private keyFor(service: string, account?: string): string {
|
|
393
|
-
return `${service}\u0000${account ?? ""}`;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export const keychainCoordinator = new KeychainCoordinator();
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { TokenResolver } from "../resolver";
|
|
2
|
-
import { BrowserCookieSourceCursor, buildCookieHeader } from "./browser-cookies";
|
|
3
|
-
|
|
4
|
-
export interface OpenCodeGoWebCandidate {
|
|
5
|
-
source: string;
|
|
6
|
-
cookies: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface OpenCodeGoWebCredentials {
|
|
10
|
-
candidates: OpenCodeGoWebCandidate[];
|
|
11
|
-
/** Demand-driven cursor advancement after a classified auth rejection. */
|
|
12
|
-
importNext?: () => Promise<OpenCodeGoWebCandidate | null>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const OPENCODE_GO_EXCLUDED_BROWSERS: ReadonlySet<string> = new Set(["claude-desktop"]);
|
|
16
|
-
|
|
17
|
-
export class OpenCodeGoWebResolver implements TokenResolver<OpenCodeGoWebCredentials> {
|
|
18
|
-
constructor(
|
|
19
|
-
private readonly cursorFactory: () => BrowserCookieSourceCursor = () =>
|
|
20
|
-
new BrowserCookieSourceCursor("opencode.ai", ["auth", "__Host-auth", "__Host-console_session"], {
|
|
21
|
-
excludeBrowsers: OPENCODE_GO_EXCLUDED_BROWSERS,
|
|
22
|
-
}),
|
|
23
|
-
) {}
|
|
24
|
-
|
|
25
|
-
resolve(): Promise<OpenCodeGoWebCredentials | null> {
|
|
26
|
-
return Promise.resolve().then(() => this.buildCredentials());
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private async buildCredentials(): Promise<OpenCodeGoWebCredentials | null> {
|
|
30
|
-
const cursor = this.cursorFactory();
|
|
31
|
-
const first = await cursor.next();
|
|
32
|
-
if (!first) return null;
|
|
33
|
-
const firstCandidate: OpenCodeGoWebCandidate = {
|
|
34
|
-
source: `${first.browser}:${first.profile}:${first.database}`,
|
|
35
|
-
cookies: buildCookieHeader(first.cookies),
|
|
36
|
-
};
|
|
37
|
-
if (!firstCandidate.cookies) return null;
|
|
38
|
-
return {
|
|
39
|
-
candidates: [firstCandidate],
|
|
40
|
-
importNext: async () => {
|
|
41
|
-
const next = await cursor.next();
|
|
42
|
-
if (!next) return null;
|
|
43
|
-
const candidate: OpenCodeGoWebCandidate = {
|
|
44
|
-
source: `${next.browser}:${next.profile}:${next.database}`,
|
|
45
|
-
cookies: buildCookieHeader(next.cookies),
|
|
46
|
-
};
|
|
47
|
-
return candidate.cookies ? candidate : null;
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async isAvailable(): Promise<boolean> {
|
|
53
|
-
return (await this.resolve()) !== null;
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/usage/types.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// Standardized unified types for usage limits — all providers share the same structure
|
|
2
|
-
|
|
3
|
-
export interface UsageWindow {
|
|
4
|
-
/** Machine-readable identifier, e.g. "session", "weekly", "monthly", "designs" */
|
|
5
|
-
id: string;
|
|
6
|
-
/** Human-readable label, e.g. "Current Session", "Weekly", "Monthly" */
|
|
7
|
-
title: string;
|
|
8
|
-
/** Percentage used (0-100) */
|
|
9
|
-
usedPercent: number;
|
|
10
|
-
/** Absolute usage count (requests, tokens, credits) — optional */
|
|
11
|
-
used?: number;
|
|
12
|
-
/** Absolute limit — optional */
|
|
13
|
-
limit?: number;
|
|
14
|
-
/** Window duration in minutes — optional */
|
|
15
|
-
windowMinutes?: number;
|
|
16
|
-
/** Reset timestamp (ISO 8601) — preferred */
|
|
17
|
-
resetsAt?: string | null;
|
|
18
|
-
/** Seconds until reset — computed if not provided by provider */
|
|
19
|
-
resetInSec?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ProviderCostSnapshot {
|
|
23
|
-
/** Amount used */
|
|
24
|
-
used: number;
|
|
25
|
-
/** Limit amount */
|
|
26
|
-
limit: number;
|
|
27
|
-
/** Currency code, e.g. "USD" */
|
|
28
|
-
currency: string;
|
|
29
|
-
/** Billing period label, e.g. "Monthly" */
|
|
30
|
-
period: string;
|
|
31
|
-
/** Remaining prepaid balance, independent from used/limit */
|
|
32
|
-
balance?: number;
|
|
33
|
-
/** Reset timestamp (ISO 8601) */
|
|
34
|
-
resetsAt?: string | null;
|
|
35
|
-
/** Last update timestamp (ISO 8601) */
|
|
36
|
-
updatedAt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface UsageProviderEntry {
|
|
40
|
-
/** Provider identifier, e.g. "claude", "opencode-go" */
|
|
41
|
-
provider: string;
|
|
42
|
-
|
|
43
|
-
/** Account identifier — stable across hosts for grouping. null only when provider is unavailable or identity could not be determined. */
|
|
44
|
-
accountId: string | null;
|
|
45
|
-
|
|
46
|
-
/** Whether this provider is available on this machine (credentials detected) */
|
|
47
|
-
available: boolean;
|
|
48
|
-
|
|
49
|
-
/** Status of this provider */
|
|
50
|
-
status: "ok" | "error" | "unavailable" | "pending";
|
|
51
|
-
|
|
52
|
-
/** Metadata about the data source and account */
|
|
53
|
-
metadata?: {
|
|
54
|
-
/** How the data was obtained, e.g. "oauth", "web", "cli" */
|
|
55
|
-
dataSource?: string;
|
|
56
|
-
/** Account email if known */
|
|
57
|
-
accountEmail?: string | null;
|
|
58
|
-
/** Organization/account name if known */
|
|
59
|
-
accountOrganization?: string | null;
|
|
60
|
-
/** Subscription type, e.g. "pro", "free" */
|
|
61
|
-
subscriptionType?: string | null;
|
|
62
|
-
/** Rate limit tier */
|
|
63
|
-
rateLimitTier?: string | null;
|
|
64
|
-
/** Workspace ID (OpenCode Go) */
|
|
65
|
-
workspaceId?: string | null;
|
|
66
|
-
/** Workspace name (OpenCode Go) */
|
|
67
|
-
workspaceName?: string | null;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/** Errors specific to this provider (only present when status === "error") */
|
|
71
|
-
errors?: string[];
|
|
72
|
-
|
|
73
|
-
/** Usage windows — standardized for all providers */
|
|
74
|
-
windows?: UsageWindow[];
|
|
75
|
-
|
|
76
|
-
/** Cost/overage snapshot if available */
|
|
77
|
-
cost?: ProviderCostSnapshot | null;
|
|
78
|
-
|
|
79
|
-
/** Last update timestamp (ISO 8601) */
|
|
80
|
-
updatedAt?: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface UsageState {
|
|
84
|
-
/** All configured providers, including unavailable ones */
|
|
85
|
-
providers: UsageProviderEntry[];
|
|
86
|
-
/** Global refresh timestamp */
|
|
87
|
-
lastRefreshedAt: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Legacy provider-specific snapshots (internal use only, mapped to UsageProviderEntry)
|
|
91
|
-
|
|
92
|
-
export interface ClaudeUsageSnapshot {
|
|
93
|
-
source: "oauth" | "web" | "cli";
|
|
94
|
-
primary: RateWindowLegacy;
|
|
95
|
-
secondary: RateWindowLegacy | null;
|
|
96
|
-
opus: RateWindowLegacy | null;
|
|
97
|
-
extraRateWindows: NamedRateWindowLegacy[];
|
|
98
|
-
providerCost: ProviderCostSnapshotLegacy | null;
|
|
99
|
-
updatedAt: Date;
|
|
100
|
-
accountEmail: string | null;
|
|
101
|
-
accountOrganization: string | null;
|
|
102
|
-
/** Organization UUID when the source exposes one (Web membership, OAuth profile). */
|
|
103
|
-
accountOrganizationId: string | null;
|
|
104
|
-
loginMethod: string | null;
|
|
105
|
-
subscriptionType: string | null;
|
|
106
|
-
rateLimitTier: string | null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface OpenCodeGoUsageSnapshot {
|
|
110
|
-
source: "web" | "local" | "api";
|
|
111
|
-
hasMonthlyUsage: boolean;
|
|
112
|
-
rollingUsagePercent: number;
|
|
113
|
-
weeklyUsagePercent: number;
|
|
114
|
-
monthlyUsagePercent: number;
|
|
115
|
-
rollingResetInSec: number;
|
|
116
|
-
weeklyResetInSec: number;
|
|
117
|
-
monthlyResetInSec: number;
|
|
118
|
-
updatedAt: Date;
|
|
119
|
-
workspaceId: string | null;
|
|
120
|
-
workspaceName: string | null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Legacy internal types (used by providers before mapping to unified model)
|
|
124
|
-
|
|
125
|
-
export interface RateWindowLegacy {
|
|
126
|
-
usedPercent: number;
|
|
127
|
-
windowMinutes: number;
|
|
128
|
-
resetsAt: Date | null;
|
|
129
|
-
resetDescription: string | null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface NamedRateWindowLegacy {
|
|
133
|
-
id: string;
|
|
134
|
-
title: string;
|
|
135
|
-
window: RateWindowLegacy;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface ProviderCostSnapshotLegacy {
|
|
139
|
-
used: number;
|
|
140
|
-
limit: number;
|
|
141
|
-
currencyCode: string;
|
|
142
|
-
period: string;
|
|
143
|
-
resetsAt: Date | null;
|
|
144
|
-
updatedAt: Date;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Credential types
|
|
148
|
-
|
|
149
|
-
export interface ClaudeOAuthCredentials {
|
|
150
|
-
accessToken: string;
|
|
151
|
-
refreshToken?: string;
|
|
152
|
-
expiresAt?: string;
|
|
153
|
-
/** Internal credential authority; never exposed through the usage API. */
|
|
154
|
-
owner?: "claude" | "environment";
|
|
155
|
-
/** Internal source identity used to preserve owner recovery rules. */
|
|
156
|
-
credentialSource?: "keychain" | "credentials-file" | "claude-desktop" | "environment";
|
|
157
|
-
isStale?: boolean;
|
|
158
|
-
scopes?: string[];
|
|
159
|
-
rateLimitTier?: string;
|
|
160
|
-
subscriptionType?: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface CodexOAuthCredentials {
|
|
164
|
-
accessToken: string;
|
|
165
|
-
refreshToken?: string;
|
|
166
|
-
expiresAt?: string;
|
|
167
|
-
tokenExpiresAt?: number;
|
|
168
|
-
lastRefreshAt?: number;
|
|
169
|
-
accountEmail?: string | null;
|
|
170
|
-
accountId?: string | null;
|
|
171
|
-
chatgptBaseUrl?: string;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface ClaudeWebSession {
|
|
175
|
-
cookieHeader: string;
|
|
176
|
-
/** Ordered, isolated browser/profile candidates for one bounded recovery attempt. */
|
|
177
|
-
cookieCandidates?: string[];
|
|
178
|
-
/** Revision observed in the daemon-lifetime in-memory cache. */
|
|
179
|
-
cacheRevision?: number;
|
|
180
|
-
/**
|
|
181
|
-
* Demand-driven cursor advancement: imports the next browser source only
|
|
182
|
-
* after a classified authentication rejection. Never called during planning.
|
|
183
|
-
*/
|
|
184
|
-
importNext?: () => Promise<string | null>;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export type ClaudeDataSource = "oauth" | "web" | "cli" | "auto";
|
|
188
|
-
|
|
189
|
-
export interface ClaudePlannerDecision {
|
|
190
|
-
primary: ClaudeDataSource;
|
|
191
|
-
fallback: ClaudeDataSource[];
|
|
192
|
-
webExtras: boolean;
|
|
193
|
-
routes: ClaudePlannedRoute[];
|
|
194
|
-
webExtrasCredentials?: ClaudeWebSession;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export type ClaudePlannedRoute =
|
|
198
|
-
| { type: "oauth"; credentials: ClaudeOAuthCredentials }
|
|
199
|
-
| { type: "web"; credentials: ClaudeWebSession }
|
|
200
|
-
| { type: "cli"; credentials: string };
|
|
201
|
-
|
|
202
|
-
export type CodexDataSource = "oauth" | "auto";
|
|
203
|
-
|
|
204
|
-
export interface CodexUsageSnapshot {
|
|
205
|
-
source: "oauth" | "cli";
|
|
206
|
-
planType: string | null;
|
|
207
|
-
primaryWindow: RateWindowLegacy | null;
|
|
208
|
-
secondaryWindow: RateWindowLegacy | null;
|
|
209
|
-
credits: {
|
|
210
|
-
hasCredits: boolean;
|
|
211
|
-
unlimited: boolean;
|
|
212
|
-
balance: number | null;
|
|
213
|
-
} | null;
|
|
214
|
-
/** Monthly extra-usage credit cap when exposed by Codex. */
|
|
215
|
-
individualLimit?: {
|
|
216
|
-
used: number;
|
|
217
|
-
limit: number;
|
|
218
|
-
resetsAt: Date | null;
|
|
219
|
-
} | null;
|
|
220
|
-
/** Whether the OAuth response announced managed spend controls. */
|
|
221
|
-
spendControlPresent?: boolean;
|
|
222
|
-
updatedAt: Date;
|
|
223
|
-
accountEmail: string | null;
|
|
224
|
-
accountId: string | null;
|
|
225
|
-
}
|