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 +6 -3
- package/dist/adapters/claude.js +38 -1
- package/dist/adapters/codex.js +45 -7
- package/dist/cli.js +10 -0
- package/package.json +1 -1
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
|
-
|
|
19
|
-
|
|
18
|
+
이게 전부입니다. 이후는 평소처럼 `claude`를 실행하기만 하면 돼요.
|
|
19
|
+
|
|
20
|
+
> 설치 중 npm이 `install-scripts` 경고를 보여줄 수 있습니다 — 구버전 npm에서
|
|
21
|
+
> 초기 설정을 자동화하던 스크립트에 대한 안내로, 무시해도 됩니다.
|
|
22
|
+
> 연결 상태는 `pinglet doctor`로 언제든 확인할 수 있어요.
|
|
20
23
|
|
|
21
24
|
- 기존 statusline 설정이 있다면 **백업 후 교체할지 물어보고**, 제거 시 원래대로 복원합니다.
|
|
22
25
|
- Codex도 지원합니다(experimental) — turn 완료 시 macOS 알림으로 Ping이 도착합니다.
|
package/dist/adapters/claude.js
CHANGED
|
@@ -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;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -74,17 +74,53 @@ function readToml() {
|
|
|
74
74
|
return "";
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
83
|
-
return (
|
|
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 =
|
|
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 =
|
|
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;
|