pinglet-cli 0.3.1 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mabyko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md CHANGED
@@ -56,6 +56,12 @@ pinglet post "メッセージ"
56
56
 
57
57
  statusline の言語はシステム言語に応じて自動選択されます(韓国語/日本語、それ以外は英語)。言語で判定できない場合はタイムゾーン(ソウル/東京)で推定します。
58
58
 
59
+ ## 自動アップデート
60
+
61
+ 新しいバージョンが出ると、バックグラウンドで1日1回チェックして自動的に
62
+ アップデートします(npm グローバルインストールのみ — pnpm/yarn は対象外)。
63
+ 無効にするには `~/.pinglet/config.json` に `"autoUpdate": false` を追加してください。
64
+
59
65
  ## コマンド
60
66
 
61
67
  | コマンド | 説明 |
package/README.ko.md CHANGED
@@ -56,6 +56,12 @@ pinglet post "메시지"
56
56
 
57
57
  statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어/일본어, 그 외는 영어). 시스템 언어로 판정이 안 되면 타임존(서울/도쿄)으로 추정합니다.
58
58
 
59
+ ## 자동 업데이트
60
+
61
+ 새 버전이 나오면 백그라운드에서 하루 1회 확인해 자동으로 업데이트합니다
62
+ (npm 전역 설치일 때만 — pnpm/yarn 설치는 건드리지 않습니다).
63
+ 끄려면 `~/.pinglet/config.json`에 `"autoUpdate": false`를 추가하세요.
64
+
59
65
  ## 명령어
60
66
 
61
67
  | 명령 | 설명 |
package/README.md CHANGED
@@ -56,6 +56,12 @@ The statusline shows how many terminals are coding alongside you right now:
56
56
 
57
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
58
 
59
+ ## Auto-update
60
+
61
+ Pinglet checks for a new version once a day in the background and updates
62
+ itself automatically (only for npm global installs — pnpm/yarn installs are
63
+ left untouched). To opt out, add `"autoUpdate": false` to `~/.pinglet/config.json`.
64
+
59
65
  ## Commands
60
66
 
61
67
  | Command | Description |
