manus-api 0.0.0-stage → 0.1.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sukka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,359 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var rl = require('@henrygd/queue/rl');
4
+ var extractErrorMessage = require('foxts/extract-error-message');
5
+ var wait = require('foxts/wait');
6
+
7
+ const AGENT_PROFILES = [
8
+ 'lite',
9
+ 'standard',
10
+ 'max'
11
+ ];
12
+ /** Thinking effort. Only applies to the `max` profile. */ const AGENT_EFFORTS = [
13
+ 'medium',
14
+ 'high',
15
+ 'extra',
16
+ 'ultra'
17
+ ];
18
+ /**
19
+ * Build the `agent_profile` value that the Manus API accepts.
20
+ *
21
+ * @throws {TypeError} when `effort` is combined with a profile other than `max`.
22
+ */ function formatAgentProfile(profile, effort) {
23
+ if (profile !== 'max') {
24
+ if (effort != null) {
25
+ throw new TypeError(`effort "${effort}" only applies to the "max" profile, got "${profile}"`);
26
+ }
27
+ return profile;
28
+ }
29
+ return effort == null ? 'max' : `max-${effort}`;
30
+ }
31
+ /** `manus-1.6-`, `2.0-`, `1.6` ... the version segment is ignored by the API and carries no information. */ const LEGACY_VERSION_PREFIX = /^(?:manus-)?\d+(?:\.\d+)*-?/;
32
+ /**
33
+ * Normalize the profile reported by the API, which uses the legacy versioned form (`manus-1.6`,
34
+ * `manus-1.6-lite`, `manus-1.6-max-high`; P0 T4, T5, T8). A bare version means `standard`.
35
+ * Returns `undefined` for values this version does not know.
36
+ */ function parseAgentProfile(value) {
37
+ if (value == null) return undefined;
38
+ const rest = value.replace(LEGACY_VERSION_PREFIX, '');
39
+ if (rest === '' || rest === 'standard') return {
40
+ agent_profile: 'standard'
41
+ };
42
+ if (rest === 'lite') return {
43
+ agent_profile: 'lite'
44
+ };
45
+ if (rest === 'max') return {
46
+ agent_profile: 'max'
47
+ };
48
+ if (rest.startsWith('max-')) {
49
+ const effort = rest.slice('max-'.length);
50
+ if (AGENT_EFFORTS.includes(effort)) {
51
+ return {
52
+ agent_profile: 'max',
53
+ effort: effort
54
+ };
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ const MANUS_API_BASE_URL = 'https://api.manus.ai';
61
+ /** Error codes seen so far: `unauthenticated` (401), `permission_denied` (403), `not_found` (404), `rate_limited` (429). */ class ManusApiError extends Error {
62
+ constructor(message, options){
63
+ super(message, {
64
+ cause: options.cause
65
+ });
66
+ this.name = 'ManusApiError';
67
+ this.status = options.status;
68
+ this.code = options.code;
69
+ this.requestId = options.requestId;
70
+ }
71
+ }
72
+ /** One process keeps its polling under `ratePerMinute` requests across all tasks it tracks (design D11). */ function createPollQueue(ratePerMinute, concurrency = 8) {
73
+ return rl.newQueue(concurrency, ratePerMinute, 60000);
74
+ }
75
+ const MAX_RATE_LIMIT_RETRIES = 5;
76
+ const MAX_BACKOFF_MS = 60000;
77
+ const TRAILING_SLASHES = /\/+$/;
78
+ function backoffMs(attempt, retryAfterHeader) {
79
+ const retryAfter = retryAfterHeader == null ? Number.NaN : Number(retryAfterHeader) * 1000;
80
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
81
+ return Math.min(retryAfter, MAX_BACKOFF_MS);
82
+ }
83
+ return Math.min(1000 * 2 ** (attempt - 1) + Math.random() * 1000, MAX_BACKOFF_MS);
84
+ }
85
+ function headerNumber(headers, name) {
86
+ const value = headers.get(name);
87
+ if (value == null) return undefined;
88
+ const n = Number(value);
89
+ return Number.isFinite(n) ? n : undefined;
90
+ }
91
+ class ManusClient {
92
+ constructor(options){
93
+ this.options = options;
94
+ this.baseUrl = (options.baseUrl ?? MANUS_API_BASE_URL).replace(TRAILING_SLASHES, '');
95
+ this.fetch = options.fetch ?? globalThis.fetch;
96
+ this.timeoutMs = options.timeoutMs ?? 60000;
97
+ this.transferTimeoutMs = options.transferTimeoutMs ?? 600000;
98
+ this.sleep = options.sleep ?? wait.wait;
99
+ }
100
+ userMe() {
101
+ return this.get('/v2/user.me');
102
+ }
103
+ projectCreate(body) {
104
+ return this.post('/v2/project.create', body);
105
+ }
106
+ projectList() {
107
+ return this.get('/v2/project.list');
108
+ }
109
+ taskCreate(body) {
110
+ return this.post('/v2/task.create', body);
111
+ }
112
+ taskDetail(taskId) {
113
+ return this.get('/v2/task.detail', {
114
+ task_id: taskId
115
+ }, true);
116
+ }
117
+ taskList(params = {}) {
118
+ return this.get('/v2/task.list', params);
119
+ }
120
+ taskListMessages(params) {
121
+ return this.get('/v2/task.listMessages', params, true);
122
+ }
123
+ taskSendMessage(body) {
124
+ return this.post('/v2/task.sendMessage', body);
125
+ }
126
+ taskStop(taskId) {
127
+ return this.post('/v2/task.stop', {
128
+ task_id: taskId
129
+ });
130
+ }
131
+ taskConfirmAction(body) {
132
+ return this.post('/v2/task.confirmAction', body);
133
+ }
134
+ /** Repeated deletes and unknown IDs both answer 404 `not_found` (P0 section 12). */ taskDelete(taskId) {
135
+ return this.post('/v2/task.delete', {
136
+ task_id: taskId
137
+ });
138
+ }
139
+ fileUpload(filename) {
140
+ return this.post('/v2/file.upload', {
141
+ filename
142
+ });
143
+ }
144
+ fileDetail(fileId) {
145
+ return this.get('/v2/file.detail', {
146
+ file_id: fileId
147
+ });
148
+ }
149
+ artifactList(params = {}) {
150
+ return this.get('/v2/artifact.list', params);
151
+ }
152
+ connectorList() {
153
+ return this.get('/v2/connector.list');
154
+ }
155
+ skillList(projectId) {
156
+ return this.get('/v2/skill.list', {
157
+ project_id: projectId
158
+ });
159
+ }
160
+ /**
161
+ * PUT to the presigned `upload_url` of `file.upload`. The URL points at a storage host, so the
162
+ * request carries no API key and no content type (P0 T10: only the host is signed). A `Blob`
163
+ * from `fs.openAsBlob` streams from disk and sets the content length.
164
+ */ async uploadToUrl(uploadUrl, body) {
165
+ const res = await this.transfer(uploadUrl, {
166
+ method: 'PUT',
167
+ body
168
+ });
169
+ if (!res.ok) {
170
+ throw new ManusApiError(`Upload to the presigned URL failed with HTTP ${res.status}`, {
171
+ status: res.status,
172
+ code: `http_${res.status}`
173
+ });
174
+ }
175
+ }
176
+ /** GET a presigned artifact URL (signed for 48 hours), with no API key; the caller streams the body. */ async downloadFromUrl(url) {
177
+ const res = await this.transfer(url, {
178
+ method: 'GET'
179
+ });
180
+ if (!res.ok) {
181
+ await res.body?.cancel();
182
+ throw new ManusApiError(`Download failed with HTTP ${res.status}`, {
183
+ status: res.status,
184
+ code: `http_${res.status}`
185
+ });
186
+ }
187
+ return res;
188
+ }
189
+ async transfer(url, init) {
190
+ try {
191
+ return await this.fetch(url, {
192
+ ...init,
193
+ signal: AbortSignal.timeout(this.transferTimeoutMs)
194
+ });
195
+ } catch (error) {
196
+ throw new ManusApiError(extractErrorMessage.extractErrorMessage(error) ?? 'transfer failed', {
197
+ status: 0,
198
+ code: 'network',
199
+ cause: error
200
+ });
201
+ }
202
+ }
203
+ get(endpoint, query, poll = false) {
204
+ return this.request('GET', endpoint, {
205
+ query,
206
+ poll
207
+ });
208
+ }
209
+ post(endpoint, body) {
210
+ return this.request('POST', endpoint, {
211
+ body
212
+ });
213
+ }
214
+ async request(method, endpoint, init) {
215
+ const url = new URL(this.baseUrl + endpoint);
216
+ if (init.query) {
217
+ const entries = Object.entries(init.query);
218
+ for(let i = 0, len = entries.length; i < len; i++){
219
+ const [key, value] = entries[i];
220
+ if (value !== undefined) url.searchParams.set(key, String(value));
221
+ }
222
+ }
223
+ let retriedAuth = false;
224
+ let rateLimitRetries = 0;
225
+ for(;;){
226
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
227
+ const apiKey = await this.options.getApiKey();
228
+ const headers = {
229
+ 'x-manus-api-key': apiKey,
230
+ accept: 'application/json'
231
+ };
232
+ if (init.body !== undefined) headers['content-type'] = 'application/json';
233
+ const send = async ()=>{
234
+ const res = await this.fetch(url, {
235
+ method,
236
+ headers,
237
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
238
+ signal: AbortSignal.timeout(this.timeoutMs)
239
+ });
240
+ return {
241
+ res,
242
+ text: await res.text()
243
+ };
244
+ };
245
+ let res;
246
+ let text;
247
+ try {
248
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
249
+ ({ res, text } = await (init.poll && this.options.pollQueue ? this.options.pollQueue.add(send) : send()));
250
+ } catch (error) {
251
+ throw new ManusApiError(extractErrorMessage.extractErrorMessage(error) ?? 'request failed', {
252
+ status: 0,
253
+ code: 'network',
254
+ cause: error
255
+ });
256
+ }
257
+ this.lastRateLimit = {
258
+ limit: headerNumber(res.headers, 'x-ratelimit-limit'),
259
+ remaining: headerNumber(res.headers, 'x-ratelimit-remaining'),
260
+ reset: headerNumber(res.headers, 'x-ratelimit-reset')
261
+ };
262
+ let json;
263
+ try {
264
+ json = text ? JSON.parse(text) : undefined;
265
+ } catch {
266
+ json = undefined;
267
+ }
268
+ if (json && res.ok && json.ok !== false) {
269
+ return json;
270
+ }
271
+ if (!retriedAuth && res.status === 401 && this.options.onUnauthorized) {
272
+ retriedAuth = true;
273
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
274
+ await this.options.onUnauthorized();
275
+ continue;
276
+ }
277
+ if (rateLimitRetries < MAX_RATE_LIMIT_RETRIES && res.status === 429) {
278
+ rateLimitRetries++;
279
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
280
+ await this.sleep(backoffMs(rateLimitRetries, res.headers.get('retry-after')));
281
+ continue;
282
+ }
283
+ throw new ManusApiError(json?.error?.message ?? `Manus API ${method} ${endpoint} failed with HTTP ${res.status}`, {
284
+ status: res.status,
285
+ code: json?.error?.code ?? (res.status === 429 ? 'rate_limited' : `http_${res.status}`),
286
+ requestId: json?.request_id
287
+ });
288
+ }
289
+ }
290
+ }
291
+
292
+ /** Event `timestamp` and plan `started_at` are Unix milliseconds, serialized as decimal strings. */ function toMillis(value) {
293
+ if (value == null) return undefined;
294
+ const n = Number(value);
295
+ return Number.isFinite(n) ? n : undefined;
296
+ }
297
+ /** All helpers below expect `events` in ascending order (`task.listMessages` with `order=asc`). */ function lastEventOfType(events, type) {
298
+ for(let i = events.length - 1; i >= 0; i--){
299
+ if (events[i].type === type) return events[i];
300
+ }
301
+ return undefined;
302
+ }
303
+ /**
304
+ * The agent's final answer: the last `assistant_message` with `delivery_kind: "result"` (P0 F3).
305
+ * Falls back to the last assistant message when no event carries `delivery_kind`.
306
+ */ function finalAssistantMessage(events) {
307
+ let fallback;
308
+ for(let i = events.length - 1; i >= 0; i--){
309
+ const event = events[i];
310
+ if (event.type !== 'assistant_message') continue;
311
+ if (event.assistant_message.delivery_kind === 'result') return event;
312
+ fallback ??= event;
313
+ }
314
+ return fallback;
315
+ }
316
+ function outputAttachments(events) {
317
+ return finalAssistantMessage(events)?.assistant_message.attachments ?? [];
318
+ }
319
+ function structuredOutput(events) {
320
+ return lastEventOfType(events, 'structured_output_result')?.structured_output_result;
321
+ }
322
+ function latestAgentStatus(events) {
323
+ return lastEventOfType(events, 'status_update')?.status_update.agent_status;
324
+ }
325
+ /**
326
+ * `task.stop` leaves the task `stopped` plus a `user_stop` event (P0 T12). The stop is only
327
+ * current when no user message came after it: a follow-up turn starts the task again.
328
+ */ function wasStoppedByUser(events) {
329
+ for(let i = events.length - 1; i >= 0; i--){
330
+ const type = events[i].type;
331
+ if (type === 'user_stop') return true;
332
+ if (type === 'user_message') return false;
333
+ }
334
+ return false;
335
+ }
336
+ /**
337
+ * Details of a pending `waiting` state, from the most recent `status_update`. Not observed in P0
338
+ * (questions arrived as a normal final answer), kept for the documented confirmation flow.
339
+ */ function pendingWaiting(events) {
340
+ const status = lastEventOfType(events, 'status_update')?.status_update;
341
+ return status?.agent_status === 'waiting' ? status.status_detail ?? {} : undefined;
342
+ }
343
+
344
+ exports.AGENT_EFFORTS = AGENT_EFFORTS;
345
+ exports.AGENT_PROFILES = AGENT_PROFILES;
346
+ exports.MANUS_API_BASE_URL = MANUS_API_BASE_URL;
347
+ exports.ManusApiError = ManusApiError;
348
+ exports.ManusClient = ManusClient;
349
+ exports.createPollQueue = createPollQueue;
350
+ exports.finalAssistantMessage = finalAssistantMessage;
351
+ exports.formatAgentProfile = formatAgentProfile;
352
+ exports.lastEventOfType = lastEventOfType;
353
+ exports.latestAgentStatus = latestAgentStatus;
354
+ exports.outputAttachments = outputAttachments;
355
+ exports.parseAgentProfile = parseAgentProfile;
356
+ exports.pendingWaiting = pendingWaiting;
357
+ exports.structuredOutput = structuredOutput;
358
+ exports.toMillis = toMillis;
359
+ exports.wasStoppedByUser = wasStoppedByUser;
@@ -0,0 +1,523 @@
1
+ import { Queue } from '@henrygd/queue/rl';
2
+
3
+ declare const AGENT_PROFILES: readonly ["lite", "standard", "max"];
4
+ type AgentProfile = typeof AGENT_PROFILES[number];
5
+ /** Thinking effort. Only applies to the `max` profile. */
6
+ declare const AGENT_EFFORTS: readonly ["medium", "high", "extra", "ultra"];
7
+ type AgentEffort = typeof AGENT_EFFORTS[number];
8
+ /** The `agent_profile` value accepted by the Manus API, e.g. `standard` or `max-high`. */
9
+ type AgentProfileValue = Exclude<AgentProfile, 'max'> | 'max' | `max-${AgentEffort}`;
10
+ /**
11
+ * Build the `agent_profile` value that the Manus API accepts.
12
+ *
13
+ * @throws {TypeError} when `effort` is combined with a profile other than `max`.
14
+ */
15
+ declare function formatAgentProfile(profile: AgentProfile, effort?: AgentEffort): AgentProfileValue;
16
+ interface ParsedAgentProfile {
17
+ agent_profile: AgentProfile;
18
+ effort?: AgentEffort;
19
+ }
20
+ /**
21
+ * Normalize the profile reported by the API, which uses the legacy versioned form (`manus-1.6`,
22
+ * `manus-1.6-lite`, `manus-1.6-max-high`; P0 T4, T5, T8). A bare version means `standard`.
23
+ * Returns `undefined` for values this version does not know.
24
+ */
25
+ declare function parseAgentProfile(value: string | null | undefined): ParsedAgentProfile | undefined;
26
+
27
+ /**
28
+ * Manus API v2 request and response shapes.
29
+ *
30
+ * Sources: `openapi_v2.json` from the official `manus-api` skill, and the P0 smoke test. Where the
31
+ * two disagree (int64 fields are serialized as decimal strings on the wire), the observed shape wins.
32
+ */
33
+ /** int64 serialized by protobuf JSON as a decimal string; older docs say integer. */
34
+ type Int64String = string | number;
35
+ interface ApiError {
36
+ code?: string;
37
+ message?: string;
38
+ }
39
+ interface ErrorResponse {
40
+ ok: false;
41
+ request_id?: string;
42
+ error?: ApiError;
43
+ }
44
+ type TaskStatus = 'running' | 'stopped' | 'waiting' | 'error';
45
+ type ShareVisibility = 'private' | 'team' | 'public';
46
+ type TaskType = 'standard' | 'project' | 'agent_subtask';
47
+ interface Task {
48
+ id: string;
49
+ status: TaskStatus;
50
+ /** Unix seconds */
51
+ created_at?: Int64String;
52
+ /** Unix seconds */
53
+ updated_at?: Int64String;
54
+ task_type?: TaskType;
55
+ share_visibility?: ShareVisibility;
56
+ title?: string;
57
+ credit_usage?: number;
58
+ task_url?: string;
59
+ created_by_api_key?: {
60
+ id?: string;
61
+ name?: string;
62
+ } | null;
63
+ /** Legacy form, e.g. `manus-1.6-max-high`. Normalize with `parseAgentProfile`. */
64
+ agent_profile?: string;
65
+ /** Optional and independent of `status`. Absent means unknown, not `false`. */
66
+ has_running_background_jobs?: boolean;
67
+ is_archived?: boolean;
68
+ }
69
+ type ContentPart = {
70
+ type: 'text';
71
+ text: string;
72
+ visibility?: 'visible' | 'hidden';
73
+ } | {
74
+ type: 'file';
75
+ file_id?: string;
76
+ file_url?: string;
77
+ file_data?: string;
78
+ filename?: string;
79
+ mime_type?: string;
80
+ visibility?: 'visible';
81
+ } | {
82
+ type: 'voice';
83
+ file_id?: string;
84
+ file_url?: string;
85
+ file_data?: string;
86
+ filename?: string;
87
+ mime_type?: string;
88
+ visibility?: 'visible';
89
+ };
90
+ interface Message {
91
+ content: string | ContentPart[];
92
+ connectors?: string[];
93
+ enable_skills?: string[];
94
+ force_skills?: string[];
95
+ /** Bare 22-character task IDs, at most 20. */
96
+ task_references?: string[];
97
+ }
98
+ /** Not in the public docs (biz proto `CreateTaskRequest` field 11); verified in P0 T2 and T4. */
99
+ type ExecutionEnvironment = {
100
+ kind: 'sandbox';
101
+ } | {
102
+ kind: 'device';
103
+ device_id: string;
104
+ working_directory: string;
105
+ };
106
+ interface CreateTaskRequest {
107
+ message: Message;
108
+ project_id?: string;
109
+ locale?: string;
110
+ interactive_mode?: boolean;
111
+ hide_in_task_list?: boolean;
112
+ share_visibility?: ShareVisibility;
113
+ agent_profile?: string;
114
+ title?: string;
115
+ structured_output_schema?: Record<string, unknown>;
116
+ execution_environment?: ExecutionEnvironment;
117
+ }
118
+ interface CreateTaskResponse {
119
+ task_id: string;
120
+ task_title?: string;
121
+ task_url?: string;
122
+ share_url?: string;
123
+ share_visibility?: ShareVisibility;
124
+ }
125
+ interface TaskDetailResponse {
126
+ task: Task;
127
+ }
128
+ interface ListTasksParams {
129
+ limit?: number;
130
+ cursor?: string;
131
+ order?: 'asc' | 'desc';
132
+ scope?: 'all' | 'agent_subtask' | 'project' | 'standard';
133
+ agent_id?: string;
134
+ project_id?: string;
135
+ oauth_client_id?: string;
136
+ api_key_id?: string;
137
+ }
138
+ interface ListTasksResponse {
139
+ data: Task[];
140
+ has_more?: boolean;
141
+ next_cursor?: string;
142
+ }
143
+ interface TaskAttachment {
144
+ type?: 'image' | 'file' | 'voice' | 'slides';
145
+ filename?: string;
146
+ url?: string;
147
+ /** Path inside the execution environment, e.g. `/home/ubuntu/report.md`. */
148
+ path?: string;
149
+ content_type?: string;
150
+ file_uid?: string;
151
+ version_uid?: string;
152
+ }
153
+ interface QuestionExpectation {
154
+ options?: string[];
155
+ selection_mode?: 'single' | 'multiple';
156
+ response_method?: 'send_message';
157
+ }
158
+ interface StatusDetail {
159
+ waiting_for_event_id?: string;
160
+ waiting_for_event_type?: string;
161
+ waiting_description?: string;
162
+ confirm_input_schema?: Record<string, unknown>;
163
+ }
164
+ interface PlanStep {
165
+ status?: 'todo' | 'doing' | 'done' | 'failed';
166
+ title?: string;
167
+ /** Unix milliseconds */
168
+ started_at?: Int64String | null;
169
+ /** Unix milliseconds */
170
+ end_at?: Int64String | null;
171
+ }
172
+ interface TaskEventBase {
173
+ id: string;
174
+ /** Unix milliseconds */
175
+ timestamp?: Int64String;
176
+ }
177
+ type TaskEvent = (TaskEventBase & {
178
+ type: 'user_message';
179
+ user_message: {
180
+ content?: string;
181
+ message_type?: 'text' | 'voice';
182
+ attachments?: TaskAttachment[];
183
+ hidden_content?: string[];
184
+ };
185
+ }) | (TaskEventBase & {
186
+ type: 'assistant_message';
187
+ assistant_message: {
188
+ content?: string;
189
+ attachments?: TaskAttachment[];
190
+ question_expectation?: QuestionExpectation;
191
+ /** `opening`, `progress` or `result` (P0). */
192
+ delivery_kind?: string;
193
+ };
194
+ }) | (TaskEventBase & {
195
+ type: 'error_message';
196
+ error_message: {
197
+ error_type?: string;
198
+ content?: string;
199
+ };
200
+ }) | (TaskEventBase & {
201
+ type: 'status_update';
202
+ status_update: {
203
+ agent_status?: TaskStatus;
204
+ status_detail?: StatusDetail;
205
+ brief?: string;
206
+ description?: string;
207
+ };
208
+ }) | (TaskEventBase & {
209
+ type: 'tool_used';
210
+ tool_used: {
211
+ tool?: string;
212
+ action_id?: string;
213
+ status?: 'success' | 'error' | 'rollback' | 'pending_confirmation';
214
+ brief?: string;
215
+ description?: string;
216
+ message?: {
217
+ action?: string;
218
+ param?: string;
219
+ };
220
+ params?: Record<string, unknown>;
221
+ result?: Record<string, unknown>;
222
+ };
223
+ }) | (TaskEventBase & {
224
+ type: 'plan_update';
225
+ plan_update: {
226
+ steps?: PlanStep[];
227
+ };
228
+ }) | (TaskEventBase & {
229
+ type: 'new_plan_step';
230
+ new_plan_step: {
231
+ step_id?: string;
232
+ title?: string;
233
+ };
234
+ }) | (TaskEventBase & {
235
+ type: 'explanation';
236
+ explanation: {
237
+ content?: string;
238
+ };
239
+ }) | (TaskEventBase & {
240
+ type: 'user_stop';
241
+ }) | (TaskEventBase & {
242
+ type: 'structured_output_result';
243
+ structured_output_result: {
244
+ success?: boolean;
245
+ value?: Record<string, unknown>;
246
+ error?: string | null;
247
+ };
248
+ });
249
+ type TaskEventType = TaskEvent['type'];
250
+ interface ListMessagesParams {
251
+ task_id: string;
252
+ limit?: number;
253
+ cursor?: string;
254
+ start_event_id?: string;
255
+ order?: 'asc' | 'desc';
256
+ /** `tool_used`, `plan_update`, `new_plan_step` and `explanation` are only returned when true. */
257
+ verbose?: boolean;
258
+ slides_format?: 'html' | 'pptx';
259
+ }
260
+ interface ListMessagesResponse {
261
+ task_id: string;
262
+ messages: TaskEvent[];
263
+ has_more?: boolean;
264
+ next_cursor?: string;
265
+ }
266
+ interface SendMessageRequest {
267
+ task_id: string;
268
+ message: Message;
269
+ /** Applies to this and later turns. Omit to keep the current profile. */
270
+ agent_profile?: string;
271
+ /** Armed for the next finish only; a new schema replaces the previous one. */
272
+ structured_output_schema?: Record<string, unknown>;
273
+ clear_connectors?: boolean;
274
+ }
275
+ interface SendMessageResponse {
276
+ task_id: string;
277
+ }
278
+ interface ConfirmActionRequest {
279
+ task_id: string;
280
+ /** `status_update.status_detail.waiting_for_event_id` */
281
+ event_id: string;
282
+ input?: Record<string, unknown>;
283
+ }
284
+ interface ConfirmActionResponse {
285
+ task_id: string;
286
+ /** `false` when the input was valid but the pending action did not consume it. */
287
+ confirmed?: boolean;
288
+ }
289
+ interface DeleteTaskResponse {
290
+ id: string;
291
+ deleted: boolean;
292
+ }
293
+ interface Project {
294
+ id: string;
295
+ name?: string;
296
+ /** Unix seconds. `project.create` returns the Go zero time `-62135596800` (server bug). */
297
+ created_at?: Int64String;
298
+ instruction?: string;
299
+ }
300
+ interface CreateProjectRequest {
301
+ name: string;
302
+ instruction?: string;
303
+ }
304
+ interface CreateProjectResponse {
305
+ project: Project;
306
+ }
307
+ interface ListProjectsResponse {
308
+ data: Project[];
309
+ }
310
+ interface UserMeResponse {
311
+ user_id: string;
312
+ message?: string;
313
+ /** Not documented, returned in P0 T1. */
314
+ name?: string;
315
+ email?: string;
316
+ avatar_url?: string;
317
+ }
318
+ type FileStatus = 'pending' | 'uploaded' | 'deleted' | 'error';
319
+ interface FileRecord {
320
+ id: string;
321
+ filename?: string;
322
+ status?: FileStatus;
323
+ /** Unix seconds */
324
+ created_at?: Int64String;
325
+ }
326
+ interface FileDetail extends FileRecord {
327
+ bytes?: Int64String;
328
+ content_type?: string;
329
+ /** Unix seconds; files live 48 hours. */
330
+ expires_at?: Int64String;
331
+ error_message?: string | null;
332
+ }
333
+ interface UploadFileResponse {
334
+ file: FileRecord;
335
+ /** Presigned PUT URL, valid for 3 minutes. */
336
+ upload_url: string;
337
+ /** Unix seconds */
338
+ upload_expires_at?: Int64String;
339
+ }
340
+ interface FileDetailResponse {
341
+ file: FileDetail;
342
+ /** Presigned GET URL, valid for at most 3 minutes. Only while `file.status` is `uploaded`. */
343
+ download_url?: string;
344
+ download_expires_at?: Int64String;
345
+ }
346
+ type ArtifactType = 'website' | 'mobile_app' | 'game' | 'documents' | 'slides' | 'spreadsheets' | 'images' | 'videos' | 'audio' | 'others';
347
+ interface ArtifactSource {
348
+ kind?: 'oauth_client' | 'api_key';
349
+ oauth_client_id?: string;
350
+ api_key_id?: string;
351
+ name?: string;
352
+ }
353
+ interface Artifact {
354
+ id: string;
355
+ task_id?: string;
356
+ task_title?: string;
357
+ filename?: string;
358
+ display_name?: string;
359
+ type?: ArtifactType;
360
+ file_type?: 'file' | 'image' | 'slide' | 'website' | 'webdev';
361
+ content_type?: string;
362
+ url?: string;
363
+ cover_image?: string;
364
+ favorite?: boolean;
365
+ /** Unix seconds */
366
+ updated_at?: Int64String;
367
+ platform_type?: number;
368
+ logo?: string;
369
+ resource_uri?: string;
370
+ source?: ArtifactSource;
371
+ }
372
+ interface ListArtifactsParams {
373
+ limit?: number;
374
+ cursor?: string;
375
+ name?: string;
376
+ type?: 'all' | ArtifactType;
377
+ favorite?: boolean;
378
+ oauth_client_id?: string;
379
+ api_key_id?: string;
380
+ }
381
+ interface ListArtifactsResponse {
382
+ data: Artifact[];
383
+ has_more?: boolean;
384
+ next_cursor?: string;
385
+ }
386
+ interface ConnectorInfo {
387
+ id: string;
388
+ name?: string;
389
+ type?: 'builtin' | 'byok' | 'mcp';
390
+ description?: string;
391
+ category?: string;
392
+ }
393
+ interface ListConnectorsResponse {
394
+ data: ConnectorInfo[];
395
+ }
396
+ interface Skill {
397
+ id: string;
398
+ name?: string;
399
+ description?: string;
400
+ owner_type?: 'personal' | 'official' | 'team' | 'project' | 'app';
401
+ creator_info?: {
402
+ user_id?: string;
403
+ name?: string;
404
+ };
405
+ created_at?: Int64String;
406
+ updated_at?: Int64String;
407
+ }
408
+ interface ListSkillsResponse {
409
+ data: Skill[];
410
+ }
411
+ /** Values of the `x-ratelimit-*` response headers. */
412
+ interface RateLimitInfo {
413
+ limit?: number;
414
+ remaining?: number;
415
+ /** Seconds until the window resets. */
416
+ reset?: number;
417
+ }
418
+
419
+ declare const MANUS_API_BASE_URL = "https://api.manus.ai";
420
+ interface ManusApiErrorOptions extends ErrorOptions {
421
+ /** HTTP status, or 0 for a network failure. */
422
+ status: number;
423
+ code: string;
424
+ requestId?: string;
425
+ }
426
+ /** Error codes seen so far: `unauthenticated` (401), `permission_denied` (403), `not_found` (404), `rate_limited` (429). */
427
+ declare class ManusApiError extends Error {
428
+ readonly status: number;
429
+ readonly code: string;
430
+ readonly requestId: string | undefined;
431
+ constructor(message: string, options: ManusApiErrorOptions);
432
+ }
433
+ type PollQueue = Pick<Queue, 'add'>;
434
+ interface ManusClientOptions {
435
+ /** Called on every request; cache inside the provider. Never logged. */
436
+ getApiKey: () => string | Promise<string>;
437
+ /** Called once after a 401 before the single retry, so a rotated key can be re-read (design D15.1). */
438
+ onUnauthorized?: () => void | Promise<void>;
439
+ baseUrl?: string;
440
+ fetch?: typeof globalThis.fetch;
441
+ /** Per API request. */
442
+ timeoutMs?: number;
443
+ /** Per presigned upload or download, which can carry a whole artifact. */
444
+ transferTimeoutMs?: number;
445
+ /** Shared budget for `task.detail` and `task.listMessages`; see `createPollQueue`. */
446
+ pollQueue?: PollQueue;
447
+ sleep?: (ms: number) => Promise<void>;
448
+ }
449
+ /** One process keeps its polling under `ratePerMinute` requests across all tasks it tracks (design D11). */
450
+ declare function createPollQueue(ratePerMinute: number, concurrency?: number): Queue;
451
+ declare class ManusClient {
452
+ private readonly options;
453
+ /** Values of the `x-ratelimit-*` headers from the most recent response. */
454
+ lastRateLimit: RateLimitInfo | undefined;
455
+ private readonly baseUrl;
456
+ private readonly fetch;
457
+ private readonly timeoutMs;
458
+ private readonly transferTimeoutMs;
459
+ private readonly sleep;
460
+ constructor(options: ManusClientOptions);
461
+ userMe(): Promise<UserMeResponse>;
462
+ projectCreate(body: CreateProjectRequest): Promise<CreateProjectResponse>;
463
+ projectList(): Promise<ListProjectsResponse>;
464
+ taskCreate(body: CreateTaskRequest): Promise<CreateTaskResponse>;
465
+ taskDetail(taskId: string): Promise<TaskDetailResponse>;
466
+ taskList(params?: ListTasksParams): Promise<ListTasksResponse>;
467
+ taskListMessages(params: ListMessagesParams): Promise<ListMessagesResponse>;
468
+ taskSendMessage(body: SendMessageRequest): Promise<SendMessageResponse>;
469
+ taskStop(taskId: string): Promise<Record<string, never>>;
470
+ taskConfirmAction(body: ConfirmActionRequest): Promise<ConfirmActionResponse>;
471
+ /** Repeated deletes and unknown IDs both answer 404 `not_found` (P0 section 12). */
472
+ taskDelete(taskId: string): Promise<DeleteTaskResponse>;
473
+ fileUpload(filename: string): Promise<UploadFileResponse>;
474
+ fileDetail(fileId: string): Promise<FileDetailResponse>;
475
+ artifactList(params?: ListArtifactsParams): Promise<ListArtifactsResponse>;
476
+ connectorList(): Promise<ListConnectorsResponse>;
477
+ skillList(projectId?: string): Promise<ListSkillsResponse>;
478
+ /**
479
+ * PUT to the presigned `upload_url` of `file.upload`. The URL points at a storage host, so the
480
+ * request carries no API key and no content type (P0 T10: only the host is signed). A `Blob`
481
+ * from `fs.openAsBlob` streams from disk and sets the content length.
482
+ */
483
+ uploadToUrl(uploadUrl: string, body: Blob): Promise<void>;
484
+ /** GET a presigned artifact URL (signed for 48 hours), with no API key; the caller streams the body. */
485
+ downloadFromUrl(url: string): Promise<Response>;
486
+ private transfer;
487
+ private get;
488
+ private post;
489
+ private request;
490
+ }
491
+
492
+ type EventOf<T extends TaskEventType> = Extract<TaskEvent, {
493
+ type: T;
494
+ }>;
495
+ /** Event `timestamp` and plan `started_at` are Unix milliseconds, serialized as decimal strings. */
496
+ declare function toMillis(value: Int64String | null | undefined): number | undefined;
497
+ /** All helpers below expect `events` in ascending order (`task.listMessages` with `order=asc`). */
498
+ declare function lastEventOfType<T extends TaskEventType>(events: readonly TaskEvent[], type: T): EventOf<T> | undefined;
499
+ /**
500
+ * The agent's final answer: the last `assistant_message` with `delivery_kind: "result"` (P0 F3).
501
+ * Falls back to the last assistant message when no event carries `delivery_kind`.
502
+ */
503
+ declare function finalAssistantMessage(events: readonly TaskEvent[]): EventOf<'assistant_message'> | undefined;
504
+ declare function outputAttachments(events: readonly TaskEvent[]): TaskAttachment[];
505
+ declare function structuredOutput(events: readonly TaskEvent[]): {
506
+ success?: boolean;
507
+ value?: Record<string, unknown>;
508
+ error?: string | null;
509
+ } | undefined;
510
+ declare function latestAgentStatus(events: readonly TaskEvent[]): TaskStatus | undefined;
511
+ /**
512
+ * `task.stop` leaves the task `stopped` plus a `user_stop` event (P0 T12). The stop is only
513
+ * current when no user message came after it: a follow-up turn starts the task again.
514
+ */
515
+ declare function wasStoppedByUser(events: readonly TaskEvent[]): boolean;
516
+ /**
517
+ * Details of a pending `waiting` state, from the most recent `status_update`. Not observed in P0
518
+ * (questions arrived as a normal final answer), kept for the documented confirmation flow.
519
+ */
520
+ declare function pendingWaiting(events: readonly TaskEvent[]): StatusDetail | undefined;
521
+
522
+ export { AGENT_EFFORTS, AGENT_PROFILES, MANUS_API_BASE_URL, ManusApiError, ManusClient, createPollQueue, finalAssistantMessage, formatAgentProfile, lastEventOfType, latestAgentStatus, outputAttachments, parseAgentProfile, pendingWaiting, structuredOutput, toMillis, wasStoppedByUser };
523
+ export type { AgentEffort, AgentProfile, AgentProfileValue, ApiError, Artifact, ArtifactSource, ArtifactType, ConfirmActionRequest, ConfirmActionResponse, ConnectorInfo, ContentPart, CreateProjectRequest, CreateProjectResponse, CreateTaskRequest, CreateTaskResponse, DeleteTaskResponse, ErrorResponse, ExecutionEnvironment, FileDetail, FileDetailResponse, FileRecord, FileStatus, Int64String, ListArtifactsParams, ListArtifactsResponse, ListConnectorsResponse, ListMessagesParams, ListMessagesResponse, ListProjectsResponse, ListSkillsResponse, ListTasksParams, ListTasksResponse, ManusApiErrorOptions, ManusClientOptions, Message, ParsedAgentProfile, PlanStep, PollQueue, Project, QuestionExpectation, RateLimitInfo, SendMessageRequest, SendMessageResponse, ShareVisibility, Skill, StatusDetail, Task, TaskAttachment, TaskDetailResponse, TaskEvent, TaskEventType, TaskStatus, TaskType, UploadFileResponse, UserMeResponse };
package/dist/index.mjs ADDED
@@ -0,0 +1,342 @@
1
+ import { newQueue } from '@henrygd/queue/rl';
2
+ import { extractErrorMessage } from 'foxts/extract-error-message';
3
+ import { wait } from 'foxts/wait';
4
+
5
+ const AGENT_PROFILES = [
6
+ 'lite',
7
+ 'standard',
8
+ 'max'
9
+ ];
10
+ /** Thinking effort. Only applies to the `max` profile. */ const AGENT_EFFORTS = [
11
+ 'medium',
12
+ 'high',
13
+ 'extra',
14
+ 'ultra'
15
+ ];
16
+ /**
17
+ * Build the `agent_profile` value that the Manus API accepts.
18
+ *
19
+ * @throws {TypeError} when `effort` is combined with a profile other than `max`.
20
+ */ function formatAgentProfile(profile, effort) {
21
+ if (profile !== 'max') {
22
+ if (effort != null) {
23
+ throw new TypeError(`effort "${effort}" only applies to the "max" profile, got "${profile}"`);
24
+ }
25
+ return profile;
26
+ }
27
+ return effort == null ? 'max' : `max-${effort}`;
28
+ }
29
+ /** `manus-1.6-`, `2.0-`, `1.6` ... the version segment is ignored by the API and carries no information. */ const LEGACY_VERSION_PREFIX = /^(?:manus-)?\d+(?:\.\d+)*-?/;
30
+ /**
31
+ * Normalize the profile reported by the API, which uses the legacy versioned form (`manus-1.6`,
32
+ * `manus-1.6-lite`, `manus-1.6-max-high`; P0 T4, T5, T8). A bare version means `standard`.
33
+ * Returns `undefined` for values this version does not know.
34
+ */ function parseAgentProfile(value) {
35
+ if (value == null) return undefined;
36
+ const rest = value.replace(LEGACY_VERSION_PREFIX, '');
37
+ if (rest === '' || rest === 'standard') return {
38
+ agent_profile: 'standard'
39
+ };
40
+ if (rest === 'lite') return {
41
+ agent_profile: 'lite'
42
+ };
43
+ if (rest === 'max') return {
44
+ agent_profile: 'max'
45
+ };
46
+ if (rest.startsWith('max-')) {
47
+ const effort = rest.slice('max-'.length);
48
+ if (AGENT_EFFORTS.includes(effort)) {
49
+ return {
50
+ agent_profile: 'max',
51
+ effort: effort
52
+ };
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ const MANUS_API_BASE_URL = 'https://api.manus.ai';
59
+ /** Error codes seen so far: `unauthenticated` (401), `permission_denied` (403), `not_found` (404), `rate_limited` (429). */ class ManusApiError extends Error {
60
+ constructor(message, options){
61
+ super(message, {
62
+ cause: options.cause
63
+ });
64
+ this.name = 'ManusApiError';
65
+ this.status = options.status;
66
+ this.code = options.code;
67
+ this.requestId = options.requestId;
68
+ }
69
+ }
70
+ /** One process keeps its polling under `ratePerMinute` requests across all tasks it tracks (design D11). */ function createPollQueue(ratePerMinute, concurrency = 8) {
71
+ return newQueue(concurrency, ratePerMinute, 60000);
72
+ }
73
+ const MAX_RATE_LIMIT_RETRIES = 5;
74
+ const MAX_BACKOFF_MS = 60000;
75
+ const TRAILING_SLASHES = /\/+$/;
76
+ function backoffMs(attempt, retryAfterHeader) {
77
+ const retryAfter = retryAfterHeader == null ? Number.NaN : Number(retryAfterHeader) * 1000;
78
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
79
+ return Math.min(retryAfter, MAX_BACKOFF_MS);
80
+ }
81
+ return Math.min(1000 * 2 ** (attempt - 1) + Math.random() * 1000, MAX_BACKOFF_MS);
82
+ }
83
+ function headerNumber(headers, name) {
84
+ const value = headers.get(name);
85
+ if (value == null) return undefined;
86
+ const n = Number(value);
87
+ return Number.isFinite(n) ? n : undefined;
88
+ }
89
+ class ManusClient {
90
+ constructor(options){
91
+ this.options = options;
92
+ this.baseUrl = (options.baseUrl ?? MANUS_API_BASE_URL).replace(TRAILING_SLASHES, '');
93
+ this.fetch = options.fetch ?? globalThis.fetch;
94
+ this.timeoutMs = options.timeoutMs ?? 60000;
95
+ this.transferTimeoutMs = options.transferTimeoutMs ?? 600000;
96
+ this.sleep = options.sleep ?? wait;
97
+ }
98
+ userMe() {
99
+ return this.get('/v2/user.me');
100
+ }
101
+ projectCreate(body) {
102
+ return this.post('/v2/project.create', body);
103
+ }
104
+ projectList() {
105
+ return this.get('/v2/project.list');
106
+ }
107
+ taskCreate(body) {
108
+ return this.post('/v2/task.create', body);
109
+ }
110
+ taskDetail(taskId) {
111
+ return this.get('/v2/task.detail', {
112
+ task_id: taskId
113
+ }, true);
114
+ }
115
+ taskList(params = {}) {
116
+ return this.get('/v2/task.list', params);
117
+ }
118
+ taskListMessages(params) {
119
+ return this.get('/v2/task.listMessages', params, true);
120
+ }
121
+ taskSendMessage(body) {
122
+ return this.post('/v2/task.sendMessage', body);
123
+ }
124
+ taskStop(taskId) {
125
+ return this.post('/v2/task.stop', {
126
+ task_id: taskId
127
+ });
128
+ }
129
+ taskConfirmAction(body) {
130
+ return this.post('/v2/task.confirmAction', body);
131
+ }
132
+ /** Repeated deletes and unknown IDs both answer 404 `not_found` (P0 section 12). */ taskDelete(taskId) {
133
+ return this.post('/v2/task.delete', {
134
+ task_id: taskId
135
+ });
136
+ }
137
+ fileUpload(filename) {
138
+ return this.post('/v2/file.upload', {
139
+ filename
140
+ });
141
+ }
142
+ fileDetail(fileId) {
143
+ return this.get('/v2/file.detail', {
144
+ file_id: fileId
145
+ });
146
+ }
147
+ artifactList(params = {}) {
148
+ return this.get('/v2/artifact.list', params);
149
+ }
150
+ connectorList() {
151
+ return this.get('/v2/connector.list');
152
+ }
153
+ skillList(projectId) {
154
+ return this.get('/v2/skill.list', {
155
+ project_id: projectId
156
+ });
157
+ }
158
+ /**
159
+ * PUT to the presigned `upload_url` of `file.upload`. The URL points at a storage host, so the
160
+ * request carries no API key and no content type (P0 T10: only the host is signed). A `Blob`
161
+ * from `fs.openAsBlob` streams from disk and sets the content length.
162
+ */ async uploadToUrl(uploadUrl, body) {
163
+ const res = await this.transfer(uploadUrl, {
164
+ method: 'PUT',
165
+ body
166
+ });
167
+ if (!res.ok) {
168
+ throw new ManusApiError(`Upload to the presigned URL failed with HTTP ${res.status}`, {
169
+ status: res.status,
170
+ code: `http_${res.status}`
171
+ });
172
+ }
173
+ }
174
+ /** GET a presigned artifact URL (signed for 48 hours), with no API key; the caller streams the body. */ async downloadFromUrl(url) {
175
+ const res = await this.transfer(url, {
176
+ method: 'GET'
177
+ });
178
+ if (!res.ok) {
179
+ await res.body?.cancel();
180
+ throw new ManusApiError(`Download failed with HTTP ${res.status}`, {
181
+ status: res.status,
182
+ code: `http_${res.status}`
183
+ });
184
+ }
185
+ return res;
186
+ }
187
+ async transfer(url, init) {
188
+ try {
189
+ return await this.fetch(url, {
190
+ ...init,
191
+ signal: AbortSignal.timeout(this.transferTimeoutMs)
192
+ });
193
+ } catch (error) {
194
+ throw new ManusApiError(extractErrorMessage(error) ?? 'transfer failed', {
195
+ status: 0,
196
+ code: 'network',
197
+ cause: error
198
+ });
199
+ }
200
+ }
201
+ get(endpoint, query, poll = false) {
202
+ return this.request('GET', endpoint, {
203
+ query,
204
+ poll
205
+ });
206
+ }
207
+ post(endpoint, body) {
208
+ return this.request('POST', endpoint, {
209
+ body
210
+ });
211
+ }
212
+ async request(method, endpoint, init) {
213
+ const url = new URL(this.baseUrl + endpoint);
214
+ if (init.query) {
215
+ const entries = Object.entries(init.query);
216
+ for(let i = 0, len = entries.length; i < len; i++){
217
+ const [key, value] = entries[i];
218
+ if (value !== undefined) url.searchParams.set(key, String(value));
219
+ }
220
+ }
221
+ let retriedAuth = false;
222
+ let rateLimitRetries = 0;
223
+ for(;;){
224
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
225
+ const apiKey = await this.options.getApiKey();
226
+ const headers = {
227
+ 'x-manus-api-key': apiKey,
228
+ accept: 'application/json'
229
+ };
230
+ if (init.body !== undefined) headers['content-type'] = 'application/json';
231
+ const send = async ()=>{
232
+ const res = await this.fetch(url, {
233
+ method,
234
+ headers,
235
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
236
+ signal: AbortSignal.timeout(this.timeoutMs)
237
+ });
238
+ return {
239
+ res,
240
+ text: await res.text()
241
+ };
242
+ };
243
+ let res;
244
+ let text;
245
+ try {
246
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
247
+ ({ res, text } = await (init.poll && this.options.pollQueue ? this.options.pollQueue.add(send) : send()));
248
+ } catch (error) {
249
+ throw new ManusApiError(extractErrorMessage(error) ?? 'request failed', {
250
+ status: 0,
251
+ code: 'network',
252
+ cause: error
253
+ });
254
+ }
255
+ this.lastRateLimit = {
256
+ limit: headerNumber(res.headers, 'x-ratelimit-limit'),
257
+ remaining: headerNumber(res.headers, 'x-ratelimit-remaining'),
258
+ reset: headerNumber(res.headers, 'x-ratelimit-reset')
259
+ };
260
+ let json;
261
+ try {
262
+ json = text ? JSON.parse(text) : undefined;
263
+ } catch {
264
+ json = undefined;
265
+ }
266
+ if (json && res.ok && json.ok !== false) {
267
+ return json;
268
+ }
269
+ if (!retriedAuth && res.status === 401 && this.options.onUnauthorized) {
270
+ retriedAuth = true;
271
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
272
+ await this.options.onUnauthorized();
273
+ continue;
274
+ }
275
+ if (rateLimitRetries < MAX_RATE_LIMIT_RETRIES && res.status === 429) {
276
+ rateLimitRetries++;
277
+ // eslint-disable-next-line no-await-in-loop -- sequential retry loop
278
+ await this.sleep(backoffMs(rateLimitRetries, res.headers.get('retry-after')));
279
+ continue;
280
+ }
281
+ throw new ManusApiError(json?.error?.message ?? `Manus API ${method} ${endpoint} failed with HTTP ${res.status}`, {
282
+ status: res.status,
283
+ code: json?.error?.code ?? (res.status === 429 ? 'rate_limited' : `http_${res.status}`),
284
+ requestId: json?.request_id
285
+ });
286
+ }
287
+ }
288
+ }
289
+
290
+ /** Event `timestamp` and plan `started_at` are Unix milliseconds, serialized as decimal strings. */ function toMillis(value) {
291
+ if (value == null) return undefined;
292
+ const n = Number(value);
293
+ return Number.isFinite(n) ? n : undefined;
294
+ }
295
+ /** All helpers below expect `events` in ascending order (`task.listMessages` with `order=asc`). */ function lastEventOfType(events, type) {
296
+ for(let i = events.length - 1; i >= 0; i--){
297
+ if (events[i].type === type) return events[i];
298
+ }
299
+ return undefined;
300
+ }
301
+ /**
302
+ * The agent's final answer: the last `assistant_message` with `delivery_kind: "result"` (P0 F3).
303
+ * Falls back to the last assistant message when no event carries `delivery_kind`.
304
+ */ function finalAssistantMessage(events) {
305
+ let fallback;
306
+ for(let i = events.length - 1; i >= 0; i--){
307
+ const event = events[i];
308
+ if (event.type !== 'assistant_message') continue;
309
+ if (event.assistant_message.delivery_kind === 'result') return event;
310
+ fallback ??= event;
311
+ }
312
+ return fallback;
313
+ }
314
+ function outputAttachments(events) {
315
+ return finalAssistantMessage(events)?.assistant_message.attachments ?? [];
316
+ }
317
+ function structuredOutput(events) {
318
+ return lastEventOfType(events, 'structured_output_result')?.structured_output_result;
319
+ }
320
+ function latestAgentStatus(events) {
321
+ return lastEventOfType(events, 'status_update')?.status_update.agent_status;
322
+ }
323
+ /**
324
+ * `task.stop` leaves the task `stopped` plus a `user_stop` event (P0 T12). The stop is only
325
+ * current when no user message came after it: a follow-up turn starts the task again.
326
+ */ function wasStoppedByUser(events) {
327
+ for(let i = events.length - 1; i >= 0; i--){
328
+ const type = events[i].type;
329
+ if (type === 'user_stop') return true;
330
+ if (type === 'user_message') return false;
331
+ }
332
+ return false;
333
+ }
334
+ /**
335
+ * Details of a pending `waiting` state, from the most recent `status_update`. Not observed in P0
336
+ * (questions arrived as a normal final answer), kept for the documented confirmation flow.
337
+ */ function pendingWaiting(events) {
338
+ const status = lastEventOfType(events, 'status_update')?.status_update;
339
+ return status?.agent_status === 'waiting' ? status.status_detail ?? {} : undefined;
340
+ }
341
+
342
+ export { AGENT_EFFORTS, AGENT_PROFILES, MANUS_API_BASE_URL, ManusApiError, ManusClient, createPollQueue, finalAssistantMessage, formatAgentProfile, lastEventOfType, latestAgentStatus, outputAttachments, parseAgentProfile, pendingWaiting, structuredOutput, toMillis, wasStoppedByUser };
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "manus-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Typed client for the Manus API v2",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/SukkaW/mcp-manus",
8
+ "directory": "packages/manus-api"
9
+ },
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.cjs",
21
+ "default": "./dist/index.cjs"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "keywords": [
26
+ "manus",
27
+ "manus-api"
28
+ ],
29
+ "author": "SukkaW <https://skk.moe>",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@henrygd/queue": "^1.2.0",
33
+ "foxts": "^5.9.1"
34
+ },
35
+ "devDependencies": {
36
+ "@swc-node/register": "^1.12.1",
37
+ "@swc/core": "^1.16.20",
38
+ "@types/mocha": "^10.0.10",
39
+ "@types/node": "^24.19.2",
40
+ "bunchee": "^6.12.2",
41
+ "earl": "^2.0.0",
42
+ "mocha": "^12.0.3",
43
+ "nyc": "^18.0.0"
44
+ },
45
+ "scripts": {
46
+ "build": "bunchee --no-sourcemap",
47
+ "lint": "eslint --format=sukka .",
48
+ "test": "SWC_NODE_IGNORE_DYNAMIC=1 SWC_NODE_INLINE_SOURCE_MAP=1 nyc mocha",
49
+ "typecheck": "tsc --build --noEmit"
50
+ }
6
51
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.