@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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/* Conductor App SDK — chat widget styles.
|
|
2
|
+
*
|
|
3
|
+
* v0.1: hand-written CSS, functional but visually minimal. The eventual
|
|
4
|
+
* extraction from web/src/features/chat will swap this for the pre-compiled
|
|
5
|
+
* Tailwind output. Until then, hosts who want polished visuals are free to
|
|
6
|
+
* override any class — every component renders semantic conductor-* classes.
|
|
7
|
+
*
|
|
8
|
+
* Theming: all colors / radii / spacing pull from CSS variables on the
|
|
9
|
+
* `.conductor-chat-view` root, which `<ChatView theme={...} />` overrides
|
|
10
|
+
* per instance.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.conductor-chat-view {
|
|
14
|
+
--accent: #e4572e;
|
|
15
|
+
--conductor-paper: #f5f1ea;
|
|
16
|
+
--conductor-text: #1a1a1a;
|
|
17
|
+
--conductor-text-muted: rgba(0, 0, 0, 0.55);
|
|
18
|
+
--conductor-bubble-user: #f1ece2;
|
|
19
|
+
--conductor-bubble-assistant: #ffffff;
|
|
20
|
+
--conductor-border: rgba(0, 0, 0, 0.08);
|
|
21
|
+
--conductor-radius: 12px;
|
|
22
|
+
--conductor-spacing: 12px;
|
|
23
|
+
--conductor-font:
|
|
24
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
25
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
26
|
+
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-rows: auto 1fr auto;
|
|
29
|
+
height: 100%;
|
|
30
|
+
background: var(--conductor-paper);
|
|
31
|
+
color: var(--conductor-text);
|
|
32
|
+
font-family: var(--conductor-font);
|
|
33
|
+
font-size: 14px;
|
|
34
|
+
line-height: 1.5;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.conductor-chat-view *,
|
|
39
|
+
.conductor-chat-view *::before,
|
|
40
|
+
.conductor-chat-view *::after {
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ----- Runtime status bar ------------------------------------------------- */
|
|
45
|
+
|
|
46
|
+
.conductor-runtime-status {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
padding: 8px var(--conductor-spacing);
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
color: var(--conductor-text-muted);
|
|
53
|
+
border-bottom: 1px solid var(--conductor-border);
|
|
54
|
+
min-height: 32px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.conductor-runtime-indicator {
|
|
58
|
+
width: 8px;
|
|
59
|
+
height: 8px;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
background: rgba(0, 0, 0, 0.2);
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.conductor-runtime-indicator--active {
|
|
66
|
+
background: var(--accent);
|
|
67
|
+
animation: conductor-pulse 1.2s ease-in-out infinite;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@keyframes conductor-pulse {
|
|
71
|
+
0%,
|
|
72
|
+
100% {
|
|
73
|
+
transform: scale(1);
|
|
74
|
+
opacity: 1;
|
|
75
|
+
}
|
|
76
|
+
50% {
|
|
77
|
+
transform: scale(1.4);
|
|
78
|
+
opacity: 0.6;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.conductor-runtime-text {
|
|
83
|
+
flex: 1;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
text-overflow: ellipsis;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.conductor-runtime-error {
|
|
90
|
+
color: #b04020;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.conductor-runtime-connection {
|
|
95
|
+
margin-left: auto;
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
color: var(--conductor-text-muted);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ----- Message list ------------------------------------------------------- */
|
|
101
|
+
|
|
102
|
+
.conductor-message-list {
|
|
103
|
+
overflow-y: auto;
|
|
104
|
+
padding: var(--conductor-spacing);
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 8px;
|
|
108
|
+
min-height: 0; /* lets it shrink inside the grid row */
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.conductor-load-earlier {
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
margin-bottom: 4px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.conductor-load-earlier button {
|
|
118
|
+
background: transparent;
|
|
119
|
+
border: 1px solid var(--conductor-border);
|
|
120
|
+
border-radius: 999px;
|
|
121
|
+
padding: 4px 12px;
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
color: var(--conductor-text-muted);
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.conductor-load-earlier button:disabled {
|
|
128
|
+
opacity: 0.5;
|
|
129
|
+
cursor: progress;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.conductor-message {
|
|
133
|
+
display: flex;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.conductor-message--user {
|
|
137
|
+
justify-content: flex-end;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.conductor-message--assistant {
|
|
141
|
+
justify-content: flex-start;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.conductor-bubble {
|
|
145
|
+
max-width: 75%;
|
|
146
|
+
padding: 10px 14px;
|
|
147
|
+
border-radius: var(--conductor-radius);
|
|
148
|
+
white-space: pre-wrap;
|
|
149
|
+
word-break: break-word;
|
|
150
|
+
border: 1px solid var(--conductor-border);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.conductor-message--user .conductor-bubble {
|
|
154
|
+
background: var(--conductor-bubble-user);
|
|
155
|
+
border-bottom-right-radius: 4px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.conductor-message--assistant .conductor-bubble {
|
|
159
|
+
background: var(--conductor-bubble-assistant);
|
|
160
|
+
border-bottom-left-radius: 4px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.conductor-message--pending .conductor-bubble {
|
|
164
|
+
opacity: 0.6;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ----- Message input ------------------------------------------------------ */
|
|
168
|
+
|
|
169
|
+
.conductor-message-input {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: flex-end;
|
|
172
|
+
gap: 8px;
|
|
173
|
+
padding: var(--conductor-spacing);
|
|
174
|
+
border-top: 1px solid var(--conductor-border);
|
|
175
|
+
background: var(--conductor-paper);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.conductor-message-input__textarea {
|
|
179
|
+
flex: 1;
|
|
180
|
+
min-height: 36px;
|
|
181
|
+
max-height: 136px;
|
|
182
|
+
resize: none;
|
|
183
|
+
padding: 8px 12px;
|
|
184
|
+
border: 1px solid var(--conductor-border);
|
|
185
|
+
border-radius: var(--conductor-radius);
|
|
186
|
+
background: #fff;
|
|
187
|
+
color: var(--conductor-text);
|
|
188
|
+
font-family: inherit;
|
|
189
|
+
font-size: 14px;
|
|
190
|
+
line-height: 1.5;
|
|
191
|
+
outline: none;
|
|
192
|
+
overflow-y: auto;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.conductor-message-input__textarea:focus {
|
|
196
|
+
border-color: var(--accent);
|
|
197
|
+
box-shadow: 0 0 0 3px rgba(228, 87, 46, 0.15);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.conductor-message-input__actions {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 6px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.conductor-button {
|
|
207
|
+
padding: 8px 14px;
|
|
208
|
+
border-radius: var(--conductor-radius);
|
|
209
|
+
border: 1px solid var(--conductor-border);
|
|
210
|
+
background: #fff;
|
|
211
|
+
color: var(--conductor-text);
|
|
212
|
+
font: inherit;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
transition: background-color 120ms ease;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.conductor-button:hover {
|
|
218
|
+
background: rgba(0, 0, 0, 0.04);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.conductor-button:disabled {
|
|
222
|
+
opacity: 0.4;
|
|
223
|
+
cursor: not-allowed;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.conductor-button--send {
|
|
227
|
+
background: var(--accent);
|
|
228
|
+
color: #fff;
|
|
229
|
+
border-color: transparent;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.conductor-button--send:hover {
|
|
233
|
+
background: var(--accent);
|
|
234
|
+
filter: brightness(0.92);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.conductor-button--interrupt {
|
|
238
|
+
background: #b04020;
|
|
239
|
+
color: #fff;
|
|
240
|
+
border-color: transparent;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.conductor-button--interrupt:hover {
|
|
244
|
+
filter: brightness(0.92);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ----- Mobile layout ------------------------------------------------------ */
|
|
248
|
+
|
|
249
|
+
@media (max-width: 600px) {
|
|
250
|
+
.conductor-chat-view {
|
|
251
|
+
font-size: 15px;
|
|
252
|
+
}
|
|
253
|
+
.conductor-bubble {
|
|
254
|
+
max-width: 85%;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.conductor-chat-view[data-layout='mobile'] .conductor-bubble {
|
|
259
|
+
max-width: 85%;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.conductor-chat-view[data-layout='desktop'] .conductor-bubble {
|
|
263
|
+
max-width: 60%;
|
|
264
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
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
|
+
* Message: one entry in a task's chat transcript.
|
|
42
|
+
*
|
|
43
|
+
* Roles:
|
|
44
|
+
* - 'user' — message sent by the user (or the SDK on their behalf).
|
|
45
|
+
* - 'sdk' — message sent by an SDK / CLI / app integration (still
|
|
46
|
+
* appears in the chat as a user-side bubble).
|
|
47
|
+
* - 'assistant' — AI reply.
|
|
48
|
+
* - 'system' — system-emitted notice (rare; renders muted).
|
|
49
|
+
*
|
|
50
|
+
* `role` is intentionally open-vocabulary; new backend roles render as
|
|
51
|
+
* a generic bubble.
|
|
52
|
+
*/
|
|
53
|
+
interface Message {
|
|
54
|
+
id: string;
|
|
55
|
+
taskId: string;
|
|
56
|
+
role: MessageRole | string;
|
|
57
|
+
content: string;
|
|
58
|
+
metadata: Record<string, unknown> | null;
|
|
59
|
+
attachments: Attachment[];
|
|
60
|
+
createdAt: string;
|
|
61
|
+
}
|
|
62
|
+
type MessageRole = 'user' | 'sdk' | 'assistant' | 'system';
|
|
63
|
+
interface Attachment {
|
|
64
|
+
id: string;
|
|
65
|
+
filename: string;
|
|
66
|
+
mimeType: string;
|
|
67
|
+
sizeBytes: number;
|
|
68
|
+
/** Resolvable URL or relative path the host must turn into a fetchable URL. */
|
|
69
|
+
url: string;
|
|
70
|
+
}
|
|
71
|
+
interface SendMessageInput {
|
|
72
|
+
content: string;
|
|
73
|
+
/** Idempotency key. When omitted, the SDK auto-generates a UUID. */
|
|
74
|
+
clientRequestId?: string;
|
|
75
|
+
metadata?: Record<string, unknown>;
|
|
76
|
+
/** Optional attachments to include with this message; must be pre-uploaded. */
|
|
77
|
+
attachmentIds?: string[];
|
|
78
|
+
/** Role override; defaults to 'sdk'. */
|
|
79
|
+
role?: MessageRole;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Runtime status pushed by the daemon while a task is in progress.
|
|
84
|
+
* Mirrors `task_runtime_status` envelope on /ws/app.
|
|
85
|
+
*/
|
|
86
|
+
interface RuntimeStatus {
|
|
87
|
+
taskId: string;
|
|
88
|
+
/** High-level phase: 'idle' | 'thinking' | 'tool_call' | 'awaiting_user' | 'done'. */
|
|
89
|
+
state: RuntimeState;
|
|
90
|
+
phase?: string | null;
|
|
91
|
+
source?: string | null;
|
|
92
|
+
/** Short status line shown next to the AI avatar ("Reading file X..."). */
|
|
93
|
+
statusLine?: string | null;
|
|
94
|
+
/** Final status line shown when the reply finishes. */
|
|
95
|
+
statusDoneLine?: string | null;
|
|
96
|
+
replyPreview?: string | null;
|
|
97
|
+
replyTo?: string | null;
|
|
98
|
+
replyInProgress?: boolean;
|
|
99
|
+
backend?: string | null;
|
|
100
|
+
threadId?: string | null;
|
|
101
|
+
daemon?: string | null;
|
|
102
|
+
pid?: number | null;
|
|
103
|
+
sessionId?: string | null;
|
|
104
|
+
sessionFilePath?: string | null;
|
|
105
|
+
tokenUsagePercent?: number | null;
|
|
106
|
+
contextUsagePercent?: number | null;
|
|
107
|
+
createdAt?: string | null;
|
|
108
|
+
}
|
|
109
|
+
type RuntimeState = 'idle' | 'thinking' | 'tool_call' | 'awaiting_user' | 'done' | string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* ChatEvent: the minimal set of events the chat widget needs to render.
|
|
113
|
+
*
|
|
114
|
+
* This is intentionally a small union — the widget should not have to
|
|
115
|
+
* pattern-match dozens of envelope types. The default REST/WS adapter
|
|
116
|
+
* funnels raw `/ws/app` envelopes into ChatEvents; custom adapters can do the
|
|
117
|
+
* same translation against any wire format.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* Error shape carried inside `task_failed` ChatEvents and `error`
|
|
121
|
+
* StreamReplyDeltas.
|
|
122
|
+
*
|
|
123
|
+
* `details` and `cause` are optional, free-form passthroughs of the original
|
|
124
|
+
* SDK error's structured fields — useful for surfacing request IDs / server
|
|
125
|
+
* payloads in host UIs without depending on the SDK error class directly.
|
|
126
|
+
*/
|
|
127
|
+
interface ChatEventError {
|
|
128
|
+
code: string;
|
|
129
|
+
message: string;
|
|
130
|
+
details?: unknown;
|
|
131
|
+
cause?: unknown;
|
|
132
|
+
}
|
|
133
|
+
type ChatEvent = {
|
|
134
|
+
type: 'message_appended';
|
|
135
|
+
message: Message;
|
|
136
|
+
} | {
|
|
137
|
+
type: 'message_updated';
|
|
138
|
+
message: Message;
|
|
139
|
+
} | {
|
|
140
|
+
type: 'runtime_status';
|
|
141
|
+
status: RuntimeStatus;
|
|
142
|
+
} | {
|
|
143
|
+
type: 'task_finished';
|
|
144
|
+
taskId: string;
|
|
145
|
+
} | {
|
|
146
|
+
type: 'task_failed';
|
|
147
|
+
taskId: string;
|
|
148
|
+
error: ChatEventError;
|
|
149
|
+
} | {
|
|
150
|
+
type: 'connection_state';
|
|
151
|
+
state: 'connected' | 'reconnecting' | 'offline';
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* StreamReplyDelta: a streaming AI reply chunk yielded by
|
|
155
|
+
* `client.tasks.streamReply(id)`.
|
|
156
|
+
*
|
|
157
|
+
* v1 semantics: each `text` delta is a *cumulative* preview-style chunk
|
|
158
|
+
* (built from `task_runtime_status.reply_preview` rolling state). The final
|
|
159
|
+
* `done` delta carries the full reply Message. A future RFC may add real
|
|
160
|
+
* token-level streaming; the shape is forward-compatible.
|
|
161
|
+
*/
|
|
162
|
+
type StreamReplyDelta = {
|
|
163
|
+
type: 'text';
|
|
164
|
+
text: string;
|
|
165
|
+
replyTo: string;
|
|
166
|
+
} | {
|
|
167
|
+
type: 'status';
|
|
168
|
+
status: RuntimeStatus;
|
|
169
|
+
} | {
|
|
170
|
+
type: 'done';
|
|
171
|
+
message: Message;
|
|
172
|
+
} | {
|
|
173
|
+
type: 'error';
|
|
174
|
+
error: ChatEventError;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Project: a workspace binding (daemon + filesystem path) inside which tasks
|
|
179
|
+
* are scoped. Maps to Conductor's Project model.
|
|
180
|
+
*
|
|
181
|
+
* `daemonHost` and `workspacePath` are the binding identity. App-SDK's
|
|
182
|
+
* `projects.bind()` is idempotent on this pair.
|
|
183
|
+
*/
|
|
184
|
+
interface Project {
|
|
185
|
+
id: string;
|
|
186
|
+
name: string;
|
|
187
|
+
daemonHost: string | null;
|
|
188
|
+
workspacePath: string | null;
|
|
189
|
+
repoRoot: string | null;
|
|
190
|
+
worktreeBranch: string | null;
|
|
191
|
+
lastCommit: string | null;
|
|
192
|
+
lastCommitAt: string | null;
|
|
193
|
+
fileCount: number | null;
|
|
194
|
+
isDefault: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* True when this project was created via the App SDK (audit hint, derived
|
|
197
|
+
* from `metadata.audit.createdByApp`). Read-only flag for UI affordances.
|
|
198
|
+
*/
|
|
199
|
+
createdByApp: boolean;
|
|
200
|
+
createdAt: string;
|
|
201
|
+
updatedAt: string;
|
|
202
|
+
}
|
|
203
|
+
interface BindProjectInput {
|
|
204
|
+
name: string;
|
|
205
|
+
daemonHost: string;
|
|
206
|
+
workspacePath: string;
|
|
207
|
+
/**
|
|
208
|
+
* Optional override for the audit `createdByApp.name` field. Defaults to the
|
|
209
|
+
* `name` argument. Only used when the SDK has to create a new project (no
|
|
210
|
+
* existing binding found).
|
|
211
|
+
*/
|
|
212
|
+
appLabel?: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
declare class ProjectsApi {
|
|
216
|
+
private readonly fetcher;
|
|
217
|
+
/**
|
|
218
|
+
* Idempotent find-or-create on (daemonHost, workspacePath).
|
|
219
|
+
*
|
|
220
|
+
* Flow:
|
|
221
|
+
* 1. POST `/api/projects/match-path` with `{ daemonHost, path: workspacePath }`.
|
|
222
|
+
* If a project matches (or is a parent of) the path, return it.
|
|
223
|
+
* 2. Otherwise POST `/api/projects` with binding fields. The server
|
|
224
|
+
* validates the binding with the user's daemon. Success returns the
|
|
225
|
+
* new project; daemon-offline returns 409 → mapped to `daemon_offline`.
|
|
226
|
+
*
|
|
227
|
+
* The created project carries `metadata.audit.createdByApp` so it can be
|
|
228
|
+
* distinguished in the main Conductor UI later. No new schema is required.
|
|
229
|
+
*/
|
|
230
|
+
bind(input: BindProjectInput, opts?: {
|
|
231
|
+
signal?: AbortSignal;
|
|
232
|
+
}): Promise<Project>;
|
|
233
|
+
list(opts?: {
|
|
234
|
+
signal?: AbortSignal;
|
|
235
|
+
}): Promise<Project[]>;
|
|
236
|
+
get(projectId: string, opts?: {
|
|
237
|
+
signal?: AbortSignal;
|
|
238
|
+
}): Promise<Project>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface ConnectOptions {
|
|
242
|
+
/** Conductor backend URL, e.g. `https://conductor.example.com`. */
|
|
243
|
+
baseUrl: string;
|
|
244
|
+
/**
|
|
245
|
+
* Bearer token issued from Conductor Settings → API Tokens.
|
|
246
|
+
* Accepts a string or an async provider (for token rotation / vault lookup).
|
|
247
|
+
*/
|
|
248
|
+
bearerToken: string | (() => Promise<string>);
|
|
249
|
+
/** Custom fetch (SSR, testing). Defaults to `globalThis.fetch`. */
|
|
250
|
+
fetch?: typeof globalThis.fetch;
|
|
251
|
+
/** Per-request timeout in ms. Default 30_000. */
|
|
252
|
+
timeoutMs?: number;
|
|
253
|
+
/** Called once when any request returns 401. */
|
|
254
|
+
onUnauthorized?: () => void;
|
|
255
|
+
/**
|
|
256
|
+
* Lazy: do not open the /ws/app connection until the first subscribe/stream
|
|
257
|
+
* call. Default `true`. Set to `false` to eagerly connect at `connect()`.
|
|
258
|
+
*/
|
|
259
|
+
lazyWebSocket?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Inject a custom WebSocket constructor (used by tests).
|
|
262
|
+
* Type widened to `unknown` to avoid forcing consumers to depend on the
|
|
263
|
+
* specific `ws` types.
|
|
264
|
+
*/
|
|
265
|
+
webSocketImpl?: unknown;
|
|
266
|
+
}
|
|
267
|
+
type AppClientOptions = ConnectOptions;
|
|
268
|
+
declare class AppClient {
|
|
269
|
+
readonly projects: ProjectsApi;
|
|
270
|
+
readonly tasks: TasksApi;
|
|
271
|
+
private readonly _fetcher;
|
|
272
|
+
private _socket;
|
|
273
|
+
private _closed;
|
|
274
|
+
private readonly options;
|
|
275
|
+
constructor(options: AppClientOptions);
|
|
276
|
+
/**
|
|
277
|
+
* Release the WS connection and mark the client closed. Idempotent —
|
|
278
|
+
* calling twice is a no-op rather than a NPE. After close, any new
|
|
279
|
+
* `tasks.subscribe()` / `tasks.streamReply()` / `tasks.*` REST call
|
|
280
|
+
* throws synchronously instead of returning a hanging iterator.
|
|
281
|
+
*/
|
|
282
|
+
close(): Promise<void>;
|
|
283
|
+
private getOrCreateSocket;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* `connect()`: thin async wrapper around the constructor. In v0.1 it's
|
|
287
|
+
* synchronous-ish (no remote probe); kept async for forward compatibility
|
|
288
|
+
* (v0.2 may probe `/api/auth/me` for early-failure semantics).
|
|
289
|
+
*/
|
|
290
|
+
declare function connect(options: ConnectOptions): Promise<AppClient>;
|
|
291
|
+
declare class TasksApi {
|
|
292
|
+
private readonly rest;
|
|
293
|
+
private readonly getSocket;
|
|
294
|
+
private readonly isClosed;
|
|
295
|
+
private assertOpen;
|
|
296
|
+
create(input: CreateTaskInput, opts?: {
|
|
297
|
+
signal?: AbortSignal;
|
|
298
|
+
}): Promise<Task>;
|
|
299
|
+
get(taskId: string, opts?: {
|
|
300
|
+
signal?: AbortSignal;
|
|
301
|
+
}): Promise<Task>;
|
|
302
|
+
list(filter?: {
|
|
303
|
+
projectId?: string;
|
|
304
|
+
status?: string;
|
|
305
|
+
}, opts?: {
|
|
306
|
+
signal?: AbortSignal;
|
|
307
|
+
}): Promise<Task[]>;
|
|
308
|
+
sendMessage(taskId: string, input: string | SendMessageInput, opts?: {
|
|
309
|
+
signal?: AbortSignal;
|
|
310
|
+
}): Promise<Message>;
|
|
311
|
+
history(taskId: string, paging?: {
|
|
312
|
+
beforeId?: string;
|
|
313
|
+
limit?: number;
|
|
314
|
+
}, opts?: {
|
|
315
|
+
signal?: AbortSignal;
|
|
316
|
+
}): Promise<{
|
|
317
|
+
messages: Message[];
|
|
318
|
+
hasMoreBefore: boolean;
|
|
319
|
+
oldestMessageId: string | null;
|
|
320
|
+
}>;
|
|
321
|
+
interrupt(taskId: string, opts: {
|
|
322
|
+
targetReplyTo: string;
|
|
323
|
+
signal?: AbortSignal;
|
|
324
|
+
}): Promise<void>;
|
|
325
|
+
/**
|
|
326
|
+
* Subscribe to a task's event stream. Yields ChatEvents until the caller
|
|
327
|
+
* breaks out of the `for await` loop, calls `signal.abort()`, or the
|
|
328
|
+
* client is closed.
|
|
329
|
+
*
|
|
330
|
+
* The first call lazily opens a /ws/app connection; subsequent calls share
|
|
331
|
+
* the same connection.
|
|
332
|
+
*/
|
|
333
|
+
subscribe(taskId: string, opts?: {
|
|
334
|
+
signal?: AbortSignal;
|
|
335
|
+
bufferCap?: number;
|
|
336
|
+
}): AsyncIterable<ChatEvent>;
|
|
337
|
+
/**
|
|
338
|
+
* Higher-level convenience: yield only AI reply deltas. Internally consumes
|
|
339
|
+
* `subscribe()` and selects runtime_status preview chunks + the final
|
|
340
|
+
* assistant message.
|
|
341
|
+
*/
|
|
342
|
+
streamReply(taskId: string, opts?: {
|
|
343
|
+
signal?: AbortSignal;
|
|
344
|
+
emitInitialPreview?: boolean;
|
|
345
|
+
idleTimeoutMs?: number;
|
|
346
|
+
}): AsyncIterable<StreamReplyDelta>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* ConductorAppError: every error thrown out of the SDK is one of these.
|
|
351
|
+
* Callers can `if (err instanceof ConductorAppError) switch (err.code)`.
|
|
352
|
+
*
|
|
353
|
+
* `code` is open-vocabulary for forward compat; the SDK promises to never
|
|
354
|
+
* remove existing codes, only add new ones, and to bump the minor version
|
|
355
|
+
* when a new code is introduced.
|
|
356
|
+
*/
|
|
357
|
+
declare class ConductorAppError extends Error {
|
|
358
|
+
readonly name = "ConductorAppError";
|
|
359
|
+
readonly code: ConductorErrorCode | string;
|
|
360
|
+
readonly status?: number;
|
|
361
|
+
readonly details?: unknown;
|
|
362
|
+
readonly requestId?: string;
|
|
363
|
+
constructor(args: {
|
|
364
|
+
code: ConductorErrorCode | string;
|
|
365
|
+
message: string;
|
|
366
|
+
status?: number;
|
|
367
|
+
details?: unknown;
|
|
368
|
+
requestId?: string;
|
|
369
|
+
cause?: unknown;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
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';
|
|
373
|
+
/** Helper: type-guard for callers that don't want `instanceof`. */
|
|
374
|
+
declare function isConductorAppError(error: unknown): error is ConductorAppError;
|
|
375
|
+
|
|
376
|
+
export { AppClient, type AppClientOptions, type Attachment, type BindProjectInput, type ChatEvent, ConductorAppError, type ConnectOptions, type CreateTaskInput, type Message, type MessageRole, type Project, type RuntimeState, type RuntimeStatus, type SendMessageInput, type StreamReplyDelta, type Task, type TaskStatus, connect, isConductorAppError };
|