@kosuke-ai/kosuke 1.0.50 → 1.0.52
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 +8 -0
- package/dist/generated/commands/attachments/create.js +21 -0
- package/dist/generated/commands/deploys/queue-update.js +2 -1
- package/dist/generated/commands/deploys/start.js +2 -1
- package/dist/generated/commands/index.js +341 -339
- package/dist/generated/commands/integrations/browser-connections-create--personal-network-linkedin.js +1 -1
- package/dist/generated/commands/integrations/browser-connections-create--personal-network-x.js +1 -1
- package/dist/generated/commands/mac/browser-login.js +1 -1
- package/dist/generated/commands/sessions/create.js +2 -1
- package/dist/generated/commands/sessions/prompt-update.js +2 -1
- package/dist/generated/commands/sessions/queue-update.js +2 -1
- package/dist/generated/commands/sessions/send.js +2 -1
- package/dist/generated/commands/tasks/create.js +2 -1
- package/dist/runtime/attach.js +16 -2
- package/dist/runtime/attachments.js +37 -8
- package/dist/runtime/client.js +20 -0
- package/dist/runtime/invoke.js +120 -5
- package/openapi.json +79 -3
- package/package.json +4 -5
|
@@ -13,7 +13,7 @@ export const operation = {
|
|
|
13
13
|
"equals": "linkedin"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"summary": "Connect LinkedIn in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
16
|
+
"summary": "Connect LinkedIn in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, DEVICE_CALL_INTERRUPTED (the connection to the Mac dropped; try again), PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
17
17
|
"pathParams": [],
|
|
18
18
|
"query": [],
|
|
19
19
|
"body": {
|
package/dist/generated/commands/integrations/browser-connections-create--personal-network-x.js
CHANGED
|
@@ -13,7 +13,7 @@ export const operation = {
|
|
|
13
13
|
"equals": "x"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"summary": "Connect X in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
16
|
+
"summary": "Connect X in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, DEVICE_CALL_INTERRUPTED (the connection to the Mac dropped; try again), PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
17
17
|
"pathParams": [],
|
|
18
18
|
"query": [],
|
|
19
19
|
"body": {
|
|
@@ -6,7 +6,7 @@ export const operation = {
|
|
|
6
6
|
"browser",
|
|
7
7
|
"login"
|
|
8
8
|
],
|
|
9
|
-
"summary": "Get a site signed in in this Sensei Thread's browser from the member's everyday Chrome: pass the browser call's profile and deviceId, and the site as a bare hostname (twitter.com is x.com; Google's sites are refused). --reuse picks up the sign-in that profile in their Chrome holds by itself, with no tap: once per site and profile in a Turn, with the profile's exact ID. It never opens Chrome or waits for anyone, and answers opened false and signedIn false with chrome none (their Chrome holds no sign-in for the site, so nothing changed), same (the one this thread's browser already holds) or different (taken into this thread's browser, taken true). Then, unless it answered none or same, open the site with browser and check the account. On SIGN_IN_NEEDS_TAP or UPDATE_REQUIRED, put the sign-in card: `kosuke sensei cards set` with --sign-in naming this site, profile and deviceId. A --reuse spends no tap, so it gives no card back. Every tapped mode (`sign-in`, `take`, `waitNew`) needs the member's tap on the sign-in card in this thread; `--forget` and `--reuse` need none. A sign-in takes the sign-in that profile in their Chrome holds when it differs from this thread's, without opening anything, and answers taken true, opened false and signedIn false, since the site has not confirmed it; or else it opens --url, or the site's page, in their Chrome and waits up to 8 minutes for the member to sign in. On LinkedIn and X it answers signedIn true and opened true. On any other site a change of the site's cookies is taken and answered taken true and signedIn false, since a change is not a sign-in, and an end with no change answers taken false; a desktop app too old to watch the site opens it and answers at once, with no taken. Every answer with taken true carries waitNewLeft: while it is above 0 and the site is still signed out, --wait-new (with the same --url), in the same Turn and with no new tap, waits for a newer sign-in, never taking the one taken already, and opens the site only when this Turn has not; once the tap's time has run out it fails with SIGN_IN_WINDOW_ENDED, and no card comes back. After the member's I'm signed in tap, --take takes that site's cookies, never Google's, and answers taken. --forget removes a sign-in this Turn took, a --reuse's included, for an account other than the one expected, and needs no tap. Each tap allows one call: a call without one, or a --wait-new that no take armed, fails with SIGN_IN_NEEDS_TAP. Then open the site with browser and check the account before account-specific work. On DEVICE_OFFLINE, TIMEOUT, SIGN_IN_TIMEOUT and KEYCHAIN_UNAVAILABLE Kosuke gives the member's card back by itself. Fails with SIGN_IN_TIMEOUT when Kosuke saw no new sign-in to LinkedIn or X within 8 minutes, PROFILE_DISCONNECTED for a profile turned off in Settings → Devices, KEYCHAIN_UNAVAILABLE when macOS did not let Kosuke read Chrome's sign-ins, STALE_TURN when the Turn stopped, and UPDATE_REQUIRED on a desktop app too old for it. Only Sensei calls it.",
|
|
9
|
+
"summary": "Get a site signed in in this Sensei Thread's browser from the member's everyday Chrome: pass the browser call's profile and deviceId, and the site as a bare hostname (twitter.com is x.com; Google's sites are refused). --reuse picks up the sign-in that profile in their Chrome holds by itself, with no tap: once per site and profile in a Turn, with the profile's exact ID. It never opens Chrome or waits for anyone, and answers opened false and signedIn false with chrome none (their Chrome holds no sign-in for the site, so nothing changed), same (the one this thread's browser already holds) or different (taken into this thread's browser, taken true). Then, unless it answered none or same, open the site with browser and check the account. On SIGN_IN_NEEDS_TAP or UPDATE_REQUIRED, put the sign-in card: `kosuke sensei cards set` with --sign-in naming this site, profile and deviceId. A --reuse spends no tap, so it gives no card back. Every tapped mode (`sign-in`, `take`, `waitNew`) needs the member's tap on the sign-in card in this thread; `--forget` and `--reuse` need none. A sign-in takes the sign-in that profile in their Chrome holds when it differs from this thread's, without opening anything, and answers taken true, opened false and signedIn false, since the site has not confirmed it; or else it opens --url, or the site's page, in their Chrome and waits up to 8 minutes for the member to sign in. On LinkedIn and X it answers signedIn true and opened true. On any other site a change of the site's cookies is taken and answered taken true and signedIn false, since a change is not a sign-in, and an end with no change answers taken false; a desktop app too old to watch the site opens it and answers at once, with no taken. Every answer with taken true carries waitNewLeft: while it is above 0 and the site is still signed out, --wait-new (with the same --url), in the same Turn and with no new tap, waits for a newer sign-in, never taking the one taken already, and opens the site only when this Turn has not; once the tap's time has run out it fails with SIGN_IN_WINDOW_ENDED, and no card comes back. After the member's I'm signed in tap, --take takes that site's cookies, never Google's, and answers taken. --forget removes a sign-in this Turn took, a --reuse's included, for an account other than the one expected, and needs no tap. Each tap allows one call: a call without one, or a --wait-new that no take armed, fails with SIGN_IN_NEEDS_TAP. Then open the site with browser and check the account before account-specific work. On DEVICE_OFFLINE, DEVICE_CALL_INTERRUPTED, TIMEOUT, SIGN_IN_TIMEOUT and KEYCHAIN_UNAVAILABLE Kosuke gives the member's card back by itself. Fails with SIGN_IN_TIMEOUT when Kosuke saw no new sign-in to LinkedIn or X within 8 minutes, PROFILE_DISCONNECTED for a profile turned off in Settings → Devices, KEYCHAIN_UNAVAILABLE when macOS did not let Kosuke read Chrome's sign-ins, STALE_TURN when the Turn stopped, and UPDATE_REQUIRED on a desktop app too old for it. Only Sensei calls it.",
|
|
10
10
|
"pathParams": [],
|
|
11
11
|
"query": [],
|
|
12
12
|
"body": {
|
package/dist/runtime/attach.js
CHANGED
|
@@ -184,8 +184,22 @@ async function run(leaf, runtime, operation, input) {
|
|
|
184
184
|
anonymous: operation.anonymous,
|
|
185
185
|
answerTimeoutMs: operation.answerTimeoutMs,
|
|
186
186
|
});
|
|
187
|
-
const
|
|
188
|
-
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
const cancel = () => controller.abort();
|
|
189
|
+
// Stop in-flight staging and prevent the final send on a CLI interruption.
|
|
190
|
+
// Hard exits and lost responses are covered by the server's staging expiry.
|
|
191
|
+
if (request.deferredAttachments) {
|
|
192
|
+
process.once('SIGINT', cancel);
|
|
193
|
+
process.once('SIGTERM', cancel);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const outcome = await execute(client, request, controller.signal);
|
|
197
|
+
runtime.output(outcome);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
process.removeListener('SIGINT', cancel);
|
|
201
|
+
process.removeListener('SIGTERM', cancel);
|
|
202
|
+
}
|
|
189
203
|
}
|
|
190
204
|
/** `(personal only)`, `(workspace only)`, `(--provider claude_code, codex)`; empty for all. */
|
|
191
205
|
function whoTakes(leafFlag, forms) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
1
|
+
import { openAsBlob, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, extname } from 'node:path';
|
|
3
3
|
/**
|
|
4
4
|
* Files on a multipart request, the way the platform reads them.
|
|
@@ -46,18 +46,47 @@ function formatBytes(bytes) {
|
|
|
46
46
|
const mib = bytes / (1024 * 1024);
|
|
47
47
|
return Number.isInteger(mib) ? `${mib} MiB` : `${bytes}-byte`;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
/** Leave room for multipart overhead below the app proxy's body limit. */
|
|
50
|
+
export const INLINE_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
51
|
+
/** Validate the entire batch before reading or uploading any file. */
|
|
52
|
+
export function attachmentBytes(paths, { maxFiles, maxBytes }) {
|
|
50
53
|
if (paths.length > maxFiles) {
|
|
51
54
|
throw new Error(`At most ${maxFiles} attachments per request; ${paths.length} given.`);
|
|
52
55
|
}
|
|
53
|
-
|
|
54
|
-
// this message, not with the runtime's own error once it has been loaded.
|
|
56
|
+
let total = 0;
|
|
55
57
|
for (const path of paths) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const size = statSync(path).size;
|
|
59
|
+
checkSize(path, size, maxBytes);
|
|
60
|
+
total += size;
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
return total;
|
|
63
|
+
}
|
|
64
|
+
function checkSize(path, size, maxBytes) {
|
|
65
|
+
if (size > maxBytes) {
|
|
66
|
+
throw new Error(`${path} is larger than the ${formatBytes(maxBytes)} attachment limit.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function appendAttachments(form, paths, limits) {
|
|
70
|
+
attachmentBytes(paths, limits);
|
|
71
|
+
const files = paths.map((path) => {
|
|
72
|
+
const bytes = readFileSync(path);
|
|
73
|
+
checkSize(path, bytes.length, limits.maxBytes);
|
|
74
|
+
return new File([bytes], basename(path), { type: mediaTypeFor(path) });
|
|
75
|
+
});
|
|
61
76
|
form.set('attachmentCount', String(files.length));
|
|
62
77
|
files.forEach((file, index) => form.set(`attachment_${index}`, file));
|
|
63
78
|
}
|
|
79
|
+
/** File-backed blobs stream on send and reject if the file changes during upload. */
|
|
80
|
+
export async function appendStreamingAttachments(form, paths, limits, signal) {
|
|
81
|
+
attachmentBytes(paths, limits);
|
|
82
|
+
const files = [];
|
|
83
|
+
for (const path of paths) {
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
const file = await openAsBlob(path, { type: mediaTypeFor(path) });
|
|
86
|
+
checkSize(path, file.size, limits.maxBytes);
|
|
87
|
+
files.push(file);
|
|
88
|
+
}
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
form.set('attachmentCount', String(files.length));
|
|
91
|
+
files.forEach((file, index) => form.set(`attachment_${index}`, file, basename(paths[index])));
|
|
92
|
+
}
|
package/dist/runtime/client.js
CHANGED
|
@@ -77,10 +77,29 @@ function isRetryable(error, repeatable) {
|
|
|
77
77
|
return true;
|
|
78
78
|
return repeatable && AMBIGUOUS_CODES.includes(code);
|
|
79
79
|
}
|
|
80
|
+
// Retrying a consumed multipart Request needs a fresh stream. Keep the original
|
|
81
|
+
// FormData (whose large files are disk-backed), not a cloned/teed 200 MiB body.
|
|
82
|
+
const multipartReplays = new WeakMap();
|
|
83
|
+
class MultipartRequest extends Request {
|
|
84
|
+
constructor(input, init) {
|
|
85
|
+
super(input, init);
|
|
86
|
+
if (init?.body instanceof FormData) {
|
|
87
|
+
multipartReplays.set(this, () => {
|
|
88
|
+
const headers = new Headers(this.headers);
|
|
89
|
+
headers.delete('content-type'); // The fresh form serializer chooses its boundary.
|
|
90
|
+
return new Request(this.url, { ...init, headers, signal: this.signal });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
80
95
|
function withTransportRetry(fetchImpl) {
|
|
81
96
|
return async (...args) => {
|
|
97
|
+
const replay = args[0] instanceof Request ? multipartReplays.get(args[0]) : undefined;
|
|
82
98
|
let attempt = 0;
|
|
83
99
|
for (;;) {
|
|
100
|
+
signalOf(args)?.throwIfAborted();
|
|
101
|
+
if (attempt && replay)
|
|
102
|
+
args = [replay(), args[1]];
|
|
84
103
|
attempt++;
|
|
85
104
|
try {
|
|
86
105
|
return await fetchImpl(...args);
|
|
@@ -178,6 +197,7 @@ function withIntegrationCap(fetchImpl) {
|
|
|
178
197
|
export function createApiClient({ apiUrl, apiKey, fetch, answerTimeoutMs, }) {
|
|
179
198
|
const base = fetch ?? globalThis.fetch;
|
|
180
199
|
return createClient({
|
|
200
|
+
Request: MultipartRequest,
|
|
181
201
|
baseUrl: apiUrl,
|
|
182
202
|
headers: {
|
|
183
203
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
package/dist/runtime/invoke.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { defaultPathSerializer } from 'openapi-fetch';
|
|
3
|
+
import { appendAttachments, appendStreamingAttachments, attachmentBytes, INLINE_ATTACHMENT_BYTES, } from './attachments.js';
|
|
3
4
|
export class UsageError extends Error {
|
|
4
5
|
}
|
|
5
6
|
export function buildRequest(operation, input) {
|
|
@@ -16,12 +17,18 @@ export function buildRequest(operation, input) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
const bodyFields = Object.fromEntries(Object.entries(input.fields).filter(([name, value]) => !queryNames.has(name) && value !== undefined));
|
|
20
|
+
const deferredAttachments = operation.attachments &&
|
|
21
|
+
input.attachments.length &&
|
|
22
|
+
attachmentBytes(input.attachments, operation.attachments) > INLINE_ATTACHMENT_BYTES
|
|
23
|
+
? { paths: [...input.attachments], limits: operation.attachments }
|
|
24
|
+
: undefined;
|
|
19
25
|
return {
|
|
26
|
+
...(deferredAttachments ? { deferredAttachments } : {}),
|
|
20
27
|
method: operation.method,
|
|
21
28
|
path: operation.path,
|
|
22
29
|
pathParams: input.pathParams,
|
|
23
30
|
query,
|
|
24
|
-
body: buildBody(operation, bodyFields, input),
|
|
31
|
+
body: buildBody(operation, bodyFields, input, !!deferredAttachments),
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
@@ -30,7 +37,7 @@ export function buildRequest(operation, input) {
|
|
|
30
37
|
* JSON otherwise, including an empty `{}` for a body-taking operation given
|
|
31
38
|
* nothing, which is how the deploy route reads "start a deploy".
|
|
32
39
|
*/
|
|
33
|
-
function buildBody(operation, fields, input) {
|
|
40
|
+
function buildBody(operation, fields, input, deferAttachments = false) {
|
|
34
41
|
const takesJson = operation.body !== null;
|
|
35
42
|
const takesMultipart = operation.multipart !== null;
|
|
36
43
|
if (!takesJson && !takesMultipart) {
|
|
@@ -75,7 +82,7 @@ function buildBody(operation, fields, input) {
|
|
|
75
82
|
throw new UsageError(`--${field.flag} is required.`);
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
|
-
if (input.attachments.length && operation.attachments)
|
|
85
|
+
if (input.attachments.length && operation.attachments && !deferAttachments)
|
|
79
86
|
appendAttachments(form, input.attachments, operation.attachments);
|
|
80
87
|
return { kind: 'multipart', value: form };
|
|
81
88
|
}
|
|
@@ -139,9 +146,22 @@ export function readWholeBody(options, readStdin = () => readFileSync(0, 'utf8')
|
|
|
139
146
|
throw new UsageError('The body is not valid JSON.');
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
|
-
export async function execute(client, request) {
|
|
149
|
+
export async function execute(client, request, signal) {
|
|
150
|
+
signal?.throwIfAborted();
|
|
151
|
+
if (request.deferredAttachments && request.body?.kind === 'multipart') {
|
|
152
|
+
// Build a fresh form per execution. Retrying never reuses partially staged slots.
|
|
153
|
+
const form = new FormData();
|
|
154
|
+
for (const [key, value] of request.body.value)
|
|
155
|
+
form.append(key, value);
|
|
156
|
+
const failure = await prepareAttachments(client, request, form, signal);
|
|
157
|
+
if (failure)
|
|
158
|
+
return failure;
|
|
159
|
+
request = { ...request, body: { kind: 'multipart', value: form } };
|
|
160
|
+
}
|
|
161
|
+
signal?.throwIfAborted();
|
|
143
162
|
const init = {
|
|
144
163
|
params: { path: request.pathParams, query: request.query },
|
|
164
|
+
...(signal ? { signal } : {}),
|
|
145
165
|
};
|
|
146
166
|
if (request.body)
|
|
147
167
|
init.body = request.body.value;
|
|
@@ -150,3 +170,98 @@ export async function execute(client, request) {
|
|
|
150
170
|
const { data, error, response } = await client.request(request.method, request.path, init);
|
|
151
171
|
return { ok: response.ok, status: response.status, body: response.ok ? data : error };
|
|
152
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Staging uses the same client/auth/retry policy as the message. A first API
|
|
175
|
+
* catch-all 404 or 405 permits the legacy inline form. No fallback on an auth,
|
|
176
|
+
* validation or transport failure, or after any upload has succeeded.
|
|
177
|
+
* Unconsumed staging is reclaimed by the server's bounded expiry sweep; never
|
|
178
|
+
* delete a reference after a message failure, whose commit may have succeeded.
|
|
179
|
+
*/
|
|
180
|
+
async function prepareAttachments(client, request, form, signal) {
|
|
181
|
+
const { paths, limits } = request.deferredAttachments;
|
|
182
|
+
attachmentBytes(paths, limits);
|
|
183
|
+
const uploadPath = limits.uploadPath;
|
|
184
|
+
if (!uploadPath) {
|
|
185
|
+
await appendStreamingAttachments(form, paths, limits, signal);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const uploadParams = {};
|
|
189
|
+
for (const match of uploadPath.matchAll(/\{([^}]+)\}/g)) {
|
|
190
|
+
const name = match[1];
|
|
191
|
+
const value = request.pathParams[name] ?? form.get(name);
|
|
192
|
+
if (typeof value !== 'string' || !value) {
|
|
193
|
+
throw new UsageError(`Missing attachment upload parameter "${name}".`);
|
|
194
|
+
}
|
|
195
|
+
uploadParams[name] = value;
|
|
196
|
+
}
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
const uploadSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
199
|
+
const references = new Array(paths.length);
|
|
200
|
+
let failure;
|
|
201
|
+
let next = 0;
|
|
202
|
+
const upload = async (index) => {
|
|
203
|
+
uploadSignal.throwIfAborted();
|
|
204
|
+
const body = new FormData();
|
|
205
|
+
await appendStreamingAttachments(body, [paths[index]], limits, uploadSignal);
|
|
206
|
+
const outcome = await execute(client, {
|
|
207
|
+
method: 'post',
|
|
208
|
+
path: uploadPath,
|
|
209
|
+
pathParams: uploadParams,
|
|
210
|
+
query: {},
|
|
211
|
+
body: { kind: 'multipart', value: body },
|
|
212
|
+
}, uploadSignal);
|
|
213
|
+
uploadSignal.throwIfAborted();
|
|
214
|
+
if (outcome.ok) {
|
|
215
|
+
const reference = stagedReference(outcome.body);
|
|
216
|
+
if (!reference)
|
|
217
|
+
throw new Error('Invalid attachment upload response.');
|
|
218
|
+
references[index] = reference;
|
|
219
|
+
}
|
|
220
|
+
return outcome;
|
|
221
|
+
};
|
|
222
|
+
// Discover legacy support before starting the other requests.
|
|
223
|
+
const first = await upload(next++);
|
|
224
|
+
if (!first.ok) {
|
|
225
|
+
const error = asRecordOrEmpty(first.body);
|
|
226
|
+
const missingRoute = first.status === 404 &&
|
|
227
|
+
error.code === 'NOT_FOUND' &&
|
|
228
|
+
error.error === `No API route at ${defaultPathSerializer(uploadPath, uploadParams)}`;
|
|
229
|
+
if (missingRoute || first.status === 405) {
|
|
230
|
+
await appendStreamingAttachments(form, paths, limits, signal);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
return first;
|
|
234
|
+
}
|
|
235
|
+
const worker = async () => {
|
|
236
|
+
try {
|
|
237
|
+
while (next < paths.length) {
|
|
238
|
+
uploadSignal.throwIfAborted();
|
|
239
|
+
const outcome = await upload(next++);
|
|
240
|
+
if (!outcome.ok) {
|
|
241
|
+
failure ??= outcome;
|
|
242
|
+
controller.abort();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
controller.abort(error);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
// Drain all requests after abort: nothing continues in the background on return.
|
|
253
|
+
const workers = await Promise.allSettled(Array.from({ length: Math.min(3, paths.length - next) }, worker));
|
|
254
|
+
signal?.throwIfAborted();
|
|
255
|
+
if (failure)
|
|
256
|
+
return failure;
|
|
257
|
+
const rejected = workers.find((result) => result.status === 'rejected');
|
|
258
|
+
if (rejected?.status === 'rejected')
|
|
259
|
+
throw controller.signal.reason;
|
|
260
|
+
form.set('attachmentCount', String(references.length));
|
|
261
|
+
references.forEach((reference, index) => form.set(`attachment_${index}`, reference));
|
|
262
|
+
}
|
|
263
|
+
function stagedReference(body) {
|
|
264
|
+
const data = asRecordOrEmpty(body).data;
|
|
265
|
+
const reference = asRecordOrEmpty(data).reference;
|
|
266
|
+
return typeof reference === 'string' && reference.length > 0 ? reference : undefined;
|
|
267
|
+
}
|
package/openapi.json
CHANGED
|
@@ -1561,7 +1561,7 @@
|
|
|
1561
1561
|
"/api/devices/mac/browser-sign-in": {
|
|
1562
1562
|
"post": {
|
|
1563
1563
|
"operationId": "macBrowserLogin",
|
|
1564
|
-
"summary": "Get a site signed in in this Sensei Thread's browser from the member's everyday Chrome: pass the browser call's profile and deviceId, and the site as a bare hostname (twitter.com is x.com; Google's sites are refused). --reuse picks up the sign-in that profile in their Chrome holds by itself, with no tap: once per site and profile in a Turn, with the profile's exact ID. It never opens Chrome or waits for anyone, and answers opened false and signedIn false with chrome none (their Chrome holds no sign-in for the site, so nothing changed), same (the one this thread's browser already holds) or different (taken into this thread's browser, taken true). Then, unless it answered none or same, open the site with browser and check the account. On SIGN_IN_NEEDS_TAP or UPDATE_REQUIRED, put the sign-in card: `kosuke sensei cards set` with --sign-in naming this site, profile and deviceId. A --reuse spends no tap, so it gives no card back. Every tapped mode (`sign-in`, `take`, `waitNew`) needs the member's tap on the sign-in card in this thread; `--forget` and `--reuse` need none. A sign-in takes the sign-in that profile in their Chrome holds when it differs from this thread's, without opening anything, and answers taken true, opened false and signedIn false, since the site has not confirmed it; or else it opens --url, or the site's page, in their Chrome and waits up to 8 minutes for the member to sign in. On LinkedIn and X it answers signedIn true and opened true. On any other site a change of the site's cookies is taken and answered taken true and signedIn false, since a change is not a sign-in, and an end with no change answers taken false; a desktop app too old to watch the site opens it and answers at once, with no taken. Every answer with taken true carries waitNewLeft: while it is above 0 and the site is still signed out, --wait-new (with the same --url), in the same Turn and with no new tap, waits for a newer sign-in, never taking the one taken already, and opens the site only when this Turn has not; once the tap's time has run out it fails with SIGN_IN_WINDOW_ENDED, and no card comes back. After the member's I'm signed in tap, --take takes that site's cookies, never Google's, and answers taken. --forget removes a sign-in this Turn took, a --reuse's included, for an account other than the one expected, and needs no tap. Each tap allows one call: a call without one, or a --wait-new that no take armed, fails with SIGN_IN_NEEDS_TAP. Then open the site with browser and check the account before account-specific work. On DEVICE_OFFLINE, TIMEOUT, SIGN_IN_TIMEOUT and KEYCHAIN_UNAVAILABLE Kosuke gives the member's card back by itself. Fails with SIGN_IN_TIMEOUT when Kosuke saw no new sign-in to LinkedIn or X within 8 minutes, PROFILE_DISCONNECTED for a profile turned off in Settings → Devices, KEYCHAIN_UNAVAILABLE when macOS did not let Kosuke read Chrome's sign-ins, STALE_TURN when the Turn stopped, and UPDATE_REQUIRED on a desktop app too old for it. Only Sensei calls it.",
|
|
1564
|
+
"summary": "Get a site signed in in this Sensei Thread's browser from the member's everyday Chrome: pass the browser call's profile and deviceId, and the site as a bare hostname (twitter.com is x.com; Google's sites are refused). --reuse picks up the sign-in that profile in their Chrome holds by itself, with no tap: once per site and profile in a Turn, with the profile's exact ID. It never opens Chrome or waits for anyone, and answers opened false and signedIn false with chrome none (their Chrome holds no sign-in for the site, so nothing changed), same (the one this thread's browser already holds) or different (taken into this thread's browser, taken true). Then, unless it answered none or same, open the site with browser and check the account. On SIGN_IN_NEEDS_TAP or UPDATE_REQUIRED, put the sign-in card: `kosuke sensei cards set` with --sign-in naming this site, profile and deviceId. A --reuse spends no tap, so it gives no card back. Every tapped mode (`sign-in`, `take`, `waitNew`) needs the member's tap on the sign-in card in this thread; `--forget` and `--reuse` need none. A sign-in takes the sign-in that profile in their Chrome holds when it differs from this thread's, without opening anything, and answers taken true, opened false and signedIn false, since the site has not confirmed it; or else it opens --url, or the site's page, in their Chrome and waits up to 8 minutes for the member to sign in. On LinkedIn and X it answers signedIn true and opened true. On any other site a change of the site's cookies is taken and answered taken true and signedIn false, since a change is not a sign-in, and an end with no change answers taken false; a desktop app too old to watch the site opens it and answers at once, with no taken. Every answer with taken true carries waitNewLeft: while it is above 0 and the site is still signed out, --wait-new (with the same --url), in the same Turn and with no new tap, waits for a newer sign-in, never taking the one taken already, and opens the site only when this Turn has not; once the tap's time has run out it fails with SIGN_IN_WINDOW_ENDED, and no card comes back. After the member's I'm signed in tap, --take takes that site's cookies, never Google's, and answers taken. --forget removes a sign-in this Turn took, a --reuse's included, for an account other than the one expected, and needs no tap. Each tap allows one call: a call without one, or a --wait-new that no take armed, fails with SIGN_IN_NEEDS_TAP. Then open the site with browser and check the account before account-specific work. On DEVICE_OFFLINE, DEVICE_CALL_INTERRUPTED, TIMEOUT, SIGN_IN_TIMEOUT and KEYCHAIN_UNAVAILABLE Kosuke gives the member's card back by itself. Fails with SIGN_IN_TIMEOUT when Kosuke saw no new sign-in to LinkedIn or X within 8 minutes, PROFILE_DISCONNECTED for a profile turned off in Settings → Devices, KEYCHAIN_UNAVAILABLE when macOS did not let Kosuke read Chrome's sign-ins, STALE_TURN when the Turn stopped, and UPDATE_REQUIRED on a desktop app too old for it. Only Sensei calls it.",
|
|
1565
1565
|
"tags": ["mac"],
|
|
1566
1566
|
"x-kosuke-command": ["mac", "browser", "login"],
|
|
1567
1567
|
"x-kosuke-answer-timeout-ms": 590000,
|
|
@@ -8360,6 +8360,7 @@
|
|
|
8360
8360
|
"x-kosuke-attachments": true,
|
|
8361
8361
|
"x-kosuke-attachment-max-files": 20,
|
|
8362
8362
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
8363
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{hintProjectId}/attachments",
|
|
8363
8364
|
"x-kosuke-sandbox-token": true,
|
|
8364
8365
|
"x-kosuke-cron-token": true,
|
|
8365
8366
|
"x-kosuke-sensei-token": true,
|
|
@@ -10252,6 +10253,75 @@
|
|
|
10252
10253
|
}
|
|
10253
10254
|
}
|
|
10254
10255
|
},
|
|
10256
|
+
"/api/projects/{projectId}/attachments": {
|
|
10257
|
+
"post": {
|
|
10258
|
+
"operationId": "attachmentsCreate",
|
|
10259
|
+
"summary": "Stage one private message attachment. Returns data.reference for an attachment slot in a subsequent message to this project, using the same credential, within one hour.",
|
|
10260
|
+
"tags": ["attachments"],
|
|
10261
|
+
"x-kosuke-command": ["attachments", "create"],
|
|
10262
|
+
"x-kosuke-attachments": true,
|
|
10263
|
+
"x-kosuke-attachment-max-files": 1,
|
|
10264
|
+
"x-kosuke-attachment-max-bytes": 10485760,
|
|
10265
|
+
"x-kosuke-sandbox-token": true,
|
|
10266
|
+
"x-kosuke-cron-token": true,
|
|
10267
|
+
"x-kosuke-sensei-token": true,
|
|
10268
|
+
"parameters": [
|
|
10269
|
+
{
|
|
10270
|
+
"name": "projectId",
|
|
10271
|
+
"in": "path",
|
|
10272
|
+
"required": true,
|
|
10273
|
+
"schema": {
|
|
10274
|
+
"type": "string",
|
|
10275
|
+
"format": "uuid",
|
|
10276
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
],
|
|
10280
|
+
"requestBody": {
|
|
10281
|
+
"required": true,
|
|
10282
|
+
"content": {
|
|
10283
|
+
"multipart/form-data": {
|
|
10284
|
+
"schema": {
|
|
10285
|
+
"type": "object",
|
|
10286
|
+
"properties": {}
|
|
10287
|
+
}
|
|
10288
|
+
}
|
|
10289
|
+
}
|
|
10290
|
+
},
|
|
10291
|
+
"responses": {
|
|
10292
|
+
"2XX": {
|
|
10293
|
+
"description": "Success envelope",
|
|
10294
|
+
"content": {
|
|
10295
|
+
"application/json": {
|
|
10296
|
+
"schema": {
|
|
10297
|
+
"$ref": "#/components/schemas/ApiSuccess"
|
|
10298
|
+
}
|
|
10299
|
+
}
|
|
10300
|
+
}
|
|
10301
|
+
},
|
|
10302
|
+
"4XX": {
|
|
10303
|
+
"description": "Error envelope",
|
|
10304
|
+
"content": {
|
|
10305
|
+
"application/json": {
|
|
10306
|
+
"schema": {
|
|
10307
|
+
"$ref": "#/components/schemas/ApiError"
|
|
10308
|
+
}
|
|
10309
|
+
}
|
|
10310
|
+
}
|
|
10311
|
+
},
|
|
10312
|
+
"5XX": {
|
|
10313
|
+
"description": "Error envelope",
|
|
10314
|
+
"content": {
|
|
10315
|
+
"application/json": {
|
|
10316
|
+
"schema": {
|
|
10317
|
+
"$ref": "#/components/schemas/ApiError"
|
|
10318
|
+
}
|
|
10319
|
+
}
|
|
10320
|
+
}
|
|
10321
|
+
}
|
|
10322
|
+
}
|
|
10323
|
+
}
|
|
10324
|
+
},
|
|
10255
10325
|
"/api/projects/{projectId}/backlog-items": {
|
|
10256
10326
|
"get": {
|
|
10257
10327
|
"operationId": "projectsBacklogItemsList",
|
|
@@ -11316,6 +11386,7 @@
|
|
|
11316
11386
|
"x-kosuke-attachments": true,
|
|
11317
11387
|
"x-kosuke-attachment-max-files": 20,
|
|
11318
11388
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
11389
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{projectId}/attachments",
|
|
11319
11390
|
"x-kosuke-sandbox-token": true,
|
|
11320
11391
|
"x-kosuke-cron-token": true,
|
|
11321
11392
|
"x-kosuke-sensei-token": true,
|
|
@@ -12641,6 +12712,7 @@
|
|
|
12641
12712
|
"x-kosuke-attachments": true,
|
|
12642
12713
|
"x-kosuke-attachment-max-files": 20,
|
|
12643
12714
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
12715
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{projectId}/attachments",
|
|
12644
12716
|
"x-kosuke-sandbox-token": true,
|
|
12645
12717
|
"x-kosuke-cron-token": true,
|
|
12646
12718
|
"x-kosuke-sensei-token": true,
|
|
@@ -14432,6 +14504,7 @@
|
|
|
14432
14504
|
"x-kosuke-attachments": true,
|
|
14433
14505
|
"x-kosuke-attachment-max-files": 20,
|
|
14434
14506
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
14507
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{projectId}/attachments",
|
|
14435
14508
|
"x-kosuke-sandbox-token": true,
|
|
14436
14509
|
"x-kosuke-cron-token": true,
|
|
14437
14510
|
"x-kosuke-sensei-token": true,
|
|
@@ -15648,6 +15721,7 @@
|
|
|
15648
15721
|
"x-kosuke-attachments": true,
|
|
15649
15722
|
"x-kosuke-attachment-max-files": 20,
|
|
15650
15723
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
15724
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{projectId}/attachments",
|
|
15651
15725
|
"x-kosuke-sandbox-token": true,
|
|
15652
15726
|
"x-kosuke-cron-token": true,
|
|
15653
15727
|
"x-kosuke-sensei-token": true,
|
|
@@ -18454,6 +18528,7 @@
|
|
|
18454
18528
|
"x-kosuke-attachments": true,
|
|
18455
18529
|
"x-kosuke-attachment-max-files": 20,
|
|
18456
18530
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
18531
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{projectId}/attachments",
|
|
18457
18532
|
"x-kosuke-sandbox-token": true,
|
|
18458
18533
|
"x-kosuke-cron-token": true,
|
|
18459
18534
|
"x-kosuke-sensei-token": true,
|
|
@@ -18741,6 +18816,7 @@
|
|
|
18741
18816
|
"x-kosuke-attachments": true,
|
|
18742
18817
|
"x-kosuke-attachment-max-files": 20,
|
|
18743
18818
|
"x-kosuke-attachment-max-bytes": 10485760,
|
|
18819
|
+
"x-kosuke-attachment-upload-path": "/api/projects/{projectId}/attachments",
|
|
18744
18820
|
"x-kosuke-sandbox-token": true,
|
|
18745
18821
|
"x-kosuke-cron-token": true,
|
|
18746
18822
|
"x-kosuke-sensei-token": true,
|
|
@@ -27153,7 +27229,7 @@
|
|
|
27153
27229
|
},
|
|
27154
27230
|
"post": {
|
|
27155
27231
|
"operationId": "integrationsBrowserConnectionsCreatePersonalNetworkLinkedin",
|
|
27156
|
-
"summary": "Connect LinkedIn in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
27232
|
+
"summary": "Connect LinkedIn in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, DEVICE_CALL_INTERRUPTED (the connection to the Mac dropped; try again), PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
27157
27233
|
"tags": ["integrations"],
|
|
27158
27234
|
"x-kosuke-command": ["integrations", "browser-connections", "create"],
|
|
27159
27235
|
"x-kosuke-variant": {
|
|
@@ -27338,7 +27414,7 @@
|
|
|
27338
27414
|
},
|
|
27339
27415
|
"post": {
|
|
27340
27416
|
"operationId": "integrationsBrowserConnectionsCreatePersonalNetworkX",
|
|
27341
|
-
"summary": "Connect X in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
27417
|
+
"summary": "Connect X in one allowed Chrome profile of one of your online Macs: creates the connection waiting for Sensei and sends Sensei the request as you, in a new Sensei thread, whose ID it answers. 404 MAC_NOT_FOUND or PROFILE_NOT_FOUND, 409 MAC_OFFLINE, DEVICE_CALL_INTERRUPTED (the connection to the Mac dropped; try again), PROFILE_OFF, or ALREADY_CONNECTED with details.connectionId.",
|
|
27342
27418
|
"tags": ["integrations"],
|
|
27343
27419
|
"x-kosuke-command": ["integrations", "browser-connections", "create"],
|
|
27344
27420
|
"x-kosuke-variant": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kosuke-ai/kosuke",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.52",
|
|
4
4
|
"description": "Kosuke platform CLI. The client customers install to drive app.kosuke.ai from their own machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,11 +40,10 @@
|
|
|
40
40
|
"vitest": "^4.1.12"
|
|
41
41
|
},
|
|
42
42
|
"kosukeRelease": {
|
|
43
|
-
"sourceCommit": "
|
|
44
|
-
"sourceIdentity": "
|
|
43
|
+
"sourceCommit": "3e2d836467c2180a7b9431f6bb3ec79b2cc0ae92",
|
|
44
|
+
"sourceIdentity": "5b0f74d867dc999558409d71533fae41b6be0d6c45ae4814eed97fd10d066704",
|
|
45
45
|
"fragments": [
|
|
46
|
-
"
|
|
47
|
-
"device-approval-per-turn-23f44b.json"
|
|
46
|
+
"device-call-interrupted-u4gfnl.json"
|
|
48
47
|
]
|
|
49
48
|
}
|
|
50
49
|
}
|