aegis-bridge 2.2.2
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/LICENSE +21 -0
- package/README.md +244 -0
- package/dashboard/dist/assets/index-CijFoeRu.css +32 -0
- package/dashboard/dist/assets/index-QtT4j0ht.js +262 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +219 -0
- package/dist/channels/index.d.ts +8 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/manager.d.ts +39 -0
- package/dist/channels/manager.js +101 -0
- package/dist/channels/telegram-style.d.ts +118 -0
- package/dist/channels/telegram-style.js +203 -0
- package/dist/channels/telegram.d.ts +76 -0
- package/dist/channels/telegram.js +1396 -0
- package/dist/channels/types.d.ts +77 -0
- package/dist/channels/types.js +9 -0
- package/dist/channels/webhook.d.ts +58 -0
- package/dist/channels/webhook.js +162 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +223 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.js +188 -0
- package/dist/dashboard/assets/index-CijFoeRu.css +32 -0
- package/dist/dashboard/assets/index-QtT4j0ht.js +262 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/events.d.ts +86 -0
- package/dist/events.js +258 -0
- package/dist/hook-settings.d.ts +67 -0
- package/dist/hook-settings.js +138 -0
- package/dist/hook.d.ts +18 -0
- package/dist/hook.js +199 -0
- package/dist/hooks.d.ts +32 -0
- package/dist/hooks.js +279 -0
- package/dist/jsonl-watcher.d.ts +57 -0
- package/dist/jsonl-watcher.js +159 -0
- package/dist/mcp-server.d.ts +60 -0
- package/dist/mcp-server.js +788 -0
- package/dist/metrics.d.ts +104 -0
- package/dist/metrics.js +226 -0
- package/dist/monitor.d.ts +84 -0
- package/dist/monitor.js +553 -0
- package/dist/permission-guard.d.ts +51 -0
- package/dist/permission-guard.js +197 -0
- package/dist/pipeline.d.ts +84 -0
- package/dist/pipeline.js +218 -0
- package/dist/screenshot.d.ts +26 -0
- package/dist/screenshot.js +57 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +1577 -0
- package/dist/session.d.ts +297 -0
- package/dist/session.js +1275 -0
- package/dist/sse-limiter.d.ts +47 -0
- package/dist/sse-limiter.js +62 -0
- package/dist/sse-writer.d.ts +31 -0
- package/dist/sse-writer.js +95 -0
- package/dist/ssrf.d.ts +57 -0
- package/dist/ssrf.js +169 -0
- package/dist/swarm-monitor.d.ts +114 -0
- package/dist/swarm-monitor.js +267 -0
- package/dist/terminal-parser.d.ts +16 -0
- package/dist/terminal-parser.js +343 -0
- package/dist/tmux.d.ts +161 -0
- package/dist/tmux.js +725 -0
- package/dist/transcript.d.ts +47 -0
- package/dist/transcript.js +244 -0
- package/dist/validation.d.ts +222 -0
- package/dist/validation.js +268 -0
- package/dist/ws-terminal.d.ts +32 -0
- package/dist/ws-terminal.js +297 -0
- package/package.json +71 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permission-guard.ts — Guard against settings overriding CLI permission mode.
|
|
3
|
+
*
|
|
4
|
+
* Problem: Claude Code checks settings in 3 locations (priority order):
|
|
5
|
+
* 1. ~/.claude/settings.json (user-level)
|
|
6
|
+
* 2. <project>/.claude/settings.json (project-level, committed)
|
|
7
|
+
* 3. <project>/.claude/settings.local.json (project-level, local)
|
|
8
|
+
*
|
|
9
|
+
* Any of these can set `permissions.defaultMode: "bypassPermissions"` which
|
|
10
|
+
* OVERRIDES the CLI `--permission-mode default` flag. When Aegis spawns a
|
|
11
|
+
* session with autoApprove: false, the user expects permission prompts — but
|
|
12
|
+
* the settings silently bypass them.
|
|
13
|
+
*
|
|
14
|
+
* Fix: Before launching CC, if autoApprove is false, we neutralize any
|
|
15
|
+
* `bypassPermissions` in ALL 3 settings files by backing them up and patching
|
|
16
|
+
* the permission mode. On session cleanup we restore them.
|
|
17
|
+
*
|
|
18
|
+
* Issue #102 safety: Backups are stored in ~/.aegis/permission-backups/
|
|
19
|
+
* instead of the project directory to prevent accidental commit of secrets.
|
|
20
|
+
*/
|
|
21
|
+
import { readFile, writeFile, rename, unlink } from 'node:fs/promises';
|
|
22
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import { ccSettingsSchema } from './validation.js';
|
|
27
|
+
const SETTINGS_DIR = '.claude';
|
|
28
|
+
const LOCAL_SETTINGS_FILE = 'settings.local.json';
|
|
29
|
+
const PROJECT_SETTINGS_FILE = 'settings.json';
|
|
30
|
+
/** Hash a workDir path to create a safe, unique backup directory name. */
|
|
31
|
+
function workDirHash(workDir) {
|
|
32
|
+
return createHash('sha256').update(workDir).digest('hex').slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
// ─── Path helpers ───
|
|
35
|
+
/** Location 3: project-level local settings */
|
|
36
|
+
export function settingsPath(workDir) {
|
|
37
|
+
return join(workDir, SETTINGS_DIR, LOCAL_SETTINGS_FILE);
|
|
38
|
+
}
|
|
39
|
+
/** Location 2: project-level committed settings */
|
|
40
|
+
export function projectSettingsPath(workDir) {
|
|
41
|
+
return join(workDir, SETTINGS_DIR, PROJECT_SETTINGS_FILE);
|
|
42
|
+
}
|
|
43
|
+
/** Location 1: user-level settings. Accepts optional homeDir for test isolation. */
|
|
44
|
+
export function userSettingsPath(homeDir) {
|
|
45
|
+
return join(homeDir ?? homedir(), SETTINGS_DIR, PROJECT_SETTINGS_FILE);
|
|
46
|
+
}
|
|
47
|
+
/** Backup directory for a given workDir. Accepts optional homeDir for test isolation. */
|
|
48
|
+
export function backupDirForWorkDir(workDir, homeDir) {
|
|
49
|
+
const aegisDir = join(homeDir ?? homedir(), '.aegis');
|
|
50
|
+
return join(aegisDir, 'permission-backups', workDirHash(workDir));
|
|
51
|
+
}
|
|
52
|
+
/** Get backup path for settings.local.json. Accepts optional homeDir for test isolation. */
|
|
53
|
+
export function backupPath(workDir, homeDir) {
|
|
54
|
+
return join(backupDirForWorkDir(workDir, homeDir), LOCAL_SETTINGS_FILE);
|
|
55
|
+
}
|
|
56
|
+
/** Legacy backup path (in project dir) — for migration/cleanup. */
|
|
57
|
+
function legacyBackupPath(workDir) {
|
|
58
|
+
return settingsPath(workDir) + '.aegis-backup';
|
|
59
|
+
}
|
|
60
|
+
/** User-level backup directory. */
|
|
61
|
+
function userBackupDir(homeDir) {
|
|
62
|
+
return join(homeDir, '.aegis', 'permission-backups', '_user_');
|
|
63
|
+
}
|
|
64
|
+
/** All settings locations that could contain bypassPermissions. */
|
|
65
|
+
function getAllSettingsLocations(workDir, homeDir) {
|
|
66
|
+
return [
|
|
67
|
+
// Location 1: user-level
|
|
68
|
+
{ filePath: userSettingsPath(homeDir), backupPath: join(userBackupDir(homeDir), PROJECT_SETTINGS_FILE) },
|
|
69
|
+
// Location 2: project-level committed
|
|
70
|
+
{ filePath: projectSettingsPath(workDir), backupPath: join(backupDirForWorkDir(workDir, homeDir), PROJECT_SETTINGS_FILE) },
|
|
71
|
+
// Location 3: project-level local
|
|
72
|
+
{ filePath: settingsPath(workDir), backupPath: backupPath(workDir, homeDir) },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check a single settings file for bypassPermissions, back it up, and patch it.
|
|
77
|
+
* Returns true if the file was patched.
|
|
78
|
+
*/
|
|
79
|
+
async function neutralizeOneFile(filePath, bpPath, targetMode) {
|
|
80
|
+
if (!existsSync(filePath))
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
84
|
+
const settingsParsed = ccSettingsSchema.safeParse(JSON.parse(raw));
|
|
85
|
+
if (!settingsParsed.success)
|
|
86
|
+
return false;
|
|
87
|
+
const settings = settingsParsed.data;
|
|
88
|
+
const mode = settings?.permissions?.defaultMode;
|
|
89
|
+
if (mode !== 'bypassPermissions')
|
|
90
|
+
return false;
|
|
91
|
+
// Back up
|
|
92
|
+
mkdirSync(join(bpPath, '..'), { recursive: true });
|
|
93
|
+
await writeFile(bpPath, raw);
|
|
94
|
+
// Patch
|
|
95
|
+
if (!settings.permissions)
|
|
96
|
+
settings.permissions = {};
|
|
97
|
+
settings.permissions.defaultMode = targetMode;
|
|
98
|
+
await writeFile(filePath, JSON.stringify(settings, null, 2) + '\n');
|
|
99
|
+
console.log(`Permission guard: neutralized bypassPermissions in ${filePath} (backup: ${bpPath})`);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error(`Permission guard: failed to neutralize ${filePath}: ${e.message}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check all 3 CC settings locations for bypassPermissions. Back up and patch
|
|
109
|
+
* any that have it. Returns true if ANY file was patched.
|
|
110
|
+
*
|
|
111
|
+
* @param homeDir - Override home directory (for test isolation). Defaults to os.homedir().
|
|
112
|
+
*/
|
|
113
|
+
export async function neutralizeBypassPermissions(workDir, targetMode = 'default', homeDir) {
|
|
114
|
+
const locations = getAllSettingsLocations(workDir, homeDir ?? homedir());
|
|
115
|
+
let anyPatched = false;
|
|
116
|
+
for (const loc of locations) {
|
|
117
|
+
const patched = await neutralizeOneFile(loc.filePath, loc.backupPath, targetMode);
|
|
118
|
+
if (patched)
|
|
119
|
+
anyPatched = true;
|
|
120
|
+
}
|
|
121
|
+
return anyPatched;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Restore a single settings file from its backup.
|
|
125
|
+
*/
|
|
126
|
+
async function restoreOneFile(filePath, bpPath) {
|
|
127
|
+
if (!existsSync(bpPath))
|
|
128
|
+
return false;
|
|
129
|
+
try {
|
|
130
|
+
const raw = await readFile(bpPath, 'utf-8');
|
|
131
|
+
await writeFile(filePath, raw);
|
|
132
|
+
await unlink(bpPath);
|
|
133
|
+
console.log(`Permission guard: restored ${filePath} from backup`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(`Permission guard: failed to restore from ${bpPath}: ${e.message}`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Restore all 3 settings files from backups.
|
|
143
|
+
* Checks new backup locations first, then legacy location for backward compat.
|
|
144
|
+
*
|
|
145
|
+
* @param homeDir - Override home directory (for test isolation). Defaults to os.homedir().
|
|
146
|
+
*/
|
|
147
|
+
export async function restoreSettings(workDir, homeDir) {
|
|
148
|
+
const locations = getAllSettingsLocations(workDir, homeDir ?? homedir());
|
|
149
|
+
for (const loc of locations) {
|
|
150
|
+
await restoreOneFile(loc.filePath, loc.backupPath);
|
|
151
|
+
}
|
|
152
|
+
// Legacy fallback for settings.local.json only (in project dir)
|
|
153
|
+
const legacy = legacyBackupPath(workDir);
|
|
154
|
+
if (existsSync(legacy)) {
|
|
155
|
+
try {
|
|
156
|
+
await rename(legacy, settingsPath(workDir));
|
|
157
|
+
console.log(`Permission guard: restored ${settingsPath(workDir)} from legacy backup`);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
console.error(`Permission guard: failed to restore from legacy ${legacy}: ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Clean up any orphaned backups (e.g. from a crash).
|
|
166
|
+
* Restores all 3 settings files from their backups.
|
|
167
|
+
*
|
|
168
|
+
* @param homeDir - Override home directory (for test isolation). Defaults to os.homedir().
|
|
169
|
+
*/
|
|
170
|
+
export async function cleanOrphanedBackup(workDir, homeDir) {
|
|
171
|
+
const locations = getAllSettingsLocations(workDir, homeDir ?? homedir());
|
|
172
|
+
for (const loc of locations) {
|
|
173
|
+
if (!existsSync(loc.backupPath))
|
|
174
|
+
continue;
|
|
175
|
+
try {
|
|
176
|
+
const raw = await readFile(loc.backupPath, 'utf-8');
|
|
177
|
+
await writeFile(loc.filePath, raw);
|
|
178
|
+
await unlink(loc.backupPath);
|
|
179
|
+
console.log(`Permission guard: cleaned orphaned backup for ${loc.filePath}`);
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
console.error(`Permission guard: failed to clean orphaned backup: ${e.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Clean legacy backup
|
|
186
|
+
const legacy = legacyBackupPath(workDir);
|
|
187
|
+
if (existsSync(legacy)) {
|
|
188
|
+
try {
|
|
189
|
+
await rename(legacy, settingsPath(workDir));
|
|
190
|
+
console.log(`Permission guard: cleaned legacy orphaned backup in ${workDir}`);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error(`Permission guard: failed to clean legacy orphaned backup: ${e.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=permission-guard.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline.ts — Batch create and pipeline orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Issue #36: Create multiple sessions in parallel, or define
|
|
5
|
+
* sequential pipelines with stage dependencies.
|
|
6
|
+
*/
|
|
7
|
+
import { type SessionManager } from './session.js';
|
|
8
|
+
import { type SessionEventBus } from './events.js';
|
|
9
|
+
export interface BatchSessionSpec {
|
|
10
|
+
name?: string;
|
|
11
|
+
workDir: string;
|
|
12
|
+
prompt?: string;
|
|
13
|
+
permissionMode?: string;
|
|
14
|
+
/** @deprecated Use permissionMode instead. */
|
|
15
|
+
autoApprove?: boolean;
|
|
16
|
+
stallThresholdMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface BatchResult {
|
|
19
|
+
sessions: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
promptDelivery?: {
|
|
23
|
+
delivered: boolean;
|
|
24
|
+
attempts: number;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
created: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
errors: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface PipelineStage {
|
|
32
|
+
name: string;
|
|
33
|
+
workDir?: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
dependsOn?: string[];
|
|
36
|
+
permissionMode?: string;
|
|
37
|
+
/** @deprecated Use permissionMode instead. */
|
|
38
|
+
autoApprove?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface PipelineConfig {
|
|
41
|
+
name: string;
|
|
42
|
+
workDir: string;
|
|
43
|
+
stages: PipelineStage[];
|
|
44
|
+
}
|
|
45
|
+
export type PipelineStageStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
46
|
+
export interface PipelineState {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
status: 'running' | 'completed' | 'failed';
|
|
50
|
+
stages: Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
status: PipelineStageStatus;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
dependsOn: string[];
|
|
55
|
+
startedAt?: number;
|
|
56
|
+
completedAt?: number;
|
|
57
|
+
error?: string;
|
|
58
|
+
}>;
|
|
59
|
+
createdAt: number;
|
|
60
|
+
}
|
|
61
|
+
export declare class PipelineManager {
|
|
62
|
+
private sessions;
|
|
63
|
+
private eventBus?;
|
|
64
|
+
private pipelines;
|
|
65
|
+
private pipelineConfigs;
|
|
66
|
+
private pollInterval;
|
|
67
|
+
constructor(sessions: SessionManager, eventBus?: SessionEventBus | undefined);
|
|
68
|
+
/** Create multiple sessions in parallel. */
|
|
69
|
+
batchCreate(specs: BatchSessionSpec[]): Promise<BatchResult>;
|
|
70
|
+
/** Create a pipeline with stage dependencies. */
|
|
71
|
+
createPipeline(config: PipelineConfig): Promise<PipelineState>;
|
|
72
|
+
/** Get pipeline state. */
|
|
73
|
+
getPipeline(id: string): PipelineState | null;
|
|
74
|
+
/** List all pipelines. */
|
|
75
|
+
listPipelines(): PipelineState[];
|
|
76
|
+
/** Advance a pipeline: start stages whose dependencies are met. */
|
|
77
|
+
private advancePipeline;
|
|
78
|
+
/** Poll running pipelines and advance stages. */
|
|
79
|
+
private pollPipelines;
|
|
80
|
+
/** Detect circular dependencies. Throws if found. */
|
|
81
|
+
private detectCycles;
|
|
82
|
+
/** Clean up. */
|
|
83
|
+
destroy(): void;
|
|
84
|
+
}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline.ts — Batch create and pipeline orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Issue #36: Create multiple sessions in parallel, or define
|
|
5
|
+
* sequential pipelines with stage dependencies.
|
|
6
|
+
*/
|
|
7
|
+
import { getErrorMessage } from './validation.js';
|
|
8
|
+
export class PipelineManager {
|
|
9
|
+
sessions;
|
|
10
|
+
eventBus;
|
|
11
|
+
pipelines = new Map();
|
|
12
|
+
pipelineConfigs = new Map(); // #219: preserve original stage config
|
|
13
|
+
pollInterval = null;
|
|
14
|
+
constructor(sessions, eventBus) {
|
|
15
|
+
this.sessions = sessions;
|
|
16
|
+
this.eventBus = eventBus;
|
|
17
|
+
}
|
|
18
|
+
/** Create multiple sessions in parallel. */
|
|
19
|
+
async batchCreate(specs) {
|
|
20
|
+
const results = await Promise.allSettled(specs.map(async (spec) => {
|
|
21
|
+
const session = await this.sessions.createSession({
|
|
22
|
+
workDir: spec.workDir,
|
|
23
|
+
name: spec.name,
|
|
24
|
+
permissionMode: spec.permissionMode,
|
|
25
|
+
autoApprove: spec.autoApprove,
|
|
26
|
+
stallThresholdMs: spec.stallThresholdMs,
|
|
27
|
+
});
|
|
28
|
+
let promptDelivery;
|
|
29
|
+
if (spec.prompt) {
|
|
30
|
+
promptDelivery = await this.sessions.sendInitialPrompt(session.id, spec.prompt);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
id: session.id,
|
|
34
|
+
name: session.windowName,
|
|
35
|
+
promptDelivery,
|
|
36
|
+
};
|
|
37
|
+
}));
|
|
38
|
+
const sessions = [];
|
|
39
|
+
const errors = [];
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
if (result.status === 'fulfilled') {
|
|
42
|
+
sessions.push(result.value);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
errors.push(result.reason?.message || 'Unknown error');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
sessions,
|
|
50
|
+
created: sessions.length,
|
|
51
|
+
failed: errors.length,
|
|
52
|
+
errors,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Create a pipeline with stage dependencies. */
|
|
56
|
+
async createPipeline(config) {
|
|
57
|
+
const id = crypto.randomUUID();
|
|
58
|
+
// Validate: all dependsOn references must exist as stage names
|
|
59
|
+
const stageNames = new Set(config.stages.map(s => s.name));
|
|
60
|
+
for (const stage of config.stages) {
|
|
61
|
+
for (const dep of stage.dependsOn || []) {
|
|
62
|
+
if (!stageNames.has(dep)) {
|
|
63
|
+
throw new Error(`Stage "${stage.name}" depends on unknown stage "${dep}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Check for circular dependencies
|
|
68
|
+
this.detectCycles(config.stages);
|
|
69
|
+
const pipeline = {
|
|
70
|
+
id,
|
|
71
|
+
name: config.name,
|
|
72
|
+
status: 'running',
|
|
73
|
+
stages: config.stages.map(s => ({
|
|
74
|
+
name: s.name,
|
|
75
|
+
status: 'pending',
|
|
76
|
+
dependsOn: s.dependsOn || [],
|
|
77
|
+
})),
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
this.pipelines.set(id, pipeline);
|
|
81
|
+
this.pipelineConfigs.set(id, config); // #219: store original config for polling
|
|
82
|
+
// Start stages with no dependencies immediately
|
|
83
|
+
await this.advancePipeline(id, config);
|
|
84
|
+
// Start polling for stage completion
|
|
85
|
+
if (!this.pollInterval) {
|
|
86
|
+
this.pollInterval = setInterval(() => this.pollPipelines(), 5000);
|
|
87
|
+
}
|
|
88
|
+
return pipeline;
|
|
89
|
+
}
|
|
90
|
+
/** Get pipeline state. */
|
|
91
|
+
getPipeline(id) {
|
|
92
|
+
return this.pipelines.get(id) || null;
|
|
93
|
+
}
|
|
94
|
+
/** List all pipelines. */
|
|
95
|
+
listPipelines() {
|
|
96
|
+
return Array.from(this.pipelines.values());
|
|
97
|
+
}
|
|
98
|
+
/** Advance a pipeline: start stages whose dependencies are met. */
|
|
99
|
+
async advancePipeline(id, config) {
|
|
100
|
+
const pipeline = this.pipelines.get(id);
|
|
101
|
+
if (!pipeline || pipeline.status !== 'running')
|
|
102
|
+
return;
|
|
103
|
+
const completedStages = new Set(pipeline.stages.filter(s => s.status === 'completed').map(s => s.name));
|
|
104
|
+
const failedStages = pipeline.stages.filter(s => s.status === 'failed');
|
|
105
|
+
// If any stage failed, fail the pipeline
|
|
106
|
+
if (failedStages.length > 0) {
|
|
107
|
+
pipeline.status = 'failed';
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Check if all stages are completed
|
|
111
|
+
if (pipeline.stages.every(s => s.status === 'completed')) {
|
|
112
|
+
pipeline.status = 'completed';
|
|
113
|
+
if (this.eventBus) {
|
|
114
|
+
this.eventBus.emitEnded(id, 'pipeline_completed');
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Start pending stages whose dependencies are all completed
|
|
119
|
+
for (const stage of pipeline.stages) {
|
|
120
|
+
if (stage.status !== 'pending')
|
|
121
|
+
continue;
|
|
122
|
+
const depsComplete = stage.dependsOn.every(d => completedStages.has(d));
|
|
123
|
+
if (!depsComplete)
|
|
124
|
+
continue;
|
|
125
|
+
// Find matching config stage
|
|
126
|
+
const stageConfig = config.stages.find(s => s.name === stage.name);
|
|
127
|
+
if (!stageConfig)
|
|
128
|
+
continue;
|
|
129
|
+
try {
|
|
130
|
+
const session = await this.sessions.createSession({
|
|
131
|
+
workDir: stageConfig.workDir || config.workDir,
|
|
132
|
+
name: `pipeline-${config.name}-${stage.name}`,
|
|
133
|
+
permissionMode: stageConfig.permissionMode,
|
|
134
|
+
autoApprove: stageConfig.autoApprove,
|
|
135
|
+
});
|
|
136
|
+
if (stageConfig.prompt) {
|
|
137
|
+
await this.sessions.sendInitialPrompt(session.id, stageConfig.prompt);
|
|
138
|
+
}
|
|
139
|
+
stage.sessionId = session.id;
|
|
140
|
+
stage.status = 'running';
|
|
141
|
+
stage.startedAt = Date.now();
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
stage.status = 'failed';
|
|
145
|
+
stage.error = getErrorMessage(e);
|
|
146
|
+
pipeline.status = 'failed';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Poll running pipelines and advance stages. */
|
|
151
|
+
async pollPipelines() {
|
|
152
|
+
for (const [id, pipeline] of this.pipelines) {
|
|
153
|
+
if (pipeline.status !== 'running')
|
|
154
|
+
continue;
|
|
155
|
+
// Check running stages for completion (idle status = done)
|
|
156
|
+
for (const stage of pipeline.stages) {
|
|
157
|
+
if (stage.status !== 'running' || !stage.sessionId)
|
|
158
|
+
continue;
|
|
159
|
+
const session = this.sessions.getSession(stage.sessionId);
|
|
160
|
+
if (!session) {
|
|
161
|
+
stage.status = 'failed';
|
|
162
|
+
stage.error = 'Session disappeared';
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (session.status === 'idle') {
|
|
166
|
+
stage.status = 'completed';
|
|
167
|
+
stage.completedAt = Date.now();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// #219: Use stored original config so stage prompt/permissionMode/autoApprove/workDir are preserved
|
|
171
|
+
const storedConfig = this.pipelineConfigs.get(id);
|
|
172
|
+
if (storedConfig) {
|
|
173
|
+
await this.advancePipeline(id, storedConfig);
|
|
174
|
+
}
|
|
175
|
+
// #221: Clean up completed/failed pipelines after 30s to avoid memory leak
|
|
176
|
+
// Note: advancePipeline may change status from 'running' to 'completed'/'failed'
|
|
177
|
+
if (pipeline.status !== 'running') {
|
|
178
|
+
const pipelineId = id;
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
this.pipelines.delete(pipelineId);
|
|
181
|
+
this.pipelineConfigs.delete(pipelineId); // #219: clean up stored config
|
|
182
|
+
}, 30_000);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Detect circular dependencies. Throws if found. */
|
|
187
|
+
detectCycles(stages) {
|
|
188
|
+
const graph = new Map();
|
|
189
|
+
for (const stage of stages) {
|
|
190
|
+
graph.set(stage.name, stage.dependsOn || []);
|
|
191
|
+
}
|
|
192
|
+
const visited = new Set();
|
|
193
|
+
const inStack = new Set();
|
|
194
|
+
const dfs = (node) => {
|
|
195
|
+
if (inStack.has(node))
|
|
196
|
+
throw new Error(`Circular dependency detected involving stage "${node}"`);
|
|
197
|
+
if (visited.has(node))
|
|
198
|
+
return;
|
|
199
|
+
inStack.add(node);
|
|
200
|
+
visited.add(node);
|
|
201
|
+
for (const dep of graph.get(node) || []) {
|
|
202
|
+
dfs(dep);
|
|
203
|
+
}
|
|
204
|
+
inStack.delete(node);
|
|
205
|
+
};
|
|
206
|
+
for (const stage of stages) {
|
|
207
|
+
dfs(stage.name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Clean up. */
|
|
211
|
+
destroy() {
|
|
212
|
+
if (this.pollInterval) {
|
|
213
|
+
clearInterval(this.pollInterval);
|
|
214
|
+
this.pollInterval = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot.ts — Headless screenshot capture via Playwright.
|
|
3
|
+
*
|
|
4
|
+
* Issue #22: Visual verification for CC sessions.
|
|
5
|
+
* Uses Playwright if available; returns 501 Not Implemented otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export interface ScreenshotOptions {
|
|
8
|
+
url: string;
|
|
9
|
+
fullPage?: boolean;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ScreenshotResult {
|
|
14
|
+
screenshot: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
url: string;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Capture a screenshot of the given URL using headless Chromium.
|
|
22
|
+
* Returns the result or throws if Playwright is not available.
|
|
23
|
+
*/
|
|
24
|
+
export declare function captureScreenshot(opts: ScreenshotOptions): Promise<ScreenshotResult>;
|
|
25
|
+
/** Check if Playwright is available for screenshot capture. */
|
|
26
|
+
export declare function isPlaywrightAvailable(): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot.ts — Headless screenshot capture via Playwright.
|
|
3
|
+
*
|
|
4
|
+
* Issue #22: Visual verification for CC sessions.
|
|
5
|
+
* Uses Playwright if available; returns 501 Not Implemented otherwise.
|
|
6
|
+
*/
|
|
7
|
+
let playwrightAvailable = false;
|
|
8
|
+
let chromium = null;
|
|
9
|
+
// Lazy-load Playwright — only fails at startup, not import time
|
|
10
|
+
try {
|
|
11
|
+
const pw = await import('playwright');
|
|
12
|
+
chromium = pw.chromium;
|
|
13
|
+
playwrightAvailable = true;
|
|
14
|
+
}
|
|
15
|
+
catch { /* playwright not installed — screenshot feature disabled */
|
|
16
|
+
playwrightAvailable = false;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Capture a screenshot of the given URL using headless Chromium.
|
|
20
|
+
* Returns the result or throws if Playwright is not available.
|
|
21
|
+
*/
|
|
22
|
+
export async function captureScreenshot(opts) {
|
|
23
|
+
if (!playwrightAvailable || !chromium) {
|
|
24
|
+
throw new Error('Playwright is not installed. Install it with: npx playwright install chromium && npm install -D playwright');
|
|
25
|
+
}
|
|
26
|
+
const browser = await chromium.launch({ headless: true });
|
|
27
|
+
try {
|
|
28
|
+
const context = await browser.newContext({
|
|
29
|
+
viewport: {
|
|
30
|
+
width: opts.width || 1280,
|
|
31
|
+
height: opts.height || 720,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const page = await context.newPage();
|
|
35
|
+
await page.goto(opts.url, { waitUntil: 'load', timeout: 30_000 });
|
|
36
|
+
const buffer = await page.screenshot({
|
|
37
|
+
fullPage: opts.fullPage || false,
|
|
38
|
+
type: 'png',
|
|
39
|
+
});
|
|
40
|
+
await context.close();
|
|
41
|
+
return {
|
|
42
|
+
screenshot: buffer.toString('base64'),
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
url: opts.url,
|
|
45
|
+
width: opts.width || 1280,
|
|
46
|
+
height: opts.height || 720,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await browser.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Check if Playwright is available for screenshot capture. */
|
|
54
|
+
export function isPlaywrightAvailable() {
|
|
55
|
+
return playwrightAvailable;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=screenshot.js.map
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server.ts — HTTP API server for Aegis.
|
|
3
|
+
*
|
|
4
|
+
* Exposes RESTful endpoints for creating, managing, and interacting
|
|
5
|
+
* with Claude Code sessions running in tmux.
|
|
6
|
+
*
|
|
7
|
+
* Notification channels (Telegram, webhooks, etc.) are pluggable —
|
|
8
|
+
* the server doesn't know which channels are active.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|