pinglet-cli 0.2.11 → 0.3.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.
- package/README.ja.md +5 -2
- package/README.ko.md +5 -2
- package/README.md +5 -2
- package/dist/adapters/claude.js +19 -3
- 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 location (system timezone) — Korean in Korea, Japanese in Japan, English everywhere else.
|
|
58
|
+
|
|
56
59
|
## Commands
|
|
57
60
|
|
|
58
61
|
| Command | Description |
|
package/dist/adapters/claude.js
CHANGED
|
@@ -180,9 +180,25 @@ function writeSettings(settings, expectedMtimeMs) {
|
|
|
180
180
|
if (settingsMtimeMs() !== expectedMtimeMs)
|
|
181
181
|
return false;
|
|
182
182
|
// settings.json은 Claude Code가 수시로 읽는 파일이라 tmp+rename으로 원자적으로 쓴다.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// tmp 이름에 pid를 붙여 프로세스마다 다르게 만든다 — 여러 세션의 statusline이
|
|
184
|
+
// 동시에 돌면 같은 tmp에 겹쳐 쓰게 되고, writeFileSync는 truncate 후 쓰기라
|
|
185
|
+
// 짧은 내용 뒤에 긴 내용의 꼬리가 남은 채로 rename 되어 settings.json이 깨진다.
|
|
186
|
+
// rename 자체는 tmp 이름과 무관하게 원자적이라 이것만으로 충분하다.
|
|
187
|
+
const tmp = `${SETTINGS_PATH}.pinglet-tmp.${process.pid}`;
|
|
188
|
+
try {
|
|
189
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
190
|
+
fs.renameSync(tmp, SETTINGS_PATH);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
// rename 전에 실패하면 tmp가 남는다 — 이름이 pid별로 달라 방치하면 쌓인다.
|
|
194
|
+
try {
|
|
195
|
+
fs.unlinkSync(tmp);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// 이미 없으면 무시
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
186
202
|
return true;
|
|
187
203
|
}
|
|
188
204
|
function isPingletTip(tip) {
|
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
|
+
* 타임존을 위치의 근사로 쓰고, 타임존을 못 읽으면 시스템 언어로 추정한다.
|
|
12
|
+
*/
|
|
13
|
+
function detectLocale() {
|
|
14
|
+
try {
|
|
15
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
|
|
16
|
+
if (tz === "Asia/Seoul")
|
|
17
|
+
return "ko";
|
|
18
|
+
if (tz === "Asia/Tokyo")
|
|
19
|
+
return "ja";
|
|
20
|
+
if (tz)
|
|
21
|
+
return "en";
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Intl이 없는 환경 — 아래 언어 폴백으로.
|
|
25
|
+
}
|
|
26
|
+
const lang = (process.env.LC_ALL ||
|
|
27
|
+
process.env.LC_MESSAGES ||
|
|
28
|
+
process.env.LANG ||
|
|
29
|
+
"").toLowerCase();
|
|
30
|
+
if (lang.startsWith("ko"))
|
|
31
|
+
return "ko";
|
|
32
|
+
if (lang.startsWith("ja"))
|
|
33
|
+
return "ja";
|
|
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.0",
|
|
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": {
|