clauderipple 0.9.4 → 0.10.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 (118) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +31 -0
  3. package/dist/app/package.json +1 -1
  4. package/dist/cli/src/hooks/agent-title.js +7 -4
  5. package/dist/cli/src/index.js +43 -0
  6. package/dist/cli/src/remote.js +263 -0
  7. package/dist/cli/src/schtasks.js +48 -40
  8. package/dist/cli/src/settings.js +2 -1
  9. package/dist/mod/hooks/i18n.ts +252 -6
  10. package/dist/mod/hooks/judge.ts +35 -17
  11. package/dist/mod/hooks/modes.ts +56 -1
  12. package/dist/mod/hooks/register.tsx +238 -32
  13. package/dist/mod/hooks/session.ts +98 -1
  14. package/dist/mod/hooks/settings.js +7 -3
  15. package/dist/mod/hooks/settings.ts +9 -4
  16. package/dist/mod/hooks/stats.ts +25 -1
  17. package/dist/mod/hooks/ui/agents.tsx +858 -79
  18. package/dist/mod/hooks/ui/alerts.tsx +20 -17
  19. package/dist/mod/hooks/ui/art.ts +33 -7
  20. package/dist/mod/hooks/ui/band.tsx +262 -81
  21. package/dist/mod/hooks/ui/flat.ts +265 -0
  22. package/dist/mod/hooks/ui/kit.tsx +70 -55
  23. package/dist/mod/hooks/ui/look.ts +129 -0
  24. package/dist/mod/hooks/ui/settings.tsx +71 -46
  25. package/dist/mod/hooks/ui/setup.tsx +15 -13
  26. package/dist/mod/hooks/ui/theme.ts +3 -0
  27. package/dist/mod/hooks/ui/tools.ts +124 -0
  28. package/dist/mod/hooks/ui/view.ts +116 -3
  29. package/dist/mod/hooks/workers.ts +33 -6
  30. package/dist/mod/tests/register.test.tsx +155 -11
  31. package/dist/mod/tests/ui.test.tsx +375 -27
  32. package/dist/mod/types/index.d.ts +68 -2
  33. package/dist/remote/package.json +28 -0
  34. package/dist/remote/src/away.js +85 -0
  35. package/dist/remote/src/bridge-client.js +295 -0
  36. package/dist/remote/src/composer-menu.js +11 -0
  37. package/dist/remote/src/config.js +102 -0
  38. package/dist/remote/src/desktop.js +1018 -0
  39. package/dist/remote/src/e2ee.js +126 -0
  40. package/dist/remote/src/e2ee.ts +126 -0
  41. package/dist/remote/src/eventlog.js +298 -0
  42. package/dist/remote/src/expo-push.js +127 -0
  43. package/dist/remote/src/folders.js +79 -0
  44. package/dist/remote/src/fsutil.js +39 -0
  45. package/dist/remote/src/handoff.js +38 -0
  46. package/dist/remote/src/host-service.js +168 -0
  47. package/dist/remote/src/images.js +215 -0
  48. package/dist/remote/src/local-control.js +138 -0
  49. package/dist/remote/src/main.js +264 -0
  50. package/dist/remote/src/permissions.js +124 -0
  51. package/dist/remote/src/power.js +76 -0
  52. package/dist/remote/src/processes.js +24 -0
  53. package/dist/remote/src/protocol.js +32 -0
  54. package/dist/remote/src/protocol.ts +284 -0
  55. package/dist/remote/src/push-content.js +32 -0
  56. package/dist/remote/src/push.js +134 -0
  57. package/dist/remote/src/receipts.js +111 -0
  58. package/dist/remote/src/relay.js +438 -0
  59. package/dist/remote/src/router.js +208 -0
  60. package/dist/remote/src/sdk-launcher.js +18 -0
  61. package/dist/remote/src/server.js +706 -0
  62. package/dist/remote/src/service.js +157 -0
  63. package/dist/remote/src/sessions.js +1800 -0
  64. package/dist/remote/src/sidebar-storage.js +211 -0
  65. package/dist/remote/src/sidebar.js +173 -0
  66. package/dist/remote/src/tool-details.js +205 -0
  67. package/dist/remote/src/tool-details.ts +153 -0
  68. package/dist/remote/src/worktree.js +99 -0
  69. package/dist/remote/web/app.ts +746 -0
  70. package/dist/remote/web/conn.ts +193 -0
  71. package/dist/remote/web/demo.ts +134 -0
  72. package/dist/remote/web/dom.ts +124 -0
  73. package/dist/remote/web/esc.ts +4 -0
  74. package/dist/remote/web/i18n.ts +374 -0
  75. package/dist/remote/web/icon.svg +1 -0
  76. package/dist/remote/web/index.html +24 -0
  77. package/dist/remote/web/manifest.webmanifest +10 -0
  78. package/dist/remote/web/mascot.ts +60 -0
  79. package/dist/remote/web/md.ts +78 -0
  80. package/dist/remote/web/notify.ts +55 -0
  81. package/dist/remote/web/pushstate.ts +27 -0
  82. package/dist/remote/web/secure.ts +109 -0
  83. package/dist/remote/web/sidebar.ts +106 -0
  84. package/dist/remote/web/style.css +311 -0
  85. package/dist/remote/web/sw.ts +48 -0
  86. package/dist/remote/web/timeline.ts +224 -0
  87. package/dist/remote/web/views.ts +323 -0
  88. package/dist/router/src/admin.js +227 -4
  89. package/dist/router/src/config.js +30 -0
  90. package/dist/router/src/index.js +39 -0
  91. package/dist/router/src/ingress/translate.js +6 -2
  92. package/dist/router/src/phone.js +82 -0
  93. package/dist/router/src/presets.js +3 -0
  94. package/dist/router/src/providers/anthropic-usage.js +33 -1
  95. package/dist/router/src/providers/chatgpt/index.js +41 -11
  96. package/dist/router/src/providers/chatgpt/translate.js +147 -7
  97. package/dist/router/src/providers/google/index.js +26 -0
  98. package/dist/router/src/providers/google/prompt-refusal.js +29 -0
  99. package/dist/router/src/providers/google/usage.js +257 -0
  100. package/dist/router/src/providers/grok/index.js +18 -0
  101. package/dist/router/src/providers/grok/usage.js +77 -0
  102. package/dist/router/src/providers/openai/index.js +29 -5
  103. package/dist/router/src/providers/openai/translate.js +260 -27
  104. package/dist/router/src/providers/opencode-usage.js +116 -0
  105. package/dist/router/src/proxy.js +339 -258
  106. package/dist/router/src/reasoning.js +157 -0
  107. package/dist/router/src/remote-host.js +320 -0
  108. package/dist/router/src/remote-tunnel.js +320 -0
  109. package/dist/router/src/requestlog.js +48 -39
  110. package/dist/router/src/thinking.js +20 -9
  111. package/dist/router/src/usage-history.js +227 -0
  112. package/dist/router/src/version.js +1 -1
  113. package/dist/ui/app.js +436 -59
  114. package/dist/ui/i18n.js +184 -0
  115. package/dist/ui/index.html +24 -5
  116. package/dist/ui/style.css +39 -0
  117. package/docs/ARCHITECTURE.md +164 -12
  118. package/package.json +7 -1
