vibe-usage-cli 0.0.0-stage → 0.1.1

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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/build/cli.js +197 -0
  4. package/build/core/claude-processes.d.ts +51 -0
  5. package/build/core/claude-processes.js +288 -0
  6. package/build/core/claude-profile.d.ts +2 -0
  7. package/build/core/claude-profile.js +146 -0
  8. package/build/core/claude-renew.d.ts +12 -0
  9. package/build/core/claude-renew.js +79 -0
  10. package/build/core/continuity.d.ts +90 -0
  11. package/build/core/continuity.js +1244 -0
  12. package/build/core/diagnostics.d.ts +25 -0
  13. package/build/core/diagnostics.js +229 -0
  14. package/build/core/environment.d.ts +12 -0
  15. package/build/core/environment.js +25 -0
  16. package/build/core/errors.d.ts +23 -0
  17. package/build/core/errors.js +48 -0
  18. package/build/core/files.d.ts +20 -0
  19. package/build/core/files.js +136 -0
  20. package/build/core/glance.d.ts +25 -0
  21. package/build/core/glance.js +107 -0
  22. package/build/core/handover.d.ts +61 -0
  23. package/build/core/handover.js +333 -0
  24. package/build/core/history.d.ts +19 -0
  25. package/build/core/history.js +134 -0
  26. package/build/core/launch.d.ts +78 -0
  27. package/build/core/launch.js +256 -0
  28. package/build/core/measurements.d.ts +11 -0
  29. package/build/core/measurements.js +75 -0
  30. package/build/core/model-families.d.ts +12 -0
  31. package/build/core/model-families.js +61 -0
  32. package/build/core/model.d.ts +32 -0
  33. package/build/core/model.js +86 -0
  34. package/build/core/models.d.ts +25 -0
  35. package/build/core/models.js +164 -0
  36. package/build/core/redact.d.ts +2 -0
  37. package/build/core/redact.js +64 -0
  38. package/build/core/selection.d.ts +65 -0
  39. package/build/core/selection.js +173 -0
  40. package/build/core/service.d.ts +35 -0
  41. package/build/core/service.js +484 -0
  42. package/build/core/sessions.d.ts +87 -0
  43. package/build/core/sessions.js +868 -0
  44. package/build/core/settings.d.ts +16 -0
  45. package/build/core/settings.js +377 -0
  46. package/build/core/tray-icon.d.ts +5 -0
  47. package/build/core/tray-icon.js +91 -0
  48. package/build/core/updater.d.ts +29 -0
  49. package/build/core/updater.js +88 -0
  50. package/build/core/updates.d.ts +5 -0
  51. package/build/core/updates.js +29 -0
  52. package/build/launchers/discover-antigravity.ps1 +68 -0
  53. package/build/launchers/terminal.ps1 +78 -0
  54. package/build/main/main.js +587 -0
  55. package/build/providers/antigravity.d.ts +48 -0
  56. package/build/providers/antigravity.js +463 -0
  57. package/build/providers/claude-keychain.d.ts +13 -0
  58. package/build/providers/claude-keychain.js +42 -0
  59. package/build/providers/claude.d.ts +33 -0
  60. package/build/providers/claude.js +218 -0
  61. package/build/providers/codex.d.ts +21 -0
  62. package/build/providers/codex.js +221 -0
  63. package/build/providers/common.d.ts +63 -0
  64. package/build/providers/common.js +294 -0
  65. package/build/providers/cursor.d.ts +14 -0
  66. package/build/providers/cursor.js +141 -0
  67. package/build/providers/profiles.d.ts +10 -0
  68. package/build/providers/profiles.js +69 -0
  69. package/build/providers/tls-trust.d.ts +7 -0
  70. package/build/providers/tls-trust.js +29 -0
  71. package/build/providers/types.d.ts +19 -0
  72. package/build/providers/types.js +1 -0
  73. package/build/shared/account-state.d.ts +19 -0
  74. package/build/shared/account-state.js +75 -0
  75. package/build/shared/catalog.d.ts +85 -0
  76. package/build/shared/catalog.js +125 -0
  77. package/build/shared/format.d.ts +40 -0
  78. package/build/shared/format.js +134 -0
  79. package/build/shared/i18n/index.d.ts +12 -0
  80. package/build/shared/i18n/index.js +40 -0
  81. package/build/shared/i18n/locales/de.d.ts +2 -0
  82. package/build/shared/i18n/locales/de.js +328 -0
  83. package/build/shared/i18n/locales/en.d.ts +328 -0
  84. package/build/shared/i18n/locales/en.js +330 -0
  85. package/build/shared/i18n/locales/es.d.ts +2 -0
  86. package/build/shared/i18n/locales/es.js +328 -0
  87. package/build/shared/i18n/locales/fr.d.ts +2 -0
  88. package/build/shared/i18n/locales/fr.js +328 -0
  89. package/build/shared/i18n/locales/it.d.ts +2 -0
  90. package/build/shared/i18n/locales/it.js +327 -0
  91. package/build/shared/i18n/locales/ja.d.ts +2 -0
  92. package/build/shared/i18n/locales/ja.js +322 -0
  93. package/build/shared/i18n/locales/ko.d.ts +2 -0
  94. package/build/shared/i18n/locales/ko.js +322 -0
  95. package/build/shared/i18n/locales/nl.d.ts +2 -0
  96. package/build/shared/i18n/locales/nl.js +327 -0
  97. package/build/shared/i18n/locales/pl.d.ts +2 -0
  98. package/build/shared/i18n/locales/pl.js +332 -0
  99. package/build/shared/i18n/locales/pt.d.ts +2 -0
  100. package/build/shared/i18n/locales/pt.js +328 -0
  101. package/build/shared/i18n/locales/ru.d.ts +2 -0
  102. package/build/shared/i18n/locales/ru.js +331 -0
  103. package/build/shared/i18n/locales/tr.d.ts +2 -0
  104. package/build/shared/i18n/locales/tr.js +322 -0
  105. package/build/shared/i18n/locales/zh.d.ts +2 -0
  106. package/build/shared/i18n/locales/zh.js +322 -0
  107. package/build/shared/i18n/messages.d.ts +10 -0
  108. package/build/shared/i18n/messages.js +1 -0
  109. package/build/shared/ipc.d.ts +138 -0
  110. package/build/shared/ipc.js +2 -0
  111. package/build/shared/tsconfig.tsbuildinfo +1 -0
  112. package/build/shared/types.d.ts +205 -0
  113. package/build/shared/types.js +1 -0
  114. package/package.json +144 -4
