pinglet-cli 0.2.8 → 0.2.10

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.md CHANGED
@@ -5,10 +5,14 @@
5
5
  Claude Code가 코드를 고치는 동안 "Befuddling…" 스피너만 바라보던 그 자리에,
6
6
  다른 개발자들이 남긴 짧은 메시지(**Ping**)가 대신 표시됩니다.
7
7
 
8
+ ![Pinglet 데모 — Claude Code 스피너 자리에 다른 개발자의 Ping이 표시된다](docs/pinglet-demo.gif)
9
+
8
10
  ```
9
11
  ✶ 💌 "금요일 오후 배포는 다음 생에 합시다." (12s · ↓ 1.2k tokens)
10
12
  ```
11
13
 
14
+ 🌐 **홈페이지**: https://pinglet.halluci.co.kr
15
+
12
16
  ## 시작하기
13
17
 
14
18
  ```bash
@@ -23,6 +27,7 @@ npm install -g pinglet-cli && pinglet install
23
27
 
24
28
  - 기존 statusline 설정이 있다면 **백업 후 교체할지 물어보고**, 제거 시 원래대로 복원합니다.
25
29
  - Codex도 지원합니다(experimental) — turn 완료 시 macOS 알림으로 Ping이 도착합니다.
30
+ 알림 방식이 시끄러울 수 있어 기본 설치에서는 제외되며, 원할 때만 `pinglet install --codex`로 켜세요.
26
31
 
27
32
  ## Ping 보내기
28
33
 
@@ -50,7 +55,7 @@ pinglet post "메시지"
50
55
 
51
56
  | 명령 | 설명 |
52
57
  |---|---|
53
- | `pinglet install` | Claude Code / Codex 감지 후 연결 |
58
+ | `pinglet install` | Claude Code 연결 (Codex는 `--codex`로 opt-in) |
54
59
  | `pinglet login` | GitHub 계정 연결 (메시지 작성에 필요) |
55
60
  | `pinglet post "메시지"` | Ping 보내기 |
56
61
  | `pinglet ping` | 지금 표시될 메시지 미리보기 |
@@ -77,10 +77,27 @@ function statusLineCommand() {
77
77
  function isPingletHookEntry(entry) {
78
78
  return (entry.hooks?.some((h) => typeof h.command === "string" && h.command.includes("hook-prompt")) ?? false);
79
79
  }
80
+ /**
81
+ * `/pinglet` 인라인 bash를 허용하는 settings.json 권한 규칙.
82
+ * auto 권한 모드에서는 allow 규칙에 없는 Bash 명령을 LLM 분류기가 심사하는데,
83
+ * 메시지 내용("~해라" 같은 명령문)에 따라 차단될 수 있어 규칙으로 명시한다.
84
+ */
85
+ function postPermissionRule() {
86
+ return `Bash(node "${(0, config_1.cliPath)()}" post:*)`;
87
+ }
88
+ /** Pinglet이 등록한 post 권한 규칙인지 식별 (경로가 바뀐 옛 규칙 포함). */
89
+ function isPingletPermissionRule(rule) {
90
+ return (rule === postPermissionRule() ||
91
+ (rule.startsWith('Bash(node "') &&
92
+ rule.includes("pinglet") &&
93
+ rule.endsWith('" post:*)')));
94
+ }
80
95
  /**
81
96
  * `/pinglet` slash command 파일을 설치한다.
82
97
  * 등록 자체는 command 안의 인라인 bash(`!` 실행)가 수행하고,
83
98
  * 모델은 그 결과 한 줄만 전달하므로 토큰 사용이 매우 적다.
99
+ * $ARGUMENTS는 따옴표로 감싸 메시지 전체가 하나의 인자로 전달되게 한다
100
+ * (메시지 속 단어가 셸/CLI 플래그로 해석되는 것을 방지).
84
101
  */
85
102
  function installSlashCommand() {
86
103
  fs.mkdirSync(COMMANDS_DIR, { recursive: true });
@@ -90,7 +107,7 @@ allowed-tools: Bash(node:*)
90
107
  ---
91
108
  <!-- ${COMMAND_MARKER} -->
92
109
 
93
- 등록 결과: !\`node "${(0, config_1.cliPath)()}" post --quiet $ARGUMENTS\`
110
+ 등록 결과: !\`node "${(0, config_1.cliPath)()}" post --quiet "$ARGUMENTS"\`
94
111
 
95
112
  위 등록 결과 줄을 아무것도 덧붙이거나 바꾸지 말고 그대로 한 줄만 출력하세요.
96
113
  다른 설명, 안내, 작업을 일절 하지 마세요.
@@ -305,6 +322,12 @@ function installClaudeIntegration(config, options = {}) {
305
322
  (0, config_1.saveConfig)(config);
306
323
  settings.statusLine = { type: "command", command: statusLineCommand() };
307
324
  uninstallPromptHook(settings); // 구버전 hook 방식 흔적 정리
325
+ // /pinglet 인라인 bash가 auto 모드 분류기에 막히지 않도록 allow 규칙 등록.
326
+ // 경로가 바뀐(예: node 버전 업) 옛 규칙은 정리하고 현재 규칙 하나만 둔다.
327
+ const permissions = (settings.permissions ??= {});
328
+ const allow = (permissions.allow ??= []);
329
+ const kept = allow.filter((rule) => !isPingletPermissionRule(rule));
330
+ permissions.allow = [...kept, postPermissionRule()];
308
331
  if (!writeSettings(settings, mtimeMs))
309
332
  continue; // 다른 프로세스와 충돌 → 재읽기
310
333
  installSlashCommand();
@@ -338,6 +361,20 @@ function uninstallClaudeIntegration(config) {
338
361
  if (uninstallPromptHook(settings)) {
339
362
  settingsChanged = true;
340
363
  }
364
+ const allow = settings.permissions?.allow;
365
+ if (allow?.some(isPingletPermissionRule)) {
366
+ const kept = allow.filter((rule) => !isPingletPermissionRule(rule));
367
+ if (kept.length > 0) {
368
+ settings.permissions.allow = kept;
369
+ }
370
+ else {
371
+ delete settings.permissions.allow;
372
+ if (Object.keys(settings.permissions).length === 0) {
373
+ delete settings.permissions;
374
+ }
375
+ }
376
+ settingsChanged = true;
377
+ }
341
378
  const verbs = settings.spinnerVerbs;
342
379
  if (verbs?.verbs?.some(isPingletTip)) {
343
380
  const verbsBackup = config.adapters.claude?.spinnerVerbsBackup;
@@ -74,17 +74,53 @@ function readToml() {
74
74
  return "";
75
75
  }
76
76
  }
77
- function findNotifyLine(toml) {
78
- const match = toml.match(/^\s*notify\s*=.*$/m);
79
- return match ? match[0] : null;
77
+ /**
78
+ * config.toml에서 notify 항목 **전체**를 찾는다. TOML 배열은 여러 줄에
79
+ * 걸칠 수 있으므로 (Codex Computer Use 앱 등이 그렇게 쓴다) 값이 `[`로
80
+ * 시작하면 문자열 리터럴을 건너뛰며 짝이 맞는 `]`까지를 항목으로 본다.
81
+ * 한 줄만 매칭하면 교체/삭제 시 배열 본문이 고아로 남아 TOML이 깨진다.
82
+ */
83
+ function findNotifyEntry(toml) {
84
+ const head = toml.match(/^[ \t]*notify[ \t]*=[ \t]*/m);
85
+ if (!head || head.index === undefined)
86
+ return null;
87
+ const start = head.index;
88
+ let i = start + head[0].length;
89
+ if (toml[i] !== "[") {
90
+ const lineEnd = toml.indexOf("\n", i);
91
+ return toml.slice(start, lineEnd === -1 ? toml.length : lineEnd);
92
+ }
93
+ let depth = 0;
94
+ for (; i < toml.length; i++) {
95
+ const ch = toml[i];
96
+ if (ch === '"' || ch === "'") {
97
+ const quote = ch;
98
+ i++;
99
+ while (i < toml.length && toml[i] !== quote) {
100
+ if (quote === '"' && toml[i] === "\\")
101
+ i++; // basic string escape
102
+ i++;
103
+ }
104
+ }
105
+ else if (ch === "[") {
106
+ depth++;
107
+ }
108
+ else if (ch === "]") {
109
+ depth--;
110
+ if (depth === 0)
111
+ return toml.slice(start, i + 1);
112
+ }
113
+ }
114
+ // 괄호 짝이 안 맞음 — 이미 깨진 파일이므로 건드리지 않는다.
115
+ return null;
80
116
  }
81
117
  function isCodexIntegrationInstalled() {
82
- const line = findNotifyLine(readToml());
83
- return (line !== null && (line.includes("pinglet") || line.includes((0, config_1.cliPath)())));
118
+ const entry = findNotifyEntry(readToml());
119
+ return (entry !== null && (entry.includes("pinglet") || entry.includes((0, config_1.cliPath)())));
84
120
  }
85
121
  function installCodexIntegration(config, options = {}) {
86
122
  const toml = readToml();
87
- const existing = findNotifyLine(toml);
123
+ const existing = findNotifyEntry(toml);
88
124
  const isOurs = existing !== null &&
89
125
  (existing.includes("pinglet") || existing.includes((0, config_1.cliPath)()));
90
126
  if (existing && !isOurs && !options.force) {
@@ -95,6 +131,8 @@ function installCodexIntegration(config, options = {}) {
95
131
  };
96
132
  }
97
133
  config.adapters.codex = {
134
+ // 재설치 시 기존 백업(notifyBackup)이 날아가지 않게 유지한다.
135
+ ...config.adapters.codex,
98
136
  installedAt: new Date().toISOString(),
99
137
  configPath: CONFIG_TOML_PATH,
100
138
  ...(existing && !isOurs && { notifyBackup: existing }),
@@ -114,7 +152,7 @@ function installCodexIntegration(config, options = {}) {
114
152
  }
115
153
  function uninstallCodexIntegration(config) {
116
154
  const toml = readToml();
117
- const existing = findNotifyLine(toml);
155
+ const existing = findNotifyEntry(toml);
118
156
  const isOurs = existing !== null &&
119
157
  (existing.includes("pinglet") || existing.includes((0, config_1.cliPath)()));
120
158
  if (!existing || !isOurs)
package/dist/cli.js CHANGED
@@ -16,8 +16,8 @@ const HELP = `pinglet v${config_1.VERSION} — AI가 생각하는 동안 만나
16
16
 
