agent-messenger 2.5.0 → 2.6.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +35 -0
- package/bun.lock +10 -2
- package/dist/package.json +3 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +4 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +21 -7
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.js +24 -55
- package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/kakaotalk/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/credential-manager.js +1 -0
- package/dist/src/platforms/kakaotalk/credential-manager.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.js +2 -2
- package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/config.d.ts +8 -2
- package/dist/src/platforms/kakaotalk/protocol/config.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/config.js +15 -2
- package/dist/src/platforms/kakaotalk/protocol/config.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +3 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +13 -10
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +10 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +1 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/tui/adapters/kakaotalk-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/kakaotalk-adapter.js +5 -2
- package/dist/src/tui/adapters/kakaotalk-adapter.js.map +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +3 -39
- package/e2e/config.ts +1 -1
- package/package.json +3 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +11 -63
- package/skills/agent-kakaotalk/references/authentication.md +4 -69
- package/skills/agent-kakaotalk/references/common-patterns.md +13 -0
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/kakaotalk/client.ts +25 -10
- package/src/platforms/kakaotalk/commands/auth.ts +22 -73
- package/src/platforms/kakaotalk/credential-manager.ts +1 -0
- package/src/platforms/kakaotalk/index.ts +1 -0
- package/src/platforms/kakaotalk/listener.test.ts +2 -2
- package/src/platforms/kakaotalk/listener.ts +2 -2
- package/src/platforms/kakaotalk/protocol/config.ts +26 -2
- package/src/platforms/kakaotalk/protocol/session.ts +16 -10
- package/src/platforms/kakaotalk/types.ts +4 -0
- package/src/tui/adapters/kakaotalk-adapter.ts +5 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-kakaotalk
|
|
3
3
|
description: Interact with KakaoTalk - send messages, read chats, manage conversations
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.6.0
|
|
5
5
|
allowed-tools: Bash(agent-kakaotalk:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -16,7 +16,7 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
# Agent KakaoTalk
|
|
18
18
|
|
|
19
|
-
A TypeScript CLI tool that enables AI agents and humans to interact with KakaoTalk through a simple command interface. Features
|
|
19
|
+
A TypeScript CLI tool that enables AI agents and humans to interact with KakaoTalk through a simple command interface. Features sub-device login that keeps your desktop app running.
|
|
20
20
|
|
|
21
21
|
## Key Concepts
|
|
22
22
|
|
|
@@ -32,12 +32,9 @@ Before diving in, a few things about KakaoTalk's architecture:
|
|
|
32
32
|
## Quick Start
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
# Login (
|
|
35
|
+
# Login (registers as sub-device, desktop app stays running)
|
|
36
36
|
agent-kakaotalk auth login
|
|
37
37
|
|
|
38
|
-
# Or extract credentials from desktop app (kicks desktop session)
|
|
39
|
-
agent-kakaotalk auth extract
|
|
40
|
-
|
|
41
38
|
# List chat rooms
|
|
42
39
|
agent-kakaotalk chat list
|
|
43
40
|
|
|
@@ -53,10 +50,6 @@ agent-kakaotalk whoami
|
|
|
53
50
|
|
|
54
51
|
## Authentication
|
|
55
52
|
|
|
56
|
-
KakaoTalk offers two authentication methods:
|
|
57
|
-
|
|
58
|
-
### Method 1: Login (Recommended)
|
|
59
|
-
|
|
60
53
|
Registers the CLI as a sub-device (tablet slot by default). Your desktop app keeps running.
|
|
61
54
|
|
|
62
55
|
```bash
|
|
@@ -77,18 +70,7 @@ agent-kakaotalk auth login --email user@example.com --password mypass
|
|
|
77
70
|
3. The CLI polls until you confirm on your phone
|
|
78
71
|
4. Login completes automatically after confirmation
|
|
79
72
|
|
|
80
|
-
**IMPORTANT**: NEVER guide the user to open a web browser, use DevTools, or manually copy tokens. Always use `agent-kakaotalk auth login
|
|
81
|
-
|
|
82
|
-
### Method 2: Extract from Desktop App
|
|
83
|
-
|
|
84
|
-
Extracts OAuth tokens directly from the KakaoTalk desktop app's cache. This **kicks the desktop session** because it reuses the desktop's credentials.
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
agent-kakaotalk auth extract
|
|
88
|
-
agent-kakaotalk auth extract --debug
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
On macOS, reads from `~/Library/Containers/com.kakao.KakaoTalkMac/Data/Library/Caches/Cache.db`. On Windows, reads from the registry and `%LocalAppData%\Kakao\login_list.dat`.
|
|
73
|
+
**IMPORTANT**: NEVER guide the user to open a web browser, use DevTools, or manually copy tokens. Always use `agent-kakaotalk auth login`.
|
|
92
74
|
|
|
93
75
|
### Agent Behavior (MANDATORY)
|
|
94
76
|
|
|
@@ -102,21 +84,13 @@ agent-kakaotalk auth status
|
|
|
102
84
|
|
|
103
85
|
If authenticated → retry the original command.
|
|
104
86
|
|
|
105
|
-
**Step 2:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
agent-kakaotalk auth extract
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
If extraction succeeds → retry the original command. Extraction is silent and requires no user input.
|
|
112
|
-
|
|
113
|
-
**Step 3: If extraction fails, use login flow**
|
|
87
|
+
**Step 2: Login (registers as sub-device — desktop app stays running)**
|
|
114
88
|
|
|
115
89
|
```bash
|
|
116
90
|
agent-kakaotalk auth login
|
|
117
91
|
```
|
|
118
92
|
|
|
119
|
-
The CLI
|
|
93
|
+
The CLI auto-extracts the email (and password if available) from the desktop app. On fresh installs, the CLI may prompt for the KakaoTalk password once (one-time device registration). After registration, the password is never needed again.
|
|
120
94
|
|
|
121
95
|
Possible responses:
|
|
122
96
|
|
|
@@ -134,7 +108,7 @@ agent-kakaotalk auth login --password-file /tmp/.kakao-pw
|
|
|
134
108
|
|
|
135
109
|
The `--password-file` flag reads the password from the file and **deletes the file immediately after reading**. The password never appears in chat, shell history, or process list.
|
|
136
110
|
|
|
137
|
-
**Step
|
|
111
|
+
**Step 3: Retry the original command**
|
|
138
112
|
After successful auth, immediately execute whatever the user originally asked for.
|
|
139
113
|
|
|
140
114
|
### Device Slots
|
|
@@ -157,7 +131,7 @@ agent-kakaotalk auth login --device-type tablet --force
|
|
|
157
131
|
|
|
158
132
|
## Multi-Account
|
|
159
133
|
|
|
160
|
-
KakaoTalk supports multiple accounts. Each login
|
|
134
|
+
KakaoTalk supports multiple accounts. Each login stores credentials separately, keyed by user ID.
|
|
161
135
|
|
|
162
136
|
### Listing Accounts
|
|
163
137
|
|
|
@@ -262,11 +236,6 @@ agent-kakaotalk auth login --email <email> --password <password>
|
|
|
262
236
|
agent-kakaotalk auth login --device-type pc --force
|
|
263
237
|
agent-kakaotalk auth login --debug
|
|
264
238
|
|
|
265
|
-
# Extract credentials from KakaoTalk desktop app
|
|
266
|
-
agent-kakaotalk auth extract
|
|
267
|
-
agent-kakaotalk auth extract --debug
|
|
268
|
-
agent-kakaotalk auth extract --unsafely-show-secrets
|
|
269
|
-
|
|
270
239
|
# Check auth status
|
|
271
240
|
agent-kakaotalk auth status
|
|
272
241
|
|
|
@@ -434,7 +403,7 @@ All commands return consistent error format:
|
|
|
434
403
|
|
|
435
404
|
Common errors:
|
|
436
405
|
|
|
437
|
-
- `No KakaoTalk credentials found` — not authenticated. Run `auth login
|
|
406
|
+
- `No KakaoTalk credentials found` — not authenticated. Run `auth login`.
|
|
438
407
|
- `bad_credentials` — wrong email or password. Cached credentials from the desktop app may be stale. Ask the user to provide credentials manually with `--email` and `--password`.
|
|
439
408
|
- `login_failed` — device slot conflict or unknown login error. Run with `--debug` for the full server response.
|
|
440
409
|
- `passcode_request_failed` — failed to request device verification code.
|
|
@@ -519,7 +488,7 @@ See the [KakaoTalk SDK documentation](https://agent-messenger.dev/docs/sdk/kakao
|
|
|
519
488
|
|
|
520
489
|
## Limitations
|
|
521
490
|
|
|
522
|
-
- macOS and Windows only (desktop app
|
|
491
|
+
- macOS and Windows only (desktop app needed for auto-extracting email/password during login)
|
|
523
492
|
- No Linux support (KakaoTalk desktop not available on Linux)
|
|
524
493
|
- No file upload or download
|
|
525
494
|
- No channel/chat room creation or management
|
|
@@ -555,17 +524,9 @@ pnpm dlx --package agent-messenger agent-kakaotalk chat list --pretty
|
|
|
555
524
|
|
|
556
525
|
**NEVER run `npx agent-kakaotalk`, `bunx agent-kakaotalk`, or `pnpm dlx agent-kakaotalk`** without `--package agent-messenger`. It will fail or install a wrong package since `agent-kakaotalk` is not the npm package name.
|
|
557
526
|
|
|
558
|
-
### No credentials found
|
|
559
|
-
|
|
560
|
-
If `auth extract` fails:
|
|
561
|
-
|
|
562
|
-
1. Make sure the KakaoTalk desktop app is installed and you're logged in
|
|
563
|
-
2. Run `agent-kakaotalk auth extract --debug` for detailed diagnostics
|
|
564
|
-
3. If extraction still fails, use `agent-kakaotalk auth login` instead (recommended)
|
|
565
|
-
|
|
566
527
|
### Password prompt on fresh install
|
|
567
528
|
|
|
568
|
-
On fresh installs, the
|
|
529
|
+
On fresh installs, the desktop app (macOS or Windows) may hash or omit the password from its cache, so the CLI cannot extract it automatically. The CLI will prompt for the password once to register the device — via a native dialog on macOS (AppKit) and Windows (PowerShell WinForms), or via a TTY prompt if a terminal is available. After registration, the password is never needed again.
|
|
569
530
|
|
|
570
531
|
When the CLI returns `{"next_action": "run_interactive", ...}`, use a tmux session to let the user type their password securely. See "Handling `run_interactive`" above for the exact steps.
|
|
571
532
|
|
|
@@ -597,19 +558,6 @@ If the passcode expires before you confirm on your phone:
|
|
|
597
558
|
2. Confirm the code on your phone within the time limit
|
|
598
559
|
3. The CLI automatically completes login after confirmation
|
|
599
560
|
|
|
600
|
-
### Cache.db not found (macOS)
|
|
601
|
-
|
|
602
|
-
The CLI looks for KakaoTalk's cache at:
|
|
603
|
-
|
|
604
|
-
```
|
|
605
|
-
~/Library/Containers/com.kakao.KakaoTalkMac/Data/Library/Caches/Cache.db
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
If this file doesn't exist:
|
|
609
|
-
1. Install KakaoTalk from the Mac App Store
|
|
610
|
-
2. Log in and send at least one message (to populate the cache)
|
|
611
|
-
3. Run `agent-kakaotalk auth extract` again
|
|
612
|
-
|
|
613
561
|
## References
|
|
614
562
|
|
|
615
563
|
- [Authentication Guide](references/authentication.md)
|
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
agent-kakaotalk
|
|
5
|
+
agent-kakaotalk authenticates by registering the CLI as a sub-device (tablet or PC slot) using KakaoTalk's Android sub-device API. Your desktop app and phone keep running.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
2. **Extract** — reads OAuth tokens from the KakaoTalk desktop app's local cache. This reuses the desktop's credentials, which kicks the desktop session.
|
|
9
|
-
|
|
10
|
-
## Method 1: Login Flow
|
|
7
|
+
## Login Flow
|
|
11
8
|
|
|
12
9
|
### How It Works
|
|
13
10
|
|
|
@@ -107,65 +104,6 @@ Each device is identified by a UUID. The CLI generates one on first login and re
|
|
|
107
104
|
|
|
108
105
|
The UUID is stored in the credentials file alongside the OAuth token.
|
|
109
106
|
|
|
110
|
-
## Method 2: Extract from Desktop App
|
|
111
|
-
|
|
112
|
-
### How It Works
|
|
113
|
-
|
|
114
|
-
The KakaoTalk desktop app (macOS) caches HTTP requests in a SQLite database (`Cache.db`). These cached requests contain:
|
|
115
|
-
|
|
116
|
-
- `Authorization` header with the OAuth token
|
|
117
|
-
- Login form body with email, password, and device UUID
|
|
118
|
-
- Refresh token from token renewal requests
|
|
119
|
-
|
|
120
|
-
The CLI reads this cache and stores the extracted credentials.
|
|
121
|
-
|
|
122
|
-
### macOS
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
agent-kakaotalk auth extract
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Cache location:
|
|
129
|
-
```
|
|
130
|
-
~/Library/Containers/com.kakao.KakaoTalkMac/Data/Library/Caches/Cache.db
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
The CLI:
|
|
134
|
-
1. Copies `Cache.db` (and WAL/SHM journals) to a temp directory
|
|
135
|
-
2. Queries for requests to `talk-pilsner.kakao.com` (messaging API)
|
|
136
|
-
3. Parses binary plist blobs for the OAuth token
|
|
137
|
-
4. Extracts device UUID and refresh token from `login.json` / `renew_token.json` cached requests
|
|
138
|
-
5. Stores extracted credentials
|
|
139
|
-
|
|
140
|
-
### Windows
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
agent-kakaotalk auth extract
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
The CLI reads from:
|
|
147
|
-
- Registry: `HKCU\Software\Kakao\KakaoTalk\DeviceInfo` (device UUID)
|
|
148
|
-
- File: `%LocalAppData%\Kakao\login_list.dat` (login credentials)
|
|
149
|
-
|
|
150
|
-
### Troubleshooting Extraction
|
|
151
|
-
|
|
152
|
-
Use `--debug` for detailed logs:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
agent-kakaotalk auth extract --debug
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
This shows:
|
|
159
|
-
- Which cache path was found
|
|
160
|
-
- How many cached requests were discovered
|
|
161
|
-
- Which tokens were successfully extracted
|
|
162
|
-
|
|
163
|
-
Use `--unsafely-show-secrets` to see full token values (debug mode only):
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
agent-kakaotalk auth extract --unsafely-show-secrets
|
|
167
|
-
```
|
|
168
|
-
|
|
169
107
|
## Credential Storage
|
|
170
108
|
|
|
171
109
|
### Location
|
|
@@ -237,13 +175,13 @@ Output when not authenticated:
|
|
|
237
175
|
|
|
238
176
|
```json
|
|
239
177
|
{
|
|
240
|
-
"error": "No account configured. Run \"auth login\"
|
|
178
|
+
"error": "No account configured. Run \"auth login\" first."
|
|
241
179
|
}
|
|
242
180
|
```
|
|
243
181
|
|
|
244
182
|
## Multi-Account
|
|
245
183
|
|
|
246
|
-
KakaoTalk supports multiple accounts. Each login
|
|
184
|
+
KakaoTalk supports multiple accounts. Each login stores credentials separately, keyed by user ID.
|
|
247
185
|
|
|
248
186
|
### Listing Accounts
|
|
249
187
|
|
|
@@ -316,9 +254,6 @@ If commands start failing with auth errors:
|
|
|
316
254
|
# Try login again (reuses saved device UUID to skip passcode)
|
|
317
255
|
agent-kakaotalk auth login
|
|
318
256
|
|
|
319
|
-
# Or re-extract from desktop app
|
|
320
|
-
agent-kakaotalk auth extract
|
|
321
|
-
|
|
322
257
|
# Verify it worked
|
|
323
258
|
agent-kakaotalk auth status
|
|
324
259
|
```
|
|
@@ -391,6 +391,19 @@ try {
|
|
|
391
391
|
}
|
|
392
392
|
```
|
|
393
393
|
|
|
394
|
+
When passing credentials manually, include `deviceType` to control which session slot the LOCO connection uses:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
const client = await new KakaoTalkClient().login({
|
|
398
|
+
oauthToken: 'token',
|
|
399
|
+
userId: '1234567890',
|
|
400
|
+
deviceUuid: 'uuid',
|
|
401
|
+
deviceType: 'tablet', // 'tablet' (default, safe) or 'pc' (kicks desktop app)
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The `deviceType` determines the LOCO protocol identity: `'tablet'` sends `os: 'android'` (tablet sub-device slot), while `'pc'` sends `os: 'mac'` on macOS or `os: 'win'` on Windows (PC slot, conflicts with the desktop app). When using auto-login (`.login()` with no arguments), `deviceType` is read from stored credentials automatically.
|
|
406
|
+
|
|
394
407
|
### Auto-Reconnect
|
|
395
408
|
|
|
396
409
|
`getChats`, `getMessages`, and `sendMessage` automatically reconnect once when the LOCO session dies (e.g. the KakaoTalk desktop app reclaims the session or the network drops). The reconnect is transparent — callers don't need to handle session-drop errors.
|
|
@@ -6,10 +6,10 @@ import { join } from 'node:path'
|
|
|
6
6
|
|
|
7
7
|
import { warn } from '@/shared/utils/stderr'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { LANG, PC_OS_NAME, getLocoDeviceConfig } from './protocol/config'
|
|
10
10
|
import { LocoSession } from './protocol/session'
|
|
11
11
|
import type { ChatListResponse, LoginListResponse, SyncState } from './protocol/types'
|
|
12
|
-
import type { KakaoChat, KakaoMessage, KakaoProfile, KakaoSendResult } from './types'
|
|
12
|
+
import type { KakaoChat, KakaoDeviceType, KakaoMessage, KakaoProfile, KakaoSendResult } from './types'
|
|
13
13
|
|
|
14
14
|
export class KakaoTalkError extends Error {
|
|
15
15
|
code: string
|
|
@@ -231,12 +231,13 @@ export class KakaoTalkClient {
|
|
|
231
231
|
private oauthToken: string | null = null
|
|
232
232
|
private userId: string | null = null
|
|
233
233
|
private deviceUuid: string | null = null
|
|
234
|
+
private deviceType: KakaoDeviceType = 'tablet'
|
|
234
235
|
private state: SessionState | null = null
|
|
235
236
|
private initPromise: Promise<SessionState> | null = null
|
|
236
237
|
private closed = false
|
|
237
238
|
|
|
238
239
|
async login(
|
|
239
|
-
credentials?: { oauthToken: string; userId: string; deviceUuid?: string },
|
|
240
|
+
credentials?: { oauthToken: string; userId: string; deviceUuid?: string; deviceType?: KakaoDeviceType },
|
|
240
241
|
accountId?: string,
|
|
241
242
|
): Promise<this> {
|
|
242
243
|
if (credentials) {
|
|
@@ -245,19 +246,26 @@ export class KakaoTalkClient {
|
|
|
245
246
|
this.oauthToken = credentials.oauthToken
|
|
246
247
|
this.userId = credentials.userId
|
|
247
248
|
this.deviceUuid = credentials.deviceUuid ?? `agent-messenger-${credentials.userId}`
|
|
249
|
+
this.deviceType = credentials.deviceType ?? 'tablet'
|
|
248
250
|
return this
|
|
249
251
|
}
|
|
250
252
|
const { ensureKakaoAuth } = await import('./ensure-auth')
|
|
251
253
|
const account = await ensureKakaoAuth(accountId)
|
|
252
|
-
return this.login({
|
|
254
|
+
return this.login({
|
|
255
|
+
oauthToken: account.oauth_token,
|
|
256
|
+
userId: account.user_id,
|
|
257
|
+
deviceUuid: account.device_uuid,
|
|
258
|
+
deviceType: account.device_type,
|
|
259
|
+
})
|
|
253
260
|
}
|
|
254
261
|
|
|
255
|
-
getCredentials(): { oauthToken: string; userId: string; deviceUuid: string } {
|
|
262
|
+
getCredentials(): { oauthToken: string; userId: string; deviceUuid: string; deviceType: KakaoDeviceType } {
|
|
256
263
|
this.ensureAuth()
|
|
257
264
|
return {
|
|
258
265
|
oauthToken: this.oauthToken!,
|
|
259
266
|
userId: this.userId!,
|
|
260
267
|
deviceUuid: this.deviceUuid!,
|
|
268
|
+
deviceType: this.deviceType,
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|
|
@@ -315,7 +323,7 @@ export class KakaoTalkClient {
|
|
|
315
323
|
const session = new LocoSession()
|
|
316
324
|
try {
|
|
317
325
|
const syncState = await loadSyncState(this.deviceUuid!)
|
|
318
|
-
const loginResult = await session.login(this.oauthToken!, this.userId!, this.deviceUuid!, syncState)
|
|
326
|
+
const loginResult = await session.login(this.oauthToken!, this.userId!, this.deviceUuid!, syncState, this.deviceType)
|
|
319
327
|
|
|
320
328
|
const newSyncState = mergeSyncState(syncState, loginResult)
|
|
321
329
|
await saveSyncState(this.deviceUuid!, newSyncState)
|
|
@@ -500,17 +508,24 @@ export class KakaoTalkClient {
|
|
|
500
508
|
async getProfile(): Promise<KakaoProfile> {
|
|
501
509
|
this.ensureAuth()
|
|
502
510
|
try {
|
|
511
|
+
const deviceConfig = getLocoDeviceConfig(this.deviceType)
|
|
512
|
+
const isPC = deviceConfig.os !== 'android'
|
|
513
|
+
const apiPrefix = isPC ? 'mac' : 'android'
|
|
514
|
+
const userAgent = isPC
|
|
515
|
+
? `KT/${deviceConfig.appVersion} Md/${PC_OS_NAME} ${LANG}`
|
|
516
|
+
: `KT/${deviceConfig.appVersion} An/13 ${LANG}`
|
|
517
|
+
|
|
503
518
|
const headers = {
|
|
504
519
|
Authorization: `${this.oauthToken}-${this.deviceUuid}`,
|
|
505
|
-
A: `${
|
|
506
|
-
'User-Agent':
|
|
520
|
+
A: `${deviceConfig.os}/${deviceConfig.appVersion}/${LANG}`,
|
|
521
|
+
'User-Agent': userAgent,
|
|
507
522
|
Accept: '*/*',
|
|
508
523
|
'Accept-Language': LANG,
|
|
509
524
|
}
|
|
510
525
|
|
|
511
526
|
const [profileRes, settingsRes] = await Promise.all([
|
|
512
|
-
fetch(
|
|
513
|
-
fetch(
|
|
527
|
+
fetch(`https://katalk.kakao.com/${apiPrefix}/profile3/me.json`, { headers }),
|
|
528
|
+
fetch(`https://katalk.kakao.com/${apiPrefix}/account/more_settings.json?since=0&lang=ko`, { headers }),
|
|
514
529
|
])
|
|
515
530
|
|
|
516
531
|
if (!profileRes.ok) {
|
|
@@ -6,7 +6,7 @@ import { handleError } from '@/shared/utils/error-handler'
|
|
|
6
6
|
import { formatOutput } from '@/shared/utils/output'
|
|
7
7
|
import { info, error, debug } from '@/shared/utils/stderr'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { loginFlow } from '../auth/kakao-login'
|
|
10
10
|
import { CredentialManager } from '../credential-manager'
|
|
11
11
|
import { KakaoTokenExtractor } from '../token-extractor'
|
|
12
12
|
import {
|
|
@@ -23,7 +23,9 @@ function isInteractiveSession(): boolean {
|
|
|
23
23
|
function hasTTY(): boolean {
|
|
24
24
|
try {
|
|
25
25
|
const { openSync, closeSync } = require('node:fs') as typeof import('node:fs')
|
|
26
|
-
|
|
26
|
+
// CONIN$ is the Windows console input device; /dev/tty is the Unix equivalent
|
|
27
|
+
const ttyDevice = process.platform === 'win32' ? 'CONIN$' : '/dev/tty'
|
|
28
|
+
const fd = openSync(ttyDevice, 'r')
|
|
27
29
|
closeSync(fd)
|
|
28
30
|
return true
|
|
29
31
|
} catch { return false }
|
|
@@ -178,6 +180,20 @@ async function promptHidden(message: string): Promise<string | undefined> {
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
async function promptHiddenTTY(message: string): Promise<string | undefined> {
|
|
183
|
+
if (process.platform === 'win32') {
|
|
184
|
+
const { execSync } = require('node:child_process') as typeof import('node:child_process')
|
|
185
|
+
try {
|
|
186
|
+
const escapedMessage = message.replace(/'/g, "''")
|
|
187
|
+
const ps = `$p = Read-Host '${escapedMessage}' -AsSecureString; [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($p))`
|
|
188
|
+
const result = execSync(`powershell -NoProfile -Command "${ps}"`, {
|
|
189
|
+
encoding: 'utf-8',
|
|
190
|
+
timeout: 120_000,
|
|
191
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
192
|
+
})
|
|
193
|
+
return result.trim() || undefined
|
|
194
|
+
} catch { return undefined }
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
const { createReadStream } = await import('node:fs')
|
|
182
198
|
const { createInterface } = await import('node:readline/promises')
|
|
183
199
|
const ttyIn = createReadStream('/dev/tty')
|
|
@@ -277,7 +293,8 @@ async function loginAction(options: KakaoAuthOptions): Promise<void> {
|
|
|
277
293
|
|
|
278
294
|
const existing = await credManager.getAccount()
|
|
279
295
|
const pendingState = await credManager.loadPendingLogin()
|
|
280
|
-
const
|
|
296
|
+
const existingUuid = existing?.auth_method === 'login' ? existing?.device_uuid : undefined
|
|
297
|
+
const savedDeviceUuid = pendingState?.device_uuid ?? existingUuid
|
|
281
298
|
|
|
282
299
|
const onPasscodeDisplay = (code: string) => {
|
|
283
300
|
if (interactive) {
|
|
@@ -355,6 +372,7 @@ async function handleLoginResult(
|
|
|
355
372
|
refresh_token: result.credentials.refresh_token,
|
|
356
373
|
device_uuid: result.credentials.device_uuid,
|
|
357
374
|
device_type: result.credentials.device_type,
|
|
375
|
+
auth_method: 'login',
|
|
358
376
|
created_at: now,
|
|
359
377
|
updated_at: now,
|
|
360
378
|
})
|
|
@@ -372,67 +390,6 @@ async function handleLoginResult(
|
|
|
372
390
|
}
|
|
373
391
|
}
|
|
374
392
|
|
|
375
|
-
async function extractAction(options: {
|
|
376
|
-
pretty?: boolean
|
|
377
|
-
debug?: boolean
|
|
378
|
-
unsafelyShowSecrets?: boolean
|
|
379
|
-
}): Promise<void> {
|
|
380
|
-
try {
|
|
381
|
-
if (options.unsafelyShowSecrets) {
|
|
382
|
-
options.debug = true
|
|
383
|
-
}
|
|
384
|
-
const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
|
|
385
|
-
const extractor = new KakaoTokenExtractor(undefined, debugLog)
|
|
386
|
-
|
|
387
|
-
const token = await extractor.extract()
|
|
388
|
-
|
|
389
|
-
if (!token) {
|
|
390
|
-
console.log(
|
|
391
|
-
formatOutput(
|
|
392
|
-
{
|
|
393
|
-
error: 'No credentials found. Make sure KakaoTalk desktop app is installed and logged in.',
|
|
394
|
-
hint: options.debug ? undefined : 'Run with --debug for more info.',
|
|
395
|
-
},
|
|
396
|
-
options.pretty,
|
|
397
|
-
),
|
|
398
|
-
)
|
|
399
|
-
process.exit(1)
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (options.debug) {
|
|
403
|
-
const display = options.unsafelyShowSecrets
|
|
404
|
-
? token.oauth_token
|
|
405
|
-
: `${token.oauth_token.substring(0, 12)}...`
|
|
406
|
-
debug(`[debug] oauth_token: ${display}`)
|
|
407
|
-
debug(`[debug] user_id: ${token.user_id}`)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const credManager = new CredentialManager()
|
|
411
|
-
const accountId = token.user_id || 'default'
|
|
412
|
-
const now = new Date().toISOString()
|
|
413
|
-
|
|
414
|
-
await credManager.setAccount({
|
|
415
|
-
account_id: accountId,
|
|
416
|
-
oauth_token: token.oauth_token,
|
|
417
|
-
user_id: token.user_id,
|
|
418
|
-
refresh_token: token.refresh_token,
|
|
419
|
-
device_uuid: token.device_uuid ?? generateDeviceUuid(),
|
|
420
|
-
device_type: 'tablet',
|
|
421
|
-
created_at: now,
|
|
422
|
-
updated_at: now,
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
const config = await credManager.load()
|
|
426
|
-
if (!config.current_account) {
|
|
427
|
-
await credManager.setCurrentAccount(accountId)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
console.log(formatOutput({ account_id: accountId, user_id: token.user_id, extracted: true }, options.pretty))
|
|
431
|
-
} catch (error) {
|
|
432
|
-
handleError(error as Error)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
393
|
async function listAction(options: { pretty?: boolean }): Promise<void> {
|
|
437
394
|
try {
|
|
438
395
|
const credManager = new CredentialManager()
|
|
@@ -483,7 +440,7 @@ async function statusAction(options: { account?: string; pretty?: boolean }): Pr
|
|
|
483
440
|
const account = await credManager.getAccount(options.account)
|
|
484
441
|
|
|
485
442
|
if (!account) {
|
|
486
|
-
console.log(formatOutput({ error: 'No account configured. Run "auth login"
|
|
443
|
+
console.log(formatOutput({ error: 'No account configured. Run "auth login" first.' }, options.pretty))
|
|
487
444
|
process.exit(1)
|
|
488
445
|
}
|
|
489
446
|
|
|
@@ -533,14 +490,6 @@ export const authCommand = new Command('auth')
|
|
|
533
490
|
.option('--debug', 'Show debug output')
|
|
534
491
|
.action(loginAction),
|
|
535
492
|
)
|
|
536
|
-
.addCommand(
|
|
537
|
-
new Command('extract')
|
|
538
|
-
.description('Extract credentials from KakaoTalk desktop app (kicks desktop session)')
|
|
539
|
-
.option('--pretty', 'Pretty print JSON output')
|
|
540
|
-
.option('--debug', 'Show debug output for troubleshooting')
|
|
541
|
-
.option('--unsafely-show-secrets', 'Show full token values in debug output')
|
|
542
|
-
.action(extractAction),
|
|
543
|
-
)
|
|
544
493
|
.addCommand(
|
|
545
494
|
new Command('list')
|
|
546
495
|
.description('List all authenticated KakaoTalk accounts')
|
|
@@ -34,6 +34,7 @@ export class KakaoCredentialManager {
|
|
|
34
34
|
|
|
35
35
|
async save(config: KakaoConfig): Promise<void> {
|
|
36
36
|
await mkdir(this.configDir, { recursive: true })
|
|
37
|
+
// TODO: Windows does not honor mode 0o600 — consider platform-specific credential storage
|
|
37
38
|
await writeFile(this.credentialsPath, JSON.stringify(config, null, 2))
|
|
38
39
|
await chmod(this.credentialsPath, 0o600)
|
|
39
40
|
}
|
|
@@ -45,7 +45,7 @@ mock.module('./protocol/session', () => ({ LocoSession: MockLocoSession }))
|
|
|
45
45
|
|
|
46
46
|
function createMockClient(overrides: Record<string, unknown> = {}) {
|
|
47
47
|
return {
|
|
48
|
-
getCredentials: mock(() => ({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })),
|
|
48
|
+
getCredentials: mock(() => ({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1', deviceType: 'tablet' as const })),
|
|
49
49
|
...overrides,
|
|
50
50
|
} as any
|
|
51
51
|
}
|
|
@@ -68,7 +68,7 @@ describe('KakaoTalkListener', () => {
|
|
|
68
68
|
await listener.start()
|
|
69
69
|
|
|
70
70
|
expect(mockLogin).toHaveBeenCalledTimes(1)
|
|
71
|
-
expect(mockLogin).toHaveBeenCalledWith('token', 'user1', 'device1')
|
|
71
|
+
expect(mockLogin).toHaveBeenCalledWith('token', 'user1', 'device1', undefined, 'tablet')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('is idempotent', async () => {
|