pinglet-cli 0.7.0 → 0.8.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/dist/api.js CHANGED
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MAX_BATCH_SIZE = void 0;
37
37
  exports.checkHealth = checkHealth;
38
38
  exports.registerInstallation = registerInstallation;
39
+ exports.safeLinkUrl = safeLinkUrl;
39
40
  exports.fetchFeed = fetchFeed;
40
41
  exports.uploadEventBatch = uploadEventBatch;
41
42
  exports.uploadEventBatchResult = uploadEventBatchResult;
@@ -91,6 +92,24 @@ async function registerInstallation(config, agentType) {
91
92
  registeredAt: new Date().toISOString(),
92
93
  };
93
94
  }
95
+ const MAX_URL_LENGTH = 2048;
96
+ /**
97
+ * statusline이 OSC 8 링크로 감싸므로, ESC·BEL 같은 제어 문자나 공백이 있으면
98
+ * 시퀀스를 탈출할 수 있다. http(s)만 받고 나머지는 링크 없이 제목만 보여준다.
99
+ */
100
+ function safeLinkUrl(url) {
101
+ if (typeof url !== "string" || url.length === 0 || url.length > MAX_URL_LENGTH)
102
+ return null;
103
+ if (/[\s\p{Cc}\p{Cf}]/u.test(url))
104
+ return null;
105
+ try {
106
+ const parsed = new URL(url);
107
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? url : null;
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
94
113
  /** GET /feed — 로컬 캐시용 메시지 30~50개를 미리 받는다. */
95
114
  async function fetchFeed(config, record, limit = 50) {
96
115
  const res = await request(config, "GET", `/feed?limit=${limit}`, { token: record.token });
@@ -104,9 +123,10 @@ async function fetchFeed(config, record, limit = 50) {
104
123
  author: item.authorNickname || "익명의 개발자",
105
124
  category: item.category,
106
125
  expiresAt: item.expiresAt,
107
- contentType: item.contentType === "SYSTEM" || item.contentType === "SPONSORED"
126
+ contentType: item.contentType === "SYSTEM" || item.contentType === "SPONSORED" || item.contentType === "NEWS"
108
127
  ? item.contentType
109
128
  : "USER",
129
+ url: safeLinkUrl(item.url),
110
130
  }));
111
131
  }
112
132
  /** POST /events/batch — eventId 기반으로 서버가 dedupe하므로 재전송에 안전하다. */
@@ -44,23 +44,6 @@ const queue_1 = require("../queue");
44
44
  const claude_1 = require("../adapters/claude");
45
45
  const runtime_1 = require("../runtime");
46
46
  const hud_1 = require("../hud");
