@scrylog/cli 0.1.4 → 0.3.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.
Files changed (97) hide show
  1. package/README.md +46 -7
  2. package/package.json +13 -10
  3. package/src/api/docs/sessions.ts +3 -2
  4. package/src/api/docs/system.ts +3 -0
  5. package/src/api/docs/usage.ts +2 -2
  6. package/src/api/routes/sessions.ts +6 -2
  7. package/src/api/routes/system.ts +4 -1
  8. package/src/api/routes/webhooks/claude.ts +9 -21
  9. package/src/api/routes/webhooks/opencode.ts +6 -1
  10. package/src/api/routes.ts +1 -1
  11. package/src/api/schemas/common.ts +8 -0
  12. package/src/api/schemas/sessions.ts +15 -1
  13. package/src/api/schemas/usage.ts +1 -0
  14. package/src/cli/commands.ts +170 -26
  15. package/src/cli/index.ts +18 -0
  16. package/src/cli/program.ts +175 -0
  17. package/src/daemon/collectors/claude/collector.ts +11 -1
  18. package/src/daemon/collectors/claude/discovery.ts +2 -23
  19. package/src/daemon/collectors/codex/collector.ts +3 -3
  20. package/src/daemon/collectors/git.ts +60 -2
  21. package/src/daemon/collectors/herdr/collector.ts +128 -0
  22. package/src/daemon/collectors/herdr/index.ts +2 -0
  23. package/src/daemon/collectors/herdr/normalization.ts +111 -0
  24. package/src/daemon/collectors/opencode/collector.ts +4 -2
  25. package/src/daemon/collectors/opencode-db.ts +17 -5
  26. package/src/daemon/collectors/tmux/collector.ts +21 -1
  27. package/src/daemon/collectors/tmux/enrichment.ts +1 -0
  28. package/src/daemon/collectors/usage.ts +115 -49
  29. package/src/daemon/index.ts +2 -0
  30. package/src/daemon/store.ts +109 -14
  31. package/src/postinstall.cjs +11 -0
  32. package/src/shared/config.ts +33 -0
  33. package/src/shared/session-exclusions.ts +5 -0
  34. package/src/shared/session-view.ts +67 -6
  35. package/src/shared/terminal-capabilities.ts +130 -0
  36. package/src/shared/types.ts +33 -2
  37. package/src/shared/version.ts +44 -0
  38. package/src/tui/App.tsx +240 -0
  39. package/src/tui/assets/providers/SOURCES.md +21 -0
  40. package/src/tui/assets/providers/claude-terminal.png +0 -0
  41. package/src/tui/assets/providers/claude-terminal.svg +62 -0
  42. package/src/tui/assets/providers/claude.png +0 -0
  43. package/src/tui/assets/providers/copilot.png +0 -0
  44. package/src/tui/assets/providers/copilot.svg +44 -0
  45. package/src/tui/assets/providers/openai-terminal.png +0 -0
  46. package/src/tui/assets/providers/openai-terminal.svg +62 -0
  47. package/src/tui/assets/providers/openai.png +0 -0
  48. package/src/tui/assets/providers/opencode-terminal.png +0 -0
  49. package/src/tui/assets/providers/opencode-terminal.svg +62 -0
  50. package/src/tui/assets/providers/opencode.png +0 -0
  51. package/src/tui/assets/providers/shell.png +0 -0
  52. package/src/tui/assets/providers/shell.svg +23 -0
  53. package/src/tui/assets/providers/unknown.png +0 -0
  54. package/src/tui/assets/providers/unknown.svg +23 -0
  55. package/src/tui/client.ts +22 -8
  56. package/src/tui/components/BindingsModal.tsx +82 -0
  57. package/src/tui/components/DashboardHeader.tsx +256 -0
  58. package/src/tui/components/KeyBar.tsx +67 -0
  59. package/src/tui/components/SessionDetailModal.tsx +169 -0
  60. package/src/tui/components/SessionsView.tsx +184 -0
  61. package/src/tui/components/TabBar.tsx +40 -0
  62. package/src/tui/components/UsageSummaryPane.tsx +272 -0
  63. package/src/tui/components/UsageView.tsx +242 -0
  64. package/src/tui/components/tool-icons.ts +24 -0
  65. package/src/tui/index.tsx +106 -0
  66. package/src/tui/keybindings.ts +49 -0
  67. package/src/tui/model.ts +295 -0
  68. package/src/tui/state.ts +77 -0
  69. package/src/tui/theme.ts +110 -0
  70. package/src/usage/README.md +3 -3
  71. package/src/usage/clients/anthropic-oauth-usage.ts +100 -20
  72. package/src/usage/clients/anthropic-web-usage.ts +133 -49
  73. package/src/usage/clients/claude-cli-probe.ts +458 -132
  74. package/src/usage/clients/codex-app-server.ts +354 -0
  75. package/src/usage/clients/codex-oauth-usage.ts +143 -16
  76. package/src/usage/clients/opencode-go-api.ts +100 -0
  77. package/src/usage/clients/opencode-go-usage.ts +163 -27
  78. package/src/usage/errors.ts +33 -0
  79. package/src/usage/index.ts +2 -1
  80. package/src/usage/local/opencode-go-local.ts +194 -0
  81. package/src/usage/mappers.ts +24 -15
  82. package/src/usage/planner.ts +89 -25
  83. package/src/usage/providers/claude.ts +203 -83
  84. package/src/usage/providers/codex.ts +92 -11
  85. package/src/usage/providers/opencode-go.ts +94 -16
  86. package/src/usage/resolvers/browser-cookies.ts +679 -69
  87. package/src/usage/resolvers/claude-auth.ts +150 -41
  88. package/src/usage/resolvers/claude-desktop-auth.ts +170 -0
  89. package/src/usage/resolvers/claude-web.ts +72 -6
  90. package/src/usage/resolvers/codex-auth.ts +74 -17
  91. package/src/usage/resolvers/keychain-coordinator.ts +397 -0
  92. package/src/usage/resolvers/opencode-go-web.ts +47 -6
  93. package/src/usage/types.ts +37 -2
  94. package/src/usage/web-session-cache.ts +37 -0
  95. package/src/neo-blessed.d.ts +0 -4
  96. package/src/tui/app.ts +0 -920
  97. package/src/tui/index.ts +0 -193
