@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,62 @@
|
|
|
1
|
+
import { ConductorConfig } from '../config/index.js';
|
|
2
|
+
export type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
export interface BackendClientOptions {
|
|
4
|
+
fetchImpl?: FetchFn;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class BackendApiError extends Error {
|
|
8
|
+
readonly statusCode?: number | undefined;
|
|
9
|
+
readonly details?: unknown | undefined;
|
|
10
|
+
constructor(message: string, statusCode?: number | undefined, details?: unknown | undefined, options?: ErrorOptions);
|
|
11
|
+
}
|
|
12
|
+
export declare class ProjectSummary {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name?: string | undefined;
|
|
15
|
+
readonly description?: string | null | undefined;
|
|
16
|
+
constructor(id: string, name?: string | undefined, description?: string | null | undefined);
|
|
17
|
+
static fromJSON(payload: Record<string, any>): ProjectSummary;
|
|
18
|
+
asObject(): Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export declare class TaskSummary {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly projectId: string | null;
|
|
23
|
+
readonly title: string;
|
|
24
|
+
readonly status: string;
|
|
25
|
+
readonly createdAt?: string | null | undefined;
|
|
26
|
+
readonly updatedAt?: string | null | undefined;
|
|
27
|
+
constructor(id: string, projectId: string | null, title: string, status: string, createdAt?: string | null | undefined, updatedAt?: string | null | undefined);
|
|
28
|
+
static fromJSON(payload: Record<string, any>): TaskSummary;
|
|
29
|
+
}
|
|
30
|
+
export declare class BackendApiClient {
|
|
31
|
+
private readonly config;
|
|
32
|
+
private readonly fetchImpl;
|
|
33
|
+
private readonly baseUrl;
|
|
34
|
+
private readonly timeoutMs;
|
|
35
|
+
constructor(config: ConductorConfig, options?: BackendClientOptions);
|
|
36
|
+
listProjects(): Promise<ProjectSummary[]>;
|
|
37
|
+
createProject(params: {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}): Promise<ProjectSummary>;
|
|
42
|
+
listTasks(params?: {
|
|
43
|
+
projectId?: string;
|
|
44
|
+
status?: string;
|
|
45
|
+
}): Promise<TaskSummary[]>;
|
|
46
|
+
createTask(params: {
|
|
47
|
+
id?: string;
|
|
48
|
+
projectId: string;
|
|
49
|
+
title: string;
|
|
50
|
+
backendType?: string;
|
|
51
|
+
initialContent?: string;
|
|
52
|
+
}): Promise<TaskSummary>;
|
|
53
|
+
createMessage(params: {
|
|
54
|
+
taskId: string;
|
|
55
|
+
role: string;
|
|
56
|
+
content: string;
|
|
57
|
+
metadata?: Record<string, unknown>;
|
|
58
|
+
}): Promise<any>;
|
|
59
|
+
private request;
|
|
60
|
+
private parseJson;
|
|
61
|
+
private safeJson;
|
|
62
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
export class BackendApiError extends Error {
|
|
2
|
+
statusCode;
|
|
3
|
+
details;
|
|
4
|
+
constructor(message, statusCode, details, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
this.details = details;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ProjectSummary {
|
|
11
|
+
id;
|
|
12
|
+
name;
|
|
13
|
+
description;
|
|
14
|
+
constructor(id, name, description) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.description = description;
|
|
18
|
+
}
|
|
19
|
+
static fromJSON(payload) {
|
|
20
|
+
const id = payload.id ? String(payload.id) : '';
|
|
21
|
+
if (!id) {
|
|
22
|
+
throw new Error('Project payload missing id');
|
|
23
|
+
}
|
|
24
|
+
return new ProjectSummary(id, payload.name ?? undefined, payload.description ?? undefined);
|
|
25
|
+
}
|
|
26
|
+
asObject() {
|
|
27
|
+
return {
|
|
28
|
+
id: this.id,
|
|
29
|
+
name: this.name,
|
|
30
|
+
description: this.description,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class TaskSummary {
|
|
35
|
+
id;
|
|
36
|
+
projectId;
|
|
37
|
+
title;
|
|
38
|
+
status;
|
|
39
|
+
createdAt;
|
|
40
|
+
updatedAt;
|
|
41
|
+
constructor(id, projectId, title, status, createdAt, updatedAt) {
|
|
42
|
+
this.id = id;
|
|
43
|
+
this.projectId = projectId;
|
|
44
|
+
this.title = title;
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.createdAt = createdAt;
|
|
47
|
+
this.updatedAt = updatedAt;
|
|
48
|
+
}
|
|
49
|
+
static fromJSON(payload) {
|
|
50
|
+
const id = payload.id ? String(payload.id) : '';
|
|
51
|
+
if (!id) {
|
|
52
|
+
throw new Error('Task payload missing id');
|
|
53
|
+
}
|
|
54
|
+
const title = payload.title ? String(payload.title) : '';
|
|
55
|
+
const status = payload.status ? String(payload.status) : '';
|
|
56
|
+
if (!title || !status) {
|
|
57
|
+
throw new Error('Task payload missing required fields');
|
|
58
|
+
}
|
|
59
|
+
return new TaskSummary(id, payload.project_id ? String(payload.project_id) : null, title, status, payload.created_at ?? null, payload.updated_at ?? null);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class BackendApiClient {
|
|
63
|
+
config;
|
|
64
|
+
fetchImpl;
|
|
65
|
+
baseUrl;
|
|
66
|
+
timeoutMs;
|
|
67
|
+
constructor(config, options = {}) {
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.fetchImpl = options.fetchImpl ?? globalFetch();
|
|
70
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
71
|
+
this.baseUrl = config.backendUrl.replace(/\/+$/, '');
|
|
72
|
+
}
|
|
73
|
+
async listProjects() {
|
|
74
|
+
const response = await this.request('GET', '/projects');
|
|
75
|
+
const payload = await this.parseJson(response);
|
|
76
|
+
if (!Array.isArray(payload)) {
|
|
77
|
+
throw new BackendApiError('Invalid projects response: expected list', response.status, payload);
|
|
78
|
+
}
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const entry of payload) {
|
|
81
|
+
if (!entry || typeof entry !== 'object') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
results.push(ProjectSummary.fromJSON(entry));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
async createProject(params) {
|
|
94
|
+
const response = await this.request('POST', '/projects', {
|
|
95
|
+
body: JSON.stringify(params),
|
|
96
|
+
});
|
|
97
|
+
const payload = await this.parseJson(response);
|
|
98
|
+
return ProjectSummary.fromJSON(payload);
|
|
99
|
+
}
|
|
100
|
+
async listTasks(params = {}) {
|
|
101
|
+
const query = new URLSearchParams();
|
|
102
|
+
if (params.projectId) {
|
|
103
|
+
query.set('project_id', params.projectId);
|
|
104
|
+
}
|
|
105
|
+
if (params.status) {
|
|
106
|
+
query.set('status', params.status);
|
|
107
|
+
}
|
|
108
|
+
const response = await this.request('GET', '/tasks', {
|
|
109
|
+
query,
|
|
110
|
+
});
|
|
111
|
+
const payload = await this.parseJson(response);
|
|
112
|
+
if (!Array.isArray(payload)) {
|
|
113
|
+
throw new BackendApiError('Invalid tasks response: expected list', response.status, payload);
|
|
114
|
+
}
|
|
115
|
+
const tasks = [];
|
|
116
|
+
for (const entry of payload) {
|
|
117
|
+
if (!entry || typeof entry !== 'object') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
tasks.push(TaskSummary.fromJSON(entry));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return tasks;
|
|
128
|
+
}
|
|
129
|
+
async createTask(params) {
|
|
130
|
+
const response = await this.request('POST', '/tasks', {
|
|
131
|
+
body: JSON.stringify(params),
|
|
132
|
+
});
|
|
133
|
+
const payload = await this.parseJson(response);
|
|
134
|
+
return TaskSummary.fromJSON(payload);
|
|
135
|
+
}
|
|
136
|
+
async createMessage(params) {
|
|
137
|
+
const response = await this.request('POST', `/tasks/${params.taskId}/messages`, {
|
|
138
|
+
body: JSON.stringify(params),
|
|
139
|
+
});
|
|
140
|
+
return this.parseJson(response);
|
|
141
|
+
}
|
|
142
|
+
async request(method, pathname, opts = {}) {
|
|
143
|
+
const url = new URL(`${this.baseUrl}${pathname}`);
|
|
144
|
+
if (opts.query) {
|
|
145
|
+
opts.query.forEach((value, key) => url.searchParams.set(key, value));
|
|
146
|
+
}
|
|
147
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
148
|
+
const timeout = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
|
|
149
|
+
try {
|
|
150
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
151
|
+
method,
|
|
152
|
+
headers: {
|
|
153
|
+
Authorization: `Bearer ${this.config.agentToken}`,
|
|
154
|
+
Accept: 'application/json',
|
|
155
|
+
...(opts.body ? { 'Content-Type': 'application/json' } : {}),
|
|
156
|
+
},
|
|
157
|
+
body: opts.body,
|
|
158
|
+
signal: controller?.signal,
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const details = await this.safeJson(response);
|
|
162
|
+
throw new BackendApiError(`Backend responded with ${response.status}`, response.status, details);
|
|
163
|
+
}
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof BackendApiError) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
throw new BackendApiError(`Backend request failed: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, error instanceof Error ? { cause: error } : undefined);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (timeout) {
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async parseJson(response) {
|
|
179
|
+
try {
|
|
180
|
+
return await response.json();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw new BackendApiError('Failed to parse backend response as JSON', response.status, null, {
|
|
184
|
+
cause: error instanceof Error ? error : undefined,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async safeJson(response) {
|
|
189
|
+
try {
|
|
190
|
+
return await response.json();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
try {
|
|
194
|
+
return await response.text();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function globalFetch() {
|
|
203
|
+
if (typeof fetch === 'function') {
|
|
204
|
+
return fetch.bind(globalThis);
|
|
205
|
+
}
|
|
206
|
+
throw new Error('Global fetch is not available; provide fetchImpl');
|
|
207
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './client.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './client.js';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { loadConfig, SessionManager, MessageRouter, BackendApiClient, ConductorWebSocketClient, MCPServer, EventReporter, SDKOrchestrator, MCPNotifier, } from '../index.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
const sessions = new SessionManager();
|
|
9
|
+
const notifier = new MCPNotifier();
|
|
10
|
+
const router = new MessageRouter(sessions, notifier);
|
|
11
|
+
const backendApi = new BackendApiClient(config);
|
|
12
|
+
const wsClient = new ConductorWebSocketClient(config);
|
|
13
|
+
const backendSender = async (envelope) => {
|
|
14
|
+
await wsClient.sendJson(envelope);
|
|
15
|
+
};
|
|
16
|
+
const reporter = new EventReporter(backendSender);
|
|
17
|
+
const mcpServerLogic = new MCPServer(config, {
|
|
18
|
+
sessionManager: sessions,
|
|
19
|
+
messageRouter: router,
|
|
20
|
+
backendSender,
|
|
21
|
+
backendApi,
|
|
22
|
+
});
|
|
23
|
+
const orchestrator = new SDKOrchestrator({
|
|
24
|
+
wsClient,
|
|
25
|
+
messageRouter: router,
|
|
26
|
+
sessionManager: sessions,
|
|
27
|
+
mcpServer: mcpServerLogic,
|
|
28
|
+
reporter,
|
|
29
|
+
});
|
|
30
|
+
const server = new Server({
|
|
31
|
+
name: 'conductor-ts',
|
|
32
|
+
version: '0.1.0',
|
|
33
|
+
}, {
|
|
34
|
+
capabilities: {
|
|
35
|
+
tools: {},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
39
|
+
return {
|
|
40
|
+
tools: [
|
|
41
|
+
{
|
|
42
|
+
name: 'create_task_session',
|
|
43
|
+
description: 'Create a new task session',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
project_id: { type: 'string' },
|
|
48
|
+
task_title: { type: 'string' },
|
|
49
|
+
prefill: { type: 'string' },
|
|
50
|
+
project_path: { type: 'string' },
|
|
51
|
+
},
|
|
52
|
+
required: ['project_id'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'list_projects',
|
|
57
|
+
description: 'List available projects',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'create_project',
|
|
65
|
+
description: 'Create a project',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
name: { type: 'string' },
|
|
70
|
+
description: { type: 'string' },
|
|
71
|
+
metadata: { type: 'object' },
|
|
72
|
+
},
|
|
73
|
+
required: ['name'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'list_tasks',
|
|
78
|
+
description: 'List tasks',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
project_id: { type: 'string' },
|
|
83
|
+
status: { type: 'string' },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'send_message',
|
|
89
|
+
description: 'Send a message to a task',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
task_id: { type: 'string' },
|
|
94
|
+
content: { type: 'string' },
|
|
95
|
+
metadata: { type: 'object' },
|
|
96
|
+
},
|
|
97
|
+
required: ['task_id', 'content'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'receive_messages',
|
|
102
|
+
description: 'Receive messages from a task',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
task_id: { type: 'string' },
|
|
107
|
+
limit: { type: 'number' },
|
|
108
|
+
},
|
|
109
|
+
required: ['task_id'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'ack_messages',
|
|
114
|
+
description: 'Acknowledge messages',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
task_id: { type: 'string' },
|
|
119
|
+
ack_token: { type: 'string' },
|
|
120
|
+
},
|
|
121
|
+
required: ['task_id', 'ack_token'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'get_local_project_id',
|
|
126
|
+
description: 'Get project ID for local path',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
project_path: { type: 'string' },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
138
|
+
const { name, arguments: args } = request.params;
|
|
139
|
+
try {
|
|
140
|
+
const result = await mcpServerLogic.handleRequest(name, args || {});
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: JSON.stringify(result),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
await orchestrator.start();
|
|
163
|
+
const transport = new StdioServerTransport();
|
|
164
|
+
await server.connect(transport);
|
|
165
|
+
// Keep alive
|
|
166
|
+
process.on('SIGINT', async () => {
|
|
167
|
+
await orchestrator.stop();
|
|
168
|
+
await server.close();
|
|
169
|
+
process.exit(0);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
main().catch((err) => {
|
|
173
|
+
console.error(err);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const CONFIG_ENV_VAR = "CONDUCTOR_CONFIG";
|
|
2
|
+
export declare const AGENT_TOKEN_ENV_VAR = "CONDUCTOR_AGENT_TOKEN";
|
|
3
|
+
export declare const BACKEND_URL_ENV_VAR = "CONDUCTOR_BACKEND_URL";
|
|
4
|
+
export declare const WS_URL_ENV_VAR = "CONDUCTOR_WS_URL";
|
|
5
|
+
export declare const LOG_LEVEL_ENV_VAR = "CONDUCTOR_LOG_LEVEL";
|
|
6
|
+
export declare class ConfigError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export declare class ConfigFileNotFound extends ConfigError {
|
|
9
|
+
readonly filePath: string;
|
|
10
|
+
constructor(filePath: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class ConfigValidationError extends ConfigError {
|
|
13
|
+
readonly errors: string[];
|
|
14
|
+
constructor(errors: string[]);
|
|
15
|
+
}
|
|
16
|
+
export interface ConductorConfigInit {
|
|
17
|
+
agentToken: string;
|
|
18
|
+
backendUrl: string;
|
|
19
|
+
websocketUrl?: string;
|
|
20
|
+
logLevel?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class ConductorConfig {
|
|
23
|
+
readonly agentToken: string;
|
|
24
|
+
readonly backendUrl: string;
|
|
25
|
+
readonly websocketUrl?: string;
|
|
26
|
+
readonly logLevel: string;
|
|
27
|
+
constructor(init: ConductorConfigInit);
|
|
28
|
+
get resolvedWebsocketUrl(): string;
|
|
29
|
+
}
|
|
30
|
+
export interface LoadConfigOptions {
|
|
31
|
+
env?: Record<string, string | undefined>;
|
|
32
|
+
}
|
|
33
|
+
export declare function loadConfig(targetPath?: string, options?: LoadConfigOptions): ConductorConfig;
|
|
@@ -0,0 +1,152 @@
|
|
|
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 CONFIG_ENV_VAR = 'CONDUCTOR_CONFIG';
|
|
6
|
+
export const AGENT_TOKEN_ENV_VAR = 'CONDUCTOR_AGENT_TOKEN';
|
|
7
|
+
export const BACKEND_URL_ENV_VAR = 'CONDUCTOR_BACKEND_URL';
|
|
8
|
+
export const WS_URL_ENV_VAR = 'CONDUCTOR_WS_URL';
|
|
9
|
+
export const LOG_LEVEL_ENV_VAR = 'CONDUCTOR_LOG_LEVEL';
|
|
10
|
+
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.conductor', 'config.yaml');
|
|
11
|
+
const ALLOWED_LOG_LEVELS = new Set(['debug', 'info', 'warning', 'error', 'critical']);
|
|
12
|
+
export class ConfigError extends Error {
|
|
13
|
+
}
|
|
14
|
+
export class ConfigFileNotFound extends ConfigError {
|
|
15
|
+
filePath;
|
|
16
|
+
constructor(filePath) {
|
|
17
|
+
super(`Conductor config file not found at ${filePath}`);
|
|
18
|
+
this.filePath = filePath;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class ConfigValidationError extends ConfigError {
|
|
22
|
+
errors;
|
|
23
|
+
constructor(errors) {
|
|
24
|
+
super(`Invalid Conductor configuration:\n- ${errors.join('\n- ')}`);
|
|
25
|
+
this.errors = errors;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class ConductorConfig {
|
|
29
|
+
agentToken;
|
|
30
|
+
backendUrl;
|
|
31
|
+
websocketUrl;
|
|
32
|
+
logLevel;
|
|
33
|
+
constructor(init) {
|
|
34
|
+
this.agentToken = init.agentToken;
|
|
35
|
+
this.backendUrl = init.backendUrl;
|
|
36
|
+
this.websocketUrl = init.websocketUrl;
|
|
37
|
+
this.logLevel = (init.logLevel || 'info').toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
get resolvedWebsocketUrl() {
|
|
40
|
+
if (this.websocketUrl) {
|
|
41
|
+
return this.websocketUrl;
|
|
42
|
+
}
|
|
43
|
+
const url = new URL(this.backendUrl);
|
|
44
|
+
const scheme = url.protocol === 'https:' ? 'wss' : 'ws';
|
|
45
|
+
return `${scheme}://${url.host}/ws/agent`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function loadConfig(targetPath, options = {}) {
|
|
49
|
+
const env = options.env ?? process.env;
|
|
50
|
+
const configPath = resolveConfigPath(targetPath, env);
|
|
51
|
+
const rawData = readYaml(configPath);
|
|
52
|
+
const merged = applyEnvOverrides(rawData, env);
|
|
53
|
+
const validationErrors = [];
|
|
54
|
+
const agentToken = normalizeToken(merged.agent_token);
|
|
55
|
+
if (!agentToken) {
|
|
56
|
+
validationErrors.push('agent_token: must be provided');
|
|
57
|
+
}
|
|
58
|
+
let backendUrl;
|
|
59
|
+
if (merged.backend_url) {
|
|
60
|
+
try {
|
|
61
|
+
backendUrl = new URL(String(merged.backend_url)).toString();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
validationErrors.push('backend_url: must be a valid URL');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
backendUrl = 'https://api.conductor.local';
|
|
69
|
+
}
|
|
70
|
+
const logLevel = normalizeLogLevel(merged.log_level);
|
|
71
|
+
if (!logLevel) {
|
|
72
|
+
validationErrors.push(`log_level: must be one of ${Array.from(ALLOWED_LOG_LEVELS).join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
let websocketUrl;
|
|
75
|
+
if (merged.websocket_url) {
|
|
76
|
+
try {
|
|
77
|
+
websocketUrl = new URL(String(merged.websocket_url)).toString();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
validationErrors.push('websocket_url: must be a valid URL');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (validationErrors.length) {
|
|
84
|
+
throw new ConfigValidationError(validationErrors);
|
|
85
|
+
}
|
|
86
|
+
return new ConductorConfig({
|
|
87
|
+
agentToken: agentToken,
|
|
88
|
+
backendUrl: backendUrl,
|
|
89
|
+
websocketUrl,
|
|
90
|
+
logLevel: logLevel,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function resolveConfigPath(explicitPath, env) {
|
|
94
|
+
const candidate = explicitPath ||
|
|
95
|
+
env[CONFIG_ENV_VAR] ||
|
|
96
|
+
DEFAULT_CONFIG_PATH;
|
|
97
|
+
const normalized = normalizePath(candidate);
|
|
98
|
+
if (!fs.existsSync(normalized)) {
|
|
99
|
+
throw new ConfigFileNotFound(normalized);
|
|
100
|
+
}
|
|
101
|
+
return normalized;
|
|
102
|
+
}
|
|
103
|
+
function normalizePath(value) {
|
|
104
|
+
const expanded = value.startsWith('~')
|
|
105
|
+
? path.join(os.homedir(), value.slice(1))
|
|
106
|
+
: value;
|
|
107
|
+
return path.resolve(expanded);
|
|
108
|
+
}
|
|
109
|
+
function readYaml(filePath) {
|
|
110
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = yaml.parse(content) ?? {};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new ConfigValidationError([`Failed to parse YAML at ${filePath}: ${String(error)}`]);
|
|
117
|
+
}
|
|
118
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
119
|
+
throw new ConfigValidationError([`Expected mapping at root of ${filePath}`]);
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
function applyEnvOverrides(data, env) {
|
|
124
|
+
const merged = { ...data };
|
|
125
|
+
if (env[AGENT_TOKEN_ENV_VAR]) {
|
|
126
|
+
merged.agent_token = env[AGENT_TOKEN_ENV_VAR];
|
|
127
|
+
}
|
|
128
|
+
if (env[BACKEND_URL_ENV_VAR]) {
|
|
129
|
+
merged.backend_url = env[BACKEND_URL_ENV_VAR];
|
|
130
|
+
}
|
|
131
|
+
if (env[WS_URL_ENV_VAR]) {
|
|
132
|
+
merged.websocket_url = env[WS_URL_ENV_VAR];
|
|
133
|
+
}
|
|
134
|
+
if (env[LOG_LEVEL_ENV_VAR]) {
|
|
135
|
+
merged.log_level = env[LOG_LEVEL_ENV_VAR];
|
|
136
|
+
}
|
|
137
|
+
return merged;
|
|
138
|
+
}
|
|
139
|
+
function normalizeToken(value) {
|
|
140
|
+
if (typeof value !== 'string') {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
return trimmed || undefined;
|
|
145
|
+
}
|
|
146
|
+
function normalizeLogLevel(value) {
|
|
147
|
+
if (typeof value !== 'string') {
|
|
148
|
+
return 'info';
|
|
149
|
+
}
|
|
150
|
+
const lowered = value.trim().toLowerCase();
|
|
151
|
+
return ALLOWED_LOG_LEVELS.has(lowered) ? lowered : undefined;
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './project_context.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './project_context.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GuessResult {
|
|
2
|
+
projectRoot: string;
|
|
3
|
+
repoRoot?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class ProjectContext {
|
|
6
|
+
private readonly root;
|
|
7
|
+
constructor(targetPath?: string);
|
|
8
|
+
guess(): GuessResult;
|
|
9
|
+
listFiles(relativeToRepo?: boolean): string[];
|
|
10
|
+
readFile(relativePath: string): string;
|
|
11
|
+
getDiff(staged?: boolean): string;
|
|
12
|
+
private gitRoot;
|
|
13
|
+
private gitListFiles;
|
|
14
|
+
}
|