@@ -73,6 +73,11 @@ function detectClaude() {
73
73
  function statusLineCommand() {
74
74
  return `node "${(0, config_1.cliPath)()}" statusline`;
75
75
  }
76
+ /**
77
+ * statusline 이벤트 트리거는 유휴 세션에서 멈추므로(새 메시지가 있어야 재실행),
78
+ * 유휴 pane이 옛 온라인 터미널 수를 계속 표시하지 않게 고정 주기(초)로도 재실행한다.
79
+ */
80
+ const STATUSLINE_REFRESH_INTERVAL_S = 30;
76
81
  /** 예전 버전이 등록한 hook entry인지 식별 (command에 hook-prompt 포함 여부). */
77
82
  function isPingletHookEntry(entry) {
78
83
  return (entry.hooks?.some((h) => typeof h.command === "string" && h.command.includes("hook-prompt")) ?? false);
@@ -336,7 +341,11 @@ function installClaudeIntegration(config, options = {}) {
336
341
  ...(existing && !isOurs && { statusLineBackup: existing }),
337
342
  };
338
343
  (0, config_1.saveConfig)(config);
339
- settings.statusLine = { type: "command", command: statusLineCommand() };
344
+ settings.statusLine = {
345
+ type: "command",
346
+ command: statusLineCommand(),
347
+ refreshInterval: STATUSLINE_REFRESH_INTERVAL_S,
348
+ };
340
349
  uninstallPromptHook(settings); // 구버전 hook 방식 흔적 정리
341
350
  // /pinglet 인라인 bash가 auto 모드 분류기에 막히지 않도록 allow 규칙 등록.
342
351
  // 경로가 바뀐(예: node 버전 업) 옛 규칙은 정리하고 현재 규칙 하나만 둔다.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runRefresh = runRefresh;
4
4
  const config_1 = require("../config");
5
5
  const api_1 = require("../api");
6
+ const update_1 = require("../update");
6
7
  const cache_1 = require("../cache");
7
8
  const claude_1 = require("../adapters/claude");
8
9
  /**
@@ -46,4 +47,6 @@ async function runRefresh() {
46
47
  (0, cache_1.saveState)(state);
47
48
  }
48
49
  }
50
+ // 하루 1회 새 버전 확인 후 npm 전역 설치면 백그라운드 자동 업데이트.
51
+ await (0, update_1.maybeSelfUpdate)(config);
49
52
  }
@@ -43,10 +43,11 @@ const queue_1 = require("../queue");
43
43
  const claude_1 = require("../adapters/claude");
44
44
  const runtime_1 = require("../runtime");
45
45
  /**
46
- * online 캐시 유효 기간 — refresh(≈5분) 3주기. 오프라인이 되면
47
- * 오래된 숫자를 "지금"이라고 보여주는 대신 표시를 접는다.
46
+ * online 캐시 유효 기간 — refresh(≈5분) 2주기. 서버의 온라인 판정 창
47
+ * (ONLINE_WINDOW_MS 10분)과 정렬해, 서버가 이미 오프라인으로 세는 시점에
48
+ * 클라이언트가 옛 숫자를 "지금"이라고 보여주지 않게 한다.
48
49
  */
49
- const ONLINE_STALE_MS = 15 * 60_000;
50
+ const ONLINE_STALE_MS = 10 * 60_000;
50
51
  /** 신선한 online 캐시가 있을 때, 나를 제외한 켜진 터미널 수 (없거나 혼자면 null). */
51
52
  function onlineOthers(now) {
52
53
  const cache = (0, cache_1.loadOnline)();
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.ONLINE_PATH = exports.POSTS_PATH = exports.EVENTS_PATH = exports.STATE_PATH = exports.FEED_PATH = exports.CONFIG_PATH = exports.PINGLET_DIR = exports.VERSION = void 0;
36
+ exports.DEFAULT_API_BASE_URL = exports.UPDATE_PATH = 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;
@@ -62,6 +62,8 @@ exports.EVENTS_PATH = path.join(exports.PINGLET_DIR, "events.jsonl");
62
62
  exports.POSTS_PATH = path.join(exports.PINGLET_DIR, "posts.json");
63
63
  /** 지금 켜져 있는 터미널 수 캐시 — heartbeat 응답으로 갱신, statusline이 읽는다 */
64
64
  exports.ONLINE_PATH = path.join(exports.PINGLET_DIR, "online.json");
65
+ /** 자동 업데이트 진행 기록 — refresh가 하루 1회 갱신 */
66
+ exports.UPDATE_PATH = path.join(exports.PINGLET_DIR, "update.json");
65
67
  exports.DEFAULT_API_BASE_URL = process.env.PINGLET_API_URL ?? "https://pinglet.halluci.co.kr";
66
68
  function ensurePingletDir() {
67
69
  fs.mkdirSync(exports.PINGLET_DIR, { recursive: true });
package/dist/update.js ADDED
@@ -0,0 +1,122 @@
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.isNewerVersion = isNewerVersion;
37
+ exports.maybeSelfUpdate = maybeSelfUpdate;
38
+ const child_process_1 = require("child_process");
39
+ const path = __importStar(require("path"));
40
+ const config_1 = require("./config");
41
+ const REGISTRY_LATEST_URL = "https://registry.npmjs.org/pinglet-cli/latest";
42
+ const CHECK_INTERVAL_MS = 24 * 3600_000; // 하루 1회 확인
43
+ const RETRY_INTERVAL_MS = 7 * 24 * 3600_000; // 같은 버전 재시도는 주 1회
44
+ /** Windows에서 npm은 npm.cmd라 shell 경유가 필요하다 (Node의 .cmd spawn 제한). */
45
+ function npmSpawnOptions() {
46
+ return process.platform === "win32" ? { shell: true } : {};
47
+ }
48
+ async function fetchLatestVersion() {
49
+ try {
50
+ const res = await fetch(REGISTRY_LATEST_URL, {
51
+ signal: AbortSignal.timeout(5000),
52
+ });
53
+ if (!res.ok)
54
+ return null;
55
+ const body = (await res.json());
56
+ return typeof body.version === "string" ? body.version : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /** 단순 semver(major.minor.patch) 비교. 형식이 어긋나면 업데이트하지 않는 쪽으로 판정한다. */
63
+ function isNewerVersion(latest, current) {
64
+ const parse = (v) => v.trim().split(".").map((n) => Number(n));
65
+ const a = parse(latest);
66
+ const b = parse(current);
67
+ if (a.length !== 3 || b.length !== 3)
68
+ return false;
69
+ if ([...a, ...b].some((n) => !Number.isInteger(n) || n < 0))
70
+ return false;
71
+ for (let i = 0; i < 3; i++) {
72
+ if (a[i] !== b[i])
73
+ return a[i] > b[i];
74
+ }
75
+ return false;
76
+ }
77
+ /** npm 전역 루트. npm이 없거나 실패하면 null — 자동 업데이트를 포기한다. */
78
+ function npmGlobalRoot() {
79
+ return new Promise((resolve) => {
80
+ (0, child_process_1.execFile)("npm", ["root", "-g"], { timeout: 10_000, windowsHide: true, ...npmSpawnOptions() }, (err, stdout) => resolve(err ? null : stdout.trim() || null));
81
+ });
82
+ }
83
+ /**
84
+ * 새 버전이 있으면 백그라운드로 자기 자신을 업데이트한다.
85
+ * - npm 전역 설치일 때만 실행한다 (cliPath가 `npm root -g` 아래인지 확인).
86
+ * pnpm/yarn/링크 설치에 npm을 덧씌우면 다른 복사본만 생기고 적용도 안 된다.
87
+ * - statusline은 매 틱 새 프로세스로 뜨므로, 설치가 끝나면 다음 틱부터
88
+ * 새 버전이 실행된다. 전역 경로는 버전과 무관해 설정 변경도 필요 없다.
89
+ * - 실패해도 조용히 넘어간다 (렌더링·refresh에 영향 금지). 재시도는 주 1회.
90
+ */
91
+ async function maybeSelfUpdate(config) {
92
+ if (config.autoUpdate === false)
93
+ return;
94
+ const now = Date.now();
95
+ const state = (0, config_1.readJsonFile)(config_1.UPDATE_PATH) ?? {};
96
+ const lastCheck = state.lastCheckAt ? Date.parse(state.lastCheckAt) : NaN;
97
+ if (!Number.isNaN(lastCheck) && now - lastCheck < CHECK_INTERVAL_MS)
98
+ return;
99
+ // 확인 시각을 먼저 기록 — 레지스트리 장애 시에도 하루 1회 이상 두드리지 않는다.
100
+ state.lastCheckAt = new Date(now).toISOString();
101
+ (0, config_1.writeJsonFile)(config_1.UPDATE_PATH, state);
102
+ const latest = await fetchLatestVersion();
103
+ if (!latest || !isNewerVersion(latest, config_1.VERSION))
104
+ return;
105
+ if (state.lastAttempt?.version === latest) {
106
+ const at = Date.parse(state.lastAttempt.at);
107
+ if (!Number.isNaN(at) && now - at < RETRY_INTERVAL_MS)
108
+ return;
109
+ }
110
+ const root = await npmGlobalRoot();
111
+ if (!root || !(0, config_1.cliPath)().startsWith(root + path.sep))
112
+ return;
113
+ state.lastAttempt = { version: latest, at: new Date(now).toISOString() };
114
+ (0, config_1.writeJsonFile)(config_1.UPDATE_PATH, state);
115
+ try {
116
+ const child = (0, child_process_1.spawn)("npm", ["install", "-g", `pinglet-cli@${latest}`], { detached: true, stdio: "ignore", windowsHide: true, ...npmSpawnOptions() });
117
+ child.unref();
118
+ }
119
+ catch {
120
+ // npm 실행 실패 — 다음 주기에 재시도된다.
121
+ }
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.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": {