pinglet-cli 0.1.4 → 0.1.6
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.
- package/README.md +11 -7
- package/dist/adapters/claude.js +17 -12
- package/dist/commands/install.js +1 -1
- package/dist/commands/refresh.js +8 -19
- package/dist/commands/statusline.js +121 -5
- package/dist/runtime.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,10 +64,12 @@ install 시 `~/.claude/commands/pinglet.md` slash command가 함께 설치되며
|
|
|
64
64
|
`node <cli> statusline`을 연결한다. statusline은 UI 영역이라 LLM context에
|
|
65
65
|
전혀 들어가지 않으며(Zero Token), Thinking/Working 중에 메시지가 보인다.
|
|
66
66
|
기존 statusLine 설정은 백업 후 uninstall 시 복원한다.
|
|
67
|
-
- **Spinner verbs** — `
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
- **Spinner verbs (pool=1 회전)** — `spinnerVerbs`(mode: "replace")에 메시지를
|
|
68
|
+
**한 번에 하나만** 등록해 "Befuddling…" 자리에 Ping이 표시된다. statusline이
|
|
69
|
+
60초마다 다음 메시지로 교체하며, Claude Code가 설정을 핫리로드하므로 세션
|
|
70
|
+
중에도 즉시 반영된다. pool이 1개라 "spinner가 돌았다 = 이 메시지가 표시됐다"가
|
|
71
|
+
확정된다. Pinglet verb는 zero-width space 마커로 식별하므로 사용자가 직접
|
|
72
|
+
등록한 verb는 건드리지 않고, uninstall 시 원래대로 복원한다.
|
|
71
73
|
- **Codex Adapter (experimental)** — `~/.codex/config.toml`의 `notify` hook에 연결.
|
|
72
74
|
notify는 TUI 밖 프로세스라 터미널 안에 그릴 수 없어, MVP에서는 turn 완료 시
|
|
73
75
|
macOS 알림으로 전달한다.
|
|
@@ -79,9 +81,11 @@ install 시 `~/.claude/commands/pinglet.md` slash command가 함께 설치되며
|
|
|
79
81
|
네트워크가 절대 들어가지 않는다.
|
|
80
82
|
- **오프라인 동작** — 서버 미연결 시 시드 메시지로 동작하고, 등록/feed 갱신은
|
|
81
83
|
백그라운드 `refresh`가 온라인이 되면 자동 재시도한다.
|
|
82
|
-
- **DELIVERED / QUALIFIED_IMPRESSION 분리** —
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
- **DELIVERED / QUALIFIED_IMPRESSION 분리** — 메시지가 spinner에 armed되면
|
|
85
|
+
DELIVERED(설치당 1회), armed된 동안 spinner가 3초 이상 실제로 돌았으면
|
|
86
|
+
QUALIFIED_IMPRESSION(visibleMs 포함)으로 정산한다. "실제로 돌았는지"는
|
|
87
|
+
statusline hook이 받는 `cost.total_api_duration_ms` 증가분으로 측정한다 —
|
|
88
|
+
pool이 1개이므로 API 활동 시간만큼 그 메시지가 표시된 것이 확정된다.
|
|
85
89
|
- **Async Telemetry** — 이벤트는 `~/.pinglet/events.jsonl`(Local Event Queue)에
|
|
86
90
|
append하고, 20개 이상 또는 60초 주기로 detached 프로세스가 batch 전송한다.
|
|
87
91
|
전송 성공분만 큐에서 제거하므로 실패분은 자동 재시도되며, 재전송 중복은
|
package/dist/adapters/claude.js
CHANGED
|
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.detectClaude = detectClaude;
|
|
37
|
-
exports.
|
|
37
|
+
exports.armSpinnerMessage = armSpinnerMessage;
|
|
38
38
|
exports.spinnerVerbsStatus = spinnerVerbsStatus;
|
|
39
39
|
exports.isClaudeIntegrationInstalled = isClaudeIntegrationInstalled;
|
|
40
40
|
exports.installClaudeIntegration = installClaudeIntegration;
|
|
@@ -184,11 +184,13 @@ function cleanupSpinnerTips(settings, config) {
|
|
|
184
184
|
return true;
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
|
-
* Claude Code spinner verb("Befuddling…" 자리)
|
|
188
|
-
*
|
|
187
|
+
* Claude Code spinner verb("Befuddling…" 자리)에 메시지 **하나만** armed한다.
|
|
188
|
+
* pool을 1개로 유지해야 "spinner가 돌았다 = 이 메시지가 표시됐다"가 확정되고,
|
|
189
|
+
* cost 델타 기반 노출 측정(statusline)이 정확해진다. 설정은 핫리로드된다.
|
|
190
|
+
* message가 null이면 우리 verb를 걷어내고 기본 spinner로 되돌린다.
|
|
189
191
|
* 마커가 붙은 항목만 Pinglet 소유로 보고 교체하며, 사용자 verb는 유지한다.
|
|
190
192
|
*/
|
|
191
|
-
function
|
|
193
|
+
function armSpinnerMessage(config, message) {
|
|
192
194
|
const adapter = config.adapters.claude;
|
|
193
195
|
if (!adapter)
|
|
194
196
|
return false;
|
|
@@ -203,9 +205,8 @@ function syncSpinnerVerbs(config, messages) {
|
|
|
203
205
|
(0, config_1.saveConfig)(config);
|
|
204
206
|
}
|
|
205
207
|
const userVerbs = existingVerbs.filter((verb) => !isPingletTip(verb));
|
|
206
|
-
const pingletVerbs = buildPingletVerbs(
|
|
207
|
-
//
|
|
208
|
-
// 우리가 넣어둔 verb가 있으면 걷어내고 Claude Code 기본 spinner로 되돌린다.
|
|
208
|
+
const pingletVerbs = message ? buildPingletVerbs([message]) : [];
|
|
209
|
+
// armed할 메시지가 없으면 spinner를 기본 상태로 되돌린다.
|
|
209
210
|
if (pingletVerbs.length === 0) {
|
|
210
211
|
if (!isOurs)
|
|
211
212
|
return false;
|
|
@@ -224,10 +225,14 @@ function syncSpinnerVerbs(config, messages) {
|
|
|
224
225
|
writeSettings(settings);
|
|
225
226
|
return false;
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
const next = [...userVerbs, ...pingletVerbs];
|
|
229
|
+
// 같은 메시지를 다시 armed하는 경우 파일 쓰기를 생략한다 (300ms 렌더링 경로 보호).
|
|
230
|
+
if (existing?.mode === "replace" &&
|
|
231
|
+
existingVerbs.length === next.length &&
|
|
232
|
+
existingVerbs.every((v, i) => v === next[i])) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
settings.spinnerVerbs = { mode: "replace", verbs: next };
|
|
231
236
|
writeSettings(settings);
|
|
232
237
|
return true;
|
|
233
238
|
}
|
|
@@ -265,7 +270,7 @@ function installClaudeIntegration(config, options = {}) {
|
|
|
265
270
|
writeSettings(settings);
|
|
266
271
|
installSlashCommand();
|
|
267
272
|
// spinner verb는 로컬 캐시에 실제 feed가 있을 때만 채운다. 없으면 기본 spinner 유지.
|
|
268
|
-
|
|
273
|
+
armSpinnerMessage(config, (0, cache_1.loadFeedMessages)()[0] ?? null);
|
|
269
274
|
return { ok: true };
|
|
270
275
|
}
|
|
271
276
|
function uninstallClaudeIntegration(config) {
|
package/dist/commands/install.js
CHANGED
|
@@ -65,7 +65,7 @@ async function runInstall(args) {
|
|
|
65
65
|
(0, cache_1.saveFeedMessages)(messages);
|
|
66
66
|
if (messages.length > 0) {
|
|
67
67
|
console.log(`✓ Feed cached (${messages.length} messages)`);
|
|
68
|
-
if ((0, claude_1.
|
|
68
|
+
if ((0, claude_1.armSpinnerMessage)(config, messages[0] ?? null)) {
|
|
69
69
|
console.log("✓ Claude spinner synced");
|
|
70
70
|
}
|
|
71
71
|
}
|
package/dist/commands/refresh.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.runRefresh = runRefresh;
|
|
|
4
4
|
const config_1 = require("../config");
|
|
5
5
|
const api_1 = require("../api");
|
|
6
6
|
const cache_1 = require("../cache");
|
|
7
|
-
const queue_1 = require("../queue");
|
|
8
7
|
const claude_1 = require("../adapters/claude");
|
|
9
8
|
/**
|
|
10
9
|
* 서버 feed를 미리 받아 Local Feed Cache를 갱신한다 (아키텍처 §6).
|
|
@@ -33,24 +32,14 @@ async function runRefresh() {
|
|
|
33
32
|
const messages = await (0, api_1.fetchFeed)(config, records[0]);
|
|
34
33
|
if (messages) {
|
|
35
34
|
(0, cache_1.saveFeedMessages)(messages);
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
(0,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
state
|
|
44
|
-
let changed = false;
|
|
45
|
-
for (const m of messages) {
|
|
46
|
-
if (state.delivered[m.id])
|
|
47
|
-
continue;
|
|
48
|
-
(0, queue_1.appendEvent)({ agentType: "CLAUDE", type: "DELIVERED", messageId: m.id });
|
|
49
|
-
state.delivered[m.id] = 1;
|
|
50
|
-
changed = true;
|
|
51
|
-
}
|
|
52
|
-
if (changed)
|
|
53
|
-
(0, cache_1.saveState)(state);
|
|
35
|
+
// spinner 회전과 DELIVERED/QUALIFIED 기록은 statusline이 담당한다 (pool=1).
|
|
36
|
+
// 여기서는 armed된 메시지가 서버에서 사라진 경우만 즉시 정리한다.
|
|
37
|
+
const state = (0, cache_1.loadState)();
|
|
38
|
+
const current = state.current;
|
|
39
|
+
if (current && !messages.some((m) => m.id === current.messageId)) {
|
|
40
|
+
(0, claude_1.armSpinnerMessage)(config, messages[0] ?? null);
|
|
41
|
+
state.current = undefined; // 다음 statusline tick이 정식으로 다음 메시지를 armed한다
|
|
42
|
+
(0, cache_1.saveState)(state);
|
|
54
43
|
}
|
|
55
44
|
}
|
|
56
45
|
// 이 기기에서 작성한 최근 메시지의 도달 통계를 갱신한다 (statusline 표시용).
|
|
@@ -1,20 +1,136 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.runStatusline = runStatusline;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const config_1 = require("../config");
|
|
4
39
|
const cache_1 = require("../cache");
|
|
5
40
|
const render_1 = require("../render");
|
|
41
|
+
const picker_1 = require("../picker");
|
|
42
|
+
const queue_1 = require("../queue");
|
|
43
|
+
const claude_1 = require("../adapters/claude");
|
|
6
44
|
const runtime_1 = require("../runtime");
|
|
45
|
+
function readPayload() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* cost.total_api_duration_ms 증가분 = 이 tick 사이에 spinner가 실제로 돈 시간.
|
|
55
|
+
* spinner pool이 1개이므로 그 시간만큼 armed 메시지가 화면에 표시된 것이 확정된다.
|
|
56
|
+
*/
|
|
57
|
+
function trackSpinnerActivity(state, payload) {
|
|
58
|
+
const sessionId = payload.session_id;
|
|
59
|
+
const apiMs = payload.cost?.total_api_duration_ms;
|
|
60
|
+
if (!sessionId || typeof apiMs !== "number")
|
|
61
|
+
return;
|
|
62
|
+
state.sessions ??= {};
|
|
63
|
+
const prev = state.sessions[sessionId];
|
|
64
|
+
state.sessions[sessionId] = apiMs;
|
|
65
|
+
// 오래된 세션 기록이 무한히 쌓이지 않게 상한을 둔다.
|
|
66
|
+
const ids = Object.keys(state.sessions);
|
|
67
|
+
if (ids.length > 20)
|
|
68
|
+
delete state.sessions[ids[0]];
|
|
69
|
+
if (prev === undefined || apiMs <= prev || !state.current)
|
|
70
|
+
return;
|
|
71
|
+
state.current.visibleMs = Math.min((state.current.visibleMs ?? 0) + (apiMs - prev), runtime_1.ROTATE_MS);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 이전 armed 메시지의 노출을 정산하고 다음 메시지를 armed한다.
|
|
75
|
+
* QUALIFIED_MS 이상 spinner가 돌았으면 유효 노출(visibleMs 포함)로 기록한다.
|
|
76
|
+
*/
|
|
77
|
+
function rotateSpinner(state, now) {
|
|
78
|
+
const config = (0, config_1.loadConfig)();
|
|
79
|
+
const prev = state.current;
|
|
80
|
+
if (prev && (prev.visibleMs ?? 0) >= runtime_1.QUALIFIED_MS) {
|
|
81
|
+
(0, queue_1.appendEvent)({
|
|
82
|
+
agentType: "CLAUDE",
|
|
83
|
+
type: "QUALIFIED_IMPRESSION",
|
|
84
|
+
messageId: prev.messageId,
|
|
85
|
+
visibleMs: prev.visibleMs,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const message = (0, picker_1.pickMessage)((0, cache_1.loadFeedMessages)(), state);
|
|
89
|
+
if (!message) {
|
|
90
|
+
(0, claude_1.armSpinnerMessage)(config, null);
|
|
91
|
+
state.current = undefined;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
(0, claude_1.armSpinnerMessage)(config, message);
|
|
95
|
+
state.seen[message.id] = (state.seen[message.id] ?? 0) + 1;
|
|
96
|
+
state.delivered ??= {};
|
|
97
|
+
if (!state.delivered[message.id]) {
|
|
98
|
+
(0, queue_1.appendEvent)({
|
|
99
|
+
agentType: "CLAUDE",
|
|
100
|
+
type: "DELIVERED",
|
|
101
|
+
messageId: message.id,
|
|
102
|
+
});
|
|
103
|
+
state.delivered[message.id] = 1;
|
|
104
|
+
}
|
|
105
|
+
state.current = {
|
|
106
|
+
messageId: message.id,
|
|
107
|
+
text: message.text,
|
|
108
|
+
author: message.author,
|
|
109
|
+
shownAt: now,
|
|
110
|
+
visibleMs: 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
7
113
|
/**
|
|
8
114
|
* Claude Code statusLine hook 진입점.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
115
|
+
* - spinner 회전(pool=1): ROTATE_MS마다 다음 메시지를 armed (설정 핫리로드로 즉시 반영)
|
|
116
|
+
* - 노출 측정: stdin payload의 cost 델타로 armed 메시지의 실제 표시 시간을 누적
|
|
117
|
+
* - statusline 표시: 이 기기에서 작성한 최근 메시지의 도달 현황
|
|
118
|
+
* - flush/refresh 백그라운드 트리거 (Claude 사용 중 항상 호출되므로 이 경로에서)
|
|
12
119
|
*/
|
|
13
120
|
function runStatusline() {
|
|
121
|
+
const payload = readPayload();
|
|
14
122
|
const state = (0, cache_1.loadState)();
|
|
15
123
|
const now = Date.now();
|
|
16
|
-
|
|
17
|
-
|
|
124
|
+
trackSpinnerActivity(state, payload);
|
|
125
|
+
const rotateDue = !state.current || now - state.current.shownAt >= runtime_1.ROTATE_MS;
|
|
126
|
+
// 300ms마다 호출되므로 저장/maintenance는 1초 스로틀. 회전 시점에는 항상 저장.
|
|
127
|
+
const saveDue = rotateDue ||
|
|
128
|
+
state.lastTickAt === undefined ||
|
|
129
|
+
now - state.lastTickAt > 1_000;
|
|
130
|
+
if (rotateDue) {
|
|
131
|
+
rotateSpinner(state, now);
|
|
132
|
+
}
|
|
133
|
+
if (saveDue) {
|
|
18
134
|
(0, runtime_1.runMaintenance)(state, now);
|
|
19
135
|
state.lastTickAt = now;
|
|
20
136
|
(0, cache_1.saveState)(state);
|
package/dist/runtime.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FEED_TTL_MS = exports.FLUSH_COUNT = exports.FLUSH_INTERVAL_MS = void 0;
|
|
3
|
+
exports.FEED_TTL_MS = exports.FLUSH_COUNT = exports.FLUSH_INTERVAL_MS = exports.QUALIFIED_MS = exports.ROTATE_MS = void 0;
|
|
4
4
|
exports.runMaintenance = runMaintenance;
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const cache_1 = require("./cache");
|
|
8
8
|
const queue_1 = require("./queue");
|
|
9
|
+
exports.ROTATE_MS = 60_000; // spinner 메시지 교체 주기 (설정 핫리로드로 즉시 반영)
|
|
10
|
+
exports.QUALIFIED_MS = 3_000; // 유효 노출로 인정하는 최소 spinner 가동 시간
|
|
9
11
|
exports.FLUSH_INTERVAL_MS = 60_000; // batch 전송 주기
|
|
10
12
|
exports.FLUSH_COUNT = 20; // 또는 이벤트 개수 기준
|
|
11
13
|
exports.FEED_TTL_MS = 5 * 60_000; // feed 캐시 갱신 주기
|