@love-moon/conductor-sdk 0.2.31 → 0.2.33

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.
@@ -36,8 +36,14 @@ export declare class BackendApiError extends Error {
36
36
  export declare class ProjectSummary {
37
37
  readonly id: string;
38
38
  readonly name?: string | undefined;
39
- readonly description?: string | null | undefined;
40
- constructor(id: string, name?: string | undefined, description?: string | null | undefined);
39
+ readonly daemonHost?: string | null | undefined;
40
+ readonly workspacePath?: string | null | undefined;
41
+ readonly repoRoot?: string | null | undefined;
42
+ readonly worktreeBranch?: string | null | undefined;
43
+ readonly lastCommit?: string | null | undefined;
44
+ readonly fileCount?: number | null | undefined;
45
+ readonly isDefault?: boolean | undefined;
46
+ constructor(id: string, name?: string | undefined, daemonHost?: string | null | undefined, workspacePath?: string | null | undefined, repoRoot?: string | null | undefined, worktreeBranch?: string | null | undefined, lastCommit?: string | null | undefined, fileCount?: number | null | undefined, isDefault?: boolean | undefined);
41
47
  static fromJSON(payload: Record<string, any>): ProjectSummary;
42
48
  asObject(): Record<string, unknown>;
43
49
  }
@@ -62,9 +68,16 @@ export declare class BackendApiClient {
62
68
  constructor(config: ConductorConfig, options?: BackendClientOptions);
63
69
  listProjects(): Promise<ProjectSummary[]>;
64
70
  createProject(params: {
65
- name: string;
66
- description?: string;
71
+ name?: string;
67
72
  metadata?: Record<string, unknown>;
73
+ isDefault?: boolean;
74
+ bindingConfirmed?: boolean;
75
+ daemonHost?: string;
76
+ workspacePath?: string;
77
+ repoRoot?: string;
78
+ worktreeBranch?: string;
79
+ lastCommit?: string;
80
+ fileCount?: number;
68
81
  }): Promise<ProjectSummary>;
69
82
  listTasks(params?: {
70
83
  projectId?: string;
@@ -132,7 +145,9 @@ export declare class BackendApiClient {
132
145
  accepted?: boolean;
133
146
  }): Promise<Record<string, unknown>>;
134
147
  matchProjectByPath(params: {
135
- hostname: string;
148
+ hostname?: string;
149
+ daemonHost?: string;
150
+ daemon_host?: string;
136
151
  path: string;
137
152
  }): Promise<{
138
153
  project: ProjectSummary | null;
@@ -141,12 +156,25 @@ export declare class BackendApiClient {
141
156
  getProject(projectId: string): Promise<{
142
157
  id: string;
143
158
  name?: string;
144
- description?: string | null;
145
159
  metadata?: Record<string, unknown>;
160
+ daemonHost?: string | null;
161
+ workspacePath?: string | null;
162
+ repoRoot?: string | null;
163
+ worktreeBranch?: string | null;
164
+ lastCommit?: string | null;
165
+ fileCount?: number | null;
166
+ isDefault?: boolean;
146
167
  }>;
147
168
  updateProject(projectId: string, params: {
148
169
  name?: string;
149
170
  metadata?: Record<string, unknown>;
171
+ bindingConfirmed?: boolean;
172
+ daemonHost?: string;
173
+ workspacePath?: string;
174
+ repoRoot?: string;
175
+ worktreeBranch?: string;
176
+ lastCommit?: string;
177
+ fileCount?: number;
150
178
  }): Promise<ProjectSummary>;
151
179
  private request;
152
180
  private buildUrl;
@@ -10,24 +10,42 @@ export class BackendApiError extends Error {
10
10
  export class ProjectSummary {
11
11
  id;
12
12
  name;
13
- description;
14
- constructor(id, name, description) {
13
+ daemonHost;
14
+ workspacePath;
15
+ repoRoot;
16
+ worktreeBranch;
17
+ lastCommit;
18
+ fileCount;
19
+ isDefault;
20
+ constructor(id, name, daemonHost, workspacePath, repoRoot, worktreeBranch, lastCommit, fileCount, isDefault) {
15
21
  this.id = id;
16
22
  this.name = name;
17
- this.description = description;
23
+ this.daemonHost = daemonHost;
24
+ this.workspacePath = workspacePath;
25
+ this.repoRoot = repoRoot;
26
+ this.worktreeBranch = worktreeBranch;
27
+ this.lastCommit = lastCommit;
28
+ this.fileCount = fileCount;
29
+ this.isDefault = isDefault;
18
30
  }
19
31
  static fromJSON(payload) {
20
32
  const id = payload.id ? String(payload.id) : '';
21
33
  if (!id) {
22
34
  throw new Error('Project payload missing id');
23
35
  }
24
- return new ProjectSummary(id, payload.name ?? undefined, payload.description ?? undefined);
36
+ return new ProjectSummary(id, payload.name ?? undefined, payload.daemonHost ?? payload.daemon_host ?? null, payload.workspacePath ?? payload.workspace_path ?? null, payload.repoRoot ?? payload.repo_root ?? null, payload.worktreeBranch ?? payload.worktree_branch ?? null, payload.lastCommit ?? payload.last_commit ?? null, payload.fileCount ?? payload.file_count ?? null, payload.isDefault ?? payload.is_default ?? undefined);
25
37
  }
26
38
  asObject() {
27
39
  return {
28
40
  id: this.id,
29
41
  name: this.name,
30
- description: this.description,
42
+ daemonHost: this.daemonHost,
43
+ workspacePath: this.workspacePath,
44
+ repoRoot: this.repoRoot,
45
+ worktreeBranch: this.worktreeBranch,
46
+ lastCommit: this.lastCommit,
47
+ fileCount: this.fileCount,
48
+ isDefault: this.isDefault,
31
49
  };
32
50
  }
33
51
  }
@@ -270,8 +288,14 @@ export class BackendApiClient {
270
288
  return {
271
289
  id: payload.id,
272
290
  name: payload.name,
273
- description: payload.description,
274
291
  metadata: payload.metadata ?? undefined,
292
+ daemonHost: payload.daemonHost ?? payload.daemon_host ?? undefined,
293
+ workspacePath: payload.workspacePath ?? payload.workspace_path ?? undefined,
294
+ repoRoot: payload.repoRoot ?? payload.repo_root ?? undefined,
295
+ worktreeBranch: payload.worktreeBranch ?? payload.worktree_branch ?? undefined,
296
+ lastCommit: payload.lastCommit ?? payload.last_commit ?? undefined,
297
+ fileCount: payload.fileCount ?? payload.file_count ?? undefined,
298
+ isDefault: payload.isDefault ?? payload.is_default ?? undefined,
275
299
  };
276
300
  }
277
301
  async updateProject(projectId, params) {
package/dist/client.d.ts CHANGED
@@ -47,6 +47,10 @@ export interface StopTaskEvent {
47
47
  requestId?: string;
48
48
  reason?: string;
49
49
  }
50
+ export interface FlushPendingUpstreamEventsOptions {
51
+ timeoutMs?: number;
52
+ retryIntervalMs?: number;
53
+ }
50
54
  export declare class ConductorClient {
51
55
  private readonly config;
52
56
  private readonly env;
@@ -68,6 +72,10 @@ export declare class ConductorClient {
68
72
  constructor(init: ConductorClientInit);
69
73
  static connect(options?: ConductorClientConnectOptions): Promise<ConductorClient>;
70
74
  close(): Promise<void>;
75
+ flushPendingUpstreamEvents(options?: FlushPendingUpstreamEventsOptions): Promise<{
76
+ flushed: boolean;
77
+ remaining: number;
78
+ }>;
71
79
  forceReconnect(reason?: string): Promise<void>;
72
80
  createTaskSession(payload: Record<string, any>): Promise<Record<string, any>>;
73
81
  sendMessage(taskId: string, content: string, metadata?: Record<string, any>): Promise<Record<string, any>>;
@@ -96,7 +104,8 @@ export declare class ConductorClient {
96
104
  receiveMessages(taskId: string, limit?: number): Promise<Record<string, any>>;
97
105
  ackMessages(taskId: string, ackToken?: string | null): Promise<Record<string, any> | undefined>;
98
106
  listProjects(): Promise<Record<string, any>>;
99
- createProject(name: string, description?: string, metadata?: Record<string, unknown>): Promise<Record<string, any>>;
107
+ createProject(name: string, metadata?: Record<string, unknown>): Promise<Record<string, any>>;
108
+ createProject(payload: Record<string, any>): Promise<Record<string, any>>;
100
109
  listTasks(payload?: Record<string, any>): Promise<Record<string, any>>;
101
110
  getLocalProjectRecord(payload?: Record<string, any>): Promise<Record<string, any>>;
102
111
  getLocalTaskRecord(payload?: Record<string, any>): Promise<Record<string, any>>;
package/dist/client.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { BackendApiClient, BackendApiError } from './backend/index.js';
3
5
  import { loadConfig } from './config/index.js';
6
+ import { ProjectContext } from './context/index.js';
4
7
  import { getPlanLimitMessageFromError } from './limits/index.js';
5
8
  import { MessageRouter } from './message/index.js';
6
9
  import { DownstreamCursorStore, DurableUpstreamOutboxStore, normalizeDownstreamCommandCursor, } from './outbox/index.js';
@@ -91,10 +94,30 @@ export class ConductorClient {
91
94
  if (this.closed) {
92
95
  return;
93
96
  }
94
- this.closed = true;
95
97
  this.clearDurableOutboxTimer();
98
+ await this.flushPendingUpstreamEvents({
99
+ timeoutMs: 2_000,
100
+ retryIntervalMs: 100,
101
+ });
102
+ this.closed = true;
96
103
  await this.wsClient.disconnect();
97
104
  }
105
+ async flushPendingUpstreamEvents(options = {}) {
106
+ const timeoutMs = Math.max(0, options.timeoutMs ?? 5_000);
107
+ const retryIntervalMs = Math.max(10, options.retryIntervalMs ?? 250);
108
+ const startedAt = Date.now();
109
+ while (true) {
110
+ await this.requestDurableOutboxFlush(true);
111
+ const remaining = this.upstreamOutbox.load().length;
112
+ if (remaining === 0) {
113
+ return { flushed: true, remaining: 0 };
114
+ }
115
+ if (Date.now() - startedAt >= timeoutMs) {
116
+ return { flushed: false, remaining };
117
+ }
118
+ await sleep(retryIntervalMs);
119
+ }
120
+ }
98
121
  async forceReconnect(reason = 'manual_reconnect') {
99
122
  if (typeof this.wsClient.forceReconnect === 'function') {
100
123
  await this.wsClient.forceReconnect(reason);
@@ -318,12 +341,14 @@ export class ConductorClient {
318
341
  : {
319
342
  id: project.id,
320
343
  name: project.name ?? null,
321
- description: project.description ?? null,
322
344
  }),
323
345
  };
324
346
  }
325
- async createProject(name, description, metadata) {
326
- const project = await this.backendApi.createProject({ name, description, metadata });
347
+ async createProject(nameOrPayload, metadata) {
348
+ const payload = typeof nameOrPayload === 'string'
349
+ ? { name: nameOrPayload, metadata }
350
+ : { ...nameOrPayload };
351
+ const project = await this.backendApi.createProject(payload);
327
352
  return typeof project.asObject === 'function' ? project.asObject() : project;
328
353
  }
329
354
  async listTasks(payload = {}) {
@@ -436,12 +461,20 @@ export class ConductorClient {
436
461
  };
437
462
  }
438
463
  async matchProjectByPath(payload = {}) {
439
- const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
440
- const projectPath = typeof payload.project_path === 'string' && payload.project_path
464
+ const daemonHost = (typeof payload.daemon_host === 'string' && payload.daemon_host.trim()) ||
465
+ (typeof payload.daemonHost === 'string' && payload.daemonHost.trim()) ||
466
+ (typeof payload.hostname === 'string' && payload.hostname.trim()) ||
467
+ this.agentHost;
468
+ const projectPath = resolveWorkspacePath(typeof payload.project_path === 'string' && payload.project_path
441
469
  ? payload.project_path
442
- : this.projectPath;
470
+ : typeof payload.projectPath === 'string' && payload.projectPath
471
+ ? payload.projectPath
472
+ : this.projectPath);
473
+ if (!projectPath) {
474
+ throw new Error('project_path is required');
475
+ }
443
476
  const result = await this.backendApi.matchProjectByPath({
444
- hostname,
477
+ daemon_host: daemonHost,
445
478
  path: projectPath,
446
479
  });
447
480
  if (result.project) {
@@ -461,20 +494,36 @@ export class ConductorClient {
461
494
  if (!projectId) {
462
495
  throw new Error('project_id is required');
463
496
  }
464
- const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
465
- const projectPath = typeof payload.project_path === 'string' && payload.project_path
497
+ const daemonHost = (typeof payload.daemon_host === 'string' && payload.daemon_host.trim()) ||
498
+ (typeof payload.daemonHost === 'string' && payload.daemonHost.trim()) ||
499
+ (typeof payload.hostname === 'string' && payload.hostname.trim()) ||
500
+ this.agentHost;
501
+ const projectPath = resolveWorkspacePath(typeof payload.project_path === 'string' && payload.project_path
466
502
  ? payload.project_path
467
- : this.projectPath;
468
- const project = await this.backendApi.getProject(projectId);
469
- const metadata = (project.metadata || {});
470
- const localPaths = (metadata.localPaths || {});
471
- localPaths[hostname] = projectPath;
472
- metadata.localPaths = localPaths;
473
- await this.backendApi.updateProject(projectId, { metadata });
503
+ : typeof payload.projectPath === 'string' && payload.projectPath
504
+ ? payload.projectPath
505
+ : this.projectPath);
506
+ const snapshot = resolveWorkspaceSnapshot(projectPath);
507
+ const boundProject = await this.backendApi.updateProject(projectId, {
508
+ bindingConfirmed: true,
509
+ daemonHost,
510
+ workspacePath: snapshot.projectRoot,
511
+ repoRoot: snapshot.repoRoot,
512
+ worktreeBranch: snapshot.worktreeBranch,
513
+ lastCommit: snapshot.lastCommit,
514
+ fileCount: snapshot.fileCount,
515
+ });
474
516
  return {
475
517
  success: true,
476
- hostname,
477
- path: projectPath,
518
+ project_id: boundProject.id,
519
+ project_name: boundProject.name ?? null,
520
+ hostname: daemonHost,
521
+ daemon_host: daemonHost,
522
+ path: snapshot.projectRoot,
523
+ repo_root: snapshot.repoRoot ?? null,
524
+ worktree_branch: snapshot.worktreeBranch ?? null,
525
+ last_commit: snapshot.lastCommit ?? null,
526
+ file_count: snapshot.fileCount ?? null,
478
527
  };
479
528
  }
480
529
  handleBackendEvent = async (payload) => {
@@ -886,3 +935,26 @@ function normalizeDeliveryScopeId(scopeId) {
886
935
  const normalized = String(scopeId || '').trim();
887
936
  return normalized || 'default';
888
937
  }
938
+ function resolveWorkspacePath(candidate) {
939
+ const trimmed = typeof candidate === 'string' ? candidate.trim() : '';
940
+ if (!trimmed) {
941
+ return '';
942
+ }
943
+ try {
944
+ return fs.realpathSync(trimmed);
945
+ }
946
+ catch {
947
+ return path.resolve(trimmed);
948
+ }
949
+ }
950
+ function resolveWorkspaceSnapshot(projectPath) {
951
+ try {
952
+ const context = new ProjectContext(projectPath);
953
+ return context.snapshot();
954
+ }
955
+ catch {
956
+ return {
957
+ projectRoot: resolveWorkspacePath(projectPath),
958
+ };
959
+ }
960
+ }
@@ -2,13 +2,24 @@ export interface GuessResult {
2
2
  projectRoot: string;
3
3
  repoRoot?: string;
4
4
  }
5
+ export interface WorkspaceSnapshot {
6
+ projectRoot: string;
7
+ repoRoot?: string;
8
+ worktreeBranch?: string;
9
+ lastCommit?: string;
10
+ fileCount?: number;
11
+ }
5
12
  export declare class ProjectContext {
6
13
  private readonly root;
7
14
  constructor(targetPath?: string);
8
15
  guess(): GuessResult;
16
+ snapshot(): WorkspaceSnapshot;
9
17
  listFiles(relativeToRepo?: boolean): string[];
10
18
  readFile(relativePath: string): string;
11
19
  getDiff(staged?: boolean): string;
12
20
  private gitRoot;
21
+ private gitBranch;
22
+ private gitHead;
23
+ private gitFileCount;
13
24
  private gitListFiles;
14
25
  }
@@ -14,6 +14,21 @@ export class ProjectContext {
14
14
  repoRoot: repoRoot ?? undefined,
15
15
  };
16
16
  }
17
+ snapshot() {
18
+ const guess = this.guess();
19
+ if (!guess.repoRoot) {
20
+ return {
21
+ projectRoot: guess.projectRoot,
22
+ };
23
+ }
24
+ return {
25
+ projectRoot: guess.projectRoot,
26
+ repoRoot: guess.repoRoot,
27
+ worktreeBranch: this.gitBranch(guess.repoRoot) ?? undefined,
28
+ lastCommit: this.gitHead(guess.repoRoot) ?? undefined,
29
+ fileCount: this.gitFileCount(guess.repoRoot) ?? undefined,
30
+ };
31
+ }
17
32
  listFiles(relativeToRepo = true) {
18
33
  const guess = this.guess();
19
34
  if (guess.repoRoot) {
@@ -55,6 +70,35 @@ export class ProjectContext {
55
70
  return null;
56
71
  }
57
72
  }
73
+ gitBranch(repoRoot) {
74
+ try {
75
+ const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).trim();
76
+ if (!branch || branch === 'HEAD') {
77
+ return null;
78
+ }
79
+ return branch;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ gitHead(repoRoot) {
86
+ try {
87
+ const head = runGit(['rev-parse', 'HEAD'], repoRoot).trim();
88
+ return head || null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ gitFileCount(repoRoot) {
95
+ try {
96
+ return this.gitListFiles(repoRoot).length;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
58
102
  gitListFiles(repoRoot) {
59
103
  try {
60
104
  const output = runGit(['ls-files'], repoRoot);
@@ -41,6 +41,7 @@ export interface WebSocketDisconnectEvent {
41
41
  }
42
42
  export interface WebSocketClientOptions {
43
43
  reconnectDelay?: number;
44
+ duplicateHostReconnectDelay?: number;
44
45
  heartbeatInterval?: number;
45
46
  extraHeaders?: Record<string, string>;
46
47
  connectImpl?: ConnectImpl;
@@ -54,6 +55,7 @@ export declare class ConductorWebSocketClient {
54
55
  private readonly url;
55
56
  private readonly token;
56
57
  private readonly reconnectDelay;
58
+ private readonly duplicateHostReconnectDelay;
57
59
  private readonly heartbeatInterval;
58
60
  private readonly connectImpl;
59
61
  private readonly onConnected?;
@@ -83,6 +85,7 @@ export declare class ConductorWebSocketClient {
83
85
  private listenLoop;
84
86
  private heartbeatLoop;
85
87
  private handleConnectionLoss;
88
+ private getReconnectDelay;
86
89
  private dispatch;
87
90
  private notifyConnected;
88
91
  private notifyReconnected;
package/dist/ws/client.js CHANGED
@@ -12,6 +12,7 @@ export class ConductorWebSocketClient {
12
12
  url;
13
13
  token;
14
14
  reconnectDelay;
15
+ duplicateHostReconnectDelay;
15
16
  heartbeatInterval;
16
17
  connectImpl;
17
18
  onConnected;
@@ -32,6 +33,7 @@ export class ConductorWebSocketClient {
32
33
  this.url = config.resolvedWebsocketUrl;
33
34
  this.token = config.agentToken;
34
35
  this.reconnectDelay = options.reconnectDelay ?? 10_000;
36
+ this.duplicateHostReconnectDelay = options.duplicateHostReconnectDelay ?? Math.max(this.reconnectDelay, 2_000);
35
37
  this.heartbeatInterval = options.heartbeatInterval ?? 20_000;
36
38
  this.extraHeaders = {
37
39
  'x-conductor-host': options.hostName ?? defaultHostName(),
@@ -184,8 +186,16 @@ export class ConductorWebSocketClient {
184
186
  this.conn = null;
185
187
  this.runtime = null;
186
188
  this.notifyDisconnected(this.buildDisconnectEvent(runtime));
189
+ await wait(this.getReconnectDelay(runtime), this.waitController.signal);
187
190
  await this.openConnection(true);
188
191
  }
192
+ getReconnectDelay(runtime) {
193
+ if (runtime?.closeCode === 4002 &&
194
+ String(runtime?.closeReason || '').trim().toLowerCase() === 'duplicate-host') {
195
+ return this.duplicateHostReconnectDelay;
196
+ }
197
+ return this.reconnectDelay;
198
+ }
189
199
  async dispatch(message) {
190
200
  const now = Date.now();
191
201
  if (this.runtime) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,5 +27,5 @@
27
27
  "typescript": "^5.6.3",
28
28
  "vitest": "^2.1.4"
29
29
  },
30
- "gitCommitId": "7e0bd83"
30
+ "gitCommitId": "db7f9bf"
31
31
  }