aegis-bridge 2.15.1 → 2.15.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/dist/api-contracts.d.ts +2 -0
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +67 -0
- package/dist/path-utils.d.ts +11 -0
- package/dist/path-utils.js +21 -0
- package/dist/pipeline.d.ts +11 -0
- package/dist/pipeline.js +38 -0
- package/dist/server.js +3 -2
- package/dist/session.d.ts +3 -0
- package/dist/session.js +4 -1
- package/dist/tmux.js +2 -1
- package/package.json +1 -1
package/dist/api-contracts.d.ts
CHANGED
|
@@ -157,6 +157,7 @@ export interface CreateSessionRequest {
|
|
|
157
157
|
workDir: string;
|
|
158
158
|
name?: string;
|
|
159
159
|
prompt?: string;
|
|
160
|
+
prd?: string;
|
|
160
161
|
resumeSessionId?: string;
|
|
161
162
|
claudeCommand?: string;
|
|
162
163
|
env?: Record<string, string>;
|
|
@@ -182,6 +183,7 @@ export interface SessionSummary {
|
|
|
182
183
|
createdAt: number;
|
|
183
184
|
lastActivity: number;
|
|
184
185
|
permissionMode: string;
|
|
186
|
+
prd?: string;
|
|
185
187
|
}
|
|
186
188
|
export interface OkResponse {
|
|
187
189
|
ok: boolean;
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -57,6 +57,16 @@ interface SessionLatencyResponse {
|
|
|
57
57
|
realtime: SessionLatency | null;
|
|
58
58
|
aggregated: SessionLatencySummary | null;
|
|
59
59
|
}
|
|
60
|
+
interface MemoryEntryResponse {
|
|
61
|
+
entry: {
|
|
62
|
+
key: string;
|
|
63
|
+
value: string;
|
|
64
|
+
namespace: string;
|
|
65
|
+
created_at: number;
|
|
66
|
+
updated_at: number;
|
|
67
|
+
expires_at?: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
60
70
|
export declare class AegisClient {
|
|
61
71
|
private baseUrl;
|
|
62
72
|
private authToken?;
|
|
@@ -103,6 +113,9 @@ export declare class AegisClient {
|
|
|
103
113
|
}>;
|
|
104
114
|
}): Promise<PipelineState>;
|
|
105
115
|
getSwarm(): Promise<Record<string, unknown>>;
|
|
116
|
+
setMemory(key: string, value: string, ttlSeconds?: number): Promise<MemoryEntryResponse>;
|
|
117
|
+
getMemory(key: string): Promise<MemoryEntryResponse>;
|
|
118
|
+
deleteMemory(key: string): Promise<OkResponse>;
|
|
106
119
|
}
|
|
107
120
|
export declare function createMcpServer(aegisPort: number, authToken?: string): McpServer;
|
|
108
121
|
export declare function startMcpServer(port: number, authToken?: string): Promise<void>;
|
package/dist/mcp-server.js
CHANGED
|
@@ -175,6 +175,20 @@ export class AegisClient {
|
|
|
175
175
|
async getSwarm() {
|
|
176
176
|
return this.request('/v1/swarm');
|
|
177
177
|
}
|
|
178
|
+
async setMemory(key, value, ttlSeconds) {
|
|
179
|
+
return this.request('/v1/memory', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
body: JSON.stringify({ key, value, ttlSeconds }),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async getMemory(key) {
|
|
185
|
+
return this.request(`/v1/memory/${encodeURIComponent(key)}`);
|
|
186
|
+
}
|
|
187
|
+
async deleteMemory(key) {
|
|
188
|
+
return this.request(`/v1/memory/${encodeURIComponent(key)}`, {
|
|
189
|
+
method: 'DELETE',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
function formatToolError(e) {
|
|
180
194
|
if (e instanceof Error) {
|
|
@@ -665,6 +679,59 @@ export function createMcpServer(aegisPort, authToken) {
|
|
|
665
679
|
return formatToolError(e);
|
|
666
680
|
}
|
|
667
681
|
});
|
|
682
|
+
// ── state_set ──
|
|
683
|
+
server.tool('state_set', 'Set a shared state key/value entry via Aegis memory bridge.', {
|
|
684
|
+
key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
|
|
685
|
+
value: z.string().describe('State payload as string'),
|
|
686
|
+
ttlSeconds: z.number().int().positive().max(86400 * 30).optional().describe('Optional TTL in seconds (max 30 days)'),
|
|
687
|
+
}, async ({ key, value, ttlSeconds }) => {
|
|
688
|
+
try {
|
|
689
|
+
const result = await client.setMemory(key, value, ttlSeconds);
|
|
690
|
+
return {
|
|
691
|
+
content: [{
|
|
692
|
+
type: 'text',
|
|
693
|
+
text: JSON.stringify(result, null, 2),
|
|
694
|
+
}],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
return formatToolError(e);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// ── state_get ──
|
|
702
|
+
server.tool('state_get', 'Get a shared state key/value entry via Aegis memory bridge.', {
|
|
703
|
+
key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
|
|
704
|
+
}, async ({ key }) => {
|
|
705
|
+
try {
|
|
706
|
+
const result = await client.getMemory(key);
|
|
707
|
+
return {
|
|
708
|
+
content: [{
|
|
709
|
+
type: 'text',
|
|
710
|
+
text: JSON.stringify(result, null, 2),
|
|
711
|
+
}],
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
return formatToolError(e);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
// ── state_delete ──
|
|
719
|
+
server.tool('state_delete', 'Delete a shared state key/value entry via Aegis memory bridge.', {
|
|
720
|
+
key: z.string().describe('State key in namespace/key format (e.g., pipeline/run-123)'),
|
|
721
|
+
}, async ({ key }) => {
|
|
722
|
+
try {
|
|
723
|
+
const result = await client.deleteMemory(key);
|
|
724
|
+
return {
|
|
725
|
+
content: [{
|
|
726
|
+
type: 'text',
|
|
727
|
+
text: JSON.stringify(result, null, 2),
|
|
728
|
+
}],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
return formatToolError(e);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
668
735
|
// ── MCP Prompts (Issue #443) ────────────────────────────────────────
|
|
669
736
|
server.prompt('implement_issue', 'Create a session and generate a structured implementation prompt for a GitHub issue.', {
|
|
670
737
|
issueNumber: z.string().describe('GitHub issue number'),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-utils.ts — path helpers shared across session/tmux logic.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Compute the Claude project hash folder from a workDir path.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - /home/user/project -> -home-user-project
|
|
9
|
+
* - D:\\Users\\me\\project -> -d-Users-me-project
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeProjectHash(workDir: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-utils.ts — path helpers shared across session/tmux logic.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Compute the Claude project hash folder from a workDir path.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - /home/user/project -> -home-user-project
|
|
9
|
+
* - D:\\Users\\me\\project -> -d-Users-me-project
|
|
10
|
+
*/
|
|
11
|
+
export function computeProjectHash(workDir) {
|
|
12
|
+
const normalized = workDir.replace(/\\/g, '/').trim();
|
|
13
|
+
const withLowerDrive = normalized.replace(/^[A-Za-z]:/, (m) => `${m[0].toLowerCase()}`);
|
|
14
|
+
const segments = withLowerDrive
|
|
15
|
+
.split('/')
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((segment) => segment.replace(/:/g, '').replace(/\s+/g, '-'));
|
|
18
|
+
if (segments.length === 0)
|
|
19
|
+
return '-';
|
|
20
|
+
return `-${segments.join('-')}`;
|
|
21
|
+
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -46,7 +46,16 @@ export type PipelineStageStatus = 'pending' | 'running' | 'completed' | 'failed'
|
|
|
46
46
|
export interface PipelineState {
|
|
47
47
|
id: string;
|
|
48
48
|
name: string;
|
|
49
|
+
currentStage: 'plan' | 'execute' | 'verify' | 'fix' | 'submit' | 'done';
|
|
49
50
|
status: 'running' | 'completed' | 'failed';
|
|
51
|
+
retryCount: number;
|
|
52
|
+
maxRetries: number;
|
|
53
|
+
stageHistory: Array<{
|
|
54
|
+
stage: string;
|
|
55
|
+
enteredAt: number;
|
|
56
|
+
exitedAt?: number;
|
|
57
|
+
output?: unknown;
|
|
58
|
+
}>;
|
|
50
59
|
stages: Array<{
|
|
51
60
|
name: string;
|
|
52
61
|
status: PipelineStageStatus;
|
|
@@ -62,6 +71,7 @@ export declare class PipelineManager {
|
|
|
62
71
|
private sessions;
|
|
63
72
|
private eventBus?;
|
|
64
73
|
private static readonly PIPELINE_RETRY_MAX_ATTEMPTS;
|
|
74
|
+
private static readonly PIPELINE_FIX_MAX_RETRIES;
|
|
65
75
|
private pipelines;
|
|
66
76
|
private pipelineConfigs;
|
|
67
77
|
private pollInterval;
|
|
@@ -78,6 +88,7 @@ export declare class PipelineManager {
|
|
|
78
88
|
private advancePipeline;
|
|
79
89
|
/** Poll running pipelines and advance stages. */
|
|
80
90
|
private pollPipelines;
|
|
91
|
+
private transitionPipelineStage;
|
|
81
92
|
/** Detect circular dependencies. Throws if found. */
|
|
82
93
|
private detectCycles;
|
|
83
94
|
/** Clean up. */
|
package/dist/pipeline.js
CHANGED
|
@@ -11,6 +11,7 @@ export class PipelineManager {
|
|
|
11
11
|
sessions;
|
|
12
12
|
eventBus;
|
|
13
13
|
static PIPELINE_RETRY_MAX_ATTEMPTS = 3;
|
|
14
|
+
static PIPELINE_FIX_MAX_RETRIES = 3;
|
|
14
15
|
pipelines = new Map();
|
|
15
16
|
pipelineConfigs = new Map(); // #219: preserve original stage config
|
|
16
17
|
pollInterval = null;
|
|
@@ -72,7 +73,11 @@ export class PipelineManager {
|
|
|
72
73
|
const pipeline = {
|
|
73
74
|
id,
|
|
74
75
|
name: config.name,
|
|
76
|
+
currentStage: 'plan',
|
|
75
77
|
status: 'running',
|
|
78
|
+
retryCount: 0,
|
|
79
|
+
maxRetries: PipelineManager.PIPELINE_FIX_MAX_RETRIES,
|
|
80
|
+
stageHistory: [{ stage: 'plan', enteredAt: Date.now() }],
|
|
76
81
|
stages: config.stages.map(s => ({
|
|
77
82
|
name: s.name,
|
|
78
83
|
status: 'pending',
|
|
@@ -108,11 +113,14 @@ export class PipelineManager {
|
|
|
108
113
|
// If any stage failed, fail the pipeline
|
|
109
114
|
if (failedStages.length > 0) {
|
|
110
115
|
pipeline.status = 'failed';
|
|
116
|
+
this.transitionPipelineStage(pipeline, 'fix', { reason: 'stage_failed', failedStages: failedStages.map(s => s.name) });
|
|
111
117
|
return;
|
|
112
118
|
}
|
|
113
119
|
// Check if all stages are completed
|
|
114
120
|
if (pipeline.stages.every(s => s.status === 'completed')) {
|
|
115
121
|
pipeline.status = 'completed';
|
|
122
|
+
this.transitionPipelineStage(pipeline, 'submit', { reason: 'all_stages_completed' });
|
|
123
|
+
this.transitionPipelineStage(pipeline, 'done', { status: 'completed' });
|
|
116
124
|
if (this.eventBus) {
|
|
117
125
|
this.eventBus.emitEnded(id, 'pipeline_completed');
|
|
118
126
|
}
|
|
@@ -148,13 +156,23 @@ export class PipelineManager {
|
|
|
148
156
|
stage.sessionId = session.id;
|
|
149
157
|
stage.status = 'running';
|
|
150
158
|
stage.startedAt = Date.now();
|
|
159
|
+
this.transitionPipelineStage(pipeline, 'execute', { stage: stage.name, sessionId: session.id });
|
|
151
160
|
}
|
|
152
161
|
catch (e) {
|
|
153
162
|
stage.status = 'failed';
|
|
154
163
|
stage.error = getErrorMessage(e);
|
|
155
164
|
pipeline.status = 'failed';
|
|
165
|
+
this.transitionPipelineStage(pipeline, 'fix', { stage: stage.name, error: stage.error });
|
|
156
166
|
}
|
|
157
167
|
}
|
|
168
|
+
const hasRunning = pipeline.stages.some(s => s.status === 'running');
|
|
169
|
+
const hasPending = pipeline.stages.some(s => s.status === 'pending');
|
|
170
|
+
if (hasRunning) {
|
|
171
|
+
this.transitionPipelineStage(pipeline, 'verify', { runningStages: pipeline.stages.filter(s => s.status === 'running').map(s => s.name) });
|
|
172
|
+
}
|
|
173
|
+
else if (hasPending) {
|
|
174
|
+
this.transitionPipelineStage(pipeline, 'plan', { pendingStages: pipeline.stages.filter(s => s.status === 'pending').map(s => s.name) });
|
|
175
|
+
}
|
|
158
176
|
}
|
|
159
177
|
/** Poll running pipelines and advance stages. */
|
|
160
178
|
async pollPipelines() {
|
|
@@ -184,6 +202,7 @@ export class PipelineManager {
|
|
|
184
202
|
if (session.status === 'idle') {
|
|
185
203
|
stage.status = 'completed';
|
|
186
204
|
stage.completedAt = Date.now();
|
|
205
|
+
this.transitionPipelineStage(pipeline, 'verify', { stageCompleted: stage.name });
|
|
187
206
|
}
|
|
188
207
|
}
|
|
189
208
|
// #219: Use stored original config so stage prompt/permissionMode/autoApprove/workDir are preserved
|
|
@@ -207,6 +226,25 @@ export class PipelineManager {
|
|
|
207
226
|
}
|
|
208
227
|
}
|
|
209
228
|
}
|
|
229
|
+
transitionPipelineStage(pipeline, stage, output) {
|
|
230
|
+
if (pipeline.currentStage === stage)
|
|
231
|
+
return;
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const previous = pipeline.stageHistory[pipeline.stageHistory.length - 1];
|
|
234
|
+
if (previous && previous.exitedAt === undefined) {
|
|
235
|
+
previous.exitedAt = now;
|
|
236
|
+
if (output !== undefined)
|
|
237
|
+
previous.output = output;
|
|
238
|
+
}
|
|
239
|
+
pipeline.currentStage = stage;
|
|
240
|
+
pipeline.stageHistory.push({ stage, enteredAt: now });
|
|
241
|
+
if (stage === 'fix') {
|
|
242
|
+
pipeline.retryCount += 1;
|
|
243
|
+
if (pipeline.retryCount > pipeline.maxRetries) {
|
|
244
|
+
pipeline.status = 'failed';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
210
248
|
/** Detect circular dependencies. Throws if found. */
|
|
211
249
|
detectCycles(stages) {
|
|
212
250
|
const graph = new Map();
|
package/dist/server.js
CHANGED
|
@@ -349,6 +349,7 @@ const createSessionSchema = z.object({
|
|
|
349
349
|
workDir: z.string().min(1),
|
|
350
350
|
name: z.string().max(200).optional(),
|
|
351
351
|
prompt: z.string().max(100_000).optional(),
|
|
352
|
+
prd: z.string().max(100_000).optional(),
|
|
352
353
|
resumeSessionId: z.string().uuid().optional(),
|
|
353
354
|
claudeCommand: z.string().max(10_000).optional(),
|
|
354
355
|
env: z.record(z.string(), z.string()).optional(),
|
|
@@ -680,7 +681,7 @@ async function createSessionHandler(req, reply) {
|
|
|
680
681
|
if (!parsed.success) {
|
|
681
682
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
682
683
|
}
|
|
683
|
-
const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId, memoryKeys } = parsed.data;
|
|
684
|
+
const { workDir, name, prompt, prd, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId, memoryKeys } = parsed.data;
|
|
684
685
|
if (!workDir)
|
|
685
686
|
return reply.status(400).send({ error: 'workDir is required' });
|
|
686
687
|
// Issue #564: Validate installed Claude Code version
|
|
@@ -729,7 +730,7 @@ async function createSessionHandler(req, reply) {
|
|
|
729
730
|
}
|
|
730
731
|
}
|
|
731
732
|
console.time("POST_CREATE_SESSION");
|
|
732
|
-
const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
|
|
733
|
+
const session = await sessions.createSession({ workDir: safeWorkDir, name, prd, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
|
|
733
734
|
console.timeEnd("POST_CREATE_SESSION");
|
|
734
735
|
console.time("POST_CHANNEL_CREATED");
|
|
735
736
|
// Issue #625: Track session in metrics so sessionsCreated counter is accurate
|
package/dist/session.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface SessionInfo {
|
|
|
40
40
|
parentId?: string;
|
|
41
41
|
children?: string[];
|
|
42
42
|
permissionPolicy?: PermissionPolicy;
|
|
43
|
+
prd?: string;
|
|
43
44
|
}
|
|
44
45
|
export interface SessionState {
|
|
45
46
|
sessions: Record<string, SessionInfo>;
|
|
@@ -135,6 +136,7 @@ export declare class SessionManager {
|
|
|
135
136
|
createSession(opts: {
|
|
136
137
|
workDir: string;
|
|
137
138
|
name?: string;
|
|
139
|
+
prd?: string;
|
|
138
140
|
resumeSessionId?: string;
|
|
139
141
|
claudeCommand?: string;
|
|
140
142
|
env?: Record<string, string>;
|
|
@@ -304,6 +306,7 @@ export declare class SessionManager {
|
|
|
304
306
|
createdAt: number;
|
|
305
307
|
lastActivity: number;
|
|
306
308
|
permissionMode: string;
|
|
309
|
+
prd?: string;
|
|
307
310
|
}>;
|
|
308
311
|
/** Paginated transcript read — does NOT advance the session's byteOffset. */
|
|
309
312
|
readTranscript(id: string, page?: number, limit?: number, roleFilter?: 'user' | 'assistant' | 'system'): Promise<{
|
package/dist/session.js
CHANGED
|
@@ -21,6 +21,7 @@ import { PermissionRequestManager } from './permission-request-manager.js';
|
|
|
21
21
|
import { QuestionManager } from './question-manager.js';
|
|
22
22
|
import { Mutex } from 'async-mutex';
|
|
23
23
|
import { maybeInjectFault } from './fault-injection.js';
|
|
24
|
+
import { computeProjectHash } from './path-utils.js';
|
|
24
25
|
/** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
|
|
25
26
|
function hydrateSessions(raw) {
|
|
26
27
|
const sessions = {};
|
|
@@ -562,6 +563,7 @@ export class SessionManager {
|
|
|
562
563
|
settingsPatched,
|
|
563
564
|
hookSettingsFile,
|
|
564
565
|
hookSecret,
|
|
566
|
+
prd: opts.prd,
|
|
565
567
|
};
|
|
566
568
|
this.state.sessions[id] = session;
|
|
567
569
|
this.invalidateSessionsListCache();
|
|
@@ -1207,6 +1209,7 @@ export class SessionManager {
|
|
|
1207
1209
|
createdAt: session.createdAt,
|
|
1208
1210
|
lastActivity: session.lastActivity,
|
|
1209
1211
|
permissionMode: session.permissionMode,
|
|
1212
|
+
prd: session.prd,
|
|
1210
1213
|
};
|
|
1211
1214
|
}
|
|
1212
1215
|
/** Paginated transcript read — does NOT advance the session's byteOffset. */
|
|
@@ -1400,7 +1403,7 @@ export class SessionManager {
|
|
|
1400
1403
|
}
|
|
1401
1404
|
/** Attempt filesystem-based discovery for a single session poll tick. */
|
|
1402
1405
|
async maybeDiscoverFromFilesystem(session, workDir) {
|
|
1403
|
-
const projectHash =
|
|
1406
|
+
const projectHash = computeProjectHash(workDir);
|
|
1404
1407
|
const projectDir = join(this.config.claudeProjectsDir, projectHash);
|
|
1405
1408
|
if (!existsSync(projectDir))
|
|
1406
1409
|
return false;
|
package/dist/tmux.js
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { homedir, tmpdir } from 'node:os';
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { computeProjectHash } from './path-utils.js';
|
|
14
15
|
/** Shell-escape a string by wrapping in single quotes and escaping embedded single quotes. */
|
|
15
16
|
function shellEscape(s) {
|
|
16
17
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
@@ -360,7 +361,7 @@ export class TmuxManager {
|
|
|
360
361
|
*/
|
|
361
362
|
async archiveStaleSessionFiles(workDir) {
|
|
362
363
|
// Compute the project hash the same way Claude CLI does
|
|
363
|
-
const projectHash =
|
|
364
|
+
const projectHash = computeProjectHash(workDir);
|
|
364
365
|
const projectDir = join(homedir(), '.claude', 'projects', projectHash);
|
|
365
366
|
if (!existsSync(projectDir))
|
|
366
367
|
return;
|