@promptctl/cc-candybar 1.0.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/bin/cc-candybar +6 -0
  4. package/dist/index.mjs +185 -0
  5. package/package.json +99 -0
  6. package/plugin/.claude-plugin/plugin.json +11 -0
  7. package/plugin/bin/preview.sh +305 -0
  8. package/plugin/commands/candybar.md +403 -0
  9. package/plugin/templates/config-essential.json +36 -0
  10. package/plugin/templates/config-full.json +55 -0
  11. package/plugin/templates/config-standard.json +39 -0
  12. package/plugin/templates/config-tui-compact.json +48 -0
  13. package/plugin/templates/config-tui-full.json +89 -0
  14. package/plugin/templates/config-tui-standard.json +56 -0
  15. package/plugin/templates/config-tui.json +18 -0
  16. package/plugin/templates/nerd-fonts-sample.txt +5 -0
  17. package/schema/cc-candybar.schema.json +1379 -0
  18. package/src/click/wire.ts +113 -0
  19. package/src/config/action.ts +91 -0
  20. package/src/config/cli.ts +170 -0
  21. package/src/config/default-dsl-config.ts +661 -0
  22. package/src/config/dsl-loader.ts +265 -0
  23. package/src/config/dsl-types.ts +425 -0
  24. package/src/config/loader/actions.ts +530 -0
  25. package/src/config/loader/cache.ts +206 -0
  26. package/src/config/loader/cross-ref.ts +326 -0
  27. package/src/config/loader/cycles.ts +148 -0
  28. package/src/config/loader/diagnostics.ts +99 -0
  29. package/src/config/loader/discovery.ts +182 -0
  30. package/src/config/loader/emit-schema.ts +63 -0
  31. package/src/config/loader/globals.ts +42 -0
  32. package/src/config/loader/helpers.ts +48 -0
  33. package/src/config/loader/layout.ts +688 -0
  34. package/src/config/loader/merge.ts +40 -0
  35. package/src/config/loader/refs.ts +96 -0
  36. package/src/config/loader/segments.ts +120 -0
  37. package/src/config/loader/validate-core.ts +674 -0
  38. package/src/config/loader/variables.ts +260 -0
  39. package/src/daemon/acquire.ts +411 -0
  40. package/src/daemon/cache/git.ts +553 -0
  41. package/src/daemon/cache/render.ts +449 -0
  42. package/src/daemon/cache/session-usage-store.ts +446 -0
  43. package/src/daemon/cache/watchers.ts +245 -0
  44. package/src/daemon/client-debug.ts +120 -0
  45. package/src/daemon/client-stats.ts +129 -0
  46. package/src/daemon/client-transport.ts +273 -0
  47. package/src/daemon/client.ts +75 -0
  48. package/src/daemon/debug-types.ts +91 -0
  49. package/src/daemon/debug.ts +264 -0
  50. package/src/daemon/limits.ts +154 -0
  51. package/src/daemon/log.ts +69 -0
  52. package/src/daemon/parent-watchdog.ts +80 -0
  53. package/src/daemon/paths.ts +127 -0
  54. package/src/daemon/protocol.ts +235 -0
  55. package/src/daemon/render-payload.ts +611 -0
  56. package/src/daemon/server.ts +1103 -0
  57. package/src/daemon/session-state-file.ts +108 -0
  58. package/src/daemon/session-state.ts +237 -0
  59. package/src/daemon/stats.ts +229 -0
  60. package/src/daemon/verbs/index.ts +458 -0
  61. package/src/daemon/verbs/state-validators.ts +708 -0
  62. package/src/demo/dsl.ts +117 -0
  63. package/src/demo/mock-data.ts +67 -0
  64. package/src/demo/statusline.json5 +92 -0
  65. package/src/dsl/node-registry.ts +281 -0
  66. package/src/dsl/render.ts +558 -0
  67. package/src/index.ts +206 -0
  68. package/src/install/index.ts +410 -0
  69. package/src/proc/launch.ts +451 -0
  70. package/src/proc/stats-handle.ts +13 -0
  71. package/src/render/action.ts +458 -0
  72. package/src/render/diagnostic-style.ts +23 -0
  73. package/src/render/diagnostic-text.ts +77 -0
  74. package/src/render/error-glyph.ts +53 -0
  75. package/src/render/outcome-plan.ts +45 -0
  76. package/src/render/picker.ts +231 -0
  77. package/src/render/split-lines.ts +51 -0
  78. package/src/render/strip.ts +103 -0
  79. package/src/segments/cache.ts +131 -0
  80. package/src/segments/context.ts +190 -0
  81. package/src/segments/git.ts +561 -0
  82. package/src/segments/metrics.ts +101 -0
  83. package/src/segments/pricing.ts +452 -0
  84. package/src/segments/session.ts +188 -0
  85. package/src/segments/tmux.ts +74 -0
  86. package/src/template-engine/cells.ts +90 -0
  87. package/src/template-engine/colors.ts +102 -0
  88. package/src/template-engine/engine.ts +108 -0
  89. package/src/template-engine/funcs.ts +216 -0
  90. package/src/template-engine/index.ts +11 -0
  91. package/src/template-engine/layout.ts +112 -0
  92. package/src/template-engine/scope.ts +62 -0
  93. package/src/themes/index.ts +19 -0
  94. package/src/themes/palette-resolvers.ts +86 -0
  95. package/src/themes/policy.ts +79 -0
  96. package/src/themes/session-random.ts +88 -0
  97. package/src/utils/cache.ts +206 -0
  98. package/src/utils/claude.ts +616 -0
  99. package/src/utils/color-support.ts +118 -0
  100. package/src/utils/formatters.ts +77 -0
  101. package/src/utils/logger.ts +5 -0
  102. package/src/utils/outcome.ts +33 -0
  103. package/src/utils/schema-validator.ts +126 -0
  104. package/src/utils/single-flight.ts +57 -0
  105. package/src/utils/terminal-width.ts +43 -0
  106. package/src/utils/terminal.ts +11 -0
  107. package/src/utils/transcript-fs.ts +162 -0
  108. package/src/var-system/index.ts +24 -0
  109. package/src/var-system/sources.ts +1038 -0
  110. package/src/var-system/store.ts +223 -0
  111. package/src/var-system/types.ts +57 -0
@@ -0,0 +1,190 @@
1
+ import type { ParsedEntry, ClaudeHookData } from "../utils/claude";
2
+
3
+ import { debug } from "../utils/logger";
4
+ import { parseJsonlFile } from "../utils/claude";
5
+ import { ABSENT, failed, ok, type Outcome } from "../utils/outcome";
6
+
7
+ export interface ContextInfo {
8
+ totalTokens: number;
9
+ // Used / remaining percentages. Sourced from Claude's native
10
+ // context_window.used_percentage / remaining_percentage when present; a
11
+ // plain token-ratio is the only fallback (no auto-compact buffer guess).
12
+ percentage: number;
13
+ contextLeftPercentage: number;
14
+ maxTokens: number;
15
+ }
16
+
17
+ interface ContextUsageThresholds {
18
+ LOW: number;
19
+ MEDIUM: number;
20
+ }
21
+
22
+ // [LAW:one-source-of-truth] The context-window size is NEVER guessed from the
23
+ // model name. Claude Code reports the real size for the active model in
24
+ // `context_window.context_window_size` (1M for the [1m] variants, 200k
25
+ // otherwise) — that field is the single authority. This constant is the
26
+ // last-resort floor for ancient clients that omit `context_window` entirely;
27
+ // it is not a per-model table and must not grow into one.
28
+ const DEFAULT_CONTEXT_WINDOW = 200000;
29
+
30
+ export class ContextProvider {
31
+ private readonly thresholds: ContextUsageThresholds = {
32
+ LOW: 50,
33
+ MEDIUM: 80,
34
+ };
35
+
36
+ getContextUsageThresholds(): ContextUsageThresholds {
37
+ return this.thresholds;
38
+ }
39
+
40
+ // Token-ratio percentages — the fallback ONLY. Used when Claude doesn't
41
+ // report used_percentage / remaining_percentage natively (transcript path,
42
+ // or a native window whose percentages are still null pre-first-call). No
43
+ // auto-compact buffer: that was a hardcoded guess at Claude's threshold and
44
+ // a soft second source; the native remaining_percentage is authoritative.
45
+ private ratioPercentages(
46
+ totalTokens: number,
47
+ contextLimit: number,
48
+ ): Pick<ContextInfo, "percentage" | "contextLeftPercentage"> {
49
+ const percentage = Math.min(
50
+ 100,
51
+ Math.max(0, Math.round((totalTokens / contextLimit) * 100)),
52
+ );
53
+ return { percentage, contextLeftPercentage: Math.max(0, 100 - percentage) };
54
+ }
55
+
56
+ /**
57
+ * Calculate context info from native Claude Code context_window data (preferred).
58
+ * Requires Claude Code 2.0.70+ with current_usage field.
59
+ */
60
+ calculateContextFromHookData(hookData: ClaudeHookData): ContextInfo | null {
61
+ const cw = hookData.context_window;
62
+ if (!cw?.current_usage) {
63
+ debug(
64
+ "No current_usage in hook data, falling back to transcript parsing",
65
+ );
66
+ return null;
67
+ }
68
+
69
+ const currentUsage = cw.current_usage;
70
+ // [LAW:no-defensive-null-guards] context_window_size is a required `number`
71
+ // within context_window; reaching here proves cw is present, so the size
72
+ // is too. No `|| default` — that would mask a malformed payload as 200k.
73
+ const contextLimit = cw.context_window_size;
74
+ const totalTokens =
75
+ (currentUsage.input_tokens || 0) +
76
+ (currentUsage.cache_creation_input_tokens || 0) +
77
+ (currentUsage.cache_read_input_tokens || 0);
78
+
79
+ debug(
80
+ `Native current_usage: input=${currentUsage.input_tokens}, cache_create=${currentUsage.cache_creation_input_tokens}, cache_read=${currentUsage.cache_read_input_tokens}, total=${totalTokens} (limit: ${contextLimit})`,
81
+ );
82
+
83
+ // [LAW:one-source-of-truth] Claude's reported used/remaining percentages
84
+ // are authoritative; the token-ratio is only a floor for the window whose
85
+ // percentages are still null (pre-first-call). remaining_percentage is NOT
86
+ // recomputed from a local buffer — it measures real headroom to the limit.
87
+ const ratio = this.ratioPercentages(totalTokens, contextLimit);
88
+ return {
89
+ totalTokens,
90
+ maxTokens: contextLimit,
91
+ percentage:
92
+ cw.used_percentage != null
93
+ ? Math.round(cw.used_percentage)
94
+ : ratio.percentage,
95
+ contextLeftPercentage:
96
+ cw.remaining_percentage != null
97
+ ? Math.round(cw.remaining_percentage)
98
+ : ratio.contextLeftPercentage,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Calculate context tokens by parsing the transcript file (fallback).
104
+ * Used for older Claude Code versions that don't provide context_window.
105
+ *
106
+ * [LAW:no-silent-failure] An unreadable transcript is `failed` (the payload
107
+ * boundary logs it); a transcript with no usable usage entry is `absent`.
108
+ * The old catch-to-null collapsed both into "no data".
109
+ */
110
+ async calculateContextTokensFromTranscript(
111
+ transcriptPath: string,
112
+ contextLimit: number,
113
+ ): Promise<Outcome<ContextInfo>> {
114
+ try {
115
+ debug(`Calculating context tokens from transcript: ${transcriptPath}`);
116
+
117
+ const parsedEntries = await parseJsonlFile(transcriptPath);
118
+
119
+ if (parsedEntries.length === 0) {
120
+ debug("No entries in transcript");
121
+ return ABSENT;
122
+ }
123
+
124
+ let mostRecentEntry: ParsedEntry | null = null;
125
+
126
+ for (let i = parsedEntries.length - 1; i >= 0; i--) {
127
+ const entry = parsedEntries[i];
128
+ if (!entry) continue;
129
+
130
+ if (!entry.message?.usage?.input_tokens) continue;
131
+ if (entry.isSidechain === true) continue;
132
+
133
+ mostRecentEntry = entry;
134
+ debug(
135
+ `Context segment: Found most recent entry at ${entry.timestamp.toISOString()}, stopping search`,
136
+ );
137
+ break;
138
+ }
139
+
140
+ if (mostRecentEntry?.message?.usage) {
141
+ const usage = mostRecentEntry.message.usage;
142
+ const totalTokens =
143
+ (usage.input_tokens || 0) +
144
+ (usage.cache_read_input_tokens || 0) +
145
+ (usage.cache_creation_input_tokens || 0);
146
+
147
+ debug(
148
+ `Most recent main chain context: ${totalTokens} tokens (limit: ${contextLimit})`,
149
+ );
150
+
151
+ return ok({
152
+ totalTokens,
153
+ maxTokens: contextLimit,
154
+ ...this.ratioPercentages(totalTokens, contextLimit),
155
+ });
156
+ }
157
+
158
+ debug("No main chain entries with usage data found");
159
+ return ABSENT;
160
+ } catch (error) {
161
+ return failed(
162
+ `context transcript: ${error instanceof Error ? error.message : String(error)}`,
163
+ );
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Get context info using native data if available, falling back to transcript parsing.
169
+ */
170
+ async getContextInfo(
171
+ hookData: ClaudeHookData,
172
+ ): Promise<Outcome<ContextInfo>> {
173
+ const nativeContext = this.calculateContextFromHookData(hookData);
174
+ if (nativeContext) {
175
+ return ok(nativeContext);
176
+ }
177
+
178
+ // [LAW:one-source-of-truth] current_usage can be null (pre-first-call or
179
+ // post-/compact) while context_window_size is still present and
180
+ // authoritative — prefer it here too, and only fall to the floor when the
181
+ // client omits context_window entirely.
182
+ const contextLimit =
183
+ hookData.context_window?.context_window_size ?? DEFAULT_CONTEXT_WINDOW;
184
+
185
+ return this.calculateContextTokensFromTranscript(
186
+ hookData.transcript_path,
187
+ contextLimit,
188
+ );
189
+ }
190
+ }