Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

ai-cli-online

Package Overview
Dependencies
Maintainers
1
Versions
42
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ai-cli-online - npm Package Compare versions

Comparing version
3.0.6
to
3.0.12
web/dist/assets/index-BHAsZRWj.js

Sorry, the diff of this file is too big to display

+2
-3

@@ -90,5 +90,4 @@ #!/usr/bin/env bash

NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=${RUN_HOME}
PrivateTmp=true
ProtectSystem=full
PrivateTmp=false

@@ -95,0 +94,0 @@ [Install]

+1
-1
{
"name": "ai-cli-online",
"version": "3.0.6",
"version": "3.0.12",
"description": "AI-Cli Online - Web Terminal for Claude Code via xterm.js + tmux",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -13,3 +13,3 @@ import express from 'express';

import { setupWebSocket, clearWsIntervals } from './websocket.js';
import { isTmuxAvailable, cleanupStaleSessions } from './tmux.js';
import { isTmuxAvailable, cleanupOrphanedProcesses } from './tmux.js';
import { cleanupOldDrafts, cleanupOldAnnotations, closeDb } from './db.js';

@@ -32,3 +32,2 @@ import { safeTokenCompare } from './auth.js';

const MAX_CONNECTIONS = parseInt(process.env.MAX_CONNECTIONS || '10', 10);
const SESSION_TTL_HOURS = parseInt(process.env.SESSION_TTL_HOURS || '24', 10);
const RATE_LIMIT_READ = parseInt(process.env.RATE_LIMIT_READ || '300', 10);

@@ -164,4 +163,10 @@ const RATE_LIMIT_WRITE = parseInt(process.env.RATE_LIMIT_WRITE || '100', 10);

});
// --- Cleanup ---
// --- Startup cleanup ---
try {
await cleanupOrphanedProcesses();
}
catch (e) {
console.error('[startup:orphans]', e);
}
try {
const purged = cleanupOldDrafts(7);

@@ -174,22 +179,24 @@ if (purged > 0)

}
let cleanupTimer = null;
if (SESSION_TTL_HOURS > 0) {
const CLEANUP_INTERVAL = 60 * 60 * 1000;
cleanupTimer = setInterval(() => {
cleanupStaleSessions(SESSION_TTL_HOURS).catch((e) => console.error('[cleanup]', e));
try {
cleanupOldDrafts(7);
}
catch (e) {
console.error('[cleanup:drafts]', e);
}
try {
cleanupOldAnnotations(7);
}
catch (e) {
console.error('[cleanup:annotations]', e);
}
}, CLEANUP_INTERVAL);
console.log(`Session TTL: ${SESSION_TTL_HOURS}h (cleanup every hour)`);
}
const CLEANUP_INTERVAL = 60 * 60 * 1000;
const cleanupTimer = setInterval(async () => {
try {
await cleanupOrphanedProcesses();
}
catch (e) {
console.error('[cleanup:orphans]', e);
}
try {
cleanupOldDrafts(7);
}
catch (e) {
console.error('[cleanup:drafts]', e);
}
try {
cleanupOldAnnotations(7);
}
catch (e) {
console.error('[cleanup:annotations]', e);
}
}, CLEANUP_INTERVAL);
console.log('Sessions persist until manually closed (cleanup every hour)');
// --- Graceful shutdown ---

@@ -196,0 +203,0 @@ const shutdown = () => {

@@ -7,2 +7,3 @@ import { Router } from 'express';

import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { resolveSession } from '../middleware/auth.js';

@@ -12,4 +13,5 @@ import { getCwd } from '../tmux.js';

const router = Router();
// Multer setup for file uploads
const UPLOAD_TMP_DIR = '/tmp/ai-cli-online-uploads';
// Multer setup for file uploads — use server/data/ instead of /tmp to survive tmpfs cleanup
const __files_dirname = dirname(fileURLToPath(import.meta.url));
const UPLOAD_TMP_DIR = join(__files_dirname, '../../data/uploads');
mkdirSync(UPLOAD_TMP_DIR, { recursive: true, mode: 0o700 });

@@ -16,0 +18,0 @@ const upload = multer({

@@ -37,3 +37,3 @@ export declare const TMUX_SOCKET_PATH: string;

export declare function listSessions(token: string): Promise<TmuxSessionInfo[]>;
/** Clean up idle tmux sessions older than the given TTL (hours) */
/** Clean up idle tmux sessions whose last activity exceeds the given TTL (hours) */
export declare function cleanupStaleSessions(ttlHours: number): Promise<void>;

@@ -46,1 +46,13 @@ /** 获取 tmux session 当前活动 pane 的工作目录 */

export declare function isTmuxAvailable(): boolean;
/**
* Clean up orphaned process trees from dead tmux sessions.
*
* With KillMode=process, tmux child processes (bash → claude → plugins) survive
* service restarts. When a tmux session is killed (by cleanup or manually), its
* child processes may keep running as orphans. This function identifies tmux server
* processes in the service cgroup whose sessions no longer exist and kills their
* entire process trees.
*
* Called once at startup.
*/
export declare function cleanupOrphanedProcesses(): Promise<void>;
import { execFile as execFileCb, execFileSync } from 'child_process';
import { promisify } from 'util';
import { createHash } from 'crypto';
import { mkdirSync } from 'fs';
import { mkdirSync, existsSync } from 'fs';
import { join } from 'path';

@@ -150,3 +150,3 @@ const _execFile = promisify(execFileCb);

}
/** Clean up idle tmux sessions older than the given TTL (hours) */
/** Clean up idle tmux sessions whose last activity exceeds the given TTL (hours) */
export async function cleanupStaleSessions(ttlHours) {

@@ -158,3 +158,3 @@ const cutoff = Math.floor(Date.now() / 1000) - ttlHours * 3600;

'-F',
'#{session_name}:#{session_created}:#{session_attached}',
'#{session_name}:#{session_activity}:#{session_attached}',
], { encoding: 'utf-8' });

@@ -173,3 +173,3 @@ const staleNames = [];

continue;
const created = parseInt(rest.slice(secondLastColon + 1), 10);
const lastActivity = parseInt(rest.slice(secondLastColon + 1), 10);
const name = rest.slice(0, secondLastColon);

@@ -180,4 +180,4 @@ if (!name.startsWith('ai-cli-online-'))

continue;
if (created < cutoff) {
console.log(`[tmux] Cleaning up stale session: ${name} (created ${new Date(created * 1000).toISOString()})`);
if (lastActivity < cutoff) {
console.log(`[tmux] Cleaning up stale session: ${name} (last activity ${new Date(lastActivity * 1000).toISOString()})`);
staleNames.push(name);

@@ -198,3 +198,12 @@ }

], { encoding: 'utf-8' });
return stdout.trim();
let cwd = stdout.trim();
// tmux appends " (deleted)" when the CWD directory has been removed (e.g. /tmp after cleanup)
if (cwd.endsWith(' (deleted)')) {
cwd = cwd.slice(0, -' (deleted)'.length);
}
// Fall back to DEFAULT_WORKING_DIR or HOME if the path no longer exists
if (!cwd || !existsSync(cwd)) {
cwd = process.env.DEFAULT_WORKING_DIR || process.env.HOME || '/root';
}
return cwd;
}

@@ -223,1 +232,80 @@ /** 获取 tmux pane 当前运行的命令名称 */

}
/**
* Clean up orphaned process trees from dead tmux sessions.
*
* With KillMode=process, tmux child processes (bash → claude → plugins) survive
* service restarts. When a tmux session is killed (by cleanup or manually), its
* child processes may keep running as orphans. This function identifies tmux server
* processes in the service cgroup whose sessions no longer exist and kills their
* entire process trees.
*
* Called once at startup.
*/
export async function cleanupOrphanedProcesses() {
// Get live session names from the socket-based tmux server
const liveSessions = new Set();
try {
const { stdout } = await tmuxExec([
'list-sessions', '-F', '#{session_name}',
], { encoding: 'utf-8' });
for (const line of stdout.trim().split('\n')) {
if (line)
liveSessions.add(line);
}
}
catch {
// tmux server not running — nothing to clean
}
// Find tmux server processes that belong to ai-cli-online but manage dead sessions.
// These show up as `tmux new-session -d -s <session-name>` in /proc/*/cmdline.
try {
const { stdout } = await _execFile('ps', [
'-eo', 'pid,ppid,args', '--no-headers',
], { encoding: 'utf-8', timeout: EXEC_TIMEOUT });
const orphanPids = [];
for (const line of stdout.trim().split('\n')) {
if (!line)
continue;
const match = line.match(/^\s*(\d+)\s+\d+\s+tmux.*new-session\s+-d\s+-s\s+(ai-cli-online-\S+)/);
if (!match)
continue;
const pid = parseInt(match[1], 10);
const sessionName = match[2];
if (!liveSessions.has(sessionName)) {
orphanPids.push(pid);
console.log(`[cleanup] Found orphaned tmux process tree: PID ${pid}, dead session: ${sessionName}`);
}
}
for (const pid of orphanPids) {
try {
// Kill the entire process group/tree rooted at this tmux server
process.kill(-pid, 'SIGTERM');
}
catch {
// Process group kill failed, try individual kill
try {
process.kill(pid, 'SIGTERM');
}
catch { /* already gone */ }
}
}
if (orphanPids.length > 0) {
// Give processes time to exit gracefully, then force-kill survivors
await new Promise((resolve) => setTimeout(resolve, 2000));
for (const pid of orphanPids) {
try {
process.kill(-pid, 'SIGKILL');
}
catch { /* already gone */ }
try {
process.kill(pid, 'SIGKILL');
}
catch { /* already gone */ }
}
console.log(`[cleanup] Cleaned up ${orphanPids.length} orphaned tmux process tree(s)`);
}
}
catch (err) {
console.error('[cleanup] Failed to scan for orphaned processes:', err);
}
}
{
"name": "ai-cli-online-server",
"version": "3.0.3",
"version": "3.0.12",
"description": "CLI-Online Backend Server",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

{
"name": "ai-cli-online-shared",
"version": "3.0.3",
"version": "3.0.12",
"description": "Shared types for CLI-Online",

@@ -5,0 +5,0 @@ "type": "module",

@@ -13,3 +13,3 @@ <!DOCTYPE html>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-webfont@1.7.0/lxgwwenkaimono-bold.css" />
<script type="module" crossorigin src="/assets/index-D4ZKzY3K.js"></script>
<script type="module" crossorigin src="/assets/index-BHAsZRWj.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-vendor-BCIvbQoU.js">

@@ -16,0 +16,0 @@ <link rel="modulepreload" crossorigin href="/assets/terminal-DnNpv9tw.js">

{
"name": "ai-cli-online-web",
"version": "3.0.3",
"version": "3.0.12",
"description": "CLI-Online Web Frontend",

@@ -5,0 +5,0 @@ "type": "module",

Sorry, the diff of this file is too big to display