@kosuke-ai/kosuke 1.0.49 → 1.0.51

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.
@@ -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, 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.",
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, and so does a request or approval once the Turn that asked for it ended.",
10
10
  "pathParams": [],
11
11
  "query": [
12
12
  {
@@ -30,7 +30,7 @@ export const operation = {
30
30
  "flag": "decision",
31
31
  "kind": "string",
32
32
  "required": true,
33
- "description": "approve lets the session use the device until it ends; decline refuses it.",
33
+ "description": "approve lets the session use the device until the Turn that asked ends; decline refuses it.",
34
34
  "choices": [
35
35
  "approve",
36
36
  "decline"
@@ -241,7 +241,8 @@ export const operation = {
241
241
  ],
242
242
  "attachments": {
243
243
  "maxFiles": 20,
244
- "maxBytes": 10485760
244
+ "maxBytes": 10485760,
245
+ "uploadPath": "/api/projects/{projectId}/attachments"
245
246
  },
246
247
  "anonymous": false,
247
248
  "agents": "all"
@@ -75,7 +75,8 @@ export const operation = {
75
75
  ],
76
76
  "attachments": {
77
77
  "maxFiles": 20,
78
- "maxBytes": 10485760
78
+ "maxBytes": 10485760,
79
+ "uploadPath": "/api/projects/{projectId}/attachments"
79
80
  },
80
81
  "anonymous": false,
81
82
  "agents": "all"
@@ -50,7 +50,8 @@ export const operation = {
50
50
  ],
51
51
  "attachments": {
52
52
  "maxFiles": 20,
53
- "maxBytes": 10485760
53
+ "maxBytes": 10485760,
54
+ "uploadPath": "/api/projects/{projectId}/attachments"
54
55
  },
55
56
  "anonymous": false,
56
57
  "agents": "all"
@@ -131,7 +131,8 @@ export const operation = {
131
131
  ],
132
132
  "attachments": {
133
133
  "maxFiles": 20,
134
- "maxBytes": 10485760
134
+ "maxBytes": 10485760,
135
+ "uploadPath": "/api/projects/{projectId}/attachments"
135
136
  },
136
137
  "anonymous": false,
137
138
  "agents": "all"
@@ -285,7 +285,8 @@ export const operation = {
285
285
  ],
286
286
  "attachments": {
287
287
  "maxFiles": 20,
288
- "maxBytes": 10485760
288
+ "maxBytes": 10485760,
289
+ "uploadPath": "/api/projects/{hintProjectId}/attachments"
289
290
  },
290
291
  "anonymous": false,
291
292
  "agents": "all"
package/dist/program.js CHANGED
@@ -14,6 +14,7 @@ import { version } from './runtime/version.js';
14
14
  * The command tree: the commands `commands/hand-written.ts` lists by hand, everything else
15
15
  * from the generated descriptors, one leaf per command over its forms. Global flags are read
16
16
  * before any subcommand runs, so the runtime the subcommands share is built once, from them.
17
+ * @internal
17
18
  */
18
19
  export function createProgram(options = {}) {
19
20
  const program = new Command('kosuke')
@@ -5,6 +5,7 @@
5
5
  * decides which commands the tree shows.
6
6
  *
7
7
  * Imports nothing, so the generator can load it on plain node.
8
+ * @internal
8
9
  */
9
10
  export const AGENT_TOKENS = [
10
11
  // Wire contract with `src/lib/auth/sandbox-agent-token.ts`: a chat sandbox's own Turn.
@@ -21,7 +22,10 @@ export function agentTokenOf(key) {
21
22
  const found = AGENT_TOKENS.find(({ prefixes }) => prefixes.some((prefix) => key.startsWith(prefix)));
22
23
  return found?.token ?? null;
23
24
  }
24
- /** The descriptor value for the agent tokens the document grants an operation. */
25
+ /**
26
+ * The descriptor value for the agent tokens the document grants an operation.
27
+ * @internal
28
+ */
25
29
  export function agentReach(granted) {
26
30
  const tokens = AGENT_TOKENS.map(({ token }) => token).filter((token) => granted.includes(token));
27
31
  if (tokens.length === 0)
@@ -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 outcome = await execute(client, request);
188
- runtime.output(outcome);
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) {
@@ -360,11 +374,17 @@ function jsonParser(raw) {
360
374
  function collect(value, previous) {
361
375
  return [...(previous ?? []), value];
362
376
  }
363
- /** commander stores `--exclude-archived` as `excludeArchived`. */
377
+ /**
378
+ * commander stores `--exclude-archived` as `excludeArchived`.
379
+ * @internal
380
+ */
364
381
  export function optionKey(flag) {
365
382
  return flag.replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
366
383
  }
367
- /** `projectId` reads as `Project id`; `env-vars` as `Env vars`. */
384
+ /**
385
+ * `projectId` reads as `Project id`; `env-vars` as `Env vars`.
386
+ * @internal
387
+ */
368
388
  export function humanize(name) {
369
389
  const words = name
370
390
  .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
@@ -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
- export function appendAttachments(form, paths, { maxFiles, maxBytes }) {
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
- // Size from the inode before the read: a file past the cap must fail with
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
- if (statSync(path).size > maxBytes) {
57
- throw new Error(`${path} is larger than the ${formatBytes(maxBytes)} attachment limit.`);
58
- }
58
+ const size = statSync(path).size;
59
+ checkSize(path, size, maxBytes);
60
+ total += size;
59
61
  }
60
- const files = paths.map((path) => new File([readFileSync(path)], basename(path), { type: mediaTypeFor(path) }));
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
+ }
@@ -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}` } : {}),
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { appendAttachments } from './attachments.js';
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
@@ -961,7 +961,7 @@
961
961
  "/api/devices/{deviceId}/requests": {
962
962
  "post": {
963
963
  "operationId": "devicesRequestsCreate",
964
- "summary": "From a Chat Session: ask to use a personal Mac from devices list, with the reason its owner reads on a card in this session's chat. A workspace device needs no request: it answers approved at once with grantId null, and no one is asked. The owner approves a personal Mac once per session: after that, asking again answers approved at once. Answers status pending or approved. Check devices list again for the answer; it expires after 30 minutes without an answer. While it is pending, go on with other work; when it is declined or expires, go on without it and say so. Once approved it stays approved for the rest of the session: pass --device-id on every mac command. One session at a time uses the device: on MAC_BUSY, wait with devices locks create, and run devices locks delete once done.",
964
+ "summary": "From a Chat Session: ask to use a personal Mac from devices list, with the reason its owner reads on a card in this session's chat. A workspace device needs no request: it answers approved at once with grantId null, and no one is asked. The owner approves a personal Mac for the Turn that asked: asking again in that Turn answers approved at once, and a later Turn asks again, with a new card for the owner. Answers status pending or approved. Check devices list again for the answer; it expires after 30 minutes without an answer, or when this Turn ends. While it is pending, go on with other work; when it is declined or expires, go on without it and say so. Once approved it lasts until this Turn ends: pass --device-id on every mac command; after that, mac commands on it are refused. One session at a time uses the device: on MAC_BUSY, wait with devices locks create, and run devices locks delete once done.",
965
965
  "tags": ["devices"],
966
966
  "x-kosuke-command": ["devices", "requests", "create"],
967
967
  "x-kosuke-sandbox-token": true,
@@ -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,
@@ -23314,7 +23390,7 @@
23314
23390
  "/api/sensei/device-requests": {
23315
23391
  "get": {
23316
23392
  "operationId": "senseiDeviceRequestsList",
23317
- "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.",
23393
+ "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, and so does a request or approval once the Turn that asked for it ended.",
23318
23394
  "tags": ["sensei"],
23319
23395
  "x-kosuke-command": ["sensei", "device-requests", "list"],
23320
23396
  "x-kosuke-sandbox-token": true,
@@ -23397,7 +23473,7 @@
23397
23473
  "decision": {
23398
23474
  "type": "string",
23399
23475
  "enum": ["approve", "decline"],
23400
- "description": "approve lets the session use the device until it ends; decline refuses it."
23476
+ "description": "approve lets the session use the device until the Turn that asked ends; decline refuses it."
23401
23477
  }
23402
23478
  },
23403
23479
  "required": ["sessionId", "requestId", "decision"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kosuke-ai/kosuke",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
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": "2fece35c238337316974c0ce32615f1fbe682fd9",
44
- "sourceIdentity": "c8c7c09262763d928f5e07b05b602a9a543c07b30c74e04325e18080ab6e3716",
43
+ "sourceCommit": "11e0393d7adbd35a8589bef145e005e29aa8d1a7",
44
+ "sourceIdentity": "39feb5bff790b571ed2317abfb3663d6bfac98ad68ff123b03e57b51389bddbc",
45
45
  "fragments": [
46
- "twenty-files-per-message-r7k2qd.json"
46
+ "cli-staged-attachments.json"
47
47
  ]
48
48
  }
49
49
  }