@love-moon/conductor-sdk 0.13.1 → 0.13.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @love-moon/conductor-sdk
2
2
 
3
+ ## 0.13.3
4
+
5
+ ## 0.13.2
6
+
7
+ ### Patch Changes
8
+
9
+ - 6547b2c: Add read-only daemon queries and archived-task search to the CLI:
10
+ `conductor daemon list [--all]` (online daemons, version, AI backends),
11
+ `conductor daemon tools <host>` (installed AI tools and reachability),
12
+ `conductor daemon quota <host> [--tool] [--refresh]` (usage windows / balance
13
+ per tool), and `conductor task list --archived [--search] [--all-projects]
14
+ [--page]`. The SDK's `BackendApiClient` gains `listAgents`,
15
+ `getAiManagerStatus`, `getAiManagerQuota` and `listAchievedTasks` over the
16
+ existing web API routes; no server change.
17
+ - 8e3a4c3: Report how many model tokens each turn consumed. After every turn (or `/goal`)
18
+ `conductor fire` sends the turn's token count (fresh input + cache reads/writes +
19
+ output) over the new `task_turn_usage` websocket event, and the server adds it
20
+ to the task's running total shown in the task detail card. A persistent task's
21
+ total restarts with each round.
22
+
23
+ Failed or interrupted turns and `/compact` are counted too; when a turn's usage
24
+ is unknown the fire reports `null`, which clears the task's last-turn count.
25
+
26
+ - `@love-moon/conductor-sdk`: new `ConductorClient.sendTurnUsage(taskId, { tokens })`.
27
+ - `@love-moon/ai-sdk`: the Codex app-server provider's turn/goal `usage` now
28
+ carries `turnTotalTokens`, the turn's share of Codex's thread-cumulative total.
29
+ Claude and Codex turn errors carry the tokens spent before the failure as
30
+ `error.usage` (Claude sums streamed usage when an interrupted query ends
31
+ without a result).
32
+ - Claude and Codex backends are counted; other backends report nothing yet.
33
+
34
+ - 6989999: `conductor task list --include-moved` also lists tasks moved into the project
35
+ from other projects (`project_id == P OR second_project_id == P`), with a MOVED
36
+ column naming the origin/target project. `--json` output now carries
37
+ `secondProjectId`. The SDK gains `TasksApi.listTasks({ includeMoved })`,
38
+ `BackendApiClient.listTasks({ projectScope: 'display' })` and
39
+ `Task/TaskSummary.secondProjectId`; it merges the server's existing real and
40
+ display scopes, so no server change is needed.
41
+
3
42
  ## 0.13.1
4
43
 
5
44
  ## 0.13.0