47
- /**
48
- * online 캐시 유효 기간 — refresh(≈5분) 2주기. 서버의 온라인 판정 창
49
- * (ONLINE_WINDOW_MS 10분)과 정렬해, 서버가 이미 오프라인으로 세는 시점에
50
- * 클라이언트가 옛 숫자를 "지금"이라고 보여주지 않게 한다.
51
- */
52
- const ONLINE_STALE_MS = 10 * 60_000;
53
- /** 신선한 online 캐시가 있을 때, 나를 제외한 켜진 터미널 수 (없거나 혼자면 null). */
54
- function onlineOthers(now) {
55
- const cache = (0, cache_1.loadOnline)();
56
- if (!cache)
57
- return null;
58
- const t = Date.parse(cache.updatedAt);
59
- if (Number.isNaN(t) || now - t > ONLINE_STALE_MS)
60
- return null;
61
- const others = cache.onlineInstallations - 1; // 이 기기의 heartbeat 제외
62
- return others >= 1 ? others : null;
63
- }
64
47
  function readPayload() {
65
48
  try {
66
49
  const parsed = JSON.parse(fs.readFileSync(0, "utf8"));
@@ -136,6 +119,7 @@ function rotateSpinner(state, now) {
136
119
  messageId: message.id,
137
120
  text: message.text,
138
121
  author: message.author,
122
+ url: message.url ?? null,
139
123
  shownAt: now,
140
124
  visibleMs: 0,
141
125
  };
@@ -172,13 +156,14 @@ function runStatusline() {
172
156
  (0, cache_1.saveState)(state);
173
157
  }
174
158
  const lines = [];
175
- const others = onlineOthers(now);
176
- if (others !== null)
177
- lines.push((0, render_1.formatOnlineNow)(others));
159
+ // spinner와 같은 항목을 링크로 보여준다. HUD 세그먼트 폭 계산(hud/width)은
160
+ // OSC 8을 모르므로 HUD 안이 아니라 별도 줄로 둔다.
161
+ if (state.current?.url)
162
+ lines.push((0, render_1.formatNewsLink)(state.current.text, state.current.url));
178
163
  return { payload, lines, hud: (0, hud_1.loadHudConfig)(config) };
179
164
  }
180
165
  /**
181
- * 잠금 밖에서 HUD(모델·컨텍스트·사용량·활동)를 그려 "함께 코딩 중" 줄 아래에 출력한다.
166
+ * 잠금 밖에서 HUD(모델·컨텍스트·사용량·활동)를 그려 뉴스 링크 줄 아래에 출력한다.
182
167
  * HUD는 실패해도 위 줄에 영향을 주면 안 된다.
183
168
  */
184
169
  async function writeStatusline(tick, now = Date.now(), write = (text) => { process.stdout.write(text); }) {
package/dist/render.js CHANGED
@@ -4,6 +4,7 @@ exports.detectLocale = detectLocale;
4
4
  exports.supportsUnicode = supportsUnicode;
5
5
  exports.sanitizeForTerminal = sanitizeForTerminal;
6
6
  exports.formatPing = formatPing;
7
+ exports.formatNewsLink = formatNewsLink;
7
8
  exports.formatOnlineNow = formatOnlineNow;
8
9
  /**
9
10
  * statusline 표시 언어. 시스템 언어를 먼저 보고(사용자가 실제로 읽는
@@ -53,6 +54,9 @@ function sanitizeForTerminal(text) {
53
54
  }
54
55
  /** 작성자 닉네임 없이 메시지만 표시한다 (spinner verb용). */
55
56
  function formatPing(message) {
57
+ if (message.contentType === "NEWS") {
58
+ return `${supportsUnicode() ? "📰" : "[news]"} ${sanitizeForTerminal(message.text)}`;
59
+ }
56
60
  const sponsored = message.contentType === "SPONSORED" ? "[AD] " : "";
57
61
  const text = sanitizeForTerminal(message.text);
58
62
  if (supportsUnicode()) {
@@ -60,6 +64,17 @@ function formatPing(message) {
60
64
  }
61
65
  return `[ping] ${sponsored}"${text}"`;
62
66
  }
67
+ /**
68
+ * statusline용 뉴스 한 줄: 제목을 OSC 8 하이퍼링크로 감싸 클릭하면 원문으로 간다.
69
+ * 제목은 감싸기 전에 sanitize한다 — 완성된 escape를 sanitize에 넣으면 시퀀스가 지워진다.
70
+ * url은 fetchFeed의 safeLinkUrl을 통과한 값만 온다. 없으면 제목만 보여준다.
71
+ */
72
+ function formatNewsLink(title, url) {
73
+ const text = `${supportsUnicode() ? "📰" : "[news]"} ${sanitizeForTerminal(title)}`;
74
+ if (!url)
75
+ return text;
76
+ return `\u001b]8;;${url}\u001b\\${text}\u001b]8;;\u001b\\`;
77
+ }
63
78
  /** statusline용: 지금 함께 켜져 있는 다른 터미널 수 한 줄. */
64
79
  function formatOnlineNow(othersCount) {
65
80
  const n = othersCount;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {