@pollit/twin-dev-bot 0.0.1

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.
Files changed (110) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +415 -0
  3. package/bin/twindevbot.js +22 -0
  4. package/dist/action-payload-store.d.ts +22 -0
  5. package/dist/action-payload-store.js +54 -0
  6. package/dist/active-runners.d.ts +44 -0
  7. package/dist/active-runners.js +114 -0
  8. package/dist/channel-store.d.ts +16 -0
  9. package/dist/channel-store.js +91 -0
  10. package/dist/claude/active-runners.d.ts +44 -0
  11. package/dist/claude/active-runners.js +114 -0
  12. package/dist/claude/claude-runner.d.ts +57 -0
  13. package/dist/claude/claude-runner.js +210 -0
  14. package/dist/claude/session-manager.d.ts +62 -0
  15. package/dist/claude/session-manager.js +247 -0
  16. package/dist/claude-runner.d.ts +57 -0
  17. package/dist/claude-runner.js +210 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +271 -0
  20. package/dist/config.d.ts +9 -0
  21. package/dist/config.js +49 -0
  22. package/dist/conversation-store.d.ts +53 -0
  23. package/dist/conversation-store.js +173 -0
  24. package/dist/core/config.d.ts +9 -0
  25. package/dist/core/config.js +49 -0
  26. package/dist/core/logger.d.ts +34 -0
  27. package/dist/core/logger.js +110 -0
  28. package/dist/core/paths.d.ts +11 -0
  29. package/dist/core/paths.js +18 -0
  30. package/dist/core/platform.d.ts +18 -0
  31. package/dist/core/platform.js +33 -0
  32. package/dist/daemon/index.d.ts +3 -0
  33. package/dist/daemon/index.js +14 -0
  34. package/dist/daemon/macos.d.ts +8 -0
  35. package/dist/daemon/macos.js +150 -0
  36. package/dist/daemon/types.d.ts +9 -0
  37. package/dist/daemon/types.js +1 -0
  38. package/dist/daemon/windows.d.ts +8 -0
  39. package/dist/daemon/windows.js +137 -0
  40. package/dist/handlers/claude-command.d.ts +2 -0
  41. package/dist/handlers/claude-command.js +634 -0
  42. package/dist/handlers/claude-runner-setup.d.ts +16 -0
  43. package/dist/handlers/claude-runner-setup.js +445 -0
  44. package/dist/handlers/index.d.ts +3 -0
  45. package/dist/handlers/index.js +3 -0
  46. package/dist/handlers/init-handlers.d.ts +2 -0
  47. package/dist/handlers/init-handlers.js +189 -0
  48. package/dist/handlers/question-handlers.d.ts +2 -0
  49. package/dist/handlers/question-handlers.js +835 -0
  50. package/dist/i18n/en.d.ts +150 -0
  51. package/dist/i18n/en.js +163 -0
  52. package/dist/i18n/index.d.ts +20 -0
  53. package/dist/i18n/index.js +31 -0
  54. package/dist/i18n/ko.d.ts +1 -0
  55. package/dist/i18n/ko.js +141 -0
  56. package/dist/logger.d.ts +34 -0
  57. package/dist/logger.js +110 -0
  58. package/dist/multi-select-state.d.ts +58 -0
  59. package/dist/multi-select-state.js +151 -0
  60. package/dist/paths.d.ts +11 -0
  61. package/dist/paths.js +18 -0
  62. package/dist/pending-questions.d.ts +53 -0
  63. package/dist/pending-questions.js +139 -0
  64. package/dist/platform.d.ts +18 -0
  65. package/dist/platform.js +33 -0
  66. package/dist/progress-tracker.d.ts +47 -0
  67. package/dist/progress-tracker.js +218 -0
  68. package/dist/question-blocks.d.ts +27 -0
  69. package/dist/question-blocks.js +235 -0
  70. package/dist/server.d.ts +1 -0
  71. package/dist/server.js +83 -0
  72. package/dist/session-manager.d.ts +62 -0
  73. package/dist/session-manager.js +247 -0
  74. package/dist/setup.d.ts +5 -0
  75. package/dist/setup.js +132 -0
  76. package/dist/slack/progress-tracker.d.ts +47 -0
  77. package/dist/slack/progress-tracker.js +218 -0
  78. package/dist/slack/question-blocks.d.ts +27 -0
  79. package/dist/slack/question-blocks.js +235 -0
  80. package/dist/stores/action-payload-store.d.ts +22 -0
  81. package/dist/stores/action-payload-store.js +54 -0
  82. package/dist/stores/channel-store.d.ts +16 -0
  83. package/dist/stores/channel-store.js +91 -0
  84. package/dist/stores/multi-select-state.d.ts +58 -0
  85. package/dist/stores/multi-select-state.js +151 -0
  86. package/dist/stores/pending-questions.d.ts +53 -0
  87. package/dist/stores/pending-questions.js +139 -0
  88. package/dist/stores/workspace-store.d.ts +27 -0
  89. package/dist/stores/workspace-store.js +160 -0
  90. package/dist/templates.d.ts +23 -0
  91. package/dist/templates.js +292 -0
  92. package/dist/types/claude-stream.d.ts +116 -0
  93. package/dist/types/claude-stream.js +3 -0
  94. package/dist/types/conversation.d.ts +16 -0
  95. package/dist/types/conversation.js +4 -0
  96. package/dist/types/index.d.ts +2 -0
  97. package/dist/types/index.js +2 -0
  98. package/dist/types/slack.d.ts +51 -0
  99. package/dist/types/slack.js +1 -0
  100. package/dist/utils/display-width.d.ts +8 -0
  101. package/dist/utils/display-width.js +33 -0
  102. package/dist/utils/safe-async.d.ts +6 -0
  103. package/dist/utils/safe-async.js +14 -0
  104. package/dist/utils/slack-message.d.ts +73 -0
  105. package/dist/utils/slack-message.js +220 -0
  106. package/dist/utils/slack-rate-limit.d.ts +5 -0
  107. package/dist/utils/slack-rate-limit.js +49 -0
  108. package/dist/workspace-store.d.ts +27 -0
  109. package/dist/workspace-store.js +160 -0
  110. package/package.json +51 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Workspace Store
