@pollit/twin-dev-bot 0.0.1 → 0.0.3
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 +16 -8
- package/bin/twindevbot.js +13 -4
- package/dist/cli.js +0 -0
- package/dist/i18n/en.d.ts +2 -2
- package/dist/i18n/en.js +2 -2
- package/package.json +1 -1
- package/dist/action-payload-store.d.ts +0 -22
- package/dist/action-payload-store.js +0 -54
- package/dist/active-runners.d.ts +0 -44
- package/dist/active-runners.js +0 -114
- package/dist/channel-store.d.ts +0 -16
- package/dist/channel-store.js +0 -91
- package/dist/claude-runner.d.ts +0 -57
- package/dist/claude-runner.js +0 -210
- package/dist/config.d.ts +0 -9
- package/dist/config.js +0 -49
- package/dist/conversation-store.d.ts +0 -53
- package/dist/conversation-store.js +0 -173
- package/dist/i18n/ko.d.ts +0 -1
- package/dist/i18n/ko.js +0 -141
- package/dist/logger.d.ts +0 -34
- package/dist/logger.js +0 -110
- package/dist/multi-select-state.d.ts +0 -58
- package/dist/multi-select-state.js +0 -151
- package/dist/paths.d.ts +0 -11
- package/dist/paths.js +0 -18
- package/dist/pending-questions.d.ts +0 -53
- package/dist/pending-questions.js +0 -139
- package/dist/platform.d.ts +0 -18
- package/dist/platform.js +0 -33
- package/dist/progress-tracker.d.ts +0 -47
- package/dist/progress-tracker.js +0 -218
- package/dist/question-blocks.d.ts +0 -27
- package/dist/question-blocks.js +0 -235
- package/dist/session-manager.d.ts +0 -62
- package/dist/session-manager.js +0 -247
- package/dist/workspace-store.d.ts +0 -27
- package/dist/workspace-store.js +0 -160
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# TwinDevBot
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="logo.png" alt="TwinDevBot" width="150" />
|
|
4
|
+
<img src="https://raw.githubusercontent.com/hyeonseungk/twin-dev-bot/main/logo.png" alt="TwinDevBot" width="150" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
A **Slack bot** that lets you develop with **Claude Code** through Slack conversations — **from anywhere**.
|
|
@@ -24,6 +24,8 @@ TwinDevBot connects your Slack workspace to Claude Code running on your machine.
|
|
|
24
24
|
|
|
25
25
|
## Requirements
|
|
26
26
|
|
|
27
|
+
**TwinDevBot is currently only available on macOS.**
|
|
28
|
+
|
|
27
29
|
Before you begin, make sure you have the following:
|
|
28
30
|
|
|
29
31
|
1. **Node.js 18 or later**
|
|
@@ -35,9 +37,8 @@ Before you begin, make sure you have the following:
|
|
|
35
37
|
- Install with: `npm install -g @anthropic-ai/claude-code`
|
|
36
38
|
- Verify by running: `claude --version`
|
|
37
39
|
|
|
38
|
-
3. **
|
|
39
|
-
- `twindevbot start --daemon`, `stop`, and `status` are supported
|
|
40
|
-
- On other platforms, run in the foreground with `twindevbot start`
|
|
40
|
+
3. **Background service**
|
|
41
|
+
- `twindevbot start --daemon`, `stop`, and `status` are supported (macOS launchd)
|
|
41
42
|
|
|
42
43
|
4. **A Slack workspace** where you have permission to install apps
|
|
43
44
|
|
|
@@ -60,9 +61,13 @@ You need to create a Slack App in your workspace. This is a one-time setup.
|
|
|
60
61
|
1. In the left sidebar, click **"Socket Mode"**
|
|
61
62
|
2. Toggle **"Enable Socket Mode"** to ON
|
|
62
63
|
3. You will be asked to create an **App-Level Token**:
|
|
64
|
+
- Enter a name for the token (e.g., `twindevbot-socket`)
|
|
63
65
|
- Add the `connections:write` scope
|
|
64
66
|
- Click **"Generate"**
|
|
65
|
-
4. **Copy the token** (starts with `xapp-`)
|
|
67
|
+
4. **Copy the token** (starts with `xapp-`)
|
|
68
|
+
|
|
69
|
+
> [!CAUTION]
|
|
70
|
+
> You will need this later. Save it somewhere safe now.
|
|
66
71
|
|
|
67
72
|
### 1.3 Add a Slash Command
|
|
68
73
|
|
|
@@ -71,7 +76,7 @@ You need to create a Slack App in your workspace. This is a one-time setup.
|
|
|
71
76
|
3. Fill in:
|
|
72
77
|
- **Command:** `/twindevbot`
|
|
73
78
|
- **Short Description:** `TwinDevBot Commands`
|
|
74
|
-
- **Usage Hint:** `init | task | new | stop`
|
|
79
|
+
- **Usage Hint:** `help | init | task | new | stop`
|
|
75
80
|
4. Click **"Save"**
|
|
76
81
|
|
|
77
82
|
### 1.4 Set Bot Permissions
|
|
@@ -111,7 +116,10 @@ You need to create a Slack App in your workspace. This is a one-time setup.
|
|
|
111
116
|
1. In the left sidebar, click **"Install App"**
|
|
112
117
|
2. Click **"Install to Workspace"**
|
|
113
118
|
3. Review the permissions and click **"Allow"**
|
|
114
|
-
4. **Copy the Bot Token** (starts with `xoxb-`)
|
|
119
|
+
4. **Copy the Bot Token** (starts with `xoxb-`)
|
|
120
|
+
|
|
121
|
+
> [!CAUTION]
|
|
122
|
+
> You will need this later. Save it somewhere safe now.
|
|
115
123
|
|
|
116
124
|
---
|
|
117
125
|
|
|
@@ -120,7 +128,7 @@ You need to create a Slack App in your workspace. This is a one-time setup.
|
|
|
120
128
|
Open your terminal and run:
|
|
121
129
|
|
|
122
130
|
```bash
|
|
123
|
-
npm install -g twin-dev-bot
|
|
131
|
+
npm install -g @pollit/twin-dev-bot
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
Verify the installation:
|
package/bin/twindevbot.js
CHANGED
|
@@ -9,11 +9,20 @@ const entry = join(projectRoot, "dist", "cli.js");
|
|
|
9
9
|
|
|
10
10
|
// start, help 명령만 빌드 (stop, status는 기존 dist 사용)
|
|
11
11
|
const cmd = process.argv[2];
|
|
12
|
-
if (
|
|
12
|
+
if (
|
|
13
|
+
!cmd ||
|
|
14
|
+
cmd === "start" ||
|
|
15
|
+
cmd === "help" ||
|
|
16
|
+
cmd === "--help" ||
|
|
17
|
+
cmd === "-h"
|
|
18
|
+
) {
|
|
13
19
|
try {
|
|
14
|
-
execSync("npx tsc", { cwd: projectRoot, stdio: "
|
|
15
|
-
} catch {
|
|
16
|
-
//
|
|
20
|
+
execSync("npx tsc", { cwd: projectRoot, stdio: "pipe" });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// 빌드 에러 시에만 stderr 출력
|
|
23
|
+
if (error.stderr) {
|
|
24
|
+
process.stderr.write(error.stderr);
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
}
|
|
19
28
|
execFileSync(process.execPath, [entry, ...process.argv.slice(2)], {
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ declare const _en: {
|
|
|
58
58
|
readonly "command.taskNoDir": ":warning: No working directory configured for this channel.\nUse `/twindevbot init` to set one up.";
|
|
59
59
|
readonly "command.taskStarted": ":file_folder: `{{dirName}}` session started.\nContinue in the thread.";
|
|
60
60
|
readonly "command.taskSuccess": ":file_folder: Working on `{{dirName}}`.\nWhat would you like to work on?";
|
|
61
|
-
readonly "command.help": ":robot_face: *TwinDevBot Commands*\nDevelop with Claude Code from anywhere.\n\n:warning: *Before you start:* Invite the bot to this channel first (`/invite @TwinDevBot`). The bot must be a channel member to receive your messages.\n\n
|
|
61
|
+
readonly "command.help": ":robot_face: *TwinDevBot Commands*\nDevelop with Claude Code from anywhere.\n\n:warning: *Before you start:* Invite the bot to this channel first (`/invite @TwinDevBot`). The bot must be a channel member to receive your messages.\n\n:wrench: *Set up channel*\n`/twindevbot init`\nSelect or enter a working directory for this channel.\n\n:rocket: *Start a task*\n`/twindevbot task`\n`/twindevbot task --autopilot`\nStart a new Claude session in the channel's working directory.\n\n:hammer_and_wrench: *Create project*\n`/twindevbot new <directory> --empty`\n`/twindevbot new <directory> --template <framework>`\n`/twindevbot new <directory> --template <framework> --autopilot`\n{{templates}}\n\n:octagonal_sign: *Stop running task*\n`/twindevbot stop`\nCancel the currently running Claude task.\n\n:bulb: *Notes*\n* Use `init` once per channel to link it to a project directory.\n* Use `task` to start a new task thread.\n* `--autopilot` mode: twindevbot automatically answers all questions and develops on its own.\n\n";
|
|
62
62
|
readonly "slack.answered": "Answer submitted";
|
|
63
63
|
readonly "slack.question": "Question";
|
|
64
64
|
readonly "progress.tool.Read": "Reading file";
|
|
@@ -97,7 +97,7 @@ declare const _en: {
|
|
|
97
97
|
readonly "cli.usage": "Usage:";
|
|
98
98
|
readonly "cli.commands": "Commands:";
|
|
99
99
|
readonly "cli.cmd.start": "Start server (foreground)";
|
|
100
|
-
readonly "cli.cmd.startDaemon": "Register and start as background service (macOS launchd
|
|
100
|
+
readonly "cli.cmd.startDaemon": "Register and start as background service (macOS launchd)";
|
|
101
101
|
readonly "cli.cmd.stop": "Stop and unregister background service";
|
|
102
102
|
readonly "cli.cmd.status": "Check background service status";
|
|
103
103
|
readonly "cli.cmd.show": "Show saved sessions";
|
package/dist/i18n/en.js
CHANGED
|
@@ -65,7 +65,7 @@ const _en = {
|
|
|
65
65
|
"command.taskStarted": ":file_folder: `{{dirName}}` session started.\nContinue in the thread.",
|
|
66
66
|
"command.taskSuccess": ":file_folder: Working on `{{dirName}}`.\nWhat would you like to work on?",
|
|
67
67
|
// help
|
|
68
|
-
"command.help": ":robot_face: *TwinDevBot Commands*\nDevelop with Claude Code from anywhere.\n\n:warning: *Before you start:* Invite the bot to this channel first (`/invite @TwinDevBot`). The bot must be a channel member to receive your messages.\n\n
|
|
68
|
+
"command.help": ":robot_face: *TwinDevBot Commands*\nDevelop with Claude Code from anywhere.\n\n:warning: *Before you start:* Invite the bot to this channel first (`/invite @TwinDevBot`). The bot must be a channel member to receive your messages.\n\n:wrench: *Set up channel*\n`/twindevbot init`\nSelect or enter a working directory for this channel.\n\n:rocket: *Start a task*\n`/twindevbot task`\n`/twindevbot task --autopilot`\nStart a new Claude session in the channel's working directory.\n\n:hammer_and_wrench: *Create project*\n`/twindevbot new <directory> --empty`\n`/twindevbot new <directory> --template <framework>`\n`/twindevbot new <directory> --template <framework> --autopilot`\n{{templates}}\n\n:octagonal_sign: *Stop running task*\n`/twindevbot stop`\nCancel the currently running Claude task.\n\n:bulb: *Notes*\n* Use `init` once per channel to link it to a project directory.\n* Use `task` to start a new task thread.\n* `--autopilot` mode: twindevbot automatically answers all questions and develops on its own.\n\n",
|
|
69
69
|
// slack-message.ts
|
|
70
70
|
"slack.answered": "Answer submitted",
|
|
71
71
|
"slack.question": "Question",
|
|
@@ -109,7 +109,7 @@ const _en = {
|
|
|
109
109
|
"cli.usage": "Usage:",
|
|
110
110
|
"cli.commands": "Commands:",
|
|
111
111
|
"cli.cmd.start": "Start server (foreground)",
|
|
112
|
-
"cli.cmd.startDaemon": "Register and start as background service (macOS launchd
|
|
112
|
+
"cli.cmd.startDaemon": "Register and start as background service (macOS launchd)",
|
|
113
113
|
"cli.cmd.stop": "Stop and unregister background service",
|
|
114
114
|
"cli.cmd.status": "Check background service status",
|
|
115
115
|
"cli.cmd.show": "Show saved sessions",
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slack action.value (~2,000 bytes) 및 private_metadata (~3,000 bytes) 크기 제한 대응.
|
|
3
|
-
* 큰 페이로드를 서버 사이드에 저장하고 짧은 키만 버튼 값에 포함.
|
|
4
|
-
* TTL 기반 자동 정리로 메모리 누수 방지.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* 페이로드 저장
|
|
8
|
-
*/
|
|
9
|
-
export declare function setPayload(key: string, data: unknown, ttlMs?: number): void;
|
|
10
|
-
/**
|
|
11
|
-
* 페이로드 조회 (기본: 조회 후 삭제하지 않음)
|
|
12
|
-
* @param remove true이면 조회 후 삭제 (일회성 데이터용)
|
|
13
|
-
*/
|
|
14
|
-
export declare function getPayload<T>(key: string, remove?: boolean): T | null;
|
|
15
|
-
/**
|
|
16
|
-
* 페이로드 삭제
|
|
17
|
-
*/
|
|
18
|
-
export declare function removePayload(key: string): void;
|
|
19
|
-
/**
|
|
20
|
-
* 테스트용: 스토어 전체 초기화
|
|
21
|
-
*/
|
|
22
|
-
export declare function clearAllPayloads(): void;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slack action.value (~2,000 bytes) 및 private_metadata (~3,000 bytes) 크기 제한 대응.
|
|
3
|
-
* 큰 페이로드를 서버 사이드에 저장하고 짧은 키만 버튼 값에 포함.
|
|
4
|
-
* TTL 기반 자동 정리로 메모리 누수 방지.
|
|
5
|
-
*/
|
|
6
|
-
const store = new Map();
|
|
7
|
-
const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000; // 2시간
|
|
8
|
-
function cleanup() {
|
|
9
|
-
const now = Date.now();
|
|
10
|
-
for (const [key, entry] of store) {
|
|
11
|
-
if (now > entry.expiresAt)
|
|
12
|
-
store.delete(key);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* 페이로드 저장
|
|
17
|
-
*/
|
|
18
|
-
export function setPayload(key, data, ttlMs = DEFAULT_TTL_MS) {
|
|
19
|
-
cleanup();
|
|
20
|
-
store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 페이로드 조회 (기본: 조회 후 삭제하지 않음)
|
|
24
|
-
* @param remove true이면 조회 후 삭제 (일회성 데이터용)
|
|
25
|
-
*/
|
|
26
|
-
export function getPayload(key, remove = false) {
|
|
27
|
-
const entry = store.get(key);
|
|
28
|
-
if (!entry)
|
|
29
|
-
return null;
|
|
30
|
-
if (Date.now() > entry.expiresAt) {
|
|
31
|
-
store.delete(key);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
if (remove) {
|
|
35
|
-
store.delete(key);
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
// TTL 갱신: 접근 시 만료 시간 리셋 (사용자가 상호작용 중임을 표시)
|
|
39
|
-
entry.expiresAt = Date.now() + DEFAULT_TTL_MS;
|
|
40
|
-
}
|
|
41
|
-
return entry.data;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 페이로드 삭제
|
|
45
|
-
*/
|
|
46
|
-
export function removePayload(key) {
|
|
47
|
-
store.delete(key);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* 테스트용: 스토어 전체 초기화
|
|
51
|
-
*/
|
|
52
|
-
export function clearAllPayloads() {
|
|
53
|
-
store.clear();
|
|
54
|
-
}
|
package/dist/active-runners.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 활성 러너 레지스트리
|
|
3
|
-
*
|
|
4
|
-
* Slack 스레드(threadTs)별로 실행 중인 ClaudeRunner 인스턴스를 추적하여
|
|
5
|
-
* 동일 스레드에서 여러 Claude 프로세스가 동시에 실행되는 것을 방지합니다.
|
|
6
|
-
*
|
|
7
|
-
* - 마지막 이벤트 수신 후 config.inactivityTimeoutMs(기본 30분) 동안
|
|
8
|
-
* 활동이 없으면 프로세스를 자동 종료하고 스레드 잠금을 해제합니다.
|
|
9
|
-
* - onTimeout 콜백을 통해 Slack 타임아웃 알림 등 외부 처리가 가능합니다.
|
|
10
|
-
*/
|
|
11
|
-
import type { ClaudeRunner } from "./claude-runner.js";
|
|
12
|
-
export interface RegisterRunnerOptions {
|
|
13
|
-
/** 타임아웃 시 호출할 콜백 (Slack 알림 등) */
|
|
14
|
-
onTimeout?: () => void;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* 러너를 활성 상태로 등록
|
|
18
|
-
*/
|
|
19
|
-
export declare function registerRunner(threadTs: string, runner: ClaudeRunner, options?: RegisterRunnerOptions): void;
|
|
20
|
-
/**
|
|
21
|
-
* 이벤트 수신 시 활동 시간 갱신 및 타임아웃 리셋
|
|
22
|
-
* 저장된 runner와 동일한 인스턴스인 경우에만 갱신 (stale 갱신 방지)
|
|
23
|
-
*/
|
|
24
|
-
export declare function refreshActivity(threadTs: string, runner: ClaudeRunner): void;
|
|
25
|
-
/**
|
|
26
|
-
* 러너를 비활성 상태로 해제
|
|
27
|
-
* 저장된 runner와 동일한 인스턴스인 경우에만 해제 (stale 해제 방지)
|
|
28
|
-
*/
|
|
29
|
-
export declare function unregisterRunner(threadTs: string, runner: ClaudeRunner): void;
|
|
30
|
-
/**
|
|
31
|
-
* 해당 스레드에 활성 러너가 있는지 확인
|
|
32
|
-
*/
|
|
33
|
-
export declare function isRunnerActive(threadTs: string): boolean;
|
|
34
|
-
/**
|
|
35
|
-
* 활성 러너를 강제 종료하고 등록 해제.
|
|
36
|
-
* autopilot 개입, /twindevbot stop 명령, 일반 모드 인터럽트 등
|
|
37
|
-
* 외부 요청에 의해 실행 중인 작업을 중단할 때 사용합니다.
|
|
38
|
-
*/
|
|
39
|
-
export declare function killActiveRunner(threadTs: string): void;
|
|
40
|
-
/**
|
|
41
|
-
* 모든 활성 러너를 강제 종료하고 등록 해제
|
|
42
|
-
* (graceful shutdown 시 고아 프로세스 방지)
|
|
43
|
-
*/
|
|
44
|
-
export declare function killAllRunners(): number;
|
package/dist/active-runners.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 활성 러너 레지스트리
|
|
3
|
-
*
|
|
4
|
-
* Slack 스레드(threadTs)별로 실행 중인 ClaudeRunner 인스턴스를 추적하여
|
|
5
|
-
* 동일 스레드에서 여러 Claude 프로세스가 동시에 실행되는 것을 방지합니다.
|
|
6
|
-
*
|
|
7
|
-
* - 마지막 이벤트 수신 후 config.inactivityTimeoutMs(기본 30분) 동안
|
|
8
|
-
* 활동이 없으면 프로세스를 자동 종료하고 스레드 잠금을 해제합니다.
|
|
9
|
-
* - onTimeout 콜백을 통해 Slack 타임아웃 알림 등 외부 처리가 가능합니다.
|
|
10
|
-
*/
|
|
11
|
-
import { createLogger } from "./logger.js";
|
|
12
|
-
import { config } from "./config.js";
|
|
13
|
-
const log = createLogger("active-runners");
|
|
14
|
-
const activeRunners = new Map();
|
|
15
|
-
function startTimer(threadTs, entry) {
|
|
16
|
-
return setTimeout(() => {
|
|
17
|
-
log.warn("Runner inactivity timeout", {
|
|
18
|
-
threadTs,
|
|
19
|
-
inactiveMs: Date.now() - entry.lastActivityAt,
|
|
20
|
-
totalMs: Date.now() - entry.registeredAt,
|
|
21
|
-
});
|
|
22
|
-
// 콜백 실행 (Slack 알림 등)
|
|
23
|
-
try {
|
|
24
|
-
entry.onTimeout?.();
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
log.error("onTimeout callback error", err);
|
|
28
|
-
}
|
|
29
|
-
// 프로세스 종료 및 등록 해제
|
|
30
|
-
entry.runner.kill();
|
|
31
|
-
activeRunners.delete(threadTs);
|
|
32
|
-
}, config.inactivityTimeoutMs);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* 러너를 활성 상태로 등록
|
|
36
|
-
*/
|
|
37
|
-
export function registerRunner(threadTs, runner, options) {
|
|
38
|
-
// 기존 엔트리가 있으면 프로세스 종료 및 타이머 정리
|
|
39
|
-
const existing = activeRunners.get(threadTs);
|
|
40
|
-
if (existing) {
|
|
41
|
-
clearTimeout(existing.timer);
|
|
42
|
-
existing.runner.kill();
|
|
43
|
-
log.info("Previous runner killed on re-register", { threadTs });
|
|
44
|
-
}
|
|
45
|
-
const now = Date.now();
|
|
46
|
-
const entry = {
|
|
47
|
-
runner,
|
|
48
|
-
timer: null,
|
|
49
|
-
registeredAt: now,
|
|
50
|
-
lastActivityAt: now,
|
|
51
|
-
onTimeout: options?.onTimeout,
|
|
52
|
-
};
|
|
53
|
-
entry.timer = startTimer(threadTs, entry);
|
|
54
|
-
activeRunners.set(threadTs, entry);
|
|
55
|
-
log.debug("Runner registered", { threadTs });
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 이벤트 수신 시 활동 시간 갱신 및 타임아웃 리셋
|
|
59
|
-
* 저장된 runner와 동일한 인스턴스인 경우에만 갱신 (stale 갱신 방지)
|
|
60
|
-
*/
|
|
61
|
-
export function refreshActivity(threadTs, runner) {
|
|
62
|
-
const entry = activeRunners.get(threadTs);
|
|
63
|
-
if (!entry || entry.runner !== runner)
|
|
64
|
-
return;
|
|
65
|
-
entry.lastActivityAt = Date.now();
|
|
66
|
-
clearTimeout(entry.timer);
|
|
67
|
-
entry.timer = startTimer(threadTs, entry);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* 러너를 비활성 상태로 해제
|
|
71
|
-
* 저장된 runner와 동일한 인스턴스인 경우에만 해제 (stale 해제 방지)
|
|
72
|
-
*/
|
|
73
|
-
export function unregisterRunner(threadTs, runner) {
|
|
74
|
-
const entry = activeRunners.get(threadTs);
|
|
75
|
-
if (!entry || entry.runner !== runner)
|
|
76
|
-
return;
|
|
77
|
-
clearTimeout(entry.timer);
|
|
78
|
-
activeRunners.delete(threadTs);
|
|
79
|
-
log.debug("Runner unregistered", { threadTs });
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* 해당 스레드에 활성 러너가 있는지 확인
|
|
83
|
-
*/
|
|
84
|
-
export function isRunnerActive(threadTs) {
|
|
85
|
-
return activeRunners.has(threadTs);
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* 활성 러너를 강제 종료하고 등록 해제.
|
|
89
|
-
* autopilot 개입, /twindevbot stop 명령, 일반 모드 인터럽트 등
|
|
90
|
-
* 외부 요청에 의해 실행 중인 작업을 중단할 때 사용합니다.
|
|
91
|
-
*/
|
|
92
|
-
export function killActiveRunner(threadTs) {
|
|
93
|
-
const entry = activeRunners.get(threadTs);
|
|
94
|
-
if (!entry)
|
|
95
|
-
return;
|
|
96
|
-
clearTimeout(entry.timer);
|
|
97
|
-
entry.runner.kill();
|
|
98
|
-
activeRunners.delete(threadTs);
|
|
99
|
-
log.info("Runner killed by external request", { threadTs });
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* 모든 활성 러너를 강제 종료하고 등록 해제
|
|
103
|
-
* (graceful shutdown 시 고아 프로세스 방지)
|
|
104
|
-
*/
|
|
105
|
-
export function killAllRunners() {
|
|
106
|
-
const count = activeRunners.size;
|
|
107
|
-
for (const [threadTs, entry] of activeRunners) {
|
|
108
|
-
clearTimeout(entry.timer);
|
|
109
|
-
entry.runner.kill();
|
|
110
|
-
log.info("Runner killed during shutdown", { threadTs });
|
|
111
|
-
}
|
|
112
|
-
activeRunners.clear();
|
|
113
|
-
return count;
|
|
114
|
-
}
|
package/dist/channel-store.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Channel Store
|
|
3
|
-
*
|
|
4
|
-
* 슬랙 채널과 작업 디렉토리의 매핑을 관리합니다.
|
|
5
|
-
* /twindevbot init으로 설정된 채널별 작업 디렉토리를 영속적으로 저장합니다.
|
|
6
|
-
*
|
|
7
|
-
* 키: channelId (Slack 채널 ID)
|
|
8
|
-
* 값: { directory, projectName }
|
|
9
|
-
*/
|
|
10
|
-
export interface ChannelDir {
|
|
11
|
-
directory: string;
|
|
12
|
-
projectName: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function setChannelDir(channelId: string, dir: ChannelDir): void;
|
|
15
|
-
export declare function getChannelDir(channelId: string): ChannelDir | undefined;
|
|
16
|
-
export declare function removeChannelDir(channelId: string): void;
|
package/dist/channel-store.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Channel Store
|
|
3
|
-
*
|
|
4
|
-
* 슬랙 채널과 작업 디렉토리의 매핑을 관리합니다.
|
|
5
|
-
* /twindevbot init으로 설정된 채널별 작업 디렉토리를 영속적으로 저장합니다.
|
|
6
|
-
*
|
|
7
|
-
* 키: channelId (Slack 채널 ID)
|
|
8
|
-
* 값: { directory, projectName }
|
|
9
|
-
*/
|
|
10
|
-
import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
11
|
-
import { createLogger } from "./logger.js";
|
|
12
|
-
import { CHANNELS_FILE } from "./paths.js";
|
|
13
|
-
const log = createLogger("channel-store");
|
|
14
|
-
const channels = new Map();
|
|
15
|
-
function loadFromFile() {
|
|
16
|
-
try {
|
|
17
|
-
if (!existsSync(CHANNELS_FILE)) {
|
|
18
|
-
log.info("No channels file found, starting fresh");
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const content = readFileSync(CHANNELS_FILE, "utf-8");
|
|
22
|
-
let data;
|
|
23
|
-
try {
|
|
24
|
-
data = JSON.parse(content);
|
|
25
|
-
}
|
|
26
|
-
catch (parseError) {
|
|
27
|
-
log.error("Channels file is corrupted, starting fresh", { parseError });
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (!data.channels || !Array.isArray(data.channels)) {
|
|
31
|
-
log.error("Channels file has invalid structure, starting fresh");
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
let loadedCount = 0;
|
|
35
|
-
for (const c of data.channels) {
|
|
36
|
-
try {
|
|
37
|
-
channels.set(c.channelId, {
|
|
38
|
-
directory: c.directory,
|
|
39
|
-
projectName: c.projectName,
|
|
40
|
-
});
|
|
41
|
-
loadedCount++;
|
|
42
|
-
}
|
|
43
|
-
catch (entryError) {
|
|
44
|
-
log.warn("Skipping invalid channel entry", { entry: c, error: entryError });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
log.info("Channels loaded from file", { count: loadedCount, total: data.channels.length });
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
log.error("Failed to load channels from file", { error });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function saveToFile() {
|
|
54
|
-
try {
|
|
55
|
-
const serialized = Array.from(channels.entries()).map(([channelId, c]) => ({
|
|
56
|
-
channelId,
|
|
57
|
-
directory: c.directory,
|
|
58
|
-
projectName: c.projectName,
|
|
59
|
-
}));
|
|
60
|
-
const data = {
|
|
61
|
-
version: 1,
|
|
62
|
-
channels: serialized,
|
|
63
|
-
};
|
|
64
|
-
const tmpFile = CHANNELS_FILE + ".tmp";
|
|
65
|
-
writeFileSync(tmpFile, JSON.stringify(data, null, 2));
|
|
66
|
-
renameSync(tmpFile, CHANNELS_FILE);
|
|
67
|
-
log.debug("Channels saved to file", { count: serialized.length });
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
log.error("Failed to save channels to file", { error });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// 모듈 로드 시 파일에서 복원
|
|
74
|
-
loadFromFile();
|
|
75
|
-
export function setChannelDir(channelId, dir) {
|
|
76
|
-
channels.set(channelId, dir);
|
|
77
|
-
log.info("Channel directory set", {
|
|
78
|
-
channelId,
|
|
79
|
-
projectName: dir.projectName,
|
|
80
|
-
directory: dir.directory,
|
|
81
|
-
});
|
|
82
|
-
saveToFile();
|
|
83
|
-
}
|
|
84
|
-
export function getChannelDir(channelId) {
|
|
85
|
-
return channels.get(channelId);
|
|
86
|
-
}
|
|
87
|
-
export function removeChannelDir(channelId) {
|
|
88
|
-
channels.delete(channelId);
|
|
89
|
-
log.debug("Channel directory removed", { channelId });
|
|
90
|
-
saveToFile();
|
|
91
|
-
}
|
package/dist/claude-runner.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "events";
|
|
2
|
-
export interface ClaudeRunnerOptions {
|
|
3
|
-
directory: string;
|
|
4
|
-
prompt: string;
|
|
5
|
-
sessionId?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface AskUserQuestionInput {
|
|
8
|
-
questions: Array<{
|
|
9
|
-
question: string;
|
|
10
|
-
header?: string;
|
|
11
|
-
options: Array<{
|
|
12
|
-
label: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
}>;
|
|
15
|
-
multiSelect?: boolean;
|
|
16
|
-
}>;
|
|
17
|
-
}
|
|
18
|
-
export interface AskUserEvent {
|
|
19
|
-
input: AskUserQuestionInput;
|
|
20
|
-
}
|
|
21
|
-
export interface InitEvent {
|
|
22
|
-
sessionId: string;
|
|
23
|
-
model?: string;
|
|
24
|
-
}
|
|
25
|
-
export interface TextEvent {
|
|
26
|
-
text: string;
|
|
27
|
-
}
|
|
28
|
-
export interface ToolUseEvent {
|
|
29
|
-
toolName: string;
|
|
30
|
-
input: Record<string, unknown>;
|
|
31
|
-
}
|
|
32
|
-
export interface ResultEvent {
|
|
33
|
-
result?: string;
|
|
34
|
-
costUsd?: number;
|
|
35
|
-
}
|
|
36
|
-
export declare class ClaudeRunner extends EventEmitter {
|
|
37
|
-
private options;
|
|
38
|
-
private sessionId;
|
|
39
|
-
private process;
|
|
40
|
-
/** 이미 처리한 tool_use ID를 추적 */
|
|
41
|
-
private processedToolUseIds;
|
|
42
|
-
/** init 이벤트 발생 여부 */
|
|
43
|
-
private initEmitted;
|
|
44
|
-
/** result 이벤트 발생 여부 */
|
|
45
|
-
private resultEmitted;
|
|
46
|
-
/** stderr 출력 누적 버퍼 */
|
|
47
|
-
private stderrLines;
|
|
48
|
-
constructor(options: ClaudeRunnerOptions);
|
|
49
|
-
get currentSessionId(): string | null;
|
|
50
|
-
/** 누적된 stderr 출력을 반환 */
|
|
51
|
-
get stderrOutput(): string;
|
|
52
|
-
run(): void;
|
|
53
|
-
private handleLine;
|
|
54
|
-
private handleEvent;
|
|
55
|
-
kill(): void;
|
|
56
|
-
}
|
|
57
|
-
export declare function runClaude(options: ClaudeRunnerOptions): ClaudeRunner;
|