@love-moon/conductor-sdk 0.1.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/dist/backend/client.d.ts +62 -0
- package/dist/backend/client.js +207 -0
- package/dist/backend/index.d.ts +1 -0
- package/dist/backend/index.js +1 -0
- package/dist/bin/mcp-server.d.ts +2 -0
- package/dist/bin/mcp-server.js +175 -0
- package/dist/config/index.d.ts +33 -0
- package/dist/config/index.js +152 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/context/project_context.d.ts +14 -0
- package/dist/context/project_context.js +92 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/notifications.d.ts +20 -0
- package/dist/mcp/notifications.js +44 -0
- package/dist/mcp/server.d.ts +37 -0
- package/dist/mcp/server.js +211 -0
- package/dist/message/index.d.ts +1 -0
- package/dist/message/index.js +1 -0
- package/dist/message/router.d.ts +19 -0
- package/dist/message/router.js +122 -0
- package/dist/orchestrator.d.ts +21 -0
- package/dist/orchestrator.js +20 -0
- package/dist/reporter/event_stream.d.ts +7 -0
- package/dist/reporter/event_stream.js +20 -0
- package/dist/reporter/index.d.ts +1 -0
- package/dist/reporter/index.js +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/manager.d.ts +39 -0
- package/dist/session/manager.js +162 -0
- package/dist/session/store.d.ts +36 -0
- package/dist/session/store.js +147 -0
- package/dist/ws/client.d.ts +49 -0
- package/dist/ws/client.js +296 -0
- package/dist/ws/index.d.ts +1 -0
- package/dist/ws/index.js +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface MessageInput {
|
|
2
|
+
messageId: string;
|
|
3
|
+
role: string;
|
|
4
|
+
content: string;
|
|
5
|
+
ackToken?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare class MessageRecord {
|
|
8
|
+
readonly messageId: string;
|
|
9
|
+
readonly role: string;
|
|
10
|
+
readonly content: string;
|
|
11
|
+
readonly createdAt: Date;
|
|
12
|
+
readonly ackToken?: string | null;
|
|
13
|
+
constructor(init: MessageInput);
|
|
14
|
+
}
|
|
15
|
+
export declare class SessionState {
|
|
16
|
+
readonly taskId: string;
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly projectId: string;
|
|
19
|
+
readonly createdAt: Date;
|
|
20
|
+
lastMessageAt: Date;
|
|
21
|
+
status: string;
|
|
22
|
+
pendingMessages: MessageRecord[];
|
|
23
|
+
ackToken: string | null;
|
|
24
|
+
constructor(taskId: string, sessionId: string, projectId: string, createdAt?: Date);
|
|
25
|
+
}
|
|
26
|
+
export declare class SessionManager {
|
|
27
|
+
private readonly sessions;
|
|
28
|
+
private readonly lock;
|
|
29
|
+
private readonly messageEvents;
|
|
30
|
+
addSession(taskId: string, sessionId: string, projectId: string): Promise<SessionState>;
|
|
31
|
+
getSession(taskId: string): Promise<SessionState | undefined>;
|
|
32
|
+
addMessage(taskId: string, message: MessageInput): Promise<void>;
|
|
33
|
+
popMessages(taskId: string, limit?: number): Promise<MessageRecord[]>;
|
|
34
|
+
waitForMessages(taskId: string, limit?: number): Promise<MessageRecord[]>;
|
|
35
|
+
ack(taskId: string, ackToken: string): Promise<boolean>;
|
|
36
|
+
listSessions(): Promise<SessionState[]>;
|
|
37
|
+
endSession(taskId: string): Promise<void>;
|
|
38
|
+
private ensureEvent;
|
|
39
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
export class MessageRecord {
|
|
3
|
+
messageId;
|
|
4
|
+
role;
|
|
5
|
+
content;
|
|
6
|
+
createdAt;
|
|
7
|
+
ackToken;
|
|
8
|
+
constructor(init) {
|
|
9
|
+
this.messageId = init.messageId;
|
|
10
|
+
this.role = init.role;
|
|
11
|
+
this.content = init.content;
|
|
12
|
+
this.ackToken = init.ackToken;
|
|
13
|
+
this.createdAt = new Date();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class SessionState {
|
|
17
|
+
taskId;
|
|
18
|
+
sessionId;
|
|
19
|
+
projectId;
|
|
20
|
+
createdAt;
|
|
21
|
+
lastMessageAt;
|
|
22
|
+
status = 'ACTIVE';
|
|
23
|
+
pendingMessages = [];
|
|
24
|
+
ackToken = null;
|
|
25
|
+
constructor(taskId, sessionId, projectId, createdAt = new Date()) {
|
|
26
|
+
this.taskId = taskId;
|
|
27
|
+
this.sessionId = sessionId;
|
|
28
|
+
this.projectId = projectId;
|
|
29
|
+
this.createdAt = createdAt;
|
|
30
|
+
this.lastMessageAt = createdAt;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class AsyncLock {
|
|
34
|
+
emitter = new EventEmitter();
|
|
35
|
+
tail = Promise.resolve();
|
|
36
|
+
async runExclusive(fn) {
|
|
37
|
+
const run = this.tail.then(fn, fn);
|
|
38
|
+
this.tail = run
|
|
39
|
+
.then(() => undefined)
|
|
40
|
+
.catch(() => undefined);
|
|
41
|
+
return run;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class MessageEvent {
|
|
45
|
+
waiter;
|
|
46
|
+
resolver = null;
|
|
47
|
+
constructor() {
|
|
48
|
+
this.waiter = this.reset();
|
|
49
|
+
}
|
|
50
|
+
async wait() {
|
|
51
|
+
await this.waiter;
|
|
52
|
+
}
|
|
53
|
+
signal() {
|
|
54
|
+
this.resolver?.();
|
|
55
|
+
this.waiter = this.reset();
|
|
56
|
+
}
|
|
57
|
+
clear() {
|
|
58
|
+
this.waiter = this.reset();
|
|
59
|
+
}
|
|
60
|
+
reset() {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
this.resolver = resolve;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class SessionManager {
|
|
67
|
+
sessions = new Map();
|
|
68
|
+
lock = new AsyncLock();
|
|
69
|
+
messageEvents = new Map();
|
|
70
|
+
async addSession(taskId, sessionId, projectId) {
|
|
71
|
+
return this.lock.runExclusive(async () => {
|
|
72
|
+
const state = new SessionState(taskId, sessionId, projectId);
|
|
73
|
+
this.sessions.set(taskId, state);
|
|
74
|
+
this.messageEvents.set(taskId, new MessageEvent());
|
|
75
|
+
return state;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async getSession(taskId) {
|
|
79
|
+
return this.lock.runExclusive(async () => this.sessions.get(taskId));
|
|
80
|
+
}
|
|
81
|
+
async addMessage(taskId, message) {
|
|
82
|
+
await this.lock.runExclusive(async () => {
|
|
83
|
+
const session = this.sessions.get(taskId);
|
|
84
|
+
if (!session) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const record = new MessageRecord(message);
|
|
88
|
+
session.pendingMessages.push(record);
|
|
89
|
+
session.lastMessageAt = record.createdAt;
|
|
90
|
+
this.ensureEvent(taskId).signal();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async popMessages(taskId, limit = 20) {
|
|
94
|
+
return this.lock.runExclusive(async () => {
|
|
95
|
+
const session = this.sessions.get(taskId);
|
|
96
|
+
if (!session) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const items = [];
|
|
100
|
+
while (session.pendingMessages.length && items.length < limit) {
|
|
101
|
+
const next = session.pendingMessages.shift();
|
|
102
|
+
if (next) {
|
|
103
|
+
items.push(next);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (items.length) {
|
|
107
|
+
session.ackToken = items[items.length - 1].ackToken ?? null;
|
|
108
|
+
}
|
|
109
|
+
const event = this.ensureEvent(taskId);
|
|
110
|
+
if (session.pendingMessages.length) {
|
|
111
|
+
event.signal();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
event.clear();
|
|
115
|
+
}
|
|
116
|
+
return items;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async waitForMessages(taskId, limit = 20) {
|
|
120
|
+
const event = this.ensureEvent(taskId);
|
|
121
|
+
// Loop until a batch is available.
|
|
122
|
+
while (true) {
|
|
123
|
+
const messages = await this.popMessages(taskId, limit);
|
|
124
|
+
if (messages.length) {
|
|
125
|
+
return messages;
|
|
126
|
+
}
|
|
127
|
+
await event.wait();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async ack(taskId, ackToken) {
|
|
131
|
+
return this.lock.runExclusive(async () => {
|
|
132
|
+
const session = this.sessions.get(taskId);
|
|
133
|
+
if (!session) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (session.ackToken && session.ackToken === ackToken) {
|
|
137
|
+
session.ackToken = null;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async listSessions() {
|
|
144
|
+
return this.lock.runExclusive(async () => Array.from(this.sessions.values()));
|
|
145
|
+
}
|
|
146
|
+
async endSession(taskId) {
|
|
147
|
+
await this.lock.runExclusive(async () => {
|
|
148
|
+
const session = this.sessions.get(taskId);
|
|
149
|
+
if (session) {
|
|
150
|
+
session.status = 'ENDED';
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
ensureEvent(taskId) {
|
|
155
|
+
let event = this.messageEvents.get(taskId);
|
|
156
|
+
if (!event) {
|
|
157
|
+
event = new MessageEvent();
|
|
158
|
+
this.messageEvents.set(taskId, event);
|
|
159
|
+
}
|
|
160
|
+
return event;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const DEFAULT_SESSION_PATH: string;
|
|
2
|
+
export declare const DEFAULT_SESSION_ENV = "CODEX_SESSION_ID";
|
|
3
|
+
export declare const DEFAULT_SESSION_FALLBACK_ENV = "SESSION_ID";
|
|
4
|
+
export interface SessionRecordInit {
|
|
5
|
+
projectId: string;
|
|
6
|
+
taskIds: string[];
|
|
7
|
+
projectPath: string;
|
|
8
|
+
sessionId?: string | null;
|
|
9
|
+
hostname?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare class SessionRecord {
|
|
12
|
+
projectId: string;
|
|
13
|
+
taskIds: string[];
|
|
14
|
+
projectPath: string;
|
|
15
|
+
sessionId?: string | null;
|
|
16
|
+
hostname?: string | null;
|
|
17
|
+
constructor(init: SessionRecordInit);
|
|
18
|
+
static fromJSON(payload: Record<string, any>): SessionRecord;
|
|
19
|
+
toJSON(): Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export declare class SessionDiskStore {
|
|
22
|
+
private readonly filePath;
|
|
23
|
+
constructor(filePath?: string);
|
|
24
|
+
load(): SessionRecord[];
|
|
25
|
+
save(records: SessionRecord[]): void;
|
|
26
|
+
findByPath(projectPath: string): SessionRecord | undefined;
|
|
27
|
+
upsert(params: {
|
|
28
|
+
projectId: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
projectPath: string;
|
|
31
|
+
sessionId?: string | null;
|
|
32
|
+
hostname?: string | null;
|
|
33
|
+
}): SessionRecord;
|
|
34
|
+
}
|
|
35
|
+
export declare function currentSessionId(env?: Record<string, string | undefined>): string | undefined;
|
|
36
|
+
export declare function currentHostname(): string;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import yaml from 'yaml';
|
|
5
|
+
export const DEFAULT_SESSION_PATH = path.join(os.homedir(), '.conductor', 'session.yaml');
|
|
6
|
+
export const DEFAULT_SESSION_ENV = 'CODEX_SESSION_ID';
|
|
7
|
+
export const DEFAULT_SESSION_FALLBACK_ENV = 'SESSION_ID';
|
|
8
|
+
export class SessionRecord {
|
|
9
|
+
projectId;
|
|
10
|
+
taskIds;
|
|
11
|
+
projectPath;
|
|
12
|
+
sessionId;
|
|
13
|
+
hostname;
|
|
14
|
+
constructor(init) {
|
|
15
|
+
this.projectId = init.projectId;
|
|
16
|
+
this.taskIds = init.taskIds;
|
|
17
|
+
this.projectPath = init.projectPath;
|
|
18
|
+
this.sessionId = init.sessionId ?? null;
|
|
19
|
+
this.hostname = init.hostname ?? null;
|
|
20
|
+
}
|
|
21
|
+
static fromJSON(payload) {
|
|
22
|
+
const projectId = payload.project_id ? String(payload.project_id) : '';
|
|
23
|
+
const projectPath = payload.project_path ? String(payload.project_path) : '';
|
|
24
|
+
if (!projectId || !projectPath) {
|
|
25
|
+
throw new Error('Session record missing project_id or project_path');
|
|
26
|
+
}
|
|
27
|
+
const rawTasks = payload.task_id || payload.task_ids || [];
|
|
28
|
+
const taskIds = Array.isArray(rawTasks)
|
|
29
|
+
? rawTasks.map((task) => String(task)).filter(Boolean)
|
|
30
|
+
: typeof rawTasks === 'string'
|
|
31
|
+
? [rawTasks]
|
|
32
|
+
: [];
|
|
33
|
+
const sessionId = payload.session_id ? String(payload.session_id) : null;
|
|
34
|
+
const hostname = payload.hostname ? String(payload.hostname) : null;
|
|
35
|
+
return new SessionRecord({
|
|
36
|
+
projectId,
|
|
37
|
+
projectPath,
|
|
38
|
+
taskIds,
|
|
39
|
+
sessionId,
|
|
40
|
+
hostname,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
toJSON() {
|
|
44
|
+
return {
|
|
45
|
+
project_id: this.projectId,
|
|
46
|
+
task_id: Array.from(this.taskIds),
|
|
47
|
+
project_path: this.projectPath,
|
|
48
|
+
session_id: this.sessionId,
|
|
49
|
+
hostname: this.hostname,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class SessionDiskStore {
|
|
54
|
+
filePath;
|
|
55
|
+
constructor(filePath = DEFAULT_SESSION_PATH) {
|
|
56
|
+
this.filePath = path.resolve(filePath);
|
|
57
|
+
}
|
|
58
|
+
load() {
|
|
59
|
+
if (!fs.existsSync(this.filePath)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const contents = fs.readFileSync(this.filePath, 'utf-8');
|
|
64
|
+
const parsed = yaml.parse(contents) ?? {};
|
|
65
|
+
const entries = Array.isArray(parsed.sessions) ? parsed.sessions : parsed;
|
|
66
|
+
if (!entries || typeof entries !== 'object') {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const list = [];
|
|
70
|
+
if (Array.isArray(entries)) {
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (entry && typeof entry === 'object') {
|
|
73
|
+
try {
|
|
74
|
+
list.push(SessionRecord.fromJSON(entry));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (Array.isArray(entries.sessions)) {
|
|
83
|
+
for (const entry of entries.sessions) {
|
|
84
|
+
if (entry && typeof entry === 'object') {
|
|
85
|
+
try {
|
|
86
|
+
list.push(SessionRecord.fromJSON(entry));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return list;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
save(records) {
|
|
101
|
+
const payload = {
|
|
102
|
+
sessions: records.map((record) => record.toJSON()),
|
|
103
|
+
};
|
|
104
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
105
|
+
fs.writeFileSync(this.filePath, yaml.stringify(payload), 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
findByPath(projectPath) {
|
|
108
|
+
const normalized = path.resolve(projectPath);
|
|
109
|
+
return this.load().find((record) => path.resolve(record.projectPath) === normalized);
|
|
110
|
+
}
|
|
111
|
+
upsert(params) {
|
|
112
|
+
const normalized = path.resolve(params.projectPath);
|
|
113
|
+
const records = this.load();
|
|
114
|
+
let record = records.find((entry) => path.resolve(entry.projectPath) === normalized);
|
|
115
|
+
if (!record) {
|
|
116
|
+
record = new SessionRecord({
|
|
117
|
+
projectId: params.projectId,
|
|
118
|
+
taskIds: [params.taskId],
|
|
119
|
+
projectPath: normalized,
|
|
120
|
+
sessionId: params.sessionId,
|
|
121
|
+
hostname: params.hostname,
|
|
122
|
+
});
|
|
123
|
+
records.push(record);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
record.projectId = params.projectId;
|
|
127
|
+
record.sessionId = params.sessionId ?? record.sessionId;
|
|
128
|
+
record.hostname = params.hostname ?? record.hostname;
|
|
129
|
+
if (!record.taskIds.includes(params.taskId)) {
|
|
130
|
+
record.taskIds.push(params.taskId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.save(records);
|
|
134
|
+
return record;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function currentSessionId(env = process.env) {
|
|
138
|
+
return env[DEFAULT_SESSION_ENV] || env[DEFAULT_SESSION_FALLBACK_ENV];
|
|
139
|
+
}
|
|
140
|
+
export function currentHostname() {
|
|
141
|
+
try {
|
|
142
|
+
return os.hostname();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return 'unknown';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ConductorConfig } from '../config/index.js';
|
|
2
|
+
export type WebSocketHandler = (payload: Record<string, any>) => Promise<void> | void;
|
|
3
|
+
export interface WebSocketLike {
|
|
4
|
+
send(data: string): Promise<void> | void;
|
|
5
|
+
ping(): Promise<void> | void;
|
|
6
|
+
close(): Promise<void> | void;
|
|
7
|
+
closed?: boolean;
|
|
8
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<string>;
|
|
9
|
+
}
|
|
10
|
+
export interface ConnectOptions {
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export type ConnectImpl = (url: string, options: ConnectOptions) => Promise<WebSocketLike>;
|
|
14
|
+
export interface WebSocketClientOptions {
|
|
15
|
+
reconnectDelay?: number;
|
|
16
|
+
heartbeatInterval?: number;
|
|
17
|
+
extraHeaders?: Record<string, string>;
|
|
18
|
+
connectImpl?: ConnectImpl;
|
|
19
|
+
hostName?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class ConductorWebSocketClient {
|
|
22
|
+
private readonly url;
|
|
23
|
+
private readonly token;
|
|
24
|
+
private readonly reconnectDelay;
|
|
25
|
+
private readonly heartbeatInterval;
|
|
26
|
+
private readonly connectImpl;
|
|
27
|
+
private readonly handlers;
|
|
28
|
+
private readonly extraHeaders;
|
|
29
|
+
private conn;
|
|
30
|
+
private stop;
|
|
31
|
+
private listenTask;
|
|
32
|
+
private heartbeatTask;
|
|
33
|
+
private readonly lock;
|
|
34
|
+
private waitController;
|
|
35
|
+
constructor(config: ConductorConfig, options?: WebSocketClientOptions);
|
|
36
|
+
registerHandler(handler: WebSocketHandler): void;
|
|
37
|
+
connect(): Promise<void>;
|
|
38
|
+
disconnect(): Promise<void>;
|
|
39
|
+
sendJson(payload: Record<string, any>): Promise<void>;
|
|
40
|
+
private ensureConnection;
|
|
41
|
+
private openConnection;
|
|
42
|
+
private cancelTasks;
|
|
43
|
+
private listenLoop;
|
|
44
|
+
private heartbeatLoop;
|
|
45
|
+
private dispatch;
|
|
46
|
+
private isConnectionClosed;
|
|
47
|
+
private sendWithReconnect;
|
|
48
|
+
private isNotOpenError;
|
|
49
|
+
}
|