17
17
  사용법: pinglet <command>
18
18
 
19
- install [--api <url>] [--force] Claude Code / Codex 자동 감지 후 integration 설치
20
- [--claude | --codex] 특정 agent만 설치
19
+ install [--api <url>] [--force] Claude Code integration 설치
20
+ [--claude | --codex] Codex는 OS 알림 방식이라 --codex로 opt-in (experimental)
21
21
  uninstall [--purge] integration 제거 (--purge: ~/.pinglet까지 삭제)
22
22
  login [--token <jwt>] GitHub 계정 연결
23
23
  post "메시지" [--category <c>] 메시지 작성 (GitHub 로그인 필요, 읽기는 익명 가능)
@@ -61,11 +61,12 @@ function confirmReplace(question) {
61
61
  }
62
62
  async function runInstall(args) {
63
63
  const force = args.includes("--force");
64
- // --claude / --codex로 대상을 고를 수 있다. 둘 다 없으면 감지된 전부.
64
+ // Claude는 기본 설치 대상. Codex는 TUI에 그릴 수 없어 OS 알림으로 전달하는
65
+ // 방식인데 이게 체감상 시끄러워서, --codex를 명시한 경우에만(opt-in) 설치한다.
65
66
  const onlyClaude = args.includes("--claude");
66
67
  const onlyCodex = args.includes("--codex");
67
68
  const wantClaude = onlyClaude || !onlyCodex;
68
- const wantCodex = onlyCodex || !onlyClaude;
69
+ const wantCodex = onlyCodex;
69
70
  const config = (0, config_1.loadConfig)();
70
71
  const apiIndex = args.indexOf("--api");
71
72
  if (apiIndex !== -1 && args[apiIndex + 1]) {
@@ -76,7 +77,12 @@ async function runInstall(args) {
76
77
  const hasClaude = wantClaude && (0, claude_1.detectClaude)();
77
78
  const hasCodex = wantCodex && (0, codex_1.detectCodex)();
78
79
  console.log(hasClaude ? "✓ Claude Code detected" : "○ Claude Code not found");
79
- console.log(hasCodex ? "✓ Codex detected" : "○ Codex not found");
80
+ if (wantCodex) {
81
+ console.log(hasCodex ? "✓ Codex detected" : "○ Codex not found");
82
+ }
83
+ else if ((0, codex_1.detectCodex)()) {
84
+ console.log("○ Codex 감지됨 — OS 알림 방식이라 기본 설치에서 제외합니다 (원하면 pinglet install --codex)");
85
+ }
80
86
  if (!hasClaude && !hasCodex) {
81
87
  console.log("\n설치할 대상이 없습니다. Claude Code 또는 Codex를 먼저 설치해 주세요.");
82
88
  process.exitCode = 1;
@@ -134,7 +140,10 @@ async function runInstall(args) {
134
140
  }
135
141
  }
136
142
  }
137
- console.log("\n설치 완료! 평소처럼 claude 또는 codex를 실행하면 Ping이 표시됩니다.");
143
+ const targets = [hasClaude && "claude", hasCodex && "codex"]
144
+ .filter(Boolean)
145
+ .join(" 또는 ");
146
+ console.log(`\n설치 완료! 평소처럼 ${targets}를 실행하면 Ping이 표시됩니다.`);
138
147
  console.log(" 상태 확인: pinglet doctor");
139
148
  console.log(' 메시지 작성: pinglet login 후 pinglet post "메시지"');
140
149
  console.log(" 계정 연결: pinglet login (메시지 작성/반응에 필요)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "AI가 생각하는 동안 다른 개발자의 Ping을 만나는 메시지 네트워크 — Claude Code / Codex integration",
5
5
  "license": "MIT",
6
6
  "repository": {