@@ -3,35 +3,113 @@ import type { OpenCodeGoUsageSnapshot } from "../types";
3
3
  import type { UsageProviderEntry } from "../types";
4
4
  import { OpenCodeGoUsageClient } from "../clients/opencode-go-usage";
5
5
  import { mapOpenCodeGoToEntry } from "../mappers";
6
+ import { classifyUsageError, isRecoverableAuthenticationError } from "../errors";
7
+ import { usageWebSessionCache, type WebSessionCache } from "../web-session-cache";
8
+ import { OpenCodeGoApiKeyUsageClient } from "../clients/opencode-go-api";
9
+ import { OpenCodeGoLocalUsageReader, type OpenCodeGoLocalUsageFetcher } from "../local/opencode-go-local";
10
+ import type { OpenCodeGoWebCredentials } from "../resolvers/opencode-go-web";
6
11
 
7
- export class OpenCodeGoUsageProvider {
8
- private readonly client = new OpenCodeGoUsageClient();
12
+ export interface OpenCodeGoApiKeyUsageClientLike {
13
+ fetch(): Promise<OpenCodeGoUsageSnapshot | null>;
14
+ }
9
15
 
16
+ const CACHE_KEY = "opencode-go";
17
+
18
+ export class OpenCodeGoUsageProvider {
10
19
  constructor(
11
- private readonly resolver: TokenResolver<{ cookies: string }>,
20
+ private readonly resolver: TokenResolver<OpenCodeGoWebCredentials>,
12
21
  private readonly serverFnId: string,
22
+ private readonly client = new OpenCodeGoUsageClient(),
23
+ private readonly cache: WebSessionCache = usageWebSessionCache,
24
+ private readonly localReader: OpenCodeGoLocalUsageFetcher = new OpenCodeGoLocalUsageReader(),
25
+ private readonly apiKeyClient: OpenCodeGoApiKeyUsageClientLike = new OpenCodeGoApiKeyUsageClient(),
13
26
  ) {}
14
27
 
15
28
  async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
16
- const errors: string[] = [];
17
- const available = await this.resolver.isAvailable();
18
- if (!available) {
19
- return { entry: mapOpenCodeGoToEntry(null, [], false), snapshot: null };
29
+ // Official Go usage API is authoritative and independent of browser cookies.
30
+ try {
31
+ const apiSnapshot = await this.apiKeyClient.fetch();
32
+ if (apiSnapshot) {
33
+ return { entry: mapOpenCodeGoToEntry(apiSnapshot, [], true), snapshot: apiSnapshot };
34
+ }
35
+ } catch {
36
+ // Fall through to cookie-based collection.
20
37
  }
38
+ return this.fetchWithCookies(options);
39
+ }
21
40
 
41
+ private async fetchWithCookies(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
42
+ const errors: string[] = [];
22
43
  const creds = await this.resolver.resolve();
23
- if (!creds) {
24
- // Should not happen if isAvailable() is correct
44
+ const observed = this.cache.observe(CACHE_KEY);
45
+ if (!observed.value && !creds?.candidates.length) {
25
46
  return { entry: mapOpenCodeGoToEntry(null, [], false), snapshot: null };
26
47
  }
27
48
 
28
- try {
29
- const snapshot = await this.client.fetch({ cookies: creds.cookies, serverFnId: this.serverFnId }, options);
30
- return { entry: mapOpenCodeGoToEntry(snapshot, errors, true), snapshot };
31
- } catch (err) {
32
- const message = err instanceof Error ? err.message : String(err);
33
- errors.push(`OpenCode Go: ${message}`);
34
- return { entry: mapOpenCodeGoToEntry(null, errors, true), snapshot: null };
49
+ const attempts = [
50
+ ...(observed.value ? [{ cookies: observed.value, cached: true }] : []),
51
+ ...(creds?.candidates ?? [])
52
+ .filter((candidate) => candidate.cookies !== observed.value)
53
+ .map((candidate) => ({ cookies: candidate.cookies, cached: false })),
54
+ ].slice(0, 2);
55
+ let expectedRevision = observed.revision;
56
+
57
+ for (let index = 0; index < attempts.length; index++) {
58
+ const attempt = attempts[index]!;
59
+ let rotated = false;
60
+ try {
61
+ const snapshot = await this.client.fetch(
62
+ {
63
+ cookies: attempt.cookies,
64
+ serverFnId: this.serverFnId,
65
+ onCookieRotation: (cookies) => {
66
+ rotated = true;
67
+ const stored = this.cache.store(CACHE_KEY, cookies, expectedRevision);
68
+ if (stored) expectedRevision = stored.revision;
69
+ },
70
+ },
71
+ options,
72
+ );
73
+ if (!attempt.cached && !rotated && this.cache.observe(CACHE_KEY).revision === expectedRevision) {
74
+ const stored = this.cache.store(CACHE_KEY, attempt.cookies, expectedRevision);
75
+ if (stored) expectedRevision = stored.revision;
76
+ }
77
+ return { entry: mapOpenCodeGoToEntry(snapshot, [], true), snapshot };
78
+ } catch (error) {
79
+ const failure = classifyUsageError(error);
80
+ if (failure.kind === "cancelled") throw failure;
81
+ if (failure.kind === "invalid-credentials") {
82
+ if (this.cache.clear(CACHE_KEY, expectedRevision)) expectedRevision = 0;
83
+ // Advance the demand-driven cursor at most once per attempt, keeping
84
+ // the two-candidate bound across cached and imported candidates.
85
+ const imported = attempts.length < 2 ? (await creds?.importNext?.()) ?? null : null;
86
+ if (imported && !attempts.some((candidate) => candidate.cookies === imported.cookies)) {
87
+ attempts.push({ cookies: imported.cookies, cached: false });
88
+ continue;
89
+ }
90
+ }
91
+ if (!(isRecoverableAuthenticationError(failure) && index + 1 < attempts.length)) {
92
+ errors.push(`OpenCode Go: ${failure.message}`);
93
+ return this.fetchLocalFallback(errors);
94
+ }
95
+ }
96
+ }
97
+
98
+ return this.fetchLocalFallback(errors);
99
+ }
100
+
101
+ /**
102
+ * Last resort after every web attempt fails: report locally accumulated
103
+ * opencode-go costs as estimated windows instead of an error. Mirrors the
104
+ * CodexBar "local" source so a stale browser session stays usable.
105
+ */
106
+ private async fetchLocalFallback(errors: string[]): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
107
+ const local = await this.localReader.fetch();
108
+ if (local) {
109
+ // Keeping errors here would force status "error" in the mapper; the local
110
+ // snapshot is reported as a usable estimate instead.
111
+ return { entry: mapOpenCodeGoToEntry(local, [], true), snapshot: local };
35
112
  }
113
+ return { entry: mapOpenCodeGoToEntry(null, errors, true), snapshot: null };
36
114
  }
37
115
  }