pinglet-cli 0.2.12 → 0.3.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.
- package/README.ja.md +5 -2
- package/README.ko.md +5 -2
- package/README.md +5 -2
- package/dist/api.js +5 -11
- package/dist/cache.js +10 -6
- package/dist/commands/post.js +1 -1
- package/dist/commands/refresh.js +5 -18
- package/dist/commands/statusline.js +19 -3
- package/dist/config.js +4 -4
- package/dist/render.js +44 -19
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -47,12 +47,15 @@ pinglet post "メッセージ"
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
読むだけならログイン不要で、**投稿にのみ GitHub ログインが必要です。**
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
いま何個のターミナルが一緒に起動しているかが statusline に表示されます:
|
|
51
52
|
|
|
52
53
|
```
|
|
53
|
-
|
|
54
|
+
🟢 いま41個のターミナルと一緒にコーディング中
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
statusline の言語はシステム言語に応じて自動選択されます(韓国語/日本語、それ以外は英語)。言語で判定できない場合はタイムゾーン(ソウル/東京)で推定します。
|
|
58
|
+
|
|
56
59
|
## コマンド
|
|
57
60
|
|
|
58
61
|
| コマンド | 説明 |
|
package/README.ko.md
CHANGED
|
@@ -47,12 +47,15 @@ pinglet post "메시지"
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
읽기는 로그인 없이 가능하고, **작성에만 GitHub 로그인이 필요합니다.**
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
지금 몇 개의 터미널이 함께 켜져 있는지는 statusline에서 보여줍니다:
|
|
51
52
|
|
|
52
53
|
```
|
|
53
|
-
|
|
54
|
+
🟢 지금 41개 터미널과 함께 코딩 중
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어/일본어, 그 외는 영어). 시스템 언어로 판정이 안 되면 타임존(서울/도쿄)으로 추정합니다.
|
|
58
|
+
|
|
56
59
|
## 명령어
|
|
57
60
|
|
|
58
61
|
| 명령 | 설명 |
|
package/README.md
CHANGED
|
@@ -47,12 +47,15 @@ pinglet post "message"
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Reading requires no login; **only posting requires a GitHub login.**
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
The statusline shows how many terminals are coding alongside you right now:
|
|
51
52
|
|
|
52
53
|
```
|
|
53
|
-
|
|
54
|
+
🟢 coding along with 41 terminals right now
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
The statusline picks its language from your system language (Korean/Japanese, English otherwise), falling back to your timezone (Seoul/Tokyo) when the language doesn't decide it.
|
|
58
|
+
|
|
56
59
|
## Commands
|
|
57
60
|
|
|
58
61
|
| Command | Description |
|
package/dist/api.js
CHANGED
|
@@ -39,7 +39,6 @@ exports.registerInstallation = registerInstallation;
|
|
|
39
39
|
exports.fetchFeed = fetchFeed;
|
|
40
40
|
exports.uploadEventBatch = uploadEventBatch;
|
|
41
41
|
exports.sendHeartbeat = sendHeartbeat;
|
|
42
|
-
exports.fetchMessageStats = fetchMessageStats;
|
|
43
42
|
exports.createMessage = createMessage;
|
|
44
43
|
exports.linkInstallation = linkInstallation;
|
|
45
44
|
const os = __importStar(require("os"));
|
|
@@ -123,20 +122,15 @@ async function uploadEventBatch(config, record, events) {
|
|
|
123
122
|
});
|
|
124
123
|
return res !== null;
|
|
125
124
|
}
|
|
125
|
+
/** POST /installations/heartbeat — lastSeenAt 갱신. 응답의 온라인 설치 수를 돌려준다(구서버는 null). */
|
|
126
126
|
async function sendHeartbeat(config, record) {
|
|
127
|
-
await request(config, "POST", "/installations/heartbeat", {
|
|
127
|
+
const res = await request(config, "POST", "/installations/heartbeat", {
|
|
128
128
|
token: record.token,
|
|
129
129
|
body: { clientVersion: config_1.VERSION },
|
|
130
130
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const token = config.userToken ?? Object.values(config.installations)[0]?.token;
|
|
135
|
-
if (!token)
|
|
136
|
-
return null;
|
|
137
|
-
return request(config, "GET", `/messages/${messageId}/stats`, {
|
|
138
|
-
token,
|
|
139
|
-
});
|
|
131
|
+
return typeof res?.onlineInstallations === "number"
|
|
132
|
+
? res.onlineInstallations
|
|
133
|
+
: null;
|
|
140
134
|
}
|
|
141
135
|
/**
|
|
142
136
|
* POST /messages — 메시지 작성 (분당 5개 제한).
|
package/dist/cache.js
CHANGED
|
@@ -6,8 +6,8 @@ exports.feedAgeMs = feedAgeMs;
|
|
|
6
6
|
exports.loadState = loadState;
|
|
7
7
|
exports.loadMyPosts = loadMyPosts;
|
|
8
8
|
exports.saveMyPost = saveMyPost;
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
9
|
+
exports.loadOnline = loadOnline;
|
|
10
|
+
exports.saveOnline = saveOnline;
|
|
11
11
|
exports.saveState = saveState;
|
|
12
12
|
const config_1 = require("./config");
|
|
13
13
|
/**
|
|
@@ -49,11 +49,15 @@ function saveMyPost(post) {
|
|
|
49
49
|
const posts = [post, ...loadMyPosts()].slice(0, 20);
|
|
50
50
|
(0, config_1.writeJsonFile)(config_1.POSTS_PATH, posts);
|
|
51
51
|
}
|
|
52
|
-
function
|
|
53
|
-
return (0, config_1.readJsonFile)(config_1.
|
|
52
|
+
function loadOnline() {
|
|
53
|
+
return (0, config_1.readJsonFile)(config_1.ONLINE_PATH);
|
|
54
54
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
55
|
+
function saveOnline(count) {
|
|
56
|
+
const cache = {
|
|
57
|
+
onlineInstallations: count,
|
|
58
|
+
updatedAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
(0, config_1.writeJsonFile)(config_1.ONLINE_PATH, cache);
|
|
57
61
|
}
|
|
58
62
|
function saveState(state) {
|
|
59
63
|
// seen/delivered map이 무한히 자라지 않게 상한을 둔다.
|
package/dist/commands/post.js
CHANGED
|
@@ -65,7 +65,7 @@ async function runPost(args) {
|
|
|
65
65
|
process.exitCode = 1;
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
//
|
|
68
|
+
// 이 기기에서 쓴 메시지를 기록해 둔다.
|
|
69
69
|
if (result.id && result.status !== "REJECTED") {
|
|
70
70
|
(0, cache_1.saveMyPost)({ id: result.id, text, postedAt: new Date().toISOString() });
|
|
71
71
|
}
|
package/dist/commands/refresh.js
CHANGED
|
@@ -28,7 +28,11 @@ async function runRefresh() {
|
|
|
28
28
|
const records = Object.values(config.installations);
|
|
29
29
|
if (records.length === 0)
|
|
30
30
|
return;
|
|
31
|
-
await Promise.all(records.map((r) => (0, api_1.sendHeartbeat)(config, r)));
|
|
31
|
+
const onlineCounts = await Promise.all(records.map((r) => (0, api_1.sendHeartbeat)(config, r)));
|
|
32
|
+
const online = onlineCounts.find((n) => n !== null);
|
|
33
|
+
if (online !== undefined) {
|
|
34
|
+
(0, cache_1.saveOnline)(online);
|
|
35
|
+
}
|
|
32
36
|
const messages = await (0, api_1.fetchFeed)(config, records[0]);
|
|
33
37
|
if (messages) {
|
|
34
38
|
(0, cache_1.saveFeedMessages)(messages);
|
|
@@ -42,21 +46,4 @@ async function runRefresh() {
|
|
|
42
46
|
(0, cache_1.saveState)(state);
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
// 이 기기에서 작성한 최근 메시지의 도달 통계를 갱신한다 (statusline 표시용).
|
|
46
|
-
const latest = (0, cache_1.loadMyPosts)()[0];
|
|
47
|
-
if (latest) {
|
|
48
|
-
const stats = await (0, api_1.fetchMessageStats)(config, latest.id);
|
|
49
|
-
if (stats) {
|
|
50
|
-
(0, cache_1.saveMyStats)({
|
|
51
|
-
messageId: latest.id,
|
|
52
|
-
text: latest.text,
|
|
53
|
-
reachedInstallations: stats.reachedInstallations,
|
|
54
|
-
activeInstallations: stats.activeInstallations,
|
|
55
|
-
delivered: stats.delivered,
|
|
56
|
-
qualifiedImpressions: stats.qualifiedImpressions,
|
|
57
|
-
reactions: stats.reactions,
|
|
58
|
-
updatedAt: new Date().toISOString(),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
49
|
}
|
|
@@ -42,6 +42,22 @@ const picker_1 = require("../picker");
|
|
|
42
42
|
const queue_1 = require("../queue");
|
|
43
43
|
const claude_1 = require("../adapters/claude");
|
|
44
44
|
const runtime_1 = require("../runtime");
|
|
45
|
+
/**
|
|
46
|
+
* online 캐시 유효 기간 — refresh(≈5분) 3주기. 오프라인이 되면
|
|
47
|
+
* 오래된 숫자를 "지금"이라고 보여주는 대신 표시를 접는다.
|
|
48
|
+
*/
|
|
49
|
+
const ONLINE_STALE_MS = 15 * 60_000;
|
|
50
|
+
/** 신선한 online 캐시가 있을 때, 나를 제외한 켜진 터미널 수 (없거나 혼자면 null). */
|
|
51
|
+
function onlineOthers(now) {
|
|
52
|
+
const cache = (0, cache_1.loadOnline)();
|
|
53
|
+
if (!cache)
|
|
54
|
+
return null;
|
|
55
|
+
const t = Date.parse(cache.updatedAt);
|
|
56
|
+
if (Number.isNaN(t) || now - t > ONLINE_STALE_MS)
|
|
57
|
+
return null;
|
|
58
|
+
const others = cache.onlineInstallations - 1; // 이 기기의 heartbeat 제외
|
|
59
|
+
return others >= 1 ? others : null;
|
|
60
|
+
}
|
|
45
61
|
function readPayload() {
|
|
46
62
|
try {
|
|
47
63
|
return JSON.parse(fs.readFileSync(0, "utf8"));
|
|
@@ -114,7 +130,7 @@ function rotateSpinner(state, now) {
|
|
|
114
130
|
* Claude Code statusLine hook 진입점.
|
|
115
131
|
* - spinner 회전(pool=1): ROTATE_MS마다 다음 메시지를 armed (설정 핫리로드로 즉시 반영)
|
|
116
132
|
* - 노출 측정: stdin payload의 cost 델타로 armed 메시지의 실제 표시 시간을 누적
|
|
117
|
-
* - statusline 표시:
|
|
133
|
+
* - statusline 표시: 지금 함께 켜져 있는 터미널 수
|
|
118
134
|
* - flush/refresh 백그라운드 트리거 (Claude 사용 중 항상 호출되므로 이 경로에서)
|
|
119
135
|
*/
|
|
120
136
|
function runStatusline() {
|
|
@@ -135,6 +151,6 @@ function runStatusline() {
|
|
|
135
151
|
state.lastTickAt = now;
|
|
136
152
|
(0, cache_1.saveState)(state);
|
|
137
153
|
}
|
|
138
|
-
const
|
|
139
|
-
process.stdout.write(
|
|
154
|
+
const others = onlineOthers(now);
|
|
155
|
+
process.stdout.write(others !== null ? (0, render_1.formatOnlineNow)(others) : "");
|
|
140
156
|
}
|
package/dist/config.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DEFAULT_API_BASE_URL = exports.
|
|
36
|
+
exports.DEFAULT_API_BASE_URL = exports.ONLINE_PATH = exports.POSTS_PATH = exports.EVENTS_PATH = exports.STATE_PATH = exports.FEED_PATH = exports.CONFIG_PATH = exports.PINGLET_DIR = exports.VERSION = void 0;
|
|
37
37
|
exports.ensurePingletDir = ensurePingletDir;
|
|
38
38
|
exports.readJsonFile = readJsonFile;
|
|
39
39
|
exports.writeJsonFile = writeJsonFile;
|
|
@@ -58,10 +58,10 @@ exports.CONFIG_PATH = path.join(exports.PINGLET_DIR, "config.json");
|
|
|
58
58
|
exports.FEED_PATH = path.join(exports.PINGLET_DIR, "feed.json");
|
|
59
59
|
exports.STATE_PATH = path.join(exports.PINGLET_DIR, "state.json");
|
|
60
60
|
exports.EVENTS_PATH = path.join(exports.PINGLET_DIR, "events.jsonl");
|
|
61
|
-
/** 이 기기에서 작성한 메시지 기록
|
|
61
|
+
/** 이 기기에서 작성한 메시지 기록 */
|
|
62
62
|
exports.POSTS_PATH = path.join(exports.PINGLET_DIR, "posts.json");
|
|
63
|
-
/**
|
|
64
|
-
exports.
|
|
63
|
+
/** 지금 켜져 있는 터미널 수 캐시 — heartbeat 응답으로 갱신, statusline이 읽는다 */
|
|
64
|
+
exports.ONLINE_PATH = path.join(exports.PINGLET_DIR, "online.json");
|
|
65
65
|
exports.DEFAULT_API_BASE_URL = process.env.PINGLET_API_URL ?? "https://pinglet.halluci.co.kr";
|
|
66
66
|
function ensurePingletDir() {
|
|
67
67
|
fs.mkdirSync(exports.PINGLET_DIR, { recursive: true });
|
package/dist/render.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectLocale = detectLocale;
|
|
3
4
|
exports.supportsUnicode = supportsUnicode;
|
|
4
5
|
exports.sanitizeForTerminal = sanitizeForTerminal;
|
|
5
6
|
exports.formatPing = formatPing;
|
|
6
|
-
exports.
|
|
7
|
+
exports.formatOnlineNow = formatOnlineNow;
|
|
8
|
+
/**
|
|
9
|
+
* statusline 표시 언어. 시스템 언어를 먼저 보고(사용자가 실제로 읽는
|
|
10
|
+
* 언어라 가장 강한 신호), 언어로 판정이 안 되면 타임존을 위치의 근사로
|
|
11
|
+
* 쓴다. 한국·일본은 둘 다 UTC+9라 타임존만으로는 오판하는 기기가 있다
|
|
12
|
+
* (예: 한국에서 Asia/Tokyo 타임존을 쓰는 기기 — 시계가 맞아서 티가 안 남).
|
|
13
|
+
* 네트워크 없이 판정한다 (오프라인 동작 원칙).
|
|
14
|
+
*/
|
|
15
|
+
function detectLocale() {
|
|
16
|
+
const lang = (process.env.LC_ALL ||
|
|
17
|
+
process.env.LC_MESSAGES ||
|
|
18
|
+
process.env.LANG ||
|
|
19
|
+
"").toLowerCase();
|
|
20
|
+
if (lang.startsWith("ko"))
|
|
21
|
+
return "ko";
|
|
22
|
+
if (lang.startsWith("ja"))
|
|
23
|
+
return "ja";
|
|
24
|
+
try {
|
|
25
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
|
|
26
|
+
if (tz === "Asia/Seoul")
|
|
27
|
+
return "ko";
|
|
28
|
+
if (tz === "Asia/Tokyo")
|
|
29
|
+
return "ja";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Intl이 없는 환경 — 영어로.
|
|
33
|
+
}
|
|
34
|
+
return "en";
|
|
35
|
+
}
|
|
7
36
|
/** 터미널 독립 원칙: unicode가 불확실하면 plain ASCII로 fallback (아키텍처 §5). */
|
|
8
37
|
function supportsUnicode() {
|
|
9
38
|
if (process.platform === "win32")
|
|
@@ -31,23 +60,19 @@ function formatPing(message) {
|
|
|
31
60
|
}
|
|
32
61
|
return `[ping] ${sponsored}"${text}"`;
|
|
33
62
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
/** statusline용: 지금 함께 켜져 있는 다른 터미널 수 한 줄. */
|
|
64
|
+
function formatOnlineNow(othersCount) {
|
|
65
|
+
const n = othersCount;
|
|
66
|
+
// 한국어·일본어는 unicode 전제 — 못 그리는 터미널이면 ASCII 영어로.
|
|
67
|
+
if (!supportsUnicode()) {
|
|
68
|
+
return `[ping] coding along with ${n} ${n === 1 ? "terminal" : "terminals"} now`;
|
|
69
|
+
}
|
|
70
|
+
switch (detectLocale()) {
|
|
71
|
+
case "ko":
|
|
72
|
+
return `🟢 지금 ${n}개 터미널과 함께 코딩 중`;
|
|
73
|
+
case "ja":
|
|
74
|
+
return `🟢 いま${n}個のターミナルと一緒にコーディング中`;
|
|
75
|
+
default:
|
|
76
|
+
return `🟢 coding along with ${n} ${n === 1 ? "terminal" : "terminals"} right now`;
|
|
48
77
|
}
|
|
49
|
-
const asciiCoverage = active > 0
|
|
50
|
-
? `reach ${Math.round((reached / active) * 100)}%`
|
|
51
|
-
: `reached ${reached} terminals`;
|
|
52
|
-
return `[ping] "${text}" -> ${asciiCoverage}`;
|
|
53
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pinglet-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Short messages from other developers shown in your Claude Code spinner while the AI thinks — zero tokens, never reads your code. Claude Code / Codex integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|