@aion0/forge 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +9 -9
- package/README.md +10 -10
- package/app/api/settings/route.ts +52 -5
- package/app/api/tunnel/route.ts +12 -4
- package/app/login/page.tsx +35 -6
- package/bin/forge-server.mjs +1 -1
- package/cli/mw.ts +12 -12
- package/components/SettingsModal.tsx +297 -35
- package/components/TunnelToggle.tsx +48 -5
- package/install.sh +2 -2
- package/instrumentation.ts +9 -4
- package/lib/auth.ts +13 -9
- package/lib/cloudflared.ts +6 -0
- package/lib/crypto.ts +68 -0
- package/lib/init.ts +47 -9
- package/lib/password.ts +67 -47
- package/lib/settings.ts +38 -3
- package/lib/telegram-bot.ts +56 -23
- package/lib/telegram-standalone.ts +0 -1
- package/package.json +1 -1
package/lib/init.ts
CHANGED
|
@@ -7,19 +7,51 @@
|
|
|
7
7
|
import { ensureRunnerStarted } from './task-manager';
|
|
8
8
|
import { startTelegramBot, stopTelegramBot } from './telegram-bot';
|
|
9
9
|
import { startWatcherLoop } from './session-watcher';
|
|
10
|
-
import {
|
|
11
|
-
import { loadSettings } from './settings';
|
|
10
|
+
import { getAdminPassword } from './password';
|
|
11
|
+
import { loadSettings, saveSettings } from './settings';
|
|
12
12
|
import { startTunnel } from './cloudflared';
|
|
13
|
+
import { isEncrypted, SECRET_FIELDS } from './crypto';
|
|
13
14
|
import { spawn } from 'node:child_process';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
16
|
|
|
16
17
|
const initKey = Symbol.for('mw-initialized');
|
|
17
18
|
const gInit = globalThis as any;
|
|
18
19
|
|
|
20
|
+
/** Migrate plaintext secrets to encrypted on first run */
|
|
21
|
+
function migrateSecrets() {
|
|
22
|
+
try {
|
|
23
|
+
const { existsSync, readFileSync } = require('node:fs');
|
|
24
|
+
const { homedir } = require('node:os');
|
|
25
|
+
const YAML = require('yaml');
|
|
26
|
+
const dataDir = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
|
|
27
|
+
const file = join(dataDir, 'settings.yaml');
|
|
28
|
+
if (!existsSync(file)) return;
|
|
29
|
+
const raw = YAML.parse(readFileSync(file, 'utf-8')) || {};
|
|
30
|
+
let needsSave = false;
|
|
31
|
+
for (const field of SECRET_FIELDS) {
|
|
32
|
+
if (raw[field] && typeof raw[field] === 'string' && !isEncrypted(raw[field])) {
|
|
33
|
+
needsSave = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (needsSave) {
|
|
38
|
+
// loadSettings returns decrypted, saveSettings encrypts
|
|
39
|
+
const settings = loadSettings();
|
|
40
|
+
saveSettings(settings);
|
|
41
|
+
console.log('[init] Migrated plaintext secrets to encrypted storage');
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('[init] Secret migration error:', e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
19
48
|
export function ensureInitialized() {
|
|
20
49
|
if (gInit[initKey]) return;
|
|
21
50
|
gInit[initKey] = true;
|
|
22
51
|
|
|
52
|
+
// Migrate plaintext secrets on startup
|
|
53
|
+
migrateSecrets();
|
|
54
|
+
|
|
23
55
|
// Task runner is safe in every worker (DB-level coordination)
|
|
24
56
|
ensureRunnerStarted();
|
|
25
57
|
|
|
@@ -28,17 +60,23 @@ export function ensureInitialized() {
|
|
|
28
60
|
|
|
29
61
|
// If services are managed externally (forge-server), skip
|
|
30
62
|
if (process.env.FORGE_EXTERNAL_SERVICES === '1') {
|
|
31
|
-
// Password display
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
// Password display
|
|
64
|
+
const admin = getAdminPassword();
|
|
65
|
+
if (admin) {
|
|
66
|
+
console.log(`[init] Admin password: configured`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log('[init] No admin password set — configure in Settings');
|
|
69
|
+
};
|
|
35
70
|
return;
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
// Standalone mode (pnpm dev without forge-server) — start everything here
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
74
|
+
const admin2 = getAdminPassword();
|
|
75
|
+
if (admin2) {
|
|
76
|
+
console.log(`[init] Admin password: configured`);
|
|
77
|
+
} else {
|
|
78
|
+
console.log('[init] No admin password set — configure in Settings');
|
|
79
|
+
}
|
|
42
80
|
|
|
43
81
|
startTelegramBot(); // registers task event listener only
|
|
44
82
|
startTerminalProcess();
|
package/lib/password.ts
CHANGED
|
@@ -1,77 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Password management.
|
|
3
|
+
*
|
|
4
|
+
* - Admin password: set in Settings, encrypted in settings.yaml
|
|
5
|
+
* Used for: local login, tunnel start, secret changes, Telegram commands
|
|
6
|
+
* - Session code: random 8-digit numeric, generated each time tunnel starts
|
|
7
|
+
* Used for: remote login 2FA (admin password + session code)
|
|
8
|
+
*
|
|
9
|
+
* Local login: admin password only
|
|
10
|
+
* Remote login (tunnel): admin password + session code
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
14
|
import { homedir } from 'node:os';
|
|
9
15
|
import { join, dirname } from 'node:path';
|
|
10
|
-
import {
|
|
16
|
+
import { randomInt } from 'node:crypto';
|
|
11
17
|
|
|
12
|
-
const
|
|
18
|
+
const DATA_DIR = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
|
|
19
|
+
const SESSION_CODE_FILE = join(DATA_DIR, 'session-code.json');
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return
|
|
21
|
+
/** Generate a random 8-digit numeric code */
|
|
22
|
+
function generateSessionCode(): string {
|
|
23
|
+
return String(randomInt(10000000, 99999999));
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
function
|
|
20
|
-
|
|
26
|
+
function readSessionCode(): string {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(SESSION_CODE_FILE)) return '';
|
|
29
|
+
const data = JSON.parse(readFileSync(SESSION_CODE_FILE, 'utf-8'));
|
|
30
|
+
return data?.code || '';
|
|
31
|
+
} catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
function saveSessionCode(code: string) {
|
|
37
|
+
const dir = dirname(SESSION_CODE_FILE);
|
|
38
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
|
+
writeFileSync(SESSION_CODE_FILE, JSON.stringify({ code }), { mode: 0o600 });
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
|
|
42
|
+
/** Get the admin password from settings */
|
|
43
|
+
export function getAdminPassword(): string {
|
|
29
44
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
const { loadSettings } = require('./settings');
|
|
46
|
+
const settings = loadSettings();
|
|
47
|
+
return settings.telegramTunnelPassword || '';
|
|
32
48
|
} catch {
|
|
33
|
-
return
|
|
49
|
+
return '';
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
/** Get current session code (empty if none) */
|
|
54
|
+
export function getSessionCode(): string {
|
|
55
|
+
return readSessionCode();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Generate new session code. Called on tunnel start. */
|
|
59
|
+
export function rotateSessionCode(): string {
|
|
60
|
+
const code = generateSessionCode();
|
|
61
|
+
saveSessionCode(code);
|
|
62
|
+
console.log(`[password] New session code: ${code}`);
|
|
63
|
+
return code;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
67
|
+
* Verify login credentials.
|
|
68
|
+
* @param password - admin password
|
|
69
|
+
* @param sessionCode - session code (required for remote, empty for local)
|
|
70
|
+
* @param isRemote - true if accessing via tunnel
|
|
48
71
|
*/
|
|
49
|
-
export function
|
|
50
|
-
|
|
51
|
-
if (process.env.MW_PASSWORD && process.env.MW_PASSWORD !== 'auto') {
|
|
52
|
-
return process.env.MW_PASSWORD;
|
|
53
|
-
}
|
|
72
|
+
export function verifyLogin(password: string, sessionCode?: string, isRemote?: boolean): boolean {
|
|
73
|
+
if (!password) return false;
|
|
54
74
|
|
|
55
|
-
const
|
|
56
|
-
|
|
75
|
+
const admin = getAdminPassword();
|
|
76
|
+
if (!admin) return false;
|
|
77
|
+
if (password !== admin) return false;
|
|
57
78
|
|
|
58
|
-
//
|
|
59
|
-
if (
|
|
60
|
-
|
|
79
|
+
// Remote access requires session code as 2FA
|
|
80
|
+
if (isRemote) {
|
|
81
|
+
const currentCode = readSessionCode();
|
|
82
|
+
if (!currentCode || sessionCode !== currentCode) return false;
|
|
61
83
|
}
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
const password = generatePassword();
|
|
65
|
-
savePasswordData({ password, date: today });
|
|
66
|
-
console.log(`[password] New daily password generated for ${today}`);
|
|
67
|
-
return password;
|
|
85
|
+
return true;
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Verify admin password for privileged operations
|
|
90
|
+
* (tunnel start, secret changes, Telegram commands).
|
|
91
|
+
*/
|
|
92
|
+
export function verifyAdmin(input: string): boolean {
|
|
93
|
+
if (!input) return false;
|
|
94
|
+
const admin = getAdminPassword();
|
|
95
|
+
return admin ? input === admin : false;
|
|
77
96
|
}
|
|
97
|
+
|
package/lib/settings.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
|
+
import { encryptSecret, decryptSecret, isEncrypted, SECRET_FIELDS } from './crypto';
|
|
5
6
|
|
|
6
7
|
const DATA_DIR = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
|
|
7
8
|
const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
|
|
@@ -15,7 +16,7 @@ export interface Settings {
|
|
|
15
16
|
notifyOnComplete: boolean; // Notify when task completes
|
|
16
17
|
notifyOnFailure: boolean; // Notify when task fails
|
|
17
18
|
tunnelAutoStart: boolean; // Auto-start Cloudflare Tunnel on startup
|
|
18
|
-
telegramTunnelPassword: string; //
|
|
19
|
+
telegramTunnelPassword: string; // Admin password (encrypted) — for login, tunnel, secrets, Telegram
|
|
19
20
|
taskModel: string; // Model for tasks (default: sonnet)
|
|
20
21
|
pipelineModel: string; // Model for pipelines (default: sonnet)
|
|
21
22
|
telegramModel: string; // Model for Telegram AI features (default: sonnet)
|
|
@@ -38,18 +39,52 @@ const defaults: Settings = {
|
|
|
38
39
|
skipPermissions: false,
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
/** Load settings with secrets decrypted (for internal use) */
|
|
41
43
|
export function loadSettings(): Settings {
|
|
42
44
|
if (!existsSync(SETTINGS_FILE)) return { ...defaults };
|
|
43
45
|
try {
|
|
44
46
|
const raw = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
45
|
-
|
|
47
|
+
const parsed = { ...defaults, ...YAML.parse(raw) };
|
|
48
|
+
// Decrypt secret fields
|
|
49
|
+
for (const field of SECRET_FIELDS) {
|
|
50
|
+
if (parsed[field] && isEncrypted(parsed[field])) {
|
|
51
|
+
parsed[field] = decryptSecret(parsed[field]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
46
55
|
} catch {
|
|
47
56
|
return { ...defaults };
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
/** Load settings with secrets masked (for API response to frontend) */
|
|
61
|
+
export function loadSettingsMasked(): Settings & { _secretStatus: Record<string, boolean> } {
|
|
62
|
+
const settings = loadSettings();
|
|
63
|
+
const status: Record<string, boolean> = {};
|
|
64
|
+
for (const field of SECRET_FIELDS) {
|
|
65
|
+
status[field] = !!settings[field];
|
|
66
|
+
settings[field] = settings[field] ? '••••••••' : '';
|
|
67
|
+
}
|
|
68
|
+
return { ...settings, _secretStatus: status };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Save settings, encrypting secret fields */
|
|
51
72
|
export function saveSettings(settings: Settings) {
|
|
52
73
|
const dir = dirname(SETTINGS_FILE);
|
|
53
74
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
54
|
-
|
|
75
|
+
// Encrypt secret fields before saving
|
|
76
|
+
const toSave = { ...settings };
|
|
77
|
+
for (const field of SECRET_FIELDS) {
|
|
78
|
+
if (toSave[field] && !isEncrypted(toSave[field])) {
|
|
79
|
+
toSave[field] = encryptSecret(toSave[field]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(SETTINGS_FILE, YAML.stringify(toSave), 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Verify a secret field's current value */
|
|
86
|
+
export function verifySecret(field: string, value: string): boolean {
|
|
87
|
+
const settings = loadSettings();
|
|
88
|
+
const current = (settings as any)[field] || '';
|
|
89
|
+
return current === value;
|
|
55
90
|
}
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { scanProjects } from './projects';
|
|
|
13
13
|
import { listClaudeSessions, getSessionFilePath, readSessionEntries } from './claude-sessions';
|
|
14
14
|
import { listWatchers, createWatcher, deleteWatcher, toggleWatcher } from './session-watcher';
|
|
15
15
|
import { startTunnel, stopTunnel, getTunnelStatus } from './cloudflared';
|
|
16
|
-
|
|
16
|
+
// Password verification is done via require() in handler functions
|
|
17
17
|
import type { Task, TaskLogEntry } from '@/src/types';
|
|
18
18
|
|
|
19
19
|
// Persist state across hot-reloads
|
|
@@ -260,13 +260,13 @@ async function handleMessage(msg: any) {
|
|
|
260
260
|
await handleTunnelStatus(chatId);
|
|
261
261
|
break;
|
|
262
262
|
case '/tunnel_start':
|
|
263
|
-
await handleTunnelStart(chatId);
|
|
263
|
+
await handleTunnelStart(chatId, args[0]);
|
|
264
264
|
break;
|
|
265
265
|
case '/tunnel_stop':
|
|
266
266
|
await handleTunnelStop(chatId);
|
|
267
267
|
break;
|
|
268
|
-
case '/
|
|
269
|
-
await
|
|
268
|
+
case '/tunnel_code':
|
|
269
|
+
await handleTunnelCode(chatId, args[0], msg.message_id);
|
|
270
270
|
break;
|
|
271
271
|
default:
|
|
272
272
|
await send(chatId, `Unknown command: ${cmd}\nUse /help to see available commands.`);
|
|
@@ -308,7 +308,7 @@ async function sendHelp(chatId: number) {
|
|
|
308
308
|
`/projects — list projects\n\n` +
|
|
309
309
|
`🌐 /tunnel — status\n` +
|
|
310
310
|
`/tunnel_start / /tunnel_stop\n` +
|
|
311
|
-
`/
|
|
311
|
+
`/tunnel_code <admin_pw> — get session code\n\n` +
|
|
312
312
|
`Reply number to select`
|
|
313
313
|
);
|
|
314
314
|
}
|
|
@@ -892,10 +892,21 @@ async function handleTunnelStatus(chatId: number) {
|
|
|
892
892
|
}
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
-
async function handleTunnelStart(chatId: number) {
|
|
895
|
+
async function handleTunnelStart(chatId: number, password?: string) {
|
|
896
896
|
const settings = loadSettings();
|
|
897
897
|
if (String(chatId) !== settings.telegramChatId) { await send(chatId, '⛔ Unauthorized'); return; }
|
|
898
898
|
|
|
899
|
+
// Require admin password
|
|
900
|
+
if (!password) {
|
|
901
|
+
await send(chatId, '🔑 Usage: /tunnel_start <password>');
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const { verifyAdmin } = require('./password');
|
|
905
|
+
if (!verifyAdmin(password)) {
|
|
906
|
+
await send(chatId, '⛔ Wrong password');
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
899
910
|
// Check if tunnel is already running and still reachable
|
|
900
911
|
const status = getTunnelStatus();
|
|
901
912
|
if (status.status === 'running' && status.url) {
|
|
@@ -936,36 +947,36 @@ async function handleTunnelStop(chatId: number) {
|
|
|
936
947
|
await send(chatId, '🛑 Tunnel stopped');
|
|
937
948
|
}
|
|
938
949
|
|
|
939
|
-
async function
|
|
950
|
+
async function handleTunnelCode(chatId: number, password?: string, userMsgId?: number) {
|
|
940
951
|
const settings = loadSettings();
|
|
941
952
|
if (String(chatId) !== settings.telegramChatId) {
|
|
942
953
|
await send(chatId, '⛔ Unauthorized');
|
|
943
954
|
return;
|
|
944
955
|
}
|
|
945
956
|
|
|
946
|
-
if (!settings.telegramTunnelPassword) {
|
|
947
|
-
await send(chatId, '⚠️ Telegram tunnel password not configured.\nSet it in Settings → Remote Access → Telegram tunnel password');
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
957
|
if (!password) {
|
|
952
|
-
await send(chatId, 'Usage: /
|
|
958
|
+
await send(chatId, 'Usage: /tunnel_code <admin-password>');
|
|
953
959
|
return;
|
|
954
960
|
}
|
|
955
961
|
|
|
956
962
|
// Immediately delete user's message containing password
|
|
957
963
|
if (userMsgId) deleteMessageLater(chatId, userMsgId, 0);
|
|
958
964
|
|
|
959
|
-
|
|
965
|
+
const { verifyAdmin, getSessionCode } = require('./password');
|
|
966
|
+
if (!verifyAdmin(password)) {
|
|
960
967
|
await send(chatId, '⛔ Wrong password');
|
|
961
968
|
return;
|
|
962
969
|
}
|
|
963
970
|
|
|
964
|
-
|
|
971
|
+
// Show the session code (for remote login 2FA)
|
|
972
|
+
const code = getSessionCode();
|
|
965
973
|
const status = getTunnelStatus();
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
974
|
+
if (!code) {
|
|
975
|
+
await send(chatId, '⚠️ No session code. Start tunnel first to generate one.');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const labelId = await send(chatId, '🔑 Session code for remote login (auto-deletes in 30s):');
|
|
979
|
+
const pwId = await sendHtml(chatId, `<code>${code}</code>`);
|
|
969
980
|
if (labelId) deleteMessageLater(chatId, labelId);
|
|
970
981
|
if (pwId) deleteMessageLater(chatId, pwId);
|
|
971
982
|
if (status.status === 'running' && status.url) {
|
|
@@ -1182,6 +1193,7 @@ async function sendNoteToDocsClaude(chatId: number, content: string) {
|
|
|
1182
1193
|
const { join } = require('path');
|
|
1183
1194
|
const { homedir } = require('os');
|
|
1184
1195
|
const SESSION_NAME = 'mw-docs-claude';
|
|
1196
|
+
const docRoot = docRoots[0];
|
|
1185
1197
|
|
|
1186
1198
|
// Check if the docs tmux session exists
|
|
1187
1199
|
let sessionExists = false;
|
|
@@ -1190,9 +1202,22 @@ async function sendNoteToDocsClaude(chatId: number, content: string) {
|
|
|
1190
1202
|
sessionExists = true;
|
|
1191
1203
|
} catch {}
|
|
1192
1204
|
|
|
1205
|
+
// Auto-create session if it doesn't exist
|
|
1193
1206
|
if (!sessionExists) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1207
|
+
try {
|
|
1208
|
+
execSync(`tmux new-session -d -s ${SESSION_NAME} -x 120 -y 30`, { timeout: 5000 });
|
|
1209
|
+
// Wait for shell to initialize
|
|
1210
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1211
|
+
// cd to doc root and start claude
|
|
1212
|
+
const sf = settings.skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1213
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, `cd "${docRoot}" && claude --resume${sf}`, 'Enter'], { timeout: 5000 });
|
|
1214
|
+
// Wait for Claude to start up
|
|
1215
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1216
|
+
await send(chatId, '🚀 Auto-started Docs Claude session.');
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
await send(chatId, '❌ Failed to create Docs Claude session.');
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1196
1221
|
}
|
|
1197
1222
|
|
|
1198
1223
|
// Check if Claude is the active process (not shell)
|
|
@@ -1201,9 +1226,17 @@ async function sendNoteToDocsClaude(chatId: number, content: string) {
|
|
|
1201
1226
|
paneCmd = execSync(`tmux display-message -p -t ${SESSION_NAME} '#{pane_current_command}'`, { encoding: 'utf-8', timeout: 2000 }).trim();
|
|
1202
1227
|
} catch {}
|
|
1203
1228
|
|
|
1229
|
+
// If Claude is not running, start it
|
|
1204
1230
|
if (paneCmd === 'zsh' || paneCmd === 'bash' || paneCmd === 'fish' || !paneCmd) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1231
|
+
try {
|
|
1232
|
+
const sf = settings.skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1233
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, `cd "${docRoot}" && claude --resume${sf}`, 'Enter'], { timeout: 5000 });
|
|
1234
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1235
|
+
await send(chatId, '🚀 Auto-started Claude in Docs session.');
|
|
1236
|
+
} catch {
|
|
1237
|
+
await send(chatId, '❌ Failed to start Claude in Docs session.');
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1207
1240
|
}
|
|
1208
1241
|
|
|
1209
1242
|
// Write content to a temp file, then use tmux to send a prompt referencing it
|
|
@@ -1357,7 +1390,7 @@ async function setBotCommands(token: string) {
|
|
|
1357
1390
|
{ command: 'tunnel', description: 'Tunnel status' },
|
|
1358
1391
|
{ command: 'tunnel_start', description: 'Start tunnel' },
|
|
1359
1392
|
{ command: 'tunnel_stop', description: 'Stop tunnel' },
|
|
1360
|
-
{ command: '
|
|
1393
|
+
{ command: 'tunnel_code', description: 'Get session code for remote login' },
|
|
1361
1394
|
{ command: 'help', description: 'Show help' },
|
|
1362
1395
|
],
|
|
1363
1396
|
}),
|
package/package.json
CHANGED