pinglet-cli 0.1.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.
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runPost = runPost;
4
+ const config_1 = require("../config");
5
+ const api_1 = require("../api");
6
+ const REASON_LABELS = {
7
+ EMPTY: "빈 메시지는 등록할 수 없습니다",
8
+ TOO_LONG: "메시지가 너무 깁니다 (기본 100자 제한)",
9
+ URL_NOT_ALLOWED: "URL은 허용되지 않습니다",
10
+ PII_DETECTED: "이메일/전화번호 등 개인정보가 포함되어 있습니다",
11
+ BANNED_WORD: "정책상 검토가 필요한 표현이 포함되어 있습니다",
12
+ };
13
+ /**
14
+ * `pinglet post "메시지" [--category <카테고리>] [--quiet]`
15
+ * --quiet: /pinglet slash command용 — id/로그인 안내 없이 결과 한 줄만 출력.
16
+ */
17
+ async function runPost(args) {
18
+ let category;
19
+ let quiet = false;
20
+ const rest = [];
21
+ for (let i = 0; i < args.length; i++) {
22
+ if (args[i] === "--category" && args[i + 1]) {
23
+ category = args[++i];
24
+ }
25
+ else if (args[i] === "--quiet") {
26
+ quiet = true;
27
+ }
28
+ else {
29
+ rest.push(args[i]);
30
+ }
31
+ }
32
+ const text = rest.join(" ").trim();
33
+ if (!text) {
34
+ console.log('사용법: pinglet post "메시지" [--category <카테고리>]');
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ const config = (0, config_1.loadConfig)();
39
+ const result = await (0, api_1.createMessage)(config, text, category);
40
+ if (!result.ok) {
41
+ switch (result.error) {
42
+ case "UNAUTHORIZED":
43
+ console.log("✗ 인증 정보가 없습니다. 먼저 `pinglet install`을 실행하세요 (로그인 없이도 익명 작성 가능).");
44
+ break;
45
+ case "RATE_LIMITED":
46
+ console.log("✗ 작성 제한(분당 5개)에 걸렸습니다. 잠시 후 다시 시도하세요.");
47
+ break;
48
+ case "NETWORK":
49
+ console.log(`✗ 서버(${config.apiBaseUrl})에 연결할 수 없습니다.`);
50
+ break;
51
+ default:
52
+ console.log("✗ 메시지 등록에 실패했습니다.");
53
+ }
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ const reason = result.moderationReason
58
+ ? REASON_LABELS[result.moderationReason] ?? result.moderationReason
59
+ : undefined;
60
+ switch (result.status) {
61
+ case "APPROVED":
62
+ console.log("✓ 메시지가 등록됐습니다. 곧 다른 개발자들의 터미널에 표시됩니다.");
63
+ if (!quiet) {
64
+ console.log(` id: ${result.id}`);
65
+ if (!config.userToken) {
66
+ console.log(" '익명의 개발자'로 표시됩니다 — 닉네임을 달려면 `pinglet login`");
67
+ }
68
+ }
69
+ break;
70
+ case "PENDING_REVIEW":
71
+ console.log(`○ 메시지가 검토 대기 중입니다${reason ? ` — ${reason}` : ""}.`);
72
+ if (!quiet)
73
+ console.log(" 승인되면 feed에 노출됩니다.");
74
+ break;
75
+ case "REJECTED":
76
+ console.log(`✗ 메시지가 거절됐습니다${reason ? ` — ${reason}` : ""}.`);
77
+ process.exitCode = 1;
78
+ break;
79
+ }
80
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRefresh = runRefresh;
4
+ const config_1 = require("../config");
5
+ const api_1 = require("../api");
6
+ const cache_1 = require("../cache");
7
+ const claude_1 = require("../adapters/claude");
8
+ /**
9
+ * 서버 feed를 미리 받아 Local Feed Cache를 갱신한다 (아키텍처 §6).
10
+ * 설치 당시 오프라인이었다면 installation 등록도 여기서 재시도한다.
11
+ */
12
+ async function runRefresh() {
13
+ const config = (0, config_1.loadConfig)();
14
+ const wanted = [];
15
+ if (config.adapters.claude)
16
+ wanted.push("CLAUDE");
17
+ if (config.adapters.codex)
18
+ wanted.push("CODEX");
19
+ for (const agentType of wanted) {
20
+ if (!config.installations[agentType]) {
21
+ const record = await (0, api_1.registerInstallation)(config, agentType);
22
+ if (record) {
23
+ config.installations[agentType] = record;
24
+ (0, config_1.saveConfig)(config);
25
+ }
26
+ }
27
+ }
28
+ const records = Object.values(config.installations);
29
+ if (records.length === 0)
30
+ return;
31
+ await Promise.all(records.map((r) => (0, api_1.sendHeartbeat)(config, r)));
32
+ const messages = await (0, api_1.fetchFeed)(config, records[0]);
33
+ if (messages) {
34
+ (0, cache_1.saveFeedMessages)(messages);
35
+ // 새 feed를 Claude Code spinner verb에도 반영한다 (설정 파일만 다시 쓰면 됨).
36
+ // feed가 비었으면 spinner를 기본 상태로 되돌린다.
37
+ (0, claude_1.syncSpinnerVerbs)(config, messages);
38
+ }
39
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runStatusline = runStatusline;
4
+ const cache_1 = require("../cache");
5
+ const render_1 = require("../render");
6
+ const runtime_1 = require("../runtime");
7
+ /**
8
+ * Claude Code statusLine hook 진입점.
9
+ * Claude Code가 상태 변경마다(최대 300ms 간격) 호출하며, stdout 한 줄이 status 영역에 그려진다.
10
+ * 이 경로는 반드시 빨라야 하므로 로컬 파일만 읽고, 네트워크 작업은 detached로 넘긴다.
11
+ */
12
+ function runStatusline() {
13
+ const state = (0, cache_1.loadState)();
14
+ const now = Date.now();
15
+ const current = state.current;
16
+ let line;
17
+ if (!current || now - current.shownAt >= runtime_1.ROTATE_MS) {
18
+ const message = (0, runtime_1.rotateMessage)(state, "CLAUDE", now);
19
+ // feed가 없으면 빈 줄 — statusLine에 아무것도 그리지 않는다.
20
+ line = message ? (0, render_1.formatPing)(message) : "";
21
+ (0, runtime_1.runMaintenance)(state, now);
22
+ state.lastTickAt = now;
23
+ (0, cache_1.saveState)(state);
24
+ }
25
+ else {
26
+ line = (0, render_1.formatPing)({
27
+ id: current.messageId,
28
+ text: current.text,
29
+ author: current.author,
30
+ contentType: "USER",
31
+ });
32
+ // visibleMs 추정용 tick. 300ms마다 쓰지 않도록 1초 스로틀.
33
+ if (state.lastTickAt === undefined || now - state.lastTickAt > 1_000) {
34
+ state.lastTickAt = now;
35
+ (0, cache_1.saveState)(state);
36
+ }
37
+ }
38
+ process.stdout.write(line);
39
+ }
@@ -0,0 +1,58 @@
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.runUninstall = runUninstall;
37
+ const fs = __importStar(require("fs"));
38
+ const config_1 = require("../config");
39
+ const claude_1 = require("../adapters/claude");
40
+ const codex_1 = require("../adapters/codex");
41
+ function runUninstall(args) {
42
+ const config = (0, config_1.loadConfig)();
43
+ const claudeRemoved = (0, claude_1.uninstallClaudeIntegration)(config);
44
+ const codexRemoved = (0, codex_1.uninstallCodexIntegration)(config);
45
+ console.log(claudeRemoved
46
+ ? "✓ Claude integration removed"
47
+ : "○ Claude integration not installed");
48
+ console.log(codexRemoved
49
+ ? "✓ Codex integration removed"
50
+ : "○ Codex integration not installed");
51
+ if (args.includes("--purge")) {
52
+ fs.rmSync(config_1.PINGLET_DIR, { recursive: true, force: true });
53
+ console.log("✓ ~/.pinglet removed (config / cache / event queue)");
54
+ }
55
+ else {
56
+ console.log("○ ~/.pinglet은 유지됩니다. 전부 지우려면 --purge를 사용하세요.");
57
+ }
58
+ }
package/dist/config.js ADDED
@@ -0,0 +1,92 @@
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.DEFAULT_API_BASE_URL = exports.EVENTS_PATH = exports.STATE_PATH = exports.FEED_PATH = exports.CONFIG_PATH = exports.PINGLET_DIR = exports.VERSION = void 0;
37
+ exports.ensurePingletDir = ensurePingletDir;
38
+ exports.readJsonFile = readJsonFile;
39
+ exports.writeJsonFile = writeJsonFile;
40
+ exports.loadConfig = loadConfig;
41
+ exports.saveConfig = saveConfig;
42
+ exports.cliPath = cliPath;
43
+ const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
45
+ const path = __importStar(require("path"));
46
+ exports.VERSION = "0.1.0";
47
+ exports.PINGLET_DIR = path.join(os.homedir(), ".pinglet");
48
+ exports.CONFIG_PATH = path.join(exports.PINGLET_DIR, "config.json");
49
+ exports.FEED_PATH = path.join(exports.PINGLET_DIR, "feed.json");
50
+ exports.STATE_PATH = path.join(exports.PINGLET_DIR, "state.json");
51
+ exports.EVENTS_PATH = path.join(exports.PINGLET_DIR, "events.jsonl");
52
+ exports.DEFAULT_API_BASE_URL = process.env.PINGLET_API_URL ?? "https://pinglet.halluci.co.kr";
53
+ function ensurePingletDir() {
54
+ fs.mkdirSync(exports.PINGLET_DIR, { recursive: true });
55
+ }
56
+ function readJsonFile(filePath) {
57
+ try {
58
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function writeJsonFile(filePath, value) {
65
+ ensurePingletDir();
66
+ const tmp = filePath + ".tmp";
67
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2));
68
+ fs.renameSync(tmp, filePath);
69
+ }
70
+ function loadConfig() {
71
+ const existing = readJsonFile(exports.CONFIG_PATH);
72
+ if (existing) {
73
+ existing.installations ??= {};
74
+ existing.adapters ??= {};
75
+ return existing;
76
+ }
77
+ const fresh = {
78
+ apiBaseUrl: exports.DEFAULT_API_BASE_URL,
79
+ createdAt: new Date().toISOString(),
80
+ installations: {},
81
+ adapters: {},
82
+ };
83
+ writeJsonFile(exports.CONFIG_PATH, fresh);
84
+ return fresh;
85
+ }
86
+ function saveConfig(config) {
87
+ writeJsonFile(exports.CONFIG_PATH, config);
88
+ }
89
+ /** 현재 실행 중인 CLI 엔트리(dist/cli.js)의 절대 경로. adapter command에 박아 넣는다. */
90
+ function cliPath() {
91
+ return fs.realpathSync(process.argv[1]);
92
+ }
package/dist/picker.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickMessage = pickMessage;
4
+ /**
5
+ * 로컬 메시지 선택 (아키텍처 §12).
6
+ * 서버가 이미 scoring해서 내려주므로 클라이언트는
7
+ * seen penalty + 같은 메시지/작성자 연속 노출 억제만 담당한다.
8
+ */
9
+ function pickMessage(messages, state) {
10
+ if (messages.length === 0)
11
+ return null;
12
+ const current = state.current;
13
+ let candidates = messages;
14
+ if (messages.length > 1 && current) {
15
+ const filtered = messages.filter((m) => m.id !== current.messageId);
16
+ // 작성자 다양성: 직전과 같은 작성자를 피할 수 있으면 피한다.
17
+ const diverse = filtered.filter((m) => m.author !== current.author);
18
+ candidates = diverse.length > 0 ? diverse : filtered;
19
+ }
20
+ const weights = candidates.map((m) => 1 / (1 + (state.seen[m.id] ?? 0)));
21
+ const total = weights.reduce((sum, w) => sum + w, 0);
22
+ let roll = Math.random() * total;
23
+ for (let i = 0; i < candidates.length; i++) {
24
+ roll -= weights[i];
25
+ if (roll <= 0)
26
+ return candidates[i];
27
+ }
28
+ return candidates[candidates.length - 1];
29
+ }
package/dist/queue.js ADDED
@@ -0,0 +1,91 @@
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.appendEvent = appendEvent;
37
+ exports.readEvents = readEvents;
38
+ exports.countEvents = countEvents;
39
+ exports.removeEvents = removeEvents;
40
+ const fs = __importStar(require("fs"));
41
+ const crypto_1 = require("crypto");
42
+ const config_1 = require("./config");
43
+ /**
44
+ * Local Event Queue (append-only JSONL).
45
+ * 노출 즉시 서버를 호출하지 않고 여기 기록했다가 batch 전송한다 (아키텍처 §8).
46
+ */
47
+ function appendEvent(input) {
48
+ (0, config_1.ensurePingletDir)();
49
+ const event = {
50
+ eventId: "evt_" + (0, crypto_1.randomUUID)(),
51
+ agentType: input.agentType,
52
+ type: input.type,
53
+ messageId: input.messageId,
54
+ ...(input.visibleMs !== undefined && { visibleMs: input.visibleMs }),
55
+ occurredAt: new Date().toISOString(),
56
+ };
57
+ fs.appendFileSync(config_1.EVENTS_PATH, JSON.stringify(event) + "\n");
58
+ }
59
+ function readEvents() {
60
+ let raw;
61
+ try {
62
+ raw = fs.readFileSync(config_1.EVENTS_PATH, "utf8");
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ const events = [];
68
+ for (const line of raw.split("\n")) {
69
+ if (!line.trim())
70
+ continue;
71
+ try {
72
+ const parsed = JSON.parse(line);
73
+ if (parsed.eventId && parsed.messageId && parsed.type) {
74
+ events.push(parsed);
75
+ }
76
+ }
77
+ catch {
78
+ // 손상된 라인은 버린다.
79
+ }
80
+ }
81
+ return events;
82
+ }
83
+ function countEvents() {
84
+ return readEvents().length;
85
+ }
86
+ /** 전송에 성공한 이벤트만 제거하고, 그 사이에 append된 이벤트는 보존한다. */
87
+ function removeEvents(sentEventIds) {
88
+ const remaining = readEvents().filter((e) => !sentEventIds.has(e.eventId));
89
+ const body = remaining.map((e) => JSON.stringify(e)).join("\n");
90
+ fs.writeFileSync(config_1.EVENTS_PATH, body ? body + "\n" : "");
91
+ }
package/dist/render.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supportsUnicode = supportsUnicode;
4
+ exports.formatPing = formatPing;
5
+ /** 터미널 독립 원칙: unicode가 불확실하면 plain ASCII로 fallback (아키텍처 §5). */
6
+ function supportsUnicode() {
7
+ if (process.platform === "win32")
8
+ return true; // Windows Terminal 기준
9
+ const locale = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
10
+ return /utf-?8/i.test(locale);
11
+ }
12
+ /** 작성자 닉네임 없이 메시지만 표시한다 (statusline / spinner verb 공용). */
13
+ function formatPing(message) {
14
+ const sponsored = message.contentType === "SPONSORED" ? "[AD] " : "";
15
+ if (supportsUnicode()) {
16
+ return `💌 ${sponsored}"${message.text}"`;
17
+ }
18
+ return `[ping] ${sponsored}"${message.text}"`;
19
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FEED_TTL_MS = exports.FLUSH_COUNT = exports.FLUSH_INTERVAL_MS = exports.QUALIFIED_MS = exports.ROTATE_MS = void 0;
4
+ exports.rotateMessage = rotateMessage;
5
+ exports.runMaintenance = runMaintenance;
6
+ const child_process_1 = require("child_process");
7
+ const config_1 = require("./config");
8
+ const cache_1 = require("./cache");
9
+ const picker_1 = require("./picker");
10
+ const queue_1 = require("./queue");
11
+ exports.ROTATE_MS = 30_000; // 메시지 교체 주기
12
+ exports.QUALIFIED_MS = 3_000; // 정책상 최소 노출 시간 (아키텍처 §7.2)
13
+ exports.FLUSH_INTERVAL_MS = 60_000; // batch 전송 주기
14
+ exports.FLUSH_COUNT = 20; // 또는 이벤트 개수 기준
15
+ exports.FEED_TTL_MS = 30 * 60_000; // feed 캐시 갱신 주기
16
+ /** 구버전 시드 메시지 id는 서버에 존재하지 않으므로 이벤트를 만들지 않는다. */
17
+ function isServerMessage(messageId) {
18
+ return !messageId.startsWith("seed_");
19
+ }
20
+ /**
21
+ * 이전 메시지의 QUALIFIED_IMPRESSION을 정산하고 다음 메시지를 뽑는다.
22
+ * 렌더링 path이므로 네트워크 호출 없이 로컬 캐시만 읽는다 (Local-first).
23
+ * feed가 비어 있으면 null — 호출부는 아무것도 표시하지 않는다.
24
+ */
25
+ function rotateMessage(state, agentType, now) {
26
+ const prev = state.current;
27
+ if (prev && isServerMessage(prev.messageId)) {
28
+ // 세션이 중간에 끊겼으면 마지막 tick까지만 보였다고 간주한다.
29
+ const sessionGap = state.lastTickAt !== undefined && now - state.lastTickAt > exports.ROTATE_MS * 2;
30
+ const visibleEnd = sessionGap ? state.lastTickAt : now;
31
+ const visibleMs = Math.max(0, Math.min(visibleEnd - prev.shownAt, exports.ROTATE_MS));
32
+ if (visibleMs >= exports.QUALIFIED_MS) {
33
+ (0, queue_1.appendEvent)({
34
+ agentType,
35
+ type: "QUALIFIED_IMPRESSION",
36
+ messageId: prev.messageId,
37
+ visibleMs,
38
+ });
39
+ }
40
+ }
41
+ const message = (0, picker_1.pickMessage)((0, cache_1.loadFeedMessages)(), state);
42
+ if (!message) {
43
+ delete state.current;
44
+ return null;
45
+ }
46
+ if (isServerMessage(message.id)) {
47
+ (0, queue_1.appendEvent)({ agentType, type: "DELIVERED", messageId: message.id });
48
+ }
49
+ state.seen[message.id] = (state.seen[message.id] ?? 0) + 1;
50
+ state.current = {
51
+ messageId: message.id,
52
+ text: message.text,
53
+ author: message.author,
54
+ shownAt: now,
55
+ };
56
+ return message;
57
+ }
58
+ function spawnDetachedCommand(args) {
59
+ try {
60
+ const child = (0, child_process_1.spawn)(process.execPath, [(0, config_1.cliPath)(), ...args], {
61
+ detached: true,
62
+ stdio: "ignore",
63
+ });
64
+ child.unref();
65
+ }
66
+ catch {
67
+ // 백그라운드 작업 실패는 렌더링에 영향을 주면 안 된다.
68
+ }
69
+ }
70
+ /**
71
+ * flush/refresh가 필요하면 detached 프로세스로 넘긴다.
72
+ * 렌더링 프로세스는 절대 네트워크를 기다리지 않는다 (Async Telemetry).
73
+ */
74
+ function runMaintenance(state, now) {
75
+ const queued = (0, queue_1.countEvents)();
76
+ const flushDue = queued >= exports.FLUSH_COUNT ||
77
+ (queued > 0 &&
78
+ (state.lastFlushAt === undefined ||
79
+ now - state.lastFlushAt >= exports.FLUSH_INTERVAL_MS));
80
+ if (flushDue) {
81
+ state.lastFlushAt = now; // spawn 시점 기록 — 실패해도 다음 주기에 재시도
82
+ spawnDetachedCommand(["flush"]);
83
+ }
84
+ const age = (0, cache_1.feedAgeMs)();
85
+ const refreshDue = (age === null || age >= exports.FEED_TTL_MS) &&
86
+ (state.lastRefreshAt === undefined ||
87
+ now - state.lastRefreshAt >= 5 * 60_000);
88
+ if (refreshDue) {
89
+ state.lastRefreshAt = now;
90
+ spawnDetachedCommand(["refresh"]);
91
+ }
92
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "pinglet-cli",
3
+ "version": "0.1.0",
4
+ "description": "AI가 생각하는 동안 다른 개발자의 Ping을 만나는 메시지 네트워크 — Claude Code / Codex integration",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mabyko/pinglet-client.git"
9
+ },
10
+ "homepage": "https://github.com/mabyko/pinglet-client#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/mabyko/pinglet-client/issues"
13
+ },
14
+ "keywords": [
15
+ "claude-code",
16
+ "codex",
17
+ "statusline",
18
+ "spinner",
19
+ "cli",
20
+ "developer-community"
21
+ ],
22
+ "bin": {
23
+ "pinglet": "dist/cli.js"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "dev": "tsc --watch",
31
+ "prepare": "npm run build"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.14.0",
38
+ "typescript": "^5.5.0"
39
+ }
40
+ }