pinglet-cli 0.6.2 → 0.8.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.
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getOutputSpeed = getOutputSpeed;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto_1 = require("crypto");
40
+ const config_1 = require("../config");
41
+ const storage_1 = require("../storage");
42
+ /**
43
+ * 출력 속도(tok/s) — claude-hud의 speed-tracker와 같은 방식.
44
+ * 이전 tick의 output_tokens와 시각을 transcript 경로별 캐시에 남겨 두고 증가분으로 계산한다.
45
+ * - 2초를 넘긴 창은 버린다 (유휴 뒤 첫 tick이 거대한 속도로 보이지 않게)
46
+ * - 0.5초 미만 창은 보고하지 않는다 (렌더가 초당 여러 번 돌면 노이즈가 커진다)
47
+ * - output_tokens가 없으면 transcript 파일 크기 증가(≈4바이트/토큰)로 추정한다
48
+ */
49
+ const SPEED_WINDOW_MS = 2000;
50
+ const MIN_DELTA_MS = 500;
51
+ const BYTES_PER_TOKEN = 4;
52
+ const CACHE_DIR = path.join(config_1.PINGLET_DIR, "hud-cache", "speed");
53
+ function cachePath(transcriptPath, suffix) {
54
+ const hash = (0, crypto_1.createHash)("sha256").update(path.resolve(transcriptPath)).digest("hex").slice(0, 32);
55
+ return path.join(CACHE_DIR, `${hash}${suffix}.json`);
56
+ }
57
+ function readSample(file) {
58
+ try {
59
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
60
+ return typeof parsed?.value === "number" && typeof parsed.timestamp === "number" ? parsed : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function writeSample(file, sample) {
67
+ try {
68
+ (0, storage_1.atomicWrite)(file, JSON.stringify(sample));
69
+ }
70
+ catch {
71
+ // 캐시 실패는 표시에 영향을 주면 안 된다.
72
+ }
73
+ }
74
+ function rate(file, value, now) {
75
+ const previous = readSample(file);
76
+ if (!previous || value < previous.value) {
77
+ writeSample(file, { value, timestamp: now });
78
+ return null;
79
+ }
80
+ const deltaValue = value - previous.value;
81
+ const deltaMs = now - previous.timestamp;
82
+ if (deltaMs > SPEED_WINDOW_MS || deltaValue <= 0) {
83
+ writeSample(file, { value, timestamp: now });
84
+ return null;
85
+ }
86
+ if (deltaMs < MIN_DELTA_MS)
87
+ return null;
88
+ writeSample(file, { value, timestamp: now });
89
+ return deltaValue / (deltaMs / 1000);
90
+ }
91
+ function getOutputSpeed(payload, now = Date.now()) {
92
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path.trim() : "";
93
+ if (!transcriptPath)
94
+ return null; // 세션 키가 없으면 다른 세션과 캐시가 섞인다.
95
+ const outputTokens = payload.context_window?.current_usage?.output_tokens;
96
+ if (typeof outputTokens === "number" && Number.isFinite(outputTokens)) {
97
+ return rate(cachePath(transcriptPath, ""), outputTokens, now);
98
+ }
99
+ try {
100
+ const stat = fs.statSync(transcriptPath);
101
+ if (!stat.isFile())
102
+ return null;
103
+ const tokens = rate(cachePath(transcriptPath, ".fs"), stat.size, now);
104
+ return tokens === null ? null : tokens / BYTES_PER_TOKEN;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTotalTokens = getTotalTokens;
4
+ exports.getContextPercent = getContextPercent;
5
+ exports.getModelName = getModelName;
6
+ exports.formatModelName = formatModelName;
7
+ exports.resolveEffortLevel = resolveEffortLevel;
8
+ exports.parseScopedWindows = parseScopedWindows;
9
+ exports.getUsage = getUsage;
10
+ exports.formatTokens = formatTokens;
11
+ exports.formatUsd = formatUsd;
12
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
13
+ function getTotalTokens(payload) {
14
+ const usage = payload.context_window?.current_usage;
15
+ return ((num(usage?.input_tokens) ?? 0) +
16
+ (num(usage?.cache_creation_input_tokens) ?? 0) +
17
+ (num(usage?.cache_read_input_tokens) ?? 0));
18
+ }
19
+ /**
20
+ * 컨텍스트 사용률. Claude Code가 주는 used_percentage를 우선하고(/context와 일치),
21
+ * 0이거나 없으면 토큰 합/창 크기로 계산한다 — 첫 응답 전에는 used_percentage가
22
+ * 0으로 오지만 current_usage에는 초기 컨텍스트(시스템 프롬프트 등)가 이미 있다.
23
+ */
24
+ function getContextPercent(payload) {
25
+ const native = num(payload.context_window?.used_percentage);
26
+ if (native !== null && native > 0)
27
+ return Math.min(100, Math.max(0, Math.round(native)));
28
+ const size = num(payload.context_window?.context_window_size);
29
+ if (!size || size <= 0)
30
+ return 0;
31
+ return Math.min(100, Math.round((getTotalTokens(payload) / size) * 100));
32
+ }
33
+ function getModelName(payload) {
34
+ const displayName = payload.model?.display_name?.trim();
35
+ if (displayName)
36
+ return displayName;
37
+ return payload.model?.id?.trim() || "Unknown";
38
+ }
39
+ /**
40
+ * full: 표시 이름 그대로 (예: "Opus 4.6 (1M context)")
41
+ * compact: 컨텍스트 창 접미사 제거 (예: "Opus 4.6")
42
+ * short: 접미사와 "Claude " 접두사 모두 제거
43
+ */
44
+ function formatModelName(name, format) {
45
+ if (format === "full")
46
+ return name;
47
+ let result = name.replace(/\s*\([^)]*\bcontext\b[^)]*\)/i, "").trim();
48
+ if (format === "short")
49
+ result = result.replace(/^Claude\s+/i, "");
50
+ return result;
51
+ }
52
+ const EFFORT_SYMBOLS = {
53
+ low: "○",
54
+ medium: "◔",
55
+ high: "◑",
56
+ xhigh: "◕",
57
+ max: "●",
58
+ };
59
+ /**
60
+ * stdin의 effort(문자열 또는 `{ level }`)를 표시용으로 정리한다.
61
+ * ultracode는 stdin에 일반 레벨로 오므로 transcript 신호가 있으면 `ultracode(<level>)`로 감싼다.
62
+ */
63
+ function resolveEffortLevel(effort, ultracodeActive) {
64
+ const raw = typeof effort === "string"
65
+ ? effort
66
+ : effort && typeof effort === "object" && typeof effort.level === "string" ? effort.level : "";
67
+ const level = raw.toLowerCase().trim().replace(/[\p{Cc}\p{Cf}]/gu, "").slice(0, 16);
68
+ if (!level)
69
+ return null;
70
+ const symbol = EFFORT_SYMBOLS[level] ?? "";
71
+ return ultracodeActive === true ? { level: `ultracode(${level})`, symbol } : { level, symbol };
72
+ }
73
+ function percent(v) {
74
+ const n = num(v);
75
+ return n === null ? null : Math.round(Math.min(100, Math.max(0, n)));
76
+ }
77
+ function resetAt(v) {
78
+ const n = num(v);
79
+ return n === null || n <= 0 ? null : new Date(n * 1000);
80
+ }
81
+ const SCOPED_MAX_WINDOWS = 8;
82
+ /** `rate_limits.model_scoped` — 잘못된 항목은 버리고 개수·라벨 길이를 제한한다 (stdin은 신뢰하지 않는다). */
83
+ function parseScopedWindows(modelScoped) {
84
+ if (!Array.isArray(modelScoped))
85
+ return [];
86
+ const windows = [];
87
+ for (const raw of modelScoped) {
88
+ if (windows.length >= SCOPED_MAX_WINDOWS)
89
+ break;
90
+ const entry = raw;
91
+ const label = typeof entry?.display_name === "string"
92
+ ? entry.display_name.replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\s+/g, " ").trim().slice(0, 64)
93
+ : "";
94
+ if (!label)
95
+ continue;
96
+ const utilization = entry?.utilization;
97
+ const value = utilization === null ? null : percent(utilization);
98
+ if (utilization !== null && value === null)
99
+ continue;
100
+ const resetRaw = entry?.resets_at;
101
+ const reset = typeof resetRaw === "string" && resetRaw.length <= 64 && !Number.isNaN(Date.parse(resetRaw))
102
+ ? new Date(resetRaw)
103
+ : null;
104
+ windows.push({ label, percent: value, resetAt: reset });
105
+ }
106
+ return windows;
107
+ }
108
+ function getUsage(payload) {
109
+ const limits = payload.rate_limits;
110
+ if (!limits)
111
+ return null;
112
+ const fiveHour = percent(limits.five_hour?.used_percentage);
113
+ const sevenDay = percent(limits.seven_day?.used_percentage);
114
+ const scopedWindows = parseScopedWindows(limits.model_scoped);
115
+ if (fiveHour === null && sevenDay === null && scopedWindows.length === 0)
116
+ return null;
117
+ return {
118
+ fiveHour,
119
+ sevenDay,
120
+ fiveHourResetAt: resetAt(limits.five_hour?.resets_at),
121
+ sevenDayResetAt: resetAt(limits.seven_day?.resets_at),
122
+ scopedWindows,
123
+ };
124
+ }
125
+ function formatTokens(n) {
126
+ if (n >= 1_000_000)
127
+ return `${(n / 1_000_000).toFixed(1)}M`;
128
+ if (n >= 1000)
129
+ return `${(n / 1000).toFixed(0)}k`;
130
+ return String(Math.max(0, Math.trunc(n)));
131
+ }
132
+ function formatUsd(v) {
133
+ return v >= 100 ? `$${v.toFixed(0)}` : v >= 10 ? `$${v.toFixed(1)}` : `$${v.toFixed(2)}`;
134
+ }