@delt/claude-alarm 0.9.0 → 1.0.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 +47 -4
- package/dist/channel/server.js +41 -22
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +460 -145
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +774 -0
- package/dist/codex/main.js.map +1 -0
- package/dist/dashboard/index.html +44 -37
- package/dist/hub/server.js +292 -93
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.js +338 -120
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +44 -37
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/codex/main.ts","../../src/shared/config.ts","../../src/shared/constants.ts","../../src/shared/crash-guard.ts","../../src/shared/logger.ts","../../src/channel/hub-client.ts","../../src/codex/transport.ts","../../src/codex/rpc.ts","../../src/codex/mapping.ts","../../src/codex/adapter.ts"],"sourcesContent":["import fs from 'node:fs';\r\nimport { loadConfig } from '../shared/config.js';\r\nimport { CODEX_PID_FILE, DEFAULT_HUB_HOST } from '../shared/constants.js';\r\nimport { installCrashGuard, logStartup } from '../shared/crash-guard.js';\r\nimport { logger } from '../shared/logger.js';\r\nimport { CodexAdapter } from './adapter.js';\r\n\r\nfunction isRunning(pid: number): boolean {\r\n try {\r\n process.kill(pid, 0);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\ninstallCrashGuard('codex adapter');\r\n// Once the hub is gone its pipes break; without listeners every log line would become an uncaught EPIPE.\r\nprocess.stdout.on('error', () => {});\r\nprocess.stderr.on('error', () => {});\r\nconst config = loadConfig();\r\n\r\nconst existing = fs.existsSync(CODEX_PID_FILE) ? parseInt(fs.readFileSync(CODEX_PID_FILE, 'utf-8').trim(), 10) : NaN;\r\nif (!Number.isNaN(existing) && existing !== process.pid && isRunning(existing)) {\r\n logger.info(`Codex adapter already running (PID: ${existing})`);\r\n process.exit(0);\r\n}\r\nfs.writeFileSync(CODEX_PID_FILE, String(process.pid), 'utf-8');\r\nlogStartup('Codex adapter');\r\n\r\nconst host = process.env.CLAUDE_ALARM_HUB_HOST ?? (config.hub.host === '0.0.0.0' ? DEFAULT_HUB_HOST : config.hub.host);\r\nconst port = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;\r\nconst token = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;\r\n\r\nconst adapter = new CodexAdapter({ command: config.codex?.command ?? 'codex', hub: { host, port, token } });\r\nadapter.start();\r\n\r\nlet exiting = false;\r\nconst shutdown = () => {\r\n if (exiting) return;\r\n exiting = true;\r\n adapter.stop();\r\n try {\r\n if (fs.readFileSync(CODEX_PID_FILE, 'utf-8').trim() === String(process.pid)) fs.unlinkSync(CODEX_PID_FILE);\r\n } catch {}\r\n process.exit(0);\r\n};\r\nprocess.on('SIGINT', shutdown);\r\nprocess.on('SIGTERM', shutdown);\r\n\r\n// On Windows `hub stop` kills the hub without running its exit handlers, so stdin EOF is the only sign the parent is gone.\r\nif (process.argv.includes('--watch-stdin')) {\r\n process.stdin.on('end', shutdown);\r\n process.stdin.on('close', shutdown);\r\n process.stdin.resume();\r\n}\r\n","import fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { randomUUID } from 'node:crypto';\r\nimport { CONFIG_DIR, CONFIG_FILE, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT } from './constants.js';\r\nimport type { AppConfig } from './types.js';\r\n\r\nconst DEFAULT_CONFIG: AppConfig = {\r\n hub: {\r\n host: DEFAULT_HUB_HOST,\r\n port: DEFAULT_HUB_PORT,\r\n },\r\n notifications: {\r\n desktop: true,\r\n sound: true,\r\n },\r\n webhooks: [],\r\n};\r\n\r\nexport function ensureConfigDir(): void {\r\n if (!fs.existsSync(CONFIG_DIR)) {\r\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\r\n }\r\n}\r\n\r\nexport function loadConfig(): AppConfig {\r\n ensureConfigDir();\r\n let config: AppConfig;\r\n if (!fs.existsSync(CONFIG_FILE)) {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n } else {\r\n try {\r\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\r\n const parsed = JSON.parse(raw);\r\n config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...(parsed.telegram ? { telegram: parsed.telegram } : {}) };\r\n } catch {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n }\r\n }\r\n\r\n // Auto-generate token if missing\r\n if (!config.hub.token) {\r\n config.hub.token = randomUUID();\r\n saveConfig(config);\r\n }\r\n\r\n return config;\r\n}\r\n\r\n/** Get the current token, generating one if needed */\r\nexport function getOrCreateToken(): string {\r\n const config = loadConfig();\r\n return config.hub.token!;\r\n}\r\n\r\nexport function saveConfig(config: AppConfig): void {\r\n ensureConfigDir();\r\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n}\r\n\r\nexport function setCodexEnabled(enabled: boolean): AppConfig {\r\n const config = loadConfig();\r\n config.codex = { ...config.codex, enabled };\r\n saveConfig(config);\r\n return config;\r\n}\r\n\r\n/**\r\n * Add claude-alarm as an MCP channel server to .mcp.json\r\n */\r\nexport function setupMcpConfig(targetDir?: string): string {\r\n const dir = targetDir ?? process.cwd();\r\n const mcpPath = path.join(dir, '.mcp.json');\r\n\r\n let mcpConfig: Record<string, any> = {};\r\n if (fs.existsSync(mcpPath)) {\r\n try {\r\n mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));\r\n } catch {\r\n mcpConfig = {};\r\n }\r\n }\r\n\r\n if (!mcpConfig.mcpServers) {\r\n mcpConfig.mcpServers = {};\r\n }\r\n\r\n mcpConfig.mcpServers['claude-alarm'] = {\r\n command: 'npx',\r\n args: ['-y', '@delt/claude-alarm', 'serve'],\r\n env: {\r\n CLAUDE_ALARM_SESSION_NAME: path.basename(dir),\r\n },\r\n };\r\n\r\n fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');\r\n return mcpPath;\r\n}\r\n","import path from 'node:path';\r\nimport os from 'node:os';\r\n\r\nexport const DEFAULT_HUB_HOST = '127.0.0.1';\r\nexport const DEFAULT_HUB_PORT = 7900;\r\n\r\nexport const CONFIG_DIR = path.join(os.homedir(), '.claude-alarm');\r\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\r\nexport const PID_FILE = path.join(CONFIG_DIR, 'hub.pid');\r\nexport const LOG_FILE = path.join(CONFIG_DIR, 'hub.log');\r\nexport const UPLOADS_DIR = path.join(CONFIG_DIR, 'uploads');\r\nexport const CODEX_PID_FILE = path.join(CONFIG_DIR, 'codex.pid');\r\nexport const CODEX_LOG_FILE = path.join(CONFIG_DIR, 'codex.log');\r\n\r\nexport const WS_PATH_CHANNEL = '/ws/channel';\r\nexport const WS_PATH_DASHBOARD = '/ws/dashboard';\r\n\r\nexport const CHANNEL_SERVER_NAME = 'claude-alarm';\r\nexport const CHANNEL_SERVER_VERSION = '0.1.0';\r\n","import fs from 'node:fs';\nimport { CONFIG_DIR, LOG_FILE } from './constants.js';\nimport { logger } from './logger.js';\n\nconst BURST_WINDOW_MS = 60_000;\nconst BURST_LIMIT = 10;\n\nfunction appendToLog(text: string): void {\n try {\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\n fs.appendFileSync(LOG_FILE, text, 'utf-8');\n } catch {}\n}\n\nfunction describe(err: unknown): string {\n if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;\n try {\n return JSON.stringify(err);\n } catch {\n return String(err);\n }\n}\n\n/**\n * Last-resort net for exceptions no local handler caught.\n *\n * The hub keeps running afterwards: a notification relay that dies silently is\n * worse than one running on possibly-degraded state, and in foreground mode the\n * stack trace would otherwise vanish with the console window. A repeated burst\n * still exits, so a self-feeding error loop cannot spin forever.\n */\nexport function installCrashGuard(context: string): void {\n let recent: number[] = [];\n\n const handle = (kind: string, err: unknown): void => {\n const stack = describe(err);\n appendToLog(`\\n[${new Date().toISOString()}] ${kind} (${context})\\n${stack}\\n`);\n logger.error(`${kind} caught by crash guard: ${stack}`);\n\n const now = Date.now();\n recent = recent.filter((t) => now - t < BURST_WINDOW_MS);\n recent.push(now);\n\n if (recent.length > BURST_LIMIT) {\n const msg = `${recent.length} unhandled errors within ${BURST_WINDOW_MS / 1000}s — exiting`;\n appendToLog(`[${new Date().toISOString()}] ${msg}\\n`);\n logger.error(msg);\n process.exit(1);\n }\n };\n\n process.on('uncaughtException', (err) => handle('uncaughtException', err));\n process.on('unhandledRejection', (reason) => handle('unhandledRejection', reason));\n}\n\nexport function logStartup(context: string): void {\n appendToLog(`\\n[${new Date().toISOString()}] ${context} started (pid ${process.pid})\\n`);\n}\n","/**\r\n * Logger that writes to stderr only.\r\n * CRITICAL: In MCP channel servers, stdout is used for the stdio protocol.\r\n * Any console.log() would corrupt the protocol. Always use this logger.\r\n */\r\nexport const logger = {\r\n info(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm] ${msg}`, ...args);\r\n },\r\n warn(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm WARN] ${msg}`, ...args);\r\n },\r\n error(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm ERROR] ${msg}`, ...args);\r\n },\r\n debug(msg: string, ...args: unknown[]) {\r\n if (process.env.CLAUDE_ALARM_DEBUG) {\r\n console.error(`[claude-alarm DEBUG] ${msg}`, ...args);\r\n }\r\n },\r\n};\r\n","import WebSocket from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport { DEFAULT_HUB_HOST, DEFAULT_HUB_PORT, WS_PATH_CHANNEL } from '../shared/constants.js';\r\nimport type { ChannelMessage, SessionInfo } from '../shared/types.js';\r\n\r\nexport class HubClient {\r\n private ws: WebSocket | null = null;\r\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n private messageHandlers: Array<(msg: ChannelMessage) => void> = [];\r\n private queue: ChannelMessage[] = [];\r\n private connected = false;\r\n private closed = false;\r\n\r\n constructor(\r\n private sessionId: string,\r\n private sessionName: string,\r\n private hubHost = DEFAULT_HUB_HOST,\r\n private hubPort = DEFAULT_HUB_PORT,\r\n private token?: string,\r\n private getPeerName: () => string | undefined = () => undefined,\r\n private getRegistration: () => Partial<SessionInfo> = () => ({}),\r\n ) {}\r\n\r\n connect(): void {\r\n this.closed = false;\r\n const tokenQuery = this.token ? `?token=${encodeURIComponent(this.token)}` : '';\r\n const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;\r\n logger.debug(`Connecting to hub at ${url}`);\r\n\r\n try {\r\n const ws = new WebSocket(url);\r\n this.ws = ws;\r\n\r\n ws.on('open', () => {\r\n if (this.ws !== ws) return;\r\n logger.info('Connected to hub');\r\n this.connected = true;\r\n\r\n this.ws!.send(JSON.stringify(this.registration()));\r\n\r\n // Flush queued messages\r\n for (const msg of this.queue) {\r\n this.ws!.send(JSON.stringify(msg));\r\n }\r\n this.queue = [];\r\n });\r\n\r\n ws.on('message', (data) => {\r\n if (this.ws !== ws) return;\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n for (const handler of this.messageHandlers) {\r\n handler(msg);\r\n }\r\n } catch (err) {\r\n logger.warn('Failed to parse hub message:', err);\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n if (this.ws !== ws) return;\r\n logger.info('Disconnected from hub');\r\n this.connected = false;\r\n if (!this.closed) this.scheduleReconnect();\r\n });\r\n\r\n ws.on('error', (err) => {\r\n if (this.ws !== ws) return;\r\n logger.debug(`Hub connection error: ${err.message}`);\r\n this.connected = false;\r\n });\r\n } catch {\r\n logger.debug('Failed to connect to hub, will retry');\r\n this.scheduleReconnect();\r\n }\r\n }\r\n\r\n send(msg: ChannelMessage): void {\r\n if (this.connected && this.ws?.readyState === WebSocket.OPEN) {\r\n this.ws.send(JSON.stringify(msg));\r\n } else {\r\n if (this.queue.length < 100) {\r\n this.queue.push(msg);\r\n }\r\n logger.debug('Hub not connected, message queued');\r\n }\r\n }\r\n\r\n onMessage(handler: (msg: ChannelMessage) => void): void {\r\n this.messageHandlers.push(handler);\r\n }\r\n\r\n disconnect(): void {\r\n this.closed = true;\r\n if (this.reconnectTimer) {\r\n clearTimeout(this.reconnectTimer);\r\n this.reconnectTimer = null;\r\n }\r\n if (this.ws) {\r\n this.ws.close();\r\n this.ws = null;\r\n }\r\n this.connected = false;\r\n }\r\n\r\n reregister(): void {\r\n if (this.connected && this.ws?.readyState === WebSocket.OPEN) {\r\n this.ws.send(JSON.stringify(this.registration()));\r\n }\r\n }\r\n\r\n private registration(): ChannelMessage {\r\n return {\r\n type: 'register',\r\n session: {\r\n id: this.sessionId,\r\n name: this.sessionName,\r\n status: 'idle',\r\n connectedAt: Date.now(),\r\n lastActivity: Date.now(),\r\n cwd: process.cwd(),\r\n channelEnabled: true,\r\n peerName: this.getPeerName(),\r\n ...this.getRegistration(),\r\n },\r\n };\r\n }\r\n\r\n private scheduleReconnect(): void {\r\n if (this.reconnectTimer || this.closed) return;\r\n this.reconnectTimer = setTimeout(() => {\r\n this.reconnectTimer = null;\r\n this.connect();\r\n }, 5000);\r\n }\r\n}\r\n","import { spawn, type ChildProcess } from 'node:child_process';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { Duplex } from 'node:stream';\r\nimport WebSocket, { type ClientOptions } from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\n\r\nexport type SpawnFn = (command: string, args: string[]) => ChildProcess;\r\n\r\nexport interface ProxyConnection {\r\n ws: WebSocket;\r\n close(): void;\r\n}\r\n\r\n// npm installs Codex on Windows as a codex.cmd shim, which only runs through a shell.\r\nexport function resolveCommand(\r\n command: string,\r\n platform: NodeJS.Platform = process.platform,\r\n env: NodeJS.ProcessEnv = process.env,\r\n): { file: string; shell: boolean } {\r\n if (platform !== 'win32' || /[\\\\/]/.test(command) || path.extname(command)) return { file: command, shell: false };\r\n const dirs = (env.PATH ?? env.Path ?? '').split(path.delimiter).filter(Boolean);\r\n for (const ext of ['.exe', '.cmd']) {\r\n for (const dir of dirs) {\r\n const file = path.join(dir, command + ext);\r\n if (fs.existsSync(file)) return { file, shell: ext === '.cmd' };\r\n }\r\n }\r\n return { file: command, shell: false };\r\n}\r\n\r\nexport const defaultSpawn: SpawnFn = (command, args) => {\r\n const { file, shell } = resolveCommand(command);\r\n if (shell) return spawn(`\"${file}\" ${args.join(' ')}`, { stdio: 'pipe', windowsHide: true, shell: true });\r\n return spawn(file, args, { stdio: 'pipe', windowsHide: true });\r\n};\r\n\r\n// The daemon control socket speaks WebSocket, not JSONL: `codex app-server proxy` only relays bytes.\r\nexport function connectProxy(command: string, spawnFn: SpawnFn = defaultSpawn): Promise<ProxyConnection> {\r\n const child = spawnFn(command, ['app-server', 'proxy']);\r\n child.stdin?.on('error', () => {});\r\n child.stderr?.on('data', (d) => logger.debug(`codex proxy: ${String(d).trim()}`));\r\n\r\n const stream = new Duplex({\r\n read() {},\r\n write(chunk, _encoding, callback) {\r\n if (child.stdin?.writable) child.stdin.write(chunk, callback);\r\n else callback();\r\n },\r\n final(callback) {\r\n child.stdin?.end();\r\n callback();\r\n },\r\n });\r\n child.stdout?.on('data', (d) => stream.push(d));\r\n child.stdout?.on('end', () => stream.push(null));\r\n Object.assign(stream, { setNoDelay() {}, setTimeout() {}, setKeepAlive() {}, ref() {}, unref() {} });\r\n\r\n const ws = new WebSocket('ws://localhost/', {\r\n createConnection: (() => stream) as unknown as ClientOptions['createConnection'],\r\n });\r\n const close = () => {\r\n ws.terminate();\r\n child.kill();\r\n };\r\n\r\n return new Promise((resolve, reject) => {\r\n let settled = false;\r\n const fail = (err: Error) => {\r\n if (settled) return;\r\n settled = true;\r\n close();\r\n reject(err);\r\n };\r\n child.on('error', fail);\r\n child.once('exit', (code) => fail(new Error(`codex proxy exited (code ${code})`)));\r\n ws.on('error', fail);\r\n ws.once('open', () => {\r\n if (settled) return;\r\n settled = true;\r\n child.on('error', (err) => logger.warn(`codex proxy error: ${err.message}`));\r\n ws.on('error', (err) => logger.warn(`codex daemon socket error: ${err.message}`));\r\n resolve({ ws, close });\r\n });\r\n });\r\n}\r\n","import { EventEmitter } from 'node:events';\r\nimport type WebSocket from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\n\r\nexport type RpcId = number | string;\r\n\r\nexport class RpcError extends Error {\r\n constructor(public readonly code: number, message: string) {\r\n super(message);\r\n }\r\n}\r\n\r\ninterface Pending {\r\n resolve: (value: any) => void;\r\n reject: (err: Error) => void;\r\n timer: ReturnType<typeof setTimeout>;\r\n}\r\n\r\nexport class RpcClient extends EventEmitter {\r\n private nextId = 1;\r\n private pending = new Map<RpcId, Pending>();\r\n\r\n constructor(private ws: WebSocket, private timeoutMs = 30_000) {\r\n super();\r\n ws.on('message', (data) => this.onMessage(String(data)));\r\n ws.on('close', () => {\r\n for (const p of this.pending.values()) {\r\n clearTimeout(p.timer);\r\n p.reject(new Error('connection closed'));\r\n }\r\n this.pending.clear();\r\n this.emit('close');\r\n });\r\n }\r\n\r\n request<T = any>(method: string, params?: unknown): Promise<T> {\r\n if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error('connection closed'));\r\n const id = this.nextId++;\r\n return new Promise<T>((resolve, reject) => {\r\n const timer = setTimeout(() => {\r\n this.pending.delete(id);\r\n reject(new Error(`${method} timed out`));\r\n }, this.timeoutMs);\r\n this.pending.set(id, { resolve, reject, timer });\r\n this.ws.send(JSON.stringify(params === undefined ? { id, method } : { id, method, params }));\r\n });\r\n }\r\n\r\n notify(method: string, params?: unknown): void {\r\n if (this.ws.readyState !== this.ws.OPEN) return;\r\n this.ws.send(JSON.stringify(params === undefined ? { method } : { method, params }));\r\n }\r\n\r\n // ws stops reading the socket for good if a 'message' listener throws.\r\n private guarded(method: string, fn: () => void): void {\r\n try {\r\n fn();\r\n } catch (err) {\r\n logger.warn(`Codex ${method} handling failed: ${(err as Error).message}`);\r\n }\r\n }\r\n\r\n private onMessage(text: string): void {\r\n let msg: any;\r\n try {\r\n msg = JSON.parse(text);\r\n } catch {\r\n return;\r\n }\r\n if (!msg || typeof msg !== 'object') return;\r\n if (msg.method === undefined && msg.id !== undefined) {\r\n const p = this.pending.get(msg.id);\r\n if (!p) return;\r\n this.pending.delete(msg.id);\r\n clearTimeout(p.timer);\r\n if (msg.error) p.reject(new RpcError(msg.error.code, msg.error.message));\r\n else p.resolve(msg.result);\r\n } else if (msg.method !== undefined && msg.id !== undefined) {\r\n this.guarded(msg.method, () => this.emit('request', msg.id, msg.method, msg.params));\r\n } else if (msg.method !== undefined) {\r\n this.guarded(msg.method, () => this.emit('notification', msg.method, msg.params));\r\n }\r\n }\r\n}\r\n","import type { MessageSource, SessionStatus } from '../shared/types.js';\r\n\r\nexport type CodexThreadStatus =\r\n | { type: 'notLoaded' | 'idle' | 'systemError' }\r\n | { type: 'active'; activeFlags: string[] };\r\n\r\nexport interface CodexThread {\r\n id: string;\r\n name?: string | null;\r\n preview?: string;\r\n cwd: string;\r\n status: CodexThreadStatus;\r\n parentThreadId?: string | null;\r\n ephemeral?: boolean;\r\n}\r\n\r\nexport interface AgentMessage {\r\n text: string;\r\n phase?: string | null;\r\n}\r\n\r\nconst TITLE_MAX = 30;\r\nconst SOURCE_LABEL: Record<MessageSource, string> = { dashboard: 'Dashboard', telegram: 'Telegram', api: 'API' };\r\n\r\nexport function codexSessionId(threadId: string): string {\r\n return `codex:${threadId}`;\r\n}\r\n\r\nexport function isTrackable(thread: CodexThread): boolean {\r\n return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== 'notLoaded';\r\n}\r\n\r\nexport function threadTitle(thread: CodexThread): string {\r\n const name = thread.name?.trim();\r\n if (name) return name;\r\n const preview = (thread.preview ?? '').replace(/\\s+/g, ' ').trim();\r\n if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}…` : preview;\r\n return thread.cwd.replace(/^.*[/\\\\]/, '') || thread.id.slice(0, 8);\r\n}\r\n\r\nexport function hubStatus(status: CodexThreadStatus): SessionStatus {\r\n if (status.type !== 'active') return 'idle';\r\n return status.activeFlags.some((f) => f === 'waitingOnApproval' || f === 'waitingOnUserInput') ? 'waiting_input' : 'working';\r\n}\r\n\r\nexport function withSourcePrefix(content: string, source?: MessageSource): string {\r\n return source ? `[claude-alarm · ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;\r\n}\r\n\r\nexport function finalAnswer(messages: AgentMessage[]): string | null {\r\n const finals = messages.filter((m) => m.phase === 'final_answer').map((m) => m.text);\r\n if (finals.length) return finals.join('\\n\\n');\r\n return messages.at(-1)?.text ?? null;\r\n}\r\n","import { HubClient } from '../channel/hub-client.js';\r\nimport { CHANNEL_SERVER_VERSION } from '../shared/constants.js';\r\nimport { logger } from '../shared/logger.js';\r\nimport type { ChannelMessage, MessageSource, NotifyLevel, SessionInfo } from '../shared/types.js';\r\nimport { connectProxy, type ProxyConnection, type SpawnFn } from './transport.js';\r\nimport { RpcClient } from './rpc.js';\r\nimport {\r\n codexSessionId,\r\n finalAnswer,\r\n hubStatus,\r\n isTrackable,\r\n threadTitle,\r\n withSourcePrefix,\r\n type AgentMessage,\r\n type CodexThread,\r\n type CodexThreadStatus,\r\n} from './mapping.js';\r\n\r\nexport interface CodexAdapterOptions {\r\n command: string;\r\n hub: { host: string; port: number; token?: string };\r\n spawnFn?: SpawnFn;\r\n reconnectMinMs?: number;\r\n reconnectMaxMs?: number;\r\n idleReleaseMs?: number;\r\n}\r\n\r\ninterface Tracked {\r\n thread: CodexThread;\r\n hub: HubClient;\r\n subscribed: boolean;\r\n wantSubscribed: boolean;\r\n sync: Promise<void>;\r\n pendingTurn: boolean;\r\n turns: Map<string, AgentMessage[]>;\r\n releaseTimer?: ReturnType<typeof setTimeout>;\r\n unrelayed: boolean;\r\n}\r\n\r\nconst APPROVAL_REQUESTS = new Set([\r\n 'item/commandExecution/requestApproval',\r\n 'item/fileChange/requestApproval',\r\n 'mcpServer/elicitation/request',\r\n]);\r\n\r\nexport class CodexAdapter {\r\n private conn?: ProxyConnection;\r\n private rpc?: RpcClient;\r\n private threads = new Map<string, Tracked>();\r\n private stopped = false;\r\n private retryTimer?: ReturnType<typeof setTimeout>;\r\n private delay: number;\r\n\r\n constructor(private opts: CodexAdapterOptions) {\r\n this.delay = opts.reconnectMinMs ?? 2000;\r\n }\r\n\r\n start(): void {\r\n this.stopped = false;\r\n void this.connect();\r\n }\r\n\r\n stop(): void {\r\n this.stopped = true;\r\n if (this.retryTimer) clearTimeout(this.retryTimer);\r\n this.retryTimer = undefined;\r\n for (const id of [...this.threads.keys()]) this.drop(id);\r\n this.conn?.close();\r\n }\r\n\r\n private async connect(): Promise<void> {\r\n let rpc: RpcClient | undefined;\r\n try {\r\n const conn = await connectProxy(this.opts.command, this.opts.spawnFn);\r\n if (this.stopped) {\r\n conn.close();\r\n return;\r\n }\r\n const live = new RpcClient(conn.ws);\r\n rpc = live;\r\n live.on('notification', (method: string, params: any) => this.onNotification(method, params));\r\n live.on('request', (_id: unknown, method: string, params: any) => this.onServerRequest(method, params));\r\n live.on('close', () => {\r\n if (this.rpc === live) this.onDaemonLost();\r\n });\r\n this.conn = conn;\r\n this.rpc = live;\r\n const init = await live.request<{ userAgent?: string }>('initialize', {\r\n clientInfo: { name: 'claude-alarm', version: CHANNEL_SERVER_VERSION },\r\n });\r\n live.notify('initialized');\r\n logger.info(`Connected to Codex daemon (${init.userAgent ?? 'unknown version'})`);\r\n await this.discover();\r\n this.delay = this.opts.reconnectMinMs ?? 2000;\r\n } catch (err) {\r\n logger.warn(`Codex daemon connection failed: ${(err as Error).message}`);\r\n if (rpc && this.rpc === rpc) {\r\n this.rpc = undefined;\r\n this.conn?.close();\r\n this.conn = undefined;\r\n }\r\n for (const id of [...this.threads.keys()]) this.drop(id);\r\n this.scheduleRetry();\r\n }\r\n }\r\n\r\n private onDaemonLost(): void {\r\n this.rpc = undefined;\r\n this.conn?.close();\r\n this.conn = undefined;\r\n for (const id of [...this.threads.keys()]) this.drop(id);\r\n if (!this.stopped) logger.warn('Codex daemon connection lost');\r\n this.scheduleRetry();\r\n }\r\n\r\n private scheduleRetry(): void {\r\n if (this.stopped || this.retryTimer) return;\r\n this.retryTimer = setTimeout(() => {\r\n this.retryTimer = undefined;\r\n void this.connect();\r\n }, this.delay);\r\n this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 60_000);\r\n }\r\n\r\n private async discover(): Promise<void> {\r\n const ids: string[] = [];\r\n let cursor: string | null | undefined;\r\n do {\r\n const page = await this.rpc!.request<{ data: string[]; nextCursor?: string | null }>(\r\n 'thread/loaded/list',\r\n cursor ? { cursor } : {},\r\n );\r\n ids.push(...page.data);\r\n cursor = page.nextCursor;\r\n } while (cursor);\r\n for (const id of ids) await this.refresh(id);\r\n }\r\n\r\n private async refresh(threadId: string): Promise<void> {\r\n try {\r\n const { thread } = await this.rpc!.request<{ thread: CodexThread }>('thread/read', { threadId, includeTurns: false });\r\n this.upsert(thread);\r\n } catch (err) {\r\n logger.debug(`thread/read ${threadId} failed: ${(err as Error).message}`);\r\n }\r\n }\r\n\r\n private upsert(thread: CodexThread): void {\r\n if (!isTrackable(thread)) {\r\n this.drop(thread.id);\r\n return;\r\n }\r\n const existing = this.threads.get(thread.id);\r\n if (existing) {\r\n existing.thread = thread;\r\n existing.hub.reregister();\r\n } else {\r\n const { host, port, token } = this.opts.hub;\r\n const hub = new HubClient(codexSessionId(thread.id), threadTitle(thread), host, port, token, () => undefined, () =>\r\n this.registration(thread.id),\r\n );\r\n this.threads.set(thread.id, {\r\n thread,\r\n hub,\r\n subscribed: false,\r\n wantSubscribed: false,\r\n sync: Promise.resolve(),\r\n pendingTurn: false,\r\n turns: new Map(),\r\n unrelayed: false,\r\n });\r\n hub.onMessage((msg) => this.onHubMessage(thread.id, msg));\r\n hub.connect();\r\n }\r\n void this.want(thread.id, thread.status.type === 'active');\r\n }\r\n\r\n private registration(threadId: string): Partial<SessionInfo> {\r\n const t = this.threads.get(threadId);\r\n if (!t) return {};\r\n const title = threadTitle(t.thread);\r\n return { name: title, title, cwd: t.thread.cwd, agentKind: 'codex', status: hubStatus(t.thread.status) };\r\n }\r\n\r\n private want(threadId: string, subscribed: boolean): Promise<void> {\r\n const t = this.threads.get(threadId);\r\n if (!t) return Promise.resolve();\r\n t.wantSubscribed = subscribed;\r\n t.sync = t.sync.then(() => this.syncSubscription(t));\r\n return t.sync;\r\n }\r\n\r\n private async syncSubscription(t: Tracked): Promise<void> {\r\n const threadId = t.thread.id;\r\n if (!this.rpc || this.threads.get(threadId) !== t || t.subscribed === t.wantSubscribed) return;\r\n try {\r\n if (t.wantSubscribed) {\r\n // Only these two fields: any other resume field overrides the user's own thread settings.\r\n await this.rpc.request('thread/resume', { threadId, excludeTurns: true });\r\n t.subscribed = true;\r\n t.unrelayed = false;\r\n } else {\r\n // The daemon unloads a conversation 60 s after its last subscriber leaves; staying subscribed would keep closed Codex windows alive.\r\n await this.rpc.request('thread/unsubscribe', { threadId });\r\n t.subscribed = false;\r\n }\r\n } catch (err) {\r\n logger.debug(`subscription sync for ${threadId} deferred: ${(err as Error).message}`);\r\n }\r\n }\r\n\r\n private releaseLater(t: Tracked): void {\r\n clearTimeout(t.releaseTimer);\r\n t.releaseTimer = setTimeout(() => {\r\n t.releaseTimer = undefined;\r\n if (this.threads.get(t.thread.id) === t && t.thread.status.type !== 'active' && !t.pendingTurn) {\r\n void this.want(t.thread.id, false);\r\n }\r\n }, this.opts.idleReleaseMs ?? 1000);\r\n }\r\n\r\n private drop(threadId: string): void {\r\n const t = this.threads.get(threadId);\r\n if (!t) return;\r\n this.threads.delete(threadId);\r\n clearTimeout(t.releaseTimer);\r\n t.releaseTimer = undefined;\r\n t.hub.disconnect();\r\n }\r\n\r\n private onNotification(method: string, params: any): void {\r\n switch (method) {\r\n case 'thread/started':\r\n this.upsert(params.thread);\r\n break;\r\n case 'thread/status/changed':\r\n this.onStatus(params.threadId, params.status);\r\n break;\r\n case 'thread/name/updated': {\r\n const t = this.threads.get(params.threadId);\r\n if (t) {\r\n t.thread.name = params.threadName;\r\n t.hub.reregister();\r\n }\r\n break;\r\n }\r\n case 'thread/closed':\r\n case 'thread/archived':\r\n case 'thread/deleted':\r\n this.drop(params.threadId);\r\n break;\r\n case 'item/completed':\r\n if (params.item?.type === 'agentMessage') this.collect(params.threadId, params.turnId, params.item);\r\n break;\r\n case 'turn/completed':\r\n this.onTurnCompleted(params.threadId, params.turn);\r\n break;\r\n }\r\n }\r\n\r\n private onStatus(threadId: string, status: CodexThreadStatus): void {\r\n const t = this.threads.get(threadId);\r\n if (!t) {\r\n if (status.type !== 'notLoaded') void this.refresh(threadId);\r\n return;\r\n }\r\n if (status.type === 'notLoaded') {\r\n this.drop(threadId);\r\n return;\r\n }\r\n t.thread.status = status;\r\n t.pendingTurn = false;\r\n t.hub.send({ type: 'status', sessionId: codexSessionId(threadId), status: hubStatus(status) });\r\n if (status.type === 'systemError') this.notify(threadId, 'Codex error', 'The Codex conversation hit a system error.', 'error');\r\n if (status.type === 'active') {\r\n void this.want(threadId, true);\r\n return;\r\n }\r\n if (t.unrelayed) {\r\n t.unrelayed = false;\r\n this.notify(threadId, 'Reply not relayed', 'Codex finished, but its reply could not be relayed here. Check the Codex window.', 'warning');\r\n }\r\n // The daemon emits idle just before turn/completed; unsubscribing at once could cut off the reply.\r\n this.releaseLater(t);\r\n }\r\n\r\n private collect(threadId: string, turnId: string, item: AgentMessage): void {\r\n const t = this.threads.get(threadId);\r\n if (!t) return;\r\n const list = t.turns.get(turnId) ?? [];\r\n list.push({ text: item.text, phase: item.phase });\r\n t.turns.set(turnId, list);\r\n }\r\n\r\n private onTurnCompleted(\r\n threadId: string,\r\n turn: { id: string; status: string; error?: { message: string } | null; items?: Array<{ type: string } & AgentMessage> },\r\n ): void {\r\n const t = this.threads.get(threadId);\r\n if (!t) return;\r\n const collected = t.turns.get(turn.id) ?? [];\r\n t.turns.delete(turn.id);\r\n if (t.thread.status.type !== 'active') void this.want(threadId, false);\r\n if (turn.status === 'failed' || turn.error) {\r\n this.notify(threadId, 'Codex task failed', turn.error?.message ?? 'The task ended with an error.', 'error');\r\n return;\r\n }\r\n if (turn.status === 'interrupted') {\r\n this.notify(threadId, 'Codex task stopped', 'The task was interrupted.', 'info');\r\n return;\r\n }\r\n // turn/completed may carry only a summary of the items, so prefer what was collected live.\r\n const fromTurn = (turn.items ?? []).filter((i) => i.type === 'agentMessage');\r\n const text = finalAnswer(collected.length ? collected : fromTurn);\r\n if (text) t.hub.send({ type: 'reply', sessionId: codexSessionId(threadId), content: text });\r\n }\r\n\r\n private onHubMessage(threadId: string, msg: ChannelMessage): void {\r\n if (msg.type === 'message_to_session') {\r\n void this.sendTurn(threadId, msg.content, msg.source);\r\n } else if (msg.type === 'image_to_session') {\r\n this.notify(threadId, 'Not delivered', 'Codex sessions do not accept images yet.', 'warning');\r\n }\r\n }\r\n\r\n private async sendTurn(threadId: string, content: string, source?: MessageSource): Promise<void> {\r\n const t = this.threads.get(threadId);\r\n if (!t) return;\r\n if (t.thread.status.type !== 'idle' || t.pendingTurn) {\r\n this.notify(threadId, 'Not delivered', 'Codex is busy, so the message was not delivered. Send it again when the task finishes.', 'warning');\r\n return;\r\n }\r\n t.pendingTurn = true;\r\n try {\r\n if (!this.rpc) throw new Error('not connected to the Codex daemon');\r\n await this.want(threadId, true);\r\n // A new conversation cannot be subscribed before its first turn (no rollout found); the active broadcast retries it.\r\n t.unrelayed = !t.subscribed;\r\n await this.rpc.request('turn/start', { threadId, input: [{ type: 'text', text: withSourcePrefix(content, source) }] });\r\n } catch (err) {\r\n t.pendingTurn = false;\r\n t.unrelayed = false;\r\n this.releaseLater(t);\r\n this.notify(threadId, 'Not delivered', `Codex rejected the message: ${(err as Error).message}`, 'warning');\r\n }\r\n }\r\n\r\n private onServerRequest(method: string, params: any): void {\r\n if (!APPROVAL_REQUESTS.has(method) || !params?.threadId) {\r\n logger.debug(`Ignoring Codex server request ${method}`);\r\n return;\r\n }\r\n const detail = params.commandActions?.[0]?.command ?? params.command ?? params.reason ?? params.message ?? method;\r\n this.notify(params.threadId, 'Codex approval needed', `Approve or decline in Codex: ${String(detail).slice(0, 300)}`, 'warning');\r\n }\r\n\r\n private notify(threadId: string, title: string, message: string, level: NotifyLevel): void {\r\n this.threads.get(threadId)?.hub.send({ type: 'notify', sessionId: codexSessionId(threadId), title, message, level });\r\n }\r\n}\r\n"],"mappings":"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","names":["fs","path","config","config","fs","fs","token","fs","path","WebSocket","path","fs","WebSocket","existing","host","port","token","fs"]}
|
|
@@ -209,6 +209,14 @@
|
|
|
209
209
|
justify-content: center;
|
|
210
210
|
padding: 0 5px;
|
|
211
211
|
}
|
|
212
|
+
.agent-badge {
|
|
213
|
+
font-size: 10px;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
padding: 1px 6px;
|
|
216
|
+
border-radius: 8px;
|
|
217
|
+
border: 1px solid var(--green);
|
|
218
|
+
color: var(--green);
|
|
219
|
+
}
|
|
212
220
|
.session-status {
|
|
213
221
|
display: inline-block;
|
|
214
222
|
font-size: 11px;
|
|
@@ -871,7 +879,7 @@
|
|
|
871
879
|
<div class="token-overlay hidden" id="tokenOverlay">
|
|
872
880
|
<div class="token-form">
|
|
873
881
|
<h2>Authentication Required</h2>
|
|
874
|
-
<p>Enter the hub token to connect.
|
|
882
|
+
<p>Enter the hub token to connect. Run <code>claude-alarm token</code>, or open the login link printed by <code>claude-alarm hub start</code>.</p>
|
|
875
883
|
<input type="text" id="tokenInput" placeholder="Paste token here..." autocomplete="off">
|
|
876
884
|
<button id="tokenSubmit">Connect</button>
|
|
877
885
|
<div class="token-error" id="tokenError">Connection failed. Check your token.</div>
|
|
@@ -1026,7 +1034,6 @@
|
|
|
1026
1034
|
selectedSession: null,
|
|
1027
1035
|
messages: {},
|
|
1028
1036
|
notifications: [],
|
|
1029
|
-
token: null,
|
|
1030
1037
|
pendingImage: null,
|
|
1031
1038
|
unread: {},
|
|
1032
1039
|
waitingReply: {},
|
|
@@ -1131,6 +1138,10 @@
|
|
|
1131
1138
|
return state.sessionMeta.names[s.id] || s.displayName || s.name;
|
|
1132
1139
|
}
|
|
1133
1140
|
|
|
1141
|
+
function agentName(s) {
|
|
1142
|
+
return s && s.agentKind === 'codex' ? 'Codex' : 'Claude';
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1134
1145
|
function normName(s) {
|
|
1135
1146
|
return String(s).normalize('NFC').trim().toLowerCase();
|
|
1136
1147
|
}
|
|
@@ -1154,7 +1165,7 @@
|
|
|
1154
1165
|
// Built-in SendMessage only reaches sessions on the hub's own machine.
|
|
1155
1166
|
function mentionTargets() {
|
|
1156
1167
|
const selected = state.sessions[state.selectedSession];
|
|
1157
|
-
if (!selected || !selected.isLocal) return [];
|
|
1168
|
+
if (!selected || !selected.isLocal || selected.agentKind === 'codex') return [];
|
|
1158
1169
|
return Object.values(state.sessions).filter(s => s.id !== selected.id && s.isLocal && s.peerName);
|
|
1159
1170
|
}
|
|
1160
1171
|
|
|
@@ -1191,6 +1202,8 @@
|
|
|
1191
1202
|
// The routing line is the contract with the channel instructions in
|
|
1192
1203
|
// src/channel/server.ts (ROUTING) — change both together.
|
|
1193
1204
|
function buildRouting(text) {
|
|
1205
|
+
const target = state.sessions[state.selectedSession];
|
|
1206
|
+
if (target && target.agentKind === 'codex') return { ok: true, content: text };
|
|
1194
1207
|
const mentions = extractMentions(text);
|
|
1195
1208
|
if (!mentions.length) return { ok: true, content: text };
|
|
1196
1209
|
const unknown = [];
|
|
@@ -1291,25 +1304,28 @@
|
|
|
1291
1304
|
initTheme();
|
|
1292
1305
|
|
|
1293
1306
|
// --- Token handling ---
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return urlToken;
|
|
1307
|
+
async function checkAuth() {
|
|
1308
|
+
try {
|
|
1309
|
+
return (await fetch('/api/status')).status !== 401;
|
|
1310
|
+
} catch {
|
|
1311
|
+
return true;
|
|
1300
1312
|
}
|
|
1301
|
-
return sessionStorage.getItem('claude-alarm-token');
|
|
1302
1313
|
}
|
|
1303
1314
|
|
|
1304
1315
|
function showTokenForm() { $('#tokenOverlay').classList.remove('hidden'); }
|
|
1305
1316
|
function hideTokenForm() { $('#tokenOverlay').classList.add('hidden'); }
|
|
1306
1317
|
|
|
1307
|
-
$('#tokenSubmit').addEventListener('click', () => {
|
|
1318
|
+
$('#tokenSubmit').addEventListener('click', async () => {
|
|
1308
1319
|
const token = $('#tokenInput').value.trim();
|
|
1309
1320
|
if (!token) return;
|
|
1310
|
-
|
|
1311
|
-
|
|
1321
|
+
const res = await fetch('/api/login', {
|
|
1322
|
+
method: 'POST',
|
|
1323
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1324
|
+
body: JSON.stringify({ token }),
|
|
1325
|
+
}).catch(() => null);
|
|
1326
|
+
if (!res || !res.ok) { $('#tokenError').style.display = 'block'; return; }
|
|
1312
1327
|
$('#tokenError').style.display = 'none';
|
|
1328
|
+
$('#tokenInput').value = '';
|
|
1313
1329
|
hideTokenForm();
|
|
1314
1330
|
connect();
|
|
1315
1331
|
});
|
|
@@ -1320,25 +1336,23 @@
|
|
|
1320
1336
|
// --- WebSocket ---
|
|
1321
1337
|
function connect() {
|
|
1322
1338
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1339
|
+
const ws = new WebSocket(`${proto}://${location.host}/ws/dashboard`);
|
|
1340
|
+
let opened = false;
|
|
1325
1341
|
|
|
1326
1342
|
ws.onopen = () => {
|
|
1343
|
+
opened = true;
|
|
1327
1344
|
state.ws = ws;
|
|
1328
1345
|
hideTokenForm();
|
|
1329
1346
|
$('#connDot').classList.add('connected');
|
|
1330
1347
|
$('#connLabel').textContent = 'Connected';
|
|
1331
1348
|
};
|
|
1332
|
-
ws.onclose = () => {
|
|
1349
|
+
ws.onclose = async () => {
|
|
1333
1350
|
state.ws = null;
|
|
1334
1351
|
$('#connDot').classList.remove('connected');
|
|
1335
1352
|
$('#connLabel').textContent = 'Disconnected';
|
|
1353
|
+
if (!opened && !(await checkAuth())) { showTokenForm(); return; }
|
|
1336
1354
|
setTimeout(connect, 3000);
|
|
1337
1355
|
};
|
|
1338
|
-
ws.onerror = () => {
|
|
1339
|
-
if (!state.token) { showTokenForm(); } else { $('#tokenError').style.display = 'block'; showTokenForm(); }
|
|
1340
|
-
ws.close();
|
|
1341
|
-
};
|
|
1342
1356
|
ws.onmessage = (e) => { try { handleMessage(JSON.parse(e.data)); } catch {} };
|
|
1343
1357
|
}
|
|
1344
1358
|
|
|
@@ -1433,7 +1447,7 @@
|
|
|
1433
1447
|
return `<div class="session-card${active}" data-id="${id}">
|
|
1434
1448
|
<span class="drag-handle" title="Drag to reorder">⠿</span>
|
|
1435
1449
|
<div class="session-body">
|
|
1436
|
-
<div class="session-name" title="Double-click to rename">${esc(dispName)}${unread ? `<span class="unread-badge">${unread}</span>` : ''}</div>
|
|
1450
|
+
<div class="session-name" title="Double-click to rename">${esc(dispName)}${s.agentKind === 'codex' ? '<span class="agent-badge">Codex</span>' : ''}${unread ? `<span class="unread-badge">${unread}</span>` : ''}</div>
|
|
1437
1451
|
${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)} · ${connTime}</div>` : `<div class="session-cwd">${connTime}</div>`}
|
|
1438
1452
|
<span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
|
|
1439
1453
|
</div>
|
|
@@ -1580,7 +1594,7 @@
|
|
|
1580
1594
|
|
|
1581
1595
|
function updateImageUI() {
|
|
1582
1596
|
const s = state.selectedSession ? state.sessions[state.selectedSession] : null;
|
|
1583
|
-
const canImage = s && s.isLocal;
|
|
1597
|
+
const canImage = s && s.isLocal && s.agentKind !== 'codex';
|
|
1584
1598
|
$('#attachBtn').disabled = !canImage;
|
|
1585
1599
|
}
|
|
1586
1600
|
|
|
@@ -1616,7 +1630,7 @@
|
|
|
1616
1630
|
content = m.from === 'session' ? renderMarkdown(m.content) : esc(m.content).replace(/\n/g, '<br>');
|
|
1617
1631
|
}
|
|
1618
1632
|
return `<div class="message ${cls}">
|
|
1619
|
-
<div class="message-meta">${m.from === 'session' ?
|
|
1633
|
+
<div class="message-meta">${m.from === 'session' ? agentName(s) : 'You'} · ${timeStr}</div>
|
|
1620
1634
|
<div class="message-body">${content}</div>
|
|
1621
1635
|
</div>`;
|
|
1622
1636
|
}).join('');
|
|
@@ -2128,16 +2142,14 @@
|
|
|
2128
2142
|
$('#settingsToggle').addEventListener('click', async () => {
|
|
2129
2143
|
// Load webhook data
|
|
2130
2144
|
try {
|
|
2131
|
-
const
|
|
2132
|
-
const res = await fetch(`/api/webhooks${tokenQuery}`);
|
|
2145
|
+
const res = await fetch(`/api/webhooks`);
|
|
2133
2146
|
const data = await res.json();
|
|
2134
2147
|
webhookData = data.webhooks || [];
|
|
2135
2148
|
} catch { webhookData = []; }
|
|
2136
2149
|
renderWebhooks();
|
|
2137
2150
|
// Load telegram data
|
|
2138
2151
|
try {
|
|
2139
|
-
const
|
|
2140
|
-
const res = await fetch(`/api/telegram${tokenQuery}`);
|
|
2152
|
+
const res = await fetch(`/api/telegram`);
|
|
2141
2153
|
const data = await res.json();
|
|
2142
2154
|
const tg = data.telegram || {};
|
|
2143
2155
|
if (!$('#tgBotToken').value || $('#tgBotToken').value.includes('...')) {
|
|
@@ -2172,8 +2184,7 @@
|
|
|
2172
2184
|
$('#webhookSave').addEventListener('click', async () => {
|
|
2173
2185
|
const valid = webhookData.filter(w => w.url.trim());
|
|
2174
2186
|
try {
|
|
2175
|
-
|
|
2176
|
-
await fetch(`/api/webhooks${tokenQuery}`, {
|
|
2187
|
+
await fetch(`/api/webhooks`, {
|
|
2177
2188
|
method: 'POST',
|
|
2178
2189
|
headers: { 'Content-Type': 'application/json' },
|
|
2179
2190
|
body: JSON.stringify({ webhooks: valid }),
|
|
@@ -2248,8 +2259,7 @@
|
|
|
2248
2259
|
$('#tgDetectStatus').textContent = 'Detecting...';
|
|
2249
2260
|
$('#tgChatList').innerHTML = '';
|
|
2250
2261
|
try {
|
|
2251
|
-
const
|
|
2252
|
-
const res = await fetch(`/api/telegram/detect${tokenQuery}`, {
|
|
2262
|
+
const res = await fetch(`/api/telegram/detect`, {
|
|
2253
2263
|
method: 'POST',
|
|
2254
2264
|
headers: { 'Content-Type': 'application/json' },
|
|
2255
2265
|
body: JSON.stringify({ botToken }),
|
|
@@ -2286,8 +2296,7 @@
|
|
|
2286
2296
|
const chatId = $('#tgChatId').value.trim();
|
|
2287
2297
|
if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
|
|
2288
2298
|
try {
|
|
2289
|
-
const
|
|
2290
|
-
const res = await fetch(`/api/telegram/test${tokenQuery}`, {
|
|
2299
|
+
const res = await fetch(`/api/telegram/test`, {
|
|
2291
2300
|
method: 'POST',
|
|
2292
2301
|
headers: { 'Content-Type': 'application/json' },
|
|
2293
2302
|
body: JSON.stringify({ botToken, chatId }),
|
|
@@ -2306,8 +2315,7 @@
|
|
|
2306
2315
|
if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
|
|
2307
2316
|
if (botToken.includes('...')) { showTgStatus('Bot Token is masked. Please enter the full token.', false); return; }
|
|
2308
2317
|
try {
|
|
2309
|
-
|
|
2310
|
-
await fetch(`/api/telegram${tokenQuery}`, {
|
|
2318
|
+
await fetch(`/api/telegram`, {
|
|
2311
2319
|
method: 'POST',
|
|
2312
2320
|
headers: { 'Content-Type': 'application/json' },
|
|
2313
2321
|
body: JSON.stringify({ telegram: { botToken, chatId, enabled } }),
|
|
@@ -2335,8 +2343,7 @@
|
|
|
2335
2343
|
document.querySelector('.sessions-panel').classList.add('mobile-active');
|
|
2336
2344
|
}
|
|
2337
2345
|
|
|
2338
|
-
|
|
2339
|
-
connect();
|
|
2346
|
+
checkAuth().then((ok) => (ok ? connect() : showTokenForm()));
|
|
2340
2347
|
})();
|
|
2341
2348
|
</script>
|
|
2342
2349
|
</body>
|