pinglet-cli 0.2.6 → 0.2.9

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
@@ -12,11 +12,14 @@ Claude Code가 코드를 고치는 동안 "Befuddling…" 스피너만 바라보
12
12
  ## 시작하기
13
13
 
14
14
  ```bash
15
- npm install -g pinglet-cli
15
+ npm install -g pinglet-cli && pinglet install
16
16
  ```
17
17
 
18
- 설치가 끝나면 자동으로 Claude Code에 연결됩니다. 이후는 평소처럼 `claude`를
19
- 실행하기만 하면 돼요. 자동 설정이 안 됐다면 `pinglet install` 한 번이면 됩니다.
18
+ 이게 전부입니다. 이후는 평소처럼 `claude`를 실행하기만 하면 돼요.
19
+
20
+ > 설치 중 npm이 `install-scripts` 경고를 보여줄 수 있습니다 — 구버전 npm에서
21
+ > 초기 설정을 자동화하던 스크립트에 대한 안내로, 무시해도 됩니다.
22
+ > 연결 상태는 `pinglet doctor`로 언제든 확인할 수 있어요.
20
23
 
21
24
  - 기존 statusline 설정이 있다면 **백업 후 교체할지 물어보고**, 제거 시 원래대로 복원합니다.
22
25
  - Codex도 지원합니다(experimental) — turn 완료 시 macOS 알림으로 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
@@ -68,6 +68,16 @@ async function main() {
68
68
  console.log(config_1.VERSION);
69
69
  break;
70
70
  default:
71
+ // 최신 npm은 postinstall을 차단하므로, 설정 전 상태에서 bare `pinglet`을
72
+ // 치면 help 대신 바로 첫 설정을 시작한다 (막다른 길 제거).
73
+ if (!command) {
74
+ const config = (0, config_1.loadConfig)();
75
+ if (!config.adapters.claude && !config.adapters.codex) {
76
+ console.log("pinglet: 아직 설정 전입니다 — 첫 설정을 시작합니다.");
77
+ await (0, install_1.runInstall)([]);
78
+ break;
79
+ }
80
+ }
71
81
  console.log(HELP);
72
82
  if (command && command !== "help" && command !== "--help") {
73
83
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.9",
4
4
  "description": "AI가 생각하는 동안 다른 개발자의 Ping을 만나는 메시지 네트워크 — Claude Code / Codex integration",
5
5
  "license": "MIT",
6
6
  "repository": {