tabclaw 0.1.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/README.md +113 -0
- package/config.example.toml +29 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1858 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/daemon/process.js +5712 -0
- package/dist/daemon/process.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command8 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/attach.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/shared/settings-merge.ts
|
|
10
|
+
function isTabclawHook(hook) {
|
|
11
|
+
return hook.type === "http" && hook.url.includes("/hook/");
|
|
12
|
+
}
|
|
13
|
+
function isTabclawEntry(entry) {
|
|
14
|
+
return entry.hooks.some((hook) => isTabclawHook(hook));
|
|
15
|
+
}
|
|
16
|
+
function removeTabclawHooks(settings) {
|
|
17
|
+
if (!settings.hooks) return settings;
|
|
18
|
+
const cleanedHooks = {};
|
|
19
|
+
for (const [eventName, entries] of Object.entries(settings.hooks)) {
|
|
20
|
+
const cleanedEntries = entries.filter((entry) => !isTabclawEntry(entry));
|
|
21
|
+
if (cleanedEntries.length > 0) {
|
|
22
|
+
cleanedHooks[eventName] = cleanedEntries;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { ...settings, hooks: cleanedHooks };
|
|
26
|
+
}
|
|
27
|
+
function mergeHooks(existing, newHooks) {
|
|
28
|
+
const cleaned = removeTabclawHooks(existing);
|
|
29
|
+
const mergedHooks = {};
|
|
30
|
+
const allEvents = /* @__PURE__ */ new Set([
|
|
31
|
+
...Object.keys(cleaned.hooks || {}),
|
|
32
|
+
...Object.keys(newHooks)
|
|
33
|
+
]);
|
|
34
|
+
for (const eventName of allEvents) {
|
|
35
|
+
const existingEntries = cleaned.hooks?.[eventName] || [];
|
|
36
|
+
const newEntries = newHooks[eventName] || [];
|
|
37
|
+
const existingUrls = new Set(
|
|
38
|
+
existingEntries.flatMap((e) => e.hooks.map((h) => h.url))
|
|
39
|
+
);
|
|
40
|
+
const uniqueNewEntries = newEntries.filter(
|
|
41
|
+
(entry) => !entry.hooks.some((h) => existingUrls.has(h.url))
|
|
42
|
+
);
|
|
43
|
+
const merged = [...uniqueNewEntries, ...existingEntries];
|
|
44
|
+
if (merged.length > 0) {
|
|
45
|
+
mergedHooks[eventName] = merged;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { ...cleaned, hooks: mergedHooks };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/shared/credentials.ts
|
|
52
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
53
|
+
import { join } from "path";
|
|
54
|
+
import { homedir } from "os";
|
|
55
|
+
import { randomBytes } from "crypto";
|
|
56
|
+
var CREDENTIALS_VERSION = "1";
|
|
57
|
+
var DEFAULT_TOKEN_LENGTH = 32;
|
|
58
|
+
function getCredentialsPath() {
|
|
59
|
+
return join(homedir(), ".tabclaw", "credentials.json");
|
|
60
|
+
}
|
|
61
|
+
function generateToken() {
|
|
62
|
+
return randomBytes(DEFAULT_TOKEN_LENGTH).toString("hex");
|
|
63
|
+
}
|
|
64
|
+
function loadCredentials() {
|
|
65
|
+
const path = getCredentialsPath();
|
|
66
|
+
if (!existsSync(path)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const content = readFileSync(path, "utf-8");
|
|
71
|
+
return JSON.parse(content);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`Failed to load credentials from ${path}:`, error);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function saveCredentials(credentials) {
|
|
78
|
+
const path = getCredentialsPath();
|
|
79
|
+
writeFileSync(path, JSON.stringify(credentials, null, 2), { mode: 384 });
|
|
80
|
+
}
|
|
81
|
+
function getOrCreateHookToken() {
|
|
82
|
+
const existing = loadCredentials();
|
|
83
|
+
if (existing?.hookToken) {
|
|
84
|
+
return existing.hookToken;
|
|
85
|
+
}
|
|
86
|
+
const newToken = generateToken();
|
|
87
|
+
const credentials = {
|
|
88
|
+
version: CREDENTIALS_VERSION,
|
|
89
|
+
hookToken: newToken,
|
|
90
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
91
|
+
};
|
|
92
|
+
saveCredentials(credentials);
|
|
93
|
+
return newToken;
|
|
94
|
+
}
|
|
95
|
+
function getHookToken() {
|
|
96
|
+
return loadCredentials()?.hookToken ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/cli/attach.ts
|
|
100
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
101
|
+
import { homedir as homedir3 } from "os";
|
|
102
|
+
import { join as join3, dirname } from "path";
|
|
103
|
+
|
|
104
|
+
// src/config.ts
|
|
105
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
106
|
+
import { join as join2 } from "path";
|
|
107
|
+
import { homedir as homedir2 } from "os";
|
|
108
|
+
import TOML from "@iarna/toml";
|
|
109
|
+
import dotenv from "dotenv";
|
|
110
|
+
dotenv.config();
|
|
111
|
+
var DEFAULT_CONFIG = {
|
|
112
|
+
general: {
|
|
113
|
+
default_agent: "claude",
|
|
114
|
+
log_level: "info",
|
|
115
|
+
hook_server_port: 9876
|
|
116
|
+
},
|
|
117
|
+
channel: {
|
|
118
|
+
logger: {
|
|
119
|
+
enabled: true
|
|
120
|
+
},
|
|
121
|
+
telegram: {
|
|
122
|
+
enabled: true,
|
|
123
|
+
bot_token: "",
|
|
124
|
+
allowed_users: []
|
|
125
|
+
},
|
|
126
|
+
feishu: {
|
|
127
|
+
enabled: false,
|
|
128
|
+
app_id: "",
|
|
129
|
+
app_secret: ""
|
|
130
|
+
},
|
|
131
|
+
permission: {
|
|
132
|
+
timeout_ms: 3e5,
|
|
133
|
+
// 5 minutes
|
|
134
|
+
default_on_timeout: "deny"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
agent: {
|
|
138
|
+
claude: {
|
|
139
|
+
adapter: "hooks",
|
|
140
|
+
binary: "claude",
|
|
141
|
+
work_dir: "",
|
|
142
|
+
model: "",
|
|
143
|
+
permission_mode: "",
|
|
144
|
+
permission: {
|
|
145
|
+
lowRiskTools: ["Read", "Glob", "Grep", "List", "WebSearch", "codesearch"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
function getConfigPath() {
|
|
151
|
+
return join2(homedir2(), ".tabclaw", "config.toml");
|
|
152
|
+
}
|
|
153
|
+
function mergeWithDefaults(raw) {
|
|
154
|
+
return {
|
|
155
|
+
general: {
|
|
156
|
+
...DEFAULT_CONFIG.general,
|
|
157
|
+
...raw.general || {},
|
|
158
|
+
hook_server_port: parseInt(
|
|
159
|
+
process.env.TABCLAW_HOOK_SERVER_PORT || String(raw.general?.hook_server_port || DEFAULT_CONFIG.general.hook_server_port),
|
|
160
|
+
10
|
|
161
|
+
)
|
|
162
|
+
},
|
|
163
|
+
channel: {
|
|
164
|
+
logger: {
|
|
165
|
+
...DEFAULT_CONFIG.channel.logger,
|
|
166
|
+
...raw.channel?.logger || {},
|
|
167
|
+
notify_types: raw.channel?.logger?.notify_types
|
|
168
|
+
},
|
|
169
|
+
telegram: {
|
|
170
|
+
...DEFAULT_CONFIG.channel.telegram,
|
|
171
|
+
...raw.channel?.telegram || {},
|
|
172
|
+
allowed_users: (raw.channel?.telegram?.allowed_users || []).map(String),
|
|
173
|
+
bot_token: process.env.TABCLAW_TELEGRAM_BOT_TOKEN || raw.channel?.telegram?.bot_token || "",
|
|
174
|
+
notify_types: raw.channel?.telegram?.notify_types
|
|
175
|
+
},
|
|
176
|
+
feishu: {
|
|
177
|
+
...DEFAULT_CONFIG.channel.feishu,
|
|
178
|
+
...raw.channel?.feishu || {},
|
|
179
|
+
app_id: process.env.TABCLAW_FEISHU_APP_ID || raw.channel?.feishu?.app_id || "",
|
|
180
|
+
app_secret: process.env.TABCLAW_FEISHU_APP_SECRET || raw.channel?.feishu?.app_secret || ""
|
|
181
|
+
},
|
|
182
|
+
permission: {
|
|
183
|
+
...DEFAULT_CONFIG.channel.permission,
|
|
184
|
+
...raw.channel?.permission || {},
|
|
185
|
+
// Support env var override
|
|
186
|
+
timeout_ms: parseInt(
|
|
187
|
+
process.env.TABCLAW_PERMISSION_TIMEOUT_MS || String(
|
|
188
|
+
raw.channel?.permission?.timeout_ms || DEFAULT_CONFIG.channel.permission.timeout_ms
|
|
189
|
+
),
|
|
190
|
+
10
|
|
191
|
+
),
|
|
192
|
+
default_on_timeout: process.env.TABCLAW_PERMISSION_DEFAULT || raw.channel?.permission?.default_on_timeout || DEFAULT_CONFIG.channel.permission.default_on_timeout
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
agent: {
|
|
196
|
+
claude: {
|
|
197
|
+
...DEFAULT_CONFIG.agent.claude,
|
|
198
|
+
...raw.agent?.claude || {},
|
|
199
|
+
permission: {
|
|
200
|
+
...DEFAULT_CONFIG.agent.claude.permission,
|
|
201
|
+
...raw.agent?.claude?.permission || {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function loadConfig() {
|
|
208
|
+
const configPath = getConfigPath();
|
|
209
|
+
if (!existsSync2(configPath)) {
|
|
210
|
+
return DEFAULT_CONFIG;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
214
|
+
const raw = TOML.parse(content);
|
|
215
|
+
return mergeWithDefaults(raw);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(`Failed to load config from ${configPath}:`, error);
|
|
218
|
+
return DEFAULT_CONFIG;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/i18n/index.ts
|
|
223
|
+
import i18next from "i18next";
|
|
224
|
+
|
|
225
|
+
// src/i18n/locales/en.ts
|
|
226
|
+
var en_default = {
|
|
227
|
+
wizard: {
|
|
228
|
+
section: {
|
|
229
|
+
cli: "[ CLI Configuration ]",
|
|
230
|
+
channel: "[ Channel Configuration ]",
|
|
231
|
+
complete: "[ Setup Complete ]"
|
|
232
|
+
},
|
|
233
|
+
cli: {
|
|
234
|
+
runAgain: "Run init again when Claude Code is installed.",
|
|
235
|
+
detected: "Claude Code detected.",
|
|
236
|
+
hooksInstalled: "Hooks already installed.",
|
|
237
|
+
hooksNotInstalled: "Hooks not installed.",
|
|
238
|
+
actionQuestion: "Select action:",
|
|
239
|
+
injectAction: "Inject hooks configuration",
|
|
240
|
+
updateAction: "Update hooks configuration",
|
|
241
|
+
backAction: "Back",
|
|
242
|
+
hookCheckbox: "Select hooks:",
|
|
243
|
+
noHooksSelected: "No hooks selected, skipping injection.",
|
|
244
|
+
injecting: "Injecting hooks...",
|
|
245
|
+
injected: "Hooks injected",
|
|
246
|
+
failed: "Failed: {{error}}"
|
|
247
|
+
},
|
|
248
|
+
channel: {
|
|
249
|
+
configuring: "Configuring {{channel}}",
|
|
250
|
+
telegram: {
|
|
251
|
+
botToken: "Telegram bot token:",
|
|
252
|
+
allowedUsers: "Allowed Telegram user IDs (comma-separated):",
|
|
253
|
+
testing: "Testing Telegram connection...",
|
|
254
|
+
connected: "Connected to Telegram",
|
|
255
|
+
sendingTest: "Sending test message...",
|
|
256
|
+
testSent: "Test message sent",
|
|
257
|
+
testContent: "TabClaw test message",
|
|
258
|
+
testWarning: "Could not send test message (user may not have messaged bot yet)",
|
|
259
|
+
apiError: "API error: {{status}}",
|
|
260
|
+
connFailed: "Connection failed: {{error}}"
|
|
261
|
+
},
|
|
262
|
+
feishu: {
|
|
263
|
+
appId: "Feishu App ID:",
|
|
264
|
+
appSecret: "Feishu App Secret:",
|
|
265
|
+
required: "app_id and app_secret are required. Skipping."
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
notify: {
|
|
269
|
+
title: "Notification types for {{channel}}",
|
|
270
|
+
checkbox: "Notification types:",
|
|
271
|
+
saved: "Notification types saved for {{channel}}"
|
|
272
|
+
},
|
|
273
|
+
menu: {
|
|
274
|
+
select: "Select:",
|
|
275
|
+
next: "--- Next ---",
|
|
276
|
+
testFailed: "Test failed. Action:",
|
|
277
|
+
retry: "Retry",
|
|
278
|
+
reenter: "Re-enter",
|
|
279
|
+
backToMenu: "Back to menu",
|
|
280
|
+
restartQuestion: "Restart Gateway now?"
|
|
281
|
+
},
|
|
282
|
+
final: {
|
|
283
|
+
noItems: "No items configured.",
|
|
284
|
+
itemOk: "{{item}}: OK",
|
|
285
|
+
running: "Gateway is running",
|
|
286
|
+
runningChanges: "Gateway is running with pending changes",
|
|
287
|
+
configModified: "Configuration was modified. Restart required to apply changes.",
|
|
288
|
+
runningReady: "Gateway is already running and ready.",
|
|
289
|
+
notRunning: "Gateway is not running",
|
|
290
|
+
configModifiedStart: "Configuration was modified. Starting Gateway with new configuration.",
|
|
291
|
+
restarting: "Restarting...",
|
|
292
|
+
restarted: "Gateway restarted with new configuration",
|
|
293
|
+
starting: "Starting...",
|
|
294
|
+
started: "Gateway started",
|
|
295
|
+
startFailed: "Failed: {{error}}",
|
|
296
|
+
restartTip: "Run `tabclaw gateway start` to restart.",
|
|
297
|
+
startTip: "Run `tabclaw gateway start` to start.",
|
|
298
|
+
done: "Done."
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
gateway: {
|
|
302
|
+
stopping: "Gateway is running, stopping first...",
|
|
303
|
+
startedOnPort: "Gateway started on port {{port}}",
|
|
304
|
+
stopped: "Gateway stopped",
|
|
305
|
+
restartedOnPort: "Gateway restarted on port {{port}}",
|
|
306
|
+
startFailed: "Failed to start Gateway",
|
|
307
|
+
stopFailed: "Failed to stop Gateway",
|
|
308
|
+
restartFailed: "Failed to restart Gateway",
|
|
309
|
+
error: " Error: {{error}}",
|
|
310
|
+
checkLogs: "Check logs at: {{path}}",
|
|
311
|
+
status: {
|
|
312
|
+
title: "Gateway Status:",
|
|
313
|
+
running: "Running: {{value}}",
|
|
314
|
+
sessions: "Sessions: {{count}}",
|
|
315
|
+
clients: "Connected clients: {{count}}",
|
|
316
|
+
activeSessions: "Active sessions:",
|
|
317
|
+
notResponding: "Gateway is not responding",
|
|
318
|
+
notRunning: "Gateway is not running"
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
daemon: {
|
|
322
|
+
alreadyRunning: "Daemon is already running",
|
|
323
|
+
startedOnPort: "Daemon started on port {{port}}",
|
|
324
|
+
startFailed: "Daemon failed to start. Check logs for details.",
|
|
325
|
+
checkLogs: "Check logs at: {{path}}",
|
|
326
|
+
portInUse: "Warning: Port {{port}} still in use after {{ms}}ms",
|
|
327
|
+
startError: "Failed to start daemon: {{error}}",
|
|
328
|
+
lackingToken: "Daemon is running but PID file lacks token, restarting...",
|
|
329
|
+
scriptNotFound: "Daemon script not found: {{path}}"
|
|
330
|
+
},
|
|
331
|
+
status: {
|
|
332
|
+
title: "=== Tabclaw Status ===",
|
|
333
|
+
daemon: {
|
|
334
|
+
running: "Daemon: \u2713 Running",
|
|
335
|
+
notRunning: "Daemon: \u2717 Not running"
|
|
336
|
+
},
|
|
337
|
+
port: "Port: {{port}}",
|
|
338
|
+
logLevel: "Log Level: {{level}}",
|
|
339
|
+
channels: "Channels:",
|
|
340
|
+
telegram: {
|
|
341
|
+
enabled: "Telegram: \u2713 Enabled",
|
|
342
|
+
disabled: "Telegram: \u2717 Disabled"
|
|
343
|
+
},
|
|
344
|
+
feishu: {
|
|
345
|
+
enabled: "Feishu: \u2713 Enabled",
|
|
346
|
+
disabled: "Feishu: \u2717 Disabled"
|
|
347
|
+
},
|
|
348
|
+
settingsBackup: "Settings: \u2713 Backup exists",
|
|
349
|
+
settingsNoBackup: "Settings: \u2717 No backup",
|
|
350
|
+
paths: "Paths:",
|
|
351
|
+
pathConfig: "Config: {{path}}",
|
|
352
|
+
pathLogs: "Logs: {{path}}",
|
|
353
|
+
pathBackup: "Backup: {{path}}"
|
|
354
|
+
},
|
|
355
|
+
cli: {
|
|
356
|
+
init: {
|
|
357
|
+
configExists: "Configuration exists. Continue?",
|
|
358
|
+
cancelled: "Cancelled."
|
|
359
|
+
},
|
|
360
|
+
stop: {
|
|
361
|
+
notRunning: "Tabclaw daemon is not running",
|
|
362
|
+
stopping: "Stopping tabclaw daemon...",
|
|
363
|
+
stopped: "Daemon stopped successfully",
|
|
364
|
+
failed: "Failed to stop daemon cleanly"
|
|
365
|
+
},
|
|
366
|
+
attach: {
|
|
367
|
+
checking: "Checking Gateway status...",
|
|
368
|
+
running: "Gateway is running",
|
|
369
|
+
notRunning: "Gateway is not running. Start it with: tabclaw gateway start",
|
|
370
|
+
tokenNotFound: 'Hook token not found. Please run "tabclaw init" first.',
|
|
371
|
+
attached: "Successfully attached {{agent}} to Gateway",
|
|
372
|
+
start: 'Start "claude" to begin a session.',
|
|
373
|
+
usingToken: "Using hook token: {{token}}",
|
|
374
|
+
settingsBacked: "Claude settings backed up to: {{path}}",
|
|
375
|
+
configuringHooks: "Configuring hooks in Claude settings...",
|
|
376
|
+
connected: "Your Claude Code is now connected to tabclaw Gateway.",
|
|
377
|
+
forwarding: "Events will be forwarded to connected clients.",
|
|
378
|
+
tipStatus: 'Use "tabclaw gateway status" to check status',
|
|
379
|
+
tipDetach: 'Use "tabclaw detach" to remove hooks and restore settings'
|
|
380
|
+
},
|
|
381
|
+
agent: {
|
|
382
|
+
alreadyRunning: "Tabclaw daemon is already running",
|
|
383
|
+
starting: "Starting tabclaw daemon...",
|
|
384
|
+
started: "Daemon started",
|
|
385
|
+
startFailed: "Failed to start daemon",
|
|
386
|
+
tokenNotFound: "Failed to get token from daemon",
|
|
387
|
+
hooksConfigured: "Claude hooks configured",
|
|
388
|
+
startingIn: "Starting Claude Code in {{dir}}...",
|
|
389
|
+
pressCtrlC: "Press Ctrl+C to stop (tabclaw daemon will keep running)",
|
|
390
|
+
configuring: "Configuring Claude Code...",
|
|
391
|
+
settingsBacked: "Claude settings backed up",
|
|
392
|
+
exited: "\nClaude Code exited with code {{code}}",
|
|
393
|
+
daemonRunning: "Tabclaw daemon is still running in the background",
|
|
394
|
+
tipStop: 'Use "tabclaw stop" to stop the daemon and restore settings',
|
|
395
|
+
detaching: "\nDetaching from Claude..."
|
|
396
|
+
},
|
|
397
|
+
debug: {
|
|
398
|
+
starting: "Starting hook debug server on port {{port}}",
|
|
399
|
+
capturing: "This will capture all incoming webhook requests...",
|
|
400
|
+
pressCtrlC: "Press Ctrl+C to stop.",
|
|
401
|
+
stopping: "\nStopping debug server..."
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
notifications: {
|
|
405
|
+
permission: {
|
|
406
|
+
title: "\u{1F510} Permission Request",
|
|
407
|
+
session: "Session: {{sessionId}}",
|
|
408
|
+
tool: "Tool: {{toolName}}",
|
|
409
|
+
args: "Args: {{args}}",
|
|
410
|
+
requestId: "Request ID: {{requestId}}",
|
|
411
|
+
allow: "\u2705 Allow",
|
|
412
|
+
deny: "\u274C Deny"
|
|
413
|
+
},
|
|
414
|
+
finished: {
|
|
415
|
+
titleTelegram: "\u2705 Task Completed",
|
|
416
|
+
titleLogger: "\u{1F3C1} Task Completed",
|
|
417
|
+
session: "Session: {{sessionId}}",
|
|
418
|
+
message: "Message: {{message}}",
|
|
419
|
+
eventType: "Type: {{eventType}}"
|
|
420
|
+
},
|
|
421
|
+
sessionStart: {
|
|
422
|
+
title: "\u{1F7E2} Session Started",
|
|
423
|
+
session: "Session: {{sessionId}}"
|
|
424
|
+
},
|
|
425
|
+
error: {
|
|
426
|
+
title: "\u274C Error",
|
|
427
|
+
session: "Session: {{sessionId}}",
|
|
428
|
+
tool: "Tool: {{toolName}}",
|
|
429
|
+
message: "Message: {{message}}"
|
|
430
|
+
},
|
|
431
|
+
unauthorized: "You are not authorized to use this bot.",
|
|
432
|
+
received: "Message received.",
|
|
433
|
+
processingError: "Error processing message."
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/i18n/index.ts
|
|
438
|
+
var lng = process.env.TABCLAW_LANG ?? "en";
|
|
439
|
+
i18next.init({
|
|
440
|
+
initImmediate: false,
|
|
441
|
+
lng,
|
|
442
|
+
fallbackLng: "en",
|
|
443
|
+
resources: { en: { translation: en_default } },
|
|
444
|
+
interpolation: { escapeValue: false }
|
|
445
|
+
});
|
|
446
|
+
var t = i18next.t.bind(i18next);
|
|
447
|
+
|
|
448
|
+
// src/cli/attach.ts
|
|
449
|
+
function getSettingsPath() {
|
|
450
|
+
return join3(homedir3(), ".claude", "settings.json");
|
|
451
|
+
}
|
|
452
|
+
function backupSettings(settingsPath) {
|
|
453
|
+
const backupPath = settingsPath + ".tabclaw-backup";
|
|
454
|
+
if (existsSync3(settingsPath)) {
|
|
455
|
+
writeFileSync2(backupPath, readFileSync3(settingsPath, "utf-8"));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function writeSettings(settingsPath, settings) {
|
|
459
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
460
|
+
writeFileSync2(settingsPath, JSON.stringify(settings, null, 2));
|
|
461
|
+
}
|
|
462
|
+
function getGatewayBaseUrl(port) {
|
|
463
|
+
return `http://localhost:${port}`;
|
|
464
|
+
}
|
|
465
|
+
async function checkGatewayHealth(port) {
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch(`${getGatewayBaseUrl(port)}/health`, {
|
|
468
|
+
method: "GET",
|
|
469
|
+
signal: AbortSignal.timeout(5e3)
|
|
470
|
+
});
|
|
471
|
+
return response.ok;
|
|
472
|
+
} catch {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function generateHooksConfig(token, port) {
|
|
477
|
+
const baseUrl = `${getGatewayBaseUrl(port)}/hook/${token}`;
|
|
478
|
+
const createHookEntry = (event) => ({
|
|
479
|
+
matcher: "",
|
|
480
|
+
hooks: [{ type: "http", url: `${baseUrl}/${event}` }]
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
Stop: [createHookEntry("Stop")],
|
|
484
|
+
SessionStart: [createHookEntry("SessionStart")],
|
|
485
|
+
SessionEnd: [createHookEntry("SessionEnd")],
|
|
486
|
+
UserPromptSubmit: [createHookEntry("UserPromptSubmit")],
|
|
487
|
+
PermissionRequest: [createHookEntry("PermissionRequest")]
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
var attachCommand = new Command("attach").description("Attach an agent to the Gateway").argument("<agent-type>", "Agent type (e.g., claude)").action(async (agentType) => {
|
|
491
|
+
const config = loadConfig();
|
|
492
|
+
const gatewayPort = config.general.hook_server_port;
|
|
493
|
+
console.log(t("cli.attach.checking"));
|
|
494
|
+
const isHealthy = await checkGatewayHealth(gatewayPort);
|
|
495
|
+
if (!isHealthy) {
|
|
496
|
+
console.error(t("cli.attach.notRunning"));
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
console.log(t("cli.attach.running"));
|
|
500
|
+
const hookToken = getHookToken();
|
|
501
|
+
if (!hookToken) {
|
|
502
|
+
console.error(t("cli.attach.tokenNotFound"));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
const token = hookToken.substring(0, 8) + "...";
|
|
506
|
+
console.log(t("cli.attach.usingToken", { token }));
|
|
507
|
+
const settingsPath = getSettingsPath();
|
|
508
|
+
const backupPath = settingsPath + ".tabclaw-backup";
|
|
509
|
+
if (!existsSync3(backupPath)) {
|
|
510
|
+
backupSettings(settingsPath);
|
|
511
|
+
console.log(t("cli.attach.settingsBacked", { path: backupPath }));
|
|
512
|
+
}
|
|
513
|
+
console.log(t("cli.attach.configuringHooks"));
|
|
514
|
+
let currentSettings = {};
|
|
515
|
+
if (existsSync3(settingsPath)) {
|
|
516
|
+
currentSettings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
517
|
+
}
|
|
518
|
+
const hooksConfig = generateHooksConfig(hookToken, gatewayPort);
|
|
519
|
+
const mergedSettings = mergeHooks(currentSettings, hooksConfig);
|
|
520
|
+
writeSettings(settingsPath, mergedSettings);
|
|
521
|
+
console.log("");
|
|
522
|
+
console.log(t("cli.attach.attached", { agent: agentType }));
|
|
523
|
+
console.log("");
|
|
524
|
+
console.log(t("cli.attach.connected"));
|
|
525
|
+
console.log(t("cli.attach.start"));
|
|
526
|
+
console.log("");
|
|
527
|
+
console.log(t("cli.attach.forwarding"));
|
|
528
|
+
console.log(t("cli.attach.tipStatus"));
|
|
529
|
+
console.log(t("cli.attach.tipDetach"));
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// src/cli/init.ts
|
|
533
|
+
import { Command as Command2 } from "commander";
|
|
534
|
+
import { existsSync as existsSync9 } from "fs";
|
|
535
|
+
import { join as join9 } from "path";
|
|
536
|
+
import { homedir as homedir8 } from "os";
|
|
537
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
538
|
+
|
|
539
|
+
// src/cli/wizard/engine.ts
|
|
540
|
+
import { existsSync as existsSync8 } from "fs";
|
|
541
|
+
import { join as join8 } from "path";
|
|
542
|
+
import { homedir as homedir7 } from "os";
|
|
543
|
+
|
|
544
|
+
// src/cli/wizard/state.ts
|
|
545
|
+
function createInitialState(hasExistingConfig) {
|
|
546
|
+
return {
|
|
547
|
+
current: "cli-menu",
|
|
548
|
+
hasExistingConfig,
|
|
549
|
+
pendingChanges: {},
|
|
550
|
+
itemStatus: /* @__PURE__ */ new Map(),
|
|
551
|
+
sessionConfigured: /* @__PURE__ */ new Set(),
|
|
552
|
+
validationResults: /* @__PURE__ */ new Map()
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/cli/wizard/applicator.ts
|
|
557
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
558
|
+
import { join as join4 } from "path";
|
|
559
|
+
import { homedir as homedir4 } from "os";
|
|
560
|
+
import TOML2 from "@iarna/toml";
|
|
561
|
+
var ConfigApplicator = class {
|
|
562
|
+
apply(pending) {
|
|
563
|
+
const current = loadConfig();
|
|
564
|
+
const merged = {
|
|
565
|
+
...current,
|
|
566
|
+
general: { ...current.general },
|
|
567
|
+
channel: {
|
|
568
|
+
...current.channel,
|
|
569
|
+
...pending.channel || {}
|
|
570
|
+
},
|
|
571
|
+
agent: {
|
|
572
|
+
...current.agent,
|
|
573
|
+
...pending.agent ? { claude: { ...current.agent?.claude ?? {}, ...pending.agent } } : {}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const configDir = join4(homedir4(), ".tabclaw");
|
|
577
|
+
mkdirSync2(configDir, { recursive: true });
|
|
578
|
+
const configPath = join4(configDir, "config.toml");
|
|
579
|
+
const content = TOML2.stringify(merged);
|
|
580
|
+
writeFileSync3(configPath, content);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/cli/wizard/detectors/claude.ts
|
|
585
|
+
import { execSync } from "child_process";
|
|
586
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
587
|
+
import { join as join5 } from "path";
|
|
588
|
+
import { homedir as homedir5 } from "os";
|
|
589
|
+
function detectClaude() {
|
|
590
|
+
const settingsPath = join5(homedir5(), ".claude", "settings.json");
|
|
591
|
+
let binaryPath;
|
|
592
|
+
let supported = false;
|
|
593
|
+
try {
|
|
594
|
+
binaryPath = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
595
|
+
supported = !!binaryPath;
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
if (!supported) {
|
|
599
|
+
return {
|
|
600
|
+
supported: false,
|
|
601
|
+
hooked: false,
|
|
602
|
+
message: "Claude Code not found in PATH. Please install Claude Code first.",
|
|
603
|
+
settingsPath
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
let hooked = false;
|
|
607
|
+
if (existsSync4(settingsPath)) {
|
|
608
|
+
try {
|
|
609
|
+
const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
610
|
+
hooked = isTabclawHooked(settings);
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
supported: true,
|
|
616
|
+
hooked,
|
|
617
|
+
message: hooked ? "Claude Code hooks already configured." : "Claude Code detected.",
|
|
618
|
+
binaryPath,
|
|
619
|
+
settingsPath
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function isTabclawHooked(settings) {
|
|
623
|
+
const hooks = settings.hooks;
|
|
624
|
+
if (!hooks) return false;
|
|
625
|
+
for (const key of Object.keys(hooks)) {
|
|
626
|
+
const entries = hooks[key];
|
|
627
|
+
for (const entry of entries) {
|
|
628
|
+
if (!entry || typeof entry !== "object") continue;
|
|
629
|
+
const e = entry;
|
|
630
|
+
const inner = e.hooks;
|
|
631
|
+
if (Array.isArray(inner) && inner.some((h) => isHttpHookWithTabclawUrl(h))) {
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
function isHttpHookWithTabclawUrl(entry) {
|
|
639
|
+
if (!entry || typeof entry !== "object") return false;
|
|
640
|
+
const e = entry;
|
|
641
|
+
if (e.type !== "http") return false;
|
|
642
|
+
const url = e.url;
|
|
643
|
+
return typeof url === "string" && url.includes("localhost") && url.includes("/hook/");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/cli/wizard/prompts.ts
|
|
647
|
+
import { confirm, input, checkbox, select } from "@inquirer/prompts";
|
|
648
|
+
var prompts = {
|
|
649
|
+
feishuAppId: (current) => input({
|
|
650
|
+
message: t("wizard.channel.feishu.appId"),
|
|
651
|
+
default: current || ""
|
|
652
|
+
}),
|
|
653
|
+
feishuAppSecret: (current) => input({
|
|
654
|
+
message: t("wizard.channel.feishu.appSecret"),
|
|
655
|
+
default: current || ""
|
|
656
|
+
}),
|
|
657
|
+
cliActionSelect: (hooked) => select({
|
|
658
|
+
message: t("wizard.cli.actionQuestion"),
|
|
659
|
+
choices: hooked ? [
|
|
660
|
+
{ name: t("wizard.cli.updateAction"), value: "update" },
|
|
661
|
+
{ name: t("wizard.cli.backAction"), value: "back" }
|
|
662
|
+
] : [
|
|
663
|
+
{ name: t("wizard.cli.injectAction"), value: "inject" },
|
|
664
|
+
{ name: t("wizard.cli.backAction"), value: "back" }
|
|
665
|
+
]
|
|
666
|
+
}),
|
|
667
|
+
cliHookSelect: (selected) => checkbox({
|
|
668
|
+
message: t("wizard.cli.hookCheckbox"),
|
|
669
|
+
choices: [
|
|
670
|
+
{ name: "Stop", value: "Stop", checked: selected?.includes("Stop") ?? true },
|
|
671
|
+
{ name: "SessionStart", value: "SessionStart", checked: selected?.includes("SessionStart") ?? true },
|
|
672
|
+
{ name: "SessionEnd", value: "SessionEnd", checked: selected?.includes("SessionEnd") ?? true },
|
|
673
|
+
{ name: "PermissionRequest", value: "PermissionRequest", checked: selected?.includes("PermissionRequest") ?? true },
|
|
674
|
+
{ name: "UserPromptSubmit", value: "UserPromptSubmit", checked: selected?.includes("UserPromptSubmit") ?? false },
|
|
675
|
+
{ name: "PostToolUse", value: "PostToolUse", checked: selected?.includes("PostToolUse") ?? false },
|
|
676
|
+
{ name: "PostToolUseFailure", value: "PostToolUseFailure", checked: selected?.includes("PostToolUseFailure") ?? false },
|
|
677
|
+
{ name: "Notification", value: "Notification", checked: selected?.includes("Notification") ?? false },
|
|
678
|
+
{ name: "SubagentStart", value: "SubagentStart", checked: selected?.includes("SubagentStart") ?? false },
|
|
679
|
+
{ name: "SubagentStop", value: "SubagentStop", checked: selected?.includes("SubagentStop") ?? false }
|
|
680
|
+
]
|
|
681
|
+
}),
|
|
682
|
+
botToken: (current) => input({
|
|
683
|
+
message: t("wizard.channel.telegram.botToken"),
|
|
684
|
+
default: current || ""
|
|
685
|
+
}),
|
|
686
|
+
allowedUsers: (current) => input({
|
|
687
|
+
message: t("wizard.channel.telegram.allowedUsers"),
|
|
688
|
+
default: current || ""
|
|
689
|
+
}),
|
|
690
|
+
channelMenuSelect: (channels) => select({
|
|
691
|
+
message: t("wizard.menu.select"),
|
|
692
|
+
choices: [
|
|
693
|
+
...channels.map((c) => ({
|
|
694
|
+
name: `${c.name} ${c.status}`,
|
|
695
|
+
value: c.name
|
|
696
|
+
})),
|
|
697
|
+
{ name: t("wizard.menu.next"), value: "__next__" }
|
|
698
|
+
]
|
|
699
|
+
}),
|
|
700
|
+
notifyTypes: (selected) => checkbox({
|
|
701
|
+
message: t("wizard.notify.checkbox"),
|
|
702
|
+
choices: [
|
|
703
|
+
{ name: "permission_request", value: "permission_request", checked: selected?.includes("permission_request") },
|
|
704
|
+
{ name: "finished", value: "finished", checked: selected?.includes("finished") },
|
|
705
|
+
{ name: "session_start", value: "session_start", checked: selected?.includes("session_start") },
|
|
706
|
+
{ name: "error", value: "error", checked: selected?.includes("error") }
|
|
707
|
+
]
|
|
708
|
+
}),
|
|
709
|
+
channelConfigError: () => select({
|
|
710
|
+
message: t("wizard.menu.testFailed"),
|
|
711
|
+
choices: [
|
|
712
|
+
{ name: t("wizard.menu.retry"), value: "retry" },
|
|
713
|
+
{ name: t("wizard.menu.reenter"), value: "reinput" },
|
|
714
|
+
{ name: t("wizard.menu.backToMenu"), value: "skip" }
|
|
715
|
+
]
|
|
716
|
+
}),
|
|
717
|
+
restartGateway: () => confirm({
|
|
718
|
+
message: t("wizard.menu.restartQuestion"),
|
|
719
|
+
default: true
|
|
720
|
+
})
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// src/cli/wizard/steps/cli.ts
|
|
724
|
+
import { writeFileSync as writeFileSync4, existsSync as existsSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
725
|
+
import { dirname as dirname2 } from "path";
|
|
726
|
+
import pc from "picocolors";
|
|
727
|
+
import ora from "ora";
|
|
728
|
+
async function stepCli(state) {
|
|
729
|
+
const cliName = state.currentCliName || "claude";
|
|
730
|
+
console.log(pc.bold(`
|
|
731
|
+
${t("wizard.section.cli")}
|
|
732
|
+
`));
|
|
733
|
+
if (cliName === "claude") {
|
|
734
|
+
const detection = state.claudeDetection ?? detectClaude();
|
|
735
|
+
if (!detection.supported) {
|
|
736
|
+
console.log(pc.yellow(`! ${detection.message}`));
|
|
737
|
+
console.log(pc.gray(` ${t("wizard.cli.runAgain")}
|
|
738
|
+
`));
|
|
739
|
+
return { action: "next", next: "cli-menu" };
|
|
740
|
+
}
|
|
741
|
+
console.log(pc.green(`* ${t("wizard.cli.detected")}`));
|
|
742
|
+
console.log(pc.gray(` Settings: ${detection.settingsPath}`));
|
|
743
|
+
const port = loadConfig().general.hook_server_port;
|
|
744
|
+
const token = getHookToken();
|
|
745
|
+
console.log(pc.gray(` Gateway: http://localhost:${port}/hook/${token ? token.substring(0, 8) + "..." : "unknown"}`));
|
|
746
|
+
if (detection.hooked) {
|
|
747
|
+
console.log(pc.green(` ${t("wizard.cli.hooksInstalled")}`));
|
|
748
|
+
} else {
|
|
749
|
+
console.log(pc.yellow(` ${t("wizard.cli.hooksNotInstalled")}`));
|
|
750
|
+
}
|
|
751
|
+
console.log("");
|
|
752
|
+
const action = await prompts.cliActionSelect(detection.hooked);
|
|
753
|
+
if (action === "back") {
|
|
754
|
+
console.log("");
|
|
755
|
+
return { action: "next", next: "cli-menu" };
|
|
756
|
+
}
|
|
757
|
+
const selectedHooks = await prompts.cliHookSelect();
|
|
758
|
+
if (selectedHooks.length === 0) {
|
|
759
|
+
console.log(pc.yellow(`! ${t("wizard.cli.noHooksSelected")}
|
|
760
|
+
`));
|
|
761
|
+
return { action: "next", next: "cli-menu" };
|
|
762
|
+
}
|
|
763
|
+
const spinner = ora(t("wizard.cli.injecting")).start();
|
|
764
|
+
try {
|
|
765
|
+
injectClaudeHooks(detection.settingsPath, selectedHooks);
|
|
766
|
+
spinner.succeed(t("wizard.cli.injected"));
|
|
767
|
+
state.itemStatus.set("claude", "configured");
|
|
768
|
+
state.sessionConfigured.add("claude");
|
|
769
|
+
state.pendingChanges.agent = { adapter: "hooks" };
|
|
770
|
+
state.validationResults.set("claude", { ok: true });
|
|
771
|
+
} catch (err) {
|
|
772
|
+
spinner.fail(t("wizard.cli.failed", { error: String(err) }));
|
|
773
|
+
state.itemStatus.set("claude", "error");
|
|
774
|
+
state.validationResults.set("claude", { ok: false, message: String(err) });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return { action: "next", next: "cli-menu" };
|
|
778
|
+
}
|
|
779
|
+
function injectClaudeHooks(settingsPath, events) {
|
|
780
|
+
const port = loadConfig().general.hook_server_port;
|
|
781
|
+
const token = getOrCreateHookToken();
|
|
782
|
+
const baseUrl = `http://localhost:${port}/hook/${token}`;
|
|
783
|
+
const hooksConfig = generateHooksConfig2(baseUrl, events);
|
|
784
|
+
let currentSettings = {};
|
|
785
|
+
if (existsSync5(settingsPath)) {
|
|
786
|
+
try {
|
|
787
|
+
currentSettings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const mergedSettings = mergeHooks(
|
|
792
|
+
currentSettings,
|
|
793
|
+
hooksConfig
|
|
794
|
+
);
|
|
795
|
+
mkdirSync3(dirname2(settingsPath), { recursive: true });
|
|
796
|
+
writeFileSync4(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
797
|
+
}
|
|
798
|
+
function generateHooksConfig2(baseUrl, events) {
|
|
799
|
+
const createEntry = (event) => ({
|
|
800
|
+
matcher: "",
|
|
801
|
+
hooks: [{ type: "http", url: `${baseUrl}/${event}` }]
|
|
802
|
+
});
|
|
803
|
+
const config = {};
|
|
804
|
+
for (const event of events) {
|
|
805
|
+
config[event] = [createEntry(event)];
|
|
806
|
+
}
|
|
807
|
+
return config;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/cli/wizard/detectors/channel.ts
|
|
811
|
+
async function detectChannel(name) {
|
|
812
|
+
const config = loadConfig();
|
|
813
|
+
switch (name) {
|
|
814
|
+
case "logger":
|
|
815
|
+
return { configured: config.channel.logger?.enabled === true };
|
|
816
|
+
case "telegram": {
|
|
817
|
+
const token = config.channel.telegram?.bot_token;
|
|
818
|
+
if (!token || token === "YOUR_BOT_TOKEN") {
|
|
819
|
+
return { configured: false };
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
823
|
+
signal: AbortSignal.timeout(5e3)
|
|
824
|
+
});
|
|
825
|
+
if (!response.ok) {
|
|
826
|
+
return { configured: false, error: `Telegram API error: ${response.status}` };
|
|
827
|
+
}
|
|
828
|
+
return { configured: true };
|
|
829
|
+
} catch (err) {
|
|
830
|
+
return { configured: false, error: `Connection failed: ${err}` };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
case "feishu":
|
|
834
|
+
return { configured: !!(config.channel.feishu?.enabled && config.channel.feishu?.app_id) };
|
|
835
|
+
default:
|
|
836
|
+
return { configured: false };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function getChannelStatusLabel(status, error) {
|
|
840
|
+
switch (status) {
|
|
841
|
+
case "configured":
|
|
842
|
+
return "[done]";
|
|
843
|
+
case "error":
|
|
844
|
+
return `[error]${error ? ` (${error})` : ""}`;
|
|
845
|
+
default:
|
|
846
|
+
return "";
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/cli/wizard/steps/channel-menu.ts
|
|
851
|
+
import pc2 from "picocolors";
|
|
852
|
+
async function stepChannelMenu(state) {
|
|
853
|
+
console.log(pc2.bold(`
|
|
854
|
+
${t("wizard.section.channel")}
|
|
855
|
+
`));
|
|
856
|
+
const channelNames = ["logger", "telegram", "feishu"];
|
|
857
|
+
const channels = channelNames.map((name) => {
|
|
858
|
+
const status = state.itemStatus.get(name) || "unconfigured";
|
|
859
|
+
const validation = state.validationResults.get(name);
|
|
860
|
+
return {
|
|
861
|
+
name,
|
|
862
|
+
status: getChannelStatusLabel(status, validation?.message)
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
const choice = await prompts.channelMenuSelect(channels);
|
|
866
|
+
if (choice === "__next__") {
|
|
867
|
+
return { action: "next", next: "final" };
|
|
868
|
+
}
|
|
869
|
+
state.currentChannelName = choice;
|
|
870
|
+
return { action: "next", next: "channel-config" };
|
|
871
|
+
}
|
|
872
|
+
async function stepCliMenu(state) {
|
|
873
|
+
console.log(pc2.bold(`
|
|
874
|
+
${t("wizard.section.cli")}
|
|
875
|
+
`));
|
|
876
|
+
const cliStatus = state.itemStatus.get("claude") || "unconfigured";
|
|
877
|
+
const label = getChannelStatusLabel(cliStatus);
|
|
878
|
+
const choice = await prompts.channelMenuSelect([{
|
|
879
|
+
name: "claude",
|
|
880
|
+
status: label
|
|
881
|
+
}]);
|
|
882
|
+
if (choice === "__next__") {
|
|
883
|
+
return { action: "next", next: "channel-menu" };
|
|
884
|
+
}
|
|
885
|
+
state.currentCliName = choice;
|
|
886
|
+
return { action: "next", next: "cli" };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/cli/wizard/steps/channel-config.ts
|
|
890
|
+
import pc3 from "picocolors";
|
|
891
|
+
import ora2 from "ora";
|
|
892
|
+
async function stepChannelConfig(state) {
|
|
893
|
+
const channelName = state.currentChannelName || "telegram";
|
|
894
|
+
console.log(pc3.bold(`
|
|
895
|
+
${t("wizard.channel.configuring", { channel: channelName })}
|
|
896
|
+
`));
|
|
897
|
+
while (true) {
|
|
898
|
+
const cfg = loadConfig();
|
|
899
|
+
const channelMap = cfg.channel;
|
|
900
|
+
const currentConfig = channelMap[channelName] ?? {};
|
|
901
|
+
if (channelName === "telegram") {
|
|
902
|
+
const botToken = await prompts.botToken(currentConfig.bot_token);
|
|
903
|
+
const allowedUsers = await prompts.allowedUsers(String(currentConfig.allowed_users || ""));
|
|
904
|
+
const userIds = allowedUsers.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
905
|
+
const spinner = ora2(t("wizard.channel.telegram.testing")).start();
|
|
906
|
+
let connected = false;
|
|
907
|
+
try {
|
|
908
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
|
|
909
|
+
signal: AbortSignal.timeout(8e3)
|
|
910
|
+
});
|
|
911
|
+
if (!response.ok) {
|
|
912
|
+
spinner.fail(t("wizard.channel.telegram.apiError", { status: response.status }));
|
|
913
|
+
} else {
|
|
914
|
+
spinner.succeed(t("wizard.channel.telegram.connected"));
|
|
915
|
+
connected = true;
|
|
916
|
+
}
|
|
917
|
+
} catch (err) {
|
|
918
|
+
spinner.fail(t("wizard.channel.telegram.connFailed", { error: String(err) }));
|
|
919
|
+
}
|
|
920
|
+
if (!connected) {
|
|
921
|
+
state.itemStatus.set(channelName, "error");
|
|
922
|
+
const choice = await prompts.channelConfigError();
|
|
923
|
+
if (choice === "retry" || choice === "reinput") {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
state.itemStatus.set(channelName, "unconfigured");
|
|
927
|
+
return { action: "next", next: "channel-menu" };
|
|
928
|
+
}
|
|
929
|
+
const msgSpinner = ora2(t("wizard.channel.telegram.sendingTest")).start();
|
|
930
|
+
try {
|
|
931
|
+
await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
932
|
+
method: "POST",
|
|
933
|
+
headers: { "Content-Type": "application/json" },
|
|
934
|
+
body: JSON.stringify({ chat_id: userIds[0] || 0, text: t("wizard.channel.telegram.testContent") }),
|
|
935
|
+
signal: AbortSignal.timeout(8e3)
|
|
936
|
+
});
|
|
937
|
+
msgSpinner.succeed(t("wizard.channel.telegram.testSent"));
|
|
938
|
+
} catch {
|
|
939
|
+
msgSpinner.warn(t("wizard.channel.telegram.testWarning"));
|
|
940
|
+
}
|
|
941
|
+
state.pendingChanges.channel = {
|
|
942
|
+
...state.pendingChanges.channel || {},
|
|
943
|
+
telegram: { enabled: true, bot_token: botToken, allowed_users: userIds }
|
|
944
|
+
};
|
|
945
|
+
state.itemStatus.set("telegram", "configured");
|
|
946
|
+
state.sessionConfigured.add("telegram");
|
|
947
|
+
state.validationResults.set("telegram", { ok: true });
|
|
948
|
+
return { action: "next", next: "channel-notify" };
|
|
949
|
+
}
|
|
950
|
+
if (channelName === "logger") {
|
|
951
|
+
state.pendingChanges.channel = {
|
|
952
|
+
...state.pendingChanges.channel || {},
|
|
953
|
+
logger: { enabled: true }
|
|
954
|
+
};
|
|
955
|
+
state.itemStatus.set("logger", "configured");
|
|
956
|
+
state.sessionConfigured.add("logger");
|
|
957
|
+
return { action: "next", next: "channel-notify" };
|
|
958
|
+
}
|
|
959
|
+
if (channelName === "feishu") {
|
|
960
|
+
const appId = await prompts.feishuAppId();
|
|
961
|
+
const appSecret = await prompts.feishuAppSecret();
|
|
962
|
+
if (!appId || !appSecret) {
|
|
963
|
+
console.log(pc3.yellow(`! ${t("wizard.channel.feishu.required")}
|
|
964
|
+
`));
|
|
965
|
+
state.itemStatus.set("feishu", "unconfigured");
|
|
966
|
+
return { action: "next", next: "channel-menu" };
|
|
967
|
+
}
|
|
968
|
+
state.pendingChanges.channel = {
|
|
969
|
+
...state.pendingChanges.channel || {},
|
|
970
|
+
feishu: { enabled: true, app_id: appId, app_secret: appSecret }
|
|
971
|
+
};
|
|
972
|
+
state.itemStatus.set("feishu", "configured");
|
|
973
|
+
state.sessionConfigured.add("feishu");
|
|
974
|
+
return { action: "next", next: "channel-notify" };
|
|
975
|
+
}
|
|
976
|
+
return { action: "next", next: "channel-menu" };
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/cli/wizard/steps/channel-notify.ts
|
|
981
|
+
import pc4 from "picocolors";
|
|
982
|
+
async function stepChannelNotify(state) {
|
|
983
|
+
const channelName = state.currentChannelName || "telegram";
|
|
984
|
+
console.log(pc4.bold(`
|
|
985
|
+
${t("wizard.notify.title", { channel: channelName })}
|
|
986
|
+
`));
|
|
987
|
+
const cfg = loadConfig();
|
|
988
|
+
const channelConfig = cfg.channel[channelName] || {};
|
|
989
|
+
const currentTypes = channelConfig.notify_types || [];
|
|
990
|
+
const selected = await prompts.notifyTypes(currentTypes);
|
|
991
|
+
if (!state.pendingChanges.channel) state.pendingChanges.channel = {};
|
|
992
|
+
if (!state.pendingChanges.channel[channelName]) state.pendingChanges.channel[channelName] = {};
|
|
993
|
+
state.pendingChanges.channel[channelName] = {
|
|
994
|
+
...state.pendingChanges.channel[channelName],
|
|
995
|
+
notify_types: selected
|
|
996
|
+
};
|
|
997
|
+
console.log(pc4.green(`
|
|
998
|
+
\u2713 ${t("wizard.notify.saved", { channel: channelName })}
|
|
999
|
+
`));
|
|
1000
|
+
return { action: "next", next: "channel-menu" };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/cli/daemon.ts
|
|
1004
|
+
import { spawn } from "child_process";
|
|
1005
|
+
import { exec } from "child_process";
|
|
1006
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
1007
|
+
import { fileURLToPath } from "url";
|
|
1008
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
|
|
1009
|
+
|
|
1010
|
+
// src/shared/pidfile.ts
|
|
1011
|
+
import { writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync6, unlinkSync } from "fs";
|
|
1012
|
+
function readPidFile(pidFilePath) {
|
|
1013
|
+
if (!existsSync6(pidFilePath)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
const content = readFileSync6(pidFilePath, "utf-8");
|
|
1018
|
+
return JSON.parse(content);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function removePidFile(pidFilePath) {
|
|
1024
|
+
if (existsSync6(pidFilePath)) {
|
|
1025
|
+
unlinkSync(pidFilePath);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function isProcessRunning(pid) {
|
|
1029
|
+
try {
|
|
1030
|
+
process.kill(pid, 0);
|
|
1031
|
+
return true;
|
|
1032
|
+
} catch {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/cli/daemon.ts
|
|
1038
|
+
function isPortInUse(port) {
|
|
1039
|
+
return new Promise((resolve) => {
|
|
1040
|
+
exec(`lsof -i :${port}`, (err) => {
|
|
1041
|
+
resolve(!err);
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
var DaemonManager = class {
|
|
1046
|
+
pidFilePath;
|
|
1047
|
+
port;
|
|
1048
|
+
lastError;
|
|
1049
|
+
constructor(pidFilePath, port) {
|
|
1050
|
+
this.pidFilePath = pidFilePath;
|
|
1051
|
+
this.port = port;
|
|
1052
|
+
}
|
|
1053
|
+
getPort() {
|
|
1054
|
+
return this.port;
|
|
1055
|
+
}
|
|
1056
|
+
getPidFilePath() {
|
|
1057
|
+
return this.pidFilePath;
|
|
1058
|
+
}
|
|
1059
|
+
getLastError() {
|
|
1060
|
+
return this.lastError;
|
|
1061
|
+
}
|
|
1062
|
+
isRunning() {
|
|
1063
|
+
const data = readPidFile(this.pidFilePath);
|
|
1064
|
+
if (!data) return false;
|
|
1065
|
+
if (!isProcessRunning(data.pid)) {
|
|
1066
|
+
removePidFile(this.pidFilePath);
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
async start() {
|
|
1072
|
+
this.lastError = void 0;
|
|
1073
|
+
const existingPidData = readPidFile(this.pidFilePath);
|
|
1074
|
+
if (existingPidData && isProcessRunning(existingPidData.pid)) {
|
|
1075
|
+
if (!existingPidData.token) {
|
|
1076
|
+
console.log(t("daemon.lackingToken"));
|
|
1077
|
+
await this.stop();
|
|
1078
|
+
await this.waitForPortFree();
|
|
1079
|
+
} else {
|
|
1080
|
+
console.log(t("daemon.alreadyRunning"));
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const homedir14 = process.env.HOME || process.env.USERPROFILE || "";
|
|
1086
|
+
const logDir = join6(homedir14, ".tabclaw", "logs");
|
|
1087
|
+
mkdirSync4(logDir, { recursive: true });
|
|
1088
|
+
removePidFile(this.pidFilePath);
|
|
1089
|
+
const __dirname3 = dirname3(fileURLToPath(import.meta.url));
|
|
1090
|
+
let daemonScript = join6(__dirname3, "..", "daemon", "process.js");
|
|
1091
|
+
if (!existsSync7(daemonScript)) {
|
|
1092
|
+
daemonScript = join6(__dirname3, "..", "..", "src", "daemon", "process.ts");
|
|
1093
|
+
}
|
|
1094
|
+
if (!existsSync7(daemonScript)) {
|
|
1095
|
+
this.lastError = t("daemon.scriptNotFound", { path: daemonScript });
|
|
1096
|
+
console.error(this.lastError);
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
const logFile = join6(logDir, "gateway.log");
|
|
1100
|
+
const isDev = daemonScript.endsWith(".ts");
|
|
1101
|
+
const execPath = isDev ? join6(__dirname3, "..", "..", "node_modules", ".bin", "tsx") : process.execPath;
|
|
1102
|
+
const child = spawn(execPath, [daemonScript], {
|
|
1103
|
+
detached: true,
|
|
1104
|
+
stdio: "ignore",
|
|
1105
|
+
env: {
|
|
1106
|
+
...process.env,
|
|
1107
|
+
TABCLAW_DAEMON: "true",
|
|
1108
|
+
TABCLAW_PORT: String(this.port),
|
|
1109
|
+
TABCLAW_PID_FILE: this.pidFilePath
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
child.unref();
|
|
1113
|
+
let pidData = null;
|
|
1114
|
+
for (let i = 0; i < 30; i++) {
|
|
1115
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1116
|
+
pidData = readPidFile(this.pidFilePath);
|
|
1117
|
+
if (pidData) {
|
|
1118
|
+
if (isProcessRunning(pidData.pid)) {
|
|
1119
|
+
console.log(t("daemon.startedOnPort", { port: this.port }));
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
this.lastError = t("daemon.startFailed");
|
|
1125
|
+
console.error(this.lastError);
|
|
1126
|
+
console.error(t("daemon.checkLogs", { path: logFile }));
|
|
1127
|
+
return false;
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1130
|
+
this.lastError = t("daemon.startError", { error: errorMsg });
|
|
1131
|
+
console.error(this.lastError);
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async stop() {
|
|
1136
|
+
const data = readPidFile(this.pidFilePath);
|
|
1137
|
+
if (!data) {
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
process.kill(data.pid, "SIGTERM");
|
|
1142
|
+
let attempts = 0;
|
|
1143
|
+
while (isProcessRunning(data.pid) && attempts < 10) {
|
|
1144
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1145
|
+
attempts++;
|
|
1146
|
+
}
|
|
1147
|
+
if (isProcessRunning(data.pid)) {
|
|
1148
|
+
process.kill(data.pid, "SIGKILL");
|
|
1149
|
+
}
|
|
1150
|
+
removePidFile(this.pidFilePath);
|
|
1151
|
+
return true;
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async forceCleanup() {
|
|
1157
|
+
removePidFile(this.pidFilePath);
|
|
1158
|
+
}
|
|
1159
|
+
async waitForPortFree(maxWaitMs = 5e3) {
|
|
1160
|
+
const startTime = Date.now();
|
|
1161
|
+
while (await isPortInUse(this.port)) {
|
|
1162
|
+
if (Date.now() - startTime > maxWaitMs) {
|
|
1163
|
+
console.log(t("daemon.portInUse", { port: this.port, ms: maxWaitMs }));
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
// src/cli/wizard/steps/final.ts
|
|
1172
|
+
import { join as join7 } from "path";
|
|
1173
|
+
import { homedir as homedir6 } from "os";
|
|
1174
|
+
import pc5 from "picocolors";
|
|
1175
|
+
import ora3 from "ora";
|
|
1176
|
+
async function stepFinal(state) {
|
|
1177
|
+
console.log(pc5.bold(`
|
|
1178
|
+
${t("wizard.section.complete")}
|
|
1179
|
+
`));
|
|
1180
|
+
if (state.sessionConfigured.size === 0) {
|
|
1181
|
+
console.log(pc5.gray(` ${t("wizard.final.noItems")}
|
|
1182
|
+
`));
|
|
1183
|
+
} else {
|
|
1184
|
+
for (const item of state.sessionConfigured) {
|
|
1185
|
+
const result = state.validationResults.get(item);
|
|
1186
|
+
if (result?.ok) {
|
|
1187
|
+
console.log(pc5.green(`* ${t("wizard.final.itemOk", { item })}`));
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
console.log("");
|
|
1191
|
+
}
|
|
1192
|
+
const pidFilePath = join7(homedir6(), ".tabclaw", "tabclaw.pid");
|
|
1193
|
+
const port = loadConfig().general.hook_server_port;
|
|
1194
|
+
const daemon = new DaemonManager(pidFilePath, port);
|
|
1195
|
+
const hasChanges = state.sessionConfigured.size > 0 || Object.keys(state.pendingChanges).length > 0;
|
|
1196
|
+
if (daemon.isRunning()) {
|
|
1197
|
+
if (hasChanges) {
|
|
1198
|
+
console.log(pc5.blue(`i ${t("wizard.final.runningChanges")}
|
|
1199
|
+
`));
|
|
1200
|
+
console.log(pc5.yellow(` ${t("wizard.final.configModified")}
|
|
1201
|
+
`));
|
|
1202
|
+
const shouldRestart = await prompts.restartGateway();
|
|
1203
|
+
if (shouldRestart) {
|
|
1204
|
+
const spinner = ora3(t("wizard.final.restarting")).start();
|
|
1205
|
+
await daemon.stop();
|
|
1206
|
+
const started = await daemon.start();
|
|
1207
|
+
if (started) {
|
|
1208
|
+
spinner.succeed(t("wizard.final.restarted"));
|
|
1209
|
+
} else {
|
|
1210
|
+
spinner.fail(t("wizard.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
console.log(pc5.gray(` ${t("wizard.final.restartTip")}
|
|
1214
|
+
`));
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
console.log(pc5.blue(`i ${t("wizard.final.running")}
|
|
1218
|
+
`));
|
|
1219
|
+
console.log(pc5.green(` ${t("wizard.final.runningReady")}
|
|
1220
|
+
`));
|
|
1221
|
+
}
|
|
1222
|
+
} else {
|
|
1223
|
+
console.log(pc5.blue(`i ${t("wizard.final.notRunning")}
|
|
1224
|
+
`));
|
|
1225
|
+
if (hasChanges) {
|
|
1226
|
+
console.log(pc5.yellow(` ${t("wizard.final.configModifiedStart")}
|
|
1227
|
+
`));
|
|
1228
|
+
}
|
|
1229
|
+
const shouldStart = await prompts.restartGateway();
|
|
1230
|
+
if (shouldStart) {
|
|
1231
|
+
const spinner2 = ora3(t("wizard.final.starting")).start();
|
|
1232
|
+
const started = await daemon.start();
|
|
1233
|
+
if (started) {
|
|
1234
|
+
spinner2.succeed(t("wizard.final.started"));
|
|
1235
|
+
} else {
|
|
1236
|
+
spinner2.fail(t("wizard.final.startFailed", { error: daemon.getLastError() ?? "unknown" }));
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
console.log(pc5.gray(` ${t("wizard.final.startTip")}
|
|
1240
|
+
`));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
console.log(pc5.green(`
|
|
1244
|
+
${t("wizard.final.done")}
|
|
1245
|
+
`));
|
|
1246
|
+
return { action: "next", next: "done" };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/cli/wizard/engine.ts
|
|
1250
|
+
var WizardEngine = class {
|
|
1251
|
+
state;
|
|
1252
|
+
applicator;
|
|
1253
|
+
constructor() {
|
|
1254
|
+
const configPath = join8(homedir7(), ".tabclaw", "config.toml");
|
|
1255
|
+
const hasExistingConfig = existsSync8(configPath);
|
|
1256
|
+
this.state = createInitialState(hasExistingConfig);
|
|
1257
|
+
this.applicator = new ConfigApplicator();
|
|
1258
|
+
}
|
|
1259
|
+
async run() {
|
|
1260
|
+
await this.detectAllStatus();
|
|
1261
|
+
while (this.state.current !== "done" && this.state.current !== "quit") {
|
|
1262
|
+
if (this.state.current === "final" && this.hasChanges()) {
|
|
1263
|
+
this.applicator.apply(this.state.pendingChanges);
|
|
1264
|
+
}
|
|
1265
|
+
const result = await this.runStep(this.state.current);
|
|
1266
|
+
if (result.action === "next") {
|
|
1267
|
+
this.state.previous = this.state.current;
|
|
1268
|
+
this.state.current = result.next;
|
|
1269
|
+
} else if (result.action === "back") {
|
|
1270
|
+
this.state.current = this.state.previous || "channel-menu";
|
|
1271
|
+
} else if (result.action === "quit") {
|
|
1272
|
+
this.state.current = "quit";
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async detectAllStatus() {
|
|
1277
|
+
const claude = detectClaude();
|
|
1278
|
+
this.state.claudeDetection = claude;
|
|
1279
|
+
this.state.itemStatus.set("claude", claude.hooked ? "configured" : "unconfigured");
|
|
1280
|
+
const channelNames = ["logger", "telegram", "feishu"];
|
|
1281
|
+
for (const name of channelNames) {
|
|
1282
|
+
const detection = await detectChannel(name);
|
|
1283
|
+
if (detection.configured) {
|
|
1284
|
+
this.state.itemStatus.set(name, "configured");
|
|
1285
|
+
} else if (detection.error) {
|
|
1286
|
+
this.state.itemStatus.set(name, "error");
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async runStep(step) {
|
|
1291
|
+
switch (step) {
|
|
1292
|
+
case "cli":
|
|
1293
|
+
return await stepCli(this.state);
|
|
1294
|
+
case "cli-menu":
|
|
1295
|
+
return await stepCliMenu(this.state);
|
|
1296
|
+
case "channel-menu":
|
|
1297
|
+
return await stepChannelMenu(this.state);
|
|
1298
|
+
case "channel-config":
|
|
1299
|
+
return await stepChannelConfig(this.state);
|
|
1300
|
+
case "channel-notify":
|
|
1301
|
+
return await stepChannelNotify(this.state);
|
|
1302
|
+
case "final":
|
|
1303
|
+
return await stepFinal(this.state);
|
|
1304
|
+
default:
|
|
1305
|
+
return { action: "next", next: "done" };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
hasChanges() {
|
|
1309
|
+
return this.state.sessionConfigured.size > 0 || Object.keys(this.state.pendingChanges).length > 0;
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// src/cli/init.ts
|
|
1314
|
+
var initCommand = new Command2("init").description("Initialize tabclaw configuration").option("-f, --force", "Force reinitialize (regenerate token)").action(async (options) => {
|
|
1315
|
+
const configPath = join9(homedir8(), ".tabclaw", "config.toml");
|
|
1316
|
+
if (existsSync9(configPath) && !options.force) {
|
|
1317
|
+
const overwrite = await confirm2({
|
|
1318
|
+
message: t("cli.init.configExists"),
|
|
1319
|
+
default: false
|
|
1320
|
+
});
|
|
1321
|
+
if (!overwrite) {
|
|
1322
|
+
console.log(t("cli.init.cancelled"));
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const wizard = new WizardEngine();
|
|
1327
|
+
await wizard.run();
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// src/cli/logs.ts
|
|
1331
|
+
import { Command as Command3 } from "commander";
|
|
1332
|
+
import { createReadStream } from "fs";
|
|
1333
|
+
import { stat, readFile } from "fs/promises";
|
|
1334
|
+
import { homedir as homedir9 } from "os";
|
|
1335
|
+
import { join as join10 } from "path";
|
|
1336
|
+
import { readdirSync, statSync, existsSync as existsSync10 } from "fs";
|
|
1337
|
+
var SESSIONS_DIR = join10(homedir9(), ".tabclaw", "sessions");
|
|
1338
|
+
function formatSize(bytes) {
|
|
1339
|
+
if (bytes === 0) return "0B";
|
|
1340
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
1341
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1342
|
+
const size = bytes / Math.pow(1024, i);
|
|
1343
|
+
return `${size.toFixed(i === 0 ? 0 : 1)}${units[i]}`;
|
|
1344
|
+
}
|
|
1345
|
+
function sleep(ms) {
|
|
1346
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1347
|
+
}
|
|
1348
|
+
function listSessions() {
|
|
1349
|
+
const sessions = [];
|
|
1350
|
+
if (!existsSync10(SESSIONS_DIR)) {
|
|
1351
|
+
return sessions;
|
|
1352
|
+
}
|
|
1353
|
+
const entries = readdirSync(SESSIONS_DIR, { withFileTypes: true });
|
|
1354
|
+
for (const entry of entries) {
|
|
1355
|
+
if (entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name)) {
|
|
1356
|
+
const date = entry.name;
|
|
1357
|
+
const dateDir = join10(SESSIONS_DIR, date);
|
|
1358
|
+
const files = readdirSync(dateDir);
|
|
1359
|
+
for (const fileName of files) {
|
|
1360
|
+
if (fileName.endsWith(".jsonl")) {
|
|
1361
|
+
const filePath = join10(dateDir, fileName);
|
|
1362
|
+
const stats = statSync(filePath);
|
|
1363
|
+
const match = fileName.match(/^([^-]+)-(.+)\.jsonl$/);
|
|
1364
|
+
if (match) {
|
|
1365
|
+
const [, agentType, sessionId] = match;
|
|
1366
|
+
sessions.push({
|
|
1367
|
+
sessionId,
|
|
1368
|
+
agentType,
|
|
1369
|
+
date,
|
|
1370
|
+
path: filePath,
|
|
1371
|
+
size: stats.size
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return sessions.sort((a, b) => {
|
|
1379
|
+
if (a.date !== b.date) {
|
|
1380
|
+
return b.date.localeCompare(a.date);
|
|
1381
|
+
}
|
|
1382
|
+
return a.sessionId.localeCompare(b.sessionId);
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function findSessionFile(sessionId) {
|
|
1386
|
+
const sessions = listSessions();
|
|
1387
|
+
const match = sessions.find((s) => s.sessionId === sessionId);
|
|
1388
|
+
if (match) return match;
|
|
1389
|
+
const matches = sessions.filter((s) => s.sessionId.includes(sessionId));
|
|
1390
|
+
if (matches.length === 1) {
|
|
1391
|
+
return matches[0];
|
|
1392
|
+
}
|
|
1393
|
+
if (matches.length > 1) {
|
|
1394
|
+
console.error(`Multiple sessions match "${sessionId}":`);
|
|
1395
|
+
for (const s of matches) {
|
|
1396
|
+
console.error(` - ${s.sessionId} (${s.agentType}, ${s.date})`);
|
|
1397
|
+
}
|
|
1398
|
+
return void 0;
|
|
1399
|
+
}
|
|
1400
|
+
return void 0;
|
|
1401
|
+
}
|
|
1402
|
+
function printEvent(event) {
|
|
1403
|
+
const timestamp = new Date(event.ts);
|
|
1404
|
+
const timeStr = timestamp.toTimeString().split(" ")[0];
|
|
1405
|
+
const type = event.type || "unknown";
|
|
1406
|
+
const typeStr = type.padEnd(16);
|
|
1407
|
+
let content = "";
|
|
1408
|
+
if (event.tool?.name) {
|
|
1409
|
+
content = `Tool: ${event.tool.name}`;
|
|
1410
|
+
} else if (event.text) {
|
|
1411
|
+
content = event.text;
|
|
1412
|
+
} else if (typeof event.content === "string") {
|
|
1413
|
+
content = event.content;
|
|
1414
|
+
} else {
|
|
1415
|
+
const { ...rest } = event;
|
|
1416
|
+
const keys = Object.keys(rest);
|
|
1417
|
+
if (keys.length > 0) {
|
|
1418
|
+
content = `${keys[0]}: ${JSON.stringify(rest[keys[0]])}`;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if (content.length > 60) {
|
|
1422
|
+
content = content.substring(0, 57) + "...";
|
|
1423
|
+
}
|
|
1424
|
+
console.log(`[${timeStr}] ${typeStr} ${content}`);
|
|
1425
|
+
}
|
|
1426
|
+
async function* readEvents(filePath) {
|
|
1427
|
+
if (!existsSync10(filePath)) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const content = await readFile(filePath, "utf-8");
|
|
1431
|
+
const lines = content.trim().split("\n").filter((line) => line.length > 0);
|
|
1432
|
+
for (const line of lines) {
|
|
1433
|
+
try {
|
|
1434
|
+
const event = JSON.parse(line);
|
|
1435
|
+
yield event;
|
|
1436
|
+
} catch {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
async function showSessionList() {
|
|
1442
|
+
const sessions = listSessions();
|
|
1443
|
+
if (sessions.length === 0) {
|
|
1444
|
+
console.log("No sessions found.");
|
|
1445
|
+
console.log(`Sessions directory: ${SESSIONS_DIR}`);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
console.log("Sessions:\n");
|
|
1449
|
+
console.log(` Agent Session ID Date Size`);
|
|
1450
|
+
console.log(` --------- ------------------------ ---------- ------`);
|
|
1451
|
+
for (const session of sessions) {
|
|
1452
|
+
const agentStr = session.agentType.padEnd(9);
|
|
1453
|
+
const sessionStr = session.sessionId.padEnd(24).substring(0, 24);
|
|
1454
|
+
const dateStr = session.date;
|
|
1455
|
+
const sizeStr = formatSize(session.size).padStart(6);
|
|
1456
|
+
console.log(` ${agentStr} ${sessionStr} ${dateStr} ${sizeStr}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async function tailSession(sessionId) {
|
|
1460
|
+
const session = findSessionFile(sessionId);
|
|
1461
|
+
if (!session) {
|
|
1462
|
+
console.error(`Session not found: ${sessionId}`);
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
console.log(`Tailing session: ${session.sessionId} (${session.agentType})`);
|
|
1466
|
+
console.log(`File: ${session.path}`);
|
|
1467
|
+
console.log("Press Ctrl+C to exit\n");
|
|
1468
|
+
for await (const event of readEvents(session.path)) {
|
|
1469
|
+
printEvent(event);
|
|
1470
|
+
}
|
|
1471
|
+
let lastSize = (await stat(session.path)).size;
|
|
1472
|
+
let running = true;
|
|
1473
|
+
process.on("SIGINT", () => {
|
|
1474
|
+
running = false;
|
|
1475
|
+
console.log("\n");
|
|
1476
|
+
process.exit(0);
|
|
1477
|
+
});
|
|
1478
|
+
while (running) {
|
|
1479
|
+
await sleep(1e3);
|
|
1480
|
+
try {
|
|
1481
|
+
const stats = await stat(session.path);
|
|
1482
|
+
if (stats.size > lastSize) {
|
|
1483
|
+
const stream = createReadStream(session.path, {
|
|
1484
|
+
start: lastSize,
|
|
1485
|
+
encoding: "utf-8"
|
|
1486
|
+
});
|
|
1487
|
+
let newContent = "";
|
|
1488
|
+
stream.on("data", (chunk) => {
|
|
1489
|
+
newContent += chunk;
|
|
1490
|
+
});
|
|
1491
|
+
await new Promise((resolve) => {
|
|
1492
|
+
stream.on("end", () => resolve());
|
|
1493
|
+
stream.on("error", () => resolve());
|
|
1494
|
+
});
|
|
1495
|
+
const lines = newContent.split("\n").filter((line) => line.trim());
|
|
1496
|
+
for (const line of lines) {
|
|
1497
|
+
try {
|
|
1498
|
+
const event = JSON.parse(line);
|
|
1499
|
+
printEvent(event);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
lastSize = stats.size;
|
|
1504
|
+
}
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
console.error("\nError reading session file:", error);
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
async function replaySession(sessionId) {
|
|
1512
|
+
const session = findSessionFile(sessionId);
|
|
1513
|
+
if (!session) {
|
|
1514
|
+
console.error(`Session not found: ${sessionId}`);
|
|
1515
|
+
process.exit(1);
|
|
1516
|
+
}
|
|
1517
|
+
console.log(`Replaying session: ${session.sessionId} (${session.agentType})`);
|
|
1518
|
+
console.log(`File: ${session.path}
|
|
1519
|
+
`);
|
|
1520
|
+
const events = [];
|
|
1521
|
+
for await (const event of readEvents(session.path)) {
|
|
1522
|
+
events.push(event);
|
|
1523
|
+
}
|
|
1524
|
+
if (events.length === 0) {
|
|
1525
|
+
console.log("No events in session.");
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
let running = true;
|
|
1529
|
+
process.on("SIGINT", () => {
|
|
1530
|
+
running = false;
|
|
1531
|
+
console.log("\nReplay interrupted.");
|
|
1532
|
+
process.exit(0);
|
|
1533
|
+
});
|
|
1534
|
+
let lastTimestamp = null;
|
|
1535
|
+
for (const event of events) {
|
|
1536
|
+
if (!running) break;
|
|
1537
|
+
const currentTimestamp = new Date(event.ts).getTime();
|
|
1538
|
+
if (lastTimestamp !== null) {
|
|
1539
|
+
const delay = currentTimestamp - lastTimestamp;
|
|
1540
|
+
const replayDelay = Math.min(delay, 1e3);
|
|
1541
|
+
if (replayDelay > 0) {
|
|
1542
|
+
await sleep(replayDelay);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
printEvent(event);
|
|
1546
|
+
lastTimestamp = currentTimestamp;
|
|
1547
|
+
}
|
|
1548
|
+
console.log("\nReplay complete.");
|
|
1549
|
+
}
|
|
1550
|
+
var logsCommand = new Command3("logs").description("View session logs").option("-t, --tail <session>", "Tail a live session").option("-r, --replay <session>", "Replay session with original timing").action(async (options) => {
|
|
1551
|
+
try {
|
|
1552
|
+
if (options.tail) {
|
|
1553
|
+
await tailSession(options.tail);
|
|
1554
|
+
} else if (options.replay) {
|
|
1555
|
+
await replaySession(options.replay);
|
|
1556
|
+
} else {
|
|
1557
|
+
await showSessionList();
|
|
1558
|
+
}
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
console.error("Failed to read logs:", error);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
// src/cli/status.ts
|
|
1566
|
+
import { Command as Command4 } from "commander";
|
|
1567
|
+
import { homedir as homedir10 } from "os";
|
|
1568
|
+
import { join as join11 } from "path";
|
|
1569
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1570
|
+
function getSettingsPath2() {
|
|
1571
|
+
return join11(homedir10(), ".claude", "settings.json");
|
|
1572
|
+
}
|
|
1573
|
+
var statusCommand = new Command4("status").description("Show current status").action(() => {
|
|
1574
|
+
const config = loadConfig();
|
|
1575
|
+
const pidFilePath = join11(homedir10(), ".tabclaw", "tabclaw.pid");
|
|
1576
|
+
const daemon = new DaemonManager(pidFilePath, config.general.hook_server_port);
|
|
1577
|
+
console.log(`
|
|
1578
|
+
${t("status.title")}
|
|
1579
|
+
`);
|
|
1580
|
+
console.log(daemon.isRunning() ? t("status.daemon.running") : t("status.daemon.notRunning"));
|
|
1581
|
+
console.log(t("status.port", { port: config.general.hook_server_port }));
|
|
1582
|
+
console.log(`${t("status.logLevel", { level: config.general.log_level })}
|
|
1583
|
+
`);
|
|
1584
|
+
console.log(t("status.channels"));
|
|
1585
|
+
console.log(` ${config.channel.telegram?.enabled ? t("status.telegram.enabled") : t("status.telegram.disabled")}`);
|
|
1586
|
+
console.log(` ${config.channel.feishu?.enabled ? t("status.feishu.enabled") : t("status.feishu.disabled")}
|
|
1587
|
+
`);
|
|
1588
|
+
const settingsPath = getSettingsPath2();
|
|
1589
|
+
const backupPath = settingsPath + ".tabclaw-backup";
|
|
1590
|
+
if (existsSync11(backupPath)) {
|
|
1591
|
+
console.log(t("status.settingsBackup"));
|
|
1592
|
+
} else {
|
|
1593
|
+
console.log(t("status.settingsNoBackup"));
|
|
1594
|
+
}
|
|
1595
|
+
const home = homedir10();
|
|
1596
|
+
console.log(`
|
|
1597
|
+
${t("status.paths")}`);
|
|
1598
|
+
console.log(` ${t("status.pathConfig", { path: `${home}/.tabclaw/config.toml` })}`);
|
|
1599
|
+
console.log(` ${t("status.pathLogs", { path: `${home}/.tabclaw/logs/gateway.log` })}`);
|
|
1600
|
+
console.log(` ${t("status.pathBackup", { path: `${home}/.claude/settings.json.tabclaw-backup` })}
|
|
1601
|
+
`);
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
// src/cli/stop.ts
|
|
1605
|
+
import { Command as Command5 } from "commander";
|
|
1606
|
+
import { homedir as homedir11 } from "os";
|
|
1607
|
+
import { join as join12 } from "path";
|
|
1608
|
+
var stopCommand = new Command5("stop").description("Stop the tabclaw daemon and restore settings").action(async () => {
|
|
1609
|
+
const config = loadConfig();
|
|
1610
|
+
const pidFilePath = join12(homedir11(), ".tabclaw", "tabclaw.pid");
|
|
1611
|
+
const daemon = new DaemonManager(pidFilePath, config.general.hook_server_port);
|
|
1612
|
+
if (!daemon.isRunning()) {
|
|
1613
|
+
console.log(t("cli.stop.notRunning"));
|
|
1614
|
+
} else {
|
|
1615
|
+
console.log(t("cli.stop.stopping"));
|
|
1616
|
+
const stopped = await daemon.stop();
|
|
1617
|
+
if (stopped) {
|
|
1618
|
+
console.log(t("cli.stop.stopped"));
|
|
1619
|
+
} else {
|
|
1620
|
+
console.error(t("cli.stop.failed"));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// src/cli/gateway.ts
|
|
1626
|
+
import { Command as Command6 } from "commander";
|
|
1627
|
+
import { join as join13 } from "path";
|
|
1628
|
+
import { homedir as homedir12 } from "os";
|
|
1629
|
+
var PID_FILE = join13(homedir12(), ".tabclaw", "tabclaw.pid");
|
|
1630
|
+
var gatewayCommand = new Command6("gateway").description("Manage tabclaw Gateway daemon").addCommand(
|
|
1631
|
+
new Command6("start").description("Start or restart the Gateway daemon").option("-p, --port <port>", "Port to listen on").action(async (options) => {
|
|
1632
|
+
const config = loadConfig();
|
|
1633
|
+
const port = options.port ? parseInt(options.port, 10) : config.general.hook_server_port;
|
|
1634
|
+
const daemon = new DaemonManager(PID_FILE, port);
|
|
1635
|
+
if (daemon.isRunning()) {
|
|
1636
|
+
console.log(t("gateway.stopping"));
|
|
1637
|
+
await daemon.stop();
|
|
1638
|
+
}
|
|
1639
|
+
const started = await daemon.start();
|
|
1640
|
+
if (started) {
|
|
1641
|
+
console.log(t("gateway.startedOnPort", { port }));
|
|
1642
|
+
} else {
|
|
1643
|
+
const error = daemon.getLastError();
|
|
1644
|
+
console.error(t("gateway.startFailed"));
|
|
1645
|
+
if (error) {
|
|
1646
|
+
console.error(t("gateway.error", { error }));
|
|
1647
|
+
}
|
|
1648
|
+
const logPath = join13(homedir12(), ".tabclaw", "logs", "gateway.log");
|
|
1649
|
+
console.error(` ${t("gateway.checkLogs", { path: logPath })}`);
|
|
1650
|
+
process.exit(1);
|
|
1651
|
+
}
|
|
1652
|
+
})
|
|
1653
|
+
).addCommand(
|
|
1654
|
+
new Command6("stop").description("Stop the Gateway daemon").action(async () => {
|
|
1655
|
+
const config = loadConfig();
|
|
1656
|
+
const daemon = new DaemonManager(PID_FILE, config.general.hook_server_port);
|
|
1657
|
+
const stopped = await daemon.stop();
|
|
1658
|
+
if (stopped) {
|
|
1659
|
+
console.log(t("gateway.stopped"));
|
|
1660
|
+
} else {
|
|
1661
|
+
console.error(t("gateway.stopFailed"));
|
|
1662
|
+
process.exit(1);
|
|
1663
|
+
}
|
|
1664
|
+
})
|
|
1665
|
+
).addCommand(
|
|
1666
|
+
new Command6("restart").description("Restart the Gateway daemon").option("-p, --port <port>", "Port to listen on").action(async (options) => {
|
|
1667
|
+
const config = loadConfig();
|
|
1668
|
+
const port = options.port ? parseInt(options.port, 10) : config.general.hook_server_port;
|
|
1669
|
+
const daemon = new DaemonManager(PID_FILE, port);
|
|
1670
|
+
await daemon.stop();
|
|
1671
|
+
const started = await daemon.start();
|
|
1672
|
+
if (started) {
|
|
1673
|
+
console.log(t("gateway.restartedOnPort", { port }));
|
|
1674
|
+
} else {
|
|
1675
|
+
const error = daemon.getLastError();
|
|
1676
|
+
console.error(t("gateway.restartFailed"));
|
|
1677
|
+
if (error) {
|
|
1678
|
+
console.error(t("gateway.error", { error }));
|
|
1679
|
+
}
|
|
1680
|
+
const logPath = join13(homedir12(), ".tabclaw", "logs", "gateway.log");
|
|
1681
|
+
console.error(` ${t("gateway.checkLogs", { path: logPath })}`);
|
|
1682
|
+
process.exit(1);
|
|
1683
|
+
}
|
|
1684
|
+
})
|
|
1685
|
+
).addCommand(
|
|
1686
|
+
new Command6("status").description("Check Gateway status").action(async () => {
|
|
1687
|
+
const config = loadConfig();
|
|
1688
|
+
try {
|
|
1689
|
+
const response = await fetch(`http://localhost:${config.general.hook_server_port}/status`);
|
|
1690
|
+
if (response.ok) {
|
|
1691
|
+
const status = await response.json();
|
|
1692
|
+
console.log(t("gateway.status.title"));
|
|
1693
|
+
console.log(` ${t("gateway.status.running", { value: status.running ?? true })}`);
|
|
1694
|
+
console.log(` ${t("gateway.status.sessions", { count: status.sessions?.length ?? (typeof status.sessions === "number" ? status.sessions : 0) })}`);
|
|
1695
|
+
console.log(` ${t("gateway.status.clients", { count: status.clients ?? 0 })}`);
|
|
1696
|
+
if (status.sessions && status.sessions.length > 0) {
|
|
1697
|
+
console.log(`
|
|
1698
|
+
${t("gateway.status.activeSessions")}`);
|
|
1699
|
+
for (const session of status.sessions) {
|
|
1700
|
+
const agentType = session.agentType || "unknown";
|
|
1701
|
+
const sessionId = session.sessionId || session.id || "unknown";
|
|
1702
|
+
console.log(` - ${agentType}/${sessionId}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
} else {
|
|
1706
|
+
console.log(t("gateway.status.notResponding"));
|
|
1707
|
+
}
|
|
1708
|
+
} catch {
|
|
1709
|
+
console.log(t("gateway.status.notRunning"));
|
|
1710
|
+
}
|
|
1711
|
+
})
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
// src/cli/debug/index.ts
|
|
1715
|
+
import { Command as Command7 } from "commander";
|
|
1716
|
+
import { spawn as spawn2 } from "child_process";
|
|
1717
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1718
|
+
import { dirname as dirname4, join as join14 } from "path";
|
|
1719
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
1720
|
+
var __dirname2 = dirname4(__filename2);
|
|
1721
|
+
var debugCommand = new Command7("debug").description("Debug tools for TabClaw").addCommand(
|
|
1722
|
+
new Command7("hooks").description("Capture and log raw hook payloads for debugging").option("-p, --port <port>", "Port to listen on", "16792").action(async (options) => {
|
|
1723
|
+
const port = parseInt(options.port, 10);
|
|
1724
|
+
console.log(t("cli.debug.starting", { port }));
|
|
1725
|
+
console.log(t("cli.debug.capturing"));
|
|
1726
|
+
console.log(t("cli.debug.pressCtrlC"));
|
|
1727
|
+
const captureScript = join14(__dirname2, "hook-capture.js");
|
|
1728
|
+
const proc = spawn2("node", [captureScript], {
|
|
1729
|
+
stdio: "inherit",
|
|
1730
|
+
env: { ...process.env, TABCLAW_DEBUG_PORT: String(port) }
|
|
1731
|
+
});
|
|
1732
|
+
proc.on("exit", (code) => {
|
|
1733
|
+
process.exit(code || 0);
|
|
1734
|
+
});
|
|
1735
|
+
process.on("SIGINT", () => {
|
|
1736
|
+
console.log(t("cli.debug.stopping"));
|
|
1737
|
+
proc.kill("SIGINT");
|
|
1738
|
+
});
|
|
1739
|
+
})
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
// src/cli/agent/claude.ts
|
|
1743
|
+
import { spawn as spawn3 } from "child_process";
|
|
1744
|
+
import { homedir as homedir13 } from "os";
|
|
1745
|
+
import { join as join15, dirname as dirname5 } from "path";
|
|
1746
|
+
import { readFileSync as readFileSync7, existsSync as existsSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
1747
|
+
function getSettingsPath3() {
|
|
1748
|
+
return join15(homedir13(), ".claude", "settings.json");
|
|
1749
|
+
}
|
|
1750
|
+
function backupSettings2(settingsPath) {
|
|
1751
|
+
const backupPath = settingsPath + ".tabclaw-backup";
|
|
1752
|
+
if (existsSync12(settingsPath)) {
|
|
1753
|
+
writeFileSync6(backupPath, readFileSync7(settingsPath, "utf-8"));
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function writeSettings2(settingsPath, settings) {
|
|
1757
|
+
mkdirSync5(dirname5(settingsPath), { recursive: true });
|
|
1758
|
+
writeFileSync6(settingsPath, JSON.stringify(settings, null, 2));
|
|
1759
|
+
}
|
|
1760
|
+
function generateHooksConfig3(port, token) {
|
|
1761
|
+
const baseUrl = `http://localhost:${port}/hook/${token}`;
|
|
1762
|
+
const createHookEntry = (event) => ({
|
|
1763
|
+
matcher: "",
|
|
1764
|
+
hooks: [{ type: "http", url: `${baseUrl}/${event}` }]
|
|
1765
|
+
});
|
|
1766
|
+
return {
|
|
1767
|
+
Stop: [createHookEntry("Stop")],
|
|
1768
|
+
PostToolUse: [createHookEntry("PostToolUse")],
|
|
1769
|
+
PostToolUseFailure: [createHookEntry("PostToolUseFailure")],
|
|
1770
|
+
Notification: [createHookEntry("Notification")],
|
|
1771
|
+
PreToolUse: [createHookEntry("PreToolUse")],
|
|
1772
|
+
SubagentStart: [createHookEntry("SubagentStart")],
|
|
1773
|
+
SubagentStop: [createHookEntry("SubagentStop")],
|
|
1774
|
+
SessionStart: [createHookEntry("SessionStart")],
|
|
1775
|
+
SessionEnd: [createHookEntry("SessionEnd")],
|
|
1776
|
+
UserPromptSubmit: [createHookEntry("UserPromptSubmit")],
|
|
1777
|
+
PermissionRequest: [createHookEntry("PermissionRequest")]
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function registerClaudeCommand(program2) {
|
|
1781
|
+
program2.command("claude").description("Start Claude Code with tabclaw integration").option("-d, --directory <dir>", "Working directory for Claude").action(async (options) => {
|
|
1782
|
+
const config = loadConfig();
|
|
1783
|
+
const port = config.general.hook_server_port;
|
|
1784
|
+
const pidFilePath = join15(homedir13(), ".tabclaw", "tabclaw.pid");
|
|
1785
|
+
const daemon = new DaemonManager(pidFilePath, port);
|
|
1786
|
+
let daemonStarted = false;
|
|
1787
|
+
if (daemon.isRunning()) {
|
|
1788
|
+
console.log(t("cli.agent.alreadyRunning"));
|
|
1789
|
+
daemonStarted = true;
|
|
1790
|
+
} else {
|
|
1791
|
+
console.log(t("cli.agent.starting"));
|
|
1792
|
+
daemonStarted = await daemon.start();
|
|
1793
|
+
if (!daemonStarted) {
|
|
1794
|
+
console.error(t("cli.agent.startFailed"));
|
|
1795
|
+
process.exit(1);
|
|
1796
|
+
}
|
|
1797
|
+
console.log(t("cli.agent.started"));
|
|
1798
|
+
}
|
|
1799
|
+
console.log(t("cli.agent.configuring"));
|
|
1800
|
+
const settingsPath = getSettingsPath3();
|
|
1801
|
+
const backupPath = settingsPath + ".tabclaw-backup";
|
|
1802
|
+
if (!existsSync12(backupPath)) {
|
|
1803
|
+
backupSettings2(settingsPath);
|
|
1804
|
+
console.log(t("cli.agent.settingsBacked"));
|
|
1805
|
+
}
|
|
1806
|
+
const pidData = readPidFile(pidFilePath);
|
|
1807
|
+
const token = pidData?.token;
|
|
1808
|
+
if (!token) {
|
|
1809
|
+
console.error(t("cli.agent.tokenNotFound"));
|
|
1810
|
+
process.exit(1);
|
|
1811
|
+
}
|
|
1812
|
+
const hooksConfig = generateHooksConfig3(port, token);
|
|
1813
|
+
let currentSettings = {};
|
|
1814
|
+
if (existsSync12(settingsPath)) {
|
|
1815
|
+
currentSettings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
1816
|
+
}
|
|
1817
|
+
const mergedSettings = mergeHooks(currentSettings, hooksConfig);
|
|
1818
|
+
writeSettings2(settingsPath, mergedSettings);
|
|
1819
|
+
console.log(t("cli.agent.hooksConfigured"));
|
|
1820
|
+
const workDir = options.directory || process.cwd();
|
|
1821
|
+
console.log(t("cli.agent.startingIn", { dir: workDir }));
|
|
1822
|
+
console.log(t("cli.agent.pressCtrlC") + "\n");
|
|
1823
|
+
const claudeProcess = spawn3("claude", [], {
|
|
1824
|
+
cwd: workDir,
|
|
1825
|
+
stdio: "inherit",
|
|
1826
|
+
shell: true
|
|
1827
|
+
});
|
|
1828
|
+
claudeProcess.on("exit", (code) => {
|
|
1829
|
+
console.log(t("cli.agent.exited", { code }));
|
|
1830
|
+
console.log(t("cli.agent.daemonRunning"));
|
|
1831
|
+
console.log(t("cli.agent.tipStop"));
|
|
1832
|
+
process.exit(code || 0);
|
|
1833
|
+
});
|
|
1834
|
+
process.on("SIGINT", () => {
|
|
1835
|
+
console.log(t("cli.agent.detaching"));
|
|
1836
|
+
claudeProcess.kill("SIGINT");
|
|
1837
|
+
});
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/cli/agent/index.ts
|
|
1842
|
+
function registerAgentCommands(program2) {
|
|
1843
|
+
registerClaudeCommand(program2);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/cli/index.ts
|
|
1847
|
+
var program = new Command8();
|
|
1848
|
+
program.name("tabclaw").description("Bridge AI programming assistants with messaging platforms").version("0.1.0");
|
|
1849
|
+
program.addCommand(attachCommand);
|
|
1850
|
+
program.addCommand(debugCommand);
|
|
1851
|
+
program.addCommand(initCommand);
|
|
1852
|
+
program.addCommand(logsCommand);
|
|
1853
|
+
program.addCommand(statusCommand);
|
|
1854
|
+
program.addCommand(stopCommand);
|
|
1855
|
+
program.addCommand(gatewayCommand);
|
|
1856
|
+
registerAgentCommands(program);
|
|
1857
|
+
program.parse(process.argv);
|
|
1858
|
+
//# sourceMappingURL=index.js.map
|