pinglet-cli 0.5.0 → 0.5.2

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.ja.md CHANGED
@@ -36,7 +36,9 @@ npm install -g pinglet-cli && pinglet install
36
36
  Claude Code セッションの中からそのまま:
37
37
 
38
38
  ```
39
+ > /pinglet-login # 初回のみ — ブラウザで GitHub または Google ログイン
39
40
  > /pinglet 今日もビルドが緑でありますように
41
+ > /pinglet-logout # このマシンのログアウト
40
42
  ```
41
43
 
42
44
  またはターミナルから:
@@ -68,6 +70,7 @@ statusline の言語はシステム言語に応じて自動選択されます(
68
70
  |---|---|
69
71
  | `pinglet install` | Claude Code に接続(Codex は `--codex` でオプトイン) |
70
72
  | `pinglet login [--github \| --google]` | GitHub または Google アカウントを連携(投稿に必要)。フラグ省略時はブラウザで選択 |
73
+ | `pinglet logout` | ログアウト(連携設定・キャッシュは維持) |
71
74
  | `pinglet post "メッセージ"` | Ping を送る |
72
75
  | `pinglet ping` | いま表示されるメッセージをプレビュー |
73
76
  | `pinglet doctor` | インストール・接続状態を診断 |
package/README.ko.md CHANGED
@@ -36,7 +36,9 @@ npm install -g pinglet-cli && pinglet install
36
36
  Claude Code 세션 안에서 바로:
37
37
 
38
38
  ```
39
+ > /pinglet-login # 최초 1회 — 브라우저에서 GitHub 또는 Google 로그인
39
40
  > /pinglet 오늘도 빌드가 초록불이길
41
+ > /pinglet-logout # 이 기기의 로그인 해제
40
42
  ```
41
43
 
42
44
  또는 터미널에서:
@@ -68,6 +70,7 @@ statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어
68
70
  |---|---|
69
71
  | `pinglet install` | Claude Code 연결 (Codex는 `--codex`로 opt-in) |
70
72
  | `pinglet login [--github \| --google]` | GitHub 또는 Google 계정 연결 (메시지 작성에 필요). 플래그가 없으면 브라우저에서 선택 |
73
+ | `pinglet logout` | 로그인 해제 (설치·캐시는 유지) |
71
74
  | `pinglet post "메시지"` | Ping 보내기 |
72
75
  | `pinglet ping` | 지금 표시될 메시지 미리보기 |
73
76
  | `pinglet doctor` | 설치·연결 상태 진단 |
package/README.md CHANGED
@@ -36,7 +36,9 @@ That's it. Just run `claude` as usual.
36
36
  Right from a Claude Code session:
37
37
 
38
38
  ```
39
+ > /pinglet-login # once — GitHub or Google login in the browser
39
40
  > /pinglet may your build stay green today
41
+ > /pinglet-logout # sign out on this machine
40
42
  ```
41
43
 
42
44
  Or from the terminal:
@@ -68,6 +70,7 @@ left untouched). To opt out, add `"autoUpdate": false` to `~/.pinglet/config.jso
68
70
  |---|---|
69
71
  | `pinglet install` | Connect to Claude Code (opt in to Codex with `--codex`) |
70
72
  | `pinglet login [--github \| --google]` | Link your GitHub or Google account (required for posting). Without a flag you pick in the browser |
73
+ | `pinglet logout` | Sign out on this machine (keeps the integration and cache) |
71
74
  | `pinglet post "message"` | Send a Ping |
72
75
  | `pinglet ping` | Preview the message that would be shown now |
73
76
  | `pinglet doctor` | Diagnose installation and connection status |
@@ -49,8 +49,10 @@ const render_1 = require("../render");
49
49
  const CLAUDE_DIR = path.join(os.homedir(), ".claude");
50
50
  const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
51
51
  const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
52
- const COMMAND_PATH = path.join(COMMANDS_DIR, "pinglet.md");
53
52
  const COMMAND_MARKER = "generated by pinglet";
53
+ /** 슬래시 명령이 호출하는 CLI 서브커맨드 — 권한 규칙과 command 파일 모두 이 목록 기준. */
54
+ const SLASH_SUBCOMMANDS = ["post", "login", "logout"];
55
+ const COMMAND_FILES = ["pinglet.md", "pinglet-login.md", "pinglet-logout.md"];
54
56
  /**
55
57
  * Claude Adapter.
56
58
  * Claude Code의 statusLine hook에 `pinglet statusline`을 연결한다.
@@ -83,53 +85,85 @@ function isPingletHookEntry(entry) {
83
85
  return (entry.hooks?.some((h) => typeof h.command === "string" && h.command.includes("hook-prompt")) ?? false);
84
86
  }
85
87
  /**
86
- * `/pinglet` 인라인 bash를 허용하는 settings.json 권한 규칙.
88
+ * `/pinglet`, `/pinglet-login`, `/pinglet-logout` 인라인 bash를 허용하는 settings.json 권한 규칙.
87
89
  * auto 권한 모드에서는 allow 규칙에 없는 Bash 명령을 LLM 분류기가 심사하는데,
88
90
  * 메시지 내용("~해라" 같은 명령문)에 따라 차단될 수 있어 규칙으로 명시한다.
89
91
  */
90
- function postPermissionRule() {
91
- return `Bash(node "${(0, config_1.cliPath)()}" post:*)`;
92
+ function slashPermissionRules() {
93
+ return SLASH_SUBCOMMANDS.map((sub) => `Bash(node "${(0, config_1.cliPath)()}" ${sub}:*)`);
92
94
  }
93
- /** Pinglet이 등록한 post 권한 규칙인지 식별 (경로가 바뀐 옛 규칙 포함). */
95
+ /** Pinglet이 등록한 권한 규칙인지 식별 (경로가 바뀐 옛 규칙 포함). */
94
96
  function isPingletPermissionRule(rule) {
95
- return (rule === postPermissionRule() ||
96
- (rule.startsWith('Bash(node "') &&
97
- rule.includes("pinglet") &&
98
- rule.endsWith('" post:*)')));
97
+ return (rule.startsWith('Bash(node "') &&
98
+ rule.includes("pinglet") &&
99
+ SLASH_SUBCOMMANDS.some((sub) => rule.endsWith(`" ${sub}:*)`)));
99
100
  }
