@kosuke-ai/kosuke 1.0.13 → 1.0.15

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.
@@ -0,0 +1,64 @@
1
+ export const operation = {
2
+ "method": "post",
3
+ "path": "/api/agent-integrations/file-uploads",
4
+ "command": [
5
+ "integrations",
6
+ "uploads",
7
+ "create"
8
+ ],
9
+ "summary": "Reserve the upload of one file for an integration tool that takes files, on the connection a call would run on. The answer is a file reference and a URL: PUT the file to the URL with the headers given, then pass the reference as the file argument of that tool on that connection. `kosuke integrations call --file` does all of it, and is the command to use. A failure answers 200 with isError true, as a call does. A Connector Token or a browser session is refused.",
10
+ "pathParams": [],
11
+ "query": [
12
+ {
13
+ "name": "workspace",
14
+ "flag": "workspace",
15
+ "kind": "string",
16
+ "required": false,
17
+ "description": "The workspace the call is made in. Required with an API key, which is refused with 403 when you are not a member of it. An agent credential reaches its own workspace and refuses another."
18
+ }
19
+ ],
20
+ "body": {
21
+ "fields": [
22
+ {
23
+ "name": "name",
24
+ "flag": "name",
25
+ "kind": "string",
26
+ "required": true,
27
+ "description": "The tool the file is for, as the tools list names it."
28
+ },
29
+ {
30
+ "name": "account",
31
+ "flag": "account",
32
+ "kind": "string",
33
+ "required": false,
34
+ "description": "The connection the call will run on: its short id, its alias or scope:label. Needed when several can run the tool."
35
+ },
36
+ {
37
+ "name": "filename",
38
+ "flag": "filename",
39
+ "kind": "string",
40
+ "required": true,
41
+ "description": "The file name the vendor receives, without a directory."
42
+ },
43
+ {
44
+ "name": "mimetype",
45
+ "flag": "mimetype",
46
+ "kind": "string",
47
+ "required": true,
48
+ "description": "The MIME type of the file, such as video/mp4."
49
+ },
50
+ {
51
+ "name": "md5",
52
+ "flag": "md5",
53
+ "kind": "string",
54
+ "required": true,
55
+ "description": "The MD5 digest of the file, in lowercase hex."
56
+ }
57
+ ],
58
+ "required": true
59
+ },
60
+ "multipart": null,
61
+ "attachments": false,
62
+ "anonymous": false,
63
+ "agents": "all"
64
+ };
@@ -6,7 +6,7 @@ export const operation = {
6
6
  "device-requests",
7
7
  "list"
8
8
  ],
9
- "summary": "List the Device Requests of a Chat Session, oldest first, as you would see them on its cards: id, device, kind, owner, reason, status (pending, approved, declined, expired, withdrawn or closed), who asked and who answered, when, and canApprove, whether you may answer it. A request past its 30 minutes reads expired.",
9
+ "summary": "List the Device Requests of a Chat Session, oldest first, as you would see them on its cards: id, device, kind, owner, reason, status (pending, approved, declined, expired, withdrawn or closed), who asked and who answered, when, canApprove, whether you may answer it, and occupiedBy, session or sensei when someone else uses the device while it is pending. A request past its 30 minutes reads expired.",
10
10
  "pathParams": [],