@@ -2,6 +2,8 @@ import { ApiClient, SdkClientOptions } from './shared.js';
2
2
  export interface Task {
3
3
  id: string;
4
4
  projectId: string | null;
5
+ /** Project the task was moved to in the web UI (display-only); null if not moved. */
6
+ secondProjectId?: string | null;
5
7
  issueId?: string | null;
6
8
  title: string;
7
9
  status: string;
@@ -91,6 +93,11 @@ export interface ListTasksInput {
91
93
  projectId?: string;
92
94
  issueId?: string;
93
95
  status?: string | string[];
96
+ /**
97
+ * Also include tasks moved into `projectId` from other projects (their
98
+ * `secondProjectId` is `projectId`), i.e. `projectId == P OR secondProjectId == P`.
99
+ */
100
+ includeMoved?: boolean;
94
101
  }
95
102
  export interface CreateTaskInput {
96
103
  projectId: string;
package/dist/api/tasks.js CHANGED
@@ -30,6 +30,7 @@ const normalizeTask = (payload) => {
30
30
  ? null
31
31
  : String(payload.project_id)
32
32
  : null,
33
+ secondProjectId: payload.secondProjectId ?? payload.second_project_id ?? null,
33
34
  issueId: payload.issueId ?? payload.issue_id ?? null,
34
35
  title: String(payload.title ?? ''),
35
36
  status: String(payload.status ?? ''),
@@ -134,10 +135,22 @@ export class TasksApi {
134
135
  // The server `/api/tasks` only filters on `project_id`; status / issue
135
136
  // filtering happens client-side here so we can support both string and
136
137
  // array `status` shapes from the facade input.
137
- const tasks = await this.client.listTasks({
138
- projectId: input.projectId,
139
- });
140
- let normalized = tasks.map((entry) => normalizeTask(typeof entry.asObject === 'function' ? entry.asObject() : entry));
138
+ const fetchNormalized = async (projectScope) => (await this.client.listTasks({ projectId: input.projectId, projectScope })).map((entry) => normalizeTask(typeof entry.asObject === 'function' ? entry.asObject() : entry));
139
+ let normalized;
140
+ if (input.includeMoved && input.projectId) {
141
+ // Real scope has every task whose projectId is P; display scope adds the
142
+ // ones moved in. Merging the two existing scopes needs no server change.
143
+ const [real, display] = await Promise.all([
144
+ fetchNormalized('real'),
145
+ fetchNormalized('display'),
146
+ ]);
147
+ const seen = new Set(real.map((task) => task.id));
148
+ const sortTime = (task) => Date.parse(task.updatedAt ?? task.createdAt ?? '') || 0;
149
+ normalized = [...real, ...display.filter((task) => !seen.has(task.id))].sort((a, b) => sortTime(b) - sortTime(a));
150
+ }
151
+ else {
152
+ normalized = await fetchNormalized('real');
153
+ }
141
154
  if (input.issueId) {
142
155
  normalized = normalized.filter((task) => task.issueId === input.issueId);
143
156
  }
@@ -78,7 +78,8 @@ export declare class TaskSummary {
78
78
  readonly createdAt?: string | null | undefined;
79
79
  readonly updatedAt?: string | null | undefined;
80
80
  readonly grouping?: (TaskGroupingSummary | null) | undefined;
81
- constructor(id: string, projectId: string | null, title: string, status: string, backendType?: string | null | undefined, sessionId?: string | null | undefined, sessionFilePath?: string | null | undefined, createdAt?: string | null | undefined, updatedAt?: string | null | undefined, grouping?: (TaskGroupingSummary | null) | undefined);
81
+ readonly secondProjectId?: string | null | undefined;
82
+ constructor(id: string, projectId: string | null, title: string, status: string, backendType?: string | null | undefined, sessionId?: string | null | undefined, sessionFilePath?: string | null | undefined, createdAt?: string | null | undefined, updatedAt?: string | null | undefined, grouping?: (TaskGroupingSummary | null) | undefined, secondProjectId?: string | null | undefined);
82
83
  static fromJSON(payload: Record<string, any>): TaskSummary;
83
84
  }
84
85
  export declare class BackendApiClient {
@@ -104,6 +105,7 @@ export declare class BackendApiClient {
104
105
  listTasks(params?: {
105
106
  projectId?: string;
106
107
  status?: string;
108
+ projectScope?: 'real' | 'display';
107
109
  }): Promise<TaskSummary[]>;
108
110
  createTask(params: BackendCreateTaskParams): Promise<TaskSummary>;
109
111
  /**
@@ -245,6 +247,26 @@ export declare class BackendApiClient {
245
247
  listScheduledMessages(taskId: string): Promise<Record<string, any>[]>;
246
248
  createScheduledMessage(taskId: string, params: Record<string, unknown>): Promise<Record<string, any>>;
247
249
  deleteScheduledMessage(taskId: string, scheduleId: string): Promise<void>;
250
+ /** Daemons (agents) currently connected for this user. */
251
+ listAgents(): Promise<Record<string, any>[]>;
252
+ /** Install / network status of the AI tools on a daemon. */
253
+ getAiManagerStatus(agentHost: string): Promise<Record<string, any>>;
254
+ /** Usage-window / balance quota of the AI tools on a daemon. */
255
+ getAiManagerQuota(agentHost: string, params?: {
256
+ tool?: string;
257
+ forceRefresh?: boolean;
258
+ }): Promise<Record<string, any>>;
259
+ /** Search archived (achieved) tasks by title and transcript; 10 per page. */
260
+ listAchievedTasks(params?: {
261
+ query?: string;
262
+ projectId?: string;
263
+ page?: number;
264
+ }): Promise<{
265
+ tasks: Record<string, any>[];
266
+ total: number;
267
+ page: number;
268
+ totalPages: number;
269
+ }>;
248
270
  private request;
249
271
  private buildUrl;
250
272
  private sendRequest;
@@ -75,7 +75,8 @@ export class TaskSummary {
75
75
  createdAt;
76
76
  updatedAt;
77
77
  grouping;
78
- constructor(id, projectId, title, status, backendType, sessionId, sessionFilePath, createdAt, updatedAt, grouping) {
78
+ secondProjectId;
79
+ constructor(id, projectId, title, status, backendType, sessionId, sessionFilePath, createdAt, updatedAt, grouping, secondProjectId) {
79
80
  this.id = id;
80
81
  this.projectId = projectId;
81
82
  this.title = title;
@@ -86,6 +87,7 @@ export class TaskSummary {
86
87
  this.createdAt = createdAt;
87
88
  this.updatedAt = updatedAt;
88
89
  this.grouping = grouping;
90
+ this.secondProjectId = secondProjectId;
89
91
  }
90
92
  static fromJSON(payload) {
91
93
  const id = payload.id ? String(payload.id) : '';
@@ -112,7 +114,7 @@ export class TaskSummary {
112
114
  : null,
113
115
  }
114
116
  : null;
115
- return new TaskSummary(id, payload.project_id ? String(payload.project_id) : null, title, status, payload.backend_type ?? payload.backendType ?? null, payload.session_id ?? payload.sessionId ?? null, payload.session_file_path ?? payload.sessionFilePath ?? null, payload.created_at ?? null, payload.updated_at ?? null, grouping);
117
+ return new TaskSummary(id, payload.project_id ? String(payload.project_id) : null, title, status, payload.backend_type ?? payload.backendType ?? null, payload.session_id ?? payload.sessionId ?? null, payload.session_file_path ?? payload.sessionFilePath ?? null, payload.created_at ?? null, payload.updated_at ?? null, grouping, payload.second_project_id ?? payload.secondProjectId ?? null);
116
118
  }
117
119
  }
118
120
  export class BackendApiClient {
@@ -157,10 +159,12 @@ export class BackendApiClient {
157
159
  const query = new URLSearchParams();
158
160
  if (params.projectId) {
159
161
  query.set('project_id', params.projectId);
160
- // Match the task's REAL project. By default the server groups by where the
161
- // user filed a task in the web UI (display-only), which would hide this
162
- // project's filed-out tasks from agents and include foreign ones.
163
- query.set('project_scope', 'real');
162
+ // Match the task's REAL project unless the caller asks for the web UI's
163
+ // display grouping (the server default), which hides this project's
164
+ // moved-out tasks and includes the ones moved in via `second_project_id`.
165
+ if (params.projectScope !== 'display') {
166
+ query.set('project_scope', 'real');
167
+ }
164
168
  }
165
169
  if (params.status) {
166
170
  query.set('status', params.status);
@@ -503,10 +507,54 @@ export class BackendApiClient {
503
507
  headers: agentActorHeaders(),
504
508
  });
505
509
  }
510
+ /** Daemons (agents) currently connected for this user. */
511
+ async listAgents() {
512
+ const response = await this.request('GET', '/api/agents');
513
+ const payload = await this.parseJson(response);
514
+ if (!Array.isArray(payload)) {
515
+ throw new BackendApiError('Invalid agents response: expected list', response.status, payload);
516
+ }
517
+ return payload;
518
+ }
519
+ /** Install / network status of the AI tools on a daemon. */
520
+ async getAiManagerStatus(agentHost) {
521
+ const query = new URLSearchParams({ agentHost });
522
+ const response = await this.request('GET', '/api/ai-manager/status', { query, timeoutMs: 45_000 });
523
+ return this.parseJson(response);
524
+ }
525
+ /** Usage-window / balance quota of the AI tools on a daemon. */
526
+ async getAiManagerQuota(agentHost, params = {}) {
527
+ const query = new URLSearchParams({ agentHost });
528
+ if (params.tool)
529
+ query.set('tool', params.tool);
530
+ if (params.forceRefresh)
531
+ query.set('forceRefresh', '1');
532
+ // The server waits up to 30s for the daemon to query every provider.
533
+ const response = await this.request('GET', '/api/ai-manager/quota', { query, timeoutMs: 45_000 });
534
+ return this.parseJson(response);
535
+ }
536
+ /** Search archived (achieved) tasks by title and transcript; 10 per page. */
537
+ async listAchievedTasks(params = {}) {
538
+ const query = new URLSearchParams();
539
+ if (params.query)
540
+ query.set('q', params.query);
541
+ if (params.projectId)
542
+ query.set('projectId', params.projectId);
543
+ if (params.page)
544
+ query.set('page', String(params.page));
545
+ const response = await this.request('GET', '/api/tasks/achieved', { query });
546
+ const payload = await this.parseJson(response);
547
+ if (!payload || !Array.isArray(payload.tasks)) {
548
+ throw new BackendApiError('Invalid achieved tasks response', response.status, payload);
549
+ }
550
+ return payload;
551
+ }
506
552
  async request(method, pathname, opts = {}) {
507
553
  const url = this.buildUrl(pathname, opts.query);
508
554
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
509
- const timeout = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
555
+ const timeout = controller
556
+ ? setTimeout(() => controller.abort(), opts.timeoutMs ?? this.timeoutMs)
557
+ : null;
510
558
  try {
511
559
  let response = await this.sendRequest(url, method, opts.body, controller, opts.headers);
512
560
  if (response.status === 404 &&
package/dist/client.d.ts CHANGED
@@ -118,6 +118,13 @@ export declare class ConductorClient {
118
118
  sendMessage(taskId: string, content: string, metadata?: Record<string, any>): Promise<Record<string, any>>;
119
119
  sendTaskStatus(taskId: string, payload: Record<string, any>): Promise<Record<string, any>>;
120
120
  sendRuntimeStatus(taskId: string, payload: Record<string, any>): Promise<Record<string, any>>;
121
+ /**
122
+ * Report the tokens one finished turn consumed; the server adds them to the task total.
123
+ * `null` marks a failed turn whose usage is unknown.
124
+ */
125
+ sendTurnUsage(taskId: string, payload: {
126
+ tokens: number | null;
127
+ }): Promise<Record<string, any>>;
121
128
  sendAgentResume(payload?: {
122
129
  active_tasks?: string[];
123
130
  source?: string;
package/dist/client.js CHANGED
@@ -305,6 +305,17 @@ export class ConductorClient {
305
305
  });
306
306
  return { delivered: true };
307
307
  }
308
+ /**
309
+ * Report the tokens one finished turn consumed; the server adds them to the task total.
310
+ * `null` marks a failed turn whose usage is unknown.
311
+ */
312
+ async sendTurnUsage(taskId, payload) {
313
+ await this.sendEnvelope({
314
+ type: 'task_turn_usage',
315
+ payload: { task_id: taskId, tokens: payload.tokens },
316
+ });
317
+ return { delivered: true };
318
+ }
308
319
  async sendAgentResume(payload = {}) {
309
320
  const storedCursor = this.downstreamCursorStore.get(this.agentHost);
310
321
  const lastAppliedCursor = payload.last_applied_cursor ?? (storedCursor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -33,5 +33,5 @@
33
33
  "typescript": "^5.6.3",
34
34
  "vitest": "^2.1.4"
35
35
  },
36
- "gitCommitId": "b82ba1c"
36
+ "gitCommitId": "d61c070"
37
37
  }