100
101
  /**
101
- * `/pinglet` slash command 파일을 설치한다.
102
- * 등록 자체는 command 안의 인라인 bash(`!` 실행)가 수행하고,
103
- * 모델은 그 결과 한 줄만 전달하므로 토큰 사용이 매우 적다.
102
+ * `/pinglet`, `/pinglet-login`, `/pinglet-logout` slash command 파일을 설치한다.
103
+ * 실제 동작은 command 안의 인라인 bash(`!` 실행)가 수행하고,
104
+ * 모델은 그 결과만 전달하므로 토큰 사용이 매우 적다.
104
105
  * $ARGUMENTS는 따옴표로 감싸 메시지 전체가 하나의 인자로 전달되게 한다
105
106
  * (메시지 속 단어가 셸/CLI 플래그로 해석되는 것을 방지).
106
107
  */
107
108
  function installSlashCommand() {
108
109
  fs.mkdirSync(COMMANDS_DIR, { recursive: true });
109
- fs.writeFileSync(COMMAND_PATH, `---
110
+ const cli = (0, config_1.cliPath)();
111
+ const files = {
112
+ "pinglet.md": `---
110
113
  description: 메시지를 Pinglet에 등록해 다른 개발자들의 터미널에 표시
111
114
  allowed-tools: Bash(node:*)
112
115
  ---
113
116
  <!-- ${COMMAND_MARKER} -->
114
117
 
115
- 등록 결과: !\`node "${(0, config_1.cliPath)()}" post --quiet "$ARGUMENTS"\`
118
+ 등록 결과: !\`node "${cli}" post --quiet "$ARGUMENTS"\`
116
119
 
117
120
  위 등록 결과 줄을 아무것도 덧붙이거나 바꾸지 말고 그대로 한 줄만 출력하세요.
118
121
  다른 설명, 안내, 작업을 일절 하지 마세요.
119
- `);
122
+ `,
123
+ "pinglet-login.md": `---
124
+ description: Pinglet 계정 연결 (브라우저에서 GitHub 또는 Google 로그인, --github/--google로 지정 가능)
125
+ allowed-tools: Bash(node:*)
126
+ ---
127
+ <!-- ${COMMAND_MARKER} -->
128
+
129
+ 로그인 결과:
130
+ !\`node "${cli}" login --quiet $ARGUMENTS\`
131
+
132
+ 위 로그인 결과를 아무것도 덧붙이거나 바꾸지 말고 그대로 출력하세요.
133
+ 다른 설명, 안내, 작업을 일절 하지 마세요.
134
+ `,
135
+ "pinglet-logout.md": `---
136
+ description: Pinglet 로그인 해제 (이 기기의 로그인 토큰만 제거)
137
+ allowed-tools: Bash(node:*)
138
+ ---
139
+ <!-- ${COMMAND_MARKER} -->
140
+
141
+ 로그아웃 결과: !\`node "${cli}" logout\`
142
+
143
+ 위 로그아웃 결과 줄을 아무것도 덧붙이거나 바꾸지 말고 그대로 한 줄만 출력하세요.
144
+ 다른 설명, 안내, 작업을 일절 하지 마세요.
145
+ `,
146
+ };
147
+ for (const [name, body] of Object.entries(files)) {
148
+ fs.writeFileSync(path.join(COMMANDS_DIR, name), body);
149
+ }
120
150
  }
