pinglet-cli 0.5.4 → 0.6.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 +8 -1
- package/README.ko.md +8 -1
- package/README.md +9 -1
- package/dist/adapters/claude.js +28 -14
- package/dist/adapters/codex.js +21 -8
- package/dist/api.js +56 -16
- package/dist/cache.js +19 -6
- package/dist/cli.js +29 -8
- package/dist/commands/flush.js +71 -6
- package/dist/commands/install.js +17 -8
- package/dist/commands/login.js +27 -5
- package/dist/commands/logout.js +18 -6
- package/dist/commands/notify.js +39 -2
- package/dist/commands/post.js +7 -1
- package/dist/commands/refresh.js +56 -13
- package/dist/commands/statusline.js +11 -2
- package/dist/commands/uninstall.js +47 -5
- package/dist/config.js +20 -8
- package/dist/i18n.js +21 -9
- package/dist/lock.js +168 -0
- package/dist/notifications.js +98 -0
- package/dist/pending-post.js +73 -0
- package/dist/postinstall.js +2 -1
- package/dist/queue.js +56 -7
- package/dist/runtime.js +2 -0
- package/dist/storage.js +61 -0
- package/package.json +2 -1
package/README.ja.md
CHANGED
|
@@ -74,7 +74,7 @@ statusline の言語はシステム言語に応じて自動選択されます(
|
|
|
74
74
|
| `pinglet post "メッセージ"` | Ping を送る |
|
|
75
75
|
| `pinglet ping` | いま表示されるメッセージをプレビュー |
|
|
76
76
|
| `pinglet doctor` | インストール・接続状態を診断 |
|
|
77
|
-
| `pinglet uninstall` |
|
|
77
|
+
| `pinglet uninstall` | 設定復元、サーバー上のインストール失効とログアウト |
|
|
78
78
|
|
|
79
79
|
## 安心して使えます
|
|
80
80
|
|
|
@@ -97,6 +97,13 @@ npm uninstall -g pinglet-cli
|
|
|
97
97
|
|
|
98
98
|
ローカルデータ(`~/.pinglet`)まで削除するには `pinglet uninstall --purge`。
|
|
99
99
|
|
|
100
|
+
`uninstall` は端末のインストールトークンと現在のログインセッションも失効させます。
|
|
101
|
+
`logout` は現在のセッションと端末のアカウント連携を解除し、連携設定と匿名閲覧は維持します。
|
|
102
|
+
サーバー接続や設定復元に失敗した場合、再試行に必要なデータを保持し、失敗を表示します。
|
|
103
|
+
`--purge` はすべての解除に成功した後に実行します。npm 本体は削除しません。
|
|
104
|
+
|
|
105
|
+
フィードキャッシュは更新後最大10分まで使用し、有効期限を過ぎたメッセージは表示しません。
|
|
106
|
+
|
|
100
107
|
## 利用規約とプライバシーポリシー
|
|
101
108
|
|
|
102
109
|
ログインしてメッセージを投稿すると、以下の文書に同意したものとみなされます。
|
package/README.ko.md
CHANGED
|
@@ -74,7 +74,7 @@ statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어
|
|
|
74
74
|
| `pinglet post "메시지"` | Ping 보내기 |
|
|
75
75
|
| `pinglet ping` | 지금 표시될 메시지 미리보기 |
|
|
76
76
|
| `pinglet doctor` | 설치·연결 상태 진단 |
|
|
77
|
-
| `pinglet uninstall` |
|
|
77
|
+
| `pinglet uninstall` | 기존 설정 복원, 서버 설치 폐기 및 로그아웃 |
|
|
78
78
|
|
|
79
79
|
## 안심하고 쓰세요
|
|
80
80
|
|
|
@@ -98,6 +98,13 @@ npm uninstall -g pinglet-cli
|
|
|
98
98
|
|
|
99
99
|
로컬 데이터(`~/.pinglet`)까지 지우려면 `pinglet uninstall --purge`.
|
|
100
100
|
|
|
101
|
+
`uninstall`은 서버의 설치 토큰과 현재 로그인 세션도 폐기합니다. `logout`은
|
|
102
|
+
현재 세션을 폐기하고 이 기기의 설치–계정 연결을 해제하되, 익명 읽기와 연동은 유지합니다.
|
|
103
|
+
서버 연결이나 설정 복원에 실패하면 완료로 처리하지 않으며 재시도에 필요한 데이터를 보존합니다.
|
|
104
|
+
`--purge`는 모든 해제가 성공한 뒤에만 실행됩니다. npm 프로그램 자체는 삭제하지 않습니다.
|
|
105
|
+
|
|
106
|
+
피드는 마지막 갱신 후 최대 10분만 사용하며, 메시지 만료 시각이 지나면 더 이상 표시하지 않습니다.
|
|
107
|
+
|
|
101
108
|
## 약관 및 개인정보처리방침
|
|
102
109
|
|
|
103
110
|
로그인하여 메시지를 작성하면 아래 문서에 동의한 것으로 간주됩니다.
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ left untouched). To opt out, add `"autoUpdate": false` to `~/.pinglet/config.jso
|
|
|
74
74
|
| `pinglet post "message"` | Send a Ping |
|
|
75
75
|
| `pinglet ping` | Preview the message that would be shown now |
|
|
76
76
|
| `pinglet doctor` | Diagnose installation and connection status |
|
|
77
|
-
| `pinglet uninstall` |
|
|
77
|
+
| `pinglet uninstall` | Restore settings, revoke installations and sign out |
|
|
78
78
|
|
|
79
79
|
## Safe to use
|
|
80
80
|
|
|
@@ -99,6 +99,14 @@ npm uninstall -g pinglet-cli
|
|
|
99
99
|
|
|
100
100
|
To also remove local data (`~/.pinglet`), run `pinglet uninstall --purge`.
|
|
101
101
|
|
|
102
|
+
`uninstall` also revokes this device's installation tokens and current login session.
|
|
103
|
+
`logout` revokes the current session and unlinks this device from the account, keeping
|
|
104
|
+
the integration and anonymous reading. If server cleanup or settings restoration fails,
|
|
105
|
+
the command reports failure and preserves the data needed to retry. `--purge` runs only
|
|
106
|
+
after successful cleanup. Neither command removes npm itself.
|
|
107
|
+
|
|
108
|
+
Cached feeds are used for at most 10 minutes and expired messages are no longer displayed.
|
|
109
|
+
|
|
102
110
|
## Terms & Privacy
|
|
103
111
|
|
|
104
112
|
By logging in and posting messages you agree to the following:
|
package/dist/adapters/claude.js
CHANGED
|
@@ -47,6 +47,7 @@ const child_process_1 = require("child_process");
|
|
|
47
47
|
const config_1 = require("../config");
|
|
48
48
|
const cache_1 = require("../cache");
|
|
49
49
|
const render_1 = require("../render");
|
|
50
|
+
const storage_1 = require("../storage");
|
|
50
51
|
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
51
52
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
52
53
|
const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
|
|
@@ -160,8 +161,9 @@ function uninstallSlashCommand() {
|
|
|
160
161
|
fs.unlinkSync(file);
|
|
161
162
|
removed = true;
|
|
162
163
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (!(0, storage_1.isMissing)(error))
|
|
166
|
+
throw error;
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
return removed;
|
|
@@ -194,20 +196,28 @@ function settingsMtimeMs() {
|
|
|
194
196
|
try {
|
|
195
197
|
return fs.statSync(SETTINGS_PATH).mtimeMs;
|
|
196
198
|
}
|
|
197
|
-
catch {
|
|
198
|
-
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (!(0, storage_1.isMissing)(error))
|
|
201
|
+
throw error;
|
|
202
|
+
return null;
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
function readSettingsWithMtime() {
|
|
202
206
|
const mtimeMs = settingsMtimeMs();
|
|
203
207
|
try {
|
|
208
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf8"));
|
|
209
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
210
|
+
throw new Error("Expected a settings object");
|
|
211
|
+
}
|
|
204
212
|
return {
|
|
205
|
-
settings:
|
|
213
|
+
settings: settings,
|
|
206
214
|
mtimeMs,
|
|
207
215
|
};
|
|
208
216
|
}
|
|
209
|
-
catch {
|
|
210
|
-
|
|
217
|
+
catch (error) {
|
|
218
|
+
if ((0, storage_1.isMissing)(error) && mtimeMs === null)
|
|
219
|
+
return { settings: {}, mtimeMs };
|
|
220
|
+
throw new Error(`Cannot read ${SETTINGS_PATH}; original settings preserved.`, { cause: error });
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
223
|
/**
|
|
@@ -227,6 +237,10 @@ function writeSettings(settings, expectedMtimeMs) {
|
|
|
227
237
|
const tmp = `${SETTINGS_PATH}.pinglet-tmp.${process.pid}`;
|
|
228
238
|
try {
|
|
229
239
|
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
240
|
+
if (settingsMtimeMs() !== expectedMtimeMs) {
|
|
241
|
+
fs.unlinkSync(tmp);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
230
244
|
fs.renameSync(tmp, SETTINGS_PATH);
|
|
231
245
|
}
|
|
232
246
|
catch (error) {
|
|
@@ -273,10 +287,7 @@ function cleanupSpinnerTips(settings, config) {
|
|
|
273
287
|
else {
|
|
274
288
|
delete settings.spinnerTipsOverride;
|
|
275
289
|
}
|
|
276
|
-
|
|
277
|
-
delete adapter.spinnerTipsBackup;
|
|
278
|
-
(0, config_1.saveConfig)(config);
|
|
279
|
-
}
|
|
290
|
+
// Keep the backup until uninstall commits successfully.
|
|
280
291
|
return true;
|
|
281
292
|
}
|
|
282
293
|
/**
|
|
@@ -402,6 +413,7 @@ function installClaudeIntegration(config, options = {}) {
|
|
|
402
413
|
}
|
|
403
414
|
function uninstallClaudeIntegration(config) {
|
|
404
415
|
let settingsChanged = false;
|
|
416
|
+
let committed = false;
|
|
405
417
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
406
418
|
const { settings, mtimeMs } = readSettingsWithMtime();
|
|
407
419
|
settingsChanged = false;
|
|
@@ -450,11 +462,13 @@ function uninstallClaudeIntegration(config) {
|
|
|
450
462
|
}
|
|
451
463
|
settingsChanged = true;
|
|
452
464
|
}
|
|
453
|
-
if (!settingsChanged)
|
|
465
|
+
if (!settingsChanged || writeSettings(settings, mtimeMs)) {
|
|
466
|
+
committed = true;
|
|
454
467
|
break;
|
|
455
|
-
|
|
456
|
-
break; // 충돌 시 재읽기 후 재적용
|
|
468
|
+
}
|
|
457
469
|
}
|
|
470
|
+
if (!committed)
|
|
471
|
+
throw new Error((0, i18n_1.t)("claude.settingsBusy"));
|
|
458
472
|
const commandRemoved = uninstallSlashCommand();
|
|
459
473
|
if (config.adapters.claude) {
|
|
460
474
|
delete config.adapters.claude;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -43,6 +43,7 @@ const os = __importStar(require("os"));
|
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const config_1 = require("../config");
|
|
46
|
+
const storage_1 = require("../storage");
|
|
46
47
|
const CODEX_DIR = path.join(os.homedir(), ".codex");
|
|
47
48
|
const CONFIG_TOML_PATH = path.join(CODEX_DIR, "config.toml");
|
|
48
49
|
/**
|
|
@@ -71,8 +72,10 @@ function readToml() {
|
|
|
71
72
|
try {
|
|
72
73
|
return fs.readFileSync(CONFIG_TOML_PATH, "utf8");
|
|
73
74
|
}
|
|
74
|
-
catch {
|
|
75
|
-
|
|
75
|
+
catch (error) {
|
|
76
|
+
if ((0, storage_1.isMissing)(error))
|
|
77
|
+
return "";
|
|
78
|
+
throw error;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
/**
|
|
@@ -94,7 +97,13 @@ function findNotifyEntry(toml) {
|
|
|
94
97
|
let depth = 0;
|
|
95
98
|
for (; i < toml.length; i++) {
|
|
96
99
|
const ch = toml[i];
|
|
97
|
-
if (ch ===
|
|
100
|
+
if (ch === "#") {
|
|
101
|
+
const end = toml.indexOf("\n", i);
|
|
102
|
+
if (end === -1)
|
|
103
|
+
break;
|
|
104
|
+
i = end;
|
|
105
|
+
}
|
|
106
|
+
else if (ch === '"' || ch === "'") {
|
|
98
107
|
const quote = ch;
|
|
99
108
|
i++;
|
|
100
109
|
while (i < toml.length && toml[i] !== quote) {
|
|
@@ -112,8 +121,7 @@ function findNotifyEntry(toml) {
|
|
|
112
121
|
return toml.slice(start, i + 1);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
|
-
|
|
116
|
-
return null;
|
|
124
|
+
throw new Error(`Invalid notify array in ${CONFIG_TOML_PATH}; original settings preserved.`);
|
|
117
125
|
}
|
|
118
126
|
function isCodexIntegrationInstalled() {
|
|
119
127
|
const entry = findNotifyEntry(readToml());
|
|
@@ -148,7 +156,7 @@ function installCodexIntegration(config, options = {}) {
|
|
|
148
156
|
next = notifyLine() + "\n" + toml;
|
|
149
157
|
}
|
|
150
158
|
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
151
|
-
|
|
159
|
+
(0, storage_1.atomicWrite)(CONFIG_TOML_PATH, next);
|
|
152
160
|
return { ok: true };
|
|
153
161
|
}
|
|
154
162
|
function uninstallCodexIntegration(config) {
|
|
@@ -156,13 +164,18 @@ function uninstallCodexIntegration(config) {
|
|
|
156
164
|
const existing = findNotifyEntry(toml);
|
|
157
165
|
const isOurs = existing !== null &&
|
|
158
166
|
(existing.includes("pinglet") || existing.includes((0, config_1.cliPath)()));
|
|
159
|
-
if (!existing || !isOurs)
|
|
167
|
+
if (!existing || !isOurs) {
|
|
168
|
+
if (config.adapters.codex) {
|
|
169
|
+
delete config.adapters.codex;
|
|
170
|
+
(0, config_1.saveConfig)(config);
|
|
171
|
+
}
|
|
160
172
|
return false;
|
|
173
|
+
}
|
|
161
174
|
const backup = config.adapters.codex?.notifyBackup;
|
|
162
175
|
const next = backup
|
|
163
176
|
? toml.replace(existing, backup)
|
|
164
177
|
: toml.replace(existing + "\n", "").replace(existing, "");
|
|
165
|
-
|
|
178
|
+
(0, storage_1.atomicWrite)(CONFIG_TOML_PATH, next);
|
|
166
179
|
delete config.adapters.codex;
|
|
167
180
|
(0, config_1.saveConfig)(config);
|
|
168
181
|
return true;
|
package/dist/api.js
CHANGED
|
@@ -38,8 +38,11 @@ exports.checkHealth = checkHealth;
|
|
|
38
38
|
exports.registerInstallation = registerInstallation;
|
|
39
39
|
exports.fetchFeed = fetchFeed;
|
|
40
40
|
exports.uploadEventBatch = uploadEventBatch;
|
|
41
|
+
exports.uploadEventBatchResult = uploadEventBatchResult;
|
|
41
42
|
exports.sendHeartbeat = sendHeartbeat;
|
|
42
43
|
exports.createMessage = createMessage;
|
|
44
|
+
exports.validateUserToken = validateUserToken;
|
|
45
|
+
exports.releaseCredential = releaseCredential;
|
|
43
46
|
exports.linkInstallation = linkInstallation;
|
|
44
47
|
const os = __importStar(require("os"));
|
|
45
48
|
const crypto_1 = require("crypto");
|
|
@@ -100,6 +103,7 @@ async function fetchFeed(config, record, limit = 50) {
|
|
|
100
103
|
text: item.text,
|
|
101
104
|
author: item.authorNickname || "익명의 개발자",
|
|
102
105
|
category: item.category,
|
|
106
|
+
expiresAt: item.expiresAt,
|
|
103
107
|
contentType: item.contentType === "SYSTEM" || item.contentType === "SPONSORED"
|
|
104
108
|
? item.contentType
|
|
105
109
|
: "USER",
|
|
@@ -107,20 +111,30 @@ async function fetchFeed(config, record, limit = 50) {
|
|
|
107
111
|
}
|
|
108
112
|
/** POST /events/batch — eventId 기반으로 서버가 dedupe하므로 재전송에 안전하다. */
|
|
109
113
|
async function uploadEventBatch(config, record, events) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
return (await uploadEventBatchResult(config, record, events)).ok;
|
|
115
|
+
}
|
|
116
|
+
async function uploadEventBatchResult(config, record, events) {
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(config.apiBaseUrl + "/events/batch", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${record.token}` },
|
|
121
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
batchId: "batch_" + (0, crypto_1.randomUUID)(),
|
|
124
|
+
events: events.map(({ eventId, type, messageId, visibleMs, occurredAt }) => ({
|
|
125
|
+
eventId,
|
|
126
|
+
type,
|
|
127
|
+
messageId,
|
|
128
|
+
...(visibleMs !== undefined && { visibleMs }),
|
|
129
|
+
occurredAt,
|
|
130
|
+
})),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
return { ok: res.ok, permanent: [400, 413, 422].includes(res.status), status: res.status };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { ok: false, permanent: false };
|
|
137
|
+
}
|
|
124
138
|
}
|
|
125
139
|
/** POST /installations/heartbeat — lastSeenAt 갱신. 응답의 온라인 설치 수를 돌려준다(구서버는 null). */
|
|
126
140
|
async function sendHeartbeat(config, record) {
|
|
@@ -136,7 +150,7 @@ async function sendHeartbeat(config, record) {
|
|
|
136
150
|
* POST /messages — 메시지 작성 (분당 5개 제한).
|
|
137
151
|
* 작성은 로그인(GitHub 또는 Google)이 필요하다 (읽기는 계속 익명).
|
|
138
152
|
*/
|
|
139
|
-
async function createMessage(config, text, category) {
|
|
153
|
+
async function createMessage(config, text, category, requestId = (0, crypto_1.randomUUID)()) {
|
|
140
154
|
const token = config.userToken;
|
|
141
155
|
if (!token)
|
|
142
156
|
return { ok: false, error: "LOGIN_REQUIRED" };
|
|
@@ -147,7 +161,7 @@ async function createMessage(config, text, category) {
|
|
|
147
161
|
"content-type": "application/json",
|
|
148
162
|
authorization: `Bearer ${token}`,
|
|
149
163
|
},
|
|
150
|
-
body: JSON.stringify({ text, ...(category && { category }) }),
|
|
164
|
+
body: JSON.stringify({ text, requestId, ...(category && { category }) }),
|
|
151
165
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
152
166
|
});
|
|
153
167
|
if (res.status === 401)
|
|
@@ -168,6 +182,32 @@ async function createMessage(config, text, category) {
|
|
|
168
182
|
return { ok: false, error: "NETWORK" };
|
|
169
183
|
}
|
|
170
184
|
}
|
|
185
|
+
async function validateUserToken(config, token) {
|
|
186
|
+
const user = await request(config, "GET", "/users/me", { token });
|
|
187
|
+
return typeof user?.id === "string" && user.id.length > 0;
|
|
188
|
+
}
|
|
189
|
+
/** Revocation/unlink are idempotent. A 401 installation credential is already unusable.
|
|
190
|
+
* Other failures (including an old server's 404) must not be reported as success.
|
|
191
|
+
*/
|
|
192
|
+
async function releaseCredential(config, kind, token) {
|
|
193
|
+
try {
|
|
194
|
+
const apiPath = kind === "user" ? "/auth/logout" : kind === "unlink" ? "/installations/unlink" : "/installations/current";
|
|
195
|
+
const res = await fetch(config.apiBaseUrl + apiPath, {
|
|
196
|
+
method: kind === "installation" ? "DELETE" : "POST",
|
|
197
|
+
headers: { authorization: `Bearer ${token}` },
|
|
198
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
199
|
+
});
|
|
200
|
+
if (res.status === 401)
|
|
201
|
+
return true;
|
|
202
|
+
if (!res.ok)
|
|
203
|
+
return false;
|
|
204
|
+
const body = await res.json();
|
|
205
|
+
return kind === "unlink" ? body.unlinked === true : body.revoked === true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
171
211
|
/**
|
|
172
212
|
* POST /installations/link — 로그인 후 기존 익명 설치를 계정에 연결한다.
|
|
173
213
|
* 소유 증명으로 installation "토큰"을 보낸다 (ID만으로는 연결 불가).
|
package/dist/cache.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_FEED_AGE_MS = void 0;
|
|
3
4
|
exports.loadFeedMessages = loadFeedMessages;
|
|
4
5
|
exports.saveFeedMessages = saveFeedMessages;
|
|
5
6
|
exports.feedAgeMs = feedAgeMs;
|
|
@@ -15,9 +16,16 @@ const config_1 = require("./config");
|
|
|
15
16
|
* 서버 feed를 아직 못 받았으면 빈 배열 — Pinglet은 아무것도 표시하지 않고
|
|
16
17
|
* Claude Code 기본 spinner가 그대로 유지된다.
|
|
17
18
|
*/
|
|
19
|
+
exports.MAX_FEED_AGE_MS = 10 * 60_000;
|
|
18
20
|
function loadFeedMessages() {
|
|
19
21
|
const cache = (0, config_1.readJsonFile)(config_1.FEED_PATH);
|
|
20
|
-
|
|
22
|
+
if (!cache || !Array.isArray(cache.messages))
|
|
23
|
+
return [];
|
|
24
|
+
const fetchedAt = Date.parse(cache.fetchedAt);
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (!Number.isFinite(fetchedAt) || fetchedAt > now || now - fetchedAt >= exports.MAX_FEED_AGE_MS)
|
|
27
|
+
return [];
|
|
28
|
+
return cache.messages.filter((m) => m.expiresAt == null || Date.parse(m.expiresAt) > now);
|
|
21
29
|
}
|
|
22
30
|
function saveFeedMessages(messages) {
|
|
23
31
|
const file = {
|
|
@@ -35,12 +43,17 @@ function feedAgeMs() {
|
|
|
35
43
|
return Number.isNaN(t) ? null : Date.now() - t;
|
|
36
44
|
}
|
|
37
45
|
function loadState() {
|
|
38
|
-
const state = (0, config_1.readJsonFile)(config_1.STATE_PATH);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const state = (0, config_1.readJsonFile)(config_1.STATE_PATH) ?? { seen: {} };
|
|
47
|
+
const installationId = (0, config_1.readJsonFile)(config_1.CONFIG_PATH)?.installations?.CLAUDE?.installationId;
|
|
48
|
+
if (state.claudeInstallationId && state.claudeInstallationId !== installationId) {
|
|
49
|
+
state.seen = {};
|
|
50
|
+
state.delivered = {};
|
|
51
|
+
state.current = undefined;
|
|
52
|
+
state.sessions = undefined;
|
|
42
53
|
}
|
|
43
|
-
|
|
54
|
+
state.claudeInstallationId = installationId;
|
|
55
|
+
state.seen ??= {};
|
|
56
|
+
return state;
|
|
44
57
|
}
|
|
45
58
|
function loadMyPosts() {
|
|
46
59
|
return (0, config_1.readJsonFile)(config_1.POSTS_PATH) ?? [];
|
package/dist/cli.js
CHANGED
|
@@ -8,12 +8,14 @@ const doctor_1 = require("./commands/doctor");
|
|
|
8
8
|
const login_1 = require("./commands/login");
|
|
9
9
|
const logout_1 = require("./commands/logout");
|
|
10
10
|
const statusline_1 = require("./commands/statusline");
|
|
11
|
-
const notify_1 = require("./commands/notify");
|
|
12
11
|
const flush_1 = require("./commands/flush");
|
|
13
12
|
const refresh_1 = require("./commands/refresh");
|
|
14
13
|
const ping_1 = require("./commands/ping");
|
|
15
14
|
const post_1 = require("./commands/post");
|
|
16
15
|
const i18n_1 = require("./i18n");
|
|
16
|
+
const lock_1 = require("./lock");
|
|
17
|
+
const notifications_1 = require("./notifications");
|
|
18
|
+
const runtime_1 = require("./runtime");
|
|
17
19
|
const HELP = (0, i18n_1.t)("cli.help", { version: config_1.VERSION });
|
|
18
20
|
async function main() {
|
|
19
21
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -22,13 +24,13 @@ async function main() {
|
|
|
22
24
|
await (0, install_1.runInstall)(rest);
|
|
23
25
|
break;
|
|
24
26
|
case "uninstall":
|
|
25
|
-
(0, uninstall_1.runUninstall)(rest);
|
|
27
|
+
await (0, uninstall_1.runUninstall)(rest);
|
|
26
28
|
break;
|
|
27
29
|
case "login":
|
|
28
30
|
await (0, login_1.runLogin)(rest);
|
|
29
31
|
break;
|
|
30
32
|
case "logout":
|
|
31
|
-
(0, logout_1.runLogout)();
|
|
33
|
+
await (0, logout_1.runLogout)();
|
|
32
34
|
break;
|
|
33
35
|
case "doctor":
|
|
34
36
|
await (0, doctor_1.runDoctor)();
|
|
@@ -42,9 +44,6 @@ async function main() {
|
|
|
42
44
|
case "statusline":
|
|
43
45
|
(0, statusline_1.runStatusline)();
|
|
44
46
|
break;
|
|
45
|
-
case "notify":
|
|
46
|
-
(0, notify_1.runNotify)(rest);
|
|
47
|
-
break;
|
|
48
47
|
case "flush":
|
|
49
48
|
await (0, flush_1.runFlush)();
|
|
50
49
|
break;
|
|
@@ -72,10 +71,32 @@ async function main() {
|
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
|
-
|
|
74
|
+
async function dispatch() {
|
|
75
|
+
const command = process.argv[2];
|
|
76
|
+
if (command === "notify") {
|
|
77
|
+
if ((0, notifications_1.enqueueNotification)(process.argv.slice(3)))
|
|
78
|
+
(0, runtime_1.spawnDetachedCommand)(["notify-drain"]);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (command === "notify-drain" || command === "statusline") {
|
|
82
|
+
await (0, lock_1.withPingletLock)(() => {
|
|
83
|
+
(0, notifications_1.drainNotifications)();
|
|
84
|
+
if (command === "statusline")
|
|
85
|
+
(0, statusline_1.runStatusline)();
|
|
86
|
+
}, { skipIfBusy: command === "statusline" });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (["--version", "-v", "help", "--help"].includes(command)) {
|
|
90
|
+
await main();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await (0, lock_1.withCommandLock)(main);
|
|
94
|
+
await (0, lock_1.withPingletLock)(notifications_1.drainNotifications);
|
|
95
|
+
}
|
|
96
|
+
dispatch().catch((error) => {
|
|
76
97
|
// statusline/notify는 조용히 실패해야 Claude/Codex 경험을 해치지 않는다.
|
|
77
98
|
const command = process.argv[2];
|
|
78
|
-
if (command === "statusline" || command === "notify") {
|
|
99
|
+
if (command === "statusline" || command === "notify" || command === "notify-drain") {
|
|
79
100
|
process.exit(0);
|
|
80
101
|
}
|
|
81
102
|
console.error("pinglet:", error instanceof Error ? error.message : error);
|
package/dist/commands/flush.js
CHANGED
|
@@ -1,19 +1,86 @@
|
|
|
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.runFlush = runFlush;
|
|
4
37
|
const config_1 = require("../config");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
5
39
|
const api_1 = require("../api");
|
|
6
40
|
const queue_1 = require("../queue");
|
|
41
|
+
const lock_1 = require("../lock");
|
|
7
42
|
/**
|
|
8
43
|
* Local Event Queue를 batch로 서버에 전송한다 (아키텍처 §8).
|
|
9
44
|
* 전송 성공한 이벤트만 큐에서 제거하므로 실패분은 다음 주기에 재시도되고,
|
|
10
45
|
* 재전송 중복은 서버가 eventId로 dedupe한다.
|
|
11
46
|
*/
|
|
12
47
|
async function runFlush() {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
48
|
+
const snapshot = await (0, lock_1.withPingletLock)(() => {
|
|
49
|
+
if (!fs.existsSync(config_1.CONFIG_PATH))
|
|
50
|
+
return;
|
|
51
|
+
const config = (0, config_1.loadConfig)();
|
|
52
|
+
const events = (0, queue_1.bindEvents)();
|
|
53
|
+
const valid = [];
|
|
54
|
+
for (const event of events) {
|
|
55
|
+
const reason = (0, queue_1.invalidEventReason)(event);
|
|
56
|
+
const record = config.installations[event.agentType];
|
|
57
|
+
if (reason)
|
|
58
|
+
(0, queue_1.quarantineEvents)([event], reason);
|
|
59
|
+
else if (event.installationId && event.installationId !== record?.installationId)
|
|
60
|
+
(0, queue_1.quarantineEvents)([event], "retired-installation");
|
|
61
|
+
else
|
|
62
|
+
valid.push(event);
|
|
63
|
+
}
|
|
64
|
+
return { config, events: valid };
|
|
65
|
+
});
|
|
66
|
+
if (!snapshot)
|
|
16
67
|
return;
|
|
68
|
+
const { config, events } = snapshot;
|
|
69
|
+
const send = async (record, chunk) => {
|
|
70
|
+
const result = await (0, api_1.uploadEventBatchResult)(config, record, chunk);
|
|
71
|
+
if (result.ok) {
|
|
72
|
+
await (0, lock_1.withPingletLock)(() => (0, queue_1.removeEvents)(new Set(chunk.map(event => event.eventId))));
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (!result.permanent)
|
|
76
|
+
return false;
|
|
77
|
+
if (chunk.length === 1) {
|
|
78
|
+
await (0, lock_1.withPingletLock)(() => (0, queue_1.quarantineEvents)(chunk, `http-${result.status}`));
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const middle = Math.floor(chunk.length / 2);
|
|
82
|
+
return await send(record, chunk.slice(0, middle)) && await send(record, chunk.slice(middle));
|
|
83
|
+
};
|
|
17
84
|
for (const agentType of ["CLAUDE", "CODEX"]) {
|
|
18
85
|
const record = config.installations[agentType];
|
|
19
86
|
const group = events.filter((e) => e.agentType === agentType);
|
|
@@ -23,10 +90,8 @@ async function runFlush() {
|
|
|
23
90
|
continue; // 미등록 상태 — refresh가 등록을 재시도한다.
|
|
24
91
|
for (let i = 0; i < group.length; i += api_1.MAX_BATCH_SIZE) {
|
|
25
92
|
const chunk = group.slice(i, i + api_1.MAX_BATCH_SIZE);
|
|
26
|
-
|
|
27
|
-
if (!ok)
|
|
93
|
+
if (!await send(record, chunk))
|
|
28
94
|
break;
|
|
29
|
-
(0, queue_1.removeEvents)(new Set(chunk.map((e) => e.eventId)));
|
|
30
95
|
}
|
|
31
96
|
}
|
|
32
97
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -41,6 +41,7 @@ const cache_1 = require("../cache");
|
|
|
41
41
|
const claude_1 = require("../adapters/claude");
|
|
42
42
|
const codex_1 = require("../adapters/codex");
|
|
43
43
|
const i18n_1 = require("../i18n");
|
|
44
|
+
const lock_1 = require("../lock");
|
|
44
45
|
/**
|
|
45
46
|
* 기존 설정 교체 확인. Enter 또는 y면 진행(기본 Yes — 설치 의도가 명확하므로).
|
|
46
47
|
* non-TTY(postinstall, CI)에서는 묻지 않고 false — 사용자 설정을 말없이
|
|
@@ -72,7 +73,11 @@ async function runInstall(args) {
|
|
|
72
73
|
const apiIndex = args.indexOf("--api");
|
|
73
74
|
if (apiIndex !== -1 && args[apiIndex + 1]) {
|
|
74
75
|
config.apiBaseUrl = args[apiIndex + 1].replace(/\/$/, "");
|
|
75
|
-
(0,
|
|
76
|
+
await (0, lock_1.withPingletLock)(() => {
|
|
77
|
+
const current = (0, config_1.loadConfig)();
|
|
78
|
+
current.apiBaseUrl = config.apiBaseUrl;
|
|
79
|
+
(0, config_1.saveConfig)(current);
|
|
80
|
+
});
|
|
76
81
|
}
|
|
77
82
|
console.log("");
|
|
78
83
|
const hasClaude = wantClaude && (0, claude_1.detectClaude)();
|
|
@@ -99,17 +104,21 @@ async function runInstall(args) {
|
|
|
99
104
|
return;
|
|
100
105
|
const record = await (0, api_1.registerInstallation)(config, agentType);
|
|
101
106
|
if (record) {
|
|
107
|
+
await (0, lock_1.withPingletLock)(() => {
|
|
108
|
+
const current = (0, config_1.loadConfig)();
|
|
109
|
+
current.installations[agentType] = record;
|
|
110
|
+
(0, config_1.saveConfig)(current);
|
|
111
|
+
});
|
|
102
112
|
config.installations[agentType] = record;
|
|
103
|
-
(0, config_1.saveConfig)(config);
|
|
104
113
|
}
|
|
105
114
|
};
|
|
106
115
|
if (hasClaude) {
|
|
107
116
|
await register("CLAUDE");
|
|
108
|
-
let result = (0, claude_1.installClaudeIntegration)(
|
|
117
|
+
let result = (await (0, lock_1.withPingletLock)(() => (0, claude_1.installClaudeIntegration)((0, config_1.loadConfig)(), { force })));
|
|
109
118
|
if (!result.ok && result.needsForce) {
|
|
110
119
|
const yes = await confirmReplace((0, i18n_1.t)("install.askReplaceStatusLine"));
|
|
111
120
|
if (yes)
|
|
112
|
-
result = (0, claude_1.installClaudeIntegration)(
|
|
121
|
+
result = (await (0, lock_1.withPingletLock)(() => (0, claude_1.installClaudeIntegration)((0, config_1.loadConfig)(), { force: true })));
|
|
113
122
|
}
|
|
114
123
|
console.log(result.ok
|
|
115
124
|
? "✓ Claude integration installed"
|
|
@@ -117,11 +126,11 @@ async function runInstall(args) {
|
|
|
117
126
|
}
|
|
118
127
|
if (hasCodex) {
|
|
119
128
|
await register("CODEX");
|
|
120
|
-
let result = (0, codex_1.installCodexIntegration)(
|
|
129
|
+
let result = (await (0, lock_1.withPingletLock)(() => (0, codex_1.installCodexIntegration)((0, config_1.loadConfig)(), { force })));
|
|
121
130
|
if (!result.ok && result.needsForce) {
|
|
122
131
|
const yes = await confirmReplace((0, i18n_1.t)("install.askReplaceNotify"));
|
|
123
132
|
if (yes)
|
|
124
|
-
result = (0, codex_1.installCodexIntegration)(
|
|
133
|
+
result = (await (0, lock_1.withPingletLock)(() => (0, codex_1.installCodexIntegration)((0, config_1.loadConfig)(), { force: true })));
|
|
125
134
|
}
|
|
126
135
|
console.log(result.ok
|
|
127
136
|
? "✓ Codex integration installed (experimental)"
|
|
@@ -132,10 +141,10 @@ async function runInstall(args) {
|
|
|
132
141
|
if (online && record) {
|
|
133
142
|
const messages = await (0, api_1.fetchFeed)(config, record);
|
|
134
143
|
if (messages) {
|
|
135
|
-
(0, cache_1.saveFeedMessages)(messages);
|
|
144
|
+
await (0, lock_1.withPingletLock)(() => (0, cache_1.saveFeedMessages)(messages));
|
|
136
145
|
if (messages.length > 0) {
|
|
137
146
|
console.log(`✓ Feed cached (${messages.length} messages)`);
|
|
138
|
-
if ((0, claude_1.armSpinnerMessage)(
|
|
147
|
+
if (await (0, lock_1.withPingletLock)(() => (0, claude_1.armSpinnerMessage)((0, config_1.loadConfig)(), messages[0] ?? null))) {
|
|
139
148
|
console.log("✓ Claude spinner synced");
|
|
140
149
|
}
|
|
141
150
|
}
|