@love-moon/conductor-sdk 0.15.1 → 0.16.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/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # @love-moon/conductor-sdk
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7c45a56: `conductor issue start` can pick the AI for the task it spawns: `--backend <b>`,
8
+ or `--global-backend <backend>@<host>` to run the AI on one of your global AI
9
+ backends while the code stays on the project's daemon (same as the web "Move
10
+ Issue To Doing" dialog). The SDK's `updateIssue` forwards `globalBackend`.
11
+
12
+ Requires a Conductor web server that ships issue global backends (the same
13
+ release): deploy the web server before publishing the CLI/SDK. An older server
14
+ ignores `--global-backend` and starts a plain task on the project's daemon; the
15
+ CLI then prints a warning instead of failing silently.
16
+
17
+ ### Patch Changes
18
+
19
+ - 0495b79: The CLI can now do what the web app can:
20
+
21
+ - `conductor task`: `stop`, `interrupt`, `restart`, `delete`, `archive`/`unarchive`,
22
+ `rename`, `pin`/`unpin`, `move`, `labels`, `share`/`unshare`, `persistent`,
23
+ `round start|end`, `cleanup-worktree`, `terminal open|show|close`, `resume`,
24
+ `attachment download`, `shared <token|link>` (read a shared task), `transcribe <audio>`
25
+ (speech to text), and `schedule update`. `create` also takes `--daemon-host`, `--agent` (multi-agent group),
26
+ `--global-backend`, `--worktree`, `--remote-worktree` and `--persistent`.
27
+ `send --attach` uploads files, `messages --follow` streams new messages, and
28
+ `list --all-projects` / `--project-ids` lists tasks across projects.
29
+ - `conductor project`: `update`, `refresh`, `delete`, `reorder`, `agents`,
30
+ `collab invite|show-invite|join|leave`, `labels list|add|rename|remove`.
31
+ - `conductor issue`: `delete`, and `list --all-projects` / `--project-ids`.
32
+ - `conductor daemon`: `restart`, `upgrade [--status|--wait]`, `sessions`, `accounts`,
33
+ `switch-account`, `commands list|run|status`, `share create|list|revoke|show-invite|accept`.
34
+ - New `conductor search`, `conductor settings` (global backends, catchphrases, daily
35
+ report, task-list and card-group preferences, generated reports) and `conductor auth`
36
+ (`whoami`, `tokens list|create|revoke`).
37
+
38
+ Fix: `conductor project hide/unhide` (and the SDK's `setProjectHidden`) no longer erase
39
+ the project's metadata (task labels, memos, binding data).
40
+
41
+ On a cross-daemon merged project, `project hide/unhide`, `delete` and
42
+ `update --merge-opt-out` act on every daemon, like the web. `hide/unhide --json`
43
+ still prints the target project, plus `ids` for every project changed.
44
+ `delete --daemon-host <h>` deletes only that copy; a group delete stops at the
45
+ first failure and says which ids were deleted. `update --merge-opt-out` lists the
46
+ same-name projects it also split or re-merged.
47
+
48
+ - b68fadf: `conductor issue` now matches the web issue board:
49
+ `start --daemon <host> [--project <id>]` picks the daemon, or the merged-group
50
+ sibling project, that the task runs on (on another member's shared project the
51
+ server decides). `--priority` takes `P0|P1|P2`, and `--status` uses
52
+ `todo|doing|done`; `create` no longer offers `doing`. `list --status` is sent to
53
+ the server and still applied locally, so an older server that ignores it filters
54
+ too. An invalid argument now exits 2 without sending a request. The SDK's
55
+ `updateIssue` forwards `projectId`, and `listIssues` sends multiple statuses to
56
+ the server and filters the result the same way.
57
+
58
+ The matching web server changes (metadata patches merge instead of replacing,
59
+ `GET /api/issues?status=` filtering) should be deployed before publishing.
60
+
61
+ ## 0.15.2
62
+
3
63
  ## 0.15.1
4
64
 
5
65
  ## 0.15.0
@@ -31,12 +31,19 @@ export interface CreateIssueInput {
31
31
  clientRequestId?: string;
32
32
  }
33
33
  export interface UpdateIssueInput {
34
+ /** Re-parent the issue (e.g. onto a merged-group sibling when starting it). */
35
+ projectId?: string;
34
36
  title?: string;
35
37
  description?: string;
36
38
  priority?: string;
37
39
  type?: string;
38
40
  status?: string;
39
41
  metadata?: Record<string, unknown>;
42
+ /** RFC 0041: on todo→doing, run the spawned task's AI on this global backend. */
43
+ globalBackend?: {
44
+ host: string;
45
+ backend: string;
46
+ };
40
47
  }
41
48
  export interface UpdateIssueStatusOptions {
42
49
  evidence?: string;
@@ -21,6 +21,13 @@ const normalizeIssue = (payload) => {
21
21
  raw: payload,
22
22
  };
23
23
  };
24
+ // Legacy statuses the server serves as their canonical value
25
+ // (web/src/lib/issues/config.ts LEGACY_ISSUE_STATUS_ALIASES).
26
+ const LEGACY_ISSUE_STATUS_ALIASES = { backlog: 'todo', review: 'doing' };
27
+ const canonicalIssueStatus = (value) => {
28
+ const normalized = String(value ?? '').trim().toLowerCase();
29
+ return LEGACY_ISSUE_STATUS_ALIASES[normalized] ?? normalized;
30
+ };
24
31
  const extractIssue = (payload) => {
25
32
  // The PATCH /issues/[issueId] route returns `{ issue, activeTask, ... }`.
26
33
  // Other routes return the issue itself. Normalize both shapes here so
@@ -58,9 +65,9 @@ export class IssuesApi {
58
65
  if (!input.projectId) {
59
66
  throw new Error('projectId is required');
60
67
  }
61
- // The server `/api/issues` route accepts a single `status` query string;
62
- // it doesn't (yet) parse comma-separated lists, so we filter on the
63
- // client side for the multi-status case to match RFC 0025 §3 wording.
68
+ // The server filters on a comma-separated `status` (and maps legacy
69
+ // aliases such as `backlog` → `todo`); servers that predate that ignore
70
+ // it, so the result is filtered here too.
64
71
  const statusFilter = Array.isArray(input.status)
65
72
  ? input.status.map((value) => String(value).trim()).filter(Boolean)
66
73
  : input.status
@@ -68,12 +75,12 @@ export class IssuesApi {
68
75
  : [];
69
76
  const issues = await this.client.listIssues({
70
77
  projectId: input.projectId,
71
- status: statusFilter.length === 1 ? statusFilter[0] : undefined,
78
+ status: statusFilter.length > 0 ? statusFilter.join(',') : undefined,
72
79
  });
73
80
  let normalized = issues.map((entry) => normalizeIssue(entry));
74
- if (statusFilter.length > 1) {
75
- const statusSet = new Set(statusFilter);
76
- normalized = normalized.filter((issue) => statusSet.has(issue.status));
81
+ if (statusFilter.length > 0) {
82
+ const wanted = new Set(statusFilter.map(canonicalIssueStatus));
83
+ normalized = normalized.filter((issue) => wanted.has(canonicalIssueStatus(issue.status)));
77
84
  }
78
85
  if (typeof input.limit === 'number' && Number.isFinite(input.limit)) {
79
86
  normalized = normalized.slice(0, Math.max(0, Math.floor(input.limit)));
@@ -125,6 +132,9 @@ export class IssuesApi {
125
132
  throw new Error('issueId is required');
126
133
  }
127
134
  const params = {};
135
+ if (patch.projectId !== undefined) {
136
+ params.projectId = patch.projectId;
137
+ }
128
138
  if (patch.title !== undefined) {
129
139
  params.title = patch.title;
130
140
  }
@@ -140,6 +150,9 @@ export class IssuesApi {
140
150
  if (patch.status !== undefined) {
141
151
  params.status = patch.status;
142
152
  }
153
+ if (patch.globalBackend !== undefined) {
154
+ params.globalBackend = patch.globalBackend;
155
+ }
143
156
  // We always include audit metadata on writes. If the caller passes a
144
157
  // `metadata` patch, we merge our audit keys with theirs (caller wins).
145
158
  params.metadata = buildAuditMetadata(patch.metadata, this.options);
@@ -157,8 +170,8 @@ export class IssuesApi {
157
170
  });
158
171
  }
159
172
  // Pull the existing metadata so we can merge `qa.evidence` without
160
- // clobbering unrelated keys. The PATCH body fully replaces metadata, so
161
- // we have to round-trip the current state (matches RFC 0025 §3 wording
173
+ // clobbering sibling `qa.*` keys. The PATCH route only shallow-merges
174
+ // metadata, so we have to round-trip the current state (matches RFC 0025 §3 wording
162
175
  // for `done --evidence`).
163
176
  const existing = await this.getIssue(issueId);
164
177
  const existingMetadata = existing.metadata && typeof existing.metadata === 'object'
@@ -43,6 +43,8 @@ export interface CreateProjectInput {
43
43
  name?: string;
44
44
  workspacePath?: string;
45
45
  daemonHost?: string;
46
+ /** Ask the daemon to mkdir `workspacePath` when it does not exist yet. */
47
+ createWorkspaceIfMissing?: boolean;
46
48
  isDefault?: boolean;
47
49
  clientRequestId?: string;
48
50
  metadata?: Record<string, unknown>;
@@ -139,6 +139,9 @@ export class ProjectsApi {
139
139
  if (input.daemonHost) {
140
140
  params.daemonHost = input.daemonHost;
141
141
  }
142
+ if (input.createWorkspaceIfMissing) {
143
+ params.createWorkspaceIfMissing = true;
144
+ }
142
145
  }
143
146
  params.metadata = metadata;
144
147
  const summary = await this.client.createProject(params);
@@ -156,8 +159,12 @@ export class ProjectsApi {
156
159
  }
157
160
  async setProjectHidden(idOrName, hidden, options = {}) {
158
161
  const targetId = looksLikeProjectId(idOrName) ? idOrName.trim() : (await this.resolveById(idOrName)).id;
159
- const metadata = buildAuditMetadata(options.metadata, this.options);
160
- const payload = await this.client.patchProjectByQuery(targetId, { hidden, metadata });
162
+ // Send `hidden` only, exactly like the web UI. The project PATCH route
163
+ // *replaces* `project.metadata` with any `metadata` it receives, so the
164
+ // audit-only blob this used to send wiped the project's task labels, memos
165
+ // and binding data on every hide/unhide.
166
+ void options;
167
+ const payload = await this.client.patchProjectByQuery(targetId, { hidden });
161
168
  return normalizeProject(payload);
162
169
  }
163
170
  async resolveProject(input = {}) {
package/dist/api/tasks.js CHANGED
@@ -226,14 +226,15 @@ export class TasksApi {
226
226
  if (typeof content !== 'string' || content.length === 0) {
227
227
  throw new Error('content is required');
228
228
  }
229
- const userMetadata = options.clientRequestId
230
- ? { clientRequestId: options.clientRequestId, ...(options.metadata ?? {}) }
231
- : options.metadata;
232
- const metadata = buildAuditMetadata(userMetadata, this.options);
229
+ const metadata = buildAuditMetadata(options.metadata, this.options);
233
230
  const body = {
234
231
  content,
235
232
  metadata,
236
233
  };
234
+ // The server dedupes on the top-level key and stamps it onto the message metadata.
235
+ if (options.clientRequestId) {
236
+ body.clientRequestId = options.clientRequestId;
237
+ }
237
238
  if (options.role) {
238
239
  body.role = options.role;
239
240
  }
@@ -19,6 +19,12 @@ export class BackendApiError extends Error {
19
19
  this.details = details;
20
20
  }
21
21
  }
22
+ /**
23
+ * Server payload per summary, so `asObject()` keeps REST-only fields (hidden,
24
+ * mergeOptOut, gitRemoteUrl, metadata). A WeakMap keeps it off the instance's
25
+ * own (serialized) properties.
26
+ */
27
+ const projectSummaryPayloads = new WeakMap();
22
28
  export class ProjectSummary {
23
29
  id;
24
30
  name;
@@ -47,10 +53,13 @@ export class ProjectSummary {
47
53
  if (!id) {
48
54
  throw new Error('Project payload missing id');
49
55
  }
50
- 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, payload.lastCommitAt ?? payload.last_commit_at ?? null);
56
+ const summary = 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, payload.lastCommitAt ?? payload.last_commit_at ?? null);
57
+ projectSummaryPayloads.set(summary, payload);
58
+ return summary;
51
59
  }
52
60
  asObject() {
53
61
  return {
62
+ ...projectSummaryPayloads.get(this),
54
63
  id: this.id,
55
64
  name: this.name,
56
65
  daemonHost: this.daemonHost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
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": "4dd9818"
36
+ "gitCommitId": "77239b9"
37
37
  }