121
151
  /** Pinglet이 생성한 command 파일만 삭제한다 (마커로 확인). */
122
152
  function uninstallSlashCommand() {
123
- try {
124
- if (!fs.readFileSync(COMMAND_PATH, "utf8").includes(COMMAND_MARKER)) {
125
- return false;
153
+ let removed = false;
154
+ for (const name of COMMAND_FILES) {
155
+ const file = path.join(COMMANDS_DIR, name);
156
+ try {
157
+ if (!fs.readFileSync(file, "utf8").includes(COMMAND_MARKER))
158
+ continue;
159
+ fs.unlinkSync(file);
160
+ removed = true;
161
+ }
162
+ catch {
163
+ // 없거나 읽을 수 없는 파일은 건너뛴다.
126
164
  }
127
- fs.unlinkSync(COMMAND_PATH);
128
- return true;
129
- }
130
- catch {
131
- return false;
132
165
  }
166
+ return removed;
133
167
  }
134
168
  /** 예전 버전이 등록한 UserPromptSubmit hook을 정리한다 (hook 방식은 폐기됨). */
135
169
  function uninstallPromptHook(settings) {
@@ -352,7 +386,7 @@ function installClaudeIntegration(config, options = {}) {
352
386
  const permissions = (settings.permissions ??= {});
353
387
  const allow = (permissions.allow ??= []);
354
388
  const kept = allow.filter((rule) => !isPingletPermissionRule(rule));
355
- permissions.allow = [...kept, postPermissionRule()];
389
+ permissions.allow = [...kept, ...slashPermissionRules()];
356
390
  if (!writeSettings(settings, mtimeMs))
357
391
  continue; // 다른 프로세스와 충돌 → 재읽기
358
392
  installSlashCommand();
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ const install_1 = require("./commands/install");
6
6
  const uninstall_1 = require("./commands/uninstall");
7
7
  const doctor_1 = require("./commands/doctor");
8
8
  const login_1 = require("./commands/login");
9
+ const logout_1 = require("./commands/logout");
9
10
  const statusline_1 = require("./commands/statusline");
10
11
  const notify_1 = require("./commands/notify");
11
12
  const flush_1 = require("./commands/flush");
@@ -21,6 +22,8 @@ const HELP = `pinglet v${config_1.VERSION} — AI가 생각하는 동안 만나
21
22
  uninstall [--purge] integration 제거 (--purge: ~/.pinglet까지 삭제)
22
23
  login [--github | --google] 계정 연결 (GitHub 또는 Google, 생략 시 브라우저에서 선택)
23
24
  [--token <jwt>]
25
+ logout 로그인 해제 (설치·캐시는 유지)
26
+ (Claude Code 안에서는 /pinglet-login, /pinglet-logout)
24
27
  post "메시지" [--category <c>] 메시지 작성 (로그인 필요, 읽기는 익명 가능)
25
28
  doctor 설치/캐시/서버 상태 진단
26
29
  ping 메시지 미리보기
@@ -43,6 +46,9 @@ async function main() {
43
46
  case "login":
44
47
  await (0, login_1.runLogin)(rest);
45
48
  break;
49
+ case "logout":
50
+ (0, logout_1.runLogout)();
51
+ break;
46
52
  case "doctor":
47
53
  await (0, doctor_1.runDoctor)();
48
54
  break;
@@ -119,9 +119,12 @@ function loginViaLoopback(apiBaseUrl, provider) {
119
119
  /**
120
120
  * 소셜 로그인(GitHub/Google) — 루프백 자동 수신, 실패 시 수동 붙여넣기 폴백.
121
121
  * `--github` / `--google`로 방식을 고정하고, 없으면 브라우저에서 고른다.
122
+ * `--quiet`: /pinglet-login slash command용 — 터미널 입력을 받을 수 없으므로
123
+ * 붙여넣기 폴백 대신 안내만 출력하고, 실패해도 exit 0 (에러 덤프 방지).
122
124
  */
123
125
  async function runLogin(args) {
124
126
  const config = (0, config_1.loadConfig)();
127
+ const quiet = args.includes("--quiet");
125
128
  let token;
126
129
  const tokenIndex = args.indexOf("--token");
127
130
  if (tokenIndex !== -1 && args[tokenIndex + 1]) {
@@ -135,6 +138,10 @@ async function runLogin(args) {
135
138
  if (!token) {
136
139
  console.log(`\n로그인하면 이용약관(${config.apiBaseUrl}/terms)과 개인정보처리방침(${config.apiBaseUrl}/privacy)에 동의하는 것으로 간주됩니다.`);
137
140
  token = (await loginViaLoopback(config.apiBaseUrl, provider)) ?? undefined;
141
+ if (!token && quiet) {
142
+ console.log("✗ 로그인이 완료되지 않았습니다 (제한 시간 3분). 터미널에서 `pinglet login`으로 다시 시도하세요.");
143
+ return;
144
+ }
138
145
  if (!token) {
139
146
  // SSH 등 브라우저와 터미널이 다른 기계인 경우를 위한 폴백.
140
147
  console.log("자동 로그인에 실패했습니다. 아래 URL에서 로그인 후 응답 JSON의 token 값을 붙여넣어 주세요.");
@@ -144,7 +151,8 @@ async function runLogin(args) {
144
151
  }
145
152
  if (!token) {
146
153
  console.log("✗ token이 비어 있습니다.");
147
- process.exitCode = 1;
154
+ if (!quiet)
155
+ process.exitCode = 1;
148
156
  return;
149
157
  }
150
158
  config.userToken = token;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runLogout = runLogout;
4
+ const config_1 = require("../config");
5
+ /**
6
+ * `pinglet logout` — 로컬에 저장된 로그인 토큰만 지운다.
7
+ * 통합 설정·installation·캐시는 그대로라 읽기는 계속 되고, 다시 `pinglet login`으로
8
+ * 다른 계정(GitHub/Google)을 연결할 수 있다. 서버 측 토큰 폐기는 하지 않는다 (JWT 만료까지 유효).
9
+ */
10
+ function runLogout() {
11
+ const config = (0, config_1.loadConfig)();
12
+ if (!config.userToken) {
13
+ console.log("○ 로그인 상태가 아닙니다.");
14
+ return;
15
+ }
16
+ delete config.userToken;
17
+ (0, config_1.saveConfig)(config);
18
+ console.log("✓ 로그아웃했습니다. 읽기는 계속 되고, 메시지 작성은 `pinglet login` 후 가능합니다.");
19
+ }
@@ -45,10 +45,14 @@ async function runPost(args) {
45
45
  if (!result.ok) {
46
46
  switch (result.error) {
47
47
  case "LOGIN_REQUIRED":
48
- console.log("✗ 메시지 작성에는 로그인(GitHub 또는 Google)이 필요합니다. `pinglet login`을 실행하세요 (읽기는 로그인 없이 가능).");
48
+ console.log(quiet
49
+ ? "✗ 메시지 작성에는 로그인(GitHub 또는 Google)이 필요합니다. `/pinglet-login`을 실행하세요 (읽기는 로그인 없이 가능)."
50
+ : "✗ 메시지 작성에는 로그인(GitHub 또는 Google)이 필요합니다. `pinglet login`을 실행하세요 (읽기는 로그인 없이 가능).");
49
51
  break;
50
52
  case "UNAUTHORIZED":
51
- console.log("✗ 로그인이 만료됐습니다. `pinglet login`으로 다시 로그인하세요.");
53
+ console.log(quiet
54
+ ? "✗ 로그인이 만료됐습니다. `/pinglet-login`으로 다시 로그인하세요."
55
+ : "✗ 로그인이 만료됐습니다. `pinglet login`으로 다시 로그인하세요.");
52
56
  break;
53
57
  case "RATE_LIMITED":
54
58
  console.log("✗ 작성 제한(분당 5개)에 걸렸습니다. 잠시 후 다시 시도하세요.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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": {