@love-moon/conductor-sdk 0.1.0 → 0.1.4

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.
@@ -56,6 +56,23 @@ export declare class BackendApiClient {
56
56
  content: string;
57
57
  metadata?: Record<string, unknown>;
58
58
  }): Promise<any>;
59
+ matchProjectByPath(params: {
60
+ hostname: string;
61
+ path: string;
62
+ }): Promise<{
63
+ project: ProjectSummary | null;
64
+ matchedPath: string | null;
65
+ }>;
66
+ getProject(projectId: string): Promise<{
67
+ id: string;
68
+ name?: string;
69
+ description?: string | null;
70
+ metadata?: Record<string, unknown>;
71
+ }>;
72
+ updateProject(projectId: string, params: {
73
+ name?: string;
74
+ metadata?: Record<string, unknown>;
75
+ }): Promise<ProjectSummary>;
59
76
  private request;
60
77
  private parseJson;
61
78
  private safeJson;
@@ -139,6 +139,33 @@ export class BackendApiClient {
139
139
  });
140
140
  return this.parseJson(response);
141
141
  }
142
+ async matchProjectByPath(params) {
143
+ const response = await this.request('POST', '/projects/match-path', {
144
+ body: JSON.stringify(params),
145
+ });
146
+ const payload = await this.parseJson(response);
147
+ return {
148
+ project: payload.project ? ProjectSummary.fromJSON(payload.project) : null,
149
+ matchedPath: payload.matched_path ?? null,
150
+ };
151
+ }
152
+ async getProject(projectId) {
153
+ const response = await this.request('GET', `/projects/${projectId}`);
154
+ const payload = await this.parseJson(response);
155
+ return {
156
+ id: payload.id,
157
+ name: payload.name,
158
+ description: payload.description,
159
+ metadata: payload.metadata ?? undefined,
160
+ };
161
+ }
162
+ async updateProject(projectId, params) {
163
+ const response = await this.request('PATCH', `/projects/${projectId}`, {
164
+ body: JSON.stringify(params),
165
+ });
166
+ const payload = await this.parseJson(response);
167
+ return ProjectSummary.fromJSON(payload);
168
+ }
142
169
  async request(method, pathname, opts = {}) {
143
170
  const url = new URL(`${this.baseUrl}${pathname}`);
144
171
  if (opts.query) {
@@ -8,7 +8,7 @@ export interface MCPServerOptions {
8
8
  sessionManager: SessionManager;
9
9
  messageRouter: MessageRouter;
10
10
  backendSender: BackendSender;
11
- backendApi: Pick<BackendApiClient, 'listProjects' | 'listTasks' | 'createProject' | 'createTask'>;
11
+ backendApi: Pick<BackendApiClient, 'listProjects' | 'listTasks' | 'createProject' | 'createTask' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
12
12
  sessionStore?: SessionDiskStore;
13
13
  env?: Record<string, string | undefined>;
14
14
  }
@@ -30,6 +30,8 @@ export declare class MCPServer {
30
30
  private toolCreateProject;
31
31
  private toolListTasks;
32
32
  private toolGetLocalProjectId;
33
+ private toolMatchProjectByPath;
34
+ private toolBindProjectPath;
33
35
  private resolveHostname;
34
36
  private waitForTaskCreation;
35
37
  private readIntEnv;
@@ -10,7 +10,8 @@ export class MCPServer {
10
10
  constructor(config, options) {
11
11
  this.config = config;
12
12
  this.options = options;
13
- this.sessionStore = options.sessionStore ?? new SessionDiskStore();
13
+ // Use backend URL to determine session file path (isolates different environments)
14
+ this.sessionStore = options.sessionStore ?? SessionDiskStore.forBackendUrl(config.backendUrl);
14
15
  this.env = options.env ?? process.env;
15
16
  this.tools = {
16
17
  create_task_session: this.toolCreateTaskSession,
@@ -21,6 +22,8 @@ export class MCPServer {
21
22
  create_project: this.toolCreateProject,
22
23
  list_tasks: this.toolListTasks,
23
24
  get_local_project_id: this.toolGetLocalProjectId,
25
+ match_project_by_path: this.toolMatchProjectByPath,
26
+ bind_project_path: this.toolBindProjectPath,
24
27
  };
25
28
  }
26
29
  async handleRequest(toolName, payload) {
@@ -154,6 +157,54 @@ export class MCPServer {
154
157
  hostname: record.hostname,
155
158
  };
156
159
  }
160
+ async toolMatchProjectByPath(payload) {
161
+ const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
162
+ const projectPath = typeof payload.project_path === 'string' && payload.project_path
163
+ ? payload.project_path
164
+ : process.cwd();
165
+ console.error(`[mcp] match_project_by_path hostname=${hostname} path=${projectPath}`);
166
+ const result = await this.options.backendApi.matchProjectByPath({
167
+ hostname,
168
+ path: projectPath,
169
+ });
170
+ if (result.project) {
171
+ return {
172
+ project_id: result.project.id,
173
+ project_name: result.project.name,
174
+ matched_path: result.matchedPath,
175
+ };
176
+ }
177
+ return {
178
+ project_id: null,
179
+ project_name: null,
180
+ matched_path: null,
181
+ };
182
+ }
183
+ async toolBindProjectPath(payload) {
184
+ const projectId = String(payload.project_id || '');
185
+ if (!projectId) {
186
+ throw new Error('project_id is required');
187
+ }
188
+ const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
189
+ const projectPath = typeof payload.project_path === 'string' && payload.project_path
190
+ ? payload.project_path
191
+ : process.cwd();
192
+ console.error(`[mcp] bind_project_path project=${projectId} hostname=${hostname} path=${projectPath}`);
193
+ // Get current project metadata
194
+ const project = await this.options.backendApi.getProject(projectId);
195
+ const metadata = (project.metadata || {});
196
+ const localPaths = (metadata.localPaths || {});
197
+ // Update localPaths with new binding
198
+ localPaths[hostname] = projectPath;
199
+ metadata.localPaths = localPaths;
200
+ // Update project
201
+ await this.options.backendApi.updateProject(projectId, { metadata });
202
+ return {
203
+ success: true,
204
+ hostname,
205
+ path: projectPath,
206
+ };
207
+ }
157
208
  resolveHostname() {
158
209
  const records = this.sessionStore.load();
159
210
  for (const record of records) {
@@ -1,3 +1,4 @@
1
+ export declare const DEFAULT_SESSION_DIR: string;
1
2
  export declare const DEFAULT_SESSION_PATH: string;
2
3
  export declare const DEFAULT_SESSION_ENV = "CODEX_SESSION_ID";
3
4
  export declare const DEFAULT_SESSION_FALLBACK_ENV = "SESSION_ID";
@@ -21,6 +22,11 @@ export declare class SessionRecord {
21
22
  export declare class SessionDiskStore {
22
23
  private readonly filePath;
23
24
  constructor(filePath?: string);
25
+ /**
26
+ * Create a SessionDiskStore for a specific backend URL.
27
+ * Sessions are stored in ~/.conductor/sessions/<host>.yaml
28
+ */
29
+ static forBackendUrl(backendUrl: string): SessionDiskStore;
24
30
  load(): SessionRecord[];
25
31
  save(records: SessionRecord[]): void;
26
32
  findByPath(projectPath: string): SessionRecord | undefined;
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import yaml from 'yaml';
5
+ export const DEFAULT_SESSION_DIR = path.join(os.homedir(), '.conductor', 'sessions');
5
6
  export const DEFAULT_SESSION_PATH = path.join(os.homedir(), '.conductor', 'session.yaml');
6
7
  export const DEFAULT_SESSION_ENV = 'CODEX_SESSION_ID';
7
8
  export const DEFAULT_SESSION_FALLBACK_ENV = 'SESSION_ID';
@@ -55,6 +56,15 @@ export class SessionDiskStore {
55
56
  constructor(filePath = DEFAULT_SESSION_PATH) {
56
57
  this.filePath = path.resolve(filePath);
57
58
  }
59
+ /**
60
+ * Create a SessionDiskStore for a specific backend URL.
61
+ * Sessions are stored in ~/.conductor/sessions/<host>.yaml
62
+ */
63
+ static forBackendUrl(backendUrl) {
64
+ const host = extractHostKey(backendUrl);
65
+ const filePath = path.join(DEFAULT_SESSION_DIR, `${host}.yaml`);
66
+ return new SessionDiskStore(filePath);
67
+ }
58
68
  load() {
59
69
  if (!fs.existsSync(this.filePath)) {
60
70
  return [];
@@ -145,3 +155,22 @@ export function currentHostname() {
145
155
  return 'unknown';
146
156
  }
147
157
  }
158
+ /**
159
+ * Extract a safe filename key from a backend URL.
160
+ * e.g., "http://localhost:6152" -> "localhost_6152"
161
+ * "https://conductor-ai.top" -> "conductor-ai.top"
162
+ */
163
+ function extractHostKey(backendUrl) {
164
+ try {
165
+ const url = new URL(backendUrl);
166
+ const host = url.hostname;
167
+ const port = url.port;
168
+ // Replace unsafe characters for filenames
169
+ const safeHost = host.replace(/[^a-zA-Z0-9.-]/g, '_');
170
+ return port ? `${safeHost}_${port}` : safeHost;
171
+ }
172
+ catch {
173
+ // Fallback: sanitize the entire URL
174
+ return backendUrl.replace(/[^a-zA-Z0-9.-]/g, '_').slice(0, 50);
175
+ }
176
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",