@@ -0,0 +1,75 @@
1
+ // States of an account read just now with a working sign-in: quota, or none to report.
2
+ export const HEALTHY_STATUSES = new Set(['ok', 'no_limits_reported']);
3
+ // Whether a limit with `remaining` percent left is low at the user's threshold.
4
+ export const isLowQuota = (remaining, threshold) => remaining !== null && remaining <= threshold;
5
+ // States where the useful next step is signing in or opening the provider's app.
6
+ const SIGN_IN_STATUSES = new Set([
7
+ 'auth_missing',
8
+ 'auth_invalid',
9
+ 'auth_expired',
10
+ 'auth_rejected',
11
+ 'keychain_unavailable',
12
+ 'app_closed'
13
+ ]);
14
+ // States that pass by themselves: the next read is already on its way.
15
+ const WAITING_STATUSES = new Set([
16
+ 'pending',
17
+ 'rate_limited',
18
+ 'transport_error',
19
+ 'request_timeout',
20
+ 'server_closed',
21
+ 'rpc_timeout',
22
+ 'process_timeout',
23
+ 'local_service_unavailable'
24
+ ]);
25
+ // Signing in helps when the source said so, or when an account was never read for a reason
26
+ // that does not pass by itself.
27
+ export const needsSignIn = (account) => SIGN_IN_STATUSES.has(account.status) || (!account.windows.length && !WAITING_STATUSES.has(account.status));
28
+ // `active` is the source's headline choice, not whether the limit binds (see selection.ts).
29
+ const isKnown = (window) => !window.stale && window.section !== 'details' && window.remainingPercent !== null;
30
+ const tightest = (windows) => [...windows].sort((p, q) => p.remainingPercent - q.remainingPercent)[0] ?? null;
31
+ // A window without a reported length counts as the longest.
32
+ const length = (window) => window.durationMinutes ?? Number.POSITIVE_INFINITY;
33
+ function limits(account) {
34
+ const known = account.windows.filter(isKnown);
35
+ const whole = known.filter(w => !w.scope && !w.partial);
36
+ const pool = whole.length ? whole : known;
37
+ const shortest = Math.min(...pool.map(length));
38
+ const window = tightest(pool.filter(w => length(w) === shortest));
39
+ // The other limit may be one model's or part of the account's: spent, it stops that work, so
40
+ // it is shown. Only a limit on the whole account runs the account out first, though.
41
+ const other = window ? tightest(known.filter(w => w !== window)) : null;
42
+ const tighter = window && other && !other.partial && other.remainingPercent < window.remainingPercent ? other : null;
43
+ return { window, other, tighter };
44
+ }
45
+ function noteFor(account) {
46
+ if (account.status === 'pending')
47
+ return 'reading';
48
+ if (account.status === 'app_closed')
49
+ return 'openApp';
50
+ if (account.status === 'no_limits_reported')
51
+ return 'noLimits';
52
+ if (WAITING_STATUSES.has(account.status))
53
+ return 'waiting';
54
+ return needsSignIn(account) ? 'signIn' : 'noData';
55
+ }
56
+ // One line per account of the tool, its main account first.
57
+ export function compactLines(state, lowQuotaPercent) {
58
+ return [...state.accounts]
59
+ .sort((a, b) => Number(b.primary) - Number(a.primary))
60
+ .map(account => {
61
+ const measured = account.status === 'ok' || (account.status === 'stale' && Boolean(account.fetchedAt));
62
+ const { window, other, tighter } = measured ? limits(account) : { window: null, other: null, tighter: null };
63
+ const isLow = (limit) => isLowQuota(limit?.remainingPercent ?? null, lowQuotaPercent);
64
+ return {
65
+ account,
66
+ window,
67
+ other,
68
+ tighter,
69
+ stale: account.status === 'stale',
70
+ low: isLow(window),
71
+ tighterLow: isLow(tighter),
72
+ note: window ? null : noteFor(account)
73
+ };
74
+ });
75
+ }
@@ -0,0 +1,85 @@
1
+ import type { MessageKey } from './i18n/index.ts';
2
+ export declare const APP_NAME = "Vibe Usage";
3
+ export declare const APP_SLUG = "vibe-usage";
4
+ export declare const APP_REPOSITORY = "mustafacagri/vibe-usage";
5
+ export declare const PROVIDER_IDS: readonly ["claude", "codex", "cursor", "antigravity"];
6
+ export type ProviderId = (typeof PROVIDER_IDS)[number];
7
+ export interface Provider {
8
+ readonly id: ProviderId;
9
+ readonly name: string;
10
+ readonly glyph: string;
11
+ readonly acceptsModel: boolean;
12
+ }
13
+ export declare const PROVIDERS: readonly Provider[];
14
+ export declare const PROFILE_DIRS: {
15
+ readonly claude: ".claude";
16
+ readonly codex: ".codex";
17
+ };
18
+ export type ProfileProviderId = keyof typeof PROFILE_DIRS;
19
+ export declare const isProfileProvider: (id: ProviderId) => id is ProfileProviderId;
20
+ export declare const PROFILE_PROVIDER_IDS: readonly ProfileProviderId[];
21
+ export declare const PROFILE_ENV: {
22
+ readonly claude: "CLAUDE_CONFIG_DIR";
23
+ readonly codex: "CODEX_HOME";
24
+ };
25
+ export declare const AUTO = "auto";
26
+ export type Auto = typeof AUTO;
27
+ export declare const ACCOUNT_ID_PATTERN: RegExp;
28
+ export declare const accountId: (providerId: ProviderId, key: string) => string;
29
+ export declare const DEFAULT_ACCOUNT_KEY = "default";
30
+ export declare const MAX_DISMISSED_ACCOUNTS = 100;
31
+ export declare const MAX_LABEL_LENGTH = 80;
32
+ export declare const MODEL_ID_PATTERN: RegExp;
33
+ export declare const CLAUDE_PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "auto", "dontAsk", "bypassPermissions"];
34
+ export type ClaudePermissionMode = (typeof CLAUDE_PERMISSION_MODES)[number];
35
+ export declare const CLAUDE_EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
36
+ export type ClaudeEffortLevel = (typeof CLAUDE_EFFORT_LEVELS)[number];
37
+ export declare const isOneOf: <T>(values: readonly T[], value: unknown) => value is T;
38
+ export declare const ACCOUNT_LABEL_KEYS: readonly ["accountA", "accountB", "defaultAccount", "newAccount"];
39
+ export declare const ACCOUNT_ORIGINS: readonly ["discovered", "user", "pending"];
40
+ export declare const AUTO_CONTINUE: readonly ["off", "accounts", "tools"];
41
+ export declare const INHERIT = "inherit";
42
+ export declare const AUTOMATION_MODES: readonly ["off", "ask", "auto"];
43
+ export declare const SUGGESTION_KINDS: readonly ["low", "return", "reopen"];
44
+ export declare const TRANSITION_STATUSES: readonly ["offered", "dispatching", "dispatched", "failed", "interrupted", "dismissed"];
45
+ export declare const LOW_QUOTA_PERCENT = 20;
46
+ export declare const LOW_QUOTA_RANGE: {
47
+ readonly min: 1;
48
+ readonly max: 99;
49
+ readonly choices: readonly [5, 10, 15, 20, 25, 30, 40, 50];
50
+ };
51
+ export declare const REFRESH_INTERVAL_SECONDS: {
52
+ readonly min: 30;
53
+ readonly max: 900;
54
+ readonly default: 120;
55
+ readonly choices: readonly [60, 120, 300, 600];
56
+ };
57
+ export declare const MINUTES: {
58
+ readonly HOUR: 60;
59
+ readonly DAY: 1440;
60
+ readonly FIVE_HOURS: 300;
61
+ readonly WEEK: 10080;
62
+ };
63
+ export declare const DURATION_MS: {
64
+ readonly SECOND: 1000;
65
+ readonly MINUTE: 60000;
66
+ readonly HOUR: 3600000;
67
+ readonly DAY: 86400000;
68
+ };
69
+ export declare const CLAUDE_SESSION_ENV_KEYS: readonly string[];
70
+ export declare const CREDENTIAL_ENV_KEYS: readonly string[];
71
+ export declare function providerById(id: ProviderId): Provider;
72
+ export declare function isProviderId(value: unknown): value is ProviderId;
73
+ export declare function normalizeProviderOrder(order: unknown): ProviderId[];
74
+ export declare const displayOrder: (settings: {
75
+ homeOrder?: unknown;
76
+ providerOrder: unknown;
77
+ }) => ProviderId[];
78
+ export interface PeriodLabel {
79
+ labelKey: MessageKey;
80
+ labelParams?: {
81
+ n: number;
82
+ };
83
+ durationMinutes?: number;
84
+ }
85
+ export declare function durationLabel(minutes: unknown): PeriodLabel;
@@ -0,0 +1,125 @@
1
+ // The product name: window and tray titles, the data folder, launched terminals and briefs.
2
+ export const APP_NAME = 'Vibe Usage';
3
+ // The product's name in identifiers: its repository, files it saves, and how it names itself to services.
4
+ export const APP_SLUG = 'vibe-usage';
5
+ // The GitHub repository releases are published to (`owner/name`).
6
+ export const APP_REPOSITORY = `mustafacagri/${APP_SLUG}`;
7
+ export const PROVIDER_IDS = ['claude', 'codex', 'cursor', 'antigravity'];
8
+ const PROVIDER_DETAILS = {
9
+ claude: { name: 'Claude', glyph: '◈', acceptsModel: true },
10
+ codex: { name: 'Codex', glyph: '✺', acceptsModel: true },
11
+ cursor: { name: 'Cursor', glyph: '✦', acceptsModel: true },
12
+ antigravity: { name: 'Antigravity', glyph: '▲', acceptsModel: false }
13
+ };
14
+ export const PROVIDERS = PROVIDER_IDS.map(id => ({ id, ...PROVIDER_DETAILS[id] }));
15
+ // Tools that keep one sign-in per profile folder, the default one in the home directory.
16
+ // Further accounts live in sibling folders (`.codex-work`) or in any folder the user adds.
17
+ export const PROFILE_DIRS = { claude: '.claude', codex: '.codex' };
18
+ export const isProfileProvider = (id) => Object.hasOwn(PROFILE_DIRS, id);
19
+ export const PROFILE_PROVIDER_IDS = PROVIDER_IDS.filter(isProfileProvider);
20
+ // The variable that points each of those tools at a profile folder other than the default one.
21
+ export const PROFILE_ENV = { claude: 'CLAUDE_CONFIG_DIR', codex: 'CODEX_HOME' };
22
+ export const AUTO = 'auto';
23
+ export const ACCOUNT_ID_PATTERN = /^[a-zA-Z0-9:@._+-]{1,120}$/;
24
+ // An account's id: its tool, and a key that tells it from the tool's other accounts.
25
+ export const accountId = (providerId, key) => `${providerId}:${key}`;
26
+ // The key of the account a tool uses when nothing selects another.
27
+ export const DEFAULT_ACCOUNT_KEY = 'default';
28
+ export const MAX_DISMISSED_ACCOUNTS = 100;
29
+ export const MAX_LABEL_LENGTH = 80;
30
+ // A model id as CLIs accept it (`opus`, `gpt-5.6-terra`, `sonnet[1m]`). It must start
31
+ // alphanumeric so that it can never be read as a command-line flag.
32
+ export const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:/[\]-]{0,99}$/;
33
+ // Claude Code's `--permission-mode` and `--effort` values.
34
+ export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'auto', 'dontAsk', 'bypassPermissions'];
35
+ export const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
36
+ // Whether `value` is one of `values`: the guard for every list of literals below.
37
+ export const isOneOf = (values, value) => values.some(entry => entry === value);
38
+ // Each list is the one owner of its values; the type of the same name in types.ts derives from it.
39
+ // Account names the app supplies; the UI translates them.
40
+ export const ACCOUNT_LABEL_KEYS = ['accountA', 'accountB', 'defaultAccount', 'newAccount'];
41
+ export const ACCOUNT_ORIGINS = ['discovered', 'user', 'pending'];
42
+ export const AUTO_CONTINUE = ['off', 'accounts', 'tools'];
43
+ // What a project's own choice is when it leaves the matter to the app-wide setting.
44
+ export const INHERIT = 'inherit';
45
+ // How the app takes a step it may take by itself: never, after asking, or on its own.
46
+ export const AUTOMATION_MODES = ['off', 'ask', 'auto'];
47
+ export const SUGGESTION_KINDS = ['low', 'return', 'reopen'];
48
+ export const TRANSITION_STATUSES = ['offered', 'dispatching', 'dispatched', 'failed', 'interrupted', 'dismissed'];
49
+ // A limit at or below this share is shown as low and, when the user opted in, notified.
50
+ export const LOW_QUOTA_PERCENT = 20;
51
+ // The values each numeric setting may take, and the ones the window offers; a saved value
52
+ // outside the offered ones (from an imported file) is offered too.
53
+ export const LOW_QUOTA_RANGE = { min: 1, max: 99, choices: [5, 10, 15, 20, 25, 30, 40, 50] };
54
+ export const REFRESH_INTERVAL_SECONDS = { min: 30, max: 900, default: 120, choices: [60, 120, 300, 600] };
55
+ export const MINUTES = { HOUR: 60, DAY: 1440, FIVE_HOURS: 300, WEEK: 10080 };
56
+ // The same units in milliseconds, for times and ages.
57
+ export const DURATION_MS = { SECOND: 1000, MINUTE: 60_000, HOUR: 3_600_000, DAY: 86_400_000 };
58
+ // What Claude Code sets for the processes it starts. A Claude Code that inherits them takes itself
59
+ // for a child session of another one and saves nothing: no transcript, no entry in the profile's
60
+ // session list. The app may itself run under Claude Code (started from one of its terminals), so
61
+ // they are never passed on.
62
+ export const CLAUDE_SESSION_ENV_KEYS = [
63
+ 'CLAUDECODE',
64
+ 'CLAUDE_CODE_CHILD_SESSION',
65
+ 'CLAUDE_CODE_SESSION_ID',
66
+ 'CLAUDE_CODE_SESSION_ATTENDED',
67
+ 'CLAUDE_CODE_ENTRYPOINT',
68
+ 'CLAUDE_CODE_EXECPATH',
69
+ 'CLAUDE_CODE_MESSAGING_SOCKET',
70
+ 'CLAUDE_CODE_MESSAGING_TOKEN',
71
+ 'CLAUDE_PID',
72
+ 'CLAUDE_EFFORT',
73
+ 'AI_AGENT',
74
+ 'TRACEPARENT'
75
+ ];
76
+ // Environment variables that would make a launched CLI or a quota probe use credentials,
77
+ // endpoints or profiles other than the account the user selected.
78
+ export const CREDENTIAL_ENV_KEYS = [
79
+ 'ANTHROPIC_API_KEY',
80
+ 'ANTHROPIC_AUTH_TOKEN',
81
+ 'ANTHROPIC_BASE_URL',
82
+ 'ANTHROPIC_PROFILE',
83
+ 'ANTHROPIC_VERTEX_PROJECT_ID',
84
+ 'ANTHROPIC_BEDROCK_BASE_URL',
85
+ 'CLAUDE_CODE_OAUTH_TOKEN',
86
+ 'CLAUDE_SECURESTORAGE_CONFIG_DIR',
87
+ 'CLAUDE_CODE_USE_BEDROCK',
88
+ 'CLAUDE_CODE_USE_VERTEX',
89
+ 'CLAUDE_CODE_USE_FOUNDRY',
90
+ 'AWS_ACCESS_KEY_ID',
91
+ 'AWS_SECRET_ACCESS_KEY',
92
+ 'AWS_SESSION_TOKEN',
93
+ 'OPENAI_API_KEY',
94
+ 'OPENAI_BASE_URL',
95
+ PROFILE_ENV.codex,
96
+ 'CODEX_API_KEY',
97
+ 'CODEX_ACCESS_TOKEN',
98
+ 'ACCESS_TOKEN',
99
+ 'CURSOR_API_KEY'
100
+ ];
101
+ export function providerById(id) {
102
+ return { id, ...PROVIDER_DETAILS[id] };
103
+ }
104
+ export function isProviderId(value) {
105
+ return typeof value === 'string' && Object.hasOwn(PROVIDER_DETAILS, value);
106
+ }
107
+ // A complete, duplicate-free order: known entries first, then any provider left out.
108
+ export function normalizeProviderOrder(order) {
109
+ const known = Array.isArray(order) ? order.filter(isProviderId) : [];
110
+ return [...new Set([...known, ...PROVIDER_IDS])];
111
+ }
112
+ // The order the window and the tray list tools in: the user's own, else the fallback order.
113
+ export const displayOrder = (settings) => normalizeProviderOrder(settings.homeOrder ?? settings.providerOrder);
114
+ // Semantic label for a quota window length; wording lives in i18n.
115
+ export function durationLabel(minutes) {
116
+ if (typeof minutes !== 'number' || !Number.isFinite(minutes) || minutes <= 0)
117
+ return { labelKey: 'labelNoPeriodReported' };
118
+ if (minutes === MINUTES.WEEK)
119
+ return { labelKey: 'labelWeekly', durationMinutes: minutes };
120
+ if (minutes >= 2 * MINUTES.DAY && minutes % MINUTES.DAY === 0)
121
+ return { labelKey: 'labelNDays', labelParams: { n: minutes / MINUTES.DAY }, durationMinutes: minutes };
122
+ if (minutes % MINUTES.HOUR === 0)
123
+ return { labelKey: 'labelNHours', labelParams: { n: minutes / MINUTES.HOUR }, durationMinutes: minutes };
124
+ return { labelKey: 'labelNMins', labelParams: { n: minutes }, durationMinutes: minutes };
125
+ }
@@ -0,0 +1,40 @@
1
+ import { type Params, type Translate } from './i18n/index.ts';
2
+ import { type ProviderId } from './catalog.ts';
3
+ import type { Account, MeasuredAccount, QuotaWindow } from './types.ts';
4
+ export interface FormatContext {
5
+ t: Translate;
6
+ locale: string;
7
+ now?: number;
8
+ }
9
+ export declare function formatPercent(value: number, locale: string): string;
10
+ export declare function formatDuration(ms: number, locale: string): string;
11
+ export interface WindowView {
12
+ label: string;
13
+ value: string;
14
+ percent: number | null;
15
+ detail: string;
16
+ reset: string;
17
+ resetAt: string;
18
+ }
19
+ export declare function describeWindow(window: QuotaWindow, { t, locale, now }: FormatContext): WindowView;
20
+ type AccountNaming = Pick<MeasuredAccount, 'label' | 'labelKey' | 'signedInAs'>;
21
+ export declare const formatAccountName: (account: AccountNaming, t: Translate) => string;
22
+ export declare const hasOwnName: (account: AccountNaming) => boolean;
23
+ export declare const LAUNCH_MESSAGES: {
24
+ readonly app: "handoverApp";
25
+ readonly terminal: "handoverTerminal";
26
+ };
27
+ export declare const toolLabel: (providerId: ProviderId, model: string | null | undefined) => string;
28
+ export declare const SUGGESTION_BUTTONS: {
29
+ readonly low: "suggestionContinueBtn";
30
+ readonly return: "suggestionReturnBtn";
31
+ readonly reopen: "suggestionReopenBtn";
32
+ };
33
+ export declare function successorName(from: ProviderId, to: {
34
+ providerId: ProviderId;
35
+ accountId: string | null;
36
+ }, accounts: readonly Account[], t: Translate): string;
37
+ export declare function formatAge(timestamp: number, { t, locale, now }: FormatContext): string;
38
+ export declare function formatFreshness(iso: string | undefined, status: string, context: FormatContext): string;
39
+ export declare function describeCode(code: string, t: Translate, params?: Params): string;
40
+ export {};
@@ -0,0 +1,134 @@
1
+ // Presentation shared by the renderer and the CLI: turns language-neutral snapshot data into
2
+ // display text. Wording comes from the injected `t`; numbers, money and durations from Intl
3
+ // in the given locale.
4
+ import { hasKey } from "./i18n/index.js";
5
+ import { DURATION_MS, providerById } from "./catalog.js";
6
+ // Countdown granularity: coarser as the distance grows (42m · 3h 11m · 49h · 4d 5h).
7
+ const MINUTE_PRECISION_UNTIL_MINUTES = 10 * 60;
8
+ const HOURS_ONLY_UNTIL_MINUTES = 72 * 60;
9
+ // CLDR's narrow units are ambiguous in some languages (tr: '3s 11d' = 3 hours 11 minutes;
10
+ // ja falls back to Latin '3h11m'), so these use the short style.
11
+ const SHORT_DURATION_LANGUAGES = /^(tr|de|nl|ja)\b/i;
12
+ const AMOUNT_FIELDS = ['used', 'limit', 'remaining', 'bonus'];
13
+ const AMOUNT_KEYS = { used: 'amountUsed', limit: 'amountLimit', remaining: 'amountRemaining', bonus: 'amountBonus' };
14
+ const CENTS_PER_DOLLAR = 100;
15
+ const CURRENCY = 'USD';
16
+ const isNumber = (value) => typeof value === 'number' && Number.isFinite(value);
17
+ const clampPercent = (value) => (isNumber(value) ? Math.max(0, Math.min(100, value)) : null);
18
+ export function formatPercent(value, locale) {
19
+ return new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 }).format(value / 100);
20
+ }
21
+ function formatMoney(cents, locale) {
22
+ return new Intl.NumberFormat(locale, { style: 'currency', currency: CURRENCY }).format(cents / CENTS_PER_DOLLAR);
23
+ }
24
+ function durationParts(ms) {
25
+ const minutes = Math.max(1, Math.round(Math.max(0, ms) / DURATION_MS.MINUTE));
26
+ if (minutes < 60)
27
+ return { minutes };
28
+ if (minutes < MINUTE_PRECISION_UNTIL_MINUTES)
29
+ return { hours: Math.floor(minutes / 60), minutes: minutes % 60 };
30
+ const hours = Math.round(minutes / 60);
31
+ if (minutes < HOURS_ONLY_UNTIL_MINUTES)
32
+ return { hours };
33
+ return { days: Math.floor(hours / 24), hours: hours % 24 };
34
+ }
35
+ export function formatDuration(ms, locale) {
36
+ const parts = Object.fromEntries(Object.entries(durationParts(ms)).filter(([, value]) => value));
37
+ const style = SHORT_DURATION_LANGUAGES.test(locale) ? 'short' : 'narrow';
38
+ return new Intl.DurationFormat(locale, { style }).format(parts);
39
+ }
40
+ function formatDate(date, locale) {
41
+ return new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hour12: false }).format(date);
42
+ }
43
+ // A window whose model scope the source marks inactive: its numbers are not an entitlement.
44
+ const isPassive = (window) => window.active === false && Boolean(window.scope);
45
+ function windowLabel(window, t) {
46
+ const base = window.labelKey ? t(window.labelKey, window.labelParams) : (window.label ?? window.id);
47
+ return window.group === 'team' ? `${t('labelTeam')} · ${base}` : base;
48
+ }
49
+ function windowValue(window, t, locale) {
50
+ if (isPassive(window)) {
51
+ const used = clampPercent(window.usedPercent);
52
+ return used === null ? t('passiveSource') : t('percentUsed', { n: formatPercent(used, locale) });
53
+ }
54
+ if (window.valueKey)
55
+ return t(window.valueKey);
56
+ if (window.amounts) {
57
+ const { remaining } = window.amounts;
58
+ return isNumber(remaining) ? t('amountLeft', { n: formatMoney(remaining, locale) }) : t('amountNotReported');
59
+ }
60
+ const remaining = clampPercent(window.remainingPercent);
61
+ return remaining === null ? t('noData') : t('percentRemaining', { n: formatPercent(remaining, locale) });
62
+ }
63
+ function amountsDetail(amounts, t, locale) {
64
+ return AMOUNT_FIELDS.flatMap(field => {
65
+ const cents = amounts[field];
66
+ return isNumber(cents) ? [t(AMOUNT_KEYS[field], { n: formatMoney(cents, locale) })] : [];
67
+ }).join(' · ');
68
+ }
69
+ function windowDetail(window, t, locale) {
70
+ if (window.amounts)
71
+ return amountsDetail(window.amounts, t, locale);
72
+ if (window.detailKey)
73
+ return t(window.detailKey);
74
+ return isPassive(window) ? t('passiveSourceDetail') : '';
75
+ }
76
+ export function describeWindow(window, { t, locale, now = Date.now() }) {
77
+ const resetDate = window.resetsAt ? new Date(window.resetsAt) : null;
78
+ const resetMs = resetDate?.getTime();
79
+ const hasReset = resetDate !== null && isNumber(resetMs);
80
+ const reset = hasReset ? (resetMs - now <= 0 ? t('resetsNow') : t('resetsIn', { time: formatDuration(resetMs - now, locale) })) : '';
81
+ return {
82
+ label: windowLabel(window, t),
83
+ value: windowValue(window, t, locale),
84
+ percent: isPassive(window) || window.amounts || window.valueKey ? null : clampPercent(window.remainingPercent),
85
+ detail: windowDetail(window, t, locale),
86
+ reset,
87
+ resetAt: hasReset ? t('resetsAt', { date: formatDate(resetDate, locale) }) : ''
88
+ };
89
+ }
90
+ const FALLBACK_ACCOUNT_NAME = 'defaultAccount';
91
+ export const formatAccountName = (account, t) => account.label ?? account.signedInAs ?? t(account.labelKey ?? FALLBACK_ACCOUNT_NAME);
92
+ // Whether the account has a name of its own, rather than the fallback every account shares.
93
+ export const hasOwnName = (account) => account.label !== undefined || account.signedInAs !== undefined || (account.labelKey ?? FALLBACK_ACCOUNT_NAME) !== FALLBACK_ACCOUNT_NAME;
94
+ // What is said once a tool started, by how it started.
95
+ export const LAUNCH_MESSAGES = { app: 'handoverApp', terminal: 'handoverTerminal' };
96
+ // A tool, with the model it was started with when one was chosen.
97
+ export const toolLabel = (providerId, model) => model ? `${providerById(providerId).name} · ${model}` : providerById(providerId).name;
98
+ // What a suggestion's button says, in the window and in its notification.
99
+ export const SUGGESTION_BUTTONS = {
100
+ low: 'suggestionContinueBtn',
101
+ return: 'suggestionReturnBtn',
102
+ reopen: 'suggestionReopenBtn'
103
+ };
104
+ // Who continues the work: another account of the same tool by its name, another tool by its.
105
+ export function successorName(from, to, accounts, t) {
106
+ const account = accounts.find(a => a.id === to.accountId);
107
+ return to.providerId === from && account ? formatAccountName(account, t) : providerById(to.providerId).name;
108
+ }
109
+ export function formatAge(timestamp, { t, locale, now = Date.now() }) {
110
+ const ms = now - timestamp;
111
+ return ms < DURATION_MS.MINUTE ? t('justNow') : t('agoTime', { time: formatDuration(ms, locale) });
112
+ }
113
+ export function formatFreshness(iso, status, context) {
114
+ const { t } = context;
115
+ if (!iso)
116
+ return t('notMeasuredYet');
117
+ const fetchedAt = Date.parse(iso);
118
+ if (!Number.isFinite(fetchedAt))
119
+ return t('measurementTimeUnknown');
120
+ const age = formatAge(fetchedAt, context);
121
+ return status === 'stale' ? t('staleAge', { age }) : age;
122
+ }
123
+ // Message for an error or status code: app errors, then provider status codes, then a generic
124
+ // line for internal codes that have no user-facing wording. Codes arrive as plain strings
125
+ // from other processes, so they are checked against the catalogue at run time.
126
+ export function describeCode(code, t, params) {
127
+ const error = `error_${code}`;
128
+ const status = `status_${code}`;
129
+ if (hasKey(error))
130
+ return t(error, params);
131
+ if (hasKey(status))
132
+ return t(status, params);
133
+ return t('error_unexpected');
134
+ }
@@ -0,0 +1,12 @@
1
+ import type { MessageKey, Messages } from './messages.ts';
2
+ export type { MessageKey } from './messages.ts';
3
+ export declare const SUPPORTED_LANGUAGES: readonly ["en", "tr", "de", "es", "fr", "it", "pt", "nl", "pl", "ja", "ko", "zh", "ru"];
4
+ export type Language = (typeof SUPPORTED_LANGUAGES)[number];
5
+ export declare const DEFAULT_LANGUAGE: Language;
6
+ export declare const LOCALES: Readonly<Record<Language, Messages>>;
7
+ export type Params = Readonly<Record<string, string | number>>;
8
+ export type Translate = (key: MessageKey, params?: Params) => string;
9
+ export declare function isLanguage(value: unknown): value is Language;
10
+ export declare function hasKey(key: string): key is MessageKey;
11
+ export declare function translate(language: string, key: MessageKey, params?: Params): string;
12
+ export declare const translator: (language: string) => Translate;
@@ -0,0 +1,40 @@
1
+ import { de } from "./locales/de.js";
2
+ import { en } from "./locales/en.js";
3
+ import { es } from "./locales/es.js";
4
+ import { fr } from "./locales/fr.js";
5
+ import { it } from "./locales/it.js";
6
+ import { ja } from "./locales/ja.js";
7
+ import { ko } from "./locales/ko.js";
8
+ import { nl } from "./locales/nl.js";
9
+ import { pl } from "./locales/pl.js";
10
+ import { pt } from "./locales/pt.js";
11
+ import { ru } from "./locales/ru.js";
12
+ import { tr } from "./locales/tr.js";
13
+ import { zh } from "./locales/zh.js";
14
+ export const SUPPORTED_LANGUAGES = ['en', 'tr', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'ja', 'ko', 'zh', 'ru'];
15
+ export const DEFAULT_LANGUAGE = 'en';
16
+ export const LOCALES = { en, tr, de, es, fr, it, pt, nl, pl, ja, ko, zh, ru };
17
+ const PLURAL_SUFFIX = '_other';
18
+ const PLURAL_FAMILIES = new Set(Object.keys(en)
19
+ .filter(key => key.endsWith(PLURAL_SUFFIX))
20
+ .map(key => key.slice(0, -PLURAL_SUFFIX.length)));
21
+ const isPluralKey = (key) => PLURAL_FAMILIES.has(key);
22
+ export function isLanguage(value) {
23
+ return typeof value === 'string' && Object.hasOwn(LOCALES, value);
24
+ }
25
+ // Key names computed at run time (`error_<code>`) are checked before lookup.
26
+ export function hasKey(key) {
27
+ return Object.hasOwn(en, key) || isPluralKey(key);
28
+ }
29
+ // A plural key resolves by `params.n`; without a number it uses the `_other` form.
30
+ function pluralText(language, key, n) {
31
+ const table = LOCALES[language];
32
+ const category = typeof n === 'number' ? new Intl.PluralRules(language).select(n) : 'other';
33
+ return (category === 'other' ? undefined : table[`${key}_${category}`]) ?? table[`${key}_other`];
34
+ }
35
+ export function translate(language, key, params = {}) {
36
+ const resolved = isLanguage(language) ? language : DEFAULT_LANGUAGE;
37
+ const text = isPluralKey(key) ? pluralText(resolved, key, params.n) : LOCALES[resolved][key];
38
+ return Object.entries(params).reduce((result, [name, value]) => result.replaceAll(`{${name}}`, String(value)), text);
39
+ }
40
+ export const translator = (language) => (key, params) => translate(language, key, params);
@@ -0,0 +1,2 @@
1
+ import type { Messages } from '../messages.ts';
2
+ export declare const de: Messages;