@@ -1,5 +1,5 @@
1
1
  import type { SessionContextUsage, SessionRateLimit } from 'claude-code'
2
- import type { CacheMemo, Hot } from '../types'
2
+ import type { CacheMemo, Hot, LimitItem } from '../types'
3
3
 
4
4
  export function hottest(rateLimits: readonly SessionRateLimit[]): Hot | null {
5
5
  const eligible = rateLimits.filter(limit => ['five_hour', 'seven_day'].includes(limit.kind) && limit.percentUsed >= 80)
@@ -33,6 +33,88 @@ export function claudeLimitsFrom(status: unknown, now: number): SessionRateLimit
33
33
  })
34
34
  }
35
35
 
36
+ /** One per-model weekly window the router reports beside `rate_limits` (see `scopedFromLimits`). */
37
+ export type ScopedLimit = { name: string; percent: number; resetsAt?: string }
38
+
39
+ /**
40
+ * The per-model weekly windows of the Claude login, from the router's GET /api/status
41
+ * (`claude.accounts[id=current].quota.scoped`: `{name, used_percent, window_minutes, reset_at}`,
42
+ * reset in epoch seconds or ms). A stale or unknown snapshot gives nothing, like claudeLimitsFrom.
43
+ */
44
+ export function claudeScopedFrom(status: unknown, now: number): ScopedLimit[] {
45
+ const object = (v: unknown): Record<string, unknown> => v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : {}
46
+ const accounts = object(object(status).claude).accounts
47
+ const current = Array.isArray(accounts) ? accounts.map(object).find(a => a.id === 'current') : undefined
48
+ const quota = object(current?.quota)
49
+ if (typeof quota.at !== 'number' || now - quota.at > CLAUDE_USAGE_STALE_MS) return []
50
+ const scoped = quota.scoped
51
+ if (!Array.isArray(scoped)) return []
52
+ return scoped.map(object).flatMap(entry => {
53
+ if (typeof entry.name !== 'string' || !entry.name || typeof entry.used_percent !== 'number' || !Number.isFinite(entry.used_percent)) return []
54
+ const reset = typeof entry.reset_at === 'number' ? entry.reset_at * (entry.reset_at < 1e12 ? 1000 : 1) : NaN
55
+ return [{ name: entry.name, percent: Math.round(entry.used_percent * 10) / 10, ...(Number.isFinite(reset) ? { resetsAt: new Date(reset).toISOString() } : {}) }]
56
+ })
57
+ }
58
+
59
+ const KINDS = ['five_hour', 'seven_day', 'scoped', 'window'] as const
60
+ type LimitKind = (typeof KINDS)[number]
61
+
62
+ /** One window of `GET /api/limits` as a limits-row item, or null when it carries nothing to show. */
63
+ function limitItemFromWindow(w: unknown, provider: string, type: string | undefined, title: string | undefined, account: number, now: number): LimitItem | null {
64
+ const o = w && typeof w === 'object' && !Array.isArray(w) ? w as Record<string, unknown> : {}
65
+ const kind = o.kind
66
+ if (typeof kind !== 'string' || !(KINDS as readonly string[]).includes(kind)) return null
67
+ const used = o.used_percent
68
+ if (typeof used !== 'number' || !Number.isFinite(used)) return null
69
+ const minutes = typeof o.window_minutes === 'number' && o.window_minutes > 0 ? o.window_minutes : undefined
70
+ const reset = typeof o.reset_at === 'number' && Number.isFinite(o.reset_at) ? o.reset_at * (o.reset_at < 1e12 ? 1000 : 1) : NaN
71
+ // A window whose reset has passed no longer stands (as the ChatGPT reader has always said).
72
+ if (Number.isFinite(reset) && reset <= now) return null
73
+ return {
74
+ provider,
75
+ ...(type ? { type } : {}),
76
+ ...(title ? { title } : {}),
77
+ account,
78
+ kind: kind as LimitKind,
79
+ label: typeof o.label === 'string' ? o.label : '',
80
+ ...(minutes !== undefined ? { windowMinutes: minutes } : {}),
81
+ percent: Math.round(used * 10) / 10,
82
+ ...(Number.isFinite(reset) ? { resetsAt: new Date(reset).toISOString() } : {}),
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Every provider's windows from the router's `GET /api/limits`: one shape for Claude, each ChatGPT
88
+ * account and each Antigravity account (see `buildLimits` in packages/router/src/admin.ts). The
89
+ * providers keep the router's order (Claude first), and each account's windows keep theirs.
90
+ */
91
+ export function routerLimitItems(body: unknown, now: number): LimitItem[] {
92
+ const object = (v: unknown): Record<string, unknown> => v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : {}
93
+ const providers = object(body).providers
94
+ if (!Array.isArray(providers)) return []
95
+ const out: LimitItem[] = []
96
+ for (const p of providers) {
97
+ const entry = object(p)
98
+ const provider = typeof entry.provider === 'string' ? entry.provider : ''
99
+ if (!provider) continue
100
+ const type = typeof entry.type === 'string' ? entry.type : undefined
101
+ const title = typeof entry.title === 'string' ? entry.title : undefined
102
+ const accounts = entry.accounts
103
+ if (!Array.isArray(accounts)) continue
104
+ for (const a of accounts) {
105
+ const account = object(a)
106
+ const index = typeof account.account === 'number' ? account.account : 0
107
+ const windows = account.windows
108
+ if (!Array.isArray(windows)) continue
109
+ for (const w of windows) {
110
+ const item = limitItemFromWindow(w, provider, type, title, index, now)
111
+ if (item) out.push(item)
112
+ }
113
+ }
114
+ }
115
+ return out
116
+ }
117
+
36
118
  /**
37
119
  * The reset is read in the person's own clock: `offsetMinutes` is `Date#getTimezoneOffset` (minutes
38
120
  * behind UTC), passed in so tests do not depend on the machine's zone.
@@ -71,6 +153,21 @@ export function keptAlert(kept: { pings: number; tokens: number } | null, memo:
71
153
  return kept !== null && kept.pings > 0 && memo !== null && memo.expiresAt > now
72
154
  }
73
155
 
156
+ /**
157
+ * Whether a put-off hot card should be forgotten: only when the window itself has moved on — another
158
+ * window became the hottest, or its reset moved forward by more than an hour. A reading that is
159
+ * missing for a while (a routed model's session carries no plan windows) or that comes from the
160
+ * other source with its reset spelled to the second rather than the minute is the same window, and
161
+ * forgetting on either brought the card back on every turn (2026-10-14, weekly at 87%).
162
+ */
163
+ export function hotWindowMoved(previous: Hot | null, next: Hot | null): boolean {
164
+ if (!previous || !next) return false
165
+ if (previous.kind !== next.kind) return true
166
+ const before = previous.resetsAt ? Date.parse(previous.resetsAt) : NaN
167
+ const after = next.resetsAt ? Date.parse(next.resetsAt) : NaN
168
+ return Number.isFinite(before) && Number.isFinite(after) && after - before > 3_600_000
169
+ }
170
+
74
171
  export function hotAlert(hot: Hot | null, hidden: number | null): boolean {
75
172
  return hot !== null && hot.percent >= 80 && (hidden === null || hot.percent >= hidden + 10)
76
173
  }
@@ -26,13 +26,17 @@ export const DEFAULTS = {
26
26
  fallback: { agent: 'general-purpose', model: 'sonnet' },
27
27
  keepWarm: false, keepWarmHours: 2, keepWarmMinTokens: 30000,
28
28
  compactWith: 'haiku', handoffSkill: '', handoffAfter: 'continue', handoffButton: 'advised',
29
- swampAt: 50, swampAlert: false, layout: 'dashboard', appearance: 'auto', language: 'auto', hide: ['reason'], extras: [], setupDone: false,
29
+ swampAt: 50, swampAlert: false, layout: 'dashboard', appearance: 'auto', skin: 'deep', language: 'auto', hide: ['reason'], extras: [], setupDone: false,
30
30
  };
31
+ /** The desktop's skins, in the order the panel offers them. */
32
+ export const SKINS = ['deep', 'flat'];
31
33
  /** Band chips that show only when switched on in Look. */
32
34
  export const EXTRAS = ['context', 'limits'];
33
35
  /**
34
36
  * Claude's own models as workers: the built-in general-purpose agent run on the model, `model` its
35
- * current id. The key is what the allow-list stores.
37
+ * current id. The key is what the allow-list stores. Mythos is not here: the Agent tool's `model`
38
+ * takes only sonnet, opus, haiku and fable (Claude Code 2.1.295, read 2026-10-14), so a
39
+ * general-purpose worker on it would be refused.
36
40
  */
37
41
  export const CLAUDE_WORKERS = [
38
42
  { name: 'general-purpose:haiku', model: 'claude-haiku-5-5', provider: 'anthropic', target: { agent: 'general-purpose', model: 'haiku' } },
@@ -281,7 +285,7 @@ export function normalizeSettings(raw, workers) {
281
285
  judge: ['haiku', 'jev', 'model'], jevSource: ['typesafe', 'zen'], bias: [-2, -1, 0, 1, 2], floor: EFFORTS,
282
286
  ceiling: EFFORTS, agentAuto: ['off', 'auto', 'all'], keepWarmHours: [1, 2, 4, 8], compactWith: ['haiku', 'session'],
283
287
  handoffAfter: ['continue', 'confirm', 'copy'], handoffButton: ['advised', 'always'], layout: ['dashboard', 'minimal'],
284
- appearance: ['auto', 'dark', 'light'], language: ['auto', 'ko', 'en', 'zh', 'ja', 'es', 'pt'],
288
+ appearance: ['auto', 'dark', 'light'], skin: SKINS, language: ['auto', 'ko', 'en', 'zh', 'ja', 'es', 'pt'],
285
289
  };
286
290
  for (const key of Object.keys(choices))
287
291
  if (choices[key].includes(value[key]))
@@ -1,4 +1,4 @@
1
- import type { Calibration, Domain, Effort, Level, Mode, Rule, Rung, Settings, SettingsChange, Step, TierTarget } from '../types/index.d.ts'
1
+ import type { Calibration, Domain, Effort, Level, Mode, Rule, Rung, Settings, SettingsChange, Skin, Step, TierTarget } from '../types/index.d.ts'
2
2
 
3
3
  /** Bump whenever a writer's normalization can lose another format's fields. */
4
4
  export const MOD_SETTINGS_SCHEMA_VERSION = 1
@@ -31,15 +31,20 @@ export const DEFAULTS: Settings = {
31
31
  fallback: { agent: 'general-purpose', model: 'sonnet' },
32
32
  keepWarm: false, keepWarmHours: 2, keepWarmMinTokens: 30000,
33
33
  compactWith: 'haiku', handoffSkill: '', handoffAfter: 'continue', handoffButton: 'advised',
34
- swampAt: 50, swampAlert: false, layout: 'dashboard', appearance: 'auto', language: 'auto', hide: ['reason'], extras: [], setupDone: false,
34
+ swampAt: 50, swampAlert: false, layout: 'dashboard', appearance: 'auto', skin: 'deep', language: 'auto', hide: ['reason'], extras: [], setupDone: false,
35
35
  }
36
36
 
37
+ /** The desktop's skins, in the order the panel offers them. */
38
+ export const SKINS: readonly Skin[] = ['deep', 'flat']
39
+
37
40
  /** Band chips that show only when switched on in Look. */
38
41
  export const EXTRAS: readonly string[] = ['context', 'limits']
39
42
 
40
43
  /**
41
44
  * Claude's own models as workers: the built-in general-purpose agent run on the model, `model` its
42
- * current id. The key is what the allow-list stores.
45
+ * current id. The key is what the allow-list stores. Mythos is not here: the Agent tool's `model`
46
+ * takes only sonnet, opus, haiku and fable (Claude Code 2.1.295, read 2026-10-14), so a
47
+ * general-purpose worker on it would be refused.
43
48
  */
44
49
  export const CLAUDE_WORKERS: readonly { name: string; model: string; provider: string; target: TierTarget }[] = [
45
50
  { name: 'general-purpose:haiku', model: 'claude-haiku-5-5', provider: 'anthropic', target: { agent: 'general-purpose', model: 'haiku' } },
@@ -272,7 +277,7 @@ export function normalizeSettings(raw: unknown, workers?: readonly Worker[]): Se
272
277
  judge: ['haiku', 'jev', 'model'], jevSource: ['typesafe', 'zen'], bias: [-2, -1, 0, 1, 2], floor: EFFORTS,
273
278
  ceiling: EFFORTS, agentAuto: ['off', 'auto', 'all'], keepWarmHours: [1, 2, 4, 8], compactWith: ['haiku', 'session'],
274
279
  handoffAfter: ['continue', 'confirm', 'copy'], handoffButton: ['advised', 'always'], layout: ['dashboard', 'minimal'],
275
- appearance: ['auto', 'dark', 'light'], language: ['auto', 'ko', 'en', 'zh', 'ja', 'es', 'pt'],
280
+ appearance: ['auto', 'dark', 'light'], skin: SKINS, language: ['auto', 'ko', 'en', 'zh', 'ja', 'es', 'pt'],
276
281
  } as const
277
282
  for (const key of Object.keys(choices) as (keyof typeof choices)[]) if ((choices[key] as readonly unknown[]).includes(value[key])) Object.assign(result, { [key]: value[key] })
278
283
  if (typeof value.judgeModel === 'string' && value.judgeModel.trim()) result.judgeModel = value.judgeModel.trim()
@@ -1,4 +1,4 @@
1
- import type { Effort, JudgeKind, Stats, Tier, Usage } from '../types'
1
+ import type { AgentRec, Effort, JudgeKind, Stats, Tier, Usage } from '../types'
2
2
  import { t } from './i18n.ts'
3
3
  import type { Lang } from './i18n.ts'
4
4
 
@@ -13,6 +13,30 @@ export function weighted(usage: Usage): number {
13
13
  (usage.cache_read_input_tokens ?? 0) * 0.1 + (usage.output_tokens ?? 0) * 5
14
14
  }
15
15
 
16
+ /**
17
+ * The effort a step was sent at: a routed id's `@effort` wins, since the router forces it over the
18
+ * request's own (packages/router/src/routing.ts: "gpt-5.6-sol@medium → effort forced to medium").
19
+ */
20
+ export function sentEffort(model: string, effort: string | number | undefined): string | number | undefined {
21
+ return /@([a-z]+)$/.exec(model)?.[1] ?? effort
22
+ }
23
+
24
+ /**
25
+ * An agent after one of its steps: the raw counts the API reported added to its own (what the
26
+ * Agents pane shows, so it agrees with the host's), the weighted cost as before, the effort the step
27
+ * was sent at and the model that answered.
28
+ */
29
+ export function addAgentStep(agent: AgentRec, usage: Usage & { model?: string }, effort: string | number | undefined): AgentRec {
30
+ const was = agent.usage ?? { input: 0, cached: 0, cacheWrite: 0, output: 0 }
31
+ return {
32
+ ...agent,
33
+ cost: agent.cost + weighted(usage),
34
+ usage: { input: was.input + (usage.input_tokens ?? 0), cached: was.cached + (usage.cache_read_input_tokens ?? 0), cacheWrite: was.cacheWrite + (usage.cache_creation_input_tokens ?? 0), output: was.output + (usage.output_tokens ?? 0) },
35
+ ...(effort !== undefined ? { effort } : {}),
36
+ ...(usage.model ? { answeredBy: usage.model } : {}),
37
+ }
38
+ }
39
+
16
40
  export function addRequest(stats: Stats, usage: Usage, effort: Effort): Stats {
17
41
  const bucket = stats.byEffort[effort] ?? { prompts: 0, cost: 0 }
18
42
  return { ...stats, requests: stats.requests + 1, input: stats.input + (usage.input_tokens ?? 0),