3
+ *
4
+ * /twindevbot goto 로 생성된 스레드와 작업 디렉토리의 매핑을 관리합니다.
5
+ * 스레드에 첫 메시지가 오면 이 매핑을 사용하여 Claude 세션을 시작합니다.
6
+ *
7
+ * 키: threadTs (Slack 스레드 부모 메시지 타임스탬프)
8
+ * 값: { directory, projectName, channelId }
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
11
+ import { createLogger } from "./logger.js";
12
+ import { WORKSPACES_FILE } from "./paths.js";
13
+ const log = createLogger("workspace-store");
14
+ const workspaces = new Map();
15
+ let cleanupInterval = null;
16
+ // 24시간이 지난 워크스페이스는 자동 삭제
17
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
18
+ function loadFromFile() {
19
+ try {
20
+ if (!existsSync(WORKSPACES_FILE)) {
21
+ log.info("No workspaces file found, starting fresh");
22
+ return;
23
+ }
24
+ const content = readFileSync(WORKSPACES_FILE, "utf-8");
25
+ let data;
26
+ try {
27
+ data = JSON.parse(content);
28
+ }
29
+ catch (parseError) {
30
+ log.error("Workspaces file is corrupted, starting fresh", { parseError });
31
+ return;
32
+ }
33
+ if (!data.workspaces || !Array.isArray(data.workspaces)) {
34
+ log.error("Workspaces file has invalid structure, starting fresh");
35
+ return;
36
+ }
37
+ let loadedCount = 0;
38
+ for (const w of data.workspaces) {
39
+ try {
40
+ workspaces.set(w.threadTs, {
41
+ directory: w.directory,
42
+ projectName: w.projectName,
43
+ channelId: w.channelId,
44
+ autopilot: w.autopilot,
45
+ createdAt: w.createdAt ? new Date(w.createdAt) : new Date(),
46
+ });
47
+ loadedCount++;
48
+ }
49
+ catch (entryError) {
50
+ log.warn("Skipping invalid workspace entry", { entry: w, error: entryError });
51
+ }
52
+ }
53
+ log.info("Workspaces loaded from file", { count: loadedCount, total: data.workspaces.length });
54
+ }
55
+ catch (error) {
56
+ log.error("Failed to load workspaces from file", { error });
57
+ }
58
+ }
59
+ function saveToFile() {
60
+ try {
61
+ const serialized = Array.from(workspaces.entries()).map(([threadTs, w]) => ({
62
+ threadTs,
63
+ directory: w.directory,
64
+ projectName: w.projectName,
65
+ channelId: w.channelId,
66
+ autopilot: w.autopilot ?? undefined,
67
+ createdAt: w.createdAt?.toISOString(),
68
+ }));
69
+ const data = {
70
+ version: 1,
71
+ workspaces: serialized,
72
+ };
73
+ const tmpFile = WORKSPACES_FILE + ".tmp";
74
+ writeFileSync(tmpFile, JSON.stringify(data, null, 2));
75
+ renameSync(tmpFile, WORKSPACES_FILE);
76
+ log.debug("Workspaces saved to file", { count: serialized.length });
77
+ }
78
+ catch (error) {
79
+ log.error("Failed to save workspaces to file", { error });
80
+ }
81
+ }
82
+ // 모듈 로드 시 파일에서 복원
83
+ loadFromFile();
84
+ // 정리 타이머 시작
85
+ function startCleanupTimer() {
86
+ if (cleanupInterval)
87
+ return;
88
+ // 매 시간마다 실행 (첫 실행은 1시간 후)
89
+ cleanupInterval = setInterval(() => {
90
+ cleanup();
91
+ }, 60 * 60 * 1000);
92
+ cleanupInterval.unref();
93
+ log.debug("Workspace cleanup timer started (runs every hour)");
94
+ }
95
+ startCleanupTimer();
96
+ export function addWorkspace(threadTs, workspace) {
97
+ // createdAt이 없으면 현재 시간으로 설정
98
+ if (!workspace.createdAt) {
99
+ workspace.createdAt = new Date();
100
+ }
101
+ workspaces.set(threadTs, workspace);
102
+ log.info("Workspace registered", {
103
+ threadTs,
104
+ projectName: workspace.projectName,
105
+ directory: workspace.directory,
106
+ });
107
+ saveToFile();
108
+ }
109
+ export function getWorkspace(threadTs) {
110
+ return workspaces.get(threadTs);
111
+ }
112
+ export function removeWorkspace(threadTs) {
113
+ workspaces.delete(threadTs);
114
+ log.debug("Workspace removed", { threadTs });
115
+ saveToFile();
116
+ }
117
+ /**
118
+ * 오래된 워크스페이스 정리 (createdAt 기준)
119
+ */
120
+ function cleanup() {
121
+ const now = Date.now();
122
+ let cleanedCount = 0;
123
+ for (const [threadTs, workspace] of workspaces.entries()) {
124
+ // createdAt이 없는 항목은 건너뜀 (레거시 데이터)
125
+ if (!workspace.createdAt) {
126
+ continue;
127
+ }
128
+ const ageMs = now - workspace.createdAt.getTime();
129
+ if (ageMs > MAX_AGE_MS) {
130
+ workspaces.delete(threadTs);
131
+ cleanedCount++;
132
+ log.info("Workspace expired", {
133
+ threadTs,
134
+ projectName: workspace.projectName,
135
+ ageDays: Math.floor(ageMs / (24 * 60 * 60 * 1000)),
136
+ });
137
+ }
138
+ }
139
+ if (cleanedCount > 0) {
140
+ saveToFile();
141
+ log.info("Workspace cleanup completed", { cleanedCount, remaining: workspaces.size });
142
+ }
143
+ return cleanedCount;
144
+ }
145
+ /**
146
+ * 정리 타이머 중지 (주로 테스트용)
147
+ */
148
+ export function stopCleanupTimer() {
149
+ if (cleanupInterval) {
150
+ clearInterval(cleanupInterval);
151
+ cleanupInterval = null;
152
+ log.debug("Workspace cleanup timer stopped");
153
+ }
154
+ }
155
+ /**
156
+ * 테스트용: cleanup 수동 실행
157
+ */
158
+ export function runCleanup() {
159
+ return cleanup();
160
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@pollit/twin-dev-bot",
3
+ "version": "0.0.1",
4
+ "description": "Slack bot that runs Claude Code via thread-based conversations",
5
+ "type": "module",
6
+ "bin": {
7
+ "twindevbot": "bin/twindevbot.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "prepublishOnly": "npm run build",
19
+ "dev:server": "tsx src/server.ts",
20
+ "start:server": "node dist/server.js",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:coverage": "vitest run --coverage"
24
+ },
25
+ "keywords": [
26
+ "claude",
27
+ "slack",
28
+ "bridge",
29
+ "claude-code",
30
+ "ai",
31
+ "automation"
32
+ ],
33
+ "license": "AGPL-3.0-only",
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@inquirer/prompts": "^8.2.0",
39
+ "@slack/bolt": "^4.6.0",
40
+ "@slack/types": "^2.19.0",
41
+ "@slack/web-api": "^7.13.0",
42
+ "dotenv": "^16.4.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.11.0",
46
+ "@vitest/coverage-v8": "^4.0.18",
47
+ "tsx": "^4.7.0",
48
+ "typescript": "^5.3.0",
49
+ "vitest": "^4.0.18"
50
+ }
51
+ }