@love-moon/app-sdk 0.3.2
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 +135 -0
- package/README.md +173 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +30 -0
- package/dist/react/index.d.ts +364 -0
- package/dist/react/index.js +814 -0
- package/dist/react/styles.css +264 -0
- package/dist/server/index.d.ts +376 -0
- package/dist/server/index.js +1387 -0
- package/examples/01_example/.env.example +17 -0
- package/examples/01_example/README.md +80 -0
- package/examples/01_example/chat-cli.mjs +125 -0
- package/examples/01_example/package-lock.json +52 -0
- package/examples/01_example/package.json +13 -0
- package/examples/02_bff/.env.example +16 -0
- package/examples/02_bff/README.md +63 -0
- package/examples/02_bff/app/api/conductor/[...path]/route.ts +277 -0
- package/examples/02_bff/app/api/conductor/bind/route.ts +45 -0
- package/examples/02_bff/app/layout.tsx +25 -0
- package/examples/02_bff/app/page.tsx +114 -0
- package/examples/02_bff/lib/conductor.ts +60 -0
- package/examples/02_bff/next.config.mjs +9 -0
- package/examples/02_bff/package-lock.json +1001 -0
- package/examples/02_bff/package.json +25 -0
- package/examples/02_bff/tsconfig.json +40 -0
- package/package.json +79 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @love-moon/app-sdk
|
|
2
|
+
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `ChatEvent.task_failed.error` and `StreamReplyDelta.error` now include
|
|
8
|
+
optional `details?: unknown` and `cause?: unknown` fields. Additive,
|
|
9
|
+
non-breaking — existing consumers ignore the new fields. The default WS
|
|
10
|
+
→ ChatEvent translation forwards both from `ConductorAppError`, so host
|
|
11
|
+
UIs can surface server payloads / request IDs / underlying causes without
|
|
12
|
+
depending on the SDK error class directly. The `<ChatView />` React store
|
|
13
|
+
(`useChat().state.error`) also carries these through.
|
|
14
|
+
- `examples/02_bff` BFF now hardcodes `role: 'user'` and strips
|
|
15
|
+
`metadata.audit` on `POST /messages`. **Integrators copying the example
|
|
16
|
+
must follow this pattern.** Without it, a malicious browser could forge
|
|
17
|
+
`role: 'system'|'assistant'` messages or stamp `audit.actor='app'` —
|
|
18
|
+
bypassing `streamReply`'s SDK-echo filter and impersonating server-side
|
|
19
|
+
app messages. The BFF is the only point in the pipeline that knows the
|
|
20
|
+
browser is untrusted; do not push role/audit choices down to the SDK.
|
|
21
|
+
- `client.tasks.streamReply()` now applies a default **120s idle timeout**
|
|
22
|
+
between consecutive `text` / status-transition deltas. If no progress is
|
|
23
|
+
observed in that window the iterator yields a terminal
|
|
24
|
+
`{ type: 'error', error: { code: 'stream_aborted', message: 'idle timeout' } }`
|
|
25
|
+
and closes. Long-running backends (e.g. tools that may legitimately go
|
|
26
|
+
silent for minutes) can opt out by passing `idleTimeoutMs: 0`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
for await (const delta of client.tasks.streamReply(taskId, { idleTimeoutMs: 0 })) {
|
|
30
|
+
// never time out — caller is responsible for cancelling via signal
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
- **Locked down the public `.d.ts` surface.** Internal transport types —
|
|
34
|
+
`Fetcher`, `FetcherOptions`, `RequestOptions`, `AppWebSocket`,
|
|
35
|
+
`AppWebSocketOptions`, `TasksRestApi` — are now tagged
|
|
36
|
+
`/** @internal */` and stripped from generated d.ts via tsup's
|
|
37
|
+
`dts.compilerOptions.stripInternal`. Previously these appeared as
|
|
38
|
+
`declare class …` in `dist/server/index.d.ts` (even though not
|
|
39
|
+
re-exported) and TypeScript surfaced them on hover / structural
|
|
40
|
+
reference. The `AppClient._internals` test-seam getter has been removed
|
|
41
|
+
(it was unused). The CI `bundle-smoke` script now also asserts none of
|
|
42
|
+
these symbols leak into any d.ts as a regression net.
|
|
43
|
+
- **`AppClient.close()` now safely terminates in-flight subscribe
|
|
44
|
+
iterators.** When a `for await` loop on `tasks.subscribe(taskId)` is
|
|
45
|
+
mid-stream and the caller invokes `client.close()`, the iterator now
|
|
46
|
+
yields a synthetic
|
|
47
|
+
`{ type: 'task_failed', taskId, error: { code: 'subscribe_failed', message: 'client closed' } }`
|
|
48
|
+
and returns instead of hanging silently. Same for `tasks.streamReply()`
|
|
49
|
+
— it surfaces an `{ type: 'error', error: { code: 'subscribe_failed' } }`
|
|
50
|
+
delta. `close()` is also idempotent (a second call is a no-op). After
|
|
51
|
+
close, calls to `tasks.subscribe()` / `tasks.streamReply()` / any
|
|
52
|
+
`tasks.*` REST method throw a synchronous
|
|
53
|
+
`ConductorAppError({ code: 'subscribe_failed', message: 'client is closed' })`
|
|
54
|
+
rather than returning a hanging iterator. Implemented internally by a
|
|
55
|
+
new `AppWebSocket.onClose(listener)` channel; the public API surface
|
|
56
|
+
is unchanged.
|
|
57
|
+
|
|
58
|
+
## 0.1.0
|
|
59
|
+
|
|
60
|
+
Initial implementation, RFC 0027 milestones M0–M3.
|
|
61
|
+
|
|
62
|
+
### Added — package layout (M0)
|
|
63
|
+
|
|
64
|
+
- Single npm package with three subpath entries:
|
|
65
|
+
- `@love-moon/app-sdk` (root, types-only + `SDK_VERSION`)
|
|
66
|
+
- `@love-moon/app-sdk/server` (Node)
|
|
67
|
+
- `@love-moon/app-sdk/react` (browser / React)
|
|
68
|
+
- `@love-moon/app-sdk/react/styles.css`
|
|
69
|
+
- tsup multi-entry build; per-entry tsconfig conditions.
|
|
70
|
+
- `react` + `react-dom` as optional peer dependencies — server-only consumers
|
|
71
|
+
don't get warnings.
|
|
72
|
+
- CI bundle smoke test (`npm run test:bundle`) statically asserts no Node
|
|
73
|
+
symbols leak into `/react` and no DOM symbols leak into `/server`.
|
|
74
|
+
- vitest with per-file `@vitest-environment` directives (node + jsdom).
|
|
75
|
+
|
|
76
|
+
### Added — `/server` (M1)
|
|
77
|
+
|
|
78
|
+
- `connect()` + `AppClient` with `projects` and `tasks` sub-APIs.
|
|
79
|
+
- `projects.bind()` — idempotent find-or-create on (daemonHost, workspacePath);
|
|
80
|
+
stamps `metadata.audit.createdByApp` on creation (zero schema change).
|
|
81
|
+
- `projects.list()` / `projects.get()`.
|
|
82
|
+
- `tasks.create()` / `tasks.get()` / `tasks.list()`.
|
|
83
|
+
- `tasks.sendMessage()` with auto-generated `clientRequestId` (idempotent).
|
|
84
|
+
- `tasks.history()` with cursor-based pagination.
|
|
85
|
+
- `tasks.interrupt()` with `targetReplyTo`.
|
|
86
|
+
- `tasks.subscribe(taskId)` — `AsyncIterable<ChatEvent>` over `/ws/app` with
|
|
87
|
+
taskId-side filtering, capped backoff reconnect, and a per-iterator abort.
|
|
88
|
+
- `tasks.streamReply(taskId)` — `AsyncIterable<StreamReplyDelta>` built on
|
|
89
|
+
subscribe; yields `text` deltas from `reply_preview` plus a terminal `done`
|
|
90
|
+
on the assistant message (or `error` on `task_status_update=failed`).
|
|
91
|
+
- Unified `ConductorAppError` with named error codes mapped from HTTP status
|
|
92
|
+
+ backend error strings.
|
|
93
|
+
- Custom fetch / WebSocket / bearerToken providers for SSR + test injection.
|
|
94
|
+
|
|
95
|
+
### Added — `/react` (M2)
|
|
96
|
+
|
|
97
|
+
- `<ChatView />` — composed widget: runtime status bar + message list + input.
|
|
98
|
+
- `<MessageList />`, `<MessageInput />`, `<RuntimeStatusBar />` —
|
|
99
|
+
building-block components for compose-your-own layouts.
|
|
100
|
+
- `<ChatProvider />` + `useChat()` — React Context + useReducer chat state
|
|
101
|
+
(no zustand dep; per-instance store so multiple widgets on the same page
|
|
102
|
+
don't collide).
|
|
103
|
+
- `createRestAdapter()` — default `ChatAdapter` implementation that talks to
|
|
104
|
+
a host BFF exposing 4 routes (`messages` GET/POST, `interrupt`, `events`
|
|
105
|
+
SSE).
|
|
106
|
+
- Optimistic send → server confirm flow with pending-message replacement.
|
|
107
|
+
- Pre-compiled CSS at `@love-moon/app-sdk/react/styles.css`; CSS-variable
|
|
108
|
+
theming via the `theme` prop; mobile/desktop layout via responsive CSS
|
|
109
|
+
+ an explicit `layout` prop override.
|
|
110
|
+
- jsdom integration tests covering hydration, live events, runtime status,
|
|
111
|
+
interrupt button visibility, and optimistic send.
|
|
112
|
+
|
|
113
|
+
### Added — examples + docs (M3)
|
|
114
|
+
|
|
115
|
+
- `examples/01_example/` — minimal Node CLI demo: bind project → create task
|
|
116
|
+
→ stream AI reply to stdout. ~35 lines of business code, no UI / BFF.
|
|
117
|
+
- `examples/02_bff/` — runnable Next.js demo: BFF wraps `/server`,
|
|
118
|
+
page mounts `<ChatView />`, ~120 lines of business code total.
|
|
119
|
+
- `lib/conductor.ts` — singleton AppClient.
|
|
120
|
+
- `app/api/conductor/bind/route.ts` — `projects.bind()` + `tasks.create()`.
|
|
121
|
+
- `app/api/conductor/[...path]/route.ts` — catch-all BFF translating to
|
|
122
|
+
the 4 widget-expected routes, including a 30-line SSE bridge over
|
|
123
|
+
`tasks.subscribe()`.
|
|
124
|
+
- README with quickstart, full integration recipe, and security model.
|
|
125
|
+
|
|
126
|
+
### Known gaps
|
|
127
|
+
|
|
128
|
+
- v0.1 chat UI is functional but visually minimal. RFC §4 phase B (physical
|
|
129
|
+
extraction of `web/src/features/chat` polished components, including
|
|
130
|
+
Markdown / attachments / 1423-line regression test) is a follow-up PR.
|
|
131
|
+
- Streaming surface yields `text` cumulative previews, not token-level
|
|
132
|
+
deltas. Real token streaming requires a backend envelope addition; the
|
|
133
|
+
`StreamReplyDelta` shape is forward-compatible.
|
|
134
|
+
- Attachments not implemented in the default REST adapter or `<ChatView>`;
|
|
135
|
+
`Attachment` types are defined for forward compat.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @love-moon/app-sdk
|
|
2
|
+
|
|
3
|
+
Embed Conductor AI tools into third-party apps. One npm package, three
|
|
4
|
+
subpath entries:
|
|
5
|
+
|
|
6
|
+
| Entry | Environment | Purpose |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| `@love-moon/app-sdk` | any | Pure types + `SDK_VERSION`. Safe everywhere. |
|
|
9
|
+
| `@love-moon/app-sdk/server` | Node 18+ | SDK for third-party **backends** to talk to Conductor REST + `/ws/app`. |
|
|
10
|
+
| `@love-moon/app-sdk/react` | browser / React 18+ | Chat widget (`<ChatView />`) wired by a `ChatAdapter`. |
|
|
11
|
+
|
|
12
|
+
See [RFC 0027](../../claw/rfc/0027-feature-conductor-app-sdk.md) for the design.
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
|
|
16
|
+
Two runnable examples ship with the package, under [`examples/`](./examples/):
|
|
17
|
+
|
|
18
|
+
| Example | Lines | What it shows |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| [`examples/01_example/`](./examples/01_example/) | ~35 LOC | **Smallest possible app.** Pure Node CLI: bind project → create task → stream the AI reply to stdout. Use this to learn the SDK in 60 seconds. |
|
|
21
|
+
| [`examples/02_bff/`](./examples/02_bff/) | ~120 LOC | Full-stack: Next.js BFF + React `<ChatView />` widget + SSE bridge over `/ws/app`. Use this as a template for real browser integrations. |
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
A complete integration is ~120 lines of business code spread across a Next.js
|
|
26
|
+
BFF and a React page. See [`examples/02_bff/`](./examples/02_bff/) for the
|
|
27
|
+
full runnable demo, or [`examples/01_example/`](./examples/01_example/) for
|
|
28
|
+
a pure-Node CLI.
|
|
29
|
+
|
|
30
|
+
### Backend (the only place your Conductor token lives)
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// lib/conductor.ts
|
|
34
|
+
import { connect } from '@love-moon/app-sdk/server';
|
|
35
|
+
|
|
36
|
+
export const client = await connect({
|
|
37
|
+
baseUrl: 'https://conductor.example.com',
|
|
38
|
+
bearerToken: process.env.CONDUCTOR_TOKEN!,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export async function bindProject() {
|
|
42
|
+
return client.projects.bind({
|
|
43
|
+
name: 'Acme Dashboard',
|
|
44
|
+
daemonHost: process.env.CONDUCTOR_DAEMON_HOST!,
|
|
45
|
+
workspacePath: process.env.CONDUCTOR_WORKSPACE_PATH!,
|
|
46
|
+
});
|
|
47
|
+
// Idempotent: matches by (daemonHost, workspacePath); creates only on miss.
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create a task once you've got the project id. `initialMessage` is the
|
|
51
|
+
// kickoff prompt — the AI reply to that message starts streaming as soon
|
|
52
|
+
// as you subscribe below.
|
|
53
|
+
const task = await client.tasks.create({
|
|
54
|
+
projectId: project.id,
|
|
55
|
+
title: 'Investigate billing anomaly',
|
|
56
|
+
initialMessage: 'Look at the last 24h of charges.',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Subscribe to receive the AI's reply (and any subsequent events on the
|
|
60
|
+
// task). Use `sendMessage` to add follow-up turns from your code; the demo
|
|
61
|
+
// here shows a single-turn flow, so we just consume events until the task
|
|
62
|
+
// finishes. To send a follow-up turn before exiting, call
|
|
63
|
+
// `client.tasks.sendMessage(task.id, 'drill into the top one')` and keep
|
|
64
|
+
// the loop running.
|
|
65
|
+
for await (const evt of client.tasks.subscribe(task.id)) {
|
|
66
|
+
if (evt.type === 'message_appended') console.log(evt.message.content);
|
|
67
|
+
if (evt.type === 'task_finished') break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// streamReply: stream the AI's reply as it builds up. Defaults to a 120s
|
|
71
|
+
// idle timeout between deltas; pass `idleTimeoutMs: 0` to disable for
|
|
72
|
+
// long-running backends that may legitimately go silent for minutes.
|
|
73
|
+
for await (const delta of client.tasks.streamReply(task.id, { idleTimeoutMs: 0 })) {
|
|
74
|
+
if (delta.type === 'text') process.stdout.write(delta.text);
|
|
75
|
+
if (delta.type === 'done') break;
|
|
76
|
+
if (delta.type === 'error') throw new Error(delta.error.message);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Frontend (chat widget)
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { ChatView, createRestAdapter } from '@love-moon/app-sdk/react';
|
|
84
|
+
import '@love-moon/app-sdk/react/styles.css';
|
|
85
|
+
|
|
86
|
+
const adapter = createRestAdapter({
|
|
87
|
+
baseUrl: '/api/conductor', // your BFF, not Conductor directly
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export default function ChatPage({ taskId }: { taskId: string }) {
|
|
91
|
+
return <ChatView taskId={taskId} adapter={adapter} />;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Backend-for-Frontend (the 4 routes the widget speaks)
|
|
96
|
+
|
|
97
|
+
The widget's default `createRestAdapter` expects:
|
|
98
|
+
|
|
99
|
+
| Route | Forward to |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `GET /tasks/:id/messages?pagination=1&limit=N&before_id=...` | `client.tasks.history()` |
|
|
102
|
+
| `POST /tasks/:id/messages` | `client.tasks.sendMessage()` |
|
|
103
|
+
| `POST /tasks/:id/interrupt` | `client.tasks.interrupt()` |
|
|
104
|
+
| `GET /tasks/:id/events` (text/event-stream) | `client.tasks.subscribe()` via SSE bridge |
|
|
105
|
+
|
|
106
|
+
The SSE bridge over Conductor's `/ws/app` is the only non-trivial piece —
|
|
107
|
+
~30 lines of code. See the
|
|
108
|
+
[example catch-all route](./examples/02_bff/app/api/conductor/[...path]/route.ts)
|
|
109
|
+
for the full pattern.
|
|
110
|
+
|
|
111
|
+
## Why a single package, three entries
|
|
112
|
+
|
|
113
|
+
The widget and the BFF must agree on wire format and event shapes; coupling
|
|
114
|
+
them in a single package + shared types makes that contract impossible to
|
|
115
|
+
accidentally split. Subpath exports + `peerDependenciesMeta.optional = true`
|
|
116
|
+
ensure server-only consumers don't pay for React, and browsers never see Node
|
|
117
|
+
code. See [Option F in the RFC](../../claw/rfc/0027-feature-conductor-app-sdk.md#proposed-design).
|
|
118
|
+
|
|
119
|
+
## Security model
|
|
120
|
+
|
|
121
|
+
The token used by `@love-moon/app-sdk/server` is equivalent to the user's
|
|
122
|
+
Conductor account. **Never** put it in a browser bundle or expose it to
|
|
123
|
+
untrusted code. The intended deployment is:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
[browser widget] ──▶ [your BFF, holds token] ──▶ [Conductor backend] ──▶ [daemon]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The widget never receives the Conductor token — it talks only to your BFF.
|
|
130
|
+
|
|
131
|
+
## Status
|
|
132
|
+
|
|
133
|
+
| Milestone | Status |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| M0 — package skeleton + exports + bundle smoke | ✓ |
|
|
136
|
+
| M1 — `/server` REST + WS subscribe + streamReply + tests | ✓ |
|
|
137
|
+
| M2 — `/react` widget + default REST adapter + integration tests | ✓ |
|
|
138
|
+
| M3 — `examples/01_example` CLI + `examples/02_bff` Next.js demo + this README | ✓ |
|
|
139
|
+
|
|
140
|
+
The v0.1 widget is intentionally minimal in visuals — the eventual physical
|
|
141
|
+
extraction of the polished UI from `web/src/features/chat` (RFC §4) is a
|
|
142
|
+
future PR. The widget's component API + adapter contract are stable now.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
cd modules/app-sdk
|
|
148
|
+
npm install # via root npm workspaces
|
|
149
|
+
npm run build # tsup + Tailwind copy
|
|
150
|
+
npm test # vitest: unit + integration tests across node + jsdom
|
|
151
|
+
npm run typecheck
|
|
152
|
+
npm run test:bundle # static guard: no Node code in /react, no DOM in /server
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Project layout
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
src/
|
|
159
|
+
types/ Pure types — Task / Message / RuntimeStatus / ChatEvent / ChatAdapter / errors
|
|
160
|
+
index.ts Root entry (re-exports types)
|
|
161
|
+
server/ Node-only: connect() / AppClient / projects.bind / tasks.* / ws subscribe / streamReply
|
|
162
|
+
react/ Browser-only: <ChatView />, components, default REST adapter, styles
|
|
163
|
+
test/
|
|
164
|
+
server/ node-env tests (fetcher, projects, tasks, subscribe, streamReply)
|
|
165
|
+
react/ jsdom-env tests (<ChatView /> integration)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Versioning policy
|
|
169
|
+
|
|
170
|
+
- 1.x freezes `<ChatView>` props, `AppClient` methods, `ChatAdapter` interface,
|
|
171
|
+
and root type exports.
|
|
172
|
+
- Adding a new `ChatEvent` variant or a new SDK method is a minor bump.
|
|
173
|
+
- Removing or renaming any of the above is a major bump.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task: a single AI conversation within a project.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the shape returned by Conductor's `/api/tasks/[taskId]` REST endpoint
|
|
5
|
+
* (camelCase normalized). All fields are present after a successful read; the
|
|
6
|
+
* `null`-able fields reflect cases where the backend hasn't populated them yet
|
|
7
|
+
* (e.g. a task created via SDK before its daemon picks it up).
|
|
8
|
+
*/
|
|
9
|
+
interface Task {
|
|
10
|
+
id: string;
|
|
11
|
+
projectId: string;
|
|
12
|
+
title: string;
|
|
13
|
+
status: TaskStatus;
|
|
14
|
+
/** Backend execution engine ("claude_code" / "codex" / etc.) or null when not selected yet. */
|
|
15
|
+
backendType: string | null;
|
|
16
|
+
/** AI session id assigned by the daemon; null until the daemon attaches. */
|
|
17
|
+
sessionId: string | null;
|
|
18
|
+
sessionFilePath: string | null;
|
|
19
|
+
/** ISO 8601 timestamps. Always present after the task is persisted. */
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
type TaskStatus = 'pending' | 'running' | 'finished' | 'failed' | 'cancelled' | string;
|
|
24
|
+
interface CreateTaskInput {
|
|
25
|
+
projectId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional first user message. When provided, the backend persists it as
|
|
29
|
+
* the task's initial message right after creation.
|
|
30
|
+
* Maps to the backend's `initialContent` field; the SDK renames it for
|
|
31
|
+
* symmetry with `sendMessage(taskId, content)`.
|
|
32
|
+
*/
|
|
33
|
+
initialMessage?: string;
|
|
34
|
+
/** Backend type override (claude_code / codex / kimi-cli / etc.). */
|
|
35
|
+
backendType?: string;
|
|
36
|
+
/** Free-form metadata; merged with SDK audit fields server-side. */
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Project: a workspace binding (daemon + filesystem path) inside which tasks
|
|
42
|
+
* are scoped. Maps to Conductor's Project model.
|
|
43
|
+
*
|
|
44
|
+
* `daemonHost` and `workspacePath` are the binding identity. App-SDK's
|
|
45
|
+
* `projects.bind()` is idempotent on this pair.
|
|
46
|
+
*/
|
|
47
|
+
interface Project {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
daemonHost: string | null;
|
|
51
|
+
workspacePath: string | null;
|
|
52
|
+
repoRoot: string | null;
|
|
53
|
+
worktreeBranch: string | null;
|
|
54
|
+
lastCommit: string | null;
|
|
55
|
+
lastCommitAt: string | null;
|
|
56
|
+
fileCount: number | null;
|
|
57
|
+
isDefault: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* True when this project was created via the App SDK (audit hint, derived
|
|
60
|
+
* from `metadata.audit.createdByApp`). Read-only flag for UI affordances.
|
|
61
|
+
*/
|
|
62
|
+
createdByApp: boolean;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
updatedAt: string;
|
|
65
|
+
}
|
|
66
|
+
interface BindProjectInput {
|
|
67
|
+
name: string;
|
|
68
|
+
daemonHost: string;
|
|
69
|
+
workspacePath: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional override for the audit `createdByApp.name` field. Defaults to the
|
|
72
|
+
* `name` argument. Only used when the SDK has to create a new project (no
|
|
73
|
+
* existing binding found).
|
|
74
|
+
*/
|
|
75
|
+
appLabel?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Message: one entry in a task's chat transcript.
|
|
80
|
+
*
|
|
81
|
+
* Roles:
|
|
82
|
+
* - 'user' — message sent by the user (or the SDK on their behalf).
|
|
83
|
+
* - 'sdk' — message sent by an SDK / CLI / app integration (still
|
|
84
|
+
* appears in the chat as a user-side bubble).
|
|
85
|
+
* - 'assistant' — AI reply.
|
|
86
|
+
* - 'system' — system-emitted notice (rare; renders muted).
|
|
87
|
+
*
|
|
88
|
+
* `role` is intentionally open-vocabulary; new backend roles render as
|
|
89
|
+
* a generic bubble.
|
|
90
|
+
*/
|
|
91
|
+
interface Message {
|
|
92
|
+
id: string;
|
|
93
|
+
taskId: string;
|
|
94
|
+
role: MessageRole | string;
|
|
95
|
+
content: string;
|
|
96
|
+
metadata: Record<string, unknown> | null;
|
|
97
|
+
attachments: Attachment[];
|
|
98
|
+
createdAt: string;
|
|
99
|
+
}
|
|
100
|
+
type MessageRole = 'user' | 'sdk' | 'assistant' | 'system';
|
|
101
|
+
interface Attachment {
|
|
102
|
+
id: string;
|
|
103
|
+
filename: string;
|
|
104
|
+
mimeType: string;
|
|
105
|
+
sizeBytes: number;
|
|
106
|
+
/** Resolvable URL or relative path the host must turn into a fetchable URL. */
|
|
107
|
+
url: string;
|
|
108
|
+
}
|
|
109
|
+
interface SendMessageInput {
|
|
110
|
+
content: string;
|
|
111
|
+
/** Idempotency key. When omitted, the SDK auto-generates a UUID. */
|
|
112
|
+
clientRequestId?: string;
|
|
113
|
+
metadata?: Record<string, unknown>;
|
|
114
|
+
/** Optional attachments to include with this message; must be pre-uploaded. */
|
|
115
|
+
attachmentIds?: string[];
|
|
116
|
+
/** Role override; defaults to 'sdk'. */
|
|
117
|
+
role?: MessageRole;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Runtime status pushed by the daemon while a task is in progress.
|
|
122
|
+
* Mirrors `task_runtime_status` envelope on /ws/app.
|
|
123
|
+
*/
|
|
124
|
+
interface RuntimeStatus {
|
|
125
|
+
taskId: string;
|
|
126
|
+
/** High-level phase: 'idle' | 'thinking' | 'tool_call' | 'awaiting_user' | 'done'. */
|
|
127
|
+
state: RuntimeState;
|
|
128
|
+
phase?: string | null;
|
|
129
|
+
source?: string | null;
|
|
130
|
+
/** Short status line shown next to the AI avatar ("Reading file X..."). */
|
|
131
|
+
statusLine?: string | null;
|
|
132
|
+
/** Final status line shown when the reply finishes. */
|
|
133
|
+
statusDoneLine?: string | null;
|
|
134
|
+
replyPreview?: string | null;
|
|
135
|
+
replyTo?: string | null;
|
|
136
|
+
replyInProgress?: boolean;
|
|
137
|
+
backend?: string | null;
|
|
138
|
+
threadId?: string | null;
|
|
139
|
+
daemon?: string | null;
|
|
140
|
+
pid?: number | null;
|
|
141
|
+
sessionId?: string | null;
|
|
142
|
+
sessionFilePath?: string | null;
|
|
143
|
+
tokenUsagePercent?: number | null;
|
|
144
|
+
contextUsagePercent?: number | null;
|
|
145
|
+
createdAt?: string | null;
|
|
146
|
+
}
|
|
147
|
+
type RuntimeState = 'idle' | 'thinking' | 'tool_call' | 'awaiting_user' | 'done' | string;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* ChatEvent: the minimal set of events the chat widget needs to render.
|
|
151
|
+
*
|
|
152
|
+
* This is intentionally a small union — the widget should not have to
|
|
153
|
+
* pattern-match dozens of envelope types. The default REST/WS adapter
|
|
154
|
+
* funnels raw `/ws/app` envelopes into ChatEvents; custom adapters can do the
|
|
155
|
+
* same translation against any wire format.
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Error shape carried inside `task_failed` ChatEvents and `error`
|
|
159
|
+
* StreamReplyDeltas.
|
|
160
|
+
*
|
|
161
|
+
* `details` and `cause` are optional, free-form passthroughs of the original
|
|
162
|
+
* SDK error's structured fields — useful for surfacing request IDs / server
|
|
163
|
+
* payloads in host UIs without depending on the SDK error class directly.
|
|
164
|
+
*/
|
|
165
|
+
interface ChatEventError {
|
|
166
|
+
code: string;
|
|
167
|
+
message: string;
|
|
168
|
+
details?: unknown;
|
|
169
|
+
cause?: unknown;
|
|
170
|
+
}
|
|
171
|
+
type ChatEvent = {
|
|
172
|
+
type: 'message_appended';
|
|
173
|
+
message: Message;
|
|
174
|
+
} | {
|
|
175
|
+
type: 'message_updated';
|
|
176
|
+
message: Message;
|
|
177
|
+
} | {
|
|
178
|
+
type: 'runtime_status';
|
|
179
|
+
status: RuntimeStatus;
|
|
180
|
+
} | {
|
|
181
|
+
type: 'task_finished';
|
|
182
|
+
taskId: string;
|
|
183
|
+
} | {
|
|
184
|
+
type: 'task_failed';
|
|
185
|
+
taskId: string;
|
|
186
|
+
error: ChatEventError;
|
|
187
|
+
} | {
|
|
188
|
+
type: 'connection_state';
|
|
189
|
+
state: 'connected' | 'reconnecting' | 'offline';
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* StreamReplyDelta: a streaming AI reply chunk yielded by
|
|
193
|
+
* `client.tasks.streamReply(id)`.
|
|
194
|
+
*
|
|
195
|
+
* v1 semantics: each `text` delta is a *cumulative* preview-style chunk
|
|
196
|
+
* (built from `task_runtime_status.reply_preview` rolling state). The final
|
|
197
|
+
* `done` delta carries the full reply Message. A future RFC may add real
|
|
198
|
+
* token-level streaming; the shape is forward-compatible.
|
|
199
|
+
*/
|
|
200
|
+
type StreamReplyDelta = {
|
|
201
|
+
type: 'text';
|
|
202
|
+
text: string;
|
|
203
|
+
replyTo: string;
|
|
204
|
+
} | {
|
|
205
|
+
type: 'status';
|
|
206
|
+
status: RuntimeStatus;
|
|
207
|
+
} | {
|
|
208
|
+
type: 'done';
|
|
209
|
+
message: Message;
|
|
210
|
+
} | {
|
|
211
|
+
type: 'error';
|
|
212
|
+
error: ChatEventError;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* ChatAdapter: the contract between the React widget and *something* that
|
|
217
|
+
* can talk to a Conductor-shaped backend. The widget calls these methods;
|
|
218
|
+
* implementations decide how to translate to HTTP / WS / GraphQL / in-process
|
|
219
|
+
* calls.
|
|
220
|
+
*
|
|
221
|
+
* The default implementation `createRestAdapter` (in `/react`) talks to a
|
|
222
|
+
* BFF that mirrors Conductor's REST shape. Hosts using a custom wire format
|
|
223
|
+
* just implement this interface directly.
|
|
224
|
+
*/
|
|
225
|
+
interface ChatAdapter {
|
|
226
|
+
fetchHistory(taskId: string, opts?: {
|
|
227
|
+
beforeId?: string;
|
|
228
|
+
limit?: number;
|
|
229
|
+
signal?: AbortSignal;
|
|
230
|
+
}): Promise<{
|
|
231
|
+
messages: Message[];
|
|
232
|
+
hasMoreBefore: boolean;
|
|
233
|
+
oldestMessageId: string | null;
|
|
234
|
+
}>;
|
|
235
|
+
subscribe(taskId: string, handler: (event: ChatEvent) => void): {
|
|
236
|
+
unsubscribe(): void;
|
|
237
|
+
};
|
|
238
|
+
sendMessage(taskId: string, input: SendMessageInput): Promise<Message>;
|
|
239
|
+
interrupt(taskId: string, opts: {
|
|
240
|
+
targetReplyTo: string;
|
|
241
|
+
}): Promise<void>;
|
|
242
|
+
/** Optional. Adapters that don't support attachments may omit. */
|
|
243
|
+
uploadAttachment?(taskId: string, file: File | Blob, opts?: {
|
|
244
|
+
signal?: AbortSignal;
|
|
245
|
+
filename?: string;
|
|
246
|
+
}): Promise<{
|
|
247
|
+
id: string;
|
|
248
|
+
url: string;
|
|
249
|
+
}>;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* ConductorAppError: every error thrown out of the SDK is one of these.
|
|
254
|
+
* Callers can `if (err instanceof ConductorAppError) switch (err.code)`.
|
|
255
|
+
*
|
|
256
|
+
* `code` is open-vocabulary for forward compat; the SDK promises to never
|
|
257
|
+
* remove existing codes, only add new ones, and to bump the minor version
|
|
258
|
+
* when a new code is introduced.
|
|
259
|
+
*/
|
|
260
|
+
declare class ConductorAppError extends Error {
|
|
261
|
+
readonly name = "ConductorAppError";
|
|
262
|
+
readonly code: ConductorErrorCode | string;
|
|
263
|
+
readonly status?: number;
|
|
264
|
+
readonly details?: unknown;
|
|
265
|
+
readonly requestId?: string;
|
|
266
|
+
constructor(args: {
|
|
267
|
+
code: ConductorErrorCode | string;
|
|
268
|
+
message: string;
|
|
269
|
+
status?: number;
|
|
270
|
+
details?: unknown;
|
|
271
|
+
requestId?: string;
|
|
272
|
+
cause?: unknown;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
type ConductorErrorCode = 'unauthorized' | 'forbidden' | 'token_revoked' | 'invalid_input' | 'project_not_found' | 'task_not_found' | 'message_not_found' | 'daemon_offline' | 'workspace_path_conflict' | 'binding_validation_failed' | 'task_not_running' | 'task_type_not_messageable' | 'task_type_not_interruptible' | 'task_fire_owner_offline' | 'network_error' | 'timeout' | 'rate_limited' | 'server_error' | 'subscribe_failed' | 'stream_aborted';
|
|
276
|
+
/** Helper: type-guard for callers that don't want `instanceof`. */
|
|
277
|
+
declare function isConductorAppError(error: unknown): error is ConductorAppError;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @conductor/app-sdk root entry.
|
|
281
|
+
*
|
|
282
|
+
* This entry is environment-agnostic: it re-exports pure types plus a few
|
|
283
|
+
* runtime constants. Safe to import from Node, browsers, Edge runtimes, and
|
|
284
|
+
* React Server Components.
|
|
285
|
+
*
|
|
286
|
+
* Use the subpath entries for runtime code:
|
|
287
|
+
* - `@love-moon/app-sdk/server` — Node SDK for talking to Conductor.
|
|
288
|
+
* - `@love-moon/app-sdk/react` — React chat widget.
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
declare const SDK_VERSION = "0.3.2";
|
|
292
|
+
declare const SDK_NAME = "@love-moon/app-sdk";
|
|
293
|
+
/**
|
|
294
|
+
* User-Agent fragment the SDK sets on every outbound HTTP request.
|
|
295
|
+
* Format: `conductor-app-sdk/<version>`.
|
|
296
|
+
*/
|
|
297
|
+
declare const SDK_USER_AGENT = "conductor-app-sdk/0.3.2";
|
|
298
|
+
|
|
299
|
+
export { type Attachment, type BindProjectInput, type ChatAdapter, type ChatEvent, ConductorAppError, type ConductorErrorCode, type CreateTaskInput, type Message, type MessageRole, type Project, type RuntimeState, type RuntimeStatus, SDK_NAME, SDK_USER_AGENT, SDK_VERSION, type SendMessageInput, type StreamReplyDelta, type Task, type TaskStatus, isConductorAppError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/types/errors.ts
|
|
2
|
+
var ConductorAppError = class extends Error {
|
|
3
|
+
name = "ConductorAppError";
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
details;
|
|
7
|
+
requestId;
|
|
8
|
+
constructor(args) {
|
|
9
|
+
super(args.message, args.cause ? { cause: args.cause } : void 0);
|
|
10
|
+
this.code = args.code;
|
|
11
|
+
this.status = args.status;
|
|
12
|
+
this.details = args.details;
|
|
13
|
+
this.requestId = args.requestId;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function isConductorAppError(error) {
|
|
17
|
+
return typeof error === "object" && error !== null && error.name === "ConductorAppError";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var SDK_VERSION = "0.3.2";
|
|
22
|
+
var SDK_NAME = "@love-moon/app-sdk";
|
|
23
|
+
var SDK_USER_AGENT = `conductor-app-sdk/${SDK_VERSION}`;
|
|
24
|
+
export {
|
|
25
|
+
ConductorAppError,
|
|
26
|
+
SDK_NAME,
|
|
27
|
+
SDK_USER_AGENT,
|
|
28
|
+
SDK_VERSION,
|
|
29
|
+
isConductorAppError
|
|
30
|
+
};
|