11
11
  "query": [
12
12
  {
@@ -78,6 +78,13 @@ export const operation = {
78
78
  "kind": "string",
79
79
  "required": false
80
80
  },
81
+ {
82
+ "name": "branch",
83
+ "flag": "branch",
84
+ "kind": "string",
85
+ "required": false,
86
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch."
87
+ },
81
88
  {
82
89
  "name": "reason",
83
90
  "flag": "reason",
@@ -186,6 +193,13 @@ export const operation = {
186
193
  "kind": "string",
187
194
  "required": false
188
195
  },
196
+ {
197
+ "name": "branch",
198
+ "flag": "branch",
199
+ "kind": "string",
200
+ "required": false,
201
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch."
202
+ },
189
203
  {
190
204
  "name": "reason",
191
205
  "flag": "reason",
@@ -64,6 +64,13 @@ export const operation = {
64
64
  "kind": "string",
65
65
  "required": false
66
66
  },
67
+ {
68
+ "name": "branch",
69
+ "flag": "branch",
70
+ "kind": "string",
71
+ "required": false,
72
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch."
73
+ },
67
74
  {
68
75
  "name": "reason",
69
76
  "flag": "reason",
@@ -194,6 +201,13 @@ export const operation = {
194
201
  "kind": "string",
195
202
  "required": false
196
203
  },
204
+ {
205
+ "name": "branch",
206
+ "flag": "branch",
207
+ "kind": "string",
208
+ "required": false,
209
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch."
210
+ },
197
211
  {
198
212
  "name": "reason",
199
213
  "flag": "reason",
@@ -0,0 +1,153 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { stat } from 'node:fs/promises';
4
+ import { basename, extname } from 'node:path';
5
+ import { Readable } from 'node:stream';
6
+ /**
7
+ * `kosuke integrations call --file <argument>=<path>`: a local file for a tool's file argument.
8
+ * The platform reserves the upload and answers a file reference; the file goes straight to the
9
+ * storage URL it names, never through the platform, so a large video costs the platform nothing.
10
+ */
11
+ export const FILE_UPLOAD_PATH = '/api/agent-integrations/file-uploads';
12
+ /** The storage takes one PUT of at most 5 GiB. */
13
+ export const MAX_FILE_BYTES = 5 * 1024 ** 3;
14
+ const NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
15
+ const INDEX = /^\d{1,4}$/;
16
+ const UNSAFE = new Set(['__proto__', 'constructor', 'prototype']);
17
+ /** `videoFilePath=./clip.mp4`, or `attachment.0=./a.pdf` for a file inside a list or object. */
18
+ export function parseFileArgument(raw) {
19
+ const split = raw.indexOf('=');
20
+ if (split <= 0 || split === raw.length - 1)
21
+ return null;
22
+ const at = raw.slice(0, split).split('.');
23
+ const path = raw.slice(split + 1);
24
+ if (!NAME.test(at[0]))
25
+ return null;
26
+ if (at.some((segment) => UNSAFE.has(segment) || !(NAME.test(segment) || INDEX.test(segment)))) {
27
+ return null;
28
+ }
29
+ return { at, path };
30
+ }
31
+ /** Puts `value` at `at`, making the lists and objects on the way; false when one is in the way. */
32
+ export function setAt(args, at, value) {
33
+ let parent = args;
34
+ for (let index = 0; index < at.length; index += 1) {
35
+ const segment = at[index];
36
+ const last = index === at.length - 1;
37
+ const nextIsIndex = !last && INDEX.test(at[index + 1]);
38
+ if (Array.isArray(parent)) {
39
+ if (!INDEX.test(segment))
40
+ return false;
41
+ const position = Number(segment);
42
+ if (position > parent.length)
43
+ return false;
44
+ if (last) {
45
+ parent[position] = value;
46
+ return true;
47
+ }
48
+ parent[position] ??= nextIsIndex ? [] : {};
49
+ const child = parent[position];
50
+ if (typeof child !== 'object' || child === null)
51
+ return false;
52
+ parent = child;
53
+ continue;
54
+ }
55
+ if (INDEX.test(segment))
56
+ return false;
57
+ if (last) {
58
+ parent[segment] = value;
59
+ return true;
60
+ }
61
+ if (!Object.hasOwn(parent, segment))
62
+ parent[segment] = nextIsIndex ? [] : {};
63
+ const child = parent[segment];
64
+ if (typeof child !== 'object' || child === null)
65
+ return false;
66
+ if (nextIsIndex !== Array.isArray(child))
67
+ return false;
68
+ parent = child;
69
+ }
70
+ return false;
71
+ }
72
+ const MIME_TYPES = {
73
+ '.3gp': 'video/3gpp',
74
+ '.avi': 'video/x-msvideo',
75
+ '.flv': 'video/x-flv',
76
+ '.m4v': 'video/x-m4v',
77
+ '.mkv': 'video/x-matroska',
78
+ '.mov': 'video/quicktime',
79
+ '.mp4': 'video/mp4',
80
+ '.mpeg': 'video/mpeg',
81
+ '.mpg': 'video/mpeg',
82
+ '.webm': 'video/webm',
83
+ '.wmv': 'video/x-ms-wmv',
84
+ '.mp3': 'audio/mpeg',
85
+ '.m4a': 'audio/mp4',
86
+ '.wav': 'audio/wav',
87
+ '.ogg': 'audio/ogg',
88
+ '.srt': 'application/x-subrip',
89
+ '.vtt': 'text/vtt',
90
+ '.sbv': 'text/plain',
91
+ '.ttml': 'application/ttml+xml',
92
+ '.dfxp': 'application/ttml+xml',
93
+ '.scc': 'text/plain',
94
+ '.gif': 'image/gif',
95
+ '.jpeg': 'image/jpeg',
96
+ '.jpg': 'image/jpeg',
97
+ '.png': 'image/png',
98
+ '.svg': 'image/svg+xml',
99
+ '.webp': 'image/webp',
100
+ '.csv': 'text/csv',
101
+ '.html': 'text/html',
102
+ '.json': 'application/json',
103
+ '.md': 'text/markdown',
104
+ '.txt': 'text/plain',
105
+ '.xml': 'application/xml',
106
+ '.pdf': 'application/pdf',
107
+ '.zip': 'application/zip',
108
+ '.doc': 'application/msword',
109
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
110
+ '.xls': 'application/vnd.ms-excel',
111
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
112
+ '.ppt': 'application/vnd.ms-powerpoint',
113
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
114
+ };
115
+ export function mimetypeOf(name) {
116
+ return MIME_TYPES[extname(name).toLowerCase()] ?? 'application/octet-stream';
117
+ }
118
+ /** The name the vendor receives: the base name, when the platform can take it. */
119
+ export function uploadName(path) {
120
+ const name = basename(path);
121
+ return name.length > 0 && name.length <= 255 && !/[/\\\p{Cc}]/u.test(name) ? name : null;
122
+ }
123
+ export async function fileSize(path) {
124
+ try {
125
+ const info = await stat(path);
126
+ return info.isFile() ? info.size : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ export async function md5Of(path) {
133
+ const hash = createHash('md5');
134
+ for await (const chunk of createReadStream(path))
135
+ hash.update(chunk);
136
+ return hash.digest('hex');
137
+ }
138
+ /**
139
+ * One PUT of the whole file. The length is stated because the storage refuses a chunked upload,
140
+ * and the file is streamed so a large video is never held in memory.
141
+ */
142
+ export async function putFile(fetch, upload, path, size, mimetype, signal) {
143
+ const response = await fetch(upload.url, {
144
+ method: 'PUT',
145
+ headers: { ...upload.headers, 'content-type': mimetype, 'content-length': String(size) },
146
+ body: Readable.toWeb(createReadStream(path)),
147
+ duplex: 'half',
148
+ redirect: 'error',
149
+ signal,
150
+ });
151
+ await response.body?.cancel().catch(() => undefined);
152
+ return response.status;
153
+ }
package/openapi.json CHANGED
@@ -232,6 +232,107 @@
232
232
  }
233
233
  }
234
234
  },
235
+ "/api/agent-integrations/file-uploads": {
236
+ "post": {
237
+ "operationId": "integrationsUploadsCreate",
238
+ "summary": "Reserve the upload of one file for an integration tool that takes files, on the connection a call would run on. The answer is a file reference and a URL: PUT the file to the URL with the headers given, then pass the reference as the file argument of that tool on that connection. `kosuke integrations call --file` does all of it, and is the command to use. A failure answers 200 with isError true, as a call does. A Connector Token or a browser session is refused.",
239
+ "tags": ["integrations"],
240
+ "x-kosuke-command": ["integrations", "uploads", "create"],
241
+ "x-kosuke-sandbox-token": true,
242
+ "x-kosuke-cron-token": true,
243
+ "x-kosuke-sensei-token": true,
244
+ "parameters": [
245
+ {
246
+ "name": "workspace",
247
+ "in": "query",
248
+ "required": false,
249
+ "description": "The workspace the call is made in. Required with an API key, which is refused with 403 when you are not a member of it. An agent credential reaches its own workspace and refuses another.",
250
+ "schema": {
251
+ "description": "The workspace the call is made in. Required with an API key, which is refused with 403 when you are not a member of it. An agent credential reaches its own workspace and refuses another.",
252
+ "type": "string",
253
+ "minLength": 1,
254
+ "maxLength": 100
255
+ }
256
+ }
257
+ ],
258
+ "requestBody": {
259
+ "required": true,
260
+ "content": {
261
+ "application/json": {
262
+ "schema": {
263
+ "type": "object",
264
+ "properties": {
265
+ "name": {
266
+ "type": "string",
267
+ "minLength": 1,
268
+ "maxLength": 80,
269
+ "description": "The tool the file is for, as the tools list names it."
270
+ },
271
+ "account": {
272
+ "description": "The connection the call will run on: its short id, its alias or scope:label. Needed when several can run the tool.",
273
+ "type": "string",
274
+ "minLength": 1,
275
+ "maxLength": 100
276
+ },
277
+ "filename": {
278
+ "type": "string",
279
+ "minLength": 1,
280
+ "maxLength": 255,
281
+ "pattern": "^[^/\\\\\\p{Cc}]+$",
282
+ "description": "The file name the vendor receives, without a directory."
283
+ },
284
+ "mimetype": {
285
+ "type": "string",
286
+ "maxLength": 255,
287
+ "pattern": "^[a-z0-9][a-z0-9!#$&^_.+-]*\\/[a-z0-9][a-z0-9!#$&^_.+-]*$",
288
+ "description": "The MIME type of the file, such as video/mp4."
289
+ },
290
+ "md5": {
291
+ "type": "string",
292
+ "pattern": "^[a-f0-9]{32}$",
293
+ "description": "The MD5 digest of the file, in lowercase hex."
294
+ }
295
+ },
296
+ "required": ["name", "filename", "mimetype", "md5"],
297
+ "additionalProperties": false
298
+ }
299
+ }
300
+ }
301
+ },
302
+ "responses": {
303
+ "2XX": {
304
+ "description": "Success envelope",
305
+ "content": {
306
+ "application/json": {
307
+ "schema": {
308
+ "$ref": "#/components/schemas/ApiSuccess"
309
+ }
310
+ }
311
+ }
312
+ },
313
+ "4XX": {
314
+ "description": "Error envelope",
315
+ "content": {
316
+ "application/json": {
317
+ "schema": {
318
+ "$ref": "#/components/schemas/ApiError"
319
+ }
320
+ }
321
+ }
322
+ },
323
+ "5XX": {
324
+ "description": "Error envelope",
325
+ "content": {
326
+ "application/json": {
327
+ "schema": {
328
+ "$ref": "#/components/schemas/ApiError"
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ },
235
336
  "/api/agent-integrations/tools": {
236
337
  "get": {
237
338
  "operationId": "integrationsToolsList",
@@ -799,7 +900,7 @@
799
900
  "/api/devices/available": {
800
901
  "get": {
801
902
  "operationId": "devicesList",
802
- "summary": "From a Chat Session: the devices it may ask for, the workspace's devices and the session member's own Macs, each with its id, kind, whether it is online, the tools it offers and this session's access: none, pending or approved. Pick an online device with the tools the test needs: a workspace device first, and the member's own Mac only when no workspace device fits, whatever the names or past approvals. Request one with `kosuke devices requests create` before any mac command, and use one only when the test truly needs a real Mac.",
903
+ "summary": "From a Chat Session: the devices it may ask for, the workspace's devices and the session member's own Macs, each with its id, kind, whether it is online, the tools it offers, this session's access: none, pending or approved, and occupied with occupiedBy (session or sensei) when someone else uses it now. Pick an online device with the tools the test needs: a workspace device first, and the member's own Mac only when no workspace device fits, whatever the names or past approvals. Of those, pick a free one, and ask for an occupied one only when no free device fits. Request one with `kosuke devices requests create` before any mac command, and use one only when the test truly needs a real Mac.",
803
904
  "tags": ["devices"],
804
905
  "x-kosuke-command": ["devices", "list"],
805
906
  "x-kosuke-sandbox-token": true,
@@ -8041,6 +8142,12 @@
8041
8142
  "minLength": 1,
8042
8143
  "maxLength": 255
8043
8144
  },
8145
+ "branch": {
8146
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch.",
8147
+ "type": "string",
8148
+ "minLength": 1,
8149
+ "maxLength": 255
8150
+ },
8044
8151
  "reason": {
8045
8152
  "description": "Why this session is being filed and what turned it up, in one line. Required from a sandbox or a Cron Run, ignored from anyone else.",
8046
8153
  "type": "string",
@@ -8123,6 +8230,12 @@
8123
8230
  "minLength": 1,
8124
8231
  "maxLength": 255
8125
8232
  },
8233
+ "branch": {
8234
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch.",
8235
+ "type": "string",
8236
+ "minLength": 1,
8237
+ "maxLength": 255
8238
+ },
8126
8239
  "reason": {
8127
8240
  "description": "Why this session is being filed and what turned it up, in one line. Required from a sandbox or a Cron Run, ignored from anyone else.",
8128
8241
  "type": "string",
@@ -10835,6 +10948,12 @@
10835
10948
  "minLength": 1,
10836
10949
  "maxLength": 255
10837
10950
  },
10951
+ "branch": {
10952
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch.",
10953
+ "type": "string",
10954
+ "minLength": 1,
10955
+ "maxLength": 255
10956
+ },
10838
10957
  "reason": {
10839
10958
  "description": "Why this session is being filed and what turned it up, in one line. Required from a sandbox or a Cron Run, ignored from anyone else.",
10840
10959
  "type": "string",
@@ -10900,6 +11019,12 @@
10900
11019
  "minLength": 1,
10901
11020
  "maxLength": 255
10902
11021
  },
11022
+ "branch": {
11023
+ "description": "An existing branch the session works on: it checks it out, pushes to it and opens its pull request from it. Not the default branch, the Target Branch, or a branch another open session works on. Absent: a new branch. Repository projects only; not with startBranch.",
11024
+ "type": "string",
11025
+ "minLength": 1,
11026
+ "maxLength": 255
11027
+ },
10903
11028
  "reason": {
10904
11029
  "description": "Why this session is being filed and what turned it up, in one line. Required from a sandbox or a Cron Run, ignored from anyone else.",
10905
11030
  "type": "string",
@@ -22053,7 +22178,7 @@
22053
22178
  "/api/sensei/device-requests": {
22054
22179
  "get": {
22055
22180
  "operationId": "senseiDeviceRequestsList",
22056
- "summary": "List the Device Requests of a Chat Session, oldest first, as you would see them on its cards: id, device, kind, owner, reason, status (pending, approved, declined, expired, withdrawn or closed), who asked and who answered, when, and canApprove, whether you may answer it. A request past its 30 minutes reads expired.",
22181
+ "summary": "List the Device Requests of a Chat Session, oldest first, as you would see them on its cards: id, device, kind, owner, reason, status (pending, approved, declined, expired, withdrawn or closed), who asked and who answered, when, canApprove, whether you may answer it, and occupiedBy, session or sensei when someone else uses the device while it is pending. A request past its 30 minutes reads expired.",
22057
22182
  "tags": ["sensei"],
22058
22183
  "x-kosuke-command": ["sensei", "device-requests", "list"],
22059
22184
  "x-kosuke-sandbox-token": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kosuke-ai/kosuke",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
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,10 +40,10 @@
40
40
  "vitest": "^4.1.12"
41
41
  },
42
42
  "kosukeRelease": {
43
- "sourceCommit": "6a882d48dc8afd6970071bffe05194bda2efb758",
44
- "sourceIdentity": "e21e09139e14ef237ab372758a821554ef499449d0068c261567d4d898925b0c",
43
+ "sourceCommit": "e8cfc6a10560808a7caeb3459689a40d350802ad",
44
+ "sourceIdentity": "53826d80538eb10915ea379597ea571039d4144d2b6d95c47bd5b01ef75588f2",
45
45
  "fragments": [
46
- "sensei-any-turn-acts-q7m2tz.json"
46
+ "cli-integrations-call-file-k7w2qd.json"
47
47
  ]
48
48
